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.
- package/README.md +190 -15
- package/dist/generator/index.js +6 -0
- package/dist/generator/index.js.map +1 -1
- package/dist/runtime/index.cjs +157 -103
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.d.cts +2 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +157 -103
- package/dist/runtime/index.js.map +1 -1
- package/package.json +1 -1
package/dist/runtime/index.d.cts
CHANGED
|
@@ -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;
|
package/dist/runtime/index.d.ts
CHANGED
|
@@ -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;
|
package/dist/runtime/index.js
CHANGED
|
@@ -944,40 +944,75 @@ function unsupported() {
|
|
|
944
944
|
return marker;
|
|
945
945
|
}
|
|
946
946
|
|
|
947
|
-
// src/shared/
|
|
948
|
-
function
|
|
949
|
-
|
|
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
|
|
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
|
|
962
|
-
for (let
|
|
963
|
-
if (patternParts[
|
|
957
|
+
let matchesCaller = true;
|
|
958
|
+
for (let index = 0; index < patternParts.length; index++) {
|
|
959
|
+
if (patternParts[index].startsWith(":"))
|
|
964
960
|
continue;
|
|
965
|
-
if (patternParts[
|
|
966
|
-
|
|
961
|
+
if (patternParts[index] !== callerParts[index]) {
|
|
962
|
+
matchesCaller = false;
|
|
967
963
|
break;
|
|
968
964
|
}
|
|
969
965
|
}
|
|
970
|
-
if (
|
|
966
|
+
if (matchesCaller)
|
|
971
967
|
matches.push(pattern);
|
|
972
968
|
}
|
|
973
|
-
|
|
974
|
-
|
|
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
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1005
|
+
return {
|
|
1006
|
+
ok: false,
|
|
1007
|
+
code: "ambiguous-caller",
|
|
1008
|
+
caller,
|
|
1009
|
+
keys,
|
|
1010
|
+
matches
|
|
1011
|
+
};
|
|
979
1012
|
}
|
|
980
|
-
|
|
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
|
|
3414
|
-
const
|
|
3415
|
-
if (
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
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
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
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(
|
|
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
|
|
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
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
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 (
|
|
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
|
|
4783
|
+
`Missing caller. This guard uses named shape routing with keys: ${keys}. Provide caller via guard(input, caller).`
|
|
4746
4784
|
);
|
|
4747
4785
|
}
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
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
|
|
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) {
|