Skip to content

Commit fc16008

Browse files
authored
fix: nullify field instead of reject when an optional relation field is not readable (#588)
1 parent b0d9154 commit fc16008

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+471
-262
lines changed

packages/plugins/swr/tests/swr.test.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,11 @@ plugin swr {
5050
5151
${sharedModel}
5252
`,
53-
true,
54-
false,
55-
[`${origDir}/dist`, 'react', '@types/react', 'swr'],
56-
true
53+
{
54+
pushDb: false,
55+
extraDependencies: [`${origDir}/dist`, 'react', '@types/react', 'swr'],
56+
compile: true,
57+
}
5758
);
5859
});
5960
});

packages/plugins/tanstack-query/tests/plugin.test.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,16 @@ plugin tanstack {
5151
5252
${sharedModel}
5353
`,
54-
true,
55-
false,
56-
[`${origDir}/dist`, 'react@18.2.0', '@types/react@18.2.0', '@tanstack/react-query@4.29.7'],
57-
true
54+
{
55+
pushDb: false,
56+
extraDependencies: [
57+
`${origDir}/dist`,
58+
'react@18.2.0',
59+
'@types/react@18.2.0',
60+
'@tanstack/react-query@4.29.7',
61+
],
62+
compile: true,
63+
}
5864
);
5965
});
6066

@@ -69,10 +75,11 @@ plugin tanstack {
6975
7076
${sharedModel}
7177
`,
72-
true,
73-
false,
74-
[`${origDir}/dist`, 'svelte@^3.0.0', '@tanstack/svelte-query@4.29.7'],
75-
true
78+
{
79+
pushDb: false,
80+
extraDependencies: [`${origDir}/dist`, 'svelte@^3.0.0', '@tanstack/svelte-query@4.29.7'],
81+
compile: true,
82+
}
7683
);
7784
});
7885
});

packages/plugins/trpc/tests/trpc.test.ts

Lines changed: 45 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,12 @@ model Foo {
4848
@@ignore
4949
}
5050
`,
51-
true,
52-
false,
53-
[`${origDir}/dist`, '@trpc/client', '@trpc/server'],
54-
true
51+
{
52+
pushDb: false,
53+
extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server'],
54+
compile: true,
55+
fullZod: true,
56+
}
5557
);
5658
});
5759

@@ -88,10 +90,12 @@ model Foo {
8890
@@ignore
8991
}
9092
`,
91-
true,
92-
false,
93-
[`${origDir}/dist`, '@trpc/client', '@trpc/server'],
94-
true
93+
{
94+
pushDb: false,
95+
extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server'],
96+
compile: true,
97+
fullZod: true,
98+
}
9599
);
96100
expect(fs.existsSync(path.join(projectDir, 'trpc'))).toBe(true);
97101
});
@@ -116,11 +120,13 @@ model Post {
116120
authorId String?
117121
}
118122
`,
119-
true,
120-
false,
121-
[`${origDir}/dist`, '@trpc/client', '@trpc/server'],
122-
true,
123-
'zenstack/schema.zmodel'
123+
{
124+
pushDb: false,
125+
extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server'],
126+
compile: true,
127+
fullZod: true,
128+
customSchemaFilePath: 'zenstack/schema.zmodel',
129+
}
124130
);
125131
expect(fs.existsSync(path.join(projectDir, 'zenstack/trpc'))).toBe(true);
126132
});
@@ -139,11 +145,13 @@ model Post {
139145
title String
140146
}
141147
`,
142-
true,
143-
false,
144-
[`${origDir}/dist`, '@trpc/client', '@trpc/server'],
145-
true,
146-
'zenstack/schema.zmodel'
148+
{
149+
pushDb: false,
150+
extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server'],
151+
compile: true,
152+
fullZod: true,
153+
customSchemaFilePath: 'zenstack/schema.zmodel',
154+
}
147155
);
148156
const content = fs.readFileSync(path.join(projectDir, 'zenstack/trpc/routers/Post.router.ts'), 'utf-8');
149157
expect(content).toContain('findMany:');
@@ -167,11 +175,13 @@ model Post {
167175
title String
168176
}
169177
`,
170-
true,
171-
false,
172-
[`${origDir}/dist`, '@trpc/client', '@trpc/server'],
173-
true,
174-
'zenstack/schema.zmodel'
178+
{
179+
pushDb: false,
180+
extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server'],
181+
compile: true,
182+
fullZod: true,
183+
customSchemaFilePath: 'zenstack/schema.zmodel',
184+
}
175185
);
176186
const content = fs.readFileSync(path.join(projectDir, 'zenstack/trpc/routers/Post.router.ts'), 'utf-8');
177187
expect(content).toContain('findMany:');
@@ -211,10 +221,12 @@ model Post {
211221
212222
${BLOG_BASE_SCHEMA}
213223
`,
214-
true,
215-
false,
216-
[`${origDir}/dist`, '@trpc/client', '@trpc/server', '@trpc/react-query'],
217-
true
224+
{
225+
pushDb: false,
226+
extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server', '@trpc/react-query'],
227+
compile: true,
228+
fullZod: true,
229+
}
218230
);
219231
});
220232

@@ -229,10 +241,12 @@ model Post {
229241
230242
${BLOG_BASE_SCHEMA}
231243
`,
232-
true,
233-
false,
234-
[`${origDir}/dist`, '@trpc/client', '@trpc/server', '@trpc/next'],
235-
true
244+
{
245+
pushDb: false,
246+
extraDependencies: [`${origDir}/dist`, '@trpc/client', '@trpc/server', '@trpc/next'],
247+
compile: true,
248+
fullZod: true,
249+
}
236250
);
237251
});
238252
});

