Skip to content

Improve working with entities related to entities that uses compound ids #1801

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions packages/plugins/openapi/src/rest-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, OAPI.SchemaObject> = {};
const relationships: Record<string, OAPI.ReferenceObject | OAPI.SchemaObject> = {};
Expand Down
15 changes: 0 additions & 15 deletions packages/plugins/openapi/tests/baseline/rest-3.0.0.baseline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3143,14 +3143,6 @@ components:
type: string
attributes:
type: object
required:
- postId
- userId
properties:
postId:
type: string
userId:
type: string
relationships:
type: object
properties:
Expand Down Expand Up @@ -3178,13 +3170,6 @@ components:
type: string
type:
type: string
attributes:
type: object
properties:
postId:
type: string
userId:
type: string
relationships:
type: object
properties:
Expand Down
17 changes: 0 additions & 17 deletions packages/plugins/openapi/tests/baseline/rest-3.1.0.baseline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -3192,13 +3182,6 @@ components:
type: string
type:
type: string
attributes:
type: object
properties:
postId:
type: string
userId:
type: string
relationships:
type: object
properties:
Expand Down
5 changes: 4 additions & 1 deletion packages/schema/src/plugins/zod/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
isEnumFieldReference,
isForeignKeyField,
isFromStdlib,
isIdField,
parseOptionAsStrings,
resolvePath,
} from '@zenstackhq/sdk';
Expand Down Expand Up @@ -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));
Expand Down
36 changes: 25 additions & 11 deletions packages/server/src/api/rest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -756,6 +757,7 @@ class RequestHandler extends APIHandlerBase {
}

const { error, attributes, relationships } = this.processRequestBody(type, requestBody, zodSchemas, 'create');

if (error) {
return error;
}
Expand All @@ -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),
};
}

Expand Down Expand Up @@ -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),
},
};
}
Expand Down Expand Up @@ -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);
}
Expand Down
35 changes: 35 additions & 0 deletions packages/server/tests/api/rest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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: '[email protected]' } });
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', () => {
Expand Down
Loading