prisma-guard 1.31.0 → 1.32.1

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.
@@ -944,40 +944,75 @@ function unsupported() {
944
944
  return marker;
945
945
  }
946
946
 
947
- // src/shared/match-caller.ts
948
- function matchCallerPattern(patterns, caller) {
949
- if (typeof caller !== "string" || caller.trim().length === 0)
950
- return null;
951
- if (patterns.includes(caller))
952
- return caller;
947
+ // src/shared/guard-variant-routing.ts
948
+ function matchVariantPatterns(keys, caller) {
949
+ const callerParts = caller.split("/");
953
950
  const matches = [];
954
- for (const pattern of patterns) {
951
+ for (const pattern of keys) {
955
952
  if (!pattern.includes(":"))
956
953
  continue;
957
954
  const patternParts = pattern.split("/");
958
- const callerParts = caller.split("/");
959
955
  if (patternParts.length !== callerParts.length)
960
956
  continue;
961
- let ok = true;
962
- for (let i = 0; i < patternParts.length; i++) {
963
- if (patternParts[i].startsWith(":"))
957
+ let matchesCaller = true;
958
+ for (let index = 0; index < patternParts.length; index++) {
959
+ if (patternParts[index].startsWith(":"))
964
960
  continue;
965
- if (patternParts[i] !== callerParts[i]) {
966
- ok = false;
961
+ if (patternParts[index] !== callerParts[index]) {
962
+ matchesCaller = false;
967
963
  break;
968
964
  }
969
965
  }
970
- if (ok)
966
+ if (matchesCaller)
971
967
  matches.push(pattern);
972
968
  }
973
- if (matches.length === 0)
974
- return null;
969
+ return matches;
970
+ }
971
+ function resolveGuardVariantKey(input) {
972
+ if (input.kind === "single") {
973
+ return { ok: true, key: "_default" };
974
+ }
975
+ const { keys, caller, reservedKeys } = input;
976
+ for (const key of keys) {
977
+ if (reservedKeys.has(key)) {
978
+ return {
979
+ ok: false,
980
+ code: "reserved-key",
981
+ key,
982
+ keys
983
+ };
984
+ }
985
+ }
986
+ const hasDefault = keys.includes("default");
987
+ if (typeof caller !== "string") {
988
+ if (hasDefault)
989
+ return { ok: true, key: "default" };
990
+ return { ok: false, code: "missing-caller", keys };
991
+ }
992
+ if (caller.trim().length === 0) {
993
+ if (hasDefault)
994
+ return { ok: true, key: "default" };
995
+ return { ok: false, code: "unknown-caller", caller, keys };
996
+ }
997
+ if (keys.includes(caller)) {
998
+ return { ok: true, key: caller };
999
+ }
1000
+ const matches = matchVariantPatterns(keys, caller);
1001
+ if (matches.length === 1) {
1002
+ return { ok: true, key: matches[0] };
1003
+ }
975
1004
  if (matches.length > 1) {
976
- throw new CallerError(
977
- `Ambiguous caller "${caller}" matches multiple patterns: ${matches.map((p) => `"${p}"`).join(", ")}`
978
- );
1005
+ return {
1006
+ ok: false,
1007
+ code: "ambiguous-caller",
1008
+ caller,
1009
+ keys,
1010
+ matches
1011
+ };
979
1012
  }
980
- return matches[0];
1013
+ if (hasDefault)
1014
+ return { ok: true, key: "default" };
1015
+ return { ok: false, code: "unknown-caller", caller, keys };
981
1016
  }
982
1017
 
983
1018
  // src/runtime/policy.ts
@@ -1451,12 +1486,15 @@ function applyForcedCountWhere(container, forcedCountWhere) {
1451
1486
  }
1452
1487
  function resolvedWhereCoversConstraint(where, constraint) {
1453
1488
  if (constraint.fields.length === 1) {
1454
- return constraint.fields[0] in where;
1489
+ const value2 = where[constraint.fields[0]];
1490
+ return value2 !== null && value2 !== void 0;
1455
1491
  }
1456
1492
  const value = where[constraint.selector];
1457
1493
  if (!isPlainObject(value))
1458
1494
  return false;
1459
- return constraint.fields.every((field) => field in value);
1495
+ return constraint.fields.every(
1496
+ (field) => value[field] !== null && value[field] !== void 0
1497
+ );
1460
1498
  }
1461
1499
  function validateResolvedUniqueWhere(model, where, method, uniqueMap) {
1462
1500
  const constraints = uniqueMap[model];
@@ -1483,17 +1521,16 @@ function assertDirectUniqueShapeValue(model, field, value, typeMap) {
1483
1521
  );
1484
1522
  }
1485
1523
  }
1486
- if (isForcedValue(value))
1487
- return;
1488
- if (isPlainObject(value)) {
1489
- const keys = Object.keys(value);
1524
+ const actual = isForcedValue(value) ? value.value : value;
1525
+ if (isPlainObject(actual)) {
1526
+ const keys = Object.keys(actual);
1490
1527
  throw new ShapeError(
1491
1528
  `Invalid unique where shape for "${model ?? "unknown"}.${field}". Prisma WhereUniqueInput does not accept filter operator objects${keys.length ? `: ${keys.join(", ")}` : ""}. Use { ${field}: true } in guard shape and { ${field}: value } in request args.`
1492
1529
  );
1493
1530
  }
1494
- if (value === null || value === void 0) {
1531
+ if (actual === null || actual === void 0) {
1495
1532
  throw new ShapeError(
1496
- `Invalid unique where shape for "${model ?? "unknown"}.${field}". Unique fields must use true or a forced value.`
1533
+ `Invalid unique where shape for "${model ?? "unknown"}.${field}". Unique fields must use true or a forced non-null value.`
1497
1534
  );
1498
1535
  }
1499
1536
  }
@@ -1507,9 +1544,8 @@ function shapeCoversConstraint(where, constraint, model, typeMap) {
1507
1544
  }
1508
1545
  if (!(constraint.selector in where))
1509
1546
  return false;
1510
- const selectorValue = where[constraint.selector];
1511
- if (isForcedValue(selectorValue))
1512
- return true;
1547
+ const rawSelectorValue = where[constraint.selector];
1548
+ const selectorValue = isForcedValue(rawSelectorValue) ? rawSelectorValue.value : rawSelectorValue;
1513
1549
  if (!isPlainObject(selectorValue)) {
1514
1550
  throw new ShapeError(
1515
1551
  `Compound unique selector "${model}.${constraint.selector}" must be an object with fields: ${constraint.fields.join(", ")}`
@@ -2012,8 +2048,18 @@ function createWhereBuilder(typeMap, enumMap, scalarBase, uniqueMap = {}) {
2012
2048
  (constraint) => constraint.selector === selector
2013
2049
  ) ?? null;
2014
2050
  }
2015
- function parseForcedUniqueValue(model, fieldName, fieldMeta, value) {
2051
+ function buildUniqueWhereFieldSchema(fieldMeta, allowNullableFilter) {
2016
2052
  const schema = buildDirectScalarSchema(fieldMeta, enumMap, scalarBase);
2053
+ if (allowNullableFilter && !fieldMeta.isRequired) {
2054
+ return schema.nullable();
2055
+ }
2056
+ return schema;
2057
+ }
2058
+ function parseForcedUniqueValue(model, fieldName, fieldMeta, value, allowNullableFilter) {
2059
+ const schema = buildUniqueWhereFieldSchema(
2060
+ fieldMeta,
2061
+ allowNullableFilter
2062
+ );
2017
2063
  const actual = isForcedValue(value) ? value.value : value;
2018
2064
  try {
2019
2065
  return schema.parse(actual);
@@ -2091,10 +2137,9 @@ function createWhereBuilder(typeMap, enumMap, scalarBase, uniqueMap = {}) {
2091
2137
  `Invalid compound unique where shape for "${model}.${key}.${fieldName}". Prisma compound unique selectors do not accept filter operator objects${operators.length ? `: ${operators.join(", ")}` : ""}. Use direct values only.`
2092
2138
  );
2093
2139
  }
2094
- const directSchema2 = buildDirectScalarSchema(
2140
+ const directSchema2 = buildUniqueWhereFieldSchema(
2095
2141
  fieldMeta2,
2096
- enumMap,
2097
- scalarBase
2142
+ false
2098
2143
  );
2099
2144
  if (fieldValue === true) {
2100
2145
  nestedSchemas[fieldName] = directSchema2;
@@ -2103,7 +2148,8 @@ function createWhereBuilder(typeMap, enumMap, scalarBase, uniqueMap = {}) {
2103
2148
  model,
2104
2149
  fieldName,
2105
2150
  fieldMeta2,
2106
- fieldValue
2151
+ fieldValue,
2152
+ false
2107
2153
  );
2108
2154
  }
2109
2155
  }
@@ -2142,7 +2188,7 @@ function createWhereBuilder(typeMap, enumMap, scalarBase, uniqueMap = {}) {
2142
2188
  `Invalid unique where shape for "${model}.${key}". Prisma WhereUniqueInput does not accept operators: ${keys.join(", ")}. Use a direct unique value shape, for example { ${key}: true }.`
2143
2189
  );
2144
2190
  }
2145
- const directSchema = buildDirectScalarSchema(fieldMeta, enumMap, scalarBase);
2191
+ const directSchema = buildUniqueWhereFieldSchema(fieldMeta, true);
2146
2192
  if (value === true) {
2147
2193
  fieldSchemas[key] = directSchema;
2148
2194
  continue;
@@ -2151,7 +2197,8 @@ function createWhereBuilder(typeMap, enumMap, scalarBase, uniqueMap = {}) {
2151
2197
  model,
2152
2198
  key,
2153
2199
  fieldMeta,
2154
- value
2200
+ value,
2201
+ true
2155
2202
  );
2156
2203
  forcedOnlyKeys.add(key);
2157
2204
  }
@@ -3410,22 +3457,27 @@ function createQueryBuilder(typeMap, enumMap, uniqueMap, scalarBase) {
3410
3457
  forcedSelectCountWhere
3411
3458
  };
3412
3459
  }
3413
- function matchCaller(shapes, caller) {
3414
- const matched = matchCallerPattern(Object.keys(shapes), caller);
3415
- if (!matched)
3416
- return null;
3417
- return { key: matched, shape: shapes[matched] };
3418
- }
3419
- function resolveDefaultShape(config, model, method, normalizedBody, opts, builtCache, isUnique) {
3420
- const shapeOrFn = config.default;
3421
- let built;
3422
- if (typeof shapeOrFn === "function") {
3423
- const resolved = resolveAndValidateShape(shapeOrFn, opts?.ctx);
3424
- built = buildShapeZodSchema(model, method, resolved);
3425
- } else {
3426
- built = builtCache.get("default");
3460
+ function throwQueryVariantResolution(resolution) {
3461
+ const keys = resolution.keys.map((key) => `"${key}"`).join(", ");
3462
+ if (resolution.code === "reserved-key") {
3463
+ throw new ShapeError(
3464
+ `Caller key "${resolution.key}" collides with reserved shape config key. Rename the caller path.`
3465
+ );
3466
+ }
3467
+ if (resolution.code === "missing-caller") {
3468
+ throw new CallerError(
3469
+ `Missing caller. This query uses named shape routing with keys: ${keys}. Provide caller via opts.caller.`
3470
+ );
3427
3471
  }
3428
- return applyBuiltShape(built, normalizedBody, isUnique);
3472
+ if (resolution.code === "ambiguous-caller") {
3473
+ const matches = (resolution.matches ?? []).map((pattern) => `"${pattern}"`).join(", ");
3474
+ throw new CallerError(
3475
+ `Ambiguous caller "${resolution.caller}" matches multiple patterns: ${matches}`
3476
+ );
3477
+ }
3478
+ throw new CallerError(
3479
+ `Unknown caller: "${resolution.caller}". Allowed: ${keys}`
3480
+ );
3429
3481
  }
3430
3482
  function buildQuerySchema(model, method, config) {
3431
3483
  const isSingle = typeof config === "function" || isShapeConfig(config);
@@ -3437,12 +3489,15 @@ function createQueryBuilder(typeMap, enumMap, uniqueMap, scalarBase) {
3437
3489
  );
3438
3490
  }
3439
3491
  if (!isSingle) {
3440
- for (const key of Object.keys(config)) {
3441
- if (SHAPE_CONFIG_KEYS.has(key)) {
3442
- throw new ShapeError(
3443
- `Caller key "${key}" collides with reserved shape config key. Rename the caller path.`
3444
- );
3445
- }
3492
+ const keys = Object.keys(config);
3493
+ const validation = resolveGuardVariantKey({
3494
+ kind: "named",
3495
+ keys,
3496
+ caller: void 0,
3497
+ reservedKeys: SHAPE_CONFIG_KEYS
3498
+ });
3499
+ if (!validation.ok && validation.code === "reserved-key") {
3500
+ throwQueryVariantResolution(validation);
3446
3501
  }
3447
3502
  for (const [key, shapeOrFn] of Object.entries(
3448
3503
  config
@@ -3480,50 +3535,37 @@ function createQueryBuilder(typeMap, enumMap, uniqueMap, scalarBase) {
3480
3535
  );
3481
3536
  }
3482
3537
  const namedConfig = config;
3483
- const caller = opts?.caller;
3484
- if (typeof caller !== "string") {
3485
- if ("default" in namedConfig) {
3486
- return resolveDefaultShape(
3487
- namedConfig,
3488
- model,
3489
- method,
3490
- normalizedBody,
3491
- opts,
3492
- builtCache,
3493
- isUnique
3494
- );
3495
- }
3496
- const allowed = Object.keys(namedConfig);
3497
- throw new CallerError(
3498
- `Missing caller. This query uses named shape routing with keys: ${allowed.map((k) => `"${k}"`).join(", ")}. Provide caller via opts.caller.`
3499
- );
3500
- }
3501
- const matched = matchCaller(namedConfig, caller);
3502
- if (!matched) {
3503
- if ("default" in namedConfig) {
3504
- return resolveDefaultShape(
3505
- namedConfig,
3506
- model,
3507
- method,
3508
- normalizedBody,
3509
- opts,
3510
- builtCache,
3511
- isUnique
3512
- );
3513
- }
3514
- const allowed = Object.keys(namedConfig);
3515
- throw new CallerError(
3516
- `Unknown caller: "${caller}". Allowed: ${allowed.map((k) => `"${k}"`).join(", ")}`
3517
- );
3518
- }
3519
- if (typeof matched.shape === "function") {
3520
- const resolved = resolveAndValidateShape(matched.shape, opts?.ctx);
3538
+ const resolution = resolveGuardVariantKey({
3539
+ kind: "named",
3540
+ keys: Object.keys(namedConfig),
3541
+ caller: opts?.caller,
3542
+ reservedKeys: SHAPE_CONFIG_KEYS
3543
+ });
3544
+ if (!resolution.ok)
3545
+ throwQueryVariantResolution(resolution);
3546
+ const shapeOrFn = namedConfig[resolution.key];
3547
+ if (typeof shapeOrFn === "function") {
3548
+ const resolved = resolveAndValidateShape(shapeOrFn, opts?.ctx);
3521
3549
  built = buildShapeZodSchema(model, method, resolved);
3522
3550
  } else {
3523
- built = builtCache.get(matched.key);
3551
+ built = builtCache.get(resolution.key);
3524
3552
  }
3525
3553
  }
3526
- return applyBuiltShape(built, normalizedBody, isUnique);
3554
+ const result = applyBuiltShape(
3555
+ built,
3556
+ normalizedBody,
3557
+ isUnique,
3558
+ model
3559
+ );
3560
+ if (isUnique && result.where) {
3561
+ validateResolvedUniqueWhere(
3562
+ model,
3563
+ result.where,
3564
+ method,
3565
+ uniqueMap
3566
+ );
3567
+ }
3568
+ return result;
3527
3569
  }
3528
3570
  };
3529
3571
  }
@@ -4755,34 +4797,43 @@ function resolveDynamicShape(shapeFn, ctx, context) {
4755
4797
  }
4756
4798
  return result;
4757
4799
  }
4758
- function resolveNamedShape(input, body, contextFn, explicitCaller) {
4759
- assertNoCallerInBody(body);
4760
- const caller = explicitCaller;
4761
- const keys = Object.keys(input);
4762
- for (const key of keys) {
4763
- if (GUARD_SHAPE_KEYS.has(key)) {
4764
- throw new ShapeError(
4765
- `Caller key "${key}" collides with reserved guard shape key. Rename the caller path.`
4766
- );
4767
- }
4800
+ function throwVariantResolution(resolution) {
4801
+ const keys = resolution.keys.map((key) => `"${key}"`).join(", ");
4802
+ if (resolution.code === "reserved-key") {
4803
+ throw new ShapeError(
4804
+ `Caller key "${resolution.key}" collides with reserved guard shape key. Rename the caller path.`
4805
+ );
4768
4806
  }
4769
- if (typeof caller !== "string") {
4770
- if ("default" in input) {
4771
- return resolveShapeEntry(input.default, body, contextFn, "default");
4772
- }
4807
+ if (resolution.code === "missing-caller") {
4773
4808
  throw new CallerError(
4774
- `Missing caller. This guard uses named shape routing with keys: ${keys.map((k) => `"${k}"`).join(", ")}. Provide caller via guard(input, caller).`
4809
+ `Missing caller. This guard uses named shape routing with keys: ${keys}. Provide caller via guard(input, caller).`
4775
4810
  );
4776
4811
  }
4777
- const matched = matchCallerPattern(keys, caller);
4778
- if (matched) {
4779
- return resolveShapeEntry(input[matched], body, contextFn, matched);
4780
- }
4781
- if ("default" in input) {
4782
- return resolveShapeEntry(input.default, body, contextFn, "default");
4812
+ if (resolution.code === "ambiguous-caller") {
4813
+ const matches = (resolution.matches ?? []).map((pattern) => `"${pattern}"`).join(", ");
4814
+ throw new CallerError(
4815
+ `Ambiguous caller "${resolution.caller}" matches multiple patterns: ${matches}`
4816
+ );
4783
4817
  }
4784
4818
  throw new CallerError(
4785
- `Unknown caller: "${caller}". Allowed: ${keys.map((k) => `"${k}"`).join(", ")}`
4819
+ `Unknown caller: "${resolution.caller}". Allowed: ${keys}`
4820
+ );
4821
+ }
4822
+ function resolveNamedShape(input, body, contextFn, explicitCaller) {
4823
+ assertNoCallerInBody(body);
4824
+ const resolution = resolveGuardVariantKey({
4825
+ kind: "named",
4826
+ keys: Object.keys(input),
4827
+ caller: explicitCaller,
4828
+ reservedKeys: GUARD_SHAPE_KEYS
4829
+ });
4830
+ if (!resolution.ok)
4831
+ throwVariantResolution(resolution);
4832
+ return resolveShapeEntry(
4833
+ input[resolution.key],
4834
+ body,
4835
+ contextFn,
4836
+ resolution.key
4786
4837
  );
4787
4838
  }
4788
4839
  function resolveShapeEntry(entry, body, contextFn, matchedKey) {