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.
@@ -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 model from scope map.`);
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 model from scope map.`);
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, roots };
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 && /[gimsuy]/.test(peek())) {
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 current = stack.pop();
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 SCALAR_BASE = {
569
- String: () => z.string(),
570
- Int: () => z.number().int(),
571
- Float: () => z.number(),
572
- Decimal: () => z.union([
573
- z.number(),
574
- z.string().refine(
575
- (s) => /^-?(\d+\.?\d*|\.\d+)([eE]-?\d+)?$/.test(s),
576
- "Invalid decimal string"
577
- )
578
- ]),
579
- BigInt: () => z.union([
580
- z.bigint(),
581
- z.number().int().refine(
582
- (v) => v >= Number.MIN_SAFE_INTEGER && v <= Number.MAX_SAFE_INTEGER,
583
- "Number exceeds safe integer range for BigInt conversion"
584
- ).transform((v) => BigInt(v)),
585
- z.string().regex(/^-?\d+$/).transform((v) => BigInt(v))
586
- ]),
587
- Boolean: () => z.boolean(),
588
- DateTime: () => z.union([
589
- z.date(),
590
- z.string().datetime({ offset: true }),
591
- z.string().datetime()
592
- ]).pipe(z.coerce.date()),
593
- Json: () => z.unknown().refine(isJsonSafe, "Value must be JSON-serializable (no undefined, functions, symbols, class instances, NaN, or Infinity)"),
594
- Bytes: () => z.union([
595
- z.string(),
596
- z.custom((v) => v instanceof Uint8Array)
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
- const base = buildGenerationBase(fieldType, isList, isEnum, enumValues);
618
- if (!base)
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 base[method] !== "function") {
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/generator/emit-client.ts
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);