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.
@@ -13,6 +13,7 @@ interface FieldMeta {
13
13
  isEnum?: boolean;
14
14
  isUnique?: boolean;
15
15
  isUnsupported?: boolean;
16
+ relationFromFields?: readonly string[];
16
17
  }
17
18
  interface GuardLogger {
18
19
  warn(message: string): void;
@@ -228,6 +229,7 @@ interface FieldMetaConst {
228
229
  readonly isEnum?: boolean;
229
230
  readonly isUnique?: boolean;
230
231
  readonly isUnsupported?: boolean;
232
+ readonly relationFromFields?: readonly string[];
231
233
  }
232
234
  interface UniqueConstraintConst {
233
235
  readonly selector: string;
@@ -13,6 +13,7 @@ interface FieldMeta {
13
13
  isEnum?: boolean;
14
14
  isUnique?: boolean;
15
15
  isUnsupported?: boolean;
16
+ relationFromFields?: readonly string[];
16
17
  }
17
18
  interface GuardLogger {
18
19
  warn(message: string): void;
@@ -228,6 +229,7 @@ interface FieldMetaConst {
228
229
  readonly isEnum?: boolean;
229
230
  readonly isUnique?: boolean;
230
231
  readonly isUnsupported?: boolean;
232
+ readonly relationFromFields?: readonly string[];
231
233
  }
232
234
  interface UniqueConstraintConst {
233
235
  readonly selector: string;
@@ -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
@@ -3410,22 +3445,27 @@ function createQueryBuilder(typeMap, enumMap, uniqueMap, scalarBase) {
3410
3445
  forcedSelectCountWhere
3411
3446
  };
3412
3447
  }
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");
3448
+ function throwQueryVariantResolution(resolution) {
3449
+ const keys = resolution.keys.map((key) => `"${key}"`).join(", ");
3450
+ if (resolution.code === "reserved-key") {
3451
+ throw new ShapeError(
3452
+ `Caller key "${resolution.key}" collides with reserved shape config key. Rename the caller path.`
3453
+ );
3454
+ }
3455
+ if (resolution.code === "missing-caller") {
3456
+ throw new CallerError(
3457
+ `Missing caller. This query uses named shape routing with keys: ${keys}. Provide caller via opts.caller.`
3458
+ );
3427
3459
  }
3428
- return applyBuiltShape(built, normalizedBody, isUnique);
3460
+ if (resolution.code === "ambiguous-caller") {
3461
+ const matches = (resolution.matches ?? []).map((pattern) => `"${pattern}"`).join(", ");
3462
+ throw new CallerError(
3463
+ `Ambiguous caller "${resolution.caller}" matches multiple patterns: ${matches}`
3464
+ );
3465
+ }
3466
+ throw new CallerError(
3467
+ `Unknown caller: "${resolution.caller}". Allowed: ${keys}`
3468
+ );
3429
3469
  }
3430
3470
  function buildQuerySchema(model, method, config) {
3431
3471
  const isSingle = typeof config === "function" || isShapeConfig(config);
@@ -3437,12 +3477,15 @@ function createQueryBuilder(typeMap, enumMap, uniqueMap, scalarBase) {
3437
3477
  );
3438
3478
  }
3439
3479
  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
- }
3480
+ const keys = Object.keys(config);
3481
+ const validation = resolveGuardVariantKey({
3482
+ kind: "named",
3483
+ keys,
3484
+ caller: void 0,
3485
+ reservedKeys: SHAPE_CONFIG_KEYS
3486
+ });
3487
+ if (!validation.ok && validation.code === "reserved-key") {
3488
+ throwQueryVariantResolution(validation);
3446
3489
  }
3447
3490
  for (const [key, shapeOrFn] of Object.entries(
3448
3491
  config
@@ -3480,47 +3523,20 @@ function createQueryBuilder(typeMap, enumMap, uniqueMap, scalarBase) {
3480
3523
  );
3481
3524
  }
3482
3525
  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);
3526
+ const resolution = resolveGuardVariantKey({
3527
+ kind: "named",
3528
+ keys: Object.keys(namedConfig),
3529
+ caller: opts?.caller,
3530
+ reservedKeys: SHAPE_CONFIG_KEYS
3531
+ });
3532
+ if (!resolution.ok)
3533
+ throwQueryVariantResolution(resolution);
3534
+ const shapeOrFn = namedConfig[resolution.key];
3535
+ if (typeof shapeOrFn === "function") {
3536
+ const resolved = resolveAndValidateShape(shapeOrFn, opts?.ctx);
3521
3537
  built = buildShapeZodSchema(model, method, resolved);
3522
3538
  } else {
3523
- built = builtCache.get(matched.key);
3539
+ built = builtCache.get(resolution.key);
3524
3540
  }
3525
3541
  }
3526
3542
  return applyBuiltShape(built, normalizedBody, isUnique);
@@ -4110,12 +4126,39 @@ function validateAllowedKeys(value, allowed, method, kind) {
4110
4126
  return `Shape key "${key}" not valid for ${method}. Allowed: ${[...allowed].join(", ")}`;
4111
4127
  });
4112
4128
  }
