Skip to content

Commit ba2a113

Browse files
authored
fix(runtime): avoid duplicating non-plain objects (#1545)
1 parent 18a4877 commit ba2a113

File tree

13 files changed

+146
-70
lines changed

13 files changed

+146
-70
lines changed

packages/runtime/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,6 @@
8181
"buffer": "^6.0.3",
8282
"change-case": "^4.1.2",
8383
"decimal.js": "^10.4.2",
84-
"deepcopy": "^2.1.0",
8584
"deepmerge": "^4.3.1",
8685
"is-plain-object": "^5.0.0",
8786
"logic-solver": "^2.0.1",
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { isPlainObject } from 'is-plain-object';
2+
3+
/**
4+
* Clones the given object. Only arrays and plain objects are cloned. Other values are returned as is.
5+
*/
6+
export function clone<T>(value: T): T {
7+
if (Array.isArray(value)) {
8+
return value.map((v) => clone(v)) as T;
9+
}
10+
11+
if (typeof value === 'object') {
12+
if (!value || !isPlainObject(value)) {
13+
return value;
14+
}
15+
16+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
17+
const result: any = {};
18+
for (const key of Object.keys(value)) {
19+
result[key] = clone(value[key as keyof T]);
20+
}
21+
return result;
22+
}
23+
24+
return value;
25+
}

packages/runtime/src/cross/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from './clone';
12
export * from './model-data-visitor';
23
export * from './model-meta';
34
export * from './mutator';

packages/runtime/src/cross/mutator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22
import { v4 as uuid } from 'uuid';
3-
import deepcopy from 'deepcopy';
43
import {
54
ModelDataVisitor,
65
NestedWriteVisitor,
@@ -10,6 +9,7 @@ import {
109
type ModelMeta,
1110
type PrismaWriteActionType,
1211
} from '.';
12+
import { clone } from './clone';
1313

1414
/**
1515
* Tries to apply a mutation to a query result.
@@ -200,7 +200,7 @@ function updateMutate(
200200
});
201201
}
202202

203-
return updated ? deepcopy(currentData) /* ensures new object identity */ : undefined;
203+
return updated ? clone(currentData) /* ensures new object identity */ : undefined;
204204
}
205205

206206
function deleteMutate(

packages/runtime/src/enhancements/default-auth.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/* eslint-disable @typescript-eslint/no-unused-vars */
22
/* eslint-disable @typescript-eslint/no-explicit-any */
33

4-
import deepcopy from 'deepcopy';
54
import { FieldInfo, NestedWriteVisitor, PrismaWriteActionType, enumerate, getFields, requireField } from '../cross';
5+
import { clone } from '../cross';
66
import { DbClientContract } from '../types';
77
import { EnhancementContext, InternalEnhancementOptions } from './create-enhancement';
88
import { DefaultPrismaProxyHandler, PrismaProxyActions, makeProxy } from './proxy';
@@ -51,7 +51,7 @@ class DefaultAuthHandler extends DefaultPrismaProxyHandler {
5151
}
5252

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

5656
const processCreatePayload = (model: string, data: any) => {
5757
const fields = getFields(this.options.modelMeta, model);

packages/runtime/src/enhancements/delegate.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
2-
import deepcopy from 'deepcopy';
32
import deepmerge, { type ArrayMergeOptions } from 'deepmerge';
43
import { isPlainObject } from 'is-plain-object';
54
import { lowerCaseFirst } from 'lower-case-first';
@@ -14,6 +13,7 @@ import {
1413
isDelegateModel,
1514
resolveField,
1615
} from '../cross';
16+
import { clone } from '../cross';
1717
import type { CrudContract, DbClientContract } from '../types';
1818
import type { InternalEnhancementOptions } from './create-enhancement';
1919
import { Logger } from './logger';
@@ -72,7 +72,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
7272
return super[method](args);
7373
}
7474

75-
args = args ? deepcopy(args) : {};
75+
args = args ? clone(args) : {};
7676

7777
this.injectWhereHierarchy(model, args?.where);
7878
this.injectSelectIncludeHierarchy(model, args);
@@ -142,7 +142,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
142142
return undefined;
143143
}
144144

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

219219
private buildSelectIncludeHierarchy(model: string, args: any) {
220-
args = deepcopy(args);
220+
args = clone(args);
221221
const selectInclude: any = this.extractSelectInclude(args) || {};
222222

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

410410
private async doCreate(db: CrudContract, model: string, args: any) {
411-
args = deepcopy(args);
411+
args = clone(args);
412412

413413
await this.injectCreateHierarchy(model, args);
414414
this.injectSelectIncludeHierarchy(model, args);
@@ -624,7 +624,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
624624
return super.upsert(args);
625625
}
626626

627-
args = deepcopy(args);
627+
args = clone(args);
628628
this.injectWhereHierarchy(this.model, (args as any)?.where);
629629
this.injectSelectIncludeHierarchy(this.model, args);
630630
if (args.create) {
@@ -642,7 +642,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
642642
}
643643

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

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

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

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

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

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

921-
args = deepcopy(args);
921+
args = clone(args);
922922

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

949-
args = deepcopy(args);
949+
args = clone(args);
950950

951951
if (args?.cursor) {
952952
args.cursor = this.buildWhereHierarchy(this.model, args.cursor);
@@ -986,7 +986,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
986986
}
987987
}
988988

989-
args = deepcopy(args);
989+
args = clone(args);
990990

991991
if (args.where) {
992992
args.where = this.buildWhereHierarchy(this.model, args.where);
@@ -1027,7 +1027,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
10271027
if (!args) {
10281028
return undefined;
10291029
}
1030-
args = deepcopy(args);
1030+
args = clone(args);
10311031
return 'select' in args
10321032
? { select: args['select'] }
10331033
: 'include' in args

packages/runtime/src/enhancements/policy/handler.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { createDeferredPromise, createFluentPromise } from '../promise';
2525
import { PrismaProxyHandler } from '../proxy';
2626
import { QueryUtils } from '../query-utils';
2727
import type { EntityCheckerFunc, PermissionCheckerConstraint } from '../types';
28-
import { clone, formatObject, isUnsafeMutate, prismaClientValidationError } from '../utils';
28+
import { formatObject, isUnsafeMutate, prismaClientValidationError } from '../utils';
2929
import { ConstraintSolver } from './constraint-solver';
3030
import { PolicyUtil } from './policy-utils';
3131

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

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

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

178178
const origArgs = args;
179-
args = clone(args);
179+
args = this.policyUtils.safeClone(args);
180180

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

446-
args = clone(args);
446+
args = this.policyUtils.safeClone(args);
447447

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

482482
const origArgs = args;
483-
args = clone(args);
483+
args = this.policyUtils.safeClone(args);
484484

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

688688
return createDeferredPromise(async () => {
689-
args = clone(args);
689+
args = this.policyUtils.safeClone(args);
690690

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

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

1242-
args = clone(args);
1242+
args = this.policyUtils.safeClone(args);
12431243
this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'update');
12441244

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

1352-
args = clone(args);
1352+
args = this.policyUtils.safeClone(args);
13531353

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

14441444
// inject policy conditions
1445-
args = clone(args);
1445+
args = this.policyUtils.safeClone(args);
14461446
this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'delete');
14471447

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

15001500
return createDeferredPromise(() => {
1501-
args = clone(args);
1501+
args = this.policyUtils.safeClone(args);
15021502

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

15181518
return createDeferredPromise(() => {
1519-
args = clone(args);
1519+
args = this.policyUtils.safeClone(args);
15201520

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

15371537
if (this.shouldLogQuery) {
@@ -1567,7 +1567,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
15671567
// include all
15681568
args = { create: {}, update: {}, delete: {} };
15691569
} else {
1570-
args = clone(args);
1570+
args = this.policyUtils.safeClone(args);
15711571
}
15721572
}
15731573

0 commit comments

Comments
 (0)