@opensaas/stack-core 0.24.0 → 0.26.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.
Files changed (91) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +263 -0
  3. package/CLAUDE.md +50 -0
  4. package/dist/access/access-filter.d.ts +39 -0
  5. package/dist/access/access-filter.d.ts.map +1 -1
  6. package/dist/access/access-filter.js +121 -0
  7. package/dist/access/access-filter.js.map +1 -1
  8. package/dist/access/field-access.d.ts +1 -0
  9. package/dist/access/field-access.d.ts.map +1 -1
  10. package/dist/access/field-access.js +79 -4
  11. package/dist/access/field-access.js.map +1 -1
  12. package/dist/access/field-access.test.js +213 -0
  13. package/dist/access/field-access.test.js.map +1 -1
  14. package/dist/access/index.d.ts +1 -1
  15. package/dist/access/index.d.ts.map +1 -1
  16. package/dist/access/index.js +1 -1
  17. package/dist/access/index.js.map +1 -1
  18. package/dist/access/types.d.ts +39 -0
  19. package/dist/access/types.d.ts.map +1 -1
  20. package/dist/config/types.d.ts +318 -0
  21. package/dist/config/types.d.ts.map +1 -1
  22. package/dist/context/apply-defaults.d.ts +36 -0
  23. package/dist/context/apply-defaults.d.ts.map +1 -0
  24. package/dist/context/apply-defaults.js +70 -0
  25. package/dist/context/apply-defaults.js.map +1 -0
  26. package/dist/context/hook-pipeline.d.ts.map +1 -1
  27. package/dist/context/hook-pipeline.js +10 -0
  28. package/dist/context/hook-pipeline.js.map +1 -1
  29. package/dist/context/index.d.ts +79 -18
  30. package/dist/context/index.d.ts.map +1 -1
  31. package/dist/context/index.js +194 -39
  32. package/dist/context/index.js.map +1 -1
  33. package/dist/context/nested-operations.d.ts +59 -3
  34. package/dist/context/nested-operations.d.ts.map +1 -1
  35. package/dist/context/nested-operations.js +558 -129
  36. package/dist/context/nested-operations.js.map +1 -1
  37. package/dist/context/transaction-boundary.d.ts +91 -0
  38. package/dist/context/transaction-boundary.d.ts.map +1 -0
  39. package/dist/context/transaction-boundary.js +329 -0
  40. package/dist/context/transaction-boundary.js.map +1 -0
  41. package/dist/context/write-pipeline.d.ts +15 -1
  42. package/dist/context/write-pipeline.d.ts.map +1 -1
  43. package/dist/context/write-pipeline.js +173 -10
  44. package/dist/context/write-pipeline.js.map +1 -1
  45. package/dist/fields/calendar-day.test.d.ts +2 -0
  46. package/dist/fields/calendar-day.test.d.ts.map +1 -0
  47. package/dist/fields/calendar-day.test.js +120 -0
  48. package/dist/fields/calendar-day.test.js.map +1 -0
  49. package/dist/fields/index.d.ts +18 -2
  50. package/dist/fields/index.d.ts.map +1 -1
  51. package/dist/fields/index.js +93 -17
  52. package/dist/fields/index.js.map +1 -1
  53. package/dist/hooks/index.d.ts +116 -0
  54. package/dist/hooks/index.d.ts.map +1 -1
  55. package/dist/hooks/index.js +154 -0
  56. package/dist/hooks/index.js.map +1 -1
  57. package/dist/index.d.ts +1 -0
  58. package/dist/index.d.ts.map +1 -1
  59. package/dist/index.js.map +1 -1
  60. package/dist/validation/schema.test.js +222 -1
  61. package/dist/validation/schema.test.js.map +1 -1
  62. package/package.json +1 -1
  63. package/src/access/access-filter.ts +156 -0
  64. package/src/access/field-access.test.ts +255 -0
  65. package/src/access/field-access.ts +91 -5
  66. package/src/access/index.ts +1 -1
  67. package/src/access/types.ts +45 -0
  68. package/src/config/types.ts +364 -0
  69. package/src/context/apply-defaults.ts +79 -0
  70. package/src/context/hook-pipeline.ts +11 -0
  71. package/src/context/index.ts +340 -68
  72. package/src/context/nested-operations.ts +976 -143
  73. package/src/context/transaction-boundary.ts +440 -0
  74. package/src/context/write-pipeline.ts +234 -13
  75. package/src/fields/calendar-day.test.ts +140 -0
  76. package/src/fields/index.ts +96 -16
  77. package/src/hooks/index.ts +265 -0
  78. package/src/index.ts +5 -0
  79. package/src/validation/schema.test.ts +266 -1
  80. package/tests/access.test.ts +24 -16
  81. package/tests/apply-defaults.test.ts +119 -0
  82. package/tests/context.test.ts +481 -0
  83. package/tests/default-value-create.test.ts +299 -0
  84. package/tests/field-types.test.ts +17 -3
  85. package/tests/interactive-transaction.test.ts +444 -0
  86. package/tests/nested-access-and-hooks.test.ts +1130 -54
  87. package/tests/nested-operation-registry.test.ts +28 -3
  88. package/tests/nested-write-hooks.test.ts +864 -0
  89. package/tests/sudo.test.ts +7 -3
  90. package/tests/transaction-boundary-hooks.test.ts +465 -0
  91. package/tsconfig.tsbuildinfo +1 -1
@@ -1,4 +1,4 @@
1
1
 
2
- > @opensaas/stack-core@0.24.0 build /home/runner/work/stack/stack/packages/core
2
+ > @opensaas/stack-core@0.26.0 build /home/runner/work/stack/stack/packages/core
3
3
  > tsc
4
4
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,268 @@
1
1
  # @opensaas/stack-core
2
2
 
