Skip to content

Commit

Permalink
fix(runtime): avoid duplicating non-plain objects
Browse files Browse the repository at this point in the history
  • Loading branch information
ymc9 committed Jun 30, 2024
1 parent 18a4877 commit 94f79e5
Show file tree
Hide file tree
Showing 13 changed files with 146 additions and 70 deletions.
1 change: 0 additions & 1 deletion packages/runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@
"buffer": "^6.0.3",
"change-case": "^4.1.2",
"decimal.js": "^10.4.2",
"deepcopy": "^2.1.0",
"deepmerge": "^4.3.1",
"is-plain-object": "^5.0.0",
"logic-solver": "^2.0.1",
Expand Down
25 changes: 25 additions & 0 deletions packages/runtime/src/cross/clone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { isPlainObject } from 'is-plain-object';

/**
* Clones the given object. Only arrays and plain objects are cloned. Other values are returned as is.
*/
export function clone<T>(value: T): T {
if (Array.isArray(value)) {
return value.map((v) => clone(v)) as T;
}

if (typeof value === 'object') {
if (!value || !isPlainObject(value)) {
return value;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result: any = {};
for (const key of Object.keys(value)) {
result[key] = clone(value[key as keyof T]);
}
return result;
}

return value;
}
1 change: 1 addition & 0 deletions packages/runtime/src/cross/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './clone';
export * from './model-data-visitor';
export * from './model-meta';
export * from './mutator';
Expand Down
4 changes: 2 additions & 2 deletions packages/runtime/src/cross/mutator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { v4 as uuid } from 'uuid';
import deepcopy from 'deepcopy';
import {
ModelDataVisitor,
NestedWriteVisitor,
Expand All @@ -10,6 +9,7 @@ import {
type ModelMeta,
type PrismaWriteActionType,
} from '.';
import { clone } from './clone';

/**
* Tries to apply a mutation to a query result.
Expand Down Expand Up @@ -200,7 +200,7 @@ function updateMutate(
});
}

return updated ? deepcopy(currentData) /* ensures new object identity */ : undefined;
return updated ? clone(currentData) /* ensures new object identity */ : undefined;
}

function deleteMutate(
Expand Down
4 changes: 2 additions & 2 deletions packages/runtime/src/enhancements/default-auth.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */

import deepcopy from 'deepcopy';
import { FieldInfo, NestedWriteVisitor, PrismaWriteActionType, enumerate, getFields, requireField } from '../cross';
import { clone } from '../cross';
import { DbClientContract } from '../types';
import { EnhancementContext, InternalEnhancementOptions } from './create-enhancement';
import { DefaultPrismaProxyHandler, PrismaProxyActions, makeProxy } from './proxy';
Expand Down Expand Up @@ -51,7 +51,7 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler {
}

private async preprocessWritePayload(model: string, action: PrismaWriteActionType, args: any) {
const newArgs = deepcopy(args);
const newArgs = clone(args);

const processCreatePayload = (model: string, data: any) => {
const fields = getFields(this.options.modelMeta, model);
Expand Down
32 changes: 16 additions & 16 deletions packages/runtime/src/enhancements/delegate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import deepcopy from 'deepcopy';
import deepmerge, { type ArrayMergeOptions } from 'deepmerge';
import { isPlainObject } from 'is-plain-object';
import { lowerCaseFirst } from 'lower-case-first';
Expand All @@ -14,6 +13,7 @@ import {
isDelegateModel,
resolveField,
} from '../cross';
import { clone } from '../cross';
import type { CrudContract, DbClientContract } from '../types';
import type { InternalEnhancementOptions } from './create-enhancement';
import { Logger } from './logger';
Expand Down Expand Up @@ -72,7 +72,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
return super[method](args);
}

args = args ? deepcopy(args) : {};
args = args ? clone(args) : {};

this.injectWhereHierarchy(model, args?.where);
this.injectSelectIncludeHierarchy(model, args);
Expand Down Expand Up @@ -142,7 +142,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
return undefined;
}

where = deepcopy(where);
where = clone(where);
Object.entries(where).forEach(([field, value]) => {
const fieldInfo = resolveField(this.options.modelMeta, model, field);
if (!fieldInfo?.inheritedFrom) {
Expand Down Expand Up @@ -217,7 +217,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
}

private buildSelectIncludeHierarchy(model: string, args: any) {
args = deepcopy(args);
args = clone(args);
const selectInclude: any = this.extractSelectInclude(args) || {};

if (selectInclude.select && typeof selectInclude.select === 'object') {
Expand Down Expand Up @@ -408,7 +408,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
}

private async doCreate(db: CrudContract, model: string, args: any) {
args = deepcopy(args);
args = clone(args);

await this.injectCreateHierarchy(model, args);
this.injectSelectIncludeHierarchy(model, args);
Expand Down Expand Up @@ -624,7 +624,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
return super.upsert(args);
}

args = deepcopy(args);
args = clone(args);
this.injectWhereHierarchy(this.model, (args as any)?.where);
this.injectSelectIncludeHierarchy(this.model, args);
if (args.create) {
Expand All @@ -642,7 +642,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
}

private async doUpdate(db: CrudContract, model: string, args: any): Promise<unknown> {
args = deepcopy(args);
args = clone(args);

await this.injectUpdateHierarchy(db, model, args);
this.injectSelectIncludeHierarchy(model, args);
Expand All @@ -662,7 +662,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
): Promise<{ count: number }> {
if (simpleUpdateMany) {
// do a direct `updateMany`
args = deepcopy(args);
args = clone(args);
await this.injectUpdateHierarchy(db, model, args);

if (this.options.logPrismaQuery) {
Expand All @@ -672,7 +672,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
} else {
// translate to plain `update` for nested write into base fields
const findArgs = {
where: deepcopy(args.where),
where: clone(args.where),
select: this.queryUtils.makeIdSelection(model),
};
await this.injectUpdateHierarchy(db, model, findArgs);
Expand All @@ -683,7 +683,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
}
const entities = await db[model].findMany(findArgs);

const updatePayload = { data: deepcopy(args.data), select: this.queryUtils.makeIdSelection(model) };
const updatePayload = { data: clone(args.data), select: this.queryUtils.makeIdSelection(model) };
await this.injectUpdateHierarchy(db, model, updatePayload);
const result = await Promise.all(
entities.map((entity) => {
Expand Down Expand Up @@ -849,7 +849,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
}
}

const deleteArgs = { ...deepcopy(args), ...selectInclude };
const deleteArgs = { ...clone(args), ...selectInclude };
return this.doDelete(tx, this.model, deleteArgs);
});
}
Expand All @@ -865,7 +865,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
private async doDeleteMany(db: CrudContract, model: string, where: any): Promise<{ count: number }> {
// query existing entities with id
const idSelection = this.queryUtils.makeIdSelection(model);
const findArgs = { where: deepcopy(where), select: idSelection };
const findArgs = { where: clone(where), select: idSelection };
this.injectWhereHierarchy(model, findArgs.where);

if (this.options.logPrismaQuery) {
Expand Down Expand Up @@ -918,7 +918,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
// check if any aggregation operator is using fields from base
this.checkAggregationArgs('aggregate', args);

args = deepcopy(args);
args = clone(args);

if (args.cursor) {
args.cursor = this.buildWhereHierarchy(this.model, args.cursor);
Expand Down Expand Up @@ -946,7 +946,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
// check if count select is using fields from base
this.checkAggregationArgs('count', args);

args = deepcopy(args);
args = clone(args);

if (args?.cursor) {
args.cursor = this.buildWhereHierarchy(this.model, args.cursor);
Expand Down Expand Up @@ -986,7 +986,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
}
}

args = deepcopy(args);
args = clone(args);

if (args.where) {
args.where = this.buildWhereHierarchy(this.model, args.where);
Expand Down Expand Up @@ -1027,7 +1027,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
if (!args) {
return undefined;
}
args = deepcopy(args);
args = clone(args);
return 'select' in args
? { select: args['select'] }
: 'include' in args
Expand Down
30 changes: 15 additions & 15 deletions packages/runtime/src/enhancements/policy/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { createDeferredPromise, createFluentPromise } from '../promise';
import { PrismaProxyHandler } from '../proxy';
import { QueryUtils } from '../query-utils';
import type { EntityCheckerFunc, PermissionCheckerConstraint } from '../types';
import { clone, formatObject, isUnsafeMutate, prismaClientValidationError } from '../utils';
import { formatObject, isUnsafeMutate, prismaClientValidationError } from '../utils';
import { ConstraintSolver } from './constraint-solver';
import { PolicyUtil } from './policy-utils';

Expand Down Expand Up @@ -127,7 +127,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr

// make a find query promise with fluent API call stubs installed
private findWithFluent(method: FindOperations, args: any, handleRejection: () => any) {
args = clone(args);
args = this.policyUtils.safeClone(args);
return createFluentPromise(
() => this.doFind(args, method, handleRejection),
args,
Expand All @@ -138,7 +138,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr

private async doFind(args: any, actionName: FindOperations, handleRejection: () => any) {
const origArgs = args;
const _args = clone(args);
const _args = this.policyUtils.safeClone(args);
if (!this.policyUtils.injectForRead(this.prisma, this.model, _args)) {
if (this.shouldLogQuery) {
this.logger.info(`[policy] \`${actionName}\` ${this.model}: unconditionally denied`);
Expand Down Expand Up @@ -176,7 +176,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
this.policyUtils.tryReject(this.prisma, this.model, 'create');

const origArgs = args;
args = clone(args);
args = this.policyUtils.safeClone(args);

// static input policy check for top-level create data
const inputCheck = this.policyUtils.checkInputGuard(this.model, args.data, 'create');
Expand Down Expand Up @@ -443,7 +443,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
return createDeferredPromise(async () => {
this.policyUtils.tryReject(this.prisma, this.model, 'create');

args = clone(args);
args = this.policyUtils.safeClone(args);

// go through create items, statically check input to determine if post-create
// check is needed, and also validate zod schema
Expand Down Expand Up @@ -480,7 +480,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
this.policyUtils.tryReject(this.prisma, this.model, 'create');

const origArgs = args;
args = clone(args);
args = this.policyUtils.safeClone(args);

// go through create items, statically check input to determine if post-create
// check is needed, and also validate zod schema
Expand Down Expand Up @@ -686,7 +686,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
}

return createDeferredPromise(async () => {
args = clone(args);
args = this.policyUtils.safeClone(args);

const { result, error } = await this.queryUtils.transaction(this.prisma, async (tx) => {
// proceed with nested writes and collect post-write checks
Expand Down Expand Up @@ -1149,7 +1149,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr

// calculate id fields used for post-update check given an update payload
private calculatePostUpdateIds(_model: string, currentIds: any, updatePayload: any) {
const result = clone(currentIds);
const result = this.policyUtils.safeClone(currentIds);
for (const key of Object.keys(currentIds)) {
const updateValue = updatePayload[key];
if (typeof updateValue === 'string' || typeof updateValue === 'number' || typeof updateValue === 'bigint') {
Expand Down Expand Up @@ -1239,7 +1239,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
return createDeferredPromise(() => {
this.policyUtils.tryReject(this.prisma, this.model, 'update');

args = clone(args);
args = this.policyUtils.safeClone(args);
this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'update');

args.data = this.validateUpdateInputSchema(this.model, args.data);
Expand Down Expand Up @@ -1349,7 +1349,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
this.policyUtils.tryReject(this.prisma, this.model, 'create');
this.policyUtils.tryReject(this.prisma, this.model, 'update');

args = clone(args);
args = this.policyUtils.safeClone(args);

// We can call the native "upsert" because we can't tell if an entity was created or updated
// for doing post-write check accordingly. Instead, decompose it into create or update.
Expand Down Expand Up @@ -1442,7 +1442,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
this.policyUtils.tryReject(this.prisma, this.model, 'delete');

// inject policy conditions
args = clone(args);
args = this.policyUtils.safeClone(args);
this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'delete');

const entityChecker = this.policyUtils.getEntityChecker(this.model, 'delete');
Expand Down Expand Up @@ -1498,7 +1498,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
}

return createDeferredPromise(() => {
args = clone(args);
args = this.policyUtils.safeClone(args);

// inject policy conditions
this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read');
Expand All @@ -1516,7 +1516,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
}

return createDeferredPromise(() => {
args = clone(args);
args = this.policyUtils.safeClone(args);

// inject policy conditions
this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read');
Expand All @@ -1531,7 +1531,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
count(args: any) {
return createDeferredPromise(() => {
// inject policy conditions
args = args ? clone(args) : {};
args = args ? this.policyUtils.safeClone(args) : {};
this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read');

if (this.shouldLogQuery) {
Expand Down Expand Up @@ -1567,7 +1567,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
// include all
args = { create: {}, update: {}, delete: {} };
} else {
args = clone(args);
args = this.policyUtils.safeClone(args);
}
}

Expand Down
Loading

0 comments on commit 94f79e5

Please sign in to comment.