prisma-guard 1.30.0 → 1.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -975,40 +975,75 @@ function unsupported() {
975
975
  return marker;
976
976
  }
977
977
 
978
- // src/shared/match-caller.ts
979
- function matchCallerPattern(patterns, caller) {
980
- if (typeof caller !== "string" || caller.trim().length === 0)
981
- return null;
982
- if (patterns.includes(caller))
983
- return caller;
978
+ // src/shared/guard-variant-routing.ts
979
+ function matchVariantPatterns(keys, caller) {
980
+ const callerParts = caller.split("/");
984
981
  const matches = [];
985
- for (const pattern of patterns) {
982
+ for (const pattern of keys) {
986
983
  if (!pattern.includes(":"))
987
984
  continue;
988
985
  const patternParts = pattern.split("/");
989
- const callerParts = caller.split("/");
990
986
  if (patternParts.length !== callerParts.length)
991
987
  continue;
992
- let ok = true;
993
- for (let i = 0; i < patternParts.length; i++) {
994
- if (patternParts[i].startsWith(":"))
988
+ let matchesCaller = true;
989
+ for (let index = 0; index < patternParts.length; index++) {
990
+ if (patternParts[index].startsWith(":"))
995
991
  continue;
996
- if (patternParts[i] !== callerParts[i]) {
997
- ok = false;
992
+ if (patternParts[index] !== callerParts[index]) {
993
+ matchesCaller = false;
998
994
  break;
999
995
  }
1000
996
  }
1001
- if (ok)
997
+ if (matchesCaller)
1002
998
  matches.push(pattern);
1003
999
  }
1004
- if (matches.length === 0)
1005
- return null;
1000
+ return matches;
1001
+ }
1002
+ function resolveGuardVariantKey(input) {
1003
+ if (input.kind === "single") {
1004
+ return { ok: true, key: "_default" };
1005
+ }
1006
+ const { keys, caller, reservedKeys } = input;
1007
+ for (const key of keys) {
1008
+ if (reservedKeys.has(key)) {
1009
+ return {
1010
+ ok: false,
1011
+ code: "reserved-key",
1012
+ key,
1013
+ keys
1014
+ };
1015
+ }
1016
+ }
1017
+ const hasDefault = keys.includes("default");
1018
+ if (typeof caller !== "string") {
1019
+ if (hasDefault)
1020
+ return { ok: true, key: "default" };
1021
+ return { ok: false, code: "missing-caller", keys };
1022
+ }
1023
+ if (caller.trim().length === 0) {
1024
+ if (hasDefault)
1025
+ return { ok: true, key: "default" };
1026
+ return { ok: false, code: "unknown-caller", caller, keys };
1027
+ }
1028
+ if (keys.includes(caller)) {
1029
+ return { ok: true, key: caller };
1030
+ }
1031
+ const matches = matchVariantPatterns(keys, caller);
1032
+ if (matches.length === 1) {
1033
+ return { ok: true, key: matches[0] };
1034
+ }
1006
1035
  if (matches.length > 1) {
1007
- throw new CallerError(
1008
- `Ambiguous caller "${caller}" matches multiple patterns: ${matches.map((p) => `"${p}"`).join(", ")}`
1009
- );
1036
+ return {
1037
+ ok: false,
1038
+ code: "ambiguous-caller",
1039
+ caller,
1040
+ keys,
1041
+ matches
1042
+ };
1010
1043
  }
1011
- return matches[0];
1044
+ if (hasDefault)
1045
+ return { ok: true, key: "default" };
1046
+ return { ok: false, code: "unknown-caller", caller, keys };
1012
1047
  }
1013
1048
 
1014
1049
  // src/runtime/policy.ts
@@ -3441,22 +3476,27 @@ function createQueryBuilder(typeMap, enumMap, uniqueMap, scalarBase) {
3441
3476
  forcedSelectCountWhere
3442
3477
  };
3443
3478
  }
3444
- function matchCaller(shapes, caller) {
3445
- const matched = matchCallerPattern(Object.keys(shapes), caller);
3446
- if (!matched)
3447
- return null;
3448
- return { key: matched, shape: shapes[matched] };
3449
- }
3450
- function resolveDefaultShape(config, model, method, normalizedBody, opts, builtCache, isUnique) {
3451
- const shapeOrFn = config.default;
3452
- let built;
3453
- if (typeof shapeOrFn === "function") {
3454
- const resolved = resolveAndValidateShape(shapeOrFn, opts?.ctx);
3455
- built = buildShapeZodSchema(model, method, resolved);
3456
- } else {
3457
- built = builtCache.get("default");
3479
+ function throwQueryVariantResolution(resolution) {
3480
+ const keys = resolution.keys.map((key) => `"${key}"`).join(", ");
3481
+ if (resolution.code === "reserved-key") {
3482
+ throw new ShapeError(
3483
+ `Caller key "${resolution.key}" collides with reserved shape config key. Rename the caller path.`
3484
+ );
3485
+ }
3486
+ if (resolution.code === "missing-caller") {
3487
+ throw new CallerError(
3488
+ `Missing caller. This query uses named shape routing with keys: ${keys}. Provide caller via opts.caller.`
3489
+ );
3458
3490
  }
3459
- return applyBuiltShape(built, normalizedBody, isUnique);
3491
+ if (resolution.code === "ambiguous-caller") {
3492
+ const matches = (resolution.matches ?? []).map((pattern) => `"${pattern}"`).join(", ");
3493
+ throw new CallerError(
3494
+ `Ambiguous caller "${resolution.caller}" matches multiple patterns: ${matches}`
3495
+ );
3496
+ }
3497
+ throw new CallerError(
3498
+ `Unknown caller: "${resolution.caller}". Allowed: ${keys}`
3499
+ );
3460
3500
  }
3461
3501
  function buildQuerySchema(model, method, config) {
3462
3502
  const isSingle = typeof config === "function" || isShapeConfig(config);
@@ -3468,12 +3508,15 @@ function createQueryBuilder(typeMap, enumMap, uniqueMap, scalarBase) {
3468
3508
  );
3469
3509
  }
3470
3510
  if (!isSingle) {
3471
- for (const key of Object.keys(config)) {
3472
- if (SHAPE_CONFIG_KEYS.has(key)) {
3473
- throw new ShapeError(
3474
- `Caller key "${key}" collides with reserved shape config key. Rename the caller path.`
3475
- );
3476
- }
3511
+ const keys = Object.keys(config);
3512
+ const validation = resolveGuardVariantKey({
3513
+ kind: "named",
3514
+ keys,
3515
+ caller: void 0,
3516
+ reservedKeys: SHAPE_CONFIG_KEYS
3517
+ });
3518
+ if (!validation.ok && validation.code === "reserved-key") {
3519
+ throwQueryVariantResolution(validation);
3477
3520
  }
3478
3521
  for (const [key, shapeOrFn] of Object.entries(
3479
3522
  config
@@ -3511,47 +3554,20 @@ function createQueryBuilder(typeMap, enumMap, uniqueMap, scalarBase) {
3511
3554
  );
3512
3555
  }
3513
3556
  const namedConfig = config;
3514
- const caller = opts?.caller;
3515
- if (typeof caller !== "string") {
3516
- if ("default" in namedConfig) {
3517
- return resolveDefaultShape(
3518
- namedConfig,
3519
- model,
3520
- method,
3521
- normalizedBody,
3522
- opts,
3523
- builtCache,
3524
- isUnique
3525
- );
3526
- }
3527
- const allowed = Object.keys(namedConfig);
3528
- throw new CallerError(
3529
- `Missing caller. This query uses named shape routing with keys: ${allowed.map((k) => `"${k}"`).join(", ")}. Provide caller via opts.caller.`
3530
- );
3531
- }
3532
- const matched = matchCaller(namedConfig, caller);
3533
- if (!matched) {
3534
- if ("default" in namedConfig) {
3535
- return resolveDefaultShape(
3536
- namedConfig,
3537
- model,
3538
- method,
3539
- normalizedBody,
3540
- opts,
3541
- builtCache,
3542
- isUnique
3543
- );
3544
- }
3545
- const allowed = Object.keys(namedConfig);
3546
- throw new CallerError(
3547
- `Unknown caller: "${caller}". Allowed: ${allowed.map((k) => `"${k}"`).join(", ")}`
3548
- );
3549
- }
3550
- if (typeof matched.shape === "function") {
3551
- const resolved = resolveAndValidateShape(matched.shape, opts?.ctx);
3557
+ const resolution = resolveGuardVariantKey({
3558
+ kind: "named",
3559
+ keys: Object.keys(namedConfig),
3560
+ caller: opts?.caller,
3561
+ reservedKeys: SHAPE_CONFIG_KEYS
3562
+ });
3563
+ if (!resolution.ok)
3564
+ throwQueryVariantResolution(resolution);
3565
+ const shapeOrFn = namedConfig[resolution.key];
3566
+ if (typeof shapeOrFn === "function") {
3567
+ const resolved = resolveAndValidateShape(shapeOrFn, opts?.ctx);
3552
3568
  built = buildShapeZodSchema(model, method, resolved);
3553
3569
  } else {
3554
- built = builtCache.get(matched.key);
3570
+ built = builtCache.get(resolution.key);
3555
3571
  }
3556
3572
  }
3557
3573
  return applyBuiltShape(built, normalizedBody, isUnique);
@@ -4141,12 +4157,39 @@ function validateAllowedKeys(value, allowed, method, kind) {
4141
4157
  return `Shape key "${key}" not valid for ${method}. Allowed: ${[...allowed].join(", ")}`;
4142
4158
  });
4143
4159
  }
4160
+ var RELATION_OPS_COVERING_FK = /* @__PURE__ */ new Set([
4161
+ "connect",
4162
+ "connectOrCreate",
4163
+ "create"
4164
+ ]);
4165
+ function collectRelationCoveredFks(modelFields, dataConfig) {
4166
+ const covered = /* @__PURE__ */ new Set();
4167
+ for (const [fieldName, value] of Object.entries(dataConfig)) {
4168
+ const meta = modelFields[fieldName];
4169
+ if (!meta || !meta.isRelation)
4170
+ continue;
4171
+ const fks = meta.relationFromFields;
4172
+ if (!fks || fks.length === 0)
4173
+ continue;
4174
+ if (!isPlainObject(value))
4175
+ continue;
4176
+ const covers = Object.keys(value).some(
4177
+ (op) => RELATION_OPS_COVERING_FK.has(op)
4178
+ );
4179
+ if (!covers)
4180
+ continue;
4181
+ for (const fk of fks)
4182
+ covered.add(fk);
4183
+ }
4184
+ return covered;
4185
+ }
4144
4186
  function validateCreateCompleteness(modelName, dataConfig, typeMap, scopeFks, zodDefaults) {
4145
4187
  const modelFields = typeMap[modelName];
4146
4188
  if (!modelFields)
4147
4189
  return;
4148
4190
  const zodDefaultFields = zodDefaults[modelName];
4149
4191
  const zodDefaultSet = zodDefaultFields ? new Set(zodDefaultFields) : void 0;
4192
+ const relationCoveredFks = collectRelationCoveredFks(modelFields, dataConfig);
4150
4193
  for (const [fieldName, meta] of Object.entries(modelFields)) {
4151
4194
  if (meta.isRelation)
4152
4195
  continue;
@@ -4160,10 +4203,12 @@ function validateCreateCompleteness(modelName, dataConfig, typeMap, scopeFks, zo
4160
4203
  continue;
4161
4204
  if (scopeFks.has(fieldName))
4162
4205
  continue;
4206
+ if (relationCoveredFks.has(fieldName))
4207
+ continue;
4163
4208
  if (zodDefaultSet && zodDefaultSet.has(fieldName))
4164
4209
  continue;
4165
4210
  throw new ShapeError(
4166
- `Required field "${fieldName}" on model "${modelName}" is missing from create data shape, has no default, and is not a scope FK`
4211
+ `Required field "${fieldName}" on model "${modelName}" is missing from create data shape, has no default, is not a scope FK, and is not covered by a relation write in the shape`
4167
4212
  );
4168
4213
  }
4169
4214
  }
@@ -4757,34 +4802,43 @@ function resolveDynamicShape(shapeFn, ctx, context) {
4757
4802
  }
4758
4803
  return result;
4759
4804
  }
4760
- function resolveNamedShape(input, body, contextFn, explicitCaller) {
4761
- assertNoCallerInBody(body);
4762
- const caller = explicitCaller;
4763
- const keys = Object.keys(input);
4764
- for (const key of keys) {
4765
- if (GUARD_SHAPE_KEYS.has(key)) {
4766
- throw new ShapeError(
4767
- `Caller key "${key}" collides with reserved guard shape key. Rename the caller path.`
4768
- );
4769
- }
4805
+ function throwVariantResolution(resolution) {
4806
+ const keys = resolution.keys.map((key) => `"${key}"`).join(", ");
4807
+ if (resolution.code === "reserved-key") {
4808
+ throw new ShapeError(
4809
+ `Caller key "${resolution.key}" collides with reserved guard shape key. Rename the caller path.`
4810
+ );
4770
4811
  }
4771
- if (typeof caller !== "string") {
4772
- if ("default" in input) {
4773
- return resolveShapeEntry(input.default, body, contextFn, "default");
4774
- }
4812
+ if (resolution.code === "missing-caller") {
4775
4813
  throw new CallerError(
4776
- `Missing caller. This guard uses named shape routing with keys: ${keys.map((k) => `"${k}"`).join(", ")}. Provide caller via guard(input, caller).`
4814
+ `Missing caller. This guard uses named shape routing with keys: ${keys}. Provide caller via guard(input, caller).`
4777
4815
  );
4778
4816
  }
4779
- const matched = matchCallerPattern(keys, caller);
4780
- if (matched) {
4781
- return resolveShapeEntry(input[matched], body, contextFn, matched);
4782
- }
4783
- if ("default" in input) {
4784
- return resolveShapeEntry(input.default, body, contextFn, "default");
4817
+ if (resolution.code === "ambiguous-caller") {
4818
+ const matches = (resolution.matches ?? []).map((pattern) => `"${pattern}"`).join(", ");
4819
+ throw new CallerError(
4820
+ `Ambiguous caller "${resolution.caller}" matches multiple patterns: ${matches}`
4821
+ );
4785
4822
  }
4786
4823
  throw new CallerError(
4787
- `Unknown caller: "${caller}". Allowed: ${keys.map((k) => `"${k}"`).join(", ")}`
4824
+ `Unknown caller: "${resolution.caller}". Allowed: ${keys}`
4825
+ );
4826
+ }
4827
+ function resolveNamedShape(input, body, contextFn, explicitCaller) {
4828
+ assertNoCallerInBody(body);
4829
+ const resolution = resolveGuardVariantKey({
4830
+ kind: "named",
4831
+ keys: Object.keys(input),
4832
+ caller: explicitCaller,
4833
+ reservedKeys: GUARD_SHAPE_KEYS
4834
+ });
4835
+ if (!resolution.ok)
4836
+ throwVariantResolution(resolution);
4837
+ return resolveShapeEntry(
4838
+ input[resolution.key],
4839
+ body,
4840
+ contextFn,
4841
+ resolution.key
4788
4842
  );
4789
4843
  }
4790
4844
  function resolveShapeEntry(entry, body, contextFn, matchedKey) {