diff --git a/packages/plugins/openapi/src/rest-generator.ts b/packages/plugins/openapi/src/rest-generator.ts index 7cf465d9e..8927198cc 100644 --- a/packages/plugins/openapi/src/rest-generator.ts +++ b/packages/plugins/openapi/src/rest-generator.ts @@ -847,8 +847,10 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase { private generateModelEntity(model: DataModel, mode: 'read' | 'create' | 'update'): OAPI.SchemaObject { const idFields = model.fields.filter((f) => isIdField(f)); - // For compound ids, each component is also exposed as a separate field - const fields = idFields.length > 1 ? model.fields : model.fields.filter((f) => !isIdField(f)); + // For compound ids each component is also exposed as a separate fields for read operations, + // but not required for write operations + const fields = + idFields.length > 1 && mode === 'read' ? model.fields : model.fields.filter((f) => !isIdField(f)); const attributes: Record = {}; const relationships: Record = {}; diff --git a/packages/plugins/openapi/tests/baseline/rest-3.0.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rest-3.0.0.baseline.yaml index 96f80d81a..adb9ded12 100644 --- a/packages/plugins/openapi/tests/baseline/rest-3.0.0.baseline.yaml +++ b/packages/plugins/openapi/tests/baseline/rest-3.0.0.baseline.yaml @@ -3143,14 +3143,6 @@ components: type: string attributes: type: object - required: - - postId - - userId - properties: - postId: - type: string - userId: - type: string relationships: type: object properties: @@ -3178,13 +3170,6 @@ components: type: string type: type: string - attributes: - type: object - properties: - postId: - type: string - userId: - type: string relationships: type: object properties: diff --git a/packages/plugins/openapi/tests/baseline/rest-3.1.0.baseline.yaml b/packages/plugins/openapi/tests/baseline/rest-3.1.0.baseline.yaml index e3f2d6821..f69536b30 100644 --- a/packages/plugins/openapi/tests/baseline/rest-3.1.0.baseline.yaml +++ b/packages/plugins/openapi/tests/baseline/rest-3.1.0.baseline.yaml @@ -3155,16 +3155,6 @@ components: properties: type: type: string - attributes: - type: object - required: - - postId - - userId - properties: - postId: - type: string - userId: - type: string relationships: type: object properties: @@ -3192,13 +3182,6 @@ components: type: string type: type: string - attributes: - type: object - properties: - postId: - type: string - userId: - type: string relationships: type: object properties: diff --git a/packages/schema/src/plugins/zod/generator.ts b/packages/schema/src/plugins/zod/generator.ts index 5021a9927..ca26ffabe 100644 --- a/packages/schema/src/plugins/zod/generator.ts +++ b/packages/schema/src/plugins/zod/generator.ts @@ -10,6 +10,7 @@ import { isEnumFieldReference, isForeignKeyField, isFromStdlib, + isIdField, parseOptionAsStrings, resolvePath, } from '@zenstackhq/sdk'; @@ -291,8 +292,10 @@ export class ZodSchemaGenerator { sf.replaceWithText((writer) => { const scalarFields = model.fields.filter( (field) => + // id fields are always included + isIdField(field) || // regular fields only - !isDataModel(field.type.reference?.ref) && !isForeignKeyField(field) + (!isDataModel(field.type.reference?.ref) && !isForeignKeyField(field)) ); const relations = model.fields.filter((field) => isDataModel(field.type.reference?.ref)); diff --git a/packages/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts index 2e0bcaec5..e4ec06ff7 100644 --- a/packages/server/src/api/rest/index.ts +++ b/packages/server/src/api/rest/index.ts @@ -720,8 +720,9 @@ class RequestHandler extends APIHandlerBase { const attributes: any = parsed.data.attributes; if (attributes) { - const schemaName = `${upperCaseFirst(type)}${upperCaseFirst(mode)}Schema`; - // zod-parse attributes if a schema is provided + // use the zod schema (that only contains non-relation fields) to validate the payload, + // if available + const schemaName = `${upperCaseFirst(type)}${upperCaseFirst(mode)}ScalarSchema`; const payloadSchema = zodSchemas?.models?.[schemaName]; if (payloadSchema) { const parsed = payloadSchema.safeParse(attributes); @@ -756,6 +757,7 @@ class RequestHandler extends APIHandlerBase { } const { error, attributes, relationships } = this.processRequestBody(type, requestBody, zodSchemas, 'create'); + if (error) { return error; } @@ -776,18 +778,16 @@ class RequestHandler extends APIHandlerBase { if (relationInfo.isCollection) { createPayload.data[key] = { - connect: enumerate(data.data).map((item: any) => ({ - [this.makePrismaIdKey(relationInfo.idFields)]: item.id, - })), + connect: enumerate(data.data).map((item: any) => + this.makeIdConnect(relationInfo.idFields, item.id) + ), }; } else { if (typeof data.data !== 'object') { return this.makeError('invalidRelationData'); } createPayload.data[key] = { - connect: { - [this.makePrismaIdKey(relationInfo.idFields)]: data.data.id, - }, + connect: this.makeIdConnect(relationInfo.idFields, data.data.id), }; } @@ -868,9 +868,7 @@ class RequestHandler extends APIHandlerBase { } else { updateArgs.data = { [relationship]: { - connect: { - [this.makePrismaIdKey(relationInfo.idFields)]: parsed.data.data.id, - }, + connect: this.makeIdConnect(relationInfo.idFields, parsed.data.data.id), }, }; } @@ -1261,6 +1259,22 @@ class RequestHandler extends APIHandlerBase { return idFields.reduce((acc, curr) => ({ ...acc, [curr.name]: true }), {}); } + private makeIdConnect(idFields: FieldInfo[], id: string | number) { + if (idFields.length === 1) { + return { [idFields[0].name]: this.coerce(idFields[0].type, id) }; + } else { + return { + [this.makePrismaIdKey(idFields)]: idFields.reduce( + (acc, curr, idx) => ({ + ...acc, + [curr.name]: this.coerce(curr.type, `${id}`.split(this.idDivider)[idx]), + }), + {} + ), + }; + } + } + private makeIdKey(idFields: FieldInfo[]) { return idFields.map((idf) => idf.name).join(this.idDivider); } diff --git a/packages/server/tests/api/rest.test.ts b/packages/server/tests/api/rest.test.ts index 3fee62d9a..1b5463650 100644 --- a/packages/server/tests/api/rest.test.ts +++ b/packages/server/tests/api/rest.test.ts @@ -74,8 +74,17 @@ describe('REST server tests', () => { superLike Boolean post Post @relation(fields: [postId], references: [id]) user User @relation(fields: [userId], references: [myId]) + likeInfos PostLikeInfo[] @@id([postId, userId]) } + + model PostLikeInfo { + id Int @id @default(autoincrement()) + text String + postId Int + userId String + postLike PostLike @relation(fields: [postId, userId], references: [postId, userId]) + } `; beforeAll(async () => { @@ -1765,6 +1774,32 @@ describe('REST server tests', () => { expect(r.status).toBe(201); }); + + it('create an entity related to an entity 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: 'post', + path: '/postLikeInfo', + query: {}, + requestBody: { + data: { + type: 'postLikeInfo', + attributes: { text: 'LikeInfo1' }, + relationships: { + postLike: { + data: { type: 'postLike', id: `1${idDivider}user1` }, + }, + }, + }, + }, + prisma, + }); + + expect(r.status).toBe(201); + }); }); describe('PUT', () => {