Skip to content

Commit e1600c8

Browse files
authored
fix: batch bug fixes (#273)
1 parent e12fc5a commit e1600c8

File tree

22 files changed

+489
-65
lines changed

22 files changed

+489
-65
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": "1.0.0-alpha.74",
3+
"version": "1.0.0-alpha.78",
44
"description": "",
55
"scripts": {
66
"build": "pnpm -r build",

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": "1.0.0-alpha.74",
3+
"version": "1.0.0-alpha.78",
44
"displayName": "ZenStack modeling language compiler",
55
"description": "ZenStack modeling language compiler",
66
"homepage": "https://zenstack.dev",

packages/next/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zenstackhq/next",
3-
"version": "1.0.0-alpha.74",
3+
"version": "1.0.0-alpha.78",
44
"displayName": "ZenStack Next.js integration",
55
"description": "ZenStack Next.js integration",
66
"homepage": "https://zenstack.dev",

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": "1.0.0-alpha.74",
4+
"version": "1.0.0-alpha.78",
55
"description": "ZenStack plugin and runtime supporting OpenAPI",
66
"main": "index.js",
77
"repository": {

packages/plugins/openapi/src/generator.ts

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export class OpenAPIGenerator {
2727
private outputObjectTypes: DMMF.OutputType[] = [];
2828
private usedComponents: Set<string> = new Set<string>();
2929
private aggregateOperationSupport: AggregateOperationSupport;
30+
private includedModels: DataModel[];
3031

3132
constructor(private model: Model, private options: PluginOptions, private dmmf: DMMF.Document) {}
3233

@@ -39,6 +40,9 @@ export class OpenAPIGenerator {
3940
// input types
4041
this.inputObjectTypes.push(...this.dmmf.schema.inputObjectTypes.prisma);
4142
this.outputObjectTypes.push(...this.dmmf.schema.outputObjectTypes.prisma);
43+
this.includedModels = this.model.declarations.filter(
44+
(d): d is DataModel => isDataModel(d) && !hasAttribute(d, '@@openapi.ignore')
45+
);
4246

4347
// add input object types that are missing from Prisma dmmf
4448
addMissingInputObjectTypesForModelArgs(this.inputObjectTypes, this.dmmf.datamodel.models);
@@ -59,7 +63,13 @@ export class OpenAPIGenerator {
5963
info: {
6064
title: this.getOption('title', 'ZenStack Generated API'),
6165
version: this.getOption('version', '1.0.0'),
66+
description: this.getOption('description', undefined),
67+
summary: this.getOption('summary', undefined),
6268
},
69+
tags: this.includedModels.map((model) => ({
70+
name: camelCase(model.name),
71+
description: `${model.name} operations`,
72+
})),
6373
components,
6474
paths,
6575
};
@@ -125,12 +135,10 @@ export class OpenAPIGenerator {
125135
private generatePaths(components: OAPI.ComponentsObject): OAPI.PathsObject {
126136
let result: OAPI.PathsObject = {};
127137

128-
const includeModels = this.model.declarations
129-
.filter((d) => isDataModel(d) && !hasAttribute(d, '@@openapi.ignore'))
130-
.map((d) => d.name);
138+
const includeModelNames = this.includedModels.map((d) => d.name);
131139

132140
for (const model of this.dmmf.datamodel.models) {
133-
if (includeModels.includes(model.name)) {
141+
if (includeModelNames.includes(model.name)) {
134142
const zmodel = this.model.declarations.find(
135143
(d) => isDataModel(d) && d.name === model.name
136144
) as DataModel;
@@ -465,11 +473,16 @@ export class OpenAPIGenerator {
465473
resolvedPath = resolvedPath.substring(1);
466474
}
467475

476+
let prefix = this.getOption('prefix', '');
477+
if (prefix.endsWith('/')) {
478+
prefix = prefix.substring(0, prefix.length - 1);
479+
}
480+
468481
// eslint-disable-next-line @typescript-eslint/no-explicit-any
469482
const def: any = {
470483
operationId: `${operation}${model.name}`,
471484
description: meta?.description ?? description,
472-
tags: meta?.tags,
485+
tags: meta?.tags || [camelCase(model.name)],
473486
summary: meta?.summary,
474487
responses: {
475488
[successCode !== undefined ? successCode : '200']: {
@@ -504,13 +517,17 @@ export class OpenAPIGenerator {
504517
name: 'q',
505518
in: 'query',
506519
required: true,
507-
schema: inputType,
520+
content: {
521+
'application/json': {
522+
schema: inputType,
523+
},
524+
},
508525
},
509526
] satisfies OAPI.ParameterObject[];
510527
}
511528
}
512529

513-
result[`/${camelCase(model.name)}/${resolvedPath}`] = {
530+
result[`${prefix}/${camelCase(model.name)}/${resolvedPath}`] = {
514531
[resolvedMethod]: def,
515532
};
516533
}
@@ -546,8 +563,14 @@ export class OpenAPIGenerator {
546563
return this.ref(name);
547564
}
548565

549-
private getOption(name: string, defaultValue: string) {
550-
return this.options[name] ? (this.options[name] as string) : defaultValue;
566+
private getOption<T extends string | undefined>(
567+
name: string,
568+
defaultValue: T
569+
): T extends string ? string : string | undefined {
570+
const value = this.options[name];
571+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
572+
// @ts-expect-error
573+
return typeof value === 'string' ? value : defaultValue;
551574
}
552575

553576
private generateComponents() {

packages/plugins/react/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@zenstackhq/react",
33
"displayName": "ZenStack plugin and runtime for ReactJS",
4-
"version": "1.0.0-alpha.74",
4+
"version": "1.0.0-alpha.78",
55
"description": "ZenStack plugin and runtime for ReactJS",
66
"main": "index.js",
77
"repository": {

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": "1.0.0-alpha.74",
4+
"version": "1.0.0-alpha.78",
55
"description": "ZenStack plugin for tRPC",
66
"main": "index.js",
77
"repository": {

packages/runtime/package.json

Lines changed: 3 additions & 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": "1.0.0-alpha.74",
4+
"version": "1.0.0-alpha.78",
55
"description": "Runtime of ZenStack for both client-side and server-side environments.",
66
"repository": {
77
"type": "git",
@@ -28,6 +28,7 @@
2828
"cuid": "^2.1.8",
2929
"decimal.js": "^10.4.2",
3030
"deepcopy": "^2.1.0",
31+
"pluralize": "^8.0.0",
3132
"superjson": "^1.11.0",
3233
"tslib": "^2.4.1",
3334
"zod": "^3.19.1",
@@ -45,6 +46,7 @@
4546
"@types/bcryptjs": "^2.4.2",
4647
"@types/jest": "^29.0.3",
4748
"@types/node": "^14.18.29",
49+
"@types/pluralize": "^0.0.29",
4850
"copyfiles": "^2.4.1",
4951
"rimraf": "^3.0.2",
5052
"typescript": "^4.9.3"

packages/runtime/src/enhancements/nested-write-vistor.ts

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { resolveField } from './model-meta';
66
import { ModelMeta } from './types';
77
import { Enumerable, ensureArray, getModelFields } from './utils';
88

9+
type NestingPathItem = { field?: FieldInfo; where: any; unique: boolean };
10+
911
/**
1012
* Context for visiting
1113
*/
@@ -23,7 +25,7 @@ export type VisitorContext = {
2325
/**
2426
* A top-down path of all nested update conditions and corresponding field till now
2527
*/
26-
nestingPath: { field?: FieldInfo; where: any }[];
28+
nestingPath: NestingPathItem[];
2729
};
2830

2931
/**
@@ -38,6 +40,10 @@ export type NestedWriterVisitorCallback = {
3840
context: VisitorContext
3941
) => Promise<void>;
4042

43+
connect?: (model: string, args: Enumerable<object>, context: VisitorContext) => Promise<void>;
44+
45+
disconnect?: (model: string, args: Enumerable<object>, context: VisitorContext) => Promise<void>;
46+
4147
update?: (model: string, args: Enumerable<{ where: object; data: any }>, context: VisitorContext) => Promise<void>;
4248

4349
updateMany?: (
@@ -103,7 +109,7 @@ export class NestedWriteVisitor {
103109
data: any,
104110
parent: any,
105111
field: FieldInfo | undefined,
106-
nestingPath: { field?: FieldInfo; where: any }[]
112+
nestingPath: NestingPathItem[]
107113
): Promise<void> {
108114
if (!data) {
109115
return;
@@ -116,7 +122,7 @@ export class NestedWriteVisitor {
116122
// visit payload
117123
switch (action) {
118124
case 'create':
119-
context.nestingPath.push({ field, where: {} });
125+
context.nestingPath.push({ field, where: {}, unique: false });
120126
if (this.callback.create) {
121127
await this.callback.create(model, data, context);
122128
}
@@ -126,7 +132,7 @@ export class NestedWriteVisitor {
126132
case 'createMany':
127133
// skip the 'data' layer so as to keep consistency with 'create'
128134
if (data.data) {
129-
context.nestingPath.push({ field, where: {} });
135+
context.nestingPath.push({ field, where: {}, unique: false });
130136
if (this.callback.create) {
131137
await this.callback.create(model, data.data, context);
132138
}
@@ -135,31 +141,48 @@ export class NestedWriteVisitor {
135141
break;
136142

137143
case 'connectOrCreate':
138-
context.nestingPath.push({ field, where: data.where });
144+
context.nestingPath.push({ field, where: data.where, unique: true });
139145
if (this.callback.connectOrCreate) {
140146
await this.callback.connectOrCreate(model, data, context);
141147
}
142148
fieldContainers.push(...ensureArray(data).map((d) => d.create));
143149
break;
144150

151+
case 'connect':
152+
context.nestingPath.push({ field, where: data, unique: true });
153+
if (this.callback.connect) {
154+
await this.callback.connect(model, data, context);
155+
}
156+
break;
157+
158+
case 'disconnect':
159+
// disconnect has two forms:
160+
// if relation is to-many, the payload is a unique filter object
161+
// if relation is to-one, the payload can only be boolean `true`
162+
context.nestingPath.push({ field, where: data, unique: typeof data === 'object' });
163+
if (this.callback.disconnect) {
164+
await this.callback.disconnect(model, data, context);
165+
}
166+
break;
167+
145168
case 'update':
146-
context.nestingPath.push({ field, where: data.where });
169+
context.nestingPath.push({ field, where: data.where, unique: false });
147170
if (this.callback.update) {
148171
await this.callback.update(model, data, context);
149172
}
150173
fieldContainers.push(...ensureArray(data).map((d) => (isToOneUpdate ? d : d.data)));
151174
break;
152175

153176
case 'updateMany':
154-
context.nestingPath.push({ field, where: data.where });
177+
context.nestingPath.push({ field, where: data.where, unique: false });
155178
if (this.callback.updateMany) {
156179
await this.callback.updateMany(model, data, context);
157180
}
158181
fieldContainers.push(...ensureArray(data));
159182
break;
160183

161184
case 'upsert':
162-
context.nestingPath.push({ field, where: data.where });
185+
context.nestingPath.push({ field, where: data.where, unique: true });
163186
if (this.callback.upsert) {
164187
await this.callback.upsert(model, data, context);
165188
}
@@ -168,14 +191,14 @@ export class NestedWriteVisitor {
168191
break;
169192

170193
case 'delete':
171-
context.nestingPath.push({ field, where: data.where });
194+
context.nestingPath.push({ field, where: data.where, unique: false });
172195
if (this.callback.delete) {
173196
await this.callback.delete(model, data, context);
174197
}
175198
break;
176199

177200
case 'deleteMany':
178-
context.nestingPath.push({ field, where: data.where });
201+
context.nestingPath.push({ field, where: data.where, unique: false });
179202
if (this.callback.deleteMany) {
180203
await this.callback.deleteMany(model, data, context);
181204
}

packages/runtime/src/enhancements/policy/handler.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22

33
import { PrismaClientValidationError } from '@prisma/client/runtime';
4+
import { CrudFailureReason } from '@zenstackhq/sdk';
45
import { AuthUser, DbClientContract, PolicyOperationKind } from '../../types';
56
import { BatchResult, PrismaProxyHandler } from '../proxy';
67
import { ModelMeta, PolicyDef } from '../types';
@@ -227,7 +228,12 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
227228
await this.modelClient.delete(args);
228229

229230
if (!readResult) {
230-
throw this.utils.deniedByPolicy(this.model, 'delete', 'result not readable');
231+
throw this.utils.deniedByPolicy(
232+
this.model,
233+
'delete',
234+
'result is not allowed to be read back',
235+
CrudFailureReason.RESULT_NOT_READABLE
236+
);
231237
} else {
232238
return readResult;
233239
}
@@ -296,7 +302,12 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
296302
const result = await this.utils.readWithCheck(this.model, readArgs);
297303
if (result.length === 0) {
298304
this.logger.warn(`${action} result cannot be read back`);
299-
throw this.utils.deniedByPolicy(this.model, operation, 'result is not allowed to be read back');
305+
throw this.utils.deniedByPolicy(
306+
this.model,
307+
operation,
308+
'result is not allowed to be read back',
309+
CrudFailureReason.RESULT_NOT_READABLE
310+
);
300311
} else if (result.length > 1) {
301312
throw this.utils.unknownError('write unexpected resulted in multiple readback entities');
302313
}

0 commit comments

Comments
 (0)