Skip to content

Commit

Permalink
merge dev to main (v2.9.4) (#1892)
Browse files Browse the repository at this point in the history
  • Loading branch information
ymc9 authored Nov 27, 2024
2 parents f5e4e7c + a747d95 commit 251c699
Show file tree
Hide file tree
Showing 21 changed files with 311 additions and 30 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "zenstack-monorepo",
"version": "2.9.3",
"version": "2.9.4",
"description": "",
"scripts": {
"build": "pnpm -r build",
Expand Down
2 changes: 1 addition & 1 deletion packages/ide/jetbrains/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ plugins {
}

group = "dev.zenstack"
version = "2.9.3"
version = "2.9.4"

repositories {
mavenCentral()
Expand Down
2 changes: 1 addition & 1 deletion packages/ide/jetbrains/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "jetbrains",
"version": "2.9.3",
"version": "2.9.4",
"displayName": "ZenStack JetBrains IDE Plugin",
"description": "ZenStack JetBrains IDE plugin",
"homepage": "https://zenstack.dev",
Expand Down
2 changes: 1 addition & 1 deletion packages/language/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/language",
"version": "2.9.3",
"version": "2.9.4",
"displayName": "ZenStack modeling language compiler",
"description": "ZenStack modeling language compiler",
"homepage": "https://zenstack.dev",
Expand Down
2 changes: 1 addition & 1 deletion packages/misc/redwood/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/redwood",
"displayName": "ZenStack RedwoodJS Integration",
"version": "2.9.3",
"version": "2.9.4",
"description": "CLI and runtime for integrating ZenStack with RedwoodJS projects.",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/openapi/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/openapi",
"displayName": "ZenStack Plugin and Runtime for OpenAPI",
"version": "2.9.3",
"version": "2.9.4",
"description": "ZenStack plugin and runtime supporting OpenAPI",
"main": "index.js",
"repository": {
Expand Down
8 changes: 3 additions & 5 deletions packages/plugins/openapi/src/rest-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -857,10 +857,8 @@ 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 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));
// For compound ids each component is also exposed as a separate fields.
const fields = idFields.length > 1 ? model.fields : model.fields.filter((f) => !isIdField(f));

const attributes: Record<string, OAPI.SchemaObject> = {};
const relationships: Record<string, OAPI.ReferenceObject | OAPI.SchemaObject> = {};
Expand Down Expand Up @@ -911,7 +909,7 @@ export class RESTfulOpenAPIGenerator extends OpenAPIGeneratorBase {
if (mode === 'create') {
// 'id' is required if there's no default value
const idFields = model.fields.filter((f) => isIdField(f));
if (idFields.length && idFields.every((f) => !hasAttribute(f, '@default'))) {
if (idFields.length === 1 && !hasAttribute(idFields[0], '@default')) {
properties = { id: { type: 'string' }, ...properties };
toplevelRequired.unshift('id');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3123,14 +3123,11 @@ components:
type: object
description: The "PostLike" model
required:
- id
- type
- attributes
properties:
type:
type: string
attributes:
type: object
relationships:
type: object
properties:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3135,7 +3135,6 @@ components:
type: object
description: The "PostLike" model
required:
- id
- type
- attributes
properties:
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/swr/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/swr",
"displayName": "ZenStack plugin for generating SWR hooks",
"version": "2.9.3",
"version": "2.9.4",
"description": "ZenStack plugin for generating SWR hooks",
"main": "index.js",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/tanstack-query/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/tanstack-query",
"displayName": "ZenStack plugin for generating tanstack-query hooks",
"version": "2.9.3",
"version": "2.9.4",
"description": "ZenStack plugin for generating tanstack-query hooks",
"main": "index.js",
"exports": {
Expand Down
4 changes: 2 additions & 2 deletions packages/plugins/tanstack-query/tests/react-hooks-v5.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ describe('Tanstack Query React Hooks V5 Test', () => {
expect(userResult.current.data).toHaveLength(1);
});

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

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

// populate the cache with a post, with an optional category relatonship
// populate the cache with a post, with an optional category relationship
const postData: any = {
id: '1',
title: 'post1',
Expand Down
1 change: 0 additions & 1 deletion packages/plugins/tanstack-query/tests/test-model-meta.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ export const modelMeta: ModelMeta = {
type: 'Category',
name: 'category',
isDataModel: true,
isOptional: true,
isRelationOwner: true,
backLink: 'posts',
foreignKeyMapping: { id: 'categoryId' },
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/trpc/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/trpc",
"displayName": "ZenStack plugin for tRPC",
"version": "2.9.3",
"version": "2.9.4",
"description": "ZenStack plugin for tRPC",
"main": "index.js",
"repository": {
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/runtime",
"displayName": "ZenStack Runtime Library",
"version": "2.9.3",
"version": "2.9.4",
"description": "Runtime of ZenStack for both client-side and server-side environments.",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion packages/schema/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"publisher": "zenstack",
"displayName": "ZenStack Language Tools",
"description": "FullStack enhancement for Prisma ORM: seamless integration from database to UI",
"version": "2.9.3",
"version": "2.9.4",
"author": {
"name": "ZenStack Team"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/sdk",
"version": "2.9.3",
"version": "2.9.4",
"description": "ZenStack plugin development SDK",
"main": "index.js",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion packages/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/server",
"version": "2.9.3",
"version": "2.9.4",
"displayName": "ZenStack Server-side Adapters",
"description": "ZenStack server-side adapters",
"homepage": "https://zenstack.dev",
Expand Down
136 changes: 133 additions & 3 deletions packages/server/src/api/rest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,13 @@ class RequestHandler extends APIHandlerBase {
data: z.array(z.object({ type: z.string(), id: z.union([z.string(), z.number()]) })),
});

private upsertMetaSchema = z.object({
meta: z.object({
operation: z.literal('upsert'),
matchFields: z.array(z.string()).min(1),
}),
});

// all known types and their metadata
private typeMap: Record<string, ModelInfo>;

Expand Down Expand Up @@ -309,8 +316,29 @@ class RequestHandler extends APIHandlerBase {

let match = this.urlPatterns.collection.match(path);
if (match) {
// resource creation
return await this.processCreate(prisma, match.type, query, requestBody, modelMeta, zodSchemas);
const body = requestBody as any;
const upsertMeta = this.upsertMetaSchema.safeParse(body);
if (upsertMeta.success) {
// resource upsert
return await this.processUpsert(
prisma,
match.type,
query,
requestBody,
modelMeta,
zodSchemas
);
} else {
// resource creation
return await this.processCreate(
prisma,
match.type,
query,
requestBody,
modelMeta,
zodSchemas
);
}
}

match = this.urlPatterns.relationship.match(path);
Expand Down Expand Up @@ -809,6 +837,90 @@ class RequestHandler extends APIHandlerBase {
};
}

private async processUpsert(
prisma: DbClientContract,
type: string,
_query: Record<string, string | string[]> | undefined,
requestBody: unknown,
modelMeta: ModelMeta,
zodSchemas?: ZodSchemas
) {
const typeInfo = this.typeMap[type];
if (!typeInfo) {
return this.makeUnsupportedModelError(type);
}

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

if (error) {
return error;
}

const matchFields = this.upsertMetaSchema.parse(requestBody).meta.matchFields;

const uniqueFields = Object.values(modelMeta.models[type].uniqueConstraints || {}).map((uf) => uf.fields);

if (
!uniqueFields.some((uniqueCombination) => uniqueCombination.every((field) => matchFields.includes(field)))
) {
return this.makeError('invalidPayload', 'Match fields must be unique fields', 400);
}

const upsertPayload: any = {
where: this.makeUpsertWhere(matchFields, attributes, typeInfo),
create: { ...attributes },
update: {
...Object.fromEntries(Object.entries(attributes).filter((e) => !matchFields.includes(e[0]))),
},
};

if (relationships) {
for (const [key, data] of Object.entries<any>(relationships)) {
if (!data?.data) {
return this.makeError('invalidRelationData');
}

const relationInfo = typeInfo.relationships[key];
if (!relationInfo) {
return this.makeUnsupportedRelationshipError(type, key, 400);
}

if (relationInfo.isCollection) {
upsertPayload.create[key] = {
connect: enumerate(data.data).map((item: any) =>
this.makeIdConnect(relationInfo.idFields, item.id)
),
};
upsertPayload.update[key] = {
set: enumerate(data.data).map((item: any) =>
this.makeIdConnect(relationInfo.idFields, item.id)
),
};
} else {
if (typeof data.data !== 'object') {
return this.makeError('invalidRelationData');
}
upsertPayload.create[key] = {
connect: this.makeIdConnect(relationInfo.idFields, data.data.id),
};
upsertPayload.update[key] = {
connect: this.makeIdConnect(relationInfo.idFields, data.data.id),
};
}
}
}

// include IDs of relation fields so that they can be serialized.
this.includeRelationshipIds(type, upsertPayload, 'include');

const entity = await prisma[type].upsert(upsertPayload);

return {
status: 201,
body: await this.serializeItems(type, entity),
};
}

private async processRelationshipCRUD(
prisma: DbClientContract,
mode: 'create' | 'update' | 'delete',
Expand Down Expand Up @@ -959,7 +1071,7 @@ class RequestHandler extends APIHandlerBase {
return this.makeError('invalidRelationData');
}
updatePayload.data[key] = {
set: {
connect: {
[this.makePrismaIdKey(relationInfo.idFields)]: data.data.id,
},
};
Expand Down Expand Up @@ -1296,6 +1408,24 @@ class RequestHandler extends APIHandlerBase {
return idFields.map((idf) => item[idf.name]).join(this.idDivider);
}

private makeUpsertWhere(matchFields: any[], attributes: any, typeInfo: ModelInfo) {
const where = matchFields.reduce((acc: any, field: string) => {
acc[field] = attributes[field] ?? null;
return acc;
}, {});

if (
typeInfo.idFields.length > 1 &&
matchFields.some((mf) => typeInfo.idFields.map((idf) => idf.name).includes(mf))
) {
return {
[this.makePrismaIdKey(typeInfo.idFields)]: where,
};
}

return where;
}

private includeRelationshipIds(model: string, args: any, mode: 'select' | 'include') {
const typeInfo = this.typeMap[model];
if (!typeInfo) {
Expand Down
Loading

0 comments on commit 251c699

Please sign in to comment.