3
+ ## 0.26.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#616](https://github.com/OpenSaasAU/stack/pull/616) [`322d5b6`](https://github.com/OpenSaasAU/stack/commit/322d5b64d11c3e3401493511e0c0e3a1fa20e210) Thanks [@borisno2](https://github.com/borisno2)! - Add `context.transaction()` — an interactive, hook-firing transaction
8
+
9
+ You can now run multiple access-checked `context.db.*` operations atomically in one transaction while preserving the access/hook boundary (unlike raw `prisma.$transaction`, which bypasses both). The callback receives a full context whose `db.*` operations enforce access control and run list/field hooks, but persist against a single interactive transaction — so a throw anywhere rolls the whole transaction back.
10
+
11
+ Options (notably `isolationLevel`, plus `maxWait`/`timeout`) pass through to Prisma, and serialization failures (Prisma `P2034`) propagate to the caller so you own the retry loop. This makes concurrency-sensitive invariants such as a capacity gate enforceable:
12
+
13
+ ```typescript
14
+ async function bookSlot(context, slotId) {
15
+ for (let attempt = 0; attempt < 5; attempt++) {
16
+ try {
17
+ return await context.transaction(
18
+ async (tx) => {
19
+ const count = await tx.db.booking.count({ where: { slotId } })
20
+ if (count >= CAPACITY) return { booked: false }
21
+ return { booked: true, item: await tx.db.booking.create({ data: { slotId } }) }
22
+ },
23
+ { isolationLevel: 'Serializable' },
24
+ )
25
+ } catch (err) {
26
+ // Serialization failures propagate — retry is caller-owned.
27
+ if (err && typeof err === 'object' && 'code' in err && err.code === 'P2034') continue
28
+ throw err
29
+ }
30
+ }
31
+ throw new Error('exceeded retry budget')
32
+ }
33
+ ```
34
+
35
+ Nested `context.db` writes inside the callback join the outer transaction. New `StackContext`, `TransactionOptions`, and `TransactionIsolationLevel` types are exported from `@opensaas/stack-core`. See ADR-0012.
36
+
37
+ ### Patch Changes
38
+
39
+ - [#620](https://github.com/OpenSaasAU/stack/pull/620) [`0be254e`](https://github.com/OpenSaasAU/stack/commit/0be254e2b2e6bbc0c2f168438aea49d2e1cc7f0b) Thanks [@borisno2](https://github.com/borisno2)! - Apply a field's defaultValue to omitted inputs before create validation (resolve-then-validate, matching Keystone), so isRequired + defaultValue no longer fails on create.
40
+
41
+ Note: because an omitted-but-defaulted field is now filled into `resolvedData` before validation, that field's create-side field-level `beforeOperation`/`afterOperation` hooks (gated on the field key being present in `resolvedData`) now fire for defaulted fields where they previously would not.
42
+
43
+ ## 0.25.0
44
+
45
+ ### Minor Changes
46
+
47
+ - [#602](https://github.com/OpenSaasAU/stack/pull/602) [`44ec937`](https://github.com/OpenSaasAU/stack/commit/44ec9375baa4dacab4e34b03cbefb27c8aec07c9) Thanks [@borisno2](https://github.com/borisno2)! - Make `calendarDay` a `YYYY-MM-DD` string end-to-end (Keystone's CalendarDay scalar)
48
+
49
+ `calendarDay` is now a `YYYY-MM-DD` **string** at the `context.db` boundary in
50
+ both directions, so its type, validation, and runtime value finally agree.
51
+ Previously the field validated a `YYYY-MM-DD` string but its TypeScript type was
52
+ `Date`, so a typed caller passing `new Date(...)` hit a runtime `ValidationError`.
53
+ - The field/read type and the generated `CreateInput`/`UpdateInput` input types
54
+ are now `string`.
55
+ - Writes accept only a `YYYY-MM-DD` string; a malformed string or a `Date` is
56
+ rejected at runtime by validation (a `ValidationError`).
57
+ - Storage is unchanged: `DateTime @db.Date` on Postgres/MySQL, the SQLite TEXT
58
+ fallback as before.
59
+
60
+ **Behavioral change (reads):** reading a `calendarDay` now returns a
61
+ `YYYY-MM-DD` string instead of a `Date`. A field `resolveOutput` transform
62
+ normalises the value Prisma returns from the `@db.Date` column, using UTC
63
+ components to avoid timezone off-by-one. Consumers that previously relied on a
64
+ `Date` on read should update to the string form:
65
+
66
+ ```typescript
67
+ const event = await context.db.event.findUnique({ where: { id } })
68
+ event?.startDate // => '2025-01-15' (string, not Date)
69
+
70
+ // Writes: pass YYYY-MM-DD strings, not Date objects
71
+ await context.db.event.create({ data: { startDate: '2025-01-15' } })
72
+ ```
73
+
74
+ - [#593](https://github.com/OpenSaasAU/stack/pull/593) [`fadd9db`](https://github.com/OpenSaasAU/stack/commit/fadd9dbd17085f4dd15899371a054ec46f943ce4) Thanks [@{](https://github.com/{)! - Nested relation writes now run the full hook pipeline inside one transaction ([#569](https://github.com/OpenSaasAU/stack/issues/569))
75
+
76
+ A record written via a nested `create`, `update`, or `delete` now fires the SAME
77
+ list- and field-level `beforeOperation`/`afterOperation` hooks as the equivalent
78
+ top-level write — so side effects (workflows, notifications, billing) are
79
+ identical whether a record is written nested or top-level. Previously nested
80
+ writes ran only `resolveInput`/`validate`/field-rules and silently skipped the
81
+ before/after side-effect hooks.
82
+ - Nested **create** runs `beforeOperation` (create) → persist → `afterOperation`
83
+ receiving the created `item`.
84
+ - Nested **update** runs `afterOperation` receiving both `originalItem` (the row
85
+ before) and the updated `item`.
86
+ - Nested **delete** runs `beforeOperation`/`afterOperation` receiving the
87
+ `originalItem`.
88
+
89
+ Existing access control, validation, silent-failure, sudo-bypass, and the [#578](https://github.com/OpenSaasAU/stack/issues/578)
90
+ nested-`connect`/`connectOrCreate` read-access + DB-reachability behavior are
91
+ unchanged. Pass-through nested kinds (`disconnect`/`set`/`updateMany`/
92
+ `deleteMany`) are out of scope and behave as before. See ADR-0010.
93
+
94
+ For to-many nested creates (`create: [{A},{B}]`), each created record's
95
+ `afterOperation` now fires exactly once against its OWN distinct row, recovered
96
+ by id-diff against the rows that existed before the write — so a pre-existing
97
+ sibling is never passed as the "created" item, and multiple creates no longer
98
+ collapse to a single row.
99
+
100
+ BEHAVIOR CHANGE — every write is now transactional, and a throwing
101
+ `beforeOperation`/`afterOperation` (or validation) rolls the whole write back.
102
+ The entire operation (parent + all nested writes) now runs inside one
103
+ `prisma.$transaction`, so it is atomic. Previously an `afterOperation` that threw
104
+ left the row committed; now it rolls back with the transaction (more
105
+ Keystone-correct). If you relied on a thrown `afterOperation` leaving the row
106
+ persisted, move that work to run after the write returns.
107
+
108
+ Inside a `beforeOperation`/`afterOperation` hook, `context.db` (and
109
+ `context.prisma`) are now bound to the write's transaction, so any `context.db`
110
+ write a hook performs participates in — and rolls back with — the same
111
+ transaction. Externally-visible side effects that must survive a rollback should
112
+ not use `context.db` from within these hooks (transaction-boundary hooks for
113
+ that are deferred — see [#590](https://github.com/OpenSaasAU/stack/issues/590)).
114
+
115
+ ```ts
116
+ // Nested create now fires the related list's beforeOperation/afterOperation,
117
+ // atomically with the parent — a throw anywhere rolls the whole write back.
118
+ await context.db.post.update({
119
+ where: { id },
120
+ data: {
121
+ title: 'Updated',
122
+ create: { name: 'New Author' } }, // User hooks fire; atomic
123
+ },
124
+ })
125
+ ```
126
+
127
+ - [#594](https://github.com/OpenSaasAU/stack/pull/594) [`4f0d407`](https://github.com/OpenSaasAU/stack/commit/4f0d40721feff1a3109647a81fcbe47db5970026) Thanks [@borisno2](https://github.com/borisno2)! - Add an opt-in **Node build** of the generated `.opensaas/` bundle (ADR-0011, [#579](https://github.com/OpenSaasAU/stack/issues/579)).
128
+
129
+ Setting `output: { buildTarget: 'node' }` in `opensaas.config.ts` makes `opensaas generate` additionally compile the bundle to a plain-Node-loadable ESM form under `.opensaas/dist/` — `.js` + `.d.ts` with a `{"type":"module"}` marker — alongside the default `.ts` bundler form. The compiled entry is `.opensaas/dist/context.js`, with the Prisma client subtree at `.opensaas/dist/prisma-client/**` and the project config compiled in as a sibling, so a live module (e.g. better-auth's Prisma adapter) can be imported in a bundler-less runtime — plain Node, a Playwright e2e helper, or a build-time script — that the default `.ts` form cannot execute.
130
+
131
+ The Node build is purely additive: with `output.buildTarget` absent (the default), generation behaves exactly as before and no `.opensaas/dist/` is emitted.
132
+
133
+ ```typescript
134
+ // opensaas.config.ts
135
+ export default config({
136
+ output: { buildTarget: 'node' },
137
+ // ...
138
+ })
139
+
140
+ // then, from a plain-Node consumer (no bundler, no tsx):
141
+ import { createAuth } from '@opensaas/stack-auth/server'
142
+ import { config, rawOpensaasContext } from './.opensaas/dist/context.js'
143
+
144
+ const auth = createAuth(config, rawOpensaasContext)
145
+ await auth.api.signUpEmail({ body: { email, password, name } })
146
+ ```
147
+
148
+ The compile runs via the TypeScript compiler API with `rewriteRelativeImportExtensions` (turning the bundle's `.ts`-extension imports into runnable `.js` specifiers), `declaration`, `skipLibCheck`, and `noEmitOnError: false`, so it reuses the bundle's type-clean guarantee without adding a build dependency. `'node'` is the only `buildTarget` today; the field is a string-literal union so future compiled targets can be added without a breaking change.
149
+
150
+ - [#592](https://github.com/OpenSaasAU/stack/pull/592) [`e355c05`](https://github.com/OpenSaasAU/stack/commit/e355c05a0787980b997609c4571271ab5c250f36) Thanks [@borisno2](https://github.com/borisno2)! - Make the generated `.opensaas/prisma-client` subtree statically resolvable by default and add a `db.prismaGeneratorOptions` passthrough.
151
+
152
+ The generated `generator client { ... }` block now emits `importFileExtension = "ts"` and `moduleFormat = "esm"` by default, so the prisma-client subtree uses explicit `.ts` import extensions and matches the extension style the rest of the `.opensaas` bundle already uses — the whole import graph is statically resolvable by a bundler out of the box, no post-generation surgery required.
153
+
154
+ A new optional `db.prismaGeneratorOptions` lets you override these values when you need a different module/extension story (e.g. emitting `.js` extensions for a plain-Node consumer). Any value you supply wins; omitted keys fall back to the `ts`/`esm` defaults. The existing `previewFeatures = ["multiSchema"]` emission (when `db.schemas` is set) is preserved and coexists with the new options.
155
+
156
+ ```typescript
157
+ export default config({
158
+ db: {
159
+ provider: 'postgresql',
160
+ prismaGeneratorOptions: {
161
+ importFileExtension: 'js',
162
+ moduleFormat: 'commonjs',
163
+ },
164
+ // ... rest of config
165
+ },
166
+ // ...
167
+ })
168
+ ```
169
+
170
+ - [#600](https://github.com/OpenSaasAU/stack/pull/600) [`a93cebb`](https://github.com/OpenSaasAU/stack/commit/a93cebb5a6ba6550d8cdbb94f010c902ad7e29f1) Thanks [@relationship({](https://github.com/relationship({)! - Gate nested `connect` by the owning relationship field's field-level access
171
+
172
+ Nested `connect` (and the connect branch of `connectOrCreate`) is now gated by
173
+ the owning relationship field's create/update field-level access, in addition to
174
+ the target list's read/query access and DB-reachability check. This completes
175
+ the Keystone-parity rule that a connect requires both read access on the target
176
+ AND write access on the owning relationship field. `sudo` bypasses the check.
177
+
178
+ ```typescript
179
+ Post: list({
180
+ fields: {
181
+ // A non-sudo caller can only connect an author when this field's
182
+ // update access permits it (and the target User is readable/reachable).
183
+
184
+ ref: 'User.posts',
185
+ access: { update: ({ session }) => session?.role === 'editor' },
186
+ }),
187
+ },
188
+ })
189
+ ```
190
+
191
+ - [#584](https://github.com/OpenSaasAU/stack/pull/584) [`b17ec45`](https://github.com/OpenSaasAU/stack/commit/b17ec45127fe55f02437892e9fd389c67373635a) Thanks [@borisno2](https://github.com/borisno2)! - Add `findFirst` to access-controlled `context.db.<list>` delegates
192
+
193
+ `findFirst` is sugar over the existing access-filtered `findMany` (`take: 1`), so
194
+ it introduces no new access surface: it applies the exact same query-access checks
195
+ and access-controlled include building as `findMany`, then returns the first
196
+ matching row or `null`. It honours the read-side silent-failure contract — an
197
+ access-denied query yields `null` rather than throwing.
198
+
199
+ ```ts
200
+ // Non-unique single-row lookup
201
+ const account = await context.db.account.findFirst({
202
+ where: { userId: '123' },
203
+ orderBy: { createdAt: 'desc' },
204
+ })
205
+
206
+ // Narrow the single result with a query fragment
207
+ const post = await context.db.post.findFirst({
208
+ where: { published: true },
209
+ query: postFragment,
210
+ })
211
+ // post: ResultOf<typeof postFragment> | null
212
+ ```
213
+
214
+ The CLI type generator now emits a `findFirst` method (and `<List>FindFirstArgs`
215
+ type) for each list in the generated `.opensaas/types.ts`, so migrated apps that
216
+ reach for the familiar Prisma `findFirst` pattern get full type support.
217
+
218
+ - [#601](https://github.com/OpenSaasAU/stack/pull/601) [`8f98e25`](https://github.com/OpenSaasAU/stack/commit/8f98e25fbef4ec0fc3ff0cba456ff7f2f7ba2ea8) Thanks [@borisno2](https://github.com/borisno2)! - Add `beforeTransaction` / `afterTransaction` transaction-boundary hooks (list- and field-level)
219
+
220
+ These run OUTSIDE the write's database transaction (in addition to the in-transaction `beforeOperation`/`afterOperation`), for non-transactional side effects like external API calls that must not hold a transaction open and cannot be rolled back. They fire per `(list, operation)` involved in the write (the top-level list plus each nested create/update/delete list) and form a symmetric compensation bracket: `afterTransaction` always runs when its paired `beforeTransaction` ran, receiving the outcome (`status: 'committed' | 'rolled-back'` plus `error` on rollback). On commit it gets the persisted `item` (and `originalItem` for update/delete) **only for the top-level record** — for nested lists these are `undefined`, since the per-record persisted row is not recoverable outside the transaction; use the in-transaction `afterOperation` for per-record nested compensation. On rollback it gets no `item` so it can undo what `beforeTransaction` did. `connectOrCreate` is enumerated as a best-effort create involvement (a resolve-to-connect still fires the bracket with no write), so compensators should be idempotent.
221
+
222
+ ```typescript
223
+ list({
224
+ fields: { name: text() },
225
+ hooks: {
226
+ // Runs before the transaction opens.
227
+ beforeTransaction: async ({ operation, inputData }) => {
228
+ await billing.reserveSeat(inputData.seatId)
229
+ },
230
+ // Always runs after the transaction settles.
231
+ afterTransaction: async (args) => {
232
+ if (args.status === 'rolled-back') {
233
+ // The write did not persist (args.error explains why) — compensate.
234
+ await billing.releaseSeat(args.inputData.seatId)
235
+ } else {
236
+ await billing.confirmSeat(args.item.seatId)
237
+ }
238
+ },
239
+ },
240
+ })
241
+ ```
242
+
243
+ A throwing `beforeTransaction` aborts the write (the transaction never opens) and fires `afterTransaction` (`rolled-back`) only for lists whose `beforeTransaction` already ran. A throwing `afterTransaction` does not stop the other compensators; errors are surfaced afterward. Sudo does not affect these hooks. This is an additive, non-Keystone extension and does not change the existing `beforeOperation`/`afterOperation` semantics.
244
+
245
+ ### Patch Changes
246
+
247
+ - [#603](https://github.com/OpenSaasAU/stack/pull/603) [`be9a896`](https://github.com/OpenSaasAU/stack/commit/be9a8965ad6338c279e99cfe3bf24162e63ffb92) Thanks [@borisno2](https://github.com/borisno2)! - Enforce required json fields on create: an omitted key is now rejected while any
248
+ present value (object, array, primitive, or null) is still accepted.
249
+
250
+ - [#583](https://github.com/OpenSaasAU/stack/pull/583) [`e39d6e9`](https://github.com/OpenSaasAU/stack/commit/e39d6e9e37be2337c8cf1979053e76877f14296c) Thanks [@borisno2](https://github.com/borisno2)! - Make non-sudo writes fail loud in `filterWritableFields` (Keystone parity).
251
+
252
+ Undeclared `data` keys on create/update now throw instead of passing through unchecked ([#564](https://github.com/OpenSaasAU/stack/issues/564)), and fields denied by field-level access now throw instead of being silently stripped ([#568](https://github.com/OpenSaasAU/stack/issues/568)). `sudo` remains the single trusted bypass; system fields and relationship foreign keys still pass through. Raw multi-column split columns (e.g. `media_url`/`media_size` from an `image()`/`file()` field) are now gated by their owning field's write access — supplying them directly under non-sudo when that field denies the write throws, instead of bypassing the field's `access.create`/`access.update`.
253
+
254
+ Behavioural narrowing: a list-level `resolveInput` hook that adds keys to `resolvedData` which are not declared fields will now be rejected by the undeclared-key throw. No production hook does this today.
255
+
256
+ - [#605](https://github.com/OpenSaasAU/stack/pull/605) [`ca4973b`](https://github.com/OpenSaasAU/stack/commit/ca4973b504eadb123d179e8f4d16d6ec8c9f8fc1) Thanks [@borisno2](https://github.com/borisno2)! - Required json fields now reject a present `null` during validation rather than failing later as a DB NOT NULL violation. Omitted keys on update are still allowed; the Prisma column nullability is unchanged.
257
+
258
+ - [#602](https://github.com/OpenSaasAU/stack/pull/602) [`44ec937`](https://github.com/OpenSaasAU/stack/commit/44ec9375baa4dacab4e34b03cbefb27c8aec07c9) Thanks [@borisno2](https://github.com/borisno2)! - Fix update validation rejecting omitted required fields under zod 4.4 by using key-optionality (`.optional()`) instead of `z.union([schema, z.undefined()])`. Partial updates that omit a required-on-create field now validate; present values still enforce their rules.
259
+
260
+ - [#587](https://github.com/OpenSaasAU/stack/pull/587) [`ecbf834`](https://github.com/OpenSaasAU/stack/commit/ecbf834059a072c428b0739d6ebcf4c74be8c893) Thanks [@borisno2](https://github.com/borisno2)! - Fix false denial of nested `connect` (and `connectOrCreate`'s connect branch): connect now requires read/query access on the target and evaluates filter results via DB reachability (`findFirst({ where: { AND: [connection, accessFilter] } })`), so nested-relation and `AND`/`OR`/`some`/`none`/`not` filters no longer always fail.
261
+
262
+ - [#589](https://github.com/OpenSaasAU/stack/pull/589) [`481d6e0`](https://github.com/OpenSaasAU/stack/commit/481d6e00be90b1159b0b30eff015e5079c840158) Thanks [@borisno2](https://github.com/borisno2)! - Fix row-level access bypass when an explicit `include` is passed to non-sudo `findUnique`/`findMany`. The caller's `include` is now merged with (not replaced by) the access-controlled include: denied relations are dropped, each relation's access `where` is AND-combined with any caller nested `where`, and nested includes are filtered at every level. Sudo and query-fragment paths are unchanged. When no access-controlled include is computed (inside a `resolveOutput`/virtual-field context, at max include depth, or for a list with no relationships), the caller's `include` is passed through unchanged rather than dropped — avoiding fail-closed data loss.
263
+
264
+ - [#586](https://github.com/OpenSaasAU/stack/pull/586) [`4622b5f`](https://github.com/OpenSaasAU/stack/commit/4622b5fa8fc731e2c8995011f1be0cfe341578da) Thanks [@borisno2](https://github.com/borisno2)! - Enforce unique-`where` for `context.db.<list>.findUnique` — a non-unique `where` now throws a clear error instead of silently returning a nondeterministic row. Use `findFirst` for non-unique single-row lookups.
265
+
3
266
  ## 0.24.0
4
267
 
5
268
  ### Minor Changes
package/CLAUDE.md CHANGED
@@ -118,6 +118,56 @@ Hook types:
118
118
 
119
119
  - `createContext(config, prisma, session?)` - Creates context wrapper
120
120
  - Returns `{ db, session }` where `db` is Prisma client with access control
121
+ - `context.transaction(fn, options?)` - Interactive, hook-firing transaction (see below)
122
+
123
+ #### Interactive transactions (`context.transaction`)
124
+
125
+ `context.transaction(async (txContext) => { … }, { isolationLevel })` runs the
126
+ callback inside **one** Prisma interactive transaction. `txContext` is a full
127
+ context whose `db.*` operations are access-checked and hook-firing exactly like
128
+ the request context, but persist against the transaction client — so every write
129
+ in the callback is **atomic** and a throw anywhere rolls the whole transaction
130
+ back. This is the secured alternative to raw `prisma.$transaction`, which
131
+ bypasses access control and hooks.
132
+
133
+ Use it to atomically enforce concurrency-sensitive invariants (e.g. a
134
+ capacity/quota gate) while preserving the access/hook boundary:
135
+
136
+ ```typescript
137
+ async function bookSlot(context: StackContext, slotId: string) {
138
+ for (let attempt = 0; attempt < 5; attempt++) {
139
+ try {
140
+ return await context.transaction(
141
+ async (tx) => {
142
+ const count = await tx.db.booking.count({ where: { slotId } })
143
+ if (count >= CAPACITY) return { booked: false }
144
+ return { booked: true, item: await tx.db.booking.create({ data: { slotId } }) }
145
+ },
146
+ { isolationLevel: 'Serializable' },
147
+ )
148
+ } catch (err) {
149
+ // Serialization failures (Prisma P2034) PROPAGATE — the caller owns retry.
150
+ if (err && typeof err === 'object' && 'code' in err && err.code === 'P2034') continue
151
+ throw err
152
+ }
153
+ }
154
+ throw new Error('exceeded retry budget')
155
+ }
156
+ ```
157
+
158
+ Key points:
159
+
160
+ - **Options pass through** to Prisma — `isolationLevel` (incl. `'Serializable'`),
161
+ `maxWait`, `timeout`.
162
+ - **Serialization failures propagate** (they are NOT converted to a silent
163
+ `null`), so the caller can implement a retry loop. Built-in retry is not
164
+ provided — the caller owns it (matching Keystone 6's `context.transaction`).
165
+ - **Nested `context.db` writes join** the outer transaction (a Prisma tx client
166
+ exposes no `$transaction`, so the Write Pipeline's existing fallback runs them
167
+ against the active client).
168
+ - If the client cannot open an interactive transaction (e.g. a test mock, or you
169
+ are already inside one), `fn` runs directly with identical hook/access
170
+ semantics. See ADR-0012.
121
171
 
122
172
  ### Generators (`src/generator/`)
123
173
 
@@ -26,4 +26,43 @@ export declare function buildIncludeWithAccessControl(fieldConfigs: Record<strin
26
26
  where?: PrismaFilter;
27
27
  include?: Record<string, boolean | /*elided*/ any>;
28
28
  }> | undefined>;
29
+ /**
30
+ * Merge a caller-supplied `include` with the access-controlled include — phase-1
31
+ * row/relation scoping for explicit caller selections.
32
+ *
33
+ * The caller's `include` decides WHICH relations to fetch; access control decides
34
+ * WHETHER each relation may be fetched and WITH WHAT filter. Replacing the
35
+ * access-controlled include with the caller's wholesale (the bug in #566) drops
36
+ * every per-relation access `where` and denied-relation exclusion, silently
37
+ * bypassing row-level access on any non-sudo read that passes `include`.
38
+ *
39
+ * For each relation the caller asks to include:
40
+ * - If the relation is a config-declared relationship but is ABSENT from the
41
+ * access-controlled include, its `query` access returned `false` → it is DROPPED
42
+ * (not fetched).
43
+ * - If it is present (allowed, possibly with a filter), the access entry is used
44
+ * as the base: the access `where` is AND-combined with any caller-supplied
45
+ * nested `where`, and nested includes are recursively merged using the related
46
+ * list's field configs (so deeply-nested selections are filtered at every
47
+ * level). A bare caller `true` becomes the access-controlled shape (filter +
48
+ * nested filtered include), never bare `true`.
49
+ * - If the caller names a key that is NOT a config-declared relationship, it is
50
+ * passed through unchanged (access control does not govern it).
51
+ *
52
+ * The access-controlled include is recursive to `MAX_DEPTH` (see
53
+ * `buildIncludeWithAccessControl`); beyond that depth no auto-include exists, so
54
+ * deeper caller selections pass through unscoped — consistent with the existing
55
+ * auto-include behaviour.
56
+ *
57
+ * `accessControlledInclude` being `undefined` is NOT "every relation denied". It
58
+ * means no access-controlled include was computed at all — a non-denial outcome
59
+ * that `buildIncludeWithAccessControl` returns when inside a `resolveOutput`/
60
+ * virtual-field context, at `MAX_DEPTH`, or when the list has no relationships.
61
+ * In every one of those cases there is nothing to merge against, so the caller's
62
+ * `include` is passed through unchanged (matching the prior `args.include || …`
63
+ * fallback). This is distinct from an `undefined` ENTRY inside a defined access
64
+ * include, which DOES mean the relation was denied and must be dropped (see the
65
+ * per-relation loop below). Only the whole-object `undefined` is a passthrough.
66
+ */
67
+ export declare function mergeIncludeWithAccessControl(callerInclude: Record<string, unknown>, accessControlledInclude: Record<string, unknown> | undefined, fieldConfigs: Record<string, FieldConfig>, config: OpenSaasConfig): Record<string, unknown>;
29
68
  //# sourceMappingURL=access-filter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"access-filter.d.ts","sourceRoot":"","sources":["../../src/access/access-filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACtE,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAGrE;;;;;;;;;;;;;;GAcG;AAEH;;;GAGG;AACH,wBAAsB,6BAA6B,CACjD,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACzC,IAAI,EAAE;IACJ,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,OAAO,EAAE,aAAa,CAAA;CACvB,EACD,MAAM,EAAE,cAAc,EACtB,KAAK,GAAE,MAAU;YAeuB,YAAY;cAAY,MAAM,CAAC,MAAM,2BAAe;gBAkD7F"}
1
+ {"version":3,"file":"access-filter.d.ts","sourceRoot":"","sources":["../../src/access/access-filter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACtE,OAAO,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAGrE;;;;;;;;;;;;;;GAcG;AAEH;;;GAGG;AACH,wBAAsB,6BAA6B,CACjD,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACzC,IAAI,EAAE;IACJ,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,OAAO,EAAE,aAAa,CAAA;CACvB,EACD,MAAM,EAAE,cAAc,EACtB,KAAK,GAAE,MAAU;YAeuB,YAAY;cAAY,MAAM,CAAC,MAAM,2BAAe;gBAkD7F;AAiDD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,6BAA6B,CAC3C,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACtC,uBAAuB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,EAC5D,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EACzC,MAAM,EAAE,cAAc,GACrB,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAgEzB"}
@@ -65,4 +65,125 @@ export async function buildIncludeWithAccessControl(fieldConfigs, args, config,
65
65
  }
66
66
  return hasRelationships ? include : undefined;
67
67
  }
68
+ /**
69
+ * Narrow an unknown include value to the structured object form (vs bare `true`
70
+ * or any other primitive). Caller-supplied includes arrive untyped at the
71
+ * runtime boundary, so we validate the shape here rather than casting.
72
+ */
73
+ function asEntryObject(value) {
74
+ if (value && typeof value === 'object') {
75
+ const obj = value;
76
+ const where = obj.where;
77
+ const include = obj.include;
78
+ const entry = {};
79
+ if (where && typeof where === 'object')
80
+ entry.where = where;
81
+ if (include && typeof include === 'object')
82
+ entry.include = include;
83
+ return entry;
84
+ }
85
+ return null;
86
+ }
87
+ /**
88
+ * AND-combine an access `where` with a caller-supplied nested `where`.
89
+ *
90
+ * The access filter is authoritative: the caller's filter may only NARROW the
91
+ * result further, never widen past what access permits. We therefore wrap both
92
+ * in a Prisma `AND` so neither can override the other. If only one side is
93
+ * present, it is returned as-is; if neither is present, the result is undefined.
94
+ */
95
+ function andWhere(accessWhere, callerWhere) {
96
+ if (accessWhere && callerWhere) {
97
+ return { AND: [accessWhere, callerWhere] };
98
+ }
99
+ return accessWhere ?? callerWhere;
100
+ }
101
+ /**
102
+ * Merge a caller-supplied `include` with the access-controlled include — phase-1
103
+ * row/relation scoping for explicit caller selections.
104
+ *
105
+ * The caller's `include` decides WHICH relations to fetch; access control decides
106
+ * WHETHER each relation may be fetched and WITH WHAT filter. Replacing the
107
+ * access-controlled include with the caller's wholesale (the bug in #566) drops
108
+ * every per-relation access `where` and denied-relation exclusion, silently
109
+ * bypassing row-level access on any non-sudo read that passes `include`.
110
+ *
111
+ * For each relation the caller asks to include:
112
+ * - If the relation is a config-declared relationship but is ABSENT from the
113
+ * access-controlled include, its `query` access returned `false` → it is DROPPED
114
+ * (not fetched).
115
+ * - If it is present (allowed, possibly with a filter), the access entry is used
116
+ * as the base: the access `where` is AND-combined with any caller-supplied
117
+ * nested `where`, and nested includes are recursively merged using the related
118
+ * list's field configs (so deeply-nested selections are filtered at every
119
+ * level). A bare caller `true` becomes the access-controlled shape (filter +
120
+ * nested filtered include), never bare `true`.
121
+ * - If the caller names a key that is NOT a config-declared relationship, it is
122
+ * passed through unchanged (access control does not govern it).
123
+ *
124
+ * The access-controlled include is recursive to `MAX_DEPTH` (see
125
+ * `buildIncludeWithAccessControl`); beyond that depth no auto-include exists, so
126
+ * deeper caller selections pass through unscoped — consistent with the existing
127
+ * auto-include behaviour.
128
+ *
129
+ * `accessControlledInclude` being `undefined` is NOT "every relation denied". It
130
+ * means no access-controlled include was computed at all — a non-denial outcome
131
+ * that `buildIncludeWithAccessControl` returns when inside a `resolveOutput`/
132
+ * virtual-field context, at `MAX_DEPTH`, or when the list has no relationships.
133
+ * In every one of those cases there is nothing to merge against, so the caller's
134
+ * `include` is passed through unchanged (matching the prior `args.include || …`
135
+ * fallback). This is distinct from an `undefined` ENTRY inside a defined access
136
+ * include, which DOES mean the relation was denied and must be dropped (see the
137
+ * per-relation loop below). Only the whole-object `undefined` is a passthrough.
138
+ */
139
+ export function mergeIncludeWithAccessControl(callerInclude, accessControlledInclude, fieldConfigs, config) {
140
+ // No access-controlled include was computed (resolveOutput/virtual context,
141
+ // MAX_DEPTH, or a list with no relationships) → nothing to scope against, so
142
+ // pass the caller's include through unchanged. Dropping relations here would be
143
+ // fail-closed data loss, not a denial. Denied relations are dropped only when a
144
+ // defined access include OMITS them (handled per-relation below).
145
+ if (accessControlledInclude === undefined) {
146
+ return callerInclude;
147
+ }
148
+ const merged = {};
149
+ const accessInclude = accessControlledInclude;
150
+ for (const [relationName, callerValue] of Object.entries(callerInclude)) {
151
+ const fieldConfig = fieldConfigs[relationName];
152
+ const isDeclaredRelationship = fieldConfig?.type === 'relationship' && 'ref' in fieldConfig && !!fieldConfig.ref;
153
+ // Not a config-declared relationship → access control does not govern it; pass through unchanged.
154
+ if (!isDeclaredRelationship) {
155
+ merged[relationName] = callerValue;
156
+ continue;
157
+ }
158
+ const accessValue = accessInclude[relationName];
159
+ // Declared relationship absent from the access include → query access denied → drop it.
160
+ if (accessValue === undefined) {
161
+ continue;
162
+ }
163
+ const accessEntry = asEntryObject(accessValue);
164
+ const callerEntry = asEntryObject(callerValue);
165
+ // Resolve the related list's field configs so nested includes merge recursively.
166
+ const relatedConfig = getRelatedListConfig(fieldConfig.ref, config);
167
+ const relatedFields = relatedConfig?.listConfig.fields;
168
+ const mergedWhere = andWhere(accessEntry?.where, callerEntry?.where);
169
+ let mergedNested;
170
+ if (callerEntry?.include && relatedFields) {
171
+ // Recurse: scope the caller's nested selection against the nested access include.
172
+ mergedNested = mergeIncludeWithAccessControl(callerEntry.include, accessEntry?.include, relatedFields, config);
173
+ }
174
+ else if (accessEntry?.include) {
175
+ // Caller selected the relation bare (no nested include); keep the
176
+ // access-controlled nested include so deeper relations stay filtered.
177
+ mergedNested = accessEntry.include;
178
+ }
179
+ const entry = {};
180
+ if (mergedWhere)
181
+ entry.where = mergedWhere;
182
+ if (mergedNested && Object.keys(mergedNested).length > 0)
183
+ entry.include = mergedNested;
184
+ // A bare-`true` relation with no access filter and no nested include stays `true`.
185
+ merged[relationName] = Object.keys(entry).length > 0 ? entry : true;
186
+ }
187
+ return merged;
188
+ }
68
189
  //# sourceMappingURL=access-filter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"access-filter.js","sourceRoot":"","sources":["../../src/access/access-filter.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AAE/D;;;;;;;;;;;;;;GAcG;AAEH;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,6BAA6B,CACjD,YAAyC,EACzC,IAGC,EACD,MAAsB,EACtB,QAAgB,CAAC;IAEjB,MAAM,SAAS,GAAG,CAAC,CAAA;IACnB,IAAI,KAAK,IAAI,SAAS,EAAE,CAAC;QACvB,OAAO,SAAS,CAAA;IAClB,CAAC;IAED,qEAAqE;IACrE,uEAAuE;IACvE,gFAAgF;IAChF,0EAA0E;IAC1E,IAAI,IAAI,CAAC,OAAO,CAAC,qBAAqB,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC;QACjD,OAAO,SAAS,CAAA;IAClB,CAAC;IAID,MAAM,OAAO,GAAiC,EAAE,CAAA;IAChD,IAAI,gBAAgB,GAAG,KAAK,CAAA;IAE5B,KAAK,MAAM,CAAC,SAAS,EAAE,WAAW,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC;QACpE,IAAI,WAAW,EAAE,IAAI,KAAK,cAAc,IAAI,KAAK,IAAI,WAAW,IAAI,WAAW,CAAC,GAAG,EAAE,CAAC;YACpF,gBAAgB,GAAG,IAAI,CAAA;YACvB,MAAM,aAAa,GAAG,oBAAoB,CAAC,WAAW,CAAC,GAAa,EAAE,MAAM,CAAC,CAAA;YAE7E,IAAI,aAAa,EAAE,CAAC;gBAClB,0CAA0C;gBAC1C,MAAM,WAAW,GAAG,aAAa,CAAC,UAAU,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,CAAA;gBACrE,MAAM,YAAY,GAAG,MAAM,WAAW,CAAC,WAAW,EAAE;oBAClD,OAAO,EAAE,IAAI,CAAC,OAAO;oBACrB,OAAO,EAAE,IAAI,CAAC,OAAO;iBACtB,CAAC,CAAA;gBAEF,4DAA4D;gBAC5D,IAAI,YAAY,KAAK,KAAK,EAAE,CAAC;oBAC3B,SAAQ;gBACV,CAAC;gBAED,0BAA0B;gBAC1B,MAAM,YAAY,GAA4B,EAAE,CAAA;gBAEhD,yDAAyD;gBACzD,IAAI,OAAO,YAAY,KAAK,QAAQ,EAAE,CAAC;oBACrC,YAAY,CAAC,KAAK,GAAG,YAAY,CAAA;gBACnC,CAAC;gBAED,oCAAoC;gBACpC,MAAM,aAAa,GAAG,MAAM,6BAA6B,CACvD,aAAa,CAAC,UAAU,CAAC,MAAM,EAC/B,IAAI,EACJ,MAAM,EACN,KAAK,GAAG,CAAC,CACV,CAAA;gBAED,IAAI,aAAa,IAAI,MAAM,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;oBAC3D,YAAY,CAAC,OAAO,GAAG,aAAa,CAAA;gBACtC,CAAC;gBAED,wBAAwB;gBACxB,OAAO,CAAC,SAAS,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAA;YACjF,CAAC;QACH,CAAC;IACH,CAAC;IAED,OAAO,gBAAgB,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAA;AAC/C,CAAC"}
1
+ {"version":3,"file":"access-filter.js","sourceRoot":"","sources":["../../src/access/access-filter.ts"],"names":[],"mappings":"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"}
@@ -33,6 +33,7 @@ export declare function checkFieldAccess(fieldAccess: FieldAccess | undefined, o
33
33
  export declare function filterWritableFields<T extends Record<string, unknown>>(data: T, fieldConfigs: Record<string, {
34
34
  access?: FieldAccess;
35
35
  type?: string;
36
+ getColumnNames?: (fieldName: string) => string[];
36
37
  }>, operation: 'create' | 'update', args: {
37
38
  session: Session | null;
38
39
  item?: Record<string, unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"field-access.d.ts","sourceRoot":"","sources":["../../src/access/field-access.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAE7C;;;;;;;;;GASG;AAEH;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,WAAW,GAAG,SAAS,EACpC,SAAS,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,EACvC,IAAI,EAAE;IACJ,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,OAAO,EAAE,aAAa,GAAG;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC,GACA,OAAO,CAAC,OAAO,CAAC,CAmClB;AA8BD;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1E,IAAI,EAAE,CAAC,EACP,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE;IAAE,MAAM,CAAC,EAAE,WAAW,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,EACrE,SAAS,EAAE,QAAQ,GAAG,QAAQ,EAC9B,IAAI,EAAE;IACJ,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,OAAO,EAAE,aAAa,GAAG;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC,GACA,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAgDrB"}
1
+ {"version":3,"file":"field-access.d.ts","sourceRoot":"","sources":["../../src/access/field-access.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAM7C;;;;;;;;;GASG;AAEH;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,WAAW,EAAE,WAAW,GAAG,SAAS,EACpC,SAAS,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,EACvC,IAAI,EAAE;IACJ,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,OAAO,EAAE,aAAa,GAAG;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC,GACA,OAAO,CAAC,OAAO,CAAC,CAmClB;AA8BD;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC1E,IAAI,EAAE,CAAC,EACP,YAAY,EAAE,MAAM,CAClB,MAAM,EACN;IACE,MAAM,CAAC,EAAE,WAAW,CAAA;IACpB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,cAAc,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,EAAE,CAAA;CACjD,CACF,EACD,SAAS,EAAE,QAAQ,GAAG,QAAQ,EAC9B,IAAI,EAAE;IACJ,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,OAAO,EAAE,aAAa,GAAG;QAAE,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAA;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACpC,GACA,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CA2HrB"}