Skip to content

Commit c219f6d

Browse files
committed
fix(delegate): @omit and field-level policies are not enforced when querying with a delegate model
Fixes #1710
1 parent f2a3686 commit c219f6d

File tree

11 files changed

+624
-3
lines changed

11 files changed

+624
-3
lines changed

packages/runtime/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
"semver": "^7.5.2",
9999
"superjson": "^1.13.0",
100100
"tiny-invariant": "^1.3.1",
101+
"traverse": "^0.6.10",
101102
"ts-pattern": "^4.3.0",
102103
"tslib": "^2.4.1",
103104
"upper-case-first": "^2.0.2",
@@ -118,6 +119,7 @@
118119
"@types/pluralize": "^0.0.29",
119120
"@types/safe-json-stringify": "^1.1.5",
120121
"@types/semver": "^7.3.13",
122+
"@types/traverse": "^0.6.37",
121123
"@types/uuid": "^8.3.4"
122124
}
123125
}

packages/runtime/src/enhancements/node/delegate.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import deepmerge, { type ArrayMergeOptions } from 'deepmerge';
33
import { isPlainObject } from 'is-plain-object';
44
import { lowerCaseFirst } from 'lower-case-first';
5+
import traverse from 'traverse';
56
import { DELEGATE_AUX_RELATION_PREFIX } from '../../constants';
67
import {
78
FieldInfo,
@@ -77,6 +78,10 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
7778
this.injectWhereHierarchy(model, args?.where);
7879
this.injectSelectIncludeHierarchy(model, args);
7980

81+
// discriminator field is needed during post process to determine the
82+
// actual concrete model type
83+
this.ensureDiscriminatorSelection(model, args);
84+
8085
if (args.orderBy) {
8186
// `orderBy` may contain fields from base types
8287
this.injectWhereHierarchy(this.model, args.orderBy);
@@ -94,6 +99,23 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
9499
}
95100
}
96101

102+
private ensureDiscriminatorSelection(model: string, args: any) {
103+
const modelInfo = getModelInfo(this.options.modelMeta, model);
104+
if (!modelInfo?.discriminator) {
105+
return;
106+
}
107+
108+
if (args.select && typeof args.select === 'object') {
109+
args.select[modelInfo.discriminator] = true;
110+
return;
111+
}
112+
113+
if (args.omit && typeof args.omit === 'object') {
114+
args.omit[modelInfo.discriminator] = false;
115+
return;
116+
}
117+
}
118+
97119
private injectWhereHierarchy(model: string, where: any) {
98120
if (!where || !isPlainObject(where)) {
99121
return;
@@ -337,6 +359,8 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
337359
);
338360
}
339361

362+
this.sanitizeMutationPayload(args.data);
363+
340364
if (isDelegateModel(this.options.modelMeta, this.model)) {
341365
throw prismaClientValidationError(
342366
this.prisma,
@@ -352,6 +376,24 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
352376
return this.doCreate(this.prisma, this.model, args);
353377
}
354378

379+
private sanitizeMutationPayload(data: any) {
380+
if (!data) {
381+
return;
382+
}
383+
384+
const prisma = this.prisma;
385+
const prismaModule = this.options.prismaModule;
386+
traverse(data).forEach(function () {
387+
if (this.key?.startsWith(DELEGATE_AUX_RELATION_PREFIX)) {
388+
throw prismaClientValidationError(
389+
prisma,
390+
prismaModule,
391+
`Auxiliary relation field "${this.key}" cannot be set directly`
392+
);
393+
}
394+
});
395+
}
396+
355397
override createMany(args: { data: any; skipDuplicates?: boolean }): Promise<{ count: number }> {
356398
if (!args) {
357399
throw prismaClientValidationError(this.prisma, this.options.prismaModule, 'query argument is required');
@@ -364,6 +406,8 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
364406
);
365407
}
366408

409+
this.sanitizeMutationPayload(args.data);
410+
367411
if (!this.involvesDelegateModel(this.model)) {
368412
return super.createMany(args);
369413
}
@@ -403,6 +447,8 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
403447
);
404448
}
405449

450+
this.sanitizeMutationPayload(args.data);
451+
406452
if (!this.involvesDelegateModel(this.model)) {
407453
return super.createManyAndReturn(args);
408454
}
@@ -589,6 +635,8 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
589635
);
590636
}
591637

