From 94f79e526a3187f380e3aa1d14e1624646bdc410 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sat, 29 Jun 2024 17:34:41 -0700 Subject: [PATCH] fix(runtime): avoid duplicating non-plain objects --- packages/runtime/package.json | 1 - packages/runtime/src/cross/clone.ts | 25 +++++++ packages/runtime/src/cross/index.ts | 1 + packages/runtime/src/cross/mutator.ts | 4 +- .../runtime/src/enhancements/default-auth.ts | 4 +- packages/runtime/src/enhancements/delegate.ts | 32 ++++----- .../src/enhancements/policy/handler.ts | 30 ++++----- .../src/enhancements/policy/policy-utils.ts | 21 ++---- packages/runtime/src/enhancements/proxy.ts | 4 +- .../runtime/src/enhancements/query-utils.ts | 12 +++- packages/runtime/src/enhancements/utils.ts | 6 -- pnpm-lock.yaml | 10 --- tests/regression/tests/issue-1533.test.ts | 66 +++++++++++++++++++ 13 files changed, 146 insertions(+), 70 deletions(-) create mode 100644 packages/runtime/src/cross/clone.ts create mode 100644 tests/regression/tests/issue-1533.test.ts diff --git a/packages/runtime/package.json b/packages/runtime/package.json index b03386877..ed1234f0b 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -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", diff --git a/packages/runtime/src/cross/clone.ts b/packages/runtime/src/cross/clone.ts new file mode 100644 index 000000000..1a355d7bf --- /dev/null +++ b/packages/runtime/src/cross/clone.ts @@ -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(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; +} diff --git a/packages/runtime/src/cross/index.ts b/packages/runtime/src/cross/index.ts index 853f8bc7d..84d23cd5d 100644 --- a/packages/runtime/src/cross/index.ts +++ b/packages/runtime/src/cross/index.ts @@ -1,3 +1,4 @@ +export * from './clone'; export * from './model-data-visitor'; export * from './model-meta'; export * from './mutator'; diff --git a/packages/runtime/src/cross/mutator.ts b/packages/runtime/src/cross/mutator.ts index 0dd66e6fb..874689c2d 100644 --- a/packages/runtime/src/cross/mutator.ts +++ b/packages/runtime/src/cross/mutator.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { v4 as uuid } from 'uuid'; -import deepcopy from 'deepcopy'; import { ModelDataVisitor, NestedWriteVisitor, @@ -10,6 +9,7 @@ import { type ModelMeta, type PrismaWriteActionType, } from '.'; +import { clone } from './clone'; /** * Tries to apply a mutation to a query result. @@ -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( diff --git a/packages/runtime/src/enhancements/default-auth.ts b/packages/runtime/src/enhancements/default-auth.ts index ef2acfcc1..ba1ead7f1 100644 --- a/packages/runtime/src/enhancements/default-auth.ts +++ b/packages/runtime/src/enhancements/default-auth.ts @@ -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'; @@ -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); diff --git a/packages/runtime/src/enhancements/delegate.ts b/packages/runtime/src/enhancements/delegate.ts index 5785be1d9..fd8ad633a 100644 --- a/packages/runtime/src/enhancements/delegate.ts +++ b/packages/runtime/src/enhancements/delegate.ts @@ -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'; @@ -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'; @@ -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); @@ -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) { @@ -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') { @@ -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); @@ -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) { @@ -642,7 +642,7 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { } private async doUpdate(db: CrudContract, model: string, args: any): Promise { - args = deepcopy(args); + args = clone(args); await this.injectUpdateHierarchy(db, model, args); this.injectSelectIncludeHierarchy(model, args); @@ -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) { @@ -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); @@ -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) => { @@ -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); }); } @@ -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) { @@ -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); @@ -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); @@ -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); @@ -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 diff --git a/packages/runtime/src/enhancements/policy/handler.ts b/packages/runtime/src/enhancements/policy/handler.ts index b6088ed25..1da8b9bb8 100644 --- a/packages/runtime/src/enhancements/policy/handler.ts +++ b/packages/runtime/src/enhancements/policy/handler.ts @@ -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'; @@ -127,7 +127,7 @@ export class PolicyProxyHandler 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, @@ -138,7 +138,7 @@ export class PolicyProxyHandler 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`); @@ -176,7 +176,7 @@ export class PolicyProxyHandler 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'); @@ -443,7 +443,7 @@ export class PolicyProxyHandler 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 @@ -480,7 +480,7 @@ export class PolicyProxyHandler 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 @@ -686,7 +686,7 @@ export class PolicyProxyHandler 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 @@ -1149,7 +1149,7 @@ export class PolicyProxyHandler 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') { @@ -1239,7 +1239,7 @@ export class PolicyProxyHandler 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); @@ -1349,7 +1349,7 @@ export class PolicyProxyHandler 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. @@ -1442,7 +1442,7 @@ export class PolicyProxyHandler 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'); @@ -1498,7 +1498,7 @@ export class PolicyProxyHandler implements Pr } return createDeferredPromise(() => { - args = clone(args); + args = this.policyUtils.safeClone(args); // inject policy conditions this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read'); @@ -1516,7 +1516,7 @@ export class PolicyProxyHandler implements Pr } return createDeferredPromise(() => { - args = clone(args); + args = this.policyUtils.safeClone(args); // inject policy conditions this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'read'); @@ -1531,7 +1531,7 @@ export class PolicyProxyHandler 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) { @@ -1567,7 +1567,7 @@ export class PolicyProxyHandler implements Pr // include all args = { create: {}, update: {}, delete: {} }; } else { - args = clone(args); + args = this.policyUtils.safeClone(args); } } diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index 316389594..69cfdffe6 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import deepcopy from 'deepcopy'; import deepmerge from 'deepmerge'; import { lowerCaseFirst } from 'lower-case-first'; import { upperCaseFirst } from 'upper-case-first'; @@ -8,6 +7,7 @@ import { ZodError } from 'zod'; import { fromZodError } from 'zod-validation-error'; import { CrudFailureReason, PrismaErrorCode } from '../../constants'; import { enumerate, getFields, getModelFields, resolveField, zip, type FieldInfo, type ModelMeta } from '../../cross'; +import { clone } from '../../cross'; import { AuthUser, CrudContract, @@ -550,7 +550,7 @@ export class PolicyUtil extends QueryUtils { } else { const mergedGuard = this.injectReadGuardForRelationFields(db, fieldInfo.type, payload, guard); // turn direct conditions into: { is: { AND: [ originalConditions, guard ] } } - const combined = this.and(deepcopy(payload), mergedGuard); + const combined = this.and(clone(payload), mergedGuard); Object.keys(payload).forEach((key) => delete payload[key]); payload.is = combined; } @@ -806,7 +806,7 @@ export class PolicyUtil extends QueryUtils { select = { ...select, ...entityChecker.selector }; } - let where = this.clone(uniqueFilter); + let where = this.safeClone(uniqueFilter); // query args may have be of combined-id form, need to flatten it to call findFirst this.flattenGeneratedUniqueField(model, where); @@ -1001,7 +1001,7 @@ export class PolicyUtil extends QueryUtils { * Checks if a model exists given a unique filter. */ async checkExistence(db: CrudContract, model: string, uniqueFilter: any, throwIfNotFound = false): Promise { - uniqueFilter = this.clone(uniqueFilter); + uniqueFilter = this.safeClone(uniqueFilter); this.flattenGeneratedUniqueField(model, uniqueFilter); if (this.shouldLogQuery) { @@ -1027,13 +1027,13 @@ export class PolicyUtil extends QueryUtils { selectInclude: { select?: any; include?: any }, uniqueFilter: any ): Promise<{ result: unknown; error?: Error }> { - uniqueFilter = this.clone(uniqueFilter); + uniqueFilter = this.safeClone(uniqueFilter); this.flattenGeneratedUniqueField(model, uniqueFilter); // make sure only select and include are picked const selectIncludeClean = this.pick(selectInclude, 'select', 'include'); const readArgs = { - ...this.clone(selectIncludeClean), + ...this.safeClone(selectIncludeClean), where: uniqueFilter, }; @@ -1278,7 +1278,7 @@ export class PolicyUtil extends QueryUtils { postProcessForRead(data: any, model: string, queryArgs: any) { // 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.clone(data); + const origData = this.safeClone(data); return this.doPostProcessForRead(data, model, origData, queryArgs, this.hasFieldLevelPolicy(model)); } @@ -1404,13 +1404,6 @@ export class PolicyUtil extends QueryUtils { return filteredData; } - /** - * Clones an object and makes sure it's not empty. - */ - clone(value: unknown): any { - return value ? deepcopy(value) : {}; - } - /** * Replace content of `target` object with `withObject` in-place. */ diff --git a/packages/runtime/src/enhancements/proxy.ts b/packages/runtime/src/enhancements/proxy.ts index 70d8f27e9..2e7e2e825 100644 --- a/packages/runtime/src/enhancements/proxy.ts +++ b/packages/runtime/src/enhancements/proxy.ts @@ -1,8 +1,8 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import deepcopy from 'deepcopy'; import { PRISMA_PROXY_ENHANCER } from '../constants'; import type { ModelMeta } from '../cross'; +import { clone } from '../cross'; import type { DbClientContract } from '../types'; import type { InternalEnhancementOptions } from './create-enhancement'; import { createDeferredPromise, createFluentPromise } from './promise'; @@ -74,7 +74,7 @@ export class DefaultPrismaProxyHandler implements PrismaProxyHandler { ) {} protected withFluentCall(method: keyof PrismaProxyHandler, args: any, postProcess = true): Promise { - args = args ? deepcopy(args) : {}; + args = args ? clone(args) : {}; const promise = createFluentPromise( async () => { args = await this.preprocessArgs(method, args); diff --git a/packages/runtime/src/enhancements/query-utils.ts b/packages/runtime/src/enhancements/query-utils.ts index 81c8d1da9..8c50252de 100644 --- a/packages/runtime/src/enhancements/query-utils.ts +++ b/packages/runtime/src/enhancements/query-utils.ts @@ -7,10 +7,11 @@ import { type FieldInfo, type NestedWriteVisitorContext, } from '../cross'; +import { clone } from '../cross'; import type { CrudContract, DbClientContract } from '../types'; import { getVersion } from '../version'; import { InternalEnhancementOptions } from './create-enhancement'; -import { clone, prismaClientUnknownRequestError, prismaClientValidationError } from './utils'; +import { prismaClientUnknownRequestError, prismaClientValidationError } from './utils'; export class QueryUtils { constructor(private readonly prisma: DbClientContract, protected readonly options: InternalEnhancementOptions) {} @@ -148,7 +149,7 @@ export class QueryUtils { return fieldData; } - const result: any = clone(fieldData); + const result: any = this.safeClone(fieldData); for (const [name, constraint] of Object.entries(uniqueConstraints)) { if (constraint.fields.length > 1 && constraint.fields.every((f) => fieldData[f] !== undefined)) { // multi-field unique constraint, compose it @@ -206,4 +207,11 @@ export class QueryUtils { getModelField(model: string, field: string) { return resolveField(this.options.modelMeta, model, field); } + + /** + * Clones an object and makes sure it's not empty. + */ + safeClone(value: unknown): any { + return value ? clone(value) : typeof value === 'object' ? {} : value; + } } diff --git a/packages/runtime/src/enhancements/utils.ts b/packages/runtime/src/enhancements/utils.ts index 5cd23610e..50b53b996 100644 --- a/packages/runtime/src/enhancements/utils.ts +++ b/packages/runtime/src/enhancements/utils.ts @@ -1,4 +1,3 @@ -import deepcopy from 'deepcopy'; import safeJsonStringify from 'safe-json-stringify'; import { resolveField, type FieldInfo, type ModelMeta } from '..'; import type { DbClientContract } from '../types'; @@ -43,8 +42,3 @@ export function isUnsafeMutate(model: string, args: any, modelMeta: ModelMeta) { export function isAutoIncrementIdField(field: FieldInfo) { return field.isId && field.isAutoIncrement; } - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function clone(value: unknown): any { - return value ? deepcopy(value) : {}; -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72ed0aa1f..3dce134e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -412,9 +412,6 @@ importers: decimal.js: specifier: ^10.4.2 version: 10.4.3 - deepcopy: - specifier: ^2.1.0 - version: 2.1.0 deepmerge: specifier: ^4.3.1 version: 4.3.1 @@ -4326,9 +4323,6 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} - deepcopy@2.1.0: - resolution: {integrity: sha512-8cZeTb1ZKC3bdSCP6XOM1IsTczIO73fdqtwa2B0N15eAz7gmyhQo+mc5gnFuulsgN3vIQYmTgbmQVKalH1dKvQ==} - deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -12866,10 +12860,6 @@ snapshots: deep-is@0.1.4: {} - deepcopy@2.1.0: - dependencies: - type-detect: 4.0.8 - deepmerge@4.3.1: {} default-browser-id@5.0.0: {} diff --git a/tests/regression/tests/issue-1533.test.ts b/tests/regression/tests/issue-1533.test.ts new file mode 100644 index 000000000..611011fc1 --- /dev/null +++ b/tests/regression/tests/issue-1533.test.ts @@ -0,0 +1,66 @@ +import { createPostgresDb, dropPostgresDb, loadSchema } from '@zenstackhq/testtools'; +describe('issue 1533', () => { + it('regression', async () => { + const dbUrl = await createPostgresDb('issue-1533'); + let prisma; + + try { + const r = await loadSchema( + ` + model Test { + id String @id @default(uuid()) @db.Uuid + metadata Json + @@allow('all', true) + } + `, + { provider: 'postgresql', dbUrl } + ); + + prisma = r.prisma; + const db = r.enhance(); + const Prisma = r.prismaModule; + + const testWithMetadata = await prisma.test.create({ + data: { + metadata: { + test: 'test', + }, + }, + }); + const testWithEmptyMetadata = await prisma.test.create({ + data: { + metadata: {}, + }, + }); + + let result = await db.test.findMany({ + where: { + metadata: { + path: ['test'], + equals: Prisma.DbNull, + }, + }, + }); + + expect(result).toHaveLength(1); + expect(result).toEqual(expect.arrayContaining([expect.objectContaining({ id: testWithEmptyMetadata.id })])); + + result = await db.test.findMany({ + where: { + metadata: { + path: ['test'], + equals: 'test', + }, + }, + }); + + expect(result).toHaveLength(1); + expect(result).toEqual(expect.arrayContaining([expect.objectContaining({ id: testWithMetadata.id })])); + } finally { + if (prisma) { + await prisma.$disconnect(); + } + await dropPostgresDb('issue-1533'); + } + }); +});