Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
"semver": "^7.5.2",
"superjson": "^1.13.0",
"tiny-invariant": "^1.3.1",
"traverse": "^0.6.10",
"ts-pattern": "^4.3.0",
"tslib": "^2.4.1",
"upper-case-first": "^2.0.2",
Expand All @@ -118,6 +119,7 @@
"@types/pluralize": "^0.0.29",
"@types/safe-json-stringify": "^1.1.5",
"@types/semver": "^7.3.13",
"@types/traverse": "^0.6.37",
"@types/uuid": "^8.3.4"
}
}
53 changes: 53 additions & 0 deletions packages/runtime/src/enhancements/node/delegate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import deepmerge, { type ArrayMergeOptions } from 'deepmerge';
import { isPlainObject } from 'is-plain-object';
import { lowerCaseFirst } from 'lower-case-first';
import traverse from 'traverse';
import { DELEGATE_AUX_RELATION_PREFIX } from '../../constants';
import {
FieldInfo,
Expand Down Expand Up @@ -77,6 +78,10 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
this.injectWhereHierarchy(model, args?.where);
this.injectSelectIncludeHierarchy(model, args);

// discriminator field is needed during post process to determine the
// actual concrete model type
this.ensureDiscriminatorSelection(model, args);

if (args.orderBy) {
// `orderBy` may contain fields from base types
this.injectWhereHierarchy(this.model, args.orderBy);
Expand All @@ -94,6 +99,23 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
}
}

private ensureDiscriminatorSelection(model: string, args: any) {
const modelInfo = getModelInfo(this.options.modelMeta, model);
if (!modelInfo?.discriminator) {
return;
}

if (args.select && typeof args.select === 'object') {
args.select[modelInfo.discriminator] = true;
return;
}

if (args.omit && typeof args.omit === 'object') {
args.omit[modelInfo.discriminator] = false;
return;
}
}

private injectWhereHierarchy(model: string, where: any) {
if (!where || !isPlainObject(where)) {
return;
Expand Down Expand Up @@ -337,6 +359,8 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
);
}

this.sanitizeMutationPayload(args.data);

if (isDelegateModel(this.options.modelMeta, this.model)) {
throw prismaClientValidationError(
this.prisma,
Expand All @@ -352,6 +376,24 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
return this.doCreate(this.prisma, this.model, args);
}

private sanitizeMutationPayload(data: any) {
if (!data) {
return;
}

const prisma = this.prisma;
const prismaModule = this.options.prismaModule;
traverse(data).forEach(function () {
if (this.key?.startsWith(DELEGATE_AUX_RELATION_PREFIX)) {
throw prismaClientValidationError(
prisma,
prismaModule,
`Auxiliary relation field "${this.key}" cannot be set directly`
);
}
});
}

override createMany(args: { data: any; skipDuplicates?: boolean }): Promise<{ count: number }> {
if (!args) {
throw prismaClientValidationError(this.prisma, this.options.prismaModule, 'query argument is required');
Expand All @@ -364,6 +406,8 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
);
}

this.sanitizeMutationPayload(args.data);

if (!this.involvesDelegateModel(this.model)) {
return super.createMany(args);
}
Expand Down Expand Up @@ -403,6 +447,8 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
);
}

this.sanitizeMutationPayload(args.data);

if (!this.involvesDelegateModel(this.model)) {
return super.createManyAndReturn(args);
}
Expand Down Expand Up @@ -589,6 +635,8 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
);
}

this.sanitizeMutationPayload(args.data);

if (!this.involvesDelegateModel(this.model)) {
return super.update(args);
}
Expand All @@ -608,6 +656,8 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
);
}

this.sanitizeMutationPayload(args.data);

if (!this.involvesDelegateModel(this.model)) {
return super.updateMany(args);
}
Expand Down Expand Up @@ -635,6 +685,9 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
);
}

this.sanitizeMutationPayload(args.update);
this.sanitizeMutationPayload(args.create);

if (isDelegateModel(this.options.modelMeta, this.model)) {
throw prismaClientValidationError(
this.prisma,
Expand Down
8 changes: 7 additions & 1 deletion packages/runtime/src/enhancements/node/omit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { enumerate, getModelFields, resolveField } from '../../cross';
import { DbClientContract } from '../../types';
import { InternalEnhancementOptions } from './create-enhancement';
import { DefaultPrismaProxyHandler, makeProxy } from './proxy';
import { QueryUtils } from './query-utils';

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

class OmitHandler extends DefaultPrismaProxyHandler {
private queryUtils: QueryUtils;

constructor(prisma: DbClientContract, model: string, options: InternalEnhancementOptions) {
super(prisma, model, options);
this.queryUtils = new QueryUtils(prisma, options);
}

// base override
Expand Down Expand Up @@ -67,8 +71,10 @@ class OmitHandler extends DefaultPrismaProxyHandler {
}

private async doPostProcess(entityData: any, model: string) {
const realModel = this.queryUtils.getDelegateConcreteModel(model, entityData);

for (const field of getModelFields(entityData)) {
const fieldInfo = await resolveField(this.options.modelMeta, model, field);
const fieldInfo = await resolveField(this.options.modelMeta, realModel, field);
if (!fieldInfo) {
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1381,7 +1381,10 @@ export class PolicyUtil extends QueryUtils {
// preserve the original data as it may be needed for checking field-level readability,
// while the "data" will be manipulated during traversal (deleting unreadable fields)
const origData = this.safeClone(data);
return this.doPostProcessForRead(data, model, origData, queryArgs, this.hasFieldLevelPolicy(model));

// use the concrete model if the data is a polymorphic entity
const realModel = this.getDelegateConcreteModel(model, data);
return this.doPostProcessForRead(data, realModel, origData, queryArgs, this.hasFieldLevelPolicy(realModel));
}

private doPostProcessForRead(
Expand Down
18 changes: 18 additions & 0 deletions packages/runtime/src/enhancements/node/query-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,22 @@ export class QueryUtils {
safeClone(value: unknown): any {
return value ? clone(value) : value === undefined || value === null ? {} : value;
}

getDelegateConcreteModel(model: string, data: any) {
if (!data || typeof data !== 'object') {
return model;
}

const modelInfo = getModelInfo(this.options.modelMeta, model);
if (modelInfo?.discriminator) {
// model has a discriminator so it can be a polymorphic base,
// need to find the concrete model
const concreteModelName = data[modelInfo.discriminator];
if (concreteModelName) {
return concreteModelName;
}
}

return model;
}
}
2 changes: 1 addition & 1 deletion packages/schema/src/res/stdlib.zmodel
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ function cuid(): String {
} @@@expressionContext([DefaultValue])

/**
* Generates an indentifier based on the nanoid spec.
* Generates an identifier based on the nanoid spec.
*/
function nanoid(length: Int?): String {
} @@@expressionContext([DefaultValue])
Expand Down
Loading
Loading