From 2ec375de063c5237dae262e23650db3c083039d4 Mon Sep 17 00:00:00 2001 From: Thomas Sunde Nielsen Date: Fri, 27 Sep 2024 13:40:30 +0200 Subject: [PATCH 01/15] Add failing test for compound id query support --- packages/server/tests/api/rest.test.ts | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/packages/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index a587678c3..8d29238cf 100644 --- a/packages/server/tests/api/rest.test.ts +++ b/packages/server/tests/api/rest.test.ts @@ -63,6 +63,13 @@ describe('REST server tests', () => { post Post @relation(fields: [postId], references: [id]) postId Int @unique } + + model PostLike { + postId Int + userId String + superLike Boolean + @@id([postId, userId]) + } `; beforeAll(async () => { @@ -1283,6 +1290,33 @@ describe('REST server tests', () => { next: null, }); }); + + it('compound id', async () => { + await prisma.user.create({ + data: { myId: 'user1', email: 'user1@abc.com', posts: { create: { title: 'Post1' } } }, + }); + await prisma.user.create({ + data: { myId: 'user2', email: 'user2@abc.com' }, + }); + await prisma.postLike.create({ + data: { userId: 'user2', postId: 1, superLike: false }, + }); + + const r = await handler({ + method: 'get', + path: '/postLike/user2,1', + prisma, + }); + + expect(r.status).toBe(200); + expect(r.body).toMatchObject({ + data: { + type: 'postLike', + id: 'user2,1', + attributes: { userId: 'user2', postId: 1, superLike: false }, + }, + }); + }); }); describe('POST', () => { From e52bd626af83639e70d8602704187fc9e4573354 Mon Sep 17 00:00:00 2001 From: Thomas Sunde Nielsen Date: Mon, 30 Sep 2024 15:55:28 +0200 Subject: [PATCH 02/15] Support compound id GET --- packages/server/src/api/rest/index.ts | 108 +++++++++++++++---------- packages/server/tests/api/rest.test.ts | 8 +- 2 files changed, 69 insertions(+), 47 deletions(-) diff --git a/packages/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts index 75b2f738b..13cf6f4fc 100644 --- a/packages/server/src/api/rest/index.ts +++ b/packages/server/src/api/rest/index.ts @@ -32,6 +32,9 @@ const urlPatterns = { relationship: new UrlPattern('/:type/:id/relationships/:relationship'), }; +export const idDivider = '_'; +export const compoundIdKey = 'compoundId'; + /** * Request handler options */ @@ -59,9 +62,13 @@ type RelationshipInfo = { isOptional: boolean; }; +type IdField = { + name: string; + type: string; +}; + type ModelInfo = { - idField: string; - idFieldType: string; + idFields: IdField[]; fields: Record; relationships: Record; }; @@ -129,10 +136,6 @@ class RequestHandler extends APIHandlerBase { status: 400, title: 'Model without an ID field is not supported', }, - multiId: { - status: 400, - title: 'Model with multiple ID fields is not supported', - }, invalidId: { status: 400, title: 'Resource ID is invalid', @@ -387,7 +390,7 @@ class RequestHandler extends APIHandlerBase { return this.makeUnsupportedModelError(type); } - const args: any = { where: this.makeIdFilter(typeInfo.idField, typeInfo.idFieldType, resourceId) }; + const args: any = { where: this.makeIdFilter(typeInfo.idFields, resourceId) }; // include IDs of relation fields so that they can be serialized this.includeRelationshipIds(type, args, 'include'); @@ -405,7 +408,12 @@ class RequestHandler extends APIHandlerBase { include = allIncludes; } - const entity = await prisma[type].findUnique(args); + let entity = await prisma[type].findUnique(args); + + if (typeInfo.idFields.length > 1) { + entity = { ...entity, [compoundIdKey]: resourceId }; + } + if (entity) { return { status: 200, @@ -451,7 +459,7 @@ class RequestHandler extends APIHandlerBase { select = select ?? { [relationship]: true }; const args: any = { - where: this.makeIdFilter(typeInfo.idField, typeInfo.idFieldType, resourceId), + where: this.makeIdFilter(typeInfo.idFields, resourceId), select, }; @@ -510,7 +518,7 @@ class RequestHandler extends APIHandlerBase { } const args: any = { - where: this.makeIdFilter(typeInfo.idField, typeInfo.idFieldType, resourceId), + where: this.makeIdFilter(typeInfo.idFields, resourceId), select: this.makeIdSelect(type, modelMeta), }; @@ -801,8 +809,11 @@ class RequestHandler extends APIHandlerBase { } const updateArgs: any = { - where: this.makeIdFilter(typeInfo.idField, typeInfo.idFieldType, resourceId), - select: { [typeInfo.idField]: true, [relationship]: { select: { [relationInfo.idField]: true } } }, + where: this.makeIdFilter(typeInfo.idFields, resourceId), + select: { + ...typeInfo.idFields.reduce((acc, field) => ({ ...acc, [field.name]: true }), {}), + [relationship]: { select: { [relationInfo.idField]: true } }, + }, }; if (!relationInfo.isCollection) { @@ -897,7 +908,7 @@ class RequestHandler extends APIHandlerBase { } const updatePayload: any = { - where: this.makeIdFilter(typeInfo.idField, typeInfo.idFieldType, resourceId), + where: this.makeIdFilter(typeInfo.idFields, resourceId), data: { ...attributes }, }; @@ -948,7 +959,7 @@ class RequestHandler extends APIHandlerBase { } await prisma[type].delete({ - where: this.makeIdFilter(typeInfo.idField, typeInfo.idFieldType, resourceId), + where: this.makeIdFilter(typeInfo.idFields, resourceId), }); return { status: 204, @@ -966,14 +977,9 @@ class RequestHandler extends APIHandlerBase { logWarning(logger, `Not including model ${model} in the API because it has no ID field`); continue; } - if (idFields.length > 1) { - logWarning(logger, `Not including model ${model} in the API because it has multiple ID fields`); - continue; - } this.typeMap[model] = { - idField: idFields[0].name, - idFieldType: idFields[0].type, + idFields, relationships: {}, fields, }; @@ -990,18 +996,15 @@ class RequestHandler extends APIHandlerBase { ); continue; } - if (fieldTypeIdFields.length > 1) { - logWarning( - logger, - `Not including relation ${model}.${field} in the API because it has multiple ID fields` - ); - continue; - } + + // TODO: Multi id relationship support + const idField = fieldTypeIdFields.length > 1 ? 'id' : fieldTypeIdFields[0].name; + const idFieldType = fieldTypeIdFields.length > 1 ? 'string' : fieldTypeIdFields[0].type; this.typeMap[model].relationships[field] = { type: fieldInfo.type, - idField: fieldTypeIdFields[0].name, - idFieldType: fieldTypeIdFields[0].type, + idField, + idFieldType, isCollection: !!fieldInfo.isArray, isOptional: !!fieldInfo.isOptional, }; @@ -1019,7 +1022,8 @@ class RequestHandler extends APIHandlerBase { for (const model of Object.keys(modelMeta.models)) { const ids = getIdFields(modelMeta, model); - if (ids.length !== 1) { + + if (ids.length < 1) { continue; } @@ -1042,7 +1046,7 @@ class RequestHandler extends APIHandlerBase { const serializer = new Serializer(model, { version: '1.1', - idKey: ids[0].name, + idKey: ids.length > 1 ? compoundIdKey : ids[0].name, linkers: { resource: linker, document: linker, @@ -1069,7 +1073,7 @@ class RequestHandler extends APIHandlerBase { continue; } const fieldIds = getIdFields(modelMeta, fieldMeta.type); - if (fieldIds.length === 1) { + if (fieldIds.length > 0) { const relator = new Relator( async (data) => { return (data as any)[field]; @@ -1107,10 +1111,10 @@ class RequestHandler extends APIHandlerBase { return undefined; } const ids = getIdFields(modelMeta, model); - if (ids.length === 1) { - return data[ids[0].name]; - } else { + if (ids.length === 0) { return undefined; + } else { + return data[ids.map((id) => id.name).join(idDivider)]; } } @@ -1178,18 +1182,31 @@ class RequestHandler extends APIHandlerBase { return r.toString(); } - private makeIdFilter(idField: string, idFieldType: string, resourceId: string) { - return { [idField]: this.coerce(idFieldType, resourceId) }; + private makeIdFilter(idFields: IdField[], resourceId: string) { + if (idFields.length === 1) { + return { [idFields[0].name]: this.coerce(idFields[0].type, resourceId) }; + } else { + return { + [idFields.map((idf) => idf.name).join('_')]: idFields.reduce( + (acc, curr, idx) => ({ + ...acc, + [curr.name]: this.coerce(curr.type, resourceId.split(idDivider)[idx]), + }), + {} + ), + }; + } } private makeIdSelect(model: string, modelMeta: ModelMeta) { const idFields = getIdFields(modelMeta, model); if (idFields.length === 0) { throw this.errors.noId; - } else if (idFields.length > 1) { - throw this.errors.multiId; + } else if (idFields.length === 1) { + return { [idFields[0].name]: true }; + } else { + return { [idFields.map((idf) => idf.name).join(',')]: true }; } - return { [idFields[0].name]: true }; } private includeRelationshipIds(model: string, args: any, mode: 'select' | 'include') { @@ -1425,7 +1442,10 @@ class RequestHandler extends APIHandlerBase { if (!relationType) { return { sort: undefined, error: this.makeUnsupportedModelError(fieldInfo.type) }; } - curr[fieldInfo.name] = { [relationType.idField]: dir }; + curr[fieldInfo.name] = relationType.idFields.reduce((acc: any, idField: IdField) => { + acc[idField.name] = dir; + return acc; + }, {}); } else { // regular field curr[fieldInfo.name] = dir; @@ -1509,11 +1529,11 @@ class RequestHandler extends APIHandlerBase { const values = value.split(',').filter((i) => i); const filterValue = values.length > 1 - ? { OR: values.map((v) => this.makeIdFilter(info.idField, info.idFieldType, v)) } - : this.makeIdFilter(info.idField, info.idFieldType, value); + ? { OR: values.map((v) => this.makeIdFilter(info.idFields, v)) } + : this.makeIdFilter(info.idFields, value); return { some: filterValue }; } else { - return { is: this.makeIdFilter(info.idField, info.idFieldType, value) }; + return { is: this.makeIdFilter(info.idFields, value) }; } } else { const coerced = this.coerce(fieldInfo.type, value); diff --git a/packages/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index 8d29238cf..989439aff 100644 --- a/packages/server/tests/api/rest.test.ts +++ b/packages/server/tests/api/rest.test.ts @@ -5,7 +5,7 @@ import { CrudFailureReason, type ModelMeta } from '@zenstackhq/runtime'; import { loadSchema, run } from '@zenstackhq/testtools'; import { Decimal } from 'decimal.js'; import SuperJSON from 'superjson'; -import makeHandler from '../../src/api/rest'; +import makeHandler, { idDivider } from '../../src/api/rest'; describe('REST server tests', () => { let prisma: any; @@ -1304,15 +1304,17 @@ describe('REST server tests', () => { const r = await handler({ method: 'get', - path: '/postLike/user2,1', + path: `/postLike/1${idDivider}user2`, // Order of ids is same as in the model @@id prisma, }); + console.log(r.body); + expect(r.status).toBe(200); expect(r.body).toMatchObject({ data: { type: 'postLike', - id: 'user2,1', + id: `1${idDivider}user2`, attributes: { userId: 'user2', postId: 1, superLike: false }, }, }); From 1bc1991780d16841938805b843223be05378c473 Mon Sep 17 00:00:00 2001 From: Thomas Sunde Nielsen Date: Tue, 1 Oct 2024 08:55:21 +0200 Subject: [PATCH 03/15] IDField -> FieldInfo --- packages/server/src/api/rest/index.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts index 13cf6f4fc..341585c85 100644 --- a/packages/server/src/api/rest/index.ts +++ b/packages/server/src/api/rest/index.ts @@ -62,13 +62,8 @@ type RelationshipInfo = { isOptional: boolean; }; -type IdField = { - name: string; - type: string; -}; - type ModelInfo = { - idFields: IdField[]; + idFields: FieldInfo[]; fields: Record; relationships: Record; }; @@ -1182,7 +1177,7 @@ class RequestHandler extends APIHandlerBase { return r.toString(); } - private makeIdFilter(idFields: IdField[], resourceId: string) { + private makeIdFilter(idFields: FieldInfo[], resourceId: string) { if (idFields.length === 1) { return { [idFields[0].name]: this.coerce(idFields[0].type, resourceId) }; } else { @@ -1442,7 +1437,7 @@ class RequestHandler extends APIHandlerBase { if (!relationType) { return { sort: undefined, error: this.makeUnsupportedModelError(fieldInfo.type) }; } - curr[fieldInfo.name] = relationType.idFields.reduce((acc: any, idField: IdField) => { + curr[fieldInfo.name] = relationType.idFields.reduce((acc: any, idField: FieldInfo) => { acc[idField.name] = dir; return acc; }, {}); From aeeea92edbf30b1fbec3b26b55dcb68a542fa8f5 Mon Sep 17 00:00:00 2001 From: Thomas Sunde Nielsen Date: Tue, 1 Oct 2024 08:59:50 +0200 Subject: [PATCH 04/15] Dynamic idKey --- packages/server/src/api/rest/index.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts index 341585c85..cb026de98 100644 --- a/packages/server/src/api/rest/index.ts +++ b/packages/server/src/api/rest/index.ts @@ -33,7 +33,6 @@ const urlPatterns = { }; export const idDivider = '_'; -export const compoundIdKey = 'compoundId'; /** * Request handler options @@ -406,7 +405,7 @@ class RequestHandler extends APIHandlerBase { let entity = await prisma[type].findUnique(args); if (typeInfo.idFields.length > 1) { - entity = { ...entity, [compoundIdKey]: resourceId }; + entity = { ...entity, [this.makeIdKey(typeInfo.idFields)]: resourceId }; } if (entity) { @@ -1041,7 +1040,7 @@ class RequestHandler extends APIHandlerBase { const serializer = new Serializer(model, { version: '1.1', - idKey: ids.length > 1 ? compoundIdKey : ids[0].name, + idKey: this.makeIdKey(ids), linkers: { resource: linker, document: linker, @@ -1204,6 +1203,10 @@ class RequestHandler extends APIHandlerBase { } } + private makeIdKey(idFields: FieldInfo[]) { + return idFields.map((idf) => idf.name).join(idDivider); + } + private includeRelationshipIds(model: string, args: any, mode: 'select' | 'include') { const typeInfo = this.typeMap[model]; if (!typeInfo) { From ee5cd5848ef9a71be2ab81b33f4e8e2980232726 Mon Sep 17 00:00:00 2001 From: Thomas Sunde Nielsen Date: Tue, 1 Oct 2024 10:09:51 +0200 Subject: [PATCH 05/15] Support compound ids in GET list --- packages/server/src/api/rest/index.ts | 86 +++++++++++++++++++------- packages/server/tests/api/rest.test.ts | 63 ++++++++++++------- 2 files changed, 107 insertions(+), 42 deletions(-) diff --git a/packages/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts index cb026de98..43231221e 100644 --- a/packages/server/src/api/rest/index.ts +++ b/packages/server/src/api/rest/index.ts @@ -248,13 +248,20 @@ class RequestHandler extends APIHandlerBase { let match = urlPatterns.single.match(path); if (match) { // single resource read - return await this.processSingleRead(prisma, match.type, match.id, query); + return await this.processSingleRead(prisma, match.type, match.id, query, modelMeta); } match = urlPatterns.fetchRelationship.match(path); if (match) { // fetch related resource(s) - return await this.processFetchRelated(prisma, match.type, match.id, match.relationship, query); + return await this.processFetchRelated( + prisma, + match.type, + match.id, + match.relationship, + query, + modelMeta + ); } match = urlPatterns.relationship.match(path); @@ -273,7 +280,7 @@ class RequestHandler extends APIHandlerBase { match = urlPatterns.collection.match(path); if (match) { // collection read - return await this.processCollectionRead(prisma, match.type, query); + return await this.processCollectionRead(prisma, match.type, query, modelMeta); } return this.makeError('invalidPath'); @@ -287,7 +294,7 @@ class RequestHandler extends APIHandlerBase { let match = urlPatterns.collection.match(path); if (match) { // resource creation - return await this.processCreate(prisma, match.type, query, requestBody, zodSchemas); + return await this.processCreate(prisma, match.type, query, requestBody, modelMeta, zodSchemas); } match = urlPatterns.relationship.match(path); @@ -300,7 +307,8 @@ class RequestHandler extends APIHandlerBase { match.id, match.relationship, query, - requestBody + requestBody, + modelMeta ); } @@ -317,7 +325,15 @@ class RequestHandler extends APIHandlerBase { let match = urlPatterns.single.match(path); if (match) { // resource update - return await this.processUpdate(prisma, match.type, match.id, query, requestBody, zodSchemas); + return await this.processUpdate( + prisma, + match.type, + match.id, + query, + requestBody, + modelMeta, + zodSchemas + ); } match = urlPatterns.relationship.match(path); @@ -330,7 +346,8 @@ class RequestHandler extends APIHandlerBase { match.id, match.relationship as string, query, - requestBody + requestBody, + modelMeta ); } @@ -354,7 +371,8 @@ class RequestHandler extends APIHandlerBase { match.id, match.relationship as string, query, - requestBody + requestBody, + modelMeta ); } @@ -377,7 +395,8 @@ class RequestHandler extends APIHandlerBase { prisma: DbClientContract, type: string, resourceId: string, - query: Record | undefined + query: Record | undefined, + modelMeta: ModelMeta ): Promise { const typeInfo = this.typeMap[type]; if (!typeInfo) { @@ -411,7 +430,7 @@ class RequestHandler extends APIHandlerBase { if (entity) { return { status: 200, - body: await this.serializeItems(type, entity, { include }), + body: await this.serializeItems(type, entity, modelMeta, { include }), }; } else { return this.makeError('notFound'); @@ -423,7 +442,8 @@ class RequestHandler extends APIHandlerBase { type: string, resourceId: string, relationship: string, - query: Record | undefined + query: Record | undefined, + modelMeta: ModelMeta ): Promise { const typeInfo = this.typeMap[type]; if (!typeInfo) { @@ -480,7 +500,7 @@ class RequestHandler extends APIHandlerBase { if (entity?.[relationship]) { return { status: 200, - body: await this.serializeItems(relationInfo.type, entity[relationship], { + body: await this.serializeItems(relationInfo.type, entity[relationship], modelMeta, { linkers: { document: new Linker(() => this.makeLinkUrl(`/${type}/${resourceId}/${relationship}`)), paginator, @@ -541,7 +561,7 @@ class RequestHandler extends APIHandlerBase { } if (entity?.[relationship]) { - const serialized: any = await this.serializeItems(relationInfo.type, entity[relationship], { + const serialized: any = await this.serializeItems(relationInfo.type, entity[relationship], modelMeta, { linkers: { document: new Linker(() => this.makeLinkUrl(`/${type}/${resourceId}/relationships/${relationship}`) @@ -563,7 +583,8 @@ class RequestHandler extends APIHandlerBase { private async processCollectionRead( prisma: DbClientContract, type: string, - query: Record | undefined + query: Record | undefined, + modelMeta: ModelMeta ): Promise { const typeInfo = this.typeMap[type]; if (!typeInfo) { @@ -613,7 +634,7 @@ class RequestHandler extends APIHandlerBase { if (limit === Infinity) { const entities = await prisma[type].findMany(args); - const body = await this.serializeItems(type, entities, { include }); + const body = await this.serializeItems(type, entities, modelMeta, { include }); const total = entities.length; body.meta = this.addTotalCountToMeta(body.meta, total); @@ -636,7 +657,7 @@ class RequestHandler extends APIHandlerBase { paginator: this.makePaginator(url, offset, limit, total), }, }; - const body = await this.serializeItems(type, entities, options); + const body = await this.serializeItems(type, entities, modelMeta, options); body.meta = this.addTotalCountToMeta(body.meta, total); return { @@ -722,6 +743,7 @@ class RequestHandler extends APIHandlerBase { type: string, _query: Record | undefined, requestBody: unknown, + modelMeta: ModelMeta, zodSchemas?: ZodSchemas ): Promise { const typeInfo = this.typeMap[type]; @@ -774,7 +796,7 @@ class RequestHandler extends APIHandlerBase { const entity = await prisma[type].create(createPayload); return { status: 201, - body: await this.serializeItems(type, entity), + body: await this.serializeItems(type, entity, modelMeta), }; } @@ -785,7 +807,8 @@ class RequestHandler extends APIHandlerBase { resourceId: string, relationship: string, query: Record | undefined, - requestBody: unknown + requestBody: unknown, + modelMeta: ModelMeta ): Promise { const typeInfo = this.typeMap[type]; if (!typeInfo) { @@ -870,7 +893,7 @@ class RequestHandler extends APIHandlerBase { const entity: any = await prisma[type].update(updateArgs); - const serialized: any = await this.serializeItems(relationInfo.type, entity[relationship], { + const serialized: any = await this.serializeItems(relationInfo.type, entity[relationship], modelMeta, { linkers: { document: new Linker(() => this.makeLinkUrl(`/${type}/${resourceId}/relationships/${relationship}`)), }, @@ -889,6 +912,7 @@ class RequestHandler extends APIHandlerBase { resourceId: string, _query: Record | undefined, requestBody: unknown, + modelMeta: ModelMeta, zodSchemas?: ZodSchemas ): Promise { const typeInfo = this.typeMap[type]; @@ -942,7 +966,7 @@ class RequestHandler extends APIHandlerBase { const entity = await prisma[type].update(updatePayload); return { status: 200, - body: await this.serializeItems(type, entity), + body: await this.serializeItems(type, entity, modelMeta), }; } @@ -1112,13 +1136,29 @@ class RequestHandler extends APIHandlerBase { } } - private async serializeItems(model: string, items: unknown, options?: Partial>) { + private async serializeItems( + model: string, + items: unknown, + modelMeta: ModelMeta, + options?: Partial> + ) { model = lowerCaseFirst(model); const serializer = this.serializers.get(model); if (!serializer) { throw new Error(`serializer not found for model ${model}`); } + const typeInfo = this.typeMap[model]; + + if (typeInfo.idFields.length > 1 && Array.isArray(items)) { + items = items.map((item: any) => { + return { + ...item, + [this.makeIdKey(typeInfo.idFields)]: this.makeCompoundId(typeInfo.idFields, item), + }; + }); + } + // serialize to JSON:API structure const serialized = await serializer.serialize(items, options); @@ -1207,6 +1247,10 @@ class RequestHandler extends APIHandlerBase { return idFields.map((idf) => idf.name).join(idDivider); } + private makeCompoundId(idFields: FieldInfo[], item: any) { + return idFields.map((idf) => item[idf.name]).join(idDivider); + } + private includeRelationshipIds(model: string, args: any, mode: 'select' | 'include') { const typeInfo = this.typeMap[model]; if (!typeInfo) { diff --git a/packages/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index 989439aff..c2c05d6cb 100644 --- a/packages/server/tests/api/rest.test.ts +++ b/packages/server/tests/api/rest.test.ts @@ -1291,32 +1291,53 @@ describe('REST server tests', () => { }); }); - it('compound id', async () => { - await prisma.user.create({ - data: { myId: 'user1', email: 'user1@abc.com', posts: { create: { title: 'Post1' } } }, - }); - await prisma.user.create({ - data: { myId: 'user2', email: 'user2@abc.com' }, - }); - await prisma.postLike.create({ - data: { userId: 'user2', postId: 1, superLike: false }, + describe('compound id', () => { + beforeEach(async () => { + await prisma.user.create({ + data: { myId: 'user1', email: 'user1@abc.com', posts: { create: { title: 'Post1' } } }, + }); + await prisma.user.create({ + data: { myId: 'user2', email: 'user2@abc.com' }, + }); + await prisma.postLike.create({ + data: { userId: 'user2', postId: 1, superLike: false }, + }); }); - const r = await handler({ - method: 'get', - path: `/postLike/1${idDivider}user2`, // Order of ids is same as in the model @@id - prisma, + it('get all', async () => { + const r = await handler({ + method: 'get', + path: '/postLike', + prisma, + }); + + expect(r.status).toBe(200); + expect(r.body).toMatchObject({ + data: [ + { + type: 'postLike', + id: `1${idDivider}user2`, + attributes: { userId: 'user2', postId: 1, superLike: false }, + }, + ], + }); }); - console.log(r.body); + it('get single', async () => { + const r = await handler({ + method: 'get', + path: `/postLike/1${idDivider}user2`, // Order of ids is same as in the model @@id + prisma, + }); - expect(r.status).toBe(200); - expect(r.body).toMatchObject({ - data: { - type: 'postLike', - id: `1${idDivider}user2`, - attributes: { userId: 'user2', postId: 1, superLike: false }, - }, + expect(r.status).toBe(200); + expect(r.body).toMatchObject({ + data: { + type: 'postLike', + id: `1${idDivider}user2`, + attributes: { userId: 'user2', postId: 1, superLike: false }, + }, + }); }); }); }); From 465a6138170f6f17ed3551f3cd20a571c67ff017 Mon Sep 17 00:00:00 2001 From: Thomas Sunde Nielsen Date: Tue, 1 Oct 2024 10:27:41 +0200 Subject: [PATCH 06/15] Create with compound id test --- packages/server/tests/api/rest.test.ts | 27 ++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index c2c05d6cb..a5930b565 100644 --- a/packages/server/tests/api/rest.test.ts +++ b/packages/server/tests/api/rest.test.ts @@ -1603,6 +1603,33 @@ describe('REST server tests', () => { expect(r.status).toBe(404); }); + + describe('compound id', () => { + beforeEach(async () => { + await prisma.user.create({ data: { myId: 'user1', email: 'user1@abc.com' } }); + await prisma.post.create({ + data: { id: 1, title: 'Post1' }, + }); + }); + + it('create single', async () => { + const r = await handler({ + method: 'post', + path: '/postLike', + query: {}, + requestBody: { + data: { + type: 'postLike', + id: `1${idDivider}user1`, + attributes: { userId: 'user1', postId: 1, superLike: false }, + }, + }, + prisma, + }); + + expect(r.status).toBe(201); + }); + }); }); describe('PUT', () => { From 2d735cd9e0559e6d739136fee12bdf4121909c70 Mon Sep 17 00:00:00 2001 From: Thomas Sunde Nielsen Date: Wed, 2 Oct 2024 09:24:48 +0200 Subject: [PATCH 07/15] Update with compound id test --- packages/server/tests/api/rest.test.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index a5930b565..f5db13b19 100644 --- a/packages/server/tests/api/rest.test.ts +++ b/packages/server/tests/api/rest.test.ts @@ -1768,6 +1768,27 @@ describe('REST server tests', () => { expect(r.body.errors[0].code).toBe('invalid-payload'); }); + it('update item with compound id', async () => { + await prisma.user.create({ data: { myId: 'user1', email: 'user1@abc.com' } }); + await prisma.post.create({ data: { id: 1, title: 'Post1' } }); + await prisma.postLike.create({ data: { userId: 'user1', postId: 1, superLike: false } }); + + const r = await handler({ + method: 'put', + path: `/postLike/1${idDivider}user1`, + query: {}, + requestBody: { + data: { + type: 'postLike', + attributes: { superLike: true }, + }, + }, + prisma, + }); + + expect(r.status).toBe(200); + }); + it('update a single relation', async () => { await prisma.user.create({ data: { myId: 'user1', email: 'user1@abc.com' } }); await prisma.post.create({ From 810a54cca7bae36bf20c7dbf763efbe20107ca68 Mon Sep 17 00:00:00 2001 From: Thomas Sunde Nielsen Date: Wed, 2 Oct 2024 09:28:50 +0200 Subject: [PATCH 08/15] Delete test --- packages/server/tests/api/rest.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index f5db13b19..9412927d2 100644 --- a/packages/server/tests/api/rest.test.ts +++ b/packages/server/tests/api/rest.test.ts @@ -6,6 +6,7 @@ import { loadSchema, run } from '@zenstackhq/testtools'; import { Decimal } from 'decimal.js'; import SuperJSON from 'superjson'; import makeHandler, { idDivider } from '../../src/api/rest'; +import e from 'express'; describe('REST server tests', () => { let prisma: any; @@ -1950,6 +1951,21 @@ describe('REST server tests', () => { expect(r.body).toBeUndefined(); }); + it('deletes an item with compound id', async () => { + await prisma.user.create({ + data: { myId: 'user1', email: 'user1@abc.com', posts: { create: { id: 1, title: 'Post1' } } }, + }); + await prisma.postLike.create({ data: { userId: 'user1', postId: 1, superLike: false } }); + + const r = await handler({ + method: 'delete', + path: `/postLike/1${idDivider}user1`, + prisma, + }); + expect(r.status).toBe(204); + expect(r.body).toBeUndefined(); + }); + it('returns 404 if the user does not exist', async () => { const r = await handler({ method: 'delete', From 07bb0932a7d812bfb56d8c38f8a52e1d2d5fc22c Mon Sep 17 00:00:00 2001 From: Thomas Sunde Nielsen Date: Wed, 2 Oct 2024 10:56:10 +0200 Subject: [PATCH 09/15] Support compound ids in relationships --- packages/server/src/api/rest/index.ts | 34 +++++++++++++-------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/packages/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts index 43231221e..d2cd89353 100644 --- a/packages/server/src/api/rest/index.ts +++ b/packages/server/src/api/rest/index.ts @@ -55,8 +55,7 @@ export type Options = { type RelationshipInfo = { type: string; - idField: string; - idFieldType: string; + idFields: FieldInfo[]; isCollection: boolean; isOptional: boolean; }; @@ -773,7 +772,7 @@ class RequestHandler extends APIHandlerBase { if (relationInfo.isCollection) { createPayload.data[key] = { connect: enumerate(data.data).map((item: any) => ({ - [relationInfo.idField]: this.coerce(relationInfo.idFieldType, item.id), + [this.makeIdKey(relationInfo.idFields)]: item.id, })), }; } else { @@ -781,14 +780,16 @@ class RequestHandler extends APIHandlerBase { return this.makeError('invalidRelationData'); } createPayload.data[key] = { - connect: { [relationInfo.idField]: this.coerce(relationInfo.idFieldType, data.data.id) }, + connect: { + [this.makeIdKey(relationInfo.idFields)]: data.data.id, + }, }; } // make sure ID fields are included for result serialization createPayload.include = { ...createPayload.include, - [key]: { select: { [relationInfo.idField]: true } }, + [key]: { select: { [this.makeIdKey(relationInfo.idFields)]: true } }, }; } } @@ -829,7 +830,7 @@ class RequestHandler extends APIHandlerBase { where: this.makeIdFilter(typeInfo.idFields, resourceId), select: { ...typeInfo.idFields.reduce((acc, field) => ({ ...acc, [field.name]: true }), {}), - [relationship]: { select: { [relationInfo.idField]: true } }, + [relationship]: { select: { [this.makeIdKey(relationInfo.idFields)]: true } }, }, }; @@ -861,7 +862,7 @@ class RequestHandler extends APIHandlerBase { updateArgs.data = { [relationship]: { connect: { - [relationInfo.idField]: this.coerce(relationInfo.idFieldType, parsed.data.data.id), + [this.makeIdKey(relationInfo.idFields)]: parsed.data.data.id, }, }, }; @@ -885,7 +886,7 @@ class RequestHandler extends APIHandlerBase { updateArgs.data = { [relationship]: { [relationVerb]: enumerate(parsed.data.data).map((item: any) => ({ - [relationInfo.idField]: this.coerce(relationInfo.idFieldType, item.id), + [this.makeIdKey(relationInfo.idFields)]: item.id, })), }, }; @@ -945,7 +946,7 @@ class RequestHandler extends APIHandlerBase { if (relationInfo.isCollection) { updatePayload.data[key] = { set: enumerate(data.data).map((item: any) => ({ - [relationInfo.idField]: this.coerce(relationInfo.idFieldType, item.id), + [this.makeIdKey(relationInfo.idFields)]: item.id, })), }; } else { @@ -953,12 +954,14 @@ class RequestHandler extends APIHandlerBase { return this.makeError('invalidRelationData'); } updatePayload.data[key] = { - set: { [relationInfo.idField]: this.coerce(relationInfo.idFieldType, data.data.id) }, + set: { + [this.makeIdKey(relationInfo.idFields)]: data.data.id, + }, }; } updatePayload.include = { ...updatePayload.include, - [key]: { select: { [relationInfo.idField]: true } }, + [key]: { select: { [this.makeIdKey(relationInfo.idFields)]: true } }, }; } } @@ -1015,14 +1018,9 @@ class RequestHandler extends APIHandlerBase { continue; } - // TODO: Multi id relationship support - const idField = fieldTypeIdFields.length > 1 ? 'id' : fieldTypeIdFields[0].name; - const idFieldType = fieldTypeIdFields.length > 1 ? 'string' : fieldTypeIdFields[0].type; - this.typeMap[model].relationships[field] = { type: fieldInfo.type, - idField, - idFieldType, + idFields: fieldTypeIdFields, isCollection: !!fieldInfo.isArray, isOptional: !!fieldInfo.isOptional, }; @@ -1257,7 +1255,7 @@ class RequestHandler extends APIHandlerBase { return; } for (const [relation, relationInfo] of Object.entries(typeInfo.relationships)) { - args[mode] = { ...args[mode], [relation]: { select: { [relationInfo.idField]: true } } }; + args[mode] = { ...args[mode], [relation]: { select: { [this.makeIdKey(relationInfo.idFields)]: true } } }; } } From fade4c9bec54397621a7edb953fbc6d7f9c6db89 Mon Sep 17 00:00:00 2001 From: Thomas Sunde Nielsen Date: Wed, 2 Oct 2024 16:00:01 +0200 Subject: [PATCH 10/15] Working compound id include read --- packages/server/src/api/rest/index.ts | 14 ++++------ packages/server/tests/api/rest.test.ts | 36 ++++++++++++++++++++++++++ packages/server/tests/api/test.zmodel | 0 3 files changed, 41 insertions(+), 9 deletions(-) create mode 100644 packages/server/tests/api/test.zmodel diff --git a/packages/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts index d2cd89353..dff6cee48 100644 --- a/packages/server/src/api/rest/index.ts +++ b/packages/server/src/api/rest/index.ts @@ -532,12 +532,12 @@ class RequestHandler extends APIHandlerBase { const args: any = { where: this.makeIdFilter(typeInfo.idFields, resourceId), - select: this.makeIdSelect(type, modelMeta), + select: this.makeIdSelect(typeInfo.idFields), }; // include IDs of relation fields so that they can be serialized // this.includeRelationshipIds(type, args, 'select'); - args.select = { ...args.select, [relationship]: { select: this.makeIdSelect(relationInfo.type, modelMeta) } }; + args.select = { ...args.select, [relationship]: { select: this.makeIdSelect(relationInfo.idFields) } }; let paginator: Paginator | undefined; @@ -1230,15 +1230,11 @@ class RequestHandler extends APIHandlerBase { } } - private makeIdSelect(model: string, modelMeta: ModelMeta) { - const idFields = getIdFields(modelMeta, model); + private makeIdSelect(idFields: FieldInfo[]) { if (idFields.length === 0) { throw this.errors.noId; - } else if (idFields.length === 1) { - return { [idFields[0].name]: true }; - } else { - return { [idFields.map((idf) => idf.name).join(',')]: true }; } + return idFields.reduce((acc, curr) => ({ ...acc, [curr.name]: true }), {}); } private makeIdKey(idFields: FieldInfo[]) { @@ -1255,7 +1251,7 @@ class RequestHandler extends APIHandlerBase { return; } for (const [relation, relationInfo] of Object.entries(typeInfo.relationships)) { - args[mode] = { ...args[mode], [relation]: { select: { [this.makeIdKey(relationInfo.idFields)]: true } } }; + args[mode] = { ...args[mode], [relation]: { select: this.makeIdSelect(relationInfo.idFields) } }; } } diff --git a/packages/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index 9412927d2..71ec7f453 100644 --- a/packages/server/tests/api/rest.test.ts +++ b/packages/server/tests/api/rest.test.ts @@ -27,6 +27,7 @@ describe('REST server tests', () => { updatedAt DateTime @updatedAt email String @unique @email posts Post[] + likes PostLike[] profile Profile? } @@ -48,6 +49,7 @@ describe('REST server tests', () => { publishedAt DateTime? viewCount Int @default(0) comments Comment[] + likes PostLike[] setting Setting? } @@ -69,6 +71,8 @@ describe('REST server tests', () => { postId Int userId String superLike Boolean + post Post @relation(fields: [postId], references: [id]) + user User @relation(fields: [userId], references: [myId]) @@id([postId, userId]) } `; @@ -133,6 +137,7 @@ describe('REST server tests', () => { path: '/user', prisma, }); + console.log('yufail', JSON.stringify(r)); expect(r.status).toBe(200); expect(r.body).toMatchObject({ data: [], @@ -299,6 +304,37 @@ describe('REST server tests', () => { }); }); + it('fetches a related resource with a compound ID', async () => { + await prisma.user.create({ + data: { + myId: 'user1', + email: 'user1@abc.com', + posts: { + create: { id: 1, title: 'Post1' }, + }, + }, + }); + await prisma.postLike.create({ + data: { postId: 1, userId: 'user1', superLike: true }, + }); + + const r = await handler({ + method: 'get', + path: '/post/1/relationships/likes', + prisma, + }); + + console.log('yufail', JSON.stringify(r)); + + expect(r.status).toBe(200); + expect(r.body).toMatchObject({ + links: { + self: 'http://localhost/api/post/1/relationships/likes', + }, + data: [{ type: 'postLike', id: '1_user1' }], + }); + }); + it('fetch a relationship', async () => { // Create a user first await prisma.user.create({ diff --git a/packages/server/tests/api/test.zmodel b/packages/server/tests/api/test.zmodel new file mode 100644 index 000000000..e69de29bb From 9ceaa78abd643b8a8e2afdda78268c3922b45b4c Mon Sep 17 00:00:00 2001 From: Thomas Sunde Nielsen Date: Thu, 3 Oct 2024 10:48:24 +0200 Subject: [PATCH 11/15] Test and fix relationship update --- packages/server/src/api/rest/index.ts | 8 ++--- packages/server/tests/api/rest.test.ts | 44 +++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/packages/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts index dff6cee48..ffd04d39e 100644 --- a/packages/server/src/api/rest/index.ts +++ b/packages/server/src/api/rest/index.ts @@ -830,7 +830,7 @@ class RequestHandler extends APIHandlerBase { where: this.makeIdFilter(typeInfo.idFields, resourceId), select: { ...typeInfo.idFields.reduce((acc, field) => ({ ...acc, [field.name]: true }), {}), - [relationship]: { select: { [this.makeIdKey(relationInfo.idFields)]: true } }, + [relationship]: { select: this.makeIdSelect(relationInfo.idFields) }, }, }; @@ -885,9 +885,9 @@ class RequestHandler extends APIHandlerBase { updateArgs.data = { [relationship]: { - [relationVerb]: enumerate(parsed.data.data).map((item: any) => ({ - [this.makeIdKey(relationInfo.idFields)]: item.id, - })), + [relationVerb]: enumerate(parsed.data.data).map((item: any) => + this.makeIdFilter(relationInfo.idFields, item.id) + ), }, }; } diff --git a/packages/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index 71ec7f453..8942cac92 100644 --- a/packages/server/tests/api/rest.test.ts +++ b/packages/server/tests/api/rest.test.ts @@ -6,7 +6,6 @@ import { loadSchema, run } from '@zenstackhq/testtools'; import { Decimal } from 'decimal.js'; import SuperJSON from 'superjson'; import makeHandler, { idDivider } from '../../src/api/rest'; -import e from 'express'; describe('REST server tests', () => { let prisma: any; @@ -137,7 +136,7 @@ describe('REST server tests', () => { path: '/user', prisma, }); - console.log('yufail', JSON.stringify(r)); + expect(r.status).toBe(200); expect(r.body).toMatchObject({ data: [], @@ -324,8 +323,6 @@ describe('REST server tests', () => { prisma, }); - console.log('yufail', JSON.stringify(r)); - expect(r.status).toBe(200); expect(r.body).toMatchObject({ links: { @@ -1641,6 +1638,27 @@ describe('REST server tests', () => { expect(r.status).toBe(404); }); + it('create relation with compound id', async () => { + await prisma.user.create({ data: { myId: 'user1', email: 'user1@abc.com' } }); + await prisma.post.create({ data: { id: 1, title: 'Post1' } }); + + const r = await handler({ + method: 'post', + path: '/postLike', + query: {}, + requestBody: { + data: { + type: 'postLike', + id: `1${idDivider}user1`, + attributes: { userId: 'user1', postId: 1, superLike: false }, + }, + }, + prisma, + }); + + expect(r.status).toBe(201); + }); + describe('compound id', () => { beforeEach(async () => { await prisma.user.create({ data: { myId: 'user1', email: 'user1@abc.com' } }); @@ -1909,6 +1927,24 @@ describe('REST server tests', () => { }); }); + it('update a collection of relations with compound id', async () => { + await prisma.user.create({ data: { myId: 'user1', email: 'user1@abc.com' } }); + await prisma.post.create({ data: { id: 1, title: 'Post1' } }); + await prisma.postLike.create({ data: { userId: 'user1', postId: 1, superLike: false } }); + + const r = await handler({ + method: 'patch', + path: '/post/1/relationships/likes', + query: {}, + requestBody: { + data: [{ type: 'postLike', id: '1_user1', attributes: { superLike: true } }], + }, + prisma, + }); + + expect(r.status).toBe(200); + }); + it('update a collection of relations to empty', async () => { await prisma.user.create({ data: { myId: 'user1', email: 'user1@abc.com', posts: { create: { id: 1, title: 'Post1' } } }, From ccdb1ccc8e0fbfe987140ccc8f7b333e6c04723f Mon Sep 17 00:00:00 2001 From: Thomas Sunde Nielsen Date: Fri, 4 Oct 2024 09:28:41 +0200 Subject: [PATCH 12/15] Cleanup --- packages/server/src/api/rest/index.ts | 81 ++++++++++----------------- 1 file changed, 31 insertions(+), 50 deletions(-) diff --git a/packages/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts index ffd04d39e..c2385b95e 100644 --- a/packages/server/src/api/rest/index.ts +++ b/packages/server/src/api/rest/index.ts @@ -247,20 +247,13 @@ class RequestHandler extends APIHandlerBase { let match = urlPatterns.single.match(path); if (match) { // single resource read - return await this.processSingleRead(prisma, match.type, match.id, query, modelMeta); + return await this.processSingleRead(prisma, match.type, match.id, query); } match = urlPatterns.fetchRelationship.match(path); if (match) { // fetch related resource(s) - return await this.processFetchRelated( - prisma, - match.type, - match.id, - match.relationship, - query, - modelMeta - ); + return await this.processFetchRelated(prisma, match.type, match.id, match.relationship, query); } match = urlPatterns.relationship.match(path); @@ -271,15 +264,14 @@ class RequestHandler extends APIHandlerBase { match.type, match.id, match.relationship, - query, - modelMeta + query ); } match = urlPatterns.collection.match(path); if (match) { // collection read - return await this.processCollectionRead(prisma, match.type, query, modelMeta); + return await this.processCollectionRead(prisma, match.type, query); } return this.makeError('invalidPath'); @@ -306,8 +298,7 @@ class RequestHandler extends APIHandlerBase { match.id, match.relationship, query, - requestBody, - modelMeta + requestBody ); } @@ -345,8 +336,7 @@ class RequestHandler extends APIHandlerBase { match.id, match.relationship as string, query, - requestBody, - modelMeta + requestBody ); } @@ -370,8 +360,7 @@ class RequestHandler extends APIHandlerBase { match.id, match.relationship as string, query, - requestBody, - modelMeta + requestBody ); } @@ -394,8 +383,7 @@ class RequestHandler extends APIHandlerBase { prisma: DbClientContract, type: string, resourceId: string, - query: Record | undefined, - modelMeta: ModelMeta + query: Record | undefined ): Promise { const typeInfo = this.typeMap[type]; if (!typeInfo) { @@ -420,16 +408,12 @@ class RequestHandler extends APIHandlerBase { include = allIncludes; } - let entity = await prisma[type].findUnique(args); - - if (typeInfo.idFields.length > 1) { - entity = { ...entity, [this.makeIdKey(typeInfo.idFields)]: resourceId }; - } + const entity = await prisma[type].findUnique(args); if (entity) { return { status: 200, - body: await this.serializeItems(type, entity, modelMeta, { include }), + body: await this.serializeItems(type, entity, { include }), }; } else { return this.makeError('notFound'); @@ -441,8 +425,7 @@ class RequestHandler extends APIHandlerBase { type: string, resourceId: string, relationship: string, - query: Record | undefined, - modelMeta: ModelMeta + query: Record | undefined ): Promise { const typeInfo = this.typeMap[type]; if (!typeInfo) { @@ -499,7 +482,7 @@ class RequestHandler extends APIHandlerBase { if (entity?.[relationship]) { return { status: 200, - body: await this.serializeItems(relationInfo.type, entity[relationship], modelMeta, { + body: await this.serializeItems(relationInfo.type, entity[relationship], { linkers: { document: new Linker(() => this.makeLinkUrl(`/${type}/${resourceId}/${relationship}`)), paginator, @@ -517,8 +500,7 @@ class RequestHandler extends APIHandlerBase { type: string, resourceId: string, relationship: string, - query: Record | undefined, - modelMeta: ModelMeta + query: Record | undefined ): Promise { const typeInfo = this.typeMap[type]; if (!typeInfo) { @@ -560,7 +542,7 @@ class RequestHandler extends APIHandlerBase { } if (entity?.[relationship]) { - const serialized: any = await this.serializeItems(relationInfo.type, entity[relationship], modelMeta, { + const serialized: any = await this.serializeItems(relationInfo.type, entity[relationship], { linkers: { document: new Linker(() => this.makeLinkUrl(`/${type}/${resourceId}/relationships/${relationship}`) @@ -582,8 +564,7 @@ class RequestHandler extends APIHandlerBase { private async processCollectionRead( prisma: DbClientContract, type: string, - query: Record | undefined, - modelMeta: ModelMeta + query: Record | undefined ): Promise { const typeInfo = this.typeMap[type]; if (!typeInfo) { @@ -633,7 +614,7 @@ class RequestHandler extends APIHandlerBase { if (limit === Infinity) { const entities = await prisma[type].findMany(args); - const body = await this.serializeItems(type, entities, modelMeta, { include }); + const body = await this.serializeItems(type, entities, { include }); const total = entities.length; body.meta = this.addTotalCountToMeta(body.meta, total); @@ -656,7 +637,7 @@ class RequestHandler extends APIHandlerBase { paginator: this.makePaginator(url, offset, limit, total), }, }; - const body = await this.serializeItems(type, entities, modelMeta, options); + const body = await this.serializeItems(type, entities, options); body.meta = this.addTotalCountToMeta(body.meta, total); return { @@ -797,7 +778,7 @@ class RequestHandler extends APIHandlerBase { const entity = await prisma[type].create(createPayload); return { status: 201, - body: await this.serializeItems(type, entity, modelMeta), + body: await this.serializeItems(type, entity), }; } @@ -808,8 +789,7 @@ class RequestHandler extends APIHandlerBase { resourceId: string, relationship: string, query: Record | undefined, - requestBody: unknown, - modelMeta: ModelMeta + requestBody: unknown ): Promise { const typeInfo = this.typeMap[type]; if (!typeInfo) { @@ -894,7 +874,7 @@ class RequestHandler extends APIHandlerBase { const entity: any = await prisma[type].update(updateArgs); - const serialized: any = await this.serializeItems(relationInfo.type, entity[relationship], modelMeta, { + const serialized: any = await this.serializeItems(relationInfo.type, entity[relationship], { linkers: { document: new Linker(() => this.makeLinkUrl(`/${type}/${resourceId}/relationships/${relationship}`)), }, @@ -969,7 +949,7 @@ class RequestHandler extends APIHandlerBase { const entity = await prisma[type].update(updatePayload); return { status: 200, - body: await this.serializeItems(type, entity, modelMeta), + body: await this.serializeItems(type, entity), }; } @@ -1134,12 +1114,7 @@ class RequestHandler extends APIHandlerBase { } } - private async serializeItems( - model: string, - items: unknown, - modelMeta: ModelMeta, - options?: Partial> - ) { + private async serializeItems(model: string, items: unknown, options?: Partial>) { model = lowerCaseFirst(model); const serializer = this.serializers.get(model); if (!serializer) { @@ -1148,17 +1123,23 @@ class RequestHandler extends APIHandlerBase { const typeInfo = this.typeMap[model]; + let itemsWithId: any = items; if (typeInfo.idFields.length > 1 && Array.isArray(items)) { - items = items.map((item: any) => { + itemsWithId = items.map((item: any) => { return { ...item, [this.makeIdKey(typeInfo.idFields)]: this.makeCompoundId(typeInfo.idFields, item), }; }); + } else if (typeInfo.idFields.length > 1 && typeof items === 'object') { + itemsWithId = { + ...items, + [this.makeIdKey(typeInfo.idFields)]: this.makeCompoundId(typeInfo.idFields, items), + }; } // serialize to JSON:API structure - const serialized = await serializer.serialize(items, options); + const serialized = await serializer.serialize(itemsWithId, options); // convert the serialization result to plain object otherwise SuperJSON won't work const plainResult = this.toPlainObject(serialized); @@ -1219,7 +1200,7 @@ class RequestHandler extends APIHandlerBase { return { [idFields[0].name]: this.coerce(idFields[0].type, resourceId) }; } else { return { - [idFields.map((idf) => idf.name).join('_')]: idFields.reduce( + [idFields.map((idf) => idf.name).join(idDivider)]: idFields.reduce( (acc, curr, idx) => ({ ...acc, [curr.name]: this.coerce(curr.type, resourceId.split(idDivider)[idx]), From 7079a9c34dba084364561af0b82df12a753cc437 Mon Sep 17 00:00:00 2001 From: Thomas Sunde Nielsen Date: Fri, 4 Oct 2024 09:32:43 +0200 Subject: [PATCH 13/15] Use idDivider in tests --- packages/server/tests/api/rest.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index 8942cac92..01c741f85 100644 --- a/packages/server/tests/api/rest.test.ts +++ b/packages/server/tests/api/rest.test.ts @@ -328,7 +328,7 @@ describe('REST server tests', () => { links: { self: 'http://localhost/api/post/1/relationships/likes', }, - data: [{ type: 'postLike', id: '1_user1' }], + data: [{ type: 'postLike', id: `1${idDivider}user1` }], }); }); @@ -1937,7 +1937,7 @@ describe('REST server tests', () => { path: '/post/1/relationships/likes', query: {}, requestBody: { - data: [{ type: 'postLike', id: '1_user1', attributes: { superLike: true } }], + data: [{ type: 'postLike', id: `1${idDivider}user1`, attributes: { superLike: true } }], }, prisma, }); From 4f3c6c11961987e0ae32955ed48368987ed93aff Mon Sep 17 00:00:00 2001 From: Thomas Sunde Nielsen Date: Mon, 7 Oct 2024 09:22:20 +0200 Subject: [PATCH 14/15] Remove empty file --- packages/server/tests/api/test.zmodel | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 packages/server/tests/api/test.zmodel diff --git a/packages/server/tests/api/test.zmodel b/packages/server/tests/api/test.zmodel deleted file mode 100644 index e69de29bb..000000000 From dfb97b9b99bd8460ec1f2e4c4d5266c54fb5b11d Mon Sep 17 00:00:00 2001 From: Thomas Sunde Nielsen Date: Mon, 7 Oct 2024 09:23:31 +0200 Subject: [PATCH 15/15] Remove unneccary nesting --- packages/server/tests/api/rest.test.ts | 38 ++++++++++++-------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/packages/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index 01c741f85..f0ba24dfe 100644 --- a/packages/server/tests/api/rest.test.ts +++ b/packages/server/tests/api/rest.test.ts @@ -1659,31 +1659,27 @@ describe('REST server tests', () => { expect(r.status).toBe(201); }); - describe('compound id', () => { - beforeEach(async () => { - await prisma.user.create({ data: { myId: 'user1', email: 'user1@abc.com' } }); - await prisma.post.create({ - data: { id: 1, title: 'Post1' }, - }); + it('compound id create single', async () => { + await prisma.user.create({ data: { myId: 'user1', email: 'user1@abc.com' } }); + await prisma.post.create({ + data: { id: 1, title: 'Post1' }, }); - it('create single', async () => { - const r = await handler({ - method: 'post', - path: '/postLike', - query: {}, - requestBody: { - data: { - type: 'postLike', - id: `1${idDivider}user1`, - attributes: { userId: 'user1', postId: 1, superLike: false }, - }, + const r = await handler({ + method: 'post', + path: '/postLike', + query: {}, + requestBody: { + data: { + type: 'postLike', + id: `1${idDivider}user1`, + attributes: { userId: 'user1', postId: 1, superLike: false }, }, - prisma, - }); - - expect(r.status).toBe(201); + }, + prisma, }); + + expect(r.status).toBe(201); }); });