Skip to content

Commit 251c699

Browse files
authored
merge dev to main (v2.9.4) (#1892)
2 parents f5e4e7c + a747d95 commit 251c699

File tree

21 files changed

+311
-30
lines changed

21 files changed

+311
-30
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "zenstack-monorepo",
3-
"version": "2.9.3",
3+
"version": "2.9.4",
44
"description": "",
55
"scripts": {
66
"build": "pnpm -r build",

packages/ide/jetbrains/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ plugins {
99
}
1010

1111
group = "dev.zenstack"
12-
version = "2.9.3"
12+
version = "2.9.4"
1313

1414
repositories {
1515
mavenCentral()

packages/ide/jetbrains/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "jetbrains",
3-
"version": "2.9.3",
3+
"version": "2.9.4",
44
"displayName": "ZenStack JetBrains IDE Plugin",
55
"description": "ZenStack JetBrains IDE plugin",
66
"homepage": "https://zenstack.dev",

packages/language/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/language",
3-
"version": "2.9.3",
3+
"version": "2.9.4",
44
"displayName": "ZenStack modeling language compiler",
55
"description": "ZenStack modeling language compiler",
66
"homepage": "https://zenstack.dev",

packages/misc/redwood/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/redwood",
33
"displayName": "ZenStack RedwoodJS Integration",
4-
"version": "2.9.3",
4+
"version": "2.9.4",
55
"description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.",
66
"repository": {
77
"type": "git",

packages/plugins/openapi/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/openapi",
33
"displayName": "ZenStack Plugin and Runtime for OpenAPI",
4-
"version": "2.9.3",
4+
"version": "2.9.4",
55
"description": "ZenStack plugin and runtime supporting OpenAPI",
66
"main": "index.js",
77
"repository": {

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -857,10 +857,8 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase {
857857

858858
private generateModelEntity(model: DataModel, mode: 'read' | 'create' | 'update'): OAPI.SchemaObject {
859859
const idFields = model.fields.filter((f) => isIdField(f));
860-
// For compound ids each component is also exposed as a separate fields for read operations,
861-
// but not required for write operations
862-
const fields =
863-
idFields.length > 1 && mode === 'read' ? model.fields : model.fields.filter((f) => !isIdField(f));
860+
// For compound ids each component is also exposed as a separate fields.
861+
const fields = idFields.length > 1 ? model.fields : model.fields.filter((f) => !isIdField(f));
864862

865863
const attributes: Record<string, OAPI.SchemaObject> = {};
866864
const relationships: Record<string, OAPI.ReferenceObject | OAPI.SchemaObject> = {};
@@ -911,7 +909,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase {
911909
if (mode === 'create') {
912910
// 'id' is required if there's no default value
913911
const idFields = model.fields.filter((f) => isIdField(f));
914-
if (idFields.length && idFields.every((f) => !hasAttribute(f, '@default'))) {
912+
if (idFields.length === 1 && !hasAttribute(idFields[0], '@default')) {
915913
properties = { id: { type: 'string' }, ...properties };
916914
toplevelRequired.unshift('id');
917915
}

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3123,14 +3123,11 @@ components:
31233123
type: object
31243124
description: The "PostLike" model
31253125
required:
3126-
- id
31273126
- type
31283127
- attributes
31293128
properties:
31303129
type:
31313130
type: string
3132-
attributes:
3133-
type: object
31343131
relationships:
31353132
type: object
31363133
properties:

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3135,7 +3135,6 @@ components:
31353135
type: object
31363136
description: The "PostLike" model
31373137
required:
3138-
- id
31393138
- type
31403139
- attributes
31413140
properties:

packages/plugins/swr/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/swr",
33
"displayName": "ZenStack plugin for generating SWR hooks",
4-
"version": "2.9.3",
4+
"version": "2.9.4",
55
"description": "ZenStack plugin for generating SWR hooks",
66
"main": "index.js",
77
"repository": {

packages/plugins/tanstack-query/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/tanstack-query",
33
"displayName": "ZenStack plugin for generating tanstack-query hooks",
4-
"version": "2.9.3",
4+
"version": "2.9.4",
55
"description": "ZenStack plugin for generating tanstack-query hooks",
66
"main": "index.js",
77
"exports": {

packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ describe('Tanstack Query React Hooks V5 Test', () => {
387387
expect(userResult.current.data).toHaveLength(1);
388388
});
389389

390-
// pupulate the cache with a category
390+
// populate the cache with a category
391391
const categoryData: any[] = [{ id: '1', name: 'category1', posts: [] }];
392392

393393
nock(BASE_URL)
@@ -501,7 +501,7 @@ describe('Tanstack Query React Hooks V5 Test', () => {
501501
it('optimistic update with optional one-to-many relationship', async () => {
502502
const { queryClient, wrapper } = createWrapper();
503503

504-
// populate the cache with a post, with an optional category relatonship
504+
// populate the cache with a post, with an optional category relationship
505505
const postData: any = {
506506
id: '1',
507507
title: 'post1',

packages/plugins/tanstack-query/tests/test-model-meta.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ export const modelMeta: ModelMeta = {
5959
type: 'Category',
6060
name: 'category',
6161
isDataModel: true,
62-
isOptional: true,
6362
isRelationOwner: true,
6463
backLink: 'posts',
6564
foreignKeyMapping: { id: 'categoryId' },

packages/plugins/trpc/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/trpc",
33
"displayName": "ZenStack plugin for tRPC",
4-
"version": "2.9.3",
4+
"version": "2.9.4",
55
"description": "ZenStack plugin for tRPC",
66
"main": "index.js",
77
"repository": {

packages/runtime/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/runtime",
33
"displayName": "ZenStack Runtime Library",
4-
"version": "2.9.3",
4+
"version": "2.9.4",
55
"description": "Runtime of ZenStack for both client-side and server-side environments.",
66
"repository": {
77
"type": "git",

packages/schema/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"publisher": "zenstack",
44
"displayName": "ZenStack Language Tools",
55
"description": "FullStack enhancement for Prisma ORM: seamless integration from database to UI",
6-
"version": "2.9.3",
6+
"version": "2.9.4",
77
"author": {
88
"name": "ZenStack Team"
99
},

packages/sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/sdk",
3-
"version": "2.9.3",
3+
"version": "2.9.4",
44
"description": "ZenStack plugin development SDK",
55
"main": "index.js",
66
"scripts": {

packages/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/server",
3-
"version": "2.9.3",
3+
"version": "2.9.4",
44
"displayName": "ZenStack Server-side Adapters",
55
"description": "ZenStack server-side adapters",
66
"homepage": "https://zenstack.dev",

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

Lines changed: 133 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,13 @@ class RequestHandler extends APIHandlerBase {
209209
data: z.array(z.object({ type: z.string(), id: z.union([z.string(), z.number()]) })),
210210
});
211211

212+
private upsertMetaSchema = z.object({
213+
meta: z.object({
214+
operation: z.literal('upsert'),
215+
matchFields: z.array(z.string()).min(1),
216+
}),
217+
});
218+
212219
// all known types and their metadata
213220
private typeMap: Record<string, ModelInfo>;
214221

@@ -309,8 +316,29 @@ class RequestHandler extends APIHandlerBase {
309316

310317
let match = this.urlPatterns.collection.match(path);
311318
if (match) {
312-
// resource creation
313-
return await this.processCreate(prisma, match.type, query, requestBody, modelMeta, zodSchemas);
319+
const body = requestBody as any;
320+
const upsertMeta = this.upsertMetaSchema.safeParse(body);
321+
if (upsertMeta.success) {
322+
// resource upsert
323+
return await this.processUpsert(
324+
prisma,
325+
match.type,
326+
query,
327+
requestBody,
328+
modelMeta,
329+
zodSchemas
330+
);
331+
} else {
332+
// resource creation
333+
return await this.processCreate(
334+
prisma,
335+
match.type,
336+
query,
337+
requestBody,
338+
modelMeta,
339+
zodSchemas
340+
);
341+
}
314342
}
315343

316344
match = this.urlPatterns.relationship.match(path);
@@ -809,6 +837,90 @@ class RequestHandler extends APIHandlerBase {
809837
};
810838
}
811839

840+
private async processUpsert(
841+
prisma: DbClientContract,
842+
type: string,
843+
_query: Record<string, string | string[]> | undefined,
844+
requestBody: unknown,
845+
modelMeta: ModelMeta,
846+
zodSchemas?: ZodSchemas
847+
) {
848+
const typeInfo = this.typeMap[type];
849+
if (!typeInfo) {
850+
return this.makeUnsupportedModelError(type);
851+
}
852+
853+
const { error, attributes, relationships } = this.processRequestBody(type, requestBody, zodSchemas, 'create');
854+
855+
if (error) {
856+
return error;
857+
}
858+
859+
const matchFields = this.upsertMetaSchema.parse(requestBody).meta.matchFields;
860+
861+
const uniqueFields = Object.values(modelMeta.models[type].uniqueConstraints || {}).map((uf) => uf.fields);
862+
863+
if (
864+
!uniqueFields.some((uniqueCombination) => uniqueCombination.every((field) => matchFields.includes(field)))
865+
) {
866+
return this.makeError('invalidPayload', 'Match fields must be unique fields', 400);
867+
}
868+
869+
const upsertPayload: any = {
870+
where: this.makeUpsertWhere(matchFields, attributes, typeInfo),
871+
create: { ...attributes },
872+
update: {
873+
...Object.fromEntries(Object.entries(attributes).filter((e) => !matchFields.includes(e[0]))),
874+
},
875+
};
876+
877+
if (relationships) {
878+
for (const [key, data] of Object.entries<any>(relationships)) {
879+
if (!data?.data) {
880+
return this.makeError('invalidRelationData');
881+
}
882+
883+
const relationInfo = typeInfo.relationships[key];
884+
if (!relationInfo) {
885+
return this.makeUnsupportedRelationshipError(type, key, 400);
886+
}
887+
888+
if (relationInfo.isCollection) {
889+
upsertPayload.create[key] = {
890+
connect: enumerate(data.data).map((item: any) =>
891+
this.makeIdConnect(relationInfo.idFields, item.id)
892+
),
893+
};
894+
upsertPayload.update[key] = {
895+
set: enumerate(data.data).map((item: any) =>
896+
this.makeIdConnect(relationInfo.idFields, item.id)
897+
),
898+
};
899+
} else {
900+
if (typeof data.data !== 'object') {
901+
return this.makeError('invalidRelationData');
902+
}
903+
upsertPayload.create[key] = {
904+
connect: this.makeIdConnect(relationInfo.idFields, data.data.id),
905+
};
906+
upsertPayload.update[key] = {
907+
connect: this.makeIdConnect(relationInfo.idFields, data.data.id),
908+
};
909+
}
910+
}
911+
}
912+
913+
// include IDs of relation fields so that they can be serialized.
914+
this.includeRelationshipIds(type, upsertPayload, 'include');
915+
916+
const entity = await prisma[type].upsert(upsertPayload);
917+
918+
return {
919+
status: 201,
920+
body: await this.serializeItems(type, entity),
921+
};
922+
}
923+
812924
private async processRelationshipCRUD(
813925
prisma: DbClientContract,
814926
mode: 'create' | 'update' | 'delete',
@@ -959,7 +1071,7 @@ class RequestHandler extends APIHandlerBase {
9591071
return this.makeError('invalidRelationData');
9601072
}
9611073
updatePayload.data[key] = {
962-
set: {
1074+
connect: {
9631075
[this.makePrismaIdKey(relationInfo.idFields)]: data.data.id,
9641076
},
9651077
};
@@ -1296,6 +1408,24 @@ class RequestHandler extends APIHandlerBase {
12961408
return idFields.map((idf) => item[idf.name]).join(this.idDivider);
12971409
}
12981410

1411+
private makeUpsertWhere(matchFields: any[], attributes: any, typeInfo: ModelInfo) {
1412+
const where = matchFields.reduce((acc: any, field: string) => {
1413+
acc[field] = attributes[field] ?? null;
1414+
return acc;
1415+
}, {});
1416+
1417+
if (
1418+
typeInfo.idFields.length > 1 &&
1419+
matchFields.some((mf) => typeInfo.idFields.map((idf) => idf.name).includes(mf))
1420+
) {
1421+
return {
1422+
[this.makePrismaIdKey(typeInfo.idFields)]: where,
1423+
};
1424+
}
1425+
1426+
return where;
1427+
}
1428+
12991429
private includeRelationshipIds(model: string, args: any, mode: 'select' | 'include') {
13001430
const typeInfo = this.typeMap[model];
13011431
if (!typeInfo) {

0 commit comments

Comments
 (0)