packages/runtime/src/constants.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,23 @@ export const GUARD_FIELD_NAME = 'zenstack_guard';
1919
export const AUXILIARY_FIELDS = [TRANSACTION_FIELD_NAME, GUARD_FIELD_NAME];
2020

2121
/**
22-
* Reasons for a CRUD operation to fail.
22+
* Reasons for a CRUD operation to fail
2323
*/
2424
export enum CrudFailureReason {
2525
/**
2626
* CRUD suceeded but the result was not readable.
2727
*/
2828
RESULT_NOT_READABLE = 'RESULT_NOT_READABLE',
29+
30+
/**
31+
* CRUD failed because of a data validation rule violation.
32+
*/
33+
DATA_VALIDATION_VIOLATION = 'DATA_VALIDATION_VIOLATION',
34+
}
35+
36+
/**
37+
* Prisma error codes used
38+
*/
39+
export enum PrismaErrorCode {
40+
CONSTRAINED_FAILED = 'P2004',
2941
}

packages/runtime/src/enhancements/policy/policy-utils.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,14 @@ import { lowerCaseFirst } from 'lower-case-first';
66
import pluralize from 'pluralize';
77
import { upperCaseFirst } from 'upper-case-first';
88
import { fromZodError } from 'zod-validation-error';
9-
import { AUXILIARY_FIELDS, CrudFailureReason, GUARD_FIELD_NAME, TRANSACTION_FIELD_NAME } from '../../constants';
9+
import {
10+
AUXILIARY_FIELDS,
11+
CrudFailureReason,
12+
GUARD_FIELD_NAME,
13+
PrismaErrorCode,
14+
TRANSACTION_FIELD_NAME,
15+
} from '../../constants';
16+
import { isPrismaClientKnownRequestError } from '../../error';
1017
import {
1118
AuthUser,
1219
DbClientContract,
@@ -402,7 +409,26 @@ export class PolicyUtil {
402409
// `Validating read of to-one relation: ${fieldInfo.type}#${formatObject(ids)}`
403410
// );
404411
// }
405-
await this.checkPolicyForFilter(fieldInfo.type, ids, operation, this.db);
412+
try {
413+
await this.checkPolicyForFilter(fieldInfo.type, ids, operation, this.db);
414+
} catch (err) {
415+
if (
416+
isPrismaClientKnownRequestError(err) &&
417+
err.code === PrismaErrorCode.CONSTRAINED_FAILED
418+
) {
419+
// denied by policy
420+
if (fieldInfo.isOptional) {
421+
// if the relation is optional, just nullify it
422+
entityData[field] = null;
423+
} else {
424+
// otherwise reject
425+
throw err;
426+
}
427+
} else {
428+
// unknown error
429+
throw err;
430+
}
431+
}
406432
}
407433
}
408434

@@ -772,7 +798,7 @@ export class PolicyUtil {
772798
return prismaClientKnownRequestError(
773799
this.db,
774800
`denied by policy: ${model} entities failed '${operation}' check${extra ? ', ' + extra : ''}`,
775-
{ clientVersion: getVersion(), code: 'P2004', meta: { reason } }
801+
{ clientVersion: getVersion(), code: PrismaErrorCode.CONSTRAINED_FAILED, meta: { reason } }
776802
);
777803
}
778804

@@ -864,7 +890,12 @@ export class PolicyUtil {
864890
if (this.logger.enabled('info')) {
865891
this.logger.info(`entity ${model} failed schema check for operation ${operation}: ${error}`);
866892
}
867-
throw this.deniedByPolicy(model, operation, `entities failed schema check: [${error}]`);
893+
throw this.deniedByPolicy(
894+
model,
895+
operation,
896+
`entities failed schema check: [${error}]`,
897+
CrudFailureReason.DATA_VALIDATION_VIOLATION
898+
);
868899
}
869900
} else {
870901
// count entities with policy injected and see if any of them are filtered out

packages/schema/src/cli/plugin-runner.ts

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -99,38 +99,49 @@ export class PluginRunner {
9999
}
100100

101101
// make sure prerequisites are included
102-
const corePlugins = ['@core/prisma', '@core/model-meta', '@core/access-policy'];
102+
const corePlugins: Array<{ provider: string; options?: Record<string, unknown> }> = [
103+
{ provider: '@core/prisma' },
104+
{ provider: '@core/model-meta' },
105+
{ provider: '@core/access-policy' },
106+
];
103107

104108
if (getDataModels(context.schema).some((model) => hasValidationAttributes(model))) {
105109
// '@core/zod' plugin is auto-enabled if there're validation rules
106-
corePlugins.push('@core/zod');
110+
corePlugins.push({ provider: '@core/zod', options: { modelOnly: true } });
107111
}
108112

109-
// core dependencies introduced by dependencies
113+
// core plugins introduced by dependencies
110114
plugins
111115
.flatMap((p) => p.dependencies)
112116
.forEach((dep) => {
113-
if (dep.startsWith('@core/') && !corePlugins.includes(dep)) {
114-
corePlugins.push(dep);
117+
if (dep.startsWith('@core/')) {
118+
const existing = corePlugins.find((p) => p.provider === dep);
119+
if (existing) {
120+
// reset options to default
121+
existing.options = undefined;
122+
} else {
123+
// add core dependency
124+
corePlugins.push({ provider: dep });
125+
}
115126
}
116127
});
117128

118129
for (const corePlugin of corePlugins.reverse()) {
119-
const existingIdx = plugins.findIndex((p) => p.provider === corePlugin);
130+
const existingIdx = plugins.findIndex((p) => p.provider === corePlugin.provider);
120131
if (existingIdx >= 0) {
121132
// shift the plugin to the front
122133
const existing = plugins[existingIdx];
123134
plugins.splice(existingIdx, 1);
124135
plugins.unshift(existing);
125136
} else {
126137
// synthesize a plugin and insert front
127-
const pluginModule = require(this.getPluginModulePath(corePlugin));
128-
const pluginName = this.getPluginName(pluginModule, corePlugin);
138+
const pluginModule = require(this.getPluginModulePath(corePlugin.provider));
139+
const pluginName = this.getPluginName(pluginModule, corePlugin.provider);
129140
plugins.unshift({
130141
name: pluginName,
131-
provider: corePlugin,
142+
provider: corePlugin.provider,
132143
dependencies: [],
133-
options: { schemaPath: context.schemaPath, name: pluginName },
144+
options: { schemaPath: context.schemaPath, name: pluginName, ...corePlugin.options },
134145
run: pluginModule.default,
135146
module: pluginModule,
136147
});
@@ -154,7 +165,9 @@ export class PluginRunner {
154165

155166
let dmmf: DMMF.Document | undefined = undefined;
156167
for (const { name, provider, run, options } of plugins) {
168+
// const start = Date.now();
157169
await this.runPlugin(name, run, context, options, dmmf, warnings);
170+
// console.log(`✅ Plugin ${colors.bold(name)} (${provider}) completed in ${Date.now() - start}ms`);
158171
if (provider === '@core/prisma') {
159172
// load prisma DMMF
160173
dmmf = await getDMMF({

packages/schema/src/plugins/prisma/schema-generator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import {
2424
analyzePolicies,
2525
getDataModels,
26+
getDMMF,
2627
getLiteral,
2728
getLiteralArray,
2829
getPrismaVersion,
@@ -32,7 +33,6 @@ import {
3233
resolved,
3334
resolvePath,
3435
TRANSACTION_FIELD_NAME,
35-
getDMMF
3636
} from '@zenstackhq/sdk';
3737
import fs from 'fs';
3838
import { writeFile } from 'fs/promises';

0 commit comments

Comments
 (0)