prisma-guard 1.31.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 +61 -11
- package/dist/runtime/index.cjs +127 -102
- package/dist/runtime/index.cjs.map +1 -1
- package/dist/runtime/index.js +127 -102
- package/dist/runtime/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1121,7 +1121,7 @@ The return value is:
|
|
|
1121
1121
|
| `shape` | The concrete resolved guard shape after caller matching and dynamic shape execution |
|
|
1122
1122
|
| `body` | The normalized request body. Omitted input becomes `{}` |
|
|
1123
1123
|
| `effectiveReadBody` | The body used for read planning. If the body has no `select` or `include`, the shape's default read projection is applied |
|
|
1124
|
-
| `matchedKey` | The selected
|
|
1124
|
+
| `matchedKey` | The selected declared key, such as `default`, `admin`, or `/user/:id`. For parameterized callers this is the pattern key, not the concrete caller value |
|
|
1125
1125
|
| `wasDynamic` | `true` when the matched shape was a function and was executed with guard context |
|
|
1126
1126
|
|
|
1127
1127
|
`resolve()` uses the same guard extension context and caller selection path as `.findMany()`, `.findFirst()`, and the other guarded methods. If the shape is context-dependent, the shape function is called with the current guard context.
|
|
@@ -1915,22 +1915,40 @@ The `default` fallback works consistently across both `.guard()` and `guard.quer
|
|
|
1915
1915
|
|
|
1916
1916
|
### Note for generated route configs
|
|
1917
1917
|
|
|
1918
|
-
The direct
|
|
1918
|
+
The direct runtime API accepts all three guard input forms:
|
|
1919
|
+
|
|
1920
|
+
- one direct guard shape
|
|
1921
|
+
- one context-dependent shape function
|
|
1922
|
+
- a named shape map
|
|
1923
|
+
|
|
1924
|
+
`prisma-generator-express` mirrors those forms under an operation's `shape` property. It also provides a `variants` descriptor API when each named shape needs its own route hooks:
|
|
1919
1925
|
|
|
1920
1926
|
```ts
|
|
1921
1927
|
const projectConfig = {
|
|
1922
1928
|
findMany: {
|
|
1923
|
-
|
|
1929
|
+
before: [authenticate],
|
|
1930
|
+
variants: {
|
|
1931
|
+
admin: {
|
|
1932
|
+
shape: {
|
|
1933
|
+
where: { title: { contains: true }, status: { equals: true } },
|
|
1934
|
+
take: { max: 100 },
|
|
1935
|
+
},
|
|
1936
|
+
before: [requireAdmin],
|
|
1937
|
+
},
|
|
1924
1938
|
default: {
|
|
1925
|
-
|
|
1926
|
-
|
|
1939
|
+
shape: {
|
|
1940
|
+
where: { title: { contains: true } },
|
|
1941
|
+
take: { max: 20, default: 10 },
|
|
1942
|
+
},
|
|
1927
1943
|
},
|
|
1928
1944
|
},
|
|
1929
1945
|
},
|
|
1930
1946
|
}
|
|
1931
1947
|
```
|
|
1932
1948
|
|
|
1933
|
-
|
|
1949
|
+
Within one generated operation, `shape` and `variants` cannot both be defined. Both may be omitted when the operation only needs operation-wide hooks or pagination. Each `variants[key].shape` is one direct shape or one context-dependent shape function; it is not another nested named map.
|
|
1950
|
+
|
|
1951
|
+
Use `shape` when only guard routing is needed. Use `variants` when hooks need to differ by the matched named shape.
|
|
1934
1952
|
|
|
1935
1953
|
### Caller resolution order
|
|
1936
1954
|
|
|
@@ -1959,16 +1977,50 @@ await prisma.project.guard({ where: { ... } }).findMany(req.body)
|
|
|
1959
1977
|
The `caller` key in the context object is not used for scope injection — it is only used for shape routing. Scope roots are identified by matching context keys against `@scope-root` model names.
|
|
1960
1978
|
|
|
1961
1979
|
### Parameterized caller patterns
|
|
1980
|
+
|
|
1962
1981
|
```text
|
|
1963
1982
|
/org/:orgId/users
|
|
1964
1983
|
/org/:orgId/users/:userId
|
|
1965
1984
|
```
|
|
1966
1985
|
|
|
1967
|
-
Matching is case-sensitive.
|
|
1986
|
+
Matching is case-sensitive. A parameter segment starts with `:` and matches exactly one path segment. Segment counts must be equal. Parameters are routing-only and are not extracted into context.
|
|
1987
|
+
|
|
1988
|
+
Caller routing uses this precedence:
|
|
1989
|
+
|
|
1990
|
+
1. Reject any named key that collides with a reserved guard shape key.
|
|
1991
|
+
2. If caller is not a string, use `default` when present; otherwise throw the missing-caller `CallerError`.
|
|
1992
|
+
3. If caller is blank or whitespace-only, skip exact and parameterized matching. Use `default` when present; otherwise throw unknown-caller `CallerError`.
|
|
1993
|
+
4. A non-blank exact key match wins immediately.
|
|
1994
|
+
5. If there is no exact match, evaluate parameterized patterns.
|
|
1995
|
+
6. One matching pattern selects that declared pattern key.
|
|
1996
|
+
7. Multiple matching patterns throw an ambiguous-caller `CallerError` listing every match.
|
|
1997
|
+
8. With no match, use `default` when present; otherwise throw unknown-caller `CallerError`.
|
|
1998
|
+
|
|
1999
|
+
Exact matching therefore wins over a pattern that would also match:
|
|
2000
|
+
|
|
2001
|
+
```ts
|
|
2002
|
+
const shapes = {
|
|
2003
|
+
'customer/123': exactShape,
|
|
2004
|
+
'customer/:id': parameterizedShape,
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
// Selects "customer/123", not "customer/:id".
|
|
2008
|
+
await prisma.order.guard(shapes, 'customer/123').findMany()
|
|
2009
|
+
```
|
|
2010
|
+
|
|
2011
|
+
For a parameterized caller, `matchedKey` is the declared pattern key. A caller of `customer/123` matched by `customer/:id` produces `matchedKey === 'customer/:id'`. This is the key used for shape memoization and returned by `resolve()`.
|
|
1968
2012
|
|
|
1969
2013
|
### Fail-closed behavior
|
|
1970
2014
|
|
|
1971
|
-
|
|
2015
|
+
Named routing fails closed:
|
|
2016
|
+
|
|
2017
|
+
- A non-string caller with no `default` throws the missing-caller `CallerError`.
|
|
2018
|
+
- A blank or whitespace-only caller never matches a blank key or a parameterized key. It uses `default` or throws unknown-caller `CallerError`.
|
|
2019
|
+
- An unmatched caller uses `default` or throws unknown-caller `CallerError`.
|
|
2020
|
+
- Multiple matching parameterized patterns throw ambiguous-caller `CallerError`.
|
|
2021
|
+
- A caller key that collides with a reserved shape key throws `ShapeError`.
|
|
2022
|
+
|
|
2023
|
+
The same precedence and error behavior apply to `.guard()` and `guard.query().parse()`.
|
|
1972
2024
|
|
|
1973
2025
|
If a request body contains a `caller` field when using named shapes, it is rejected with a `CallerError` that directs the developer to use the second argument to `.guard()` or the context function instead.
|
|
1974
2026
|
|
|
@@ -2738,6 +2790,4 @@ Possible future improvements:
|
|
|
2738
2790
|
|
|
2739
2791
|
## License
|
|
2740
2792
|
|
|
2741
|
-
MIT
|
|
2742
|
-
|
|
2743
|
-
|
|
2793
|
+
MIT
|
package/dist/runtime/index.cjs
CHANGED
|
@@ -975,40 +975,75 @@ function unsupported() {
|
|
|
975
975
|
return marker;
|
|
976
976
|
}
|
|
977
977
|
|
|
978
|
-
// src/shared/
|
|
979
|
-
function
|
|
980
|
-
|
|
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
|
|
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
|
|
993
|
-
for (let
|
|
994
|
-
if (patternParts[
|
|
988
|
+
let matchesCaller = true;
|
|
989
|
+
for (let index = 0; index < patternParts.length; index++) {
|
|
990
|
+
if (patternParts[index].startsWith(":"))
|
|
995
991
|
continue;
|
|
996
|
-
if (patternParts[
|
|
997
|
-
|
|
992
|
+
if (patternParts[index] !== callerParts[index]) {
|
|
993
|
+
matchesCaller = false;
|
|
998
994
|
break;
|
|
999
995
|
}
|
|
1000
996
|
}
|
|
1001
|
-
if (
|
|
997
|
+
if (matchesCaller)
|
|
1002
998
|
matches.push(pattern);
|
|
1003
999
|
}
|
|
1004
|
-
|
|
1005
|
-
|
|
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
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1036
|
+
return {
|
|
1037
|
+
ok: false,
|
|
1038
|
+
code: "ambiguous-caller",
|
|
1039
|
+
caller,
|
|
1040
|
+
keys,
|
|
1041
|
+
matches
|
|
1042
|
+
};
|
|
1010
1043
|
}
|
|
1011
|
-
|
|
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
|
|
3445
|
-
const
|
|
3446
|
-
if (
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
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
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
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(
|
|
3570
|
+
built = builtCache.get(resolution.key);
|
|
3555
3571
|
}
|
|
3556
3572
|
}
|
|
3557
3573
|
return applyBuiltShape(built, normalizedBody, isUnique);
|
|
@@ -4786,34 +4802,43 @@ function resolveDynamicShape(shapeFn, ctx, context) {
|
|
|
4786
4802
|
}
|
|
4787
4803
|
return result;
|
|
4788
4804
|
}
|
|
4789
|
-
function
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
4794
|
-
|
|
4795
|
-
throw new ShapeError(
|
|
4796
|
-
`Caller key "${key}" collides with reserved guard shape key. Rename the caller path.`
|
|
4797
|
-
);
|
|
4798
|
-
}
|
|
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
|
+
);
|
|
4799
4811
|
}
|
|
4800
|
-
if (
|
|
4801
|
-
if ("default" in input) {
|
|
4802
|
-
return resolveShapeEntry(input.default, body, contextFn, "default");
|
|
4803
|
-
}
|
|
4812
|
+
if (resolution.code === "missing-caller") {
|
|
4804
4813
|
throw new CallerError(
|
|
4805
|
-
`Missing caller. This guard uses named shape routing with keys: ${keys
|
|
4814
|
+
`Missing caller. This guard uses named shape routing with keys: ${keys}. Provide caller via guard(input, caller).`
|
|
4806
4815
|
);
|
|
4807
4816
|
}
|
|
4808
|
-
|
|
4809
|
-
|
|
4810
|
-
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
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
|
+
);
|
|
4814
4822
|
}
|
|
4815
4823
|
throw new CallerError(
|
|
4816
|
-
`Unknown caller: "${caller}". Allowed: ${keys
|
|
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
|
|
4817
4842
|
);
|
|
4818
4843
|
}
|
|
4819
4844
|
function resolveShapeEntry(entry, body, contextFn, matchedKey) {
|