Skip to content

Commit

Permalink
fix: batch bug fixes (zenstackhq#273)
Browse files Browse the repository at this point in the history
  • Loading branch information
ymc9 authored Mar 19, 2023
1 parent e12fc5a commit e1600c8
Show file tree
Hide file tree
Showing 22 changed files with 489 additions and 65 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": "1.0.0-alpha.74",
"version": "1.0.0-alpha.78",
"description": "",
"scripts": {
"build": "pnpm -r build",
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": "1.0.0-alpha.74",
"version": "1.0.0-alpha.78",
"displayName": "ZenStack modeling language compiler",
"description": "ZenStack modeling language compiler",
"homepage": "https://zenstack.dev",
Expand Down
2 changes: 1 addition & 1 deletion packages/next/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenstackhq/next",
"version": "1.0.0-alpha.74",
"version": "1.0.0-alpha.78",
"displayName": "ZenStack Next.js integration",
"description": "ZenStack Next.js integration",
"homepage": "https://zenstack.dev",
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": "1.0.0-alpha.74",
"version": "1.0.0-alpha.78",
"description": "ZenStack plugin and runtime supporting OpenAPI",
"main": "index.js",
"repository": {
Expand Down
41 changes: 32 additions & 9 deletions packages/plugins/openapi/src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class OpenAPIGenerator {
private outputObjectTypes: DMMF.OutputType[] = [];
private usedComponents: Set<string> = new Set<string>();
private aggregateOperationSupport: AggregateOperationSupport;
private includedModels: DataModel[];

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

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

// add input object types that are missing from Prisma dmmf
addMissingInputObjectTypesForModelArgs(this.inputObjectTypes, this.dmmf.datamodel.models);
Expand All @@ -59,7 +63,13 @@ export class OpenAPIGenerator {
info: {
title: this.getOption('title', 'ZenStack Generated API'),
version: this.getOption('version', '1.0.0'),
description: this.getOption('description', undefined),
summary: this.getOption('summary', undefined),
},
tags: this.includedModels.map((model) => ({
name: camelCase(model.name),
description: `${model.name} operations`,
})),
components,
paths,
};
Expand Down Expand Up @@ -125,12 +135,10 @@ export class OpenAPIGenerator {
private generatePaths(components: OAPI.ComponentsObject): OAPI.PathsObject {
let result: OAPI.PathsObject = {};

const includeModels = this.model.declarations
.filter((d) => isDataModel(d) && !hasAttribute(d, '@@openapi.ignore'))
.map((d) => d.name);
const includeModelNames = this.includedModels.map((d) => d.name);

for (const model of this.dmmf.datamodel.models) {
if (includeModels.includes(model.name)) {
if (includeModelNames.includes(model.name)) {
const zmodel = this.model.declarations.find(
(d) => isDataModel(d) && d.name === model.name
) as DataModel;
Expand Down Expand Up @@ -465,11 +473,16 @@ export class OpenAPIGenerator {
resolvedPath = resolvedPath.substring(1);
}

let prefix = this.getOption('prefix', '');
if (prefix.endsWith('/')) {
prefix = prefix.substring(0, prefix.length - 1);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const def: any = {
operationId: `${operation}${model.name}`,
description: meta?.description ?? description,
tags: meta?.tags,
tags: meta?.tags || [camelCase(model.name)],
summary: meta?.summary,
responses: {
[successCode !== undefined ? successCode : '200']: {
Expand Down Expand Up @@ -504,13 +517,17 @@ export class OpenAPIGenerator {
name: 'q',
in: 'query',
required: true,
schema: inputType,
content: {
'application/json': {
schema: inputType,
},
},
},
] satisfies OAPI.ParameterObject[];
}
}

result[`/${camelCase(model.name)}/${resolvedPath}`] = {
result[`${prefix}/${camelCase(model.name)}/${resolvedPath}`] = {
[resolvedMethod]: def,
};
}
Expand Down Expand Up @@ -546,8 +563,14 @@ export class OpenAPIGenerator {
return this.ref(name);
}

private getOption(name: string, defaultValue: string) {
return this.options[name] ? (this.options[name] as string) : defaultValue;
private getOption<T extends string | undefined>(
name: string,
defaultValue: T
): T extends string ? string : string | undefined {
const value = this.options[name];
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
return typeof value === 'string' ? value : defaultValue;
}

private generateComponents() {
Expand Down
2 changes: 1 addition & 1 deletion packages/plugins/react/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@zenstackhq/react",
"displayName": "ZenStack plugin and runtime for ReactJS",
"version": "1.0.0-alpha.74",
"version": "1.0.0-alpha.78",
"description": "ZenStack plugin and runtime for ReactJS",
"main": "index.js",
"repository": {
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": "1.0.0-alpha.74",
"version": "1.0.0-alpha.78",
"description": "ZenStack plugin for tRPC",
"main": "index.js",
"repository": {
Expand Down
4 changes: 3 additions & 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": "1.0.0-alpha.74",
"version": "1.0.0-alpha.78",
"description": "Runtime of ZenStack for both client-side and server-side environments.",
"repository": {
"type": "git",
Expand All @@ -28,6 +28,7 @@
"cuid": "^2.1.8",
"decimal.js": "^10.4.2",
"deepcopy": "^2.1.0",
"pluralize": "^8.0.0",
"superjson": "^1.11.0",
"tslib": "^2.4.1",
"zod": "^3.19.1",
Expand All @@ -45,6 +46,7 @@
"@types/bcryptjs": "^2.4.2",
"@types/jest": "^29.0.3",
"@types/node": "^14.18.29",
"@types/pluralize": "^0.0.29",
"copyfiles": "^2.4.1",
"rimraf": "^3.0.2",
"typescript": "^4.9.3"
Expand Down
43 changes: 33 additions & 10 deletions packages/runtime/src/enhancements/nested-write-vistor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { resolveField } from './model-meta';
import { ModelMeta } from './types';
import { Enumerable, ensureArray, getModelFields } from './utils';

type NestingPathItem = { field?: FieldInfo; where: any; unique: boolean };

/**
* Context for visiting
*/
Expand All @@ -23,7 +25,7 @@ export type VisitorContext = {
/**
* A top-down path of all nested update conditions and corresponding field till now
*/
nestingPath: { field?: FieldInfo; where: any }[];
nestingPath: NestingPathItem[];
};

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

connect?: (model: string, args: Enumerable<object>, context: VisitorContext) => Promise<void>;

disconnect?: (model: string, args: Enumerable<object>, context: VisitorContext) => Promise<void>;

update?: (model: string, args: Enumerable<{ where: object; data: any }>, context: VisitorContext) => Promise<void>;

updateMany?: (
Expand Down Expand Up @@ -103,7 +109,7 @@ export class NestedWriteVisitor {
data: any,
parent: any,
field: FieldInfo | undefined,
nestingPath: { field?: FieldInfo; where: any }[]
nestingPath: NestingPathItem[]
): Promise<void> {
if (!data) {
return;
Expand All @@ -116,7 +122,7 @@ export class NestedWriteVisitor {
// visit payload
switch (action) {
case 'create':
context.nestingPath.push({ field, where: {} });
context.nestingPath.push({ field, where: {}, unique: false });
if (this.callback.create) {
await this.callback.create(model, data, context);
}
Expand All @@ -126,7 +132,7 @@ export class NestedWriteVisitor {
case 'createMany':
// skip the 'data' layer so as to keep consistency with 'create'
if (data.data) {
context.nestingPath.push({ field, where: {} });
context.nestingPath.push({ field, where: {}, unique: false });
if (this.callback.create) {
await this.callback.create(model, data.data, context);
}
Expand All @@ -135,31 +141,48 @@ export class NestedWriteVisitor {
break;

case 'connectOrCreate':
context.nestingPath.push({ field, where: data.where });
context.nestingPath.push({ field, where: data.where, unique: true });
if (this.callback.connectOrCreate) {
await this.callback.connectOrCreate(model, data, context);
}
fieldContainers.push(...ensureArray(data).map((d) => d.create));
break;

case 'connect':
context.nestingPath.push({ field, where: data, unique: true });
if (this.callback.connect) {
await this.callback.connect(model, data, context);
}
break;

case 'disconnect':
// disconnect has two forms:
// if relation is to-many, the payload is a unique filter object
// if relation is to-one, the payload can only be boolean `true`
context.nestingPath.push({ field, where: data, unique: typeof data === 'object' });
if (this.callback.disconnect) {
await this.callback.disconnect(model, data, context);
}
break;

case 'update':
context.nestingPath.push({ field, where: data.where });
context.nestingPath.push({ field, where: data.where, unique: false });
if (this.callback.update) {
await this.callback.update(model, data, context);
}
fieldContainers.push(...ensureArray(data).map((d) => (isToOneUpdate ? d : d.data)));
break;

case 'updateMany':
context.nestingPath.push({ field, where: data.where });
context.nestingPath.push({ field, where: data.where, unique: false });
if (this.callback.updateMany) {
await this.callback.updateMany(model, data, context);
}
fieldContainers.push(...ensureArray(data));
break;

case 'upsert':
context.nestingPath.push({ field, where: data.where });
context.nestingPath.push({ field, where: data.where, unique: true });
if (this.callback.upsert) {
await this.callback.upsert(model, data, context);
}
Expand All @@ -168,14 +191,14 @@ export class NestedWriteVisitor {
break;

case 'delete':
context.nestingPath.push({ field, where: data.where });
context.nestingPath.push({ field, where: data.where, unique: false });
if (this.callback.delete) {
await this.callback.delete(model, data, context);
}
break;

case 'deleteMany':
context.nestingPath.push({ field, where: data.where });
context.nestingPath.push({ field, where: data.where, unique: false });
if (this.callback.deleteMany) {
await this.callback.deleteMany(model, data, context);
}
Expand Down
15 changes: 13 additions & 2 deletions packages/runtime/src/enhancements/policy/handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

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

if (!readResult) {
throw this.utils.deniedByPolicy(this.model, 'delete', 'result not readable');
throw this.utils.deniedByPolicy(
this.model,
'delete',
'result is not allowed to be read back',
CrudFailureReason.RESULT_NOT_READABLE
);
} else {
return readResult;
}
Expand Down Expand Up @@ -296,7 +302,12 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
const result = await this.utils.readWithCheck(this.model, readArgs);
if (result.length === 0) {
this.logger.warn(`${action} result cannot be read back`);
throw this.utils.deniedByPolicy(this.model, operation, 'result is not allowed to be read back');
throw this.utils.deniedByPolicy(
this.model,
operation,
'result is not allowed to be read back',
CrudFailureReason.RESULT_NOT_READABLE
);
} else if (result.length > 1) {
throw this.utils.unknownError('write unexpected resulted in multiple readback entities');
}
Expand Down
Loading

0 comments on commit e1600c8

Please sign in to comment.