diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 28feed481..009eab5db 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -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", @@ -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" } } diff --git a/packages/runtime/src/enhancements/node/delegate.ts b/packages/runtime/src/enhancements/node/delegate.ts index da9570bd4..9cfc720c5 100644 --- a/packages/runtime/src/enhancements/node/delegate.ts +++ b/packages/runtime/src/enhancements/node/delegate.ts @@ -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, @@ -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); @@ -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; @@ -337,6 +359,8 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { ); } + this.sanitizeMutationPayload(args.data); + if (isDelegateModel(this.options.modelMeta, this.model)) { throw prismaClientValidationError( this.prisma, @@ -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'); @@ -364,6 +406,8 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { ); } + this.sanitizeMutationPayload(args.data); + if (!this.involvesDelegateModel(this.model)) { return super.createMany(args); } @@ -403,6 +447,8 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { ); } + this.sanitizeMutationPayload(args.data); + if (!this.involvesDelegateModel(this.model)) { return super.createManyAndReturn(args); } @@ -589,6 +635,8 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { ); } + this.sanitizeMutationPayload(args.data); + if (!this.involvesDelegateModel(this.model)) { return super.update(args); } @@ -608,6 +656,8 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler { ); } + this.sanitizeMutationPayload(args.data); + if (!this.involvesDelegateModel(this.model)) { return super.updateMany(args); } @@ -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, diff --git a/packages/runtime/src/enhancements/node/omit.ts b/packages/runtime/src/enhancements/node/omit.ts index 18c81cc18..60e0251ac 100644 --- a/packages/runtime/src/enhancements/node/omit.ts +++ b/packages/runtime/src/enhancements/node/omit.ts @@ -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. @@ -21,8 +22,11 @@ export function withOmit(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 @@ -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; } diff --git a/packages/runtime/src/enhancements/node/policy/policy-utils.ts b/packages/runtime/src/enhancements/node/policy/policy-utils.ts index ab4ed8fc2..ed0ff5196 100644 --- a/packages/runtime/src/enhancements/node/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/node/policy/policy-utils.ts @@ -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( diff --git a/packages/runtime/src/enhancements/node/query-utils.ts b/packages/runtime/src/enhancements/node/query-utils.ts index 5d23c6d99..00d430696 100644 --- a/packages/runtime/src/enhancements/node/query-utils.ts +++ b/packages/runtime/src/enhancements/node/query-utils.ts @@ -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; + } } diff --git a/packages/schema/src/res/stdlib.zmodel b/packages/schema/src/res/stdlib.zmodel index c711e7404..a436ea4a8 100644 --- a/packages/schema/src/res/stdlib.zmodel +++ b/packages/schema/src/res/stdlib.zmodel @@ -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]) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 552ed617a..a4c516ce6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -433,6 +433,9 @@ importers: tiny-invariant: specifier: ^1.3.1 version: 1.3.3 + traverse: + specifier: ^0.6.10 + version: 0.6.10 ts-pattern: specifier: ^4.3.0 version: 4.3.0 @@ -464,6 +467,9 @@ importers: '@types/semver': specifier: ^7.3.13 version: 7.5.8 + '@types/traverse': + specifier: ^0.6.37 + version: 0.6.37 '@types/uuid': specifier: ^8.3.4 version: 8.3.4 @@ -3074,6 +3080,9 @@ packages: '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + '@types/traverse@0.6.37': + resolution: {integrity: sha512-c90MVeDiUI1FhOZ6rLQ3kDWr50YE8+paDpM+5zbHjbmsqEp2DlMYkqnZnwbK9oI+NvDe8yRajup4jFwnVX6xsA==} + '@types/uuid@8.3.4': resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} @@ -3519,6 +3528,10 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + arraybuffer.prototype.slice@1.0.3: + resolution: {integrity: sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==} + engines: {node: '>= 0.4'} + arrify@1.0.1: resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} engines: {node: '>=0.10.0'} @@ -4162,6 +4175,18 @@ packages: resolution: {integrity: sha512-Jy/tj3ldjZJo63sVAvg6LHt2mHvl4V6AgRAmNDtLdm7faqtsx+aJG42rsyCo9JCoRVKwPFzKlIPx3DIibwSIaQ==} engines: {node: '>=12'} + data-view-buffer@1.0.1: + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.1: + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.0: + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} + engines: {node: '>= 0.4'} + date-fns@2.30.0: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} @@ -4450,6 +4475,10 @@ packages: error-stack-parser-es@0.1.4: resolution: {integrity: sha512-l0uy0kAoo6toCgVOYaAayqtPa2a1L15efxUMEnQebKwLQX2X0OpS6wMMQdc4juJXmxd9i40DuaUHq+mjIya9TQ==} + es-abstract@1.23.3: + resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} + engines: {node: '>= 0.4'} + es-define-property@1.0.0: resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==} engines: {node: '>= 0.4'} @@ -4461,6 +4490,18 @@ packages: es-get-iterator@1.1.3: resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.2.1: + resolution: {integrity: sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==} + engines: {node: '>= 0.4'} + esbuild-android-64@0.15.18: resolution: {integrity: sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==} engines: {node: '>=12'} @@ -4917,6 +4958,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + function.prototype.name@1.1.6: + resolution: {integrity: sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==} + engines: {node: '>= 0.4'} + functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} @@ -4955,6 +5000,10 @@ packages: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} + get-symbol-description@1.0.2: + resolution: {integrity: sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==} + engines: {node: '>= 0.4'} + get-tsconfig@4.7.5: resolution: {integrity: sha512-ZCuZCnlqNzjb4QprAzXKdpp/gh6KTxSJuw3IBsPnV/7fV4NxC9ckB+vPTt8w7fJA0TaSD7c55BR47JD6MEDyDw==} @@ -5013,6 +5062,10 @@ packages: resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} engines: {node: '>=8'} + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + globby@11.1.0: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} @@ -5307,6 +5360,10 @@ packages: resolution: {integrity: sha512-a5dFJih5ZLYlRtDc0dZWP7RiKr6xIKzmn/oAYCDvdLThadVgyJwlaoQPmRtMSpz+rk0OGAgIu+TcM9HUF0fk1A==} engines: {node: '>= 0.4'} + is-data-view@1.0.1: + resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} + engines: {node: '>= 0.4'} + is-date-object@1.0.5: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} engines: {node: '>= 0.4'} @@ -5364,6 +5421,10 @@ packages: is-module@1.0.0: resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + is-number-object@1.0.7: resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} engines: {node: '>= 0.4'} @@ -5431,6 +5492,10 @@ packages: resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} engines: {node: '>= 0.4'} + is-typed-array@1.1.13: + resolution: {integrity: sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==} + engines: {node: '>= 0.4'} + is-unicode-supported@0.1.0: resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} engines: {node: '>=10'} @@ -5439,6 +5504,9 @@ packages: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} + is-weakref@1.0.2: + resolution: {integrity: sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==} + is-weakset@2.0.3: resolution: {integrity: sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==} engines: {node: '>= 0.4'} @@ -7240,6 +7308,10 @@ packages: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} + safe-array-concat@1.1.2: + resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} + engines: {node: '>=0.4'} + safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -7249,6 +7321,10 @@ packages: safe-json-stringify@1.2.0: resolution: {integrity: sha512-gH8eh2nZudPQO6TytOvbxnuhYBOvDBBLW52tz5q6X58lJcd/tkmqFR+5Z9adS8aJtURSXWThWy/xJtJwixErvg==} + safe-regex-test@1.0.3: + resolution: {integrity: sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==} + engines: {node: '>= 0.4'} + safe-regex2@3.1.0: resolution: {integrity: sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==} @@ -7518,6 +7594,17 @@ packages: resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} engines: {node: '>=12'} + string.prototype.trim@1.2.9: + resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.8: + resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + string_decoder@0.10.31: resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} @@ -7793,6 +7880,10 @@ packages: resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} engines: {node: '>=12'} + traverse@0.6.10: + resolution: {integrity: sha512-hN4uFRxbK+PX56DxYiGHsTn2dME3TVr9vbNqlQGcGcPhJAn+tdP126iA+TArMpI4YSgnTkMWyoLl5bf81Hi5TA==} + engines: {node: '>= 0.4'} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -7929,9 +8020,29 @@ packages: resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} engines: {node: '>= 0.6'} + typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.1: + resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.2: + resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.6: + resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} + engines: {node: '>= 0.4'} + typed-rest-client@1.8.11: resolution: {integrity: sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==} + typedarray.prototype.slice@1.0.3: + resolution: {integrity: sha512-8WbVAQAUlENo1q3c3zZYuy5k9VzBQvp8AX9WOtbvyWlLM1v5JaSRmjubLjzHF4JFtptjH/5c/i95yaElvcjC0A==} + engines: {node: '>= 0.4'} + typedarray@0.0.6: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} @@ -7965,6 +8076,9 @@ packages: ultrahtml@1.5.3: resolution: {integrity: sha512-GykOvZwgDWZlTQMtp5jrD4BVL+gNn2NVlVafjcFUJ7taY20tqYdwdoWBFy6GBJsNTZe1GkGPkSl5knQAjtgceg==} + unbox-primitive@1.0.2: + resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} + uncrypto@0.1.3: resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} @@ -11247,6 +11361,8 @@ snapshots: '@types/tough-cookie@4.0.5': {} + '@types/traverse@0.6.37': {} + '@types/uuid@8.3.4': {} '@types/vscode@1.90.0': {} @@ -11813,6 +11929,17 @@ snapshots: array-union@2.1.0: {} + arraybuffer.prototype.slice@1.0.3: + dependencies: + array-buffer-byte-length: 1.0.1 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + is-array-buffer: 3.0.4 + is-shared-array-buffer: 1.0.3 + arrify@1.0.1: {} asap@2.0.6: {} @@ -12571,6 +12698,24 @@ snapshots: whatwg-mimetype: 3.0.0 whatwg-url: 11.0.0 + data-view-buffer@1.0.1: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + data-view-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + + data-view-byte-offset@1.0.0: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + date-fns@2.30.0: dependencies: '@babel/runtime': 7.24.7 @@ -12808,6 +12953,55 @@ snapshots: error-stack-parser-es@0.1.4: {} + es-abstract@1.23.3: + dependencies: + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + data-view-buffer: 1.0.1 + data-view-byte-length: 1.0.1 + data-view-byte-offset: 1.0.0 + es-define-property: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.4 + get-symbol-description: 1.0.2 + globalthis: 1.0.4 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 + is-callable: 1.2.7 + is-data-view: 1.0.1 + is-negative-zero: 2.0.3 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + is-string: 1.0.7 + is-typed-array: 1.1.13 + is-weakref: 1.0.2 + object-inspect: 1.13.2 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.2 + safe-array-concat: 1.1.2 + safe-regex-test: 1.0.3 + string.prototype.trim: 1.2.9 + string.prototype.trimend: 1.0.8 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.1 + typed-array-byte-offset: 1.0.2 + typed-array-length: 1.0.6 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.15 + es-define-property@1.0.0: dependencies: get-intrinsic: 1.2.4 @@ -12826,6 +13020,22 @@ snapshots: isarray: 2.0.5 stop-iteration-iterator: 1.0.0 + es-object-atoms@1.0.0: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.0.3: + dependencies: + get-intrinsic: 1.2.4 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-to-primitive@1.2.1: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.0.5 + is-symbol: 1.0.4 + esbuild-android-64@0.15.18: optional: true @@ -13421,6 +13631,13 @@ snapshots: function-bind@1.1.2: {} + function.prototype.name@1.1.6: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + functions-have-names: 1.2.3 + functions-have-names@1.2.3: {} gauge@3.0.2: @@ -13457,6 +13674,12 @@ snapshots: get-stream@8.0.1: {} + get-symbol-description@1.0.2: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + get-intrinsic: 1.2.4 + get-tsconfig@4.7.5: dependencies: resolve-pkg-maps: 1.0.0 @@ -13534,6 +13757,11 @@ snapshots: dependencies: type-fest: 0.20.2 + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.0.1 + globby@11.1.0: dependencies: array-union: 2.1.0 @@ -13862,6 +14090,10 @@ snapshots: dependencies: hasown: 2.0.2 + is-data-view@1.0.1: + dependencies: + is-typed-array: 1.1.13 + is-date-object@1.0.5: dependencies: has-tostringtag: 1.0.2 @@ -13899,6 +14131,8 @@ snapshots: is-module@1.0.0: {} + is-negative-zero@2.0.3: {} + is-number-object@1.0.7: dependencies: has-tostringtag: 1.0.2 @@ -13954,10 +14188,18 @@ snapshots: dependencies: has-symbols: 1.0.3 + is-typed-array@1.1.13: + dependencies: + which-typed-array: 1.1.15 + is-unicode-supported@0.1.0: {} is-weakmap@2.0.2: {} + is-weakref@1.0.2: + dependencies: + call-bind: 1.0.7 + is-weakset@2.0.3: dependencies: call-bind: 1.0.7 @@ -16221,12 +16463,25 @@ snapshots: dependencies: mri: 1.2.0 + safe-array-concat@1.1.2: + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + isarray: 2.0.5 + safe-buffer@5.1.2: {} safe-buffer@5.2.1: {} safe-json-stringify@1.2.0: {} + safe-regex-test@1.0.3: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-regex: 1.1.4 + safe-regex2@3.1.0: dependencies: ret: 0.4.3 @@ -16506,6 +16761,25 @@ snapshots: emoji-regex: 9.2.2 strip-ansi: 7.1.0 + string.prototype.trim@1.2.9: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + + string.prototype.trimend@1.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + string_decoder@0.10.31: {} string_decoder@1.1.1: @@ -16803,6 +17077,12 @@ snapshots: dependencies: punycode: 2.3.1 + traverse@0.6.10: + dependencies: + gopd: 1.0.1 + typedarray.prototype.slice: 1.0.3 + which-typed-array: 1.1.15 + tree-kill@1.2.2: {} ts-api-utils@1.3.0(typescript@5.5.2): @@ -16933,12 +17213,53 @@ snapshots: media-typer: 0.3.0 mime-types: 2.1.35 + typed-array-buffer@1.0.2: + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-typed-array: 1.1.13 + + typed-array-byte-length@1.0.1: + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + + typed-array-byte-offset@1.0.2: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + + typed-array-length@1.0.6: + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 + typed-rest-client@1.8.11: dependencies: qs: 6.12.1 tunnel: 0.0.6 underscore: 1.13.6 + typedarray.prototype.slice@1.0.3: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-errors: 1.3.0 + typed-array-buffer: 1.0.2 + typed-array-byte-offset: 1.0.2 + typedarray@0.0.6: {} typescript@5.5.2: {} @@ -16959,6 +17280,13 @@ snapshots: ultrahtml@1.5.3: {} + unbox-primitive@1.0.2: + dependencies: + call-bind: 1.0.7 + has-bigints: 1.0.2 + has-symbols: 1.0.3 + which-boxed-primitive: 1.0.2 + uncrypto@0.1.3: {} unctx@2.3.1: diff --git a/tests/integration/tests/enhancements/with-delegate/omit-interaction.test.ts b/tests/integration/tests/enhancements/with-delegate/omit-interaction.test.ts new file mode 100644 index 000000000..7b4be2309 --- /dev/null +++ b/tests/integration/tests/enhancements/with-delegate/omit-interaction.test.ts @@ -0,0 +1,91 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('Polymorphic @omit', () => { + const model = ` + model User { + id Int @id @default(autoincrement()) + assets Asset[] + + @@allow('all', true) + } + + model Asset { + id Int @id @default(autoincrement()) + type String + foo String @omit + user User? @relation(fields: [userId], references: [id]) + userId Int? @unique + + @@delegate(type) + @@allow('all', true) + } + + model Post extends Asset { + title String + bar String @omit + } + `; + + it('omits when queried via a concrete model', async () => { + const { enhance } = await loadSchema(model); + + const db = enhance(); + const post = await db.post.create({ data: { foo: 'foo', bar: 'bar', title: 'Post1' } }); + expect(post.foo).toBeUndefined(); + expect(post.bar).toBeUndefined(); + + const foundPost = await db.post.findUnique({ where: { id: post.id } }); + expect(foundPost.foo).toBeUndefined(); + expect(foundPost.bar).toBeUndefined(); + }); + + it('omits when queried via a base model', async () => { + const { enhance } = await loadSchema(model); + + const db = enhance(); + const post = await db.post.create({ data: { foo: 'foo', bar: 'bar', title: 'Post1' } }); + expect(post.foo).toBeUndefined(); + expect(post.bar).toBeUndefined(); + + const foundAsset = await db.asset.findUnique({ where: { id: post.id } }); + expect(foundAsset.foo).toBeUndefined(); + expect(foundAsset.bar).toBeUndefined(); + expect(foundAsset.title).toBeTruthy(); + }); + + it('omits when discriminator is not selected', async () => { + const { enhance } = await loadSchema(model); + + const db = enhance(); + const post = await db.post.create({ + data: { foo: 'foo', bar: 'bar', title: 'Post1' }, + }); + expect(post.foo).toBeUndefined(); + expect(post.bar).toBeUndefined(); + + const foundAsset = await db.asset.findUnique({ + where: { id: post.id }, + select: { id: true, foo: true }, + }); + console.log(foundAsset); + expect(foundAsset.foo).toBeUndefined(); + expect(foundAsset.bar).toBeUndefined(); + }); + + it('omits when queried in a nested context', async () => { + const { enhance } = await loadSchema(model); + + const db = enhance(); + const user = await db.user.create({ data: {} }); + const post = await db.post.create({ + data: { foo: 'foo', bar: 'bar', title: 'Post1', user: { connect: { id: user.id } } }, + }); + expect(post.foo).toBeUndefined(); + expect(post.bar).toBeUndefined(); + + const foundUser = await db.user.findUnique({ where: { id: user.id }, include: { assets: true } }); + expect(foundUser.assets[0].foo).toBeUndefined(); + expect(foundUser.assets[0].bar).toBeUndefined(); + expect(foundUser.assets[0].title).toBeTruthy(); + }); +}); diff --git a/tests/integration/tests/enhancements/with-delegate/policy-interaction.test.ts b/tests/integration/tests/enhancements/with-delegate/policy-interaction.test.ts index f84e3c603..275e73853 100644 --- a/tests/integration/tests/enhancements/with-delegate/policy-interaction.test.ts +++ b/tests/integration/tests/enhancements/with-delegate/policy-interaction.test.ts @@ -268,4 +268,45 @@ describe('Polymorphic Policy Test', () => { user1Db.ratedPost.update({ where: { id: post1.id }, data: { comments: { connect: { id: comment1.id } } } }) ).toResolveTruthy(); }); + + it('respects field-level policies', async () => { + const { enhance } = await loadSchema(` + model User { + id Int @id @default(autoincrement()) + } + + model Asset { + id Int @id @default(autoincrement()) + type String + foo String @allow('read', auth().id == 1) + + @@delegate(type) + @@allow('all', true) + } + + model Post extends Asset { + title String + bar String @deny('read', auth().id != 1) + } + `); + + const db = enhance({ id: 1 }); + const post = await db.post.create({ data: { foo: 'foo', bar: 'bar', title: 'Post1' } }); + expect(post.foo).toBeTruthy(); + expect(post.bar).toBeTruthy(); + + const foundPost = await db.post.findUnique({ where: { id: post.id } }); + expect(foundPost.foo).toBeTruthy(); + expect(foundPost.bar).toBeTruthy(); + + const db2 = enhance({ id: 2 }); + const post2 = await db2.post.create({ data: { foo: 'foo', bar: 'bar', title: 'Post2' } }); + expect(post2.title).toBeTruthy(); + expect(post2.foo).toBeUndefined(); + expect(post2.bar).toBeUndefined(); + + const foundPost2 = await db2.post.findUnique({ where: { id: post2.id } }); + expect(foundPost2.foo).toBeUndefined(); + expect(foundPost2.bar).toBeUndefined(); + }); }); diff --git a/tests/integration/tests/enhancements/with-delegate/validation.test.ts b/tests/integration/tests/enhancements/with-delegate/validation.test.ts new file mode 100644 index 000000000..8e729c337 --- /dev/null +++ b/tests/integration/tests/enhancements/with-delegate/validation.test.ts @@ -0,0 +1,26 @@ +import { loadSchema } from '@zenstackhq/testtools'; +describe('Polymorphic input validation', () => { + it('rejects aux fields in mutation', async () => { + const { enhance } = await loadSchema( + ` + model Asset { + id Int @id @default(autoincrement()) + type String + + @@delegate(type) + @@allow('all', true) + } + + model Post extends Asset { + title String + } + ` + ); + + const db = enhance(); + const asset = await db.post.create({ data: { title: 'Post1' } }); + await expect( + db.asset.update({ where: { id: asset.id }, data: { delegate_aux_post: { update: { title: 'Post2' } } } }) + ).rejects.toThrow('Auxiliary relation field "delegate_aux_post" cannot be set directly'); + }); +}); diff --git a/tests/regression/tests/issue-1710.test.ts b/tests/regression/tests/issue-1710.test.ts new file mode 100644 index 000000000..796c403b4 --- /dev/null +++ b/tests/regression/tests/issue-1710.test.ts @@ -0,0 +1,53 @@ +import { loadSchema } from '@zenstackhq/testtools'; +describe('issue 1710', () => { + it('regression', async () => { + const { enhance } = await loadSchema( + ` + model Profile { + id String @id @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + displayName String + type String + + @@delegate(type) + @@allow('read,create', true) + } + + model User extends Profile { + email String @unique @deny('read', true) + password String @omit + role String @default('USER') @deny('read,update', true) + } + + model Organization extends Profile {} + ` + ); + + const db = enhance(); + const user = await db.user.create({ + data: { displayName: 'User1', email: 'a@b.com', password: '123' }, + }); + expect(user.email).toBeUndefined(); + expect(user.password).toBeUndefined(); + + const foundUser = await db.profile.findUnique({ where: { id: user.id } }); + expect(foundUser.email).toBeUndefined(); + expect(foundUser.password).toBeUndefined(); + + await expect( + db.profile.update({ + where: { + id: user.id, + }, + data: { + delegate_aux_user: { + update: { + role: 'ADMIN', + }, + }, + }, + }) + ).rejects.toThrow('Auxiliary relation field'); + }); +});