Skip to content

Commit d14ef9f

Browse files
Support compound id for upserts
1 parent e26f0dd commit d14ef9f

File tree

2 files changed

+117
-17
lines changed

2 files changed

+117
-17
lines changed

packages/server/src/api/rest/index.ts

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -776,12 +776,25 @@ class RequestHandler extends APIHandlerBase {
776776
}
777777

778778
let entity: any;
779-
if (operation === 'upsert') {
780-
entity = await this.runUsert(typeInfo, type, prisma, attributes, relationships, matchFields);
781-
} else if (operation === 'create') {
782-
entity = await this.runCreate(typeInfo, type, prisma, attributes, relationships);
783-
} else {
784-
return this.makeError('invalidPayload');
779+
780+
try {
781+
if (operation === 'upsert') {
782+
entity = await this.runUpsert(
783+
typeInfo,
784+
type,
785+
prisma,
786+
modelMeta,
787+
attributes,
788+
relationships,
789+
matchFields
790+
);
791+
} else if (operation === 'create') {
792+
entity = await this.runCreate(typeInfo, type, prisma, attributes, relationships);
793+
} else {
794+
return this.makeError('invalidPayload');
795+
}
796+
} catch (e) {
797+
return e as any;
785798
}
786799

787800
return {
@@ -790,19 +803,25 @@ class RequestHandler extends APIHandlerBase {
790803
};
791804
}
792805

793-
private async runUsert(
806+
private async runUpsert(
794807
typeInfo: ModelInfo,
795808
type: string,
796809
prisma: DbClientContract,
810+
modelMeta: ModelMeta,
797811
attributes: any,
798812
relationships: any,
799813
matchFields: any[]
800814
) {
815+
const uniqueFields = Object.values(modelMeta.models[type].uniqueConstraints || {}).map((uf) => uf.fields);
816+
817+
if (
818+
!uniqueFields.some((uniqueCombination) => uniqueCombination.every((field) => matchFields.includes(field)))
819+
) {
820+
throw this.makeError('invalidPayload', 'Match fields must be unique fields', 400);
821+
}
822+
801823
const upsertPayload: any = {};
802-
upsertPayload.where = matchFields.reduce((acc: any, field: string) => {
803-
acc[field] = attributes[field] ?? null;
804-
return acc;
805-
}, {});
824+
upsertPayload.where = this.makeUpsertWhere(matchFields, attributes, typeInfo);
806825

807826
upsertPayload.create = { ...attributes };
808827
upsertPayload.update = {
@@ -812,12 +831,12 @@ class RequestHandler extends APIHandlerBase {
812831
if (relationships) {
813832
for (const [key, data] of Object.entries<any>(relationships)) {
814833
if (!data?.data) {
815-
return this.makeError('invalidRelationData');
834+
throw this.makeError('invalidRelationData');
816835
}
817836

818837
const relationInfo = typeInfo.relationships[key];
819838
if (!relationInfo) {
820-
return this.makeUnsupportedRelationshipError(type, key, 400);
839+
throw this.makeUnsupportedRelationshipError(type, key, 400);
821840
}
822841

823842
if (relationInfo.isCollection) {
@@ -833,7 +852,7 @@ class RequestHandler extends APIHandlerBase {
833852
};
834853
} else {
835854
if (typeof data.data !== 'object') {
836-
return this.makeError('invalidRelationData');
855+
throw this.makeError('invalidRelationData');
837856
}
838857
upsertPayload.create[key] = {
839858
connect: this.makeIdConnect(relationInfo.idFields, data.data.id),
@@ -864,12 +883,12 @@ class RequestHandler extends APIHandlerBase {
864883
if (relationships) {
865884
for (const [key, data] of Object.entries<any>(relationships)) {
866885
if (!data?.data) {
867-
return this.makeError('invalidRelationData');
886+
throw this.makeError('invalidRelationData');
868887
}
869888

870889
const relationInfo = typeInfo.relationships[key];
871890
if (!relationInfo) {
872-
return this.makeUnsupportedRelationshipError(type, key, 400);
891+
throw this.makeUnsupportedRelationshipError(type, key, 400);
873892
}
874893

875894
if (relationInfo.isCollection) {
@@ -880,7 +899,7 @@ class RequestHandler extends APIHandlerBase {
880899
};
881900
} else {
882901
if (typeof data.data !== 'object') {
883-
return this.makeError('invalidRelationData');
902+
throw this.makeError('invalidRelationData');
884903
}
885904
createPayload.data[key] = {
886905
connect: this.makeIdConnect(relationInfo.idFields, data.data.id),
@@ -1388,6 +1407,24 @@ class RequestHandler extends APIHandlerBase {
13881407
return idFields.map((idf) => item[idf.name]).join(this.idDivider);
13891408
}
13901409

1410+
private makeUpsertWhere(matchFields: any[], attributes: any, typeInfo: ModelInfo) {
1411+
const where = matchFields.reduce((acc: any, field: string) => {
1412+
acc[field] = attributes[field] ?? null;
1413+
return acc;
1414+
}, {});
1415+
1416+
if (
1417+
typeInfo.idFields.length > 1 &&
1418+
matchFields.some((mf) => typeInfo.idFields.map((idf) => idf.name).includes(mf))
1419+
) {
1420+
return {
1421+
[this.makePrismaIdKey(typeInfo.idFields)]: where,
1422+
};
1423+
}
1424+
1425+
return where;
1426+
}
1427+
13911428
private includeRelationshipIds(model: string, args: any, mode: 'select' | 'include') {
13921429
const typeInfo = this.typeMap[model];
13931430
if (!typeInfo) {

packages/server/tests/api/rest.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1872,6 +1872,69 @@ describe('REST server tests', () => {
18721872
},
18731873
});
18741874
});
1875+
1876+
it('upsert fails if matchFields are not unique', async () => {
1877+
await prisma.user.create({
1878+
data: { myId: 'user1', email: 'user1@abc.com' },
1879+
});
1880+
1881+
const r = await handler({
1882+
method: 'post',
1883+
path: '/profile',
1884+
query: {},
1885+
requestBody: {
1886+
data: {
1887+
type: 'profile',
1888+
attributes: { gender: 'male' },
1889+
relationships: {
1890+
user: {
1891+
data: { type: 'user', id: 'user1' },
1892+
},
1893+
},
1894+
},
1895+
meta: {
1896+
operation: 'upsert',
1897+
matchFields: ['gender'],
1898+
},
1899+
},
1900+
prisma,
1901+
});
1902+
1903+
expect(r.status).toBe(400);
1904+
expect(r.body).toMatchObject({
1905+
errors: [
1906+
{
1907+
status: 400,
1908+
code: 'invalid-payload',
1909+
},
1910+
],
1911+
});
1912+
});
1913+
1914+
it('upsert works with compound id', async () => {
1915+
await prisma.user.create({ data: { myId: 'user1', email: 'user1@abc.com' } });
1916+
await prisma.post.create({ data: { id: 1, title: 'Post1' } });
1917+
1918+
const r = await handler({
1919+
method: 'post',
1920+
path: '/postLike',
1921+
query: {},
1922+
requestBody: {
1923+
data: {
1924+
type: 'postLike',
1925+
id: `1${idDivider}user1`,
1926+
attributes: { userId: 'user1', postId: 1, superLike: false },
1927+
},
1928+
meta: {
1929+
operation: 'upsert',
1930+
matchFields: ['userId', 'postId'],
1931+
},
1932+
},
1933+
prisma,
1934+
});
1935+
1936+
expect(r.status).toBe(201);
1937+
});
18751938
});
18761939

18771940
describe('PUT', () => {

0 commit comments

Comments
 (0)