Skip to content

Commit f82e239

Browse files
Functioning upsert support
1 parent 223b86f commit f82e239

File tree

2 files changed

+152
-41
lines changed

2 files changed

+152
-41
lines changed

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

Lines changed: 113 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -716,11 +716,11 @@ class RequestHandler extends APIHandlerBase {
716716
body = SuperJSON.deserialize({ json: body, meta: body.meta.serialization });
717717
}
718718

719-
let method: 'create' | 'update' | 'upsert' = mode;
719+
let operation: 'create' | 'update' | 'upsert' = mode;
720720
let matchFields = [];
721721

722722
if (body.meta?.operation === 'upsert' && body.meta?.matchFields.length) {
723-
method = 'upsert';
723+
operation = 'upsert';
724724
matchFields = body.meta.matchFields;
725725
}
726726

@@ -748,7 +748,7 @@ class RequestHandler extends APIHandlerBase {
748748
}
749749
}
750750

751-
return { attributes, relationships: parsed.data.relationships, method, matchFields };
751+
return { attributes, relationships: parsed.data.relationships, operation, matchFields };
752752
}
753753

754754
private async processCreate(
@@ -764,7 +764,7 @@ class RequestHandler extends APIHandlerBase {
764764
return this.makeUnsupportedModelError(type);
765765
}
766766

767-
const { error, attributes, relationships, method, matchFields } = this.processRequestBody(
767+
const { error, attributes, relationships, operation, matchFields } = this.processRequestBody(
768768
type,
769769
requestBody,
770770
zodSchemas,
@@ -776,55 +776,127 @@ class RequestHandler extends APIHandlerBase {
776776
}
777777

778778
let entity: any;
779-
if (method === 'upsert') {
780-
entity = await prisma[type].upsert(createPayload);
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);
781783
} else {
782-
const createPayload: any = { data: { ...attributes } };
784+
return this.makeError('invalidPayload');
785+
}
786+
787+
return {
788+
status: 201,
789+
body: await this.serializeItems(type, entity),
790+
};
791+
}
792+
793+
private async runUsert(
794+
typeInfo: ModelInfo,
795+
type: string,
796+
prisma: DbClientContract,
797+
attributes: any,
798+
relationships: any,
799+
matchFields: any[]
800+
) {
801+
const upsertPayload: any = {};
802+
upsertPayload.where = matchFields.reduce((acc: any, field: string) => {
803+
acc[field] = attributes[field] ?? null;
804+
return acc;
805+
}, {});
806+
807+
upsertPayload.create = { ...attributes };
808+
upsertPayload.update = { ...attributes };
809+
810+
if (relationships) {
811+
for (const [key, data] of Object.entries<any>(relationships)) {
812+
if (!data?.data) {
813+
return this.makeError('invalidRelationData');
814+
}
815+
816+
const relationInfo = typeInfo.relationships[key];
817+
if (!relationInfo) {
818+
return this.makeUnsupportedRelationshipError(type, key, 400);
819+
}
783820

784-
// turn relationship payload into Prisma connect objects
785-
if (relationships) {
786-
for (const [key, data] of Object.entries<any>(relationships)) {
787-
if (!data?.data) {
821+
if (relationInfo.isCollection) {
822+
upsertPayload.create[key] = {
823+
connect: enumerate(data.data).map((item: any) =>
824+
this.makeIdConnect(relationInfo.idFields, item.id)
825+
),
826+
};
827+
upsertPayload.update[key] = {
828+
connect: enumerate(data.data).map((item: any) =>
829+
this.makeIdConnect(relationInfo.idFields, item.id)
830+
),
831+
};
832+
} else {
833+
if (typeof data.data !== 'object') {
788834
return this.makeError('invalidRelationData');
789835
}
836+
upsertPayload.create[key] = {
837+
connect: this.makeIdConnect(relationInfo.idFields, data.data.id),
838+
};
839+
upsertPayload.update[key] = {
840+
connect: this.makeIdConnect(relationInfo.idFields, data.data.id),
841+
};
842+
}
843+
}
844+
}
790845

791-
const relationInfo = typeInfo.relationships[key];
792-
if (!relationInfo) {
793-
return this.makeUnsupportedRelationshipError(type, key, 400);
794-
}
846+
// include IDs of relation fields so that they can be serialized.
847+
this.includeRelationshipIds(type, upsertPayload, 'include');
795848

796-
if (relationInfo.isCollection) {
797-
createPayload.data[key] = {
798-
connect: enumerate(data.data).map((item: any) =>
799-
this.makeIdConnect(relationInfo.idFields, item.id)
800-
),
801-
};
802-
} else {
803-
if (typeof data.data !== 'object') {
804-
return this.makeError('invalidRelationData');
805-
}
806-
createPayload.data[key] = {
807-
connect: this.makeIdConnect(relationInfo.idFields, data.data.id),
808-
};
809-
}
849+
return prisma[type].upsert(upsertPayload);
850+
}
851+
852+
private async runCreate(
853+
typeInfo: ModelInfo,
854+
type: string,
855+
prisma: DbClientContract,
856+
attributes: any,
857+
relationships: any
858+
) {
859+
const createPayload: any = { data: { ...attributes } };
860+
861+
// turn relationship payload into Prisma connect objects
862+
if (relationships) {
863+
for (const [key, data] of Object.entries<any>(relationships)) {
864+
if (!data?.data) {
865+
return this.makeError('invalidRelationData');
866+
}
867+
868+
const relationInfo = typeInfo.relationships[key];
869+
if (!relationInfo) {
870+
return this.makeUnsupportedRelationshipError(type, key, 400);
871+
}
810872

811-
// make sure ID fields are included for result serialization
812-
createPayload.include = {
813-
...createPayload.include,
814-
[key]: { select: { [this.makePrismaIdKey(relationInfo.idFields)]: true } },
873+
if (relationInfo.isCollection) {
874+
createPayload.data[key] = {
875+
connect: enumerate(data.data).map((item: any) =>
876+
this.makeIdConnect(relationInfo.idFields, item.id)
877+
),
878+
};
879+
} else {
880+
if (typeof data.data !== 'object') {
881+
return this.makeError('invalidRelationData');
882+
}
883+
createPayload.data[key] = {
884+
connect: this.makeIdConnect(relationInfo.idFields, data.data.id),
815885
};
816886
}
887+
888+
// make sure ID fields are included for result serialization
889+
createPayload.include = {
890+
...createPayload.include,
891+
[key]: { select: { [this.makePrismaIdKey(relationInfo.idFields)]: true } },
892+
};
817893
}
894+
}
818895

819-
// include IDs of relation fields so that they can be serialized.
820-
this.includeRelationshipIds(type, createPayload, 'include');
896+
// include IDs of relation fields so that they can be serialized.
897+
this.includeRelationshipIds(type, createPayload, 'include');
821898

822-
entity = await prisma[type].create(createPayload);
823-
}
824-
return {
825-
status: 201,
826-
body: await this.serializeItems(type, entity),
827-
};
899+
return prisma[type].create(createPayload);
828900
}
829901

830902
private async processRelationshipCRUD(

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { createPostgresDb, dropPostgresDb, loadSchema, run } from '@zenstackhq/t
66
import { Decimal } from 'decimal.js';
77
import SuperJSON from 'superjson';
88
import makeHandler from '../../src/api/rest';
9+
import { query } from 'express';
910

1011
const idDivider = '_';
1112

@@ -1800,6 +1801,44 @@ describe('REST server tests', () => {
18001801

18011802
expect(r.status).toBe(201);
18021803
});
1804+
1805+
it('upsert a new entity', async () => {
1806+
const r = await handler({
1807+
method: 'post',
1808+
path: '/user',
1809+
query: {},
1810+
requestBody: {
1811+
data: {
1812+
type: 'user',
1813+
attributes: { myId: 'user1', email: 'user1@abc.com' },
1814+
},
1815+
meta: {
1816+
operation: 'upsert',
1817+
matchFields: ['myId'],
1818+
},
1819+
},
1820+
prisma,
1821+
});
1822+
1823+
expect(r.status).toBe(201);
1824+
expect(r.body).toMatchObject({
1825+
jsonapi: { version: '1.1' },
1826+
data: {
1827+
type: 'user',
1828+
id: 'user1',
1829+
attributes: { email: 'user1@abc.com' },
1830+
relationships: {
1831+
posts: {
1832+
links: {
1833+
self: 'http://localhost/api/user/user1/relationships/posts',
1834+
related: 'http://localhost/api/user/user1/posts',
1835+
},
1836+
data: [],
1837+
},
1838+
},
1839+
},
1840+
});
1841+
});
18031842
});
18041843

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

0 commit comments

Comments
 (0)