638+
this.sanitizeMutationPayload(args.data);
639+
592640
if (!this.involvesDelegateModel(this.model)) {
593641
return super.update(args);
594642
}
@@ -608,6 +656,8 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
608656
);
609657
}
610658

659+
this.sanitizeMutationPayload(args.data);
660+
611661
if (!this.involvesDelegateModel(this.model)) {
612662
return super.updateMany(args);
613663
}
@@ -635,6 +685,9 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
635685
);
636686
}
637687

688+
this.sanitizeMutationPayload(args.update);
689+
this.sanitizeMutationPayload(args.create);
690+
638691
if (isDelegateModel(this.options.modelMeta, this.model)) {
639692
throw prismaClientValidationError(
640693
this.prisma,

packages/runtime/src/enhancements/node/omit.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { enumerate, getModelFields, resolveField } from '../../cross';
55
import { DbClientContract } from '../../types';
66
import { InternalEnhancementOptions } from './create-enhancement';
77
import { DefaultPrismaProxyHandler, makeProxy } from './proxy';
8+
import { QueryUtils } from './query-utils';
89

910
/**
1011
* Gets an enhanced Prisma client that supports `@omit` attribute.
@@ -21,8 +22,11 @@ export function withOmit<DbClient extends object>(prisma: DbClient, options: Int
2122
}
2223

2324
class OmitHandler extends DefaultPrismaProxyHandler {
25+
private queryUtils: QueryUtils;
26+
2427
constructor(prisma: DbClientContract, model: string, options: InternalEnhancementOptions) {
2528
super(prisma, model, options);
29+
this.queryUtils = new QueryUtils(prisma, options);
2630
}
2731

2832
// base override
@@ -67,8 +71,10 @@ class OmitHandler extends DefaultPrismaProxyHandler {
6771
}
6872

6973
private async doPostProcess(entityData: any, model: string) {
74+
const realModel = this.queryUtils.getDelegateConcreteModel(model, entityData);
75+
7076
for (const field of getModelFields(entityData)) {
71-
const fieldInfo = await resolveField(this.options.modelMeta, model, field);
77+
const fieldInfo = await resolveField(this.options.modelMeta, realModel, field);
7278
if (!fieldInfo) {
7379
continue;
7480
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1381,7 +1381,10 @@ export class PolicyUtil extends QueryUtils {
13811381
// preserve the original data as it may be needed for checking field-level readability,
13821382
// while the "data" will be manipulated during traversal (deleting unreadable fields)
13831383
const origData = this.safeClone(data);
1384-
return this.doPostProcessForRead(data, model, origData, queryArgs, this.hasFieldLevelPolicy(model));
1384+
1385+
// use the concrete model if the data is a polymorphic entity
1386+
const realModel = this.getDelegateConcreteModel(model, data);
1387+
return this.doPostProcessForRead(data, realModel, origData, queryArgs, this.hasFieldLevelPolicy(realModel));
13851388
}
13861389

13871390
private doPostProcessForRead(

packages/runtime/src/enhancements/node/query-utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,4 +214,22 @@ export class QueryUtils {
214214
safeClone(value: unknown): any {
215215
return value ? clone(value) : value === undefined || value === null ? {} : value;
216216
}
217+
218+
getDelegateConcreteModel(model: string, data: any) {
219+
if (!data || typeof data !== 'object') {
220+
return model;
221+
}
222+
223+
const modelInfo = getModelInfo(this.options.modelMeta, model);
224+
if (modelInfo?.discriminator) {
225+
// model has a discriminator so it can be a polymorphic base,
226+
// need to find the concrete model
227+
const concreteModelName = data[modelInfo.discriminator];
228+
if (concreteModelName) {
229+
return concreteModelName;
230+
}
231+
}
232+
233+
return model;
234+
}
217235
}

packages/schema/src/res/stdlib.zmodel

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ function cuid(): String {
9797
} @@@expressionContext([DefaultValue])
9898

9999
/**
100-
* Generates an indentifier based on the nanoid spec.
100+
* Generates an identifier based on the nanoid spec.
101101
*/
102102
function nanoid(length: Int?): String {
103103
} @@@expressionContext([DefaultValue])

0 commit comments

Comments
 (0)