Skip to content

Commit c344f77

Browse files
thomassnielsenymc9
andauthored
Improve working with entities related to entities that uses compound ids (#1801)
Co-authored-by: ymc9 <[email protected]>
1 parent efc9da2 commit c344f77

File tree

6 files changed

+68
-46
lines changed

6 files changed

+68
-46
lines changed

packages/plugins/openapi/src/rest-generator.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -847,8 +847,10 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase {
847847

848848
private generateModelEntity(model: DataModel, mode: 'read' | 'create' | 'update'): OAPI.SchemaObject {
849849
const idFields = model.fields.filter((f) => isIdField(f));
850-
// For compound ids, each component is also exposed as a separate field
851-
const fields = idFields.length > 1 ? model.fields : model.fields.filter((f) => !isIdField(f));
850+
// For compound ids each component is also exposed as a separate fields for read operations,
851+
// but not required for write operations
852+
const fields =
853+
idFields.length > 1 && mode === 'read' ? model.fields : model.fields.filter((f) => !isIdField(f));
852854

853855
const attributes: Record<string, OAPI.SchemaObject> = {};
854856
const relationships: Record<string, OAPI.ReferenceObject | OAPI.SchemaObject> = {};

packages/plugins/openapi/tests/baseline/rest-3.0.0.baseline.yaml

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3143,14 +3143,6 @@ components:
31433143
type: string
31443144
attributes:
31453145
type: object
3146-
required:
3147-
- postId
3148-
- userId
3149-
properties:
3150-
postId:
3151-
type: string
3152-
userId:
3153-
type: string
31543146
relationships:
31553147
type: object
31563148
properties:
@@ -3178,13 +3170,6 @@ components:
31783170
type: string
31793171
type:
31803172
type: string
3181-
attributes:
3182-
type: object
3183-
properties:
3184-
postId:
3185-
type: string
3186-
userId:
3187-
type: string
31883173
relationships:
31893174
type: object
31903175
properties:

packages/plugins/openapi/tests/baseline/rest-3.1.0.baseline.yaml

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3155,16 +3155,6 @@ components:
31553155
properties:
31563156
type:
31573157
type: string
3158-
attributes:
3159-
type: object
3160-
required:
3161-
- postId
3162-
- userId
3163-
properties:
3164-
postId:
3165-
type: string
3166-
userId:
3167-
type: string
31683158
relationships:
31693159
type: object
31703160
properties:
@@ -3192,13 +3182,6 @@ components:
31923182
type: string
31933183
type:
31943184
type: string
3195-
attributes:
3196-
type: object
3197-
properties:
3198-
postId:
3199-
type: string
3200-
userId:
3201-
type: string
32023185
relationships:
32033186
type: object
32043187
properties:

packages/schema/src/plugins/zod/generator.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
isEnumFieldReference,
1111
isForeignKeyField,
1212
isFromStdlib,
13+
isIdField,
1314
parseOptionAsStrings,
1415
resolvePath,
1516
} from '@zenstackhq/sdk';
@@ -291,8 +292,10 @@ export class ZodSchemaGenerator {
291292
sf.replaceWithText((writer) => {
292293
const scalarFields = model.fields.filter(
293294
(field) =>
295+
// id fields are always included
296+
isIdField(field) ||
294297
// regular fields only
295-
!isDataModel(field.type.reference?.ref) && !isForeignKeyField(field)
298+
(!isDataModel(field.type.reference?.ref) && !isForeignKeyField(field))
296299
);
297300

298301
const relations = model.fields.filter((field) => isDataModel(field.type.reference?.ref));

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

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -720,8 +720,9 @@ class RequestHandler extends APIHandlerBase {
720720
const attributes: any = parsed.data.attributes;
721721

722722
if (attributes) {
723-
const schemaName = `${upperCaseFirst(type)}${upperCaseFirst(mode)}Schema`;
724-
// zod-parse attributes if a schema is provided
723+
// use the zod schema (that only contains non-relation fields) to validate the payload,
724+
// if available
725+
const schemaName = `${upperCaseFirst(type)}${upperCaseFirst(mode)}ScalarSchema`;
725726
const payloadSchema = zodSchemas?.models?.[schemaName];
726727
if (payloadSchema) {
727728
const parsed = payloadSchema.safeParse(attributes);
@@ -756,6 +757,7 @@ class RequestHandler extends APIHandlerBase {
756757
}
757758

758759
const { error, attributes, relationships } = this.processRequestBody(type, requestBody, zodSchemas, 'create');
760+
759761
if (error) {
760762
return error;
761763
}
@@ -776,18 +778,16 @@ class RequestHandler extends APIHandlerBase {
776778

777779
if (relationInfo.isCollection) {
778780
createPayload.data[key] = {
779-
connect: enumerate(data.data).map((item: any) => ({
780-
[this.makePrismaIdKey(relationInfo.idFields)]: item.id,
781-
})),
781+
connect: enumerate(data.data).map((item: any) =>
782+
this.makeIdConnect(relationInfo.idFields, item.id)
783+
),
782784
};
783785
} else {
784786
if (typeof data.data !== 'object') {
785787
return this.makeError('invalidRelationData');
786788
}
787789
createPayload.data[key] = {
788-
connect: {
789-
[this.makePrismaIdKey(relationInfo.idFields)]: data.data.id,
790-
},
790+
connect: this.makeIdConnect(relationInfo.idFields, data.data.id),
791791
};
792792
}
793793

@@ -868,9 +868,7 @@ class RequestHandler extends APIHandlerBase {
868868
} else {
869869
updateArgs.data = {
870870
[relationship]: {
871-
connect: {
872-
[this.makePrismaIdKey(relationInfo.idFields)]: parsed.data.data.id,
873-
},
871+
connect: this.makeIdConnect(relationInfo.idFields, parsed.data.data.id),
874872
},
875873
};
876874
}
@@ -1261,6 +1259,22 @@ class RequestHandler extends APIHandlerBase {
12611259
return idFields.reduce((acc, curr) => ({ ...acc, [curr.name]: true }), {});
12621260
}
12631261

1262+
private makeIdConnect(idFields: FieldInfo[], id: string | number) {
1263+
if (idFields.length === 1) {
1264+
return { [idFields[0].name]: this.coerce(idFields[0].type, id) };
1265+
} else {
1266+
return {
1267+
[this.makePrismaIdKey(idFields)]: idFields.reduce(
1268+
(acc, curr, idx) => ({
1269+
...acc,
1270+
[curr.name]: this.coerce(curr.type, `${id}`.split(this.idDivider)[idx]),
1271+
}),
1272+
{}
1273+
),
1274+
};
1275+
}
1276+
}
1277+
12641278
private makeIdKey(idFields: FieldInfo[]) {
12651279
return idFields.map((idf) => idf.name).join(this.idDivider);
12661280
}

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,17 @@ describe('REST server tests', () => {
7474
superLike Boolean
7575
post Post @relation(fields: [postId], references: [id])
7676
user User @relation(fields: [userId], references: [myId])
77+
likeInfos PostLikeInfo[]
7778
@@id([postId, userId])
7879
}
80+
81+
model PostLikeInfo {
82+
id Int @id @default(autoincrement())
83+
text String
84+
postId Int
85+
userId String
86+
postLike PostLike @relation(fields: [postId, userId], references: [postId, userId])
87+
}
7988
`;
8089

8190
beforeAll(async () => {
@@ -1765,6 +1774,32 @@ describe('REST server tests', () => {
17651774

17661775
expect(r.status).toBe(201);
17671776
});
1777+
1778+
it('create an entity related to an entity with compound id', async () => {
1779+
await prisma.user.create({ data: { myId: 'user1', email: '[email protected]' } });
1780+
await prisma.post.create({ data: { id: 1, title: 'Post1' } });
1781+
await prisma.postLike.create({ data: { userId: 'user1', postId: 1, superLike: false } });
1782+
1783+
const r = await handler({
1784+
method: 'post',
1785+
path: '/postLikeInfo',
1786+
query: {},
1787+
requestBody: {
1788+
data: {
1789+
type: 'postLikeInfo',
1790+
attributes: { text: 'LikeInfo1' },
1791+
relationships: {
1792+
postLike: {
1793+
data: { type: 'postLike', id: `1${idDivider}user1` },
1794+
},
1795+
},
1796+
},
1797+
},
1798+
prisma,
1799+
});
1800+
1801+
expect(r.status).toBe(201);
1802+
});
17681803
});
17691804

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

0 commit comments

Comments
 (0)