prisma-guard 1.1.0 → 1.2.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.
- package/README.md +700 -104
- package/dist/generator/index.js +189 -48
- package/dist/generator/index.js.map +1 -1
- package/dist/runtime/index.d.ts +16 -5
- package/dist/runtime/index.js +1793 -1057
- package/dist/runtime/index.js.map +1 -1
- package/package.json +1 -1
package/dist/generator/index.js
CHANGED
|
@@ -35,7 +35,7 @@ function emitScopeMap(dmmf, onAmbiguousScope) {
|
|
|
35
35
|
throw new Error(`prisma-guard: ${msg}`);
|
|
36
36
|
}
|
|
37
37
|
if (onAmbiguousScope === "warn") {
|
|
38
|
-
console.warn(`prisma-guard: ${msg} Excluding
|
|
38
|
+
console.warn(`prisma-guard: ${msg} Excluding relation "${field.name}" to scope root "${field.type}" from scope map for model "${model.name}".`);
|
|
39
39
|
}
|
|
40
40
|
continue;
|
|
41
41
|
}
|
|
@@ -65,7 +65,7 @@ function emitScopeMap(dmmf, onAmbiguousScope) {
|
|
|
65
65
|
);
|
|
66
66
|
}
|
|
67
67
|
if (onAmbiguousScope === "warn") {
|
|
68
|
-
console.warn(`prisma-guard: ${msg} Excluding
|
|
68
|
+
console.warn(`prisma-guard: ${msg} Excluding relations to scope root "${root}" from scope map for model "${model.name}".`);
|
|
69
69
|
}
|
|
70
70
|
continue;
|
|
71
71
|
}
|
|
@@ -91,7 +91,7 @@ ${mapEntries}
|
|
|
91
91
|
|
|
92
92
|
export type ScopeRoot = ${scopeRootType}
|
|
93
93
|
`;
|
|
94
|
-
return { source
|
|
94
|
+
return { source };
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
// src/generator/emit-zod-chains.ts
|
|
@@ -308,7 +308,7 @@ function validateDirective(raw) {
|
|
|
308
308
|
}
|
|
309
309
|
if (ch === "/" && !inCharClass) {
|
|
310
310
|
advance();
|
|
311
|
-
while (pos < input.length && /[
|
|
311
|
+
while (pos < input.length && /[gimsuydv]/.test(peek())) {
|
|
312
312
|
advance();
|
|
313
313
|
}
|
|
314
314
|
return null;
|
|
@@ -528,9 +528,15 @@ function validateDirective(raw) {
|
|
|
528
528
|
// src/shared/scalar-base.ts
|
|
529
529
|
import { z } from "zod";
|
|
530
530
|
function isJsonSafe(value) {
|
|
531
|
-
const stack = [value];
|
|
531
|
+
const stack = [{ tag: "visit", value }];
|
|
532
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
532
533
|
while (stack.length > 0) {
|
|
533
|
-
const
|
|
534
|
+
const entry = stack.pop();
|
|
535
|
+
if (entry.tag === "exit") {
|
|
536
|
+
ancestors.delete(entry.ref);
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const current = entry.value;
|
|
534
540
|
if (current === void 0)
|
|
535
541
|
return false;
|
|
536
542
|
if (current === null)
|
|
@@ -544,9 +550,13 @@ function isJsonSafe(value) {
|
|
|
544
550
|
return false;
|
|
545
551
|
continue;
|
|
546
552
|
case "object": {
|
|
553
|
+
if (ancestors.has(current))
|
|
554
|
+
return false;
|
|
555
|
+
ancestors.add(current);
|
|
556
|
+
stack.push({ tag: "exit", ref: current });
|
|
547
557
|
if (Array.isArray(current)) {
|
|
548
558
|
for (let i = 0; i < current.length; i++) {
|
|
549
|
-
stack.push(current[i]);
|
|
559
|
+
stack.push({ tag: "visit", value: current[i] });
|
|
550
560
|
}
|
|
551
561
|
continue;
|
|
552
562
|
}
|
|
@@ -555,7 +565,7 @@ function isJsonSafe(value) {
|
|
|
555
565
|
return false;
|
|
556
566
|
const values = Object.values(current);
|
|
557
567
|
for (let i = 0; i < values.length; i++) {
|
|
558
|
-
stack.push(values[i]);
|
|
568
|
+
stack.push({ tag: "visit", value: values[i] });
|
|
559
569
|
}
|
|
560
570
|
continue;
|
|
561
571
|
}
|
|
@@ -565,37 +575,48 @@ function isJsonSafe(value) {
|
|
|
565
575
|
}
|
|
566
576
|
return true;
|
|
567
577
|
}
|
|
568
|
-
var
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
z.
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
578
|
+
var DECIMAL_REGEX = /^-?(\d+\.?\d*|\.\d+)([eE]-?\d+)?$/;
|
|
579
|
+
var decimalStringSchema = z.string().refine(
|
|
580
|
+
(s) => DECIMAL_REGEX.test(s),
|
|
581
|
+
"Invalid decimal string"
|
|
582
|
+
);
|
|
583
|
+
var decimalObjectSchema = z.custom(
|
|
584
|
+
(v) => v !== null && typeof v === "object" && typeof v.toFixed === "function" && typeof v.toNumber === "function",
|
|
585
|
+
"Expected Decimal-compatible object"
|
|
586
|
+
);
|
|
587
|
+
function createDecimalFactory(strict) {
|
|
588
|
+
if (strict) {
|
|
589
|
+
return () => z.union([decimalStringSchema, decimalObjectSchema]);
|
|
590
|
+
}
|
|
591
|
+
return () => z.union([z.number(), decimalStringSchema, decimalObjectSchema]);
|
|
592
|
+
}
|
|
593
|
+
function createScalarBase(strictDecimal) {
|
|
594
|
+
return {
|
|
595
|
+
String: () => z.string(),
|
|
596
|
+
Int: () => z.number().int(),
|
|
597
|
+
Float: () => z.number(),
|
|
598
|
+
Decimal: createDecimalFactory(strictDecimal),
|
|
599
|
+
BigInt: () => z.union([
|
|
600
|
+
z.bigint(),
|
|
601
|
+
z.number().int().refine(
|
|
602
|
+
(v) => v >= Number.MIN_SAFE_INTEGER && v <= Number.MAX_SAFE_INTEGER,
|
|
603
|
+
"Number exceeds safe integer range for BigInt conversion"
|
|
604
|
+
).transform((v) => BigInt(v)),
|
|
605
|
+
z.string().regex(/^-?\d+$/).transform((v) => BigInt(v))
|
|
606
|
+
]),
|
|
607
|
+
Boolean: () => z.boolean(),
|
|
608
|
+
DateTime: () => z.union([
|
|
609
|
+
z.date(),
|
|
610
|
+
z.string().datetime({ offset: true })
|
|
611
|
+
]).pipe(z.coerce.date()),
|
|
612
|
+
Json: () => z.unknown().refine(isJsonSafe, "Value must be JSON-serializable (no undefined, functions, symbols, class instances, NaN, Infinity, or circular references)"),
|
|
613
|
+
Bytes: () => z.union([
|
|
614
|
+
z.string(),
|
|
615
|
+
z.custom((v) => v instanceof Uint8Array)
|
|
616
|
+
])
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
var SCALAR_BASE = createScalarBase(false);
|
|
599
620
|
|
|
600
621
|
// src/generator/emit-zod-chains.ts
|
|
601
622
|
function buildGenerationBase(fieldType, isList, isEnum, enumValues) {
|
|
@@ -613,14 +634,52 @@ function buildGenerationBase(fieldType, isList, isEnum, enumValues) {
|
|
|
613
634
|
base = z2.array(base);
|
|
614
635
|
return base;
|
|
615
636
|
}
|
|
637
|
+
var TYPE_CHANGING_METHODS = /* @__PURE__ */ new Set([
|
|
638
|
+
"optional",
|
|
639
|
+
"nullable",
|
|
640
|
+
"nullish",
|
|
641
|
+
"readonly",
|
|
642
|
+
"default",
|
|
643
|
+
"catch"
|
|
644
|
+
]);
|
|
616
645
|
function checkChainCompatibility(fieldType, isList, isEnum, enumValues, methods) {
|
|
617
|
-
|
|
618
|
-
if (!
|
|
646
|
+
let current = buildGenerationBase(fieldType, isList, isEnum, enumValues);
|
|
647
|
+
if (!current)
|
|
619
648
|
return null;
|
|
620
649
|
for (const method of methods) {
|
|
621
|
-
if (typeof
|
|
650
|
+
if (typeof current[method] !== "function") {
|
|
622
651
|
return method;
|
|
623
652
|
}
|
|
653
|
+
if (TYPE_CHANGING_METHODS.has(method)) {
|
|
654
|
+
try {
|
|
655
|
+
if (method === "default" || method === "catch") {
|
|
656
|
+
current = current[method](void 0);
|
|
657
|
+
} else {
|
|
658
|
+
current = current[method]();
|
|
659
|
+
}
|
|
660
|
+
} catch {
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
function verifyChainExecution(fieldType, isList, isEnum, enumValues, chainStr) {
|
|
667
|
+
const base = buildGenerationBase(fieldType, isList, isEnum, enumValues);
|
|
668
|
+
if (!base)
|
|
669
|
+
return null;
|
|
670
|
+
let fn;
|
|
671
|
+
try {
|
|
672
|
+
fn = new Function("base", `'use strict'; return base${chainStr}`);
|
|
673
|
+
} catch (err) {
|
|
674
|
+
return `syntax error: ${err.message}`;
|
|
675
|
+
}
|
|
676
|
+
try {
|
|
677
|
+
const result = fn(base);
|
|
678
|
+
if (result === null || result === void 0 || typeof result !== "object" || typeof result.parse !== "function") {
|
|
679
|
+
return "chain did not produce a valid Zod schema";
|
|
680
|
+
}
|
|
681
|
+
} catch (err) {
|
|
682
|
+
return err.message;
|
|
624
683
|
}
|
|
625
684
|
return null;
|
|
626
685
|
}
|
|
@@ -637,6 +696,7 @@ function emitZodChains(dmmf, onInvalidZod) {
|
|
|
637
696
|
enumValues[e.name] = e.values.map((v) => v.name);
|
|
638
697
|
}
|
|
639
698
|
const modelChains = {};
|
|
699
|
+
const defaults = {};
|
|
640
700
|
for (const model of dmmf.datamodel.models) {
|
|
641
701
|
for (const field of model.fields) {
|
|
642
702
|
if (!field.documentation)
|
|
@@ -688,14 +748,34 @@ function emitZodChains(dmmf, onInvalidZod) {
|
|
|
688
748
|
console.warn(msg);
|
|
689
749
|
continue;
|
|
690
750
|
}
|
|
751
|
+
const execError = verifyChainExecution(
|
|
752
|
+
field.type,
|
|
753
|
+
field.isList,
|
|
754
|
+
isEnum,
|
|
755
|
+
isEnum ? enumValues[field.type] : void 0,
|
|
756
|
+
chainStr
|
|
757
|
+
);
|
|
758
|
+
if (execError) {
|
|
759
|
+
const msg = `prisma-guard: @zod directive on ${model.name}.${field.name} fails at schema construction: ${execError}`;
|
|
760
|
+
if (onInvalidZod === "error") {
|
|
761
|
+
throw new Error(msg);
|
|
762
|
+
}
|
|
763
|
+
console.warn(msg);
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
691
766
|
if (!modelChains[model.name])
|
|
692
767
|
modelChains[model.name] = {};
|
|
693
768
|
modelChains[model.name][field.name] = chainStr;
|
|
769
|
+
if (result.methods.includes("default") || result.methods.includes("catch")) {
|
|
770
|
+
if (!defaults[model.name])
|
|
771
|
+
defaults[model.name] = [];
|
|
772
|
+
defaults[model.name].push(field.name);
|
|
773
|
+
}
|
|
694
774
|
}
|
|
695
775
|
}
|
|
696
776
|
const hasChains = Object.keys(modelChains).length > 0;
|
|
697
777
|
if (!hasChains) {
|
|
698
|
-
return { source: "export const ZOD_CHAINS = {}\n", hasChains: false };
|
|
778
|
+
return { source: "export const ZOD_CHAINS = {}\n", hasChains: false, defaults };
|
|
699
779
|
}
|
|
700
780
|
const entries = Object.entries(modelChains).map(([model, fields]) => {
|
|
701
781
|
const fieldEntries = Object.entries(fields).map(([field, chain]) => ` ${JSON.stringify(field)}: (base: any) => base${chain},`).join("\n");
|
|
@@ -708,7 +788,8 @@ ${fieldEntries}
|
|
|
708
788
|
${entries}
|
|
709
789
|
}
|
|
710
790
|
`,
|
|
711
|
-
hasChains: true
|
|
791
|
+
hasChains: true,
|
|
792
|
+
defaults
|
|
712
793
|
};
|
|
713
794
|
}
|
|
714
795
|
|
|
@@ -804,21 +885,50 @@ ${typesSource}
|
|
|
804
885
|
`;
|
|
805
886
|
}
|
|
806
887
|
|
|
807
|
-
// src/
|
|
888
|
+
// src/shared/constants.ts
|
|
889
|
+
var SHAPE_CONFIG_KEYS = /* @__PURE__ */ new Set([
|
|
890
|
+
"where",
|
|
891
|
+
"include",
|
|
892
|
+
"select",
|
|
893
|
+
"orderBy",
|
|
894
|
+
"cursor",
|
|
895
|
+
"take",
|
|
896
|
+
"skip",
|
|
897
|
+
"distinct",
|
|
898
|
+
"having",
|
|
899
|
+
"_count",
|
|
900
|
+
"_avg",
|
|
901
|
+
"_sum",
|
|
902
|
+
"_min",
|
|
903
|
+
"_max",
|
|
904
|
+
"by"
|
|
905
|
+
]);
|
|
906
|
+
var GUARD_SHAPE_KEYS = /* @__PURE__ */ new Set([
|
|
907
|
+
"data",
|
|
908
|
+
"create",
|
|
909
|
+
"update",
|
|
910
|
+
...SHAPE_CONFIG_KEYS
|
|
911
|
+
]);
|
|
912
|
+
var TO_MANY_RELATION_OPS = /* @__PURE__ */ new Set(["some", "every", "none"]);
|
|
913
|
+
var TO_ONE_RELATION_OPS = /* @__PURE__ */ new Set(["is", "isNot"]);
|
|
914
|
+
var ALL_RELATION_OPS = /* @__PURE__ */ new Set([...TO_MANY_RELATION_OPS, ...TO_ONE_RELATION_OPS]);
|
|
808
915
|
function toDelegateKey(modelName) {
|
|
809
916
|
return modelName[0].toLowerCase() + modelName.slice(1);
|
|
810
917
|
}
|
|
918
|
+
var FORCED_MARKER = Symbol.for("prisma-guard.forced");
|
|
919
|
+
|
|
920
|
+
// src/generator/emit-client.ts
|
|
811
921
|
function emitClient(dmmf) {
|
|
812
922
|
const modelEntries = dmmf.datamodel.models.map((model) => {
|
|
813
923
|
const key = toDelegateKey(model.name);
|
|
814
924
|
return ` ${key}: {
|
|
815
|
-
guard(input: GuardInput): GuardedModel<PrismaClient['${key}']>
|
|
925
|
+
guard(input: GuardInput, caller?: string): GuardedModel<PrismaClient['${key}']>
|
|
816
926
|
}`;
|
|
817
927
|
}).join("\n");
|
|
818
928
|
return `import type { PrismaClient } from '@prisma/client'
|
|
819
929
|
import type { GuardInput, GuardedModel } from 'prisma-guard'
|
|
820
930
|
import { createGuard } from 'prisma-guard'
|
|
821
|
-
import { SCOPE_MAP, TYPE_MAP, ENUM_MAP, ZOD_CHAINS, GUARD_CONFIG, UNIQUE_MAP } from './index.js'
|
|
931
|
+
import { SCOPE_MAP, TYPE_MAP, ENUM_MAP, ZOD_CHAINS, GUARD_CONFIG, UNIQUE_MAP, ZOD_DEFAULTS } from './index.js'
|
|
822
932
|
import type { ScopeRoot } from './index.js'
|
|
823
933
|
|
|
824
934
|
interface GuardModelExtension {
|
|
@@ -832,6 +942,7 @@ export const guard = createGuard<typeof TYPE_MAP, ScopeRoot, GuardModelExtension
|
|
|
832
942
|
zodChains: ZOD_CHAINS,
|
|
833
943
|
guardConfig: GUARD_CONFIG,
|
|
834
944
|
uniqueMap: UNIQUE_MAP,
|
|
945
|
+
zodDefaults: ZOD_DEFAULTS,
|
|
835
946
|
})
|
|
836
947
|
`;
|
|
837
948
|
}
|
|
@@ -843,6 +954,7 @@ var VALID_ON_AMBIGUOUS_SCOPE = /* @__PURE__ */ new Set(["error", "warn", "ignore
|
|
|
843
954
|
var VALID_ON_MISSING_SCOPE_CONTEXT = /* @__PURE__ */ new Set(["error", "warn", "ignore"]);
|
|
844
955
|
var VALID_FIND_UNIQUE_MODE = /* @__PURE__ */ new Set(["verify", "reject"]);
|
|
845
956
|
var VALID_ON_SCOPE_RELATION_WRITE = /* @__PURE__ */ new Set(["error", "warn", "strip"]);
|
|
957
|
+
var VALID_BOOLEAN_CONFIG = /* @__PURE__ */ new Set(["true", "false"]);
|
|
846
958
|
function validateConfigEnum(name, value, allowed) {
|
|
847
959
|
if (!allowed.has(value)) {
|
|
848
960
|
throw new Error(
|
|
@@ -851,6 +963,30 @@ function validateConfigEnum(name, value, allowed) {
|
|
|
851
963
|
}
|
|
852
964
|
return value;
|
|
853
965
|
}
|
|
966
|
+
function validateBooleanConfig(name, raw, fallback) {
|
|
967
|
+
const value = raw ?? (fallback ? "true" : "false");
|
|
968
|
+
if (!VALID_BOOLEAN_CONFIG.has(value)) {
|
|
969
|
+
throw new Error(
|
|
970
|
+
`prisma-guard: Invalid generator config "${name}": "${value}". Allowed values: true, false`
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
return value === "true";
|
|
974
|
+
}
|
|
975
|
+
function emitZodDefaults(defaults) {
|
|
976
|
+
const entries = Object.entries(defaults);
|
|
977
|
+
if (entries.length === 0) {
|
|
978
|
+
return `export const ZOD_DEFAULTS: Record<string, readonly string[]> = {}
|
|
979
|
+
`;
|
|
980
|
+
}
|
|
981
|
+
const mapEntries = entries.map(([model, fields]) => {
|
|
982
|
+
const fieldsStr = fields.map((f) => JSON.stringify(f)).join(", ");
|
|
983
|
+
return ` ${JSON.stringify(model)}: [${fieldsStr}],`;
|
|
984
|
+
}).join("\n");
|
|
985
|
+
return `export const ZOD_DEFAULTS: Record<string, readonly string[]> = {
|
|
986
|
+
${mapEntries}
|
|
987
|
+
}
|
|
988
|
+
`;
|
|
989
|
+
}
|
|
854
990
|
generatorHandler({
|
|
855
991
|
onManifest() {
|
|
856
992
|
return {
|
|
@@ -868,6 +1004,8 @@ generatorHandler({
|
|
|
868
1004
|
const onMissingScopeContext = validateConfigEnum("onMissingScopeContext", config.onMissingScopeContext ?? "error", VALID_ON_MISSING_SCOPE_CONTEXT);
|
|
869
1005
|
const findUniqueMode = validateConfigEnum("findUniqueMode", config.findUniqueMode ?? "reject", VALID_FIND_UNIQUE_MODE);
|
|
870
1006
|
const onScopeRelationWrite = validateConfigEnum("onScopeRelationWrite", config.onScopeRelationWrite ?? "error", VALID_ON_SCOPE_RELATION_WRITE);
|
|
1007
|
+
const strictDecimal = validateBooleanConfig("strictDecimal", config.strictDecimal, false);
|
|
1008
|
+
const enforceProjection = validateBooleanConfig("enforceProjection", config.enforceProjection, false);
|
|
871
1009
|
const dmmf = options.dmmf;
|
|
872
1010
|
const parts = [];
|
|
873
1011
|
parts.push(
|
|
@@ -875,6 +1013,8 @@ generatorHandler({
|
|
|
875
1013
|
onMissingScopeContext: ${JSON.stringify(onMissingScopeContext)},
|
|
876
1014
|
findUniqueMode: ${JSON.stringify(findUniqueMode)},
|
|
877
1015
|
onScopeRelationWrite: ${JSON.stringify(onScopeRelationWrite)},
|
|
1016
|
+
strictDecimal: ${JSON.stringify(strictDecimal)},
|
|
1017
|
+
enforceProjection: ${JSON.stringify(enforceProjection)},
|
|
878
1018
|
} as const
|
|
879
1019
|
`
|
|
880
1020
|
);
|
|
@@ -882,8 +1022,9 @@ generatorHandler({
|
|
|
882
1022
|
parts.push(scopeSource);
|
|
883
1023
|
const typeMapSource = emitTypeMap(dmmf);
|
|
884
1024
|
parts.push(typeMapSource);
|
|
885
|
-
const { source: zodChainsSource } = emitZodChains(dmmf, onInvalidZod);
|
|
1025
|
+
const { source: zodChainsSource, defaults } = emitZodChains(dmmf, onInvalidZod);
|
|
886
1026
|
parts.push(zodChainsSource);
|
|
1027
|
+
parts.push(emitZodDefaults(defaults));
|
|
887
1028
|
mkdirSync(output, { recursive: true });
|
|
888
1029
|
writeFileSync(join(output, "index.ts"), parts.join("\n"), "utf-8");
|
|
889
1030
|
const clientSource = emitClient(dmmf);
|