4129
+ var RELATION_OPS_COVERING_FK = /* @__PURE__ */ new Set([
4130
+ "connect",
4131
+ "connectOrCreate",
4132
+ "create"
4133
+ ]);
4134
+ function collectRelationCoveredFks(modelFields, dataConfig) {
4135
+ const covered = /* @__PURE__ */ new Set();
4136
+ for (const [fieldName, value] of Object.entries(dataConfig)) {
4137
+ const meta = modelFields[fieldName];
4138
+ if (!meta || !meta.isRelation)
4139
+ continue;
4140
+ const fks = meta.relationFromFields;
4141
+ if (!fks || fks.length === 0)
4142
+ continue;
4143
+ if (!isPlainObject(value))
4144
+ continue;
4145
+ const covers = Object.keys(value).some(
4146
+ (op) => RELATION_OPS_COVERING_FK.has(op)
4147
+ );
4148
+ if (!covers)
4149
+ continue;
4150
+ for (const fk of fks)
4151
+ covered.add(fk);
4152
+ }
4153
+ return covered;
4154
+ }
4113
4155
  function validateCreateCompleteness(modelName, dataConfig, typeMap, scopeFks, zodDefaults) {
4114
4156
  const modelFields = typeMap[modelName];
4115
4157
  if (!modelFields)
4116
4158
  return;
4117
4159
  const zodDefaultFields = zodDefaults[modelName];
4118
4160
  const zodDefaultSet = zodDefaultFields ? new Set(zodDefaultFields) : void 0;
4161
+ const relationCoveredFks = collectRelationCoveredFks(modelFields, dataConfig);
4119
4162
  for (const [fieldName, meta] of Object.entries(modelFields)) {
4120
4163
  if (meta.isRelation)
4121
4164
  continue;
@@ -4129,10 +4172,12 @@ function validateCreateCompleteness(modelName, dataConfig, typeMap, scopeFks, zo
4129
4172
  continue;
4130
4173
  if (scopeFks.has(fieldName))
4131
4174
  continue;
4175
+ if (relationCoveredFks.has(fieldName))
4176
+ continue;
4132
4177
  if (zodDefaultSet && zodDefaultSet.has(fieldName))
4133
4178
  continue;
4134
4179
  throw new ShapeError(
4135
- `Required field "${fieldName}" on model "${modelName}" is missing from create data shape, has no default, and is not a scope FK`
4180
+ `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`
4136
4181
  );
4137
4182
  }
4138
4183
  }
@@ -4726,34 +4771,43 @@ function resolveDynamicShape(shapeFn, ctx, context) {
4726
4771
  }
4727
4772
  return result;
4728
4773
  }
4729
- function resolveNamedShape(input, body, contextFn, explicitCaller) {
4730
- assertNoCallerInBody(body);
4731
- const caller = explicitCaller;
4732
- const keys = Object.keys(input);
4733
- for (const key of keys) {
4734
- if (GUARD_SHAPE_KEYS.has(key)) {
4735
- throw new ShapeError(
4736
- `Caller key "${key}" collides with reserved guard shape key. Rename the caller path.`
4737
- );
4738
- }
4774
+ function throwVariantResolution(resolution) {
4775
+ const keys = resolution.keys.map((key) => `"${key}"`).join(", ");
4776
+ if (resolution.code === "reserved-key") {
4777
+ throw new ShapeError(
4778
+ `Caller key "${resolution.key}" collides with reserved guard shape key. Rename the caller path.`
4779
+ );
4739
4780
  }
4740
- if (typeof caller !== "string") {
4741
- if ("default" in input) {
4742
- return resolveShapeEntry(input.default, body, contextFn, "default");
4743
- }
4781
+ if (resolution.code === "missing-caller") {
4744
4782
  throw new CallerError(
4745
- `Missing caller. This guard uses named shape routing with keys: ${keys.map((k) => `"${k}"`).join(", ")}. Provide caller via guard(input, caller).`
4783
+ `Missing caller. This guard uses named shape routing with keys: ${keys}. Provide caller via guard(input, caller).`
4746
4784
  );
4747
4785
  }
4748
- const matched = matchCallerPattern(keys, caller);
4749
- if (matched) {
4750
- return resolveShapeEntry(input[matched], body, contextFn, matched);
4751
- }
4752
- if ("default" in input) {
4753
- return resolveShapeEntry(input.default, body, contextFn, "default");
4786
+ if (resolution.code === "ambiguous-caller") {
4787
+ const matches = (resolution.matches ?? []).map((pattern) => `"${pattern}"`).join(", ");
4788
+ throw new CallerError(
4789
+ `Ambiguous caller "${resolution.caller}" matches multiple patterns: ${matches}`
4790
+ );
4754
4791
  }
4755
4792
  throw new CallerError(
4756
- `Unknown caller: "${caller}". Allowed: ${keys.map((k) => `"${k}"`).join(", ")}`
4793
+ `Unknown caller: "${resolution.caller}". Allowed: ${keys}`
4794
+ );
4795
+ }
4796
+ function resolveNamedShape(input, body, contextFn, explicitCaller) {
4797
+ assertNoCallerInBody(body);
4798
+ const resolution = resolveGuardVariantKey({
4799
+ kind: "named",
4800
+ keys: Object.keys(input),
4801
+ caller: explicitCaller,
4802
+ reservedKeys: GUARD_SHAPE_KEYS
4803
+ });
4804
+ if (!resolution.ok)
4805
+ throwVariantResolution(resolution);
4806
+ return resolveShapeEntry(
4807
+ input[resolution.key],
4808
+ body,
4809
+ contextFn,
4810
+ resolution.key
4757
4811
  );
4758
4812
  }
4759
4813
  function resolveShapeEntry(entry, body, contextFn, matchedKey) {