Skip to content

Commit

Permalink
fix: issue with connecting multiple relations (#450)
Browse files Browse the repository at this point in the history
  • Loading branch information
ymc9 authored Jun 3, 2023
1 parent 2855647 commit dd6be95
Show file tree
Hide file tree
Showing 24 changed files with 483 additions and 195 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.122",
"version": "1.0.0-alpha.124",
"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.122",
"version": "1.0.0-alpha.124",
"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.122",
"version": "1.0.0-alpha.124",
"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.122",
"version": "1.0.0-alpha.124",
"description": "ZenStack plugin and runtime supporting OpenAPI",
"main": "index.js",
"repository": {
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.122",
"version": "1.0.0-alpha.124",
"description": "ZenStack plugin and runtime for ReactJS",
"main": "index.js",
"repository": {
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": "1.0.0-alpha.122",
"version": "1.0.0-alpha.124",
"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": "1.0.0-alpha.122",
"version": "1.0.0-alpha.124",
"description": "ZenStack plugin for generating tanstack-query hooks",
"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.122",
"version": "1.0.0-alpha.124",
"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": "1.0.0-alpha.122",
"version": "1.0.0-alpha.124",
"description": "Runtime of ZenStack for both client-side and server-side environments.",
"repository": {
"type": "git",
Expand Down
168 changes: 98 additions & 70 deletions packages/runtime/src/enhancements/nested-write-vistor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { FieldInfo, PrismaWriteActionType, PrismaWriteActions } from '../types';
import { resolveField } from './model-meta';
import { ModelMeta } from './types';
import { Enumerable, ensureArray, getModelFields } from './utils';
import { enumerate, getModelFields } from './utils';

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

Expand Down Expand Up @@ -34,33 +34,25 @@ export type VisitorContext = {
export type NestedWriterVisitorCallback = {
create?: (model: string, args: any[], context: VisitorContext) => Promise<void>;

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

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

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

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

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

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

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

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

field?: (field: FieldInfo, action: PrismaWriteActionType, data: any, context: VisitorContext) => Promise<void>;
};
Expand Down Expand Up @@ -115,126 +107,162 @@ export class NestedWriteVisitor {
return;
}

const fieldContainers: any[] = [];
const isToOneUpdate = field?.isDataModel && !field.isArray;
const context = { parent, field, nestingPath: [...nestingPath] };

// visit payload
switch (action) {
case 'create':
context.nestingPath.push({ field, where: {}, unique: false });
if (this.callback.create) {
await this.callback.create(model, data, context);
for (const item of enumerate(data)) {
if (this.callback.create) {
await this.callback.create(model, item, context);
}
await this.visitSubPayload(model, action, item, context.nestingPath);
}
fieldContainers.push(...ensureArray(data));
break;

case 'createMany':
// skip the 'data' layer so as to keep consistency with 'create'
if (data.data) {
context.nestingPath.push({ field, where: {}, unique: false });
if (this.callback.create) {
await this.callback.create(model, data.data, context);
for (const item of enumerate(data.data)) {
if (this.callback.create) {
await this.callback.create(model, item, context);
}
await this.visitSubPayload(model, action, item, context.nestingPath);
}
fieldContainers.push(...ensureArray(data.data));
}
break;

case 'connectOrCreate':
context.nestingPath.push({ field, where: data.where, unique: true });
if (this.callback.connectOrCreate) {
await this.callback.connectOrCreate(model, data, context);
for (const item of enumerate(data)) {
if (this.callback.connectOrCreate) {
await this.callback.connectOrCreate(model, item, context);
}
await this.visitSubPayload(model, action, item.create, context.nestingPath);
}
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);
for (const item of enumerate(data)) {
const newContext = {
...context,
nestingPath: [...context.nestingPath, { field, where: item, unique: true }],
};
await this.callback.connect(model, item, newContext);
}
}
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);
for (const item of enumerate(data)) {
const newContext = {
...context,
nestingPath: [
...context.nestingPath,
{ field, where: item, unique: typeof item === 'object' },
],
};
await this.callback.disconnect(model, item, newContext);
}
}
break;

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

case 'updateMany':
context.nestingPath.push({ field, where: data.where, unique: false });
if (this.callback.updateMany) {
await this.callback.updateMany(model, data, context);
for (const item of enumerate(data)) {
if (this.callback.updateMany) {
await this.callback.updateMany(model, item, context);
}
await this.visitSubPayload(model, action, item, context.nestingPath);
}
fieldContainers.push(...ensureArray(data));
break;

case 'upsert':
case 'upsert': {
context.nestingPath.push({ field, where: data.where, unique: true });
if (this.callback.upsert) {
await this.callback.upsert(model, data, context);
for (const item of enumerate(data)) {
if (this.callback.upsert) {
await this.callback.upsert(model, item, context);
}
await this.visitSubPayload(model, action, item.create, context.nestingPath);
await this.visitSubPayload(model, action, item.update, context.nestingPath);
}
fieldContainers.push(...ensureArray(data).map((d) => d.create));
fieldContainers.push(...ensureArray(data).map((d) => d.update));
break;
}

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

case 'deleteMany':
context.nestingPath.push({ field, where: data.where, unique: false });
if (this.callback.deleteMany) {
await this.callback.deleteMany(model, data, context);
context.nestingPath.push({ field, where: data.where, unique: false });
for (const item of enumerate(data)) {
await this.callback.deleteMany(model, item, context);
}
}
break;

default: {
throw new Error(`unhandled action type ${action}`);
}
}
}

for (const fieldContainer of fieldContainers) {
for (const field of getModelFields(fieldContainer)) {
const fieldInfo = resolveField(this.modelMeta, model, field);
if (!fieldInfo) {
continue;
}
private async visitSubPayload(
model: string,
action: PrismaWriteActionType,
payload: any,
nestingPath: NestingPathItem[]
) {
for (const field of getModelFields(payload)) {
const fieldInfo = resolveField(this.modelMeta, model, field);
if (!fieldInfo) {
continue;
}

if (fieldInfo.isDataModel) {
// recurse into nested payloads
for (const [subAction, subData] of Object.entries<any>(fieldContainer[field])) {
if (this.isPrismaWriteAction(subAction) && subData) {
await this.doVisit(fieldInfo.type, subAction, subData, fieldContainer[field], fieldInfo, [
...context.nestingPath,
]);
}
}
} else {
// visit plain field
if (this.callback.field) {
await this.callback.field(fieldInfo, action, fieldContainer[field], {
parent: fieldContainer,
nestingPath: [...context.nestingPath],
field: fieldInfo,
});
if (fieldInfo.isDataModel) {
// recurse into nested payloads
for (const [subAction, subData] of Object.entries<any>(payload[field])) {
if (this.isPrismaWriteAction(subAction) && subData) {
await this.doVisit(fieldInfo.type, subAction, subData, payload[field], fieldInfo, [
...nestingPath,
]);
}
}
} else {
// visit plain field
if (this.callback.field) {
await this.callback.field(fieldInfo, action, payload[field], {
parent: payload,
nestingPath,
field: fieldInfo,
});
}
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/runtime/src/enhancements/omit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { DbClientContract } from '../types';
import { getDefaultModelMeta, resolveField } from './model-meta';
import { DefaultPrismaProxyHandler, makeProxy } from './proxy';
import { ModelMeta } from './types';
import { ensureArray, getModelFields } from './utils';
import { enumerate, getModelFields } from './utils';

/**
* Gets an enhanced Prisma client that supports @omit attribute.
Expand All @@ -28,7 +28,7 @@ class OmitHandler extends DefaultPrismaProxyHandler {
// base override
protected async processResultEntity<T>(data: T): Promise<T> {
if (data) {
for (const value of ensureArray(data)) {
for (const value of enumerate(data)) {
await this.doPostProcess(value, this.model);
}
}
Expand Down
Loading

0 comments on commit dd6be95

Please sign in to comment.