prisma-guard 1.20.0 → 1.21.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 CHANGED
@@ -39,6 +39,7 @@ database
39
39
  * [Before / After prisma-guard](#before--after-prisma-guard)
40
40
  * [Schema annotations](#schema-annotations)
41
41
  * [The guard API](#the-guard-api)
42
+ * [Relation writes in data shapes](#relation-writes-in-data-shapes)
42
43
  * [Logical combinators in where shapes](#logical-combinators-in-where-shapes)
43
44
  * [Relation filters in where shapes](#relation-filters-in-where-shapes)
44
45
  * [Mutation return projection](#mutation-return-projection)
@@ -401,7 +402,7 @@ await prisma.project
401
402
 
402
403
  In this example, `title` and `priority` use inline refines for custom validation and error messages, `status` uses `@zod` chains from the Prisma schema, `createdBy` is forced to `currentUserId`, and `isActive` is forced to `true` using the `force()` helper.
403
404
 
404
- Relation fields are not permitted in `data` shapes. Attempting to use a relation field in a data shape throws `ShapeError`. See [Limitations](#guarded-data-shapes-do-not-permit-relation-fields).
405
+ Relation fields are supported in `data` shapes with a config object describing allowed nested write operations. See [Relation writes in data shapes](#relation-writes-in-data-shapes) for syntax and security implications.
405
406
 
406
407
  ### The `force()` helper
407
408
 
@@ -443,6 +444,15 @@ await prisma.project
443
444
 
444
445
  The client can only filter by `title`, sort by `title`, and take up to 100 rows. Everything else is rejected.
445
446
 
447
+ ### Take shorthand
448
+
449
+ `take` accepts either an object or a number. When a number is provided, it serves as both the maximum and the default:
450
+ ```ts
451
+ take: 50 // equivalent to { max: 50, default: 50 }
452
+ take: { max: 100, default: 25 } // explicit max and default
453
+ take: { max: 100 } // max only, no default
454
+ ```
455
+
446
456
  ### Creates
447
457
  ```ts
448
458
  await prisma.project
@@ -602,11 +612,43 @@ Where shapes accept scalar field filters, relation filters (`some`, `every`, `no
602
612
 
603
613
  Shape config values are strictly validated at construction time. Fields in `orderBy`, `cursor`, `having`, `_count` (object form), `_avg`, `_sum`, `_min`, `_max` must have the value `true`. The `skip` config must be exactly `true`. Passing any other value (including `false`, numbers, or strings) throws `ShapeError`. This prevents accidental misconfiguration where a developer writes `{ orderBy: { title: false } }` expecting it to disable ordering — instead of silently enabling it, the shape is rejected.
604
614
 
605
- ### Where DSL is a constrained Prisma subset
615
+ ### Where DSL: Prisma-compatible subset
616
+
617
+ The where shape syntax supports a subset of Prisma's where filter API. This section documents both supported features and known differences.
618
+
619
+ **Supported operators:**
620
+
621
+ All standard scalar operators are supported: `equals`, `not`, `contains`, `startsWith`, `endsWith`, `in`, `notIn`, `gt`, `gte`, `lt`, `lte`, `search`.
622
+
623
+ The `not` operator accepts either a scalar value or a nested filter object:
624
+ ```ts
625
+ where: {
626
+ age: { not: true }, // client can send { age: { not: 5 } } or { age: { not: { gt: 5 } } }
627
+ }
628
+ ```
629
+
630
+ The `search` operator is supported for String fields with `@@fulltext` indexes:
631
+ ```ts
632
+ where: {
633
+ title: { search: true },
634
+ }
635
+ ```
636
+
637
+ **Relation existence checks with `is: null` / `isNot: null`:**
638
+
639
+ To-one relation filters support null checks for testing relation existence. In the shape config, use `null` as the operator value to force a null check:
640
+ ```ts
641
+ where: {
642
+ author: {
643
+ is: null, // forced: always filters for records where author IS null
644
+ },
645
+ }
646
+ ```
647
+
648
+ This produces `{ author: { is: null } }` in the Prisma query. Since `null` in a shape is always a forced value (the client cannot control it), this is equivalent to a forced where condition.
606
649
 
607
- The where shape syntax supports a subset of Prisma's where filter API. Notable differences from raw Prisma where clauses:
650
+ **Notable differences from raw Prisma where clauses:**
608
651
 
609
- * The `not` operator accepts a scalar value only, not a nested filter object. Prisma's `{ not: { gt: 5 } }` form is not supported.
610
652
  * `AND` and `OR` in client input must be arrays with at least one element. Prisma accepts a single object for `AND`; prisma-guard requires an array. Empty arrays are rejected.
611
653
  * `NOT` in client input accepts a single object or an array with at least one element. Empty arrays are rejected.
612
654
  * Each combinator member must specify at least one condition with a defined value. Empty objects inside combinators (e.g. `{ AND: [{}] }`) are rejected when no forced values exist.
@@ -629,6 +671,109 @@ Writes: `create`, `createMany`, `createManyAndReturn`, `update`, `updateMany`, `
629
671
 
630
672
  ---
631
673
 
674
+ ## Relation writes in data shapes
675
+
676
+ Data shapes support relation fields with a config object describing which nested write operations the client may use. Each operation (`connect`, `create`, `disconnect`, etc.) is configured individually.
677
+
678
+ > **⚠️ Security warning:** The automatic tenant scope extension only intercepts **top-level** operations. Nested writes through relation configs bypass scope entirely — no FK injection on nested creates, no tenant filtering on nested updates/deletes. If you use relation writes on scoped models, you must handle tenant isolation manually in your application code or enforce it via database constraints (e.g. RLS, triggers).
679
+
680
+ ### Syntax
681
+ ```ts
682
+ await prisma.post
683
+ .guard({
684
+ data: {
685
+ title: true,
686
+ content: true,
687
+ tags: {
688
+ connect: { id: true },
689
+ disconnect: { id: true },
690
+ },
691
+ },
692
+ })
693
+ .update({
694
+ data: {
695
+ title: 'Updated',
696
+ tags: {
697
+ connect: [{ id: 'tag1' }, { id: 'tag2' }],
698
+ disconnect: [{ id: 'tag3' }],
699
+ },
700
+ },
701
+ where: { id: { equals: 'post1' } },
702
+ })
703
+ ```
704
+
705
+ ### Supported operations
706
+
707
+ All 11 Prisma nested write operations are supported:
708
+
709
+ | Operation | To-one | To-many | Config type |
710
+ | ----------------- | ------ | ------- | ------------------------------------------------ |
711
+ | `connect` | yes | yes | `{ fieldName: true, ... }` |
712
+ | `connectOrCreate` | yes | yes | `{ where: { ... }, create: { ... } }` |
713
+ | `create` | yes | yes | `{ fieldName: true, ... }` |
714
+ | `createMany` | no | yes | `{ data: { fieldName: true, ... } }` |
715
+ | `disconnect` | yes | yes | `true` (to-one) or `{ fieldName: true }` (to-many) |
716
+ | `delete` | yes | yes | `true` (to-one) or `{ fieldName: true }` (to-many) |
717
+ | `set` | no | yes | `{ fieldName: true, ... }` |
718
+ | `update` | yes | yes | `{ fieldName: true }` or `{ where: ..., data: ... }` |
719
+ | `updateMany` | no | yes | `{ where: { ... }, data: { ... } }` |
720
+ | `upsert` | yes | yes | `{ create: { ... }, update: { ... } }` |
721
+ | `deleteMany` | no | yes | `{ fieldName: true, ... }` |
722
+
723
+ ### Example with multiple operations
724
+ ```ts
725
+ await prisma.user
726
+ .guard({
727
+ data: {
728
+ name: true,
729
+ posts: {
730
+ create: { title: true, content: true },
731
+ connect: { id: true },
732
+ disconnect: { id: true },
733
+ update: {
734
+ where: { id: true },
735
+ data: { title: true },
736
+ },
737
+ },
738
+ },
739
+ })
740
+ .update({
741
+ data: {
742
+ name: 'Updated Name',
743
+ posts: {
744
+ create: { title: 'New Post', content: 'Content' },
745
+ connect: [{ id: 'existing-post-id' }],
746
+ },
747
+ },
748
+ where: { id: { equals: userId } },
749
+ })
750
+ ```
751
+
752
+ ### Validation
753
+
754
+ Each operation's config is validated at shape construction time:
755
+
756
+ * Unknown operations throw `ShapeError`
757
+ * Operations invalid for the relation cardinality throw `ShapeError` (e.g. `set` on to-one, `disconnect: true` on to-many)
758
+ * Nested data fields are validated against the related model's type map — relation fields within nested data are not supported (no deep nesting)
759
+ * `@zod` chains apply to nested data fields
760
+
761
+ ### Scope implications
762
+
763
+ Nested writes bypass the scope extension because Prisma extension hooks only fire for top-level operations. This means:
764
+
765
+ * **Nested creates** do not get scope FK injection — the related record will not have the tenant FK set automatically
766
+ * **Nested updates/deletes** do not get tenant where conditions — they can affect records across tenants
767
+ * **Nested connects** reference records by unique fields without tenant filtering
768
+
769
+ For multi-tenant applications, consider:
770
+
771
+ * Using database-level constraints (foreign keys, RLS policies) to enforce tenant boundaries on related models
772
+ * Restricting relation write operations to `connect` and `disconnect` only (which reference existing records by ID)
773
+ * Using forced values for tenant FK fields in nested create configs where possible
774
+
775
+ ---
776
+
632
777
  ## Logical combinators in where shapes
633
778
 
634
779
  Where shapes support `AND`, `OR`, and `NOT` to compose filter conditions. The combinator value is a where config defining allowed fields inside the combinator:
@@ -720,6 +865,23 @@ await prisma.user
720
865
 
721
866
  Each relation operator value is a nested where config for the related model. All where features — scalar operators, forced values, logical combinators, and nested relation filters — work recursively inside relation filters.
722
867
 
868
+ ### Null existence checks for to-one relations
869
+
870
+ To-one relation operators support `null` for testing whether a relation exists:
871
+ ```ts
872
+ await prisma.post
873
+ .guard({
874
+ where: {
875
+ author: {
876
+ is: null, // forced: filter for posts where author IS null
877
+ },
878
+ },
879
+ })
880
+ .findMany(req.body)
881
+ ```
882
+
883
+ In the shape config, `null` as an operator value is always forced — the client cannot control it. This is the standard Prisma pattern for checking to-one relation existence.
884
+
723
885
  ### Forced values in relation filters
724
886
  ```ts
725
887
  import { force } from 'prisma-guard'
@@ -1211,13 +1373,40 @@ await prisma.project
1211
1373
 
1212
1374
  All data shape value types (`true`, literal, `force()`, function) work in named shapes, context-dependent shapes, and single shapes.
1213
1375
 
1376
+ ### Default fallback
1377
+
1378
+ Named shape maps support a `default` key that acts as a fallback when no caller is provided or no pattern matches:
1379
+ ```ts
1380
+ await prisma.project
1381
+ .guard({
1382
+ '/admin/projects': {
1383
+ where: { title: { contains: true }, status: { equals: true } },
1384
+ take: { max: 100 },
1385
+ },
1386
+ default: {
1387
+ where: { title: { contains: true } },
1388
+ take: { max: 20 },
1389
+ },
1390
+ }, req.headers['x-caller'])
1391
+ .findMany(req.body)
1392
+ ```
1393
+
1394
+ The `default` fallback is used when:
1395
+
1396
+ * No caller is provided (missing from both the second argument and context function)
1397
+ * The provided caller doesn't match any pattern
1398
+
1399
+ Without a `default` key, missing or unmatched callers throw `CallerError`.
1400
+
1401
+ The `default` fallback works consistently across both `.guard()` and `guard.query().parse()` API surfaces.
1402
+
1214
1403
  ### Caller resolution order
1215
1404
 
1216
1405
  Caller is resolved in priority order:
1217
1406
 
1218
1407
  1. **Explicit argument** — `.guard(shapes, '/admin/projects')` always wins
1219
1408
  2. **Context function** — if the context object has a `caller` string property, it is used as the default
1220
- 3. **None** — if neither source provides a caller and the shape is a named map, `CallerError` is thrown
1409
+ 3. **None** — if neither source provides a caller and the shape is a named map without a `default` key, `CallerError` is thrown
1221
1410
 
1222
1411
  This enables three usage patterns:
1223
1412
  ```ts
@@ -1247,7 +1436,7 @@ Matching is case-sensitive. Exact matches are checked first. If no exact match i
1247
1436
 
1248
1437
  ### Fail-closed behavior
1249
1438
 
1250
- If `caller` is missing or doesn't match any pattern, the request is rejected with a `CallerError`. If a caller matches multiple parameterized patterns, it is also rejected with a `CallerError`.
1439
+ If `caller` is missing or doesn't match any pattern and no `default` key exists, the request is rejected with a `CallerError`. If a caller matches multiple parameterized patterns, it is also rejected with a `CallerError`.
1251
1440
 
1252
1441
  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.
1253
1442
 
@@ -1357,7 +1546,7 @@ This applies to all top-level operations on scoped models, including reads, writ
1357
1546
  ### What is NOT scoped
1358
1547
 
1359
1548
  * Nested reads loaded via `include` or `select` — use forced where conditions in the shape to restrict these (to-many relations only; see [Limitations](#limitations))
1360
- * Nested writes Prisma extension hooks operate on top-level operations only. Guarded data shapes reject relation fields entirely, so nested writes are only possible through raw (unguarded) Prisma calls.
1549
+ * Nested writes via relation write configs in data shapes the scope extension hooks only fire for top-level operations (see [Relation writes in data shapes](#relation-writes-in-data-shapes))
1361
1550
  * `$queryRaw` and `$executeRaw` — raw SQL bypasses all guard protections
1362
1551
 
1363
1552
  ### Scope relation writes
@@ -1531,11 +1720,11 @@ These limitations are real and should be treated as part of the security model.
1531
1720
 
1532
1721
  `$queryRaw` and `$executeRaw` are not intercepted.
1533
1722
 
1534
- ### Nested writes are not intercepted
1723
+ ### Nested writes are not scope-intercepted
1535
1724
 
1536
- Prisma extension hooks operate on top-level operations. Nested writes do not trigger separate scope interception.
1725
+ Prisma extension hooks operate on top-level operations. Relation write configs in data shapes (see [Relation writes in data shapes](#relation-writes-in-data-shapes)) produce nested write operations that bypass the scope extension entirely. Nested creates do not receive scope FK injection. Nested updates and deletes do not receive tenant where conditions.
1537
1726
 
1538
- Guarded data shapes reject relation fields entirely — using a relation field in a `data`, `create`, or `update` shape throws `ShapeError`. This means nested writes (e.g. `{ author: { connect: { id: '...' } } }`) are only possible through raw (unguarded) Prisma calls. The guard layer prevents them in guarded mutations, not by intercepting nested writes, but by refusing to include relation fields in the data shape.
1727
+ For multi-tenant applications using relation writes, enforce tenant boundaries via database constraints (RLS, foreign key constraints, triggers) or application-level validation.
1539
1728
 
1540
1729
  ### Nested reads via include are not scope-filtered
1541
1730
 
@@ -1563,6 +1752,12 @@ If a model references a scope root through composite foreign keys, that specific
1563
1752
 
1564
1753
  Handle these models explicitly via shape rules.
1565
1754
 
1755
+ ### Compound unique selectors
1756
+
1757
+ Guard currently records unique constraints as arrays of field names but does not generate the named compound selector schemas that Prisma uses for `@@unique` constraints. For example, `@@unique([firstName, lastName])` requires the selector `{ firstName_lastName: { firstName: "A", lastName: "B" } }`, but guard produces flat `{ firstName: "A", lastName: "B" }` output. This affects `findUnique`, `update`, `delete`, `upsert`, `connect`, and `connectOrCreate` with compound unique constraints.
1758
+
1759
+ Single-field unique constraints work correctly. Compound unique support is planned.
1760
+
1566
1761
  ### Cursor fields must cover a unique constraint
1567
1762
 
1568
1763
  Prisma requires cursor-based pagination to use uniquely-identifiable fields. Guard enforces this at shape construction time: cursor fields must cover at least one unique constraint from the model. Non-unique cursor shapes are rejected with `ShapeError`.
@@ -1579,9 +1774,9 @@ Prisma requires cursor-based pagination to use uniquely-identifiable fields. Gua
1579
1774
 
1580
1775
  The generator writes `index.ts` and `client.ts` using `.js` extension imports. A TypeScript-capable build pipeline with ESM-aware module resolution is required (`"moduleResolution": "NodeNext"` or `"Bundler"` in `tsconfig.json`). The classic `"moduleResolution": "node"` setting is not compatible.
1581
1776
 
1582
- ### `having` is limited to scalar fields
1777
+ ### `having` supports logical combinators
1583
1778
 
1584
- Guard `having` shapes support scalar field filters with their type-appropriate operators. Aggregate-level having expressions (e.g. `_count`, `_avg` inside having) are not supported.
1779
+ Guard `having` shapes support `AND`, `OR`, and `NOT` combinators for composing complex grouped aggregation filters. The fields available inside combinators are the same fields defined in the having shape config.
1585
1780
 
1586
1781
  ### Json fields accept any JSON-serializable value
1587
1782
 
@@ -1611,7 +1806,7 @@ Prisma supports negative `take` for reverse cursor pagination. prisma-guard rest
1611
1806
 
1612
1807
  ### `skip` in shape config is a permission flag
1613
1808
 
1614
- `skip: true` in a shape config means the client is allowed to provide a `skip` value. The actual `skip` value must be a non-negative integer. The value must be exactly `true` — other truthy values are rejected with `ShapeError`. This is consistent with other shape flags but differs from `take`, which uses `{ max, default? }` syntax.
1809
+ `skip: true` in a shape config means the client is allowed to provide a `skip` value. The actual `skip` value must be a non-negative integer. The value must be exactly `true` — other truthy values are rejected with `ShapeError`. This is consistent with other shape flags but differs from `take`, which uses `{ max, default? }` syntax or a number shorthand.
1615
1810
 
1616
1811
  ### `guard.input()` defaults to allowing null for nullable fields
1617
1812
 
@@ -1625,10 +1820,6 @@ The `Decimal` base type accepts JavaScript `number`, decimal string, and Decimal
1625
1820
 
1626
1821
  `createMany` and `createManyAndReturn` accept `skipDuplicates: boolean` in the request body. This is passed through to Prisma without shape-level configuration. It is not available on `create`.
1627
1822
 
1628
- ### Guarded data shapes do not permit relation fields
1629
-
1630
- Relation fields in `data`, `create`, and `update` shapes are rejected with `ShapeError`. Nested writes (e.g. `{ author: { connect: { id: '...' } } }`) are only possible through raw (unguarded) Prisma calls. The guard layer does not intercept or validate nested writes — it prevents them entirely in guarded mutations.
1631
-
1632
1823
  ### Conflicting forced where values are rejected
1633
1824
 
1634
1825
  If the same field and operator appear as forced values in different parts of a where shape (e.g. at the top level and inside an `AND` combinator) with different values, the shape is rejected with `ShapeError` at construction time. Identical duplicate forced values are deduplicated silently. This prevents ambiguous security configurations from silently degrading.
@@ -1691,6 +1882,8 @@ All guard errors include `status` and `code` properties for HTTP response mappin
1691
1882
  | `CallerError` | 400 | `CALLER_UNKNOWN` |
1692
1883
  | `PolicyError` | 403 | `POLICY_DENIED` |
1693
1884
 
1885
+ Note: Prisma errors (`PrismaClientKnownRequestError`, `PrismaClientValidationError`, etc.) propagate through the guard layer unmodified. Error handlers should be prepared to handle both guard errors (with `status`/`code` properties) and Prisma errors.
1886
+
1694
1887
  ### ZodError wrapping
1695
1888
 
1696
1889
  By default, Zod validation failures throw a raw `ZodError`. This means error handling code must check for both `ZodError` and guard error types.
@@ -1743,7 +1936,7 @@ For create operations, fields tracked in `ZOD_DEFAULTS` that are omitted from th
1743
1936
 
1744
1937
  For fields that ARE listed in the data shape with `true` and have `@zod .default(...)` or `@zod .catch(...)`, the runtime skips wrapping the schema with `.optional()` in create mode. This preserves the Zod default/catch behavior: omitting the field from client input triggers the default rather than passing `undefined` through.
1745
1938
 
1746
- Caller routing is resolved before method execution: the explicit `caller` argument takes priority, then `contextFn().caller`, then absent (which is fine for single shapes but throws `CallerError` for named shape maps).
1939
+ Caller routing is resolved before method execution: the explicit `caller` argument takes priority, then `contextFn().caller`, then absent (which is fine for single shapes but throws `CallerError` for named shape maps without a `default` key).
1747
1940
 
1748
1941
  The context function is validated on every code path that consumes it — scope injection, caller resolution, and dynamic shape evaluation all enforce the plain-object contract and throw `PolicyError` for invalid returns. Additionally, if a context key matches a known scope root but has a non-primitive value, `PolicyError` is thrown immediately rather than silently dropping the scope.
1749
1942
 
@@ -1796,7 +1989,7 @@ For upsert, `create` and `update` data schemas are cached independently under na
1796
1989
  | `onMissingScopeContext = "ignore"` | scope bypassed for missing roots; present roots still enforced |
1797
1990
  | unsafe scoped `findUnique` | reject recommended |
1798
1991
  | invalid `@zod` directive | error by default |
1799
- | missing `caller` in named shapes | error always |
1992
+ | missing `caller` in named shapes | error unless `default` key exists |
1800
1993
  | `caller` in request body | error always |
1801
1994
  | `data` in read shape | error always |
1802
1995
  | `data` in upsert shape | error always (use `create`/`update`) |
@@ -1832,6 +2025,7 @@ For upsert, `create` and `update` data schemas are cached independently under na
1832
2025
  | `@zod .default()`/`.catch()` field omitted from shape | auto-injected as forced value |
1833
2026
  | read shape with select/include, client omits | auto-applied as default projection |
1834
2027
  | mutation shape with select/include, client omits | full payload unless enforceProjection enabled |
2028
+ | nested writes via relation configs | bypass scope extension (top-level only) |
1835
2029
 
1836
2030
  ---
1837
2031
 
@@ -1898,6 +2092,7 @@ Node 22
1898
2092
  9. Upsert uses `create`/`update` keys, not `data` — matches Prisma's own API shape
1899
2093
  10. Shape config values are validated strictly — `true` means enabled, anything else is rejected
1900
2094
  11. Read shapes with projection define both the security boundary and the default response — no client duplication needed
2095
+ 12. Nested writes are validated but not scope-intercepted — document clearly and rely on database constraints for tenant boundaries
1901
2096
 
1902
2097
  ---
1903
2098
 
@@ -1925,12 +2120,14 @@ Node 22
1925
2120
  | ZodError wrapping | opt-in | n/a |
1926
2121
  | Logical combinators in where | yes | manual |
1927
2122
  | Relation filters in where | yes | manual |
2123
+ | Relation writes in data shapes | yes | manual |
1928
2124
  | Empty relation filter rejection | yes | n/a |
1929
2125
  | Empty projection shape rejection | yes | n/a |
1930
2126
  | Forced where conflict detection | yes | n/a |
1931
2127
  | Forced boolean values via `force()` | yes | n/a |
1932
2128
  | Strict Decimal mode | opt-in | n/a |
1933
2129
  | `@zod .default()`/`.catch()` auto-injection | yes | n/a |
2130
+ | Nested write scope enforcement | no (top-level only) | no |
1934
2131
 
1935
2132
  ---
1936
2133
 
@@ -1938,8 +2135,9 @@ Node 22
1938
2135
 
1939
2136
  Possible future improvements:
1940
2137
 
1941
- * optional nested-write enforcement helpers
2138
+ * compound unique selector support
1942
2139
  * richer relation-level policies
2140
+ * nested write scope enforcement helpers
1943
2141
  * adapter integrations for SQL-backed runtimes
1944
2142
  * model-specific generated types for stronger compile-time shape validation
1945
2143
  * structured JSON field validation via schema annotations
@@ -268,11 +268,8 @@ function validateDirective(raw) {
268
268
  }
269
269
  if (peek() === "e" || peek() === "E") {
270
270
  advance();
271
- if (peek() === "-")
271
+ if (peek() === "-" || peek() === "+")
272
272
  advance();
273
- if (peek() === "+") {
274
- return { valid: false, reason: 'Invalid number: "+" not allowed in exponent' };
275
- }
276
273
  if (!/[0-9]/.test(peek())) {
277
274
  return { valid: false, reason: "Invalid number: expected digit in exponent" };
278
275
  }
@@ -602,7 +599,13 @@ function createScalarBase(strictDecimal) {
602
599
  z.string().regex(/^-?\d+$/).transform((v) => BigInt(v))
603
600
  ]),
604
601
  Boolean: () => z.boolean(),
605
- DateTime: () => z.union([z.date(), z.string().datetime({ offset: true })]).pipe(z.coerce.date()),
602
+ DateTime: () => z.union([
603
+ z.date(),
604
+ z.string().refine(
605
+ (s) => !isNaN(Date.parse(s)),
606
+ "Invalid date string"
607
+ )
608
+ ]).pipe(z.coerce.date()),
606
609
  Json: () => z.unknown().refine(
607
610
  isJsonSafe,
608
611
  "Value must be JSON-serializable (no undefined, functions, symbols, class instances, NaN, Infinity, or circular references)"
@@ -926,7 +929,7 @@ function emitClient(dmmf) {
926
929
  import type { GuardInput, GuardedModel } from 'prisma-guard'
927
930
  import { createGuard } from 'prisma-guard'
928
931
  import { SCOPE_MAP, TYPE_MAP, ENUM_MAP, ZOD_CHAINS, GUARD_CONFIG, UNIQUE_MAP, ZOD_DEFAULTS } from './index'
929
- import type { ScopeRoot } from './index.js'
932
+ import type { ScopeRoot } from './index'
930
933
 
931
934
  interface GuardModelExtension {
932
935
  ${modelEntries}