@opensaas/stack-core 0.20.1 → 0.22.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 (136) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +334 -0
  3. package/CLAUDE.md +29 -11
  4. package/dist/access/access-filter.d.ts +29 -0
  5. package/dist/access/access-filter.d.ts.map +1 -0
  6. package/dist/access/access-filter.js +68 -0
  7. package/dist/access/access-filter.js.map +1 -0
  8. package/dist/access/engine.d.ts +15 -48
  9. package/dist/access/engine.d.ts.map +1 -1
  10. package/dist/access/engine.js +14 -280
  11. package/dist/access/engine.js.map +1 -1
  12. package/dist/access/field-access.d.ts +44 -0
  13. package/dist/access/field-access.d.ts.map +1 -0
  14. package/dist/access/field-access.js +123 -0
  15. package/dist/access/field-access.js.map +1 -0
  16. package/dist/access/field-access.test.d.ts +2 -0
  17. package/dist/access/field-access.test.d.ts.map +1 -0
  18. package/dist/access/{engine.test.js → field-access.test.js} +2 -2
  19. package/dist/access/field-access.test.js.map +1 -0
  20. package/dist/access/field-visibility.d.ts +13 -0
  21. package/dist/access/field-visibility.d.ts.map +1 -0
  22. package/dist/access/field-visibility.js +178 -0
  23. package/dist/access/field-visibility.js.map +1 -0
  24. package/dist/access/index.d.ts +4 -1
  25. package/dist/access/index.d.ts.map +1 -1
  26. package/dist/access/index.js +8 -1
  27. package/dist/access/index.js.map +1 -1
  28. package/dist/access/multi-column-read-write.test.d.ts +2 -0
  29. package/dist/access/multi-column-read-write.test.d.ts.map +1 -0
  30. package/dist/access/multi-column-read-write.test.js +149 -0
  31. package/dist/access/multi-column-read-write.test.js.map +1 -0
  32. package/dist/config/index.d.ts +1 -1
  33. package/dist/config/index.d.ts.map +1 -1
  34. package/dist/config/types.d.ts +334 -5
  35. package/dist/config/types.d.ts.map +1 -1
  36. package/dist/context/hook-pipeline.d.ts +49 -0
  37. package/dist/context/hook-pipeline.d.ts.map +1 -0
  38. package/dist/context/hook-pipeline.js +75 -0
  39. package/dist/context/hook-pipeline.js.map +1 -0
  40. package/dist/context/index.d.ts.map +1 -1
  41. package/dist/context/index.js +30 -462
  42. package/dist/context/index.js.map +1 -1
  43. package/dist/context/nested-operations.d.ts.map +1 -1
  44. package/dist/context/nested-operations.js +72 -68
  45. package/dist/context/nested-operations.js.map +1 -1
  46. package/dist/context/write-pipeline.d.ts +158 -0
  47. package/dist/context/write-pipeline.d.ts.map +1 -0
  48. package/dist/context/write-pipeline.js +306 -0
  49. package/dist/context/write-pipeline.js.map +1 -0
  50. package/dist/extend.d.ts +3 -0
  51. package/dist/extend.d.ts.map +1 -0
  52. package/dist/extend.js +10 -0
  53. package/dist/extend.js.map +1 -0
  54. package/dist/fields/format-prisma-default.d.ts +35 -0
  55. package/dist/fields/format-prisma-default.d.ts.map +1 -0
  56. package/dist/fields/format-prisma-default.js +52 -0
  57. package/dist/fields/format-prisma-default.js.map +1 -0
  58. package/dist/fields/format-prisma-default.test.d.ts +2 -0
  59. package/dist/fields/format-prisma-default.test.d.ts.map +1 -0
  60. package/dist/fields/format-prisma-default.test.js +54 -0
  61. package/dist/fields/format-prisma-default.test.js.map +1 -0
  62. package/dist/fields/index.d.ts +1 -0
  63. package/dist/fields/index.d.ts.map +1 -1
  64. package/dist/fields/index.js +267 -18
  65. package/dist/fields/index.js.map +1 -1
  66. package/dist/fields/select.test.js +85 -0
  67. package/dist/fields/select.test.js.map +1 -1
  68. package/dist/fields/text-keystone-compat.test.d.ts +2 -0
  69. package/dist/fields/text-keystone-compat.test.d.ts.map +1 -0
  70. package/dist/fields/text-keystone-compat.test.js +93 -0
  71. package/dist/fields/text-keystone-compat.test.js.map +1 -0
  72. package/dist/hooks/index.d.ts +20 -0
  73. package/dist/hooks/index.d.ts.map +1 -1
  74. package/dist/hooks/index.js +246 -0
  75. package/dist/hooks/index.js.map +1 -1
  76. package/dist/index.d.ts +6 -8
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +25 -9
  79. package/dist/index.js.map +1 -1
  80. package/dist/index.test.d.ts +2 -0
  81. package/dist/index.test.d.ts.map +1 -0
  82. package/dist/index.test.js +33 -0
  83. package/dist/index.test.js.map +1 -0
  84. package/dist/internal.d.ts +8 -0
  85. package/dist/internal.d.ts.map +1 -0
  86. package/dist/internal.js +16 -0
  87. package/dist/internal.js.map +1 -0
  88. package/dist/mcp/handler.js +0 -1
  89. package/dist/mcp/handler.js.map +1 -1
  90. package/dist/validation/field-config.d.ts +55 -0
  91. package/dist/validation/field-config.d.ts.map +1 -0
  92. package/dist/validation/field-config.js +100 -0
  93. package/dist/validation/field-config.js.map +1 -0
  94. package/dist/validation/field-config.test.d.ts +2 -0
  95. package/dist/validation/field-config.test.d.ts.map +1 -0
  96. package/dist/validation/field-config.test.js +159 -0
  97. package/dist/validation/field-config.test.js.map +1 -0
  98. package/package.json +11 -3
  99. package/src/access/access-filter.ts +97 -0
  100. package/src/access/engine.ts +13 -396
  101. package/src/access/{engine.test.ts → field-access.test.ts} +1 -1
  102. package/src/access/field-access.ts +159 -0
  103. package/src/access/field-visibility.ts +269 -0
  104. package/src/access/index.ts +7 -4
  105. package/src/access/multi-column-read-write.test.ts +255 -0
  106. package/src/config/index.ts +3 -0
  107. package/src/config/types.ts +342 -4
  108. package/src/context/hook-pipeline.ts +160 -0
  109. package/src/context/index.ts +29 -667
  110. package/src/context/nested-operations.ts +142 -111
  111. package/src/context/write-pipeline.ts +543 -0
  112. package/src/extend.ts +19 -0
  113. package/src/fields/format-prisma-default.test.ts +64 -0
  114. package/src/fields/format-prisma-default.ts +67 -0
  115. package/src/fields/index.ts +375 -20
  116. package/src/fields/select.test.ts +99 -0
  117. package/src/fields/text-keystone-compat.test.ts +126 -0
  118. package/src/hooks/index.ts +270 -0
  119. package/src/index.test.ts +50 -0
  120. package/src/index.ts +35 -82
  121. package/src/internal.ts +49 -0
  122. package/src/mcp/handler.ts +0 -2
  123. package/src/validation/field-config.test.ts +199 -0
  124. package/src/validation/field-config.ts +145 -0
  125. package/tests/access-relationships.test.ts +4 -4
  126. package/tests/access.test.ts +1 -1
  127. package/tests/field-hooks.test.ts +410 -0
  128. package/tests/field-types.test.ts +1 -1
  129. package/tests/hook-pipeline.test.ts +233 -0
  130. package/tests/nested-operation-registry.test.ts +206 -0
  131. package/tests/write-pipeline.test.ts +588 -0
  132. package/tsconfig.tsbuildinfo +1 -1
  133. package/vitest.config.ts +43 -1
  134. package/dist/access/engine.test.d.ts +0 -2
  135. package/dist/access/engine.test.d.ts.map +0 -1
  136. package/dist/access/engine.test.js.map +0 -1
@@ -1,4 +1,4 @@
1
1
 
2
- > @opensaas/stack-core@0.20.1 build /home/runner/work/stack/stack/packages/core
2
+ > @opensaas/stack-core@0.22.0 build /home/runner/work/stack/stack/packages/core
3
3
  > tsc
4
4
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,339 @@
1
1
  # @opensaas/stack-core
2
2
 
3
+ ## 0.22.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#497](https://github.com/OpenSaasAU/stack/pull/497) [`be4181a`](https://github.com/OpenSaasAU/stack/commit/be4181ada3f2d6386052df4d4869ad150d360f89) Thanks [@{](https://github.com/{)! - Derive the auth plugin's Auth lists from the better-auth config
8
+
9
+ `authPlugin` now mirrors the better-auth config a developer writes instead of hardcoding the keys `User`/`Session`/`Account`/`Verification`. Per-model `modelName` becomes the OpenSaaS list key (and a table `@@map`), and the `fields` column map becomes per-field `@map`s. The plugin only ever adds/extends its own derived keys, so an app's separate domain `User` is never overwritten. The runtime `getUser`/`getCurrentUser` helpers now resolve the user list key from the configured user model instead of a hardcoded `'user'`.
10
+
11
+ Default behaviour (no overrides) is unchanged: the lists are still keyed `User`/`Session`/`Account`/`Verification` with the original field shapes and no `@@map`.
12
+
13
+ ```typescript
14
+ // Adopt existing better-auth tables without a destructive migration
15
+ authPlugin({
16
+ modelName: 'AuthUser', fields: { name: 'full_name' } },
17
+ session: { modelName: 'AuthSession', fields: { userId: 'user_id' } },
18
+ account: { modelName: 'AuthAccount' },
19
+ verification: { modelName: 'AuthVerification' },
20
+ })
21
+ // -> lists keyed AuthUser/AuthSession/AuthAccount/AuthVerification
22
+ // with @@map + column @map matching the live tables
23
+ ```
24
+
25
+ Lists also gain a model-level `db.map` option, which emits a `@@map("...")` on the generated Prisma model so a list key can differ from its physical table name.
26
+
27
+ - [#498](https://github.com/OpenSaasAU/stack/pull/498) [`dc51f23`](https://github.com/OpenSaasAU/stack/commit/dc51f237323ee53a705c4b9831dd8db85efd9bc1) Thanks [@borisno2](https://github.com/borisno2)! - Add an `output` config block so `opensaas generate` can relocate the generated Prisma schema and `.opensaas` bundle (e.g. to coexist with an existing Keystone `prisma/` during migration)
28
+
29
+ Set `output.prismaSchema` and/or `output.opensaasDir` in `opensaas.config.ts` to move where the generator writes. Defaults are unchanged (`prisma/schema.prisma`, `.opensaas/`) when the block is omitted. The generated files' cross-references follow the configured locations: `context.ts`/`prisma-extensions.ts` import `opensaas.config` from the resolved bundle, the Prisma client `generator { output }` points back at the relocated bundle, and the top-level `prisma.config.ts` references the configured schema directory so `prisma` CLI commands keep working.
30
+
31
+ The pre-existing top-level `opensaasPath` option is preserved: the effective `.opensaas` bundle directory resolves as `output.opensaasDir` > `opensaasPath` > the default `.opensaas`. Setting `opensaasPath` alone still relocates the bundle through the CLI exactly as before; `output.opensaasDir` overrides it when both are set.
32
+
33
+ ```typescript
34
+ export default config({
35
+ output: {
36
+ prismaSchema: 'prisma-opensaas/schema.prisma',
37
+ opensaasDir: 'generated/opensaas',
38
+ },
39
+ db: {
40
+ /* ... */
41
+ },
42
+ lists: {
43
+ /* ... */
44
+ },
45
+ })
46
+ ```
47
+
48
+ - [#511](https://github.com/OpenSaasAU/stack/pull/511) [`696f5c0`](https://github.com/OpenSaasAU/stack/commit/696f5c08c37d4a18107e48cb6b360c9492c7425c) Thanks [@borisno2](https://github.com/borisno2)! - Add non-destructive multi-column mode to `image()` / `file()` for adopting an existing Keystone database without dropping columns (ADR-0006).
49
+
50
+ Keystone stores an image across seven per-part columns (`_url`, `_width`, `_height`, `_filesize`, `_contentType`, `_contentDisposition`, `_pathname`) and a file across three (`_filename`, `_filesize`, `_url`). By default `image()`/`file()` still back a single `Json?` column (greenfield unchanged). Set `db.columns: 'keystone'` to map the field onto the existing per-part columns in place — assembled into an `ImageMetadata`/`FileMetadata` on read and split back on write — so a migrating project reaches a clean schema diff with no data migration and no re-upload of existing assets.
51
+
52
+ ```typescript
53
+ import { image, file } from '@opensaas/stack-storage/fields'
54
+
55
+ fields: {
56
+ // Maps onto image_url, image_width, … image_pathname in place.
57
+ avatar: image({ storage: 'images', db: { columns: 'keystone' } }),
58
+
59
+ // Per-part @map names are configurable for non-default column names.
60
+ cover: image({
61
+ storage: 'images',
62
+ db: { columns: { mode: 'keystone', map: { url: 'cover_link' } } },
63
+ }),
64
+
65
+ resume: file({ storage: 'documents', db: { columns: 'keystone' } }),
66
+ }
67
+ ```
68
+
69
+ No-re-upload guarantee (both modes): an already-shaped metadata value — or, in multi-column mode, populated columns — is authoritative and never triggers a storage upload; only a `File`-like input uploads.
70
+
71
+ Adds a multi-column field-emission contract (`getPrismaColumns`) plus `getColumnNames`/`assembleColumns`/`splitColumns` to the field-authoring surface so any field can map onto several physical columns. The generator emits one `@map`-ped Prisma line per column; reads assemble the logical value from the raw columns and strip them from the result; writes split the logical value back across the columns.
72
+
73
+ - [#499](https://github.com/OpenSaasAU/stack/pull/499) [`f9e0505`](https://github.com/OpenSaasAU/stack/commit/f9e05053c75c76781751d5d9e5d1ed5cd9be635f) Thanks [@borisno2](https://github.com/borisno2)! - Add opt-in `db.keystoneCompat` mode for Keystone-compatible empty-string text defaults
74
+
75
+ When migrating from Keystone 6, every non-null text column carries an implicit empty-string default. Set `db: { keystoneCompat: true }` to mirror that: any non-null `text()` column without an explicit `defaultValue` now generates `String @default("")`, so a migrating schema reaches parity without hand-setting `defaultValue: ''` on dozens of columns.
76
+
77
+ The mode is off by default (greenfield schemas stay clean) and never affects nullable text, fields with an explicit `defaultValue`, or any non-text field — an explicit `text({ defaultValue: 'x' })` always wins.
78
+
79
+ ```typescript
80
+ export default config({
81
+ db: {
82
+ provider: 'postgresql',
83
+ keystoneCompat: true, // non-null text without a default → @default("")
84
+ prismaClientConstructor: (PrismaClient) => {
85
+ // ... adapter setup
86
+ },
87
+ },
88
+ lists: {
89
+ Account: list({
90
+ fields: {
91
+ // required text → String @default("")
92
+ name: text({ validation: { isRequired: true } }),
93
+ // explicit default still wins → String @default("PLEASE_UPDATE")
94
+ status: text({ validation: { isRequired: true }, defaultValue: 'PLEASE_UPDATE' }),
95
+ // nullable text is untouched → String?
96
+ bio: text(),
97
+ },
98
+ }),
99
+ },
100
+ })
101
+ ```
102
+
103
+ See ADR-0004 for the full Keystone-compatible generator defaults.
104
+
105
+ - [#501](https://github.com/OpenSaasAU/stack/pull/501) [`e30f6a1`](https://github.com/OpenSaasAU/stack/commit/e30f6a1ef69dc65ae68b37539fa74c3f97823cfd) Thanks [@borisno2](https://github.com/borisno2)! - Auto-timestamps are now OFF by default; opt in with `db.timestamps`
106
+
107
+ The generator no longer appends `createdAt`/`updatedAt` to every model. This matches
108
+ Keystone 6 (which never adds them automatically) and keeps Keystone → stack migrations
109
+ non-destructive. A list opts in either by declaring the fields itself or by enabling the
110
+ new `db.timestamps` flag. See ADR-0004.
111
+
112
+ Note: this changes a long-standing default. Existing apps that relied on auto-injected
113
+ timestamps should set `db: { timestamps: true }` to keep them.
114
+
115
+ Enable globally:
116
+
117
+ ```typescript
118
+ export default config({
119
+ db: {
120
+ provider: 'postgresql',
121
+ timestamps: true, // re-enable auto createdAt/updatedAt for all lists
122
+ // ...
123
+ },
124
+ lists: {
125
+ /* ... */
126
+ },
127
+ })
128
+ ```
129
+
130
+ Override per list (takes precedence over the global setting):
131
+
132
+ ```typescript
133
+ lists: {
134
+ // Opt this one list out even though timestamps are on globally
135
+ Production: list({
136
+ fields: { name: text() },
137
+ db: { timestamps: false },
138
+ }),
139
+ // Opt this one list in even though the global default is off
140
+ Audited: list({
141
+ fields: { name: text() },
142
+ db: { timestamps: true },
143
+ }),
144
+ }
145
+ ```
146
+
147
+ When timestamps are enabled and a list already declares its own `createdAt`/`updatedAt`
148
+ field, the auto column is skipped for the declared field(s) so Prisma never sees a
149
+ duplicate (`P1012`):
150
+
151
+ ```typescript
152
+ lists: {
153
+ Post: list({
154
+ fields: {
155
+ title: text(),
156
+ createdAt: timestamp(), // kept as declared; no duplicate auto column
157
+ },
158
+ }),
159
+ }
160
+ ```
161
+
162
+ The decision is exposed as a pure, testable predicate `resolveListTimestamps(listConfig, dbConfig)`
163
+ from `@opensaas/stack-cli`, and `DatabaseConfig` is now re-exported from `@opensaas/stack-core`.
164
+
165
+ - [#503](https://github.com/OpenSaasAU/stack/pull/503) [`f471e3c`](https://github.com/OpenSaasAU/stack/commit/f471e3c95eee2254ac9fde04adc8c5693240e293) Thanks [@borisno2](https://github.com/borisno2)! - Add `select()` db options for Keystone schema parity: `db.isNullable` and `db.enumName`.
166
+
167
+ `db.isNullable: true` forces the nullable `?` on the generated column even when a
168
+ `defaultValue` is present. The default behaviour is unchanged — a select with a
169
+ `defaultValue` still generates NOT NULL unless you opt in explicitly:
170
+
171
+ ```typescript
172
+ // Optional select with a default, kept nullable for data containing NULLs
173
+ status: select({
174
+ options: [
175
+ { label: 'Draft', value: 'draft' },
176
+ { label: 'Published', value: 'published' },
177
+ ],
178
+ defaultValue: 'draft',
179
+ db: { isNullable: true },
180
+ })
181
+ // Generates: status String? @default("draft")
182
+
183
+ // Enum-backed equivalent
184
+ status: select({
185
+ options: [{ label: 'Open', value: 'open' }],
186
+ defaultValue: 'open',
187
+ db: { type: 'enum', isNullable: true },
188
+ })
189
+ // Generates: status <Enum>? @default(open)
190
+ ```
191
+
192
+ `db.enumName` overrides the derived `<List><Field>` name of the generated Prisma
193
+ enum for native-enum selects, renaming both the `enum` block and every reference
194
+ to it in the owning model — useful for matching a live DB enum (e.g. Keystone's
195
+ `…Type` suffix):
196
+
197
+ ```typescript
198
+ status: select({
199
+ options: [
200
+ { label: 'Open', value: 'open' },
201
+ { label: 'Closed', value: 'closed' },
202
+ ],
203
+ db: { type: 'enum', enumName: 'AccountNoteStatusType' },
204
+ })
205
+ // Generates: enum AccountNoteStatusType { ... } and the column references it
206
+ ```
207
+
208
+ - [#493](https://github.com/OpenSaasAU/stack/pull/493) [`acb6100`](https://github.com/OpenSaasAU/stack/commit/acb6100a078aca29e94a82ebe607d2d4f8683af2) Thanks [@borisno2](https://github.com/borisno2)! - Honour `defaultValue` for `text()`, `integer()`, and `json()` fields in the generated Prisma schema
209
+
210
+ These three field builders previously dropped `defaultValue` and emitted no `@default(...)`. They now serialise the configured default into a Prisma `@default(...)` literal via a new shared, pure `formatPrismaDefault` module, matching Keystone 6 conventions. The nullable `?` modifier is preserved independently of the default, and fields without a `defaultValue` still emit no `@default(...)`.
211
+
212
+ ```typescript
213
+ fields: {
214
+ // Int @default(3550)
215
+ quota: integer({ defaultValue: 3550 }),
216
+ // String @default("PLEASE_UPDATE")
217
+ status: text({ defaultValue: 'PLEASE_UPDATE' }),
218
+ // Json? @default("[1,2,3,4,5]") — Keystone's space-free JSON literal
219
+ limits: json({ defaultValue: [1, 2, 3, 4, 5] }),
220
+ // Json? @default("[]")
221
+ tags: json({ defaultValue: [] }),
222
+ }
223
+ ```
224
+
225
+ See ADR-0004 for the Keystone-compatibility rationale.
226
+
227
+ - [#502](https://github.com/OpenSaasAU/stack/pull/502) [`593390c`](https://github.com/OpenSaasAU/stack/commit/593390c57d9844ca7ada8f45b340c849f1d8d647) Thanks [@{](https://github.com/{)! - Add `authPlugin` schema placement so Auth lists can adopt an existing non-`public` better-auth layout (clean-diff adoption)
228
+
229
+ The auth lists can now be placed in a non-`public` Postgres schema (e.g. `auth`) so they diff CLEAN against a separate-schema better-auth installation. A plugin-level `schema` option applies `@@schema(...)` to all generated Auth lists, with a per-list override.
230
+
231
+ ```typescript
232
+ authPlugin({
233
+ schema: 'auth', // all Auth lists get @@schema("auth")
234
+ modelName: 'AuthUser' },
235
+ session: { modelName: 'AuthSession' },
236
+ account: { modelName: 'AuthAccount' },
237
+ // per-model override: relocate one list to a different schema
238
+ verification: { modelName: 'AuthVerification', schema: 'auth_internal' },
239
+ })
240
+ ```
241
+
242
+ The plugin's `beforeGenerate` hook wires the datasource `schemas` array (always including `public`) and defaults any list without an explicit `db.schema` to `public`, producing a valid multi-schema Prisma schema. With no `schema` option the output is unchanged (greenfield default stays in `public`, no `@@schema`).
243
+
244
+ Core support added for this (mirroring the `db.map` → `@@map` work):
245
+ - List-level `db.schema` → the Prisma generator emits `@@schema("...")` on the model.
246
+ - Database-level `db.schemas` → the generator emits the datasource `schemas = [...]` array and enables the `multiSchema` preview feature.
247
+
248
+ ```typescript
249
+ // Core/generator building blocks
250
+ db: { provider: 'postgresql', schemas: ['public', 'auth'] }
251
+ AuthUser: list({ fields: { ... }, db: { map: 'AuthUser', schema: 'auth' } })
252
+ // Generates: model AuthUser { ... @@map("AuthUser") @@schema("auth") }
253
+ ```
254
+
255
+ ### Patch Changes
256
+
257
+ - [#500](https://github.com/OpenSaasAU/stack/pull/500) [`309c666`](https://github.com/OpenSaasAU/stack/commit/309c666388b71e2bfbe16b7da3ee0f923b3bf716) Thanks [@borisno2](https://github.com/borisno2)! - Re-export the fragment query API (`defineFragment`, `runQuery`, `runQueryOne`, and the `ResultOf`, `RelationSelector`, `QueryArgs` types) from the package root so the documented `import { defineFragment, runQuery, runQueryOne, type ResultOf } from '@opensaas/stack-core'` resolves.
258
+
259
+ - [#511](https://github.com/OpenSaasAU/stack/pull/511) [`696f5c0`](https://github.com/OpenSaasAU/stack/commit/696f5c08c37d4a18107e48cb6b360c9492c7425c) Thanks [@borisno2](https://github.com/borisno2)! - Fix field-level write-access bypass for multi-column `image()`/`file()` fields. The per-part column split now respects the field's own `create`/`update` access (denied fields write none of their columns), matching single-column behaviour.
260
+
261
+ Note the known lossy multi-column round-trip when assembling legacy Keystone columns: `originalFilename` collapses to `filename`, `uploadedAt` is `''`, and a NULL `contentType` reads back as `application/octet-stream`.
262
+
263
+ - [#518](https://github.com/OpenSaasAU/stack/pull/518) [`d152203`](https://github.com/OpenSaasAU/stack/commit/d1522035e21b6ad7ad1b89b05264c54c13dadcf1) Thanks [@borisno2](https://github.com/borisno2)! - Remove leftover debug console.log statements from runtime code (password field resolveInput and MCP tool call handler)
264
+
265
+ ## 0.21.0
266
+
267
+ ### Minor Changes
268
+
269
+ - [#415](https://github.com/OpenSaasAU/stack/pull/415) [`8980ff3`](https://github.com/OpenSaasAU/stack/commit/8980ff36ffb0879d8f4409740493dd940572cc9d) Thanks [@borisno2](https://github.com/borisno2)! - Curate the `@opensaas/stack-core` public surface into clearly-scoped entry points
270
+
271
+ The root entry point now exposes only the everyday consumer surface — `config`,
272
+ `list`, `getContext`, the naming helpers (`getDbKey`, `getUrlKey`,
273
+ `getListKeyFromUrl`), `ValidationError`, and the config/access types you annotate
274
+ with. Plugin and field authoring contracts move to a new `/extend` path, and the
275
+ plumbing shared with sibling packages and generated code moves to `/internal`.
276
+
277
+ ```typescript
278
+ // Everyday usage (unchanged)
279
+ import { config, list, getContext } from '@opensaas/stack-core'
280
+
281
+ // Authoring a plugin or a third-party field package
282
+ import type { Plugin, BaseFieldConfig, TypeInfo } from '@opensaas/stack-core/extend'
283
+ ```
284
+
285
+ `@opensaas/stack-core/internal` carries no semver guarantees; application code
286
+ should never import from it. `Session` stays on the root entry point because it is
287
+ the module-augmentation target.
288
+
289
+ Removed from the public surface (zero callers): the nine `*HookArgs` types and the
290
+ callerless typed-query runtime types. The other `@opensaas/*` packages and the CLI
291
+ generator are updated to import from the new paths.
292
+
293
+ - [#416](https://github.com/OpenSaasAU/stack/pull/416) [`841a836`](https://github.com/OpenSaasAU/stack/commit/841a836494e2647f390ae19a8c4121d38ebd2fa4) Thanks [@borisno2](https://github.com/borisno2)! - Move field-config types to `@opensaas/stack-core/fields`, beside their builders
294
+
295
+ The concrete field-config types (`TextField`, `IntegerField`, `CheckboxField`,
296
+ `TimestampField`, `PasswordField`, `SelectField`, `RelationshipField`,
297
+ `JsonField`, `VirtualField`, plus `DecimalField`, `CalendarDayField`, and
298
+ `PrismaRelationResult`) now live on the `/fields` entry point alongside the
299
+ builders that produce them, instead of the root barrel. One concept, one import
300
+ path:
301
+
302
+ ```typescript
303
+ import { text, decimal } from '@opensaas/stack-core/fields'
304
+ import type { TextField, DecimalField } from '@opensaas/stack-core/fields'
305
+ ```
306
+
307
+ `DecimalField` and `CalendarDayField` were previously defined but exported from
308
+ nowhere — they are now public, and the CLI's lists generator maps `decimal`/
309
+ `calendarDay` fields to their precise types instead of the generic
310
+ `BaseFieldConfig` fallback. The umbrella `FieldConfig` stays on the root entry
311
+ point and `BaseFieldConfig` stays on `/extend`.
312
+
313
+ ### Patch Changes
314
+
315
+ - [#441](https://github.com/OpenSaasAU/stack/pull/441) [`bc20bf4`](https://github.com/OpenSaasAU/stack/commit/bc20bf447cf724bd0ee153ea9a69d54cc26a6bb2) Thanks [@borisno2](https://github.com/borisno2)! - Validate field self-containment at config load instead of failing deep in generation
316
+
317
+ Core now exports `validateFieldConfig(field, fieldKey, listKey?)` and `validateConfigFields(config)` (plus the `FieldConfigValidationError` type). They check each field implements its generation contract — `getPrismaType`, `getTypeScriptType`, and `getZodSchema` (or `getPrismaRelation` for relationships; virtual fields skip `getPrismaType`) — and return structured per-field errors. `opensaas generate` runs this first and fails fast with a clear message naming the list, field, and missing method, rather than throwing an opaque stack trace mid-generation.
318
+
319
+ - [#428](https://github.com/OpenSaasAU/stack/pull/428) [`50371ea`](https://github.com/OpenSaasAU/stack/commit/50371ea3dd134f6b3718f347fed2c0d3b7dc63ce) Thanks [@borisno2](https://github.com/borisno2)! - Fix outdated SQLite adapter guidance to match the installed `@prisma/adapter-better-sqlite3` API (`PrismaBetterSqlite3` constructed with `{ url }`), so copied examples actually run. Updates the CLI "missing adapter" error message and the migration config it generates, plus the `prismaClientConstructor` JSDoc example.
320
+
321
+ - [#440](https://github.com/OpenSaasAU/stack/pull/440) [`70b4f53`](https://github.com/OpenSaasAU/stack/commit/70b4f538d380bbf546af50a985d29b48a71d3b4d) Thanks [@borisno2](https://github.com/borisno2)! - Refactor nested-operation dispatch into a handler registry (internal, no behaviour change)
322
+
323
+ - [#397](https://github.com/OpenSaasAU/stack/pull/397) [`8e394ab`](https://github.com/OpenSaasAU/stack/commit/8e394abe9df2da53ba23b93836853516bb4e25d5) Thanks [@borisno2](https://github.com/borisno2)! - Move relationship Prisma schema generation into the relationship field builder
324
+
325
+ The relationship field now exposes a `getPrismaRelation()` method that returns its complete Prisma schema contribution (FK line, relation line, synthetic back-relation). The Prisma generator delegates to this method instead of special-casing relationships, keeping it a neutral coordinator. Generated schemas are unchanged.
326
+
327
+ - [#455](https://github.com/OpenSaasAU/stack/pull/455) [`d3fdf2a`](https://github.com/OpenSaasAU/stack/commit/d3fdf2a2e5374302bc7fe1fe814cb0f567a349df) Thanks [@borisno2](https://github.com/borisno2)! - Exclude `**/dist/**` from Vitest test discovery and gate coverage on `src/access`, `src/context`, and `src/validation` via per-file thresholds.
328
+
329
+ - [#403](https://github.com/OpenSaasAU/stack/pull/403) [`0f9c644`](https://github.com/OpenSaasAU/stack/commit/0f9c644a115ad747e338e6138b4762b4a48a9144) Thanks [@borisno2](https://github.com/borisno2)! - Split the access engine into named two-phase-read modules: Access Filter (pre-query), Field Visibility (post-query), and a shared field-access evaluator. No behaviour or public API change.
330
+
331
+ - [#411](https://github.com/OpenSaasAU/stack/pull/411) [`96258b0`](https://github.com/OpenSaasAU/stack/commit/96258b00bb762d9e38cfb83eacae65ce670b161f) Thanks [@borisno2](https://github.com/borisno2)! - Deduplicate field-level hook execution helpers by promoting them to `hooks/index.ts`, and remove a stray `console.log` that ran on every create/update.
332
+
333
+ - [#439](https://github.com/OpenSaasAU/stack/pull/439) [`898e477`](https://github.com/OpenSaasAU/stack/commit/898e47747abc02e457a54e2a78939450d16da5fb) Thanks [@borisno2](https://github.com/borisno2)! - Internal refactor: extract the write transform+validate span into a single Hook Pipeline that the Write Pipeline delegates to. No behaviour change.
334
+
335
+ - [#438](https://github.com/OpenSaasAU/stack/pull/438) [`29966b2`](https://github.com/OpenSaasAU/stack/commit/29966b23597199bcf4233298b1d0de6401b91acd) Thanks [@borisno2](https://github.com/borisno2)! - Refactor the write path into a single Write Pipeline. The canonical secured write sequence (hooks, validation, access, writable-field filtering, nested operations, persistence, after-hooks, Field Visibility) now lives in one module; create/update/delete are thin adapters over it parameterised by a per-operation strategy. Internal refactor only — no public API or behaviour change.
336
+
3
337
  ## 0.20.1
4
338
 
5
339
  ## 0.20.0
package/CLAUDE.md CHANGED
@@ -6,6 +6,18 @@ Core stack providing config system, access control engine, hooks, field types, a
6
6
 
7
7
  The foundation of OpenSaas Stack. Defines the config DSL, executes access control, runs hooks, and generates Prisma schema and TypeScript types from config.
8
8
 
9
+ ## Entry Points
10
+
11
+ The package exposes a curated surface across several import paths. Use the narrowest one that fits:
12
+
13
+ - **`@opensaas/stack-core`** (root) — the everyday consumer surface: `config`, `list`, `getContext`, the naming helpers (`getDbKey`, `getUrlKey`, `getListKeyFromUrl`), `ValidationError`, and the config/access types you annotate with (`OpenSaasConfig`, `ListConfig`, `FieldConfig`, `AccessControl`, `FieldAccess`, `Session`, `AccessContext`, `PrismaFilter`, `OperationAccess`).
14
+ - **`@opensaas/stack-core/fields`** — field builder functions (`text()`, `integer()`, …) and their config types (`TextField`, `IntegerField`, `DecimalField`, `CalendarDayField`, …, plus `PrismaRelationResult`). The builders and the types they produce live together here.
15
+ - **`@opensaas/stack-core/extend`** — authoring contracts: implement these to build a plugin (`Plugin`, `PluginContext`, `GeneratedFiles`) or a third-party field package (`BaseFieldConfig`, `TypeInfo`, `TypeDescriptor`).
16
+ - **`@opensaas/stack-core/mcp`** — MCP runtime handlers.
17
+ - **`@opensaas/stack-core/internal`** — `@internal` plumbing shared between the `@opensaas/*` packages and generated `.opensaas/` code. **No semver guarantees**; application code should never import from here.
18
+
19
+ `Session` deliberately stays on the root entry point because it is the module-augmentation target (`declare module '@opensaas/stack-core'`).
20
+
9
21
  ## Key Files & Exports
10
22
 
11
23
  ### Config (`src/config/`)
@@ -115,12 +127,16 @@ Hook types:
115
127
 
116
128
  Run via CLI: `pnpm generate`
117
129
 
118
- ### Utilities (`src/utils.ts`)
130
+ ### Utilities (`src/lib/case-utils.ts`)
131
+
132
+ Public naming helpers (exported from the root entry point):
119
133
 
120
134
  - `getDbKey(listKey)` - PascalCase → camelCase (e.g., `BlogPost` → `blogPost`)
121
135
  - `getUrlKey(listKey)` - PascalCase → kebab-case (e.g., `BlogPost` → `blog-post`)
122
136
  - `getListKeyFromUrl(urlKey)` - kebab-case → PascalCase (e.g., `blog-post` → `BlogPost`)
123
137
 
138
+ The lower-level converters (`pascalToCamel`, `pascalToKebab`, `kebabToPascal`, `kebabToCamel`) are internal plumbing on `@opensaas/stack-core/internal`.
139
+
124
140
  ## Architecture Patterns
125
141
 
126
142
  ### Field Self-Containment
@@ -157,21 +173,23 @@ const prismaType = field.getPrismaType(fieldName)
157
173
 
158
174
  1. List `resolveInput`
159
175
  2. Field `resolveInput` (e.g., hash password)
160
- 3. List `validateInput`
161
- 4. Field validation (isRequired, length, min/max)
162
- 5. Field-level access control (filter writable fields)
163
- 6. Field `beforeOperation`
164
- 7. List `beforeOperation`
165
- 8. **Database operation**
166
- 9. List `afterOperation`
167
- 10. Field `afterOperation`
176
+ 3. List `validate`
177
+ 4. Field `validate`
178
+ 5. Field validation (isRequired, length, min/max)
179
+ 6. Field-level access control (filter writable fields)
180
+ 7. Field `beforeOperation`
181
+ 8. List `beforeOperation`
182
+ 9. **Database operation**
183
+ 10. List `afterOperation`
184
+ 11. Field `afterOperation`
168
185
 
169
186
  ### Hook Execution Order (Read)
170
187
 
188
+ Reads run no `afterOperation` (list or field):
189
+
171
190
  1. **Database operation**
172
191
  2. Field-level access control (filter readable fields)
173
192
  3. Field `resolveOutput`
174
- 4. Field `afterOperation`
175
193
 
176
194
  ### Context Type Safety
177
195
 
@@ -206,7 +224,7 @@ const context = createContext<typeof prisma>(config, prisma, session)
206
224
 
207
225
  ### With Third-Party Field Packages
208
226
 
209
- - Packages export field builders implementing `BaseFieldConfig`
227
+ - Packages export field builders implementing `BaseFieldConfig` (imported from `@opensaas/stack-core/extend`)
210
228
  - No changes needed to core - fields are self-contained
211
229
  - Example: `@opensaas/stack-tiptap` provides `richText()` field
212
230
 
@@ -0,0 +1,29 @@
1
+ import type { Session, AccessContext, PrismaFilter } from './types.js';
2
+ import type { OpenSaasConfig, FieldConfig } from '../config/types.js';
3
+ /**
4
+ * Access Filter — phase 1 of the two-phase read (pre-query).
5
+ *
6
+ * This module scopes which rows and relationships the database is allowed to
7
+ * return, before the query runs. It evaluates *operation-level* `query` access
8
+ * on related lists and turns the results into a Prisma `include`/`where` clause,
9
+ * so denied rows and relations never leave the database.
10
+ *
11
+ * Phase 2 (post-query field stripping + `resolveOutput` + virtual computation)
12
+ * lives in `field-visibility.ts`. The two phases cannot be merged: virtual
13
+ * fields are computed in JavaScript and post-query field access can depend on
14
+ * the fetched row, neither of which is expressible in SQL. See
15
+ * `docs/adr/0001-access-control-is-a-two-phase-read.md` and the access-control
16
+ * glossary in `CONTEXT.md`.
17
+ */
18
+ /**
19
+ * Build Prisma include object with access control filters
20
+ * This allows us to filter relationships at the database level instead of in memory
21
+ */
22
+ export declare function buildIncludeWithAccessControl(fieldConfigs: Record<string, FieldConfig>, args: {
23
+ session: Session | null;
24
+ context: AccessContext;
25
+ }, config: OpenSaasConfig, depth?: number): Promise<Record<string, boolean | {
26
+ where?: PrismaFilter;
27
+ include?: Record<string, boolean | /*elided*/ any>;
28
+ }> | undefined>;
29
+ //# sourceMappingURL=access-filter.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,68 @@
1
+ import { checkAccess, getRelatedListConfig } from './engine.js';
2
+ /**
3
+ * Access Filter — phase 1 of the two-phase read (pre-query).
4
+ *
5
+ * This module scopes which rows and relationships the database is allowed to
6
+ * return, before the query runs. It evaluates *operation-level* `query` access
7
+ * on related lists and turns the results into a Prisma `include`/`where` clause,
8
+ * so denied rows and relations never leave the database.
9
+ *
10
+ * Phase 2 (post-query field stripping + `resolveOutput` + virtual computation)
11
+ * lives in `field-visibility.ts`. The two phases cannot be merged: virtual
12
+ * fields are computed in JavaScript and post-query field access can depend on
13
+ * the fetched row, neither of which is expressible in SQL. See
14
+ * `docs/adr/0001-access-control-is-a-two-phase-read.md` and the access-control
15
+ * glossary in `CONTEXT.md`.
16
+ */
17
+ /**
18
+ * Build Prisma include object with access control filters
19
+ * This allows us to filter relationships at the database level instead of in memory
20
+ */
21
+ export async function buildIncludeWithAccessControl(fieldConfigs, args, config, depth = 0) {
22
+ const MAX_DEPTH = 5;
23
+ if (depth >= MAX_DEPTH) {
24
+ return undefined;
25
+ }
26
+ // Skip auto-including relationships when inside a resolveOutput hook
27
+ // This prevents infinite loops when hooks make DB queries that include
28
+ // relationships back to the same entity (e.g., User virtual field queries Posts
29
+ // which includes author back to User, triggering the virtual field again)
30
+ if (args.context._resolveOutputCounter.depth > 0) {
31
+ return undefined;
32
+ }
33
+ const include = {};
34
+ let hasRelationships = false;
35
+ for (const [fieldName, fieldConfig] of Object.entries(fieldConfigs)) {
36
+ if (fieldConfig?.type === 'relationship' && 'ref' in fieldConfig && fieldConfig.ref) {
37
+ hasRelationships = true;
38
+ const relatedConfig = getRelatedListConfig(fieldConfig.ref, config);
39
+ if (relatedConfig) {
40
+ // Check query access for the related list
41
+ const queryAccess = relatedConfig.listConfig.access?.operation?.query;
42
+ const accessResult = await checkAccess(queryAccess, {
43
+ session: args.session,
44
+ context: args.context,
45
+ });
46
+ // If access is completely denied, exclude this relationship
47
+ if (accessResult === false) {
48
+ continue;
49
+ }
50
+ // Build the include entry
51
+ const includeEntry = {};
52
+ // If access returns a filter, add it to the where clause
53
+ if (typeof accessResult === 'object') {
54
+ includeEntry.where = accessResult;
55
+ }
56
+ // Recursively build nested includes
57
+ const nestedInclude = await buildIncludeWithAccessControl(relatedConfig.listConfig.fields, args, config, depth + 1);
58
+ if (nestedInclude && Object.keys(nestedInclude).length > 0) {
59
+ includeEntry.include = nestedInclude;
60
+ }
61
+ // Add to include object
62
+ include[fieldName] = Object.keys(includeEntry).length > 0 ? includeEntry : true;
63
+ }
64
+ }
65
+ }
66
+ return hasRelationships ? include : undefined;
67
+ }
68
+ //# sourceMappingURL=access-filter.js.map
@@ -0,0 +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,6 +1,19 @@
1
1
  import type { AccessControl, Session, AccessContext, PrismaFilter } from './types.js';
2
- import type { FieldAccess } from './types.js';
3
- import type { OpenSaasConfig, ListConfig, FieldConfig } from '../config/types.js';
2
+ import type { OpenSaasConfig, ListConfig } from '../config/types.js';
3
+ /**
4
+ * Access engine — operation-level access control and shared helpers.
5
+ *
6
+ * This module holds the *operation-level* (list-level) access primitives and
7
+ * the ref-parsing helper shared across both phases of the two-phase read:
8
+ *
9
+ * - Phase 1, Access Filter (pre-query row/relation scoping): `access-filter.ts`
10
+ * - Phase 2, Field Visibility (post-query field stripping + resolveOutput +
11
+ * virtual fields): `field-visibility.ts`
12
+ *
13
+ * Field-level access evaluation is centralized in `field-access.ts`
14
+ * (`checkFieldAccess`). See `docs/adr/0001-access-control-is-a-two-phase-read.md`
15
+ * and the access-control glossary in `CONTEXT.md`.
16
+ */
4
17
  /**
5
18
  * Check if access control result is a boolean
6
19
  */
@@ -33,50 +46,4 @@ export declare function checkAccess<T = Record<string, unknown>>(accessControl:
33
46
  * Merge user filter with access control filter
34
47
  */
35
48
  export declare function mergeFilters(userFilter: PrismaFilter | undefined, accessFilter: boolean | PrismaFilter): PrismaFilter | null;
36
- /**
37
- * Check field-level access for a specific operation
38
- */
39
- export declare function checkFieldAccess(fieldAccess: FieldAccess | undefined, operation: 'read' | 'create' | 'update', args: {
40
- session: Session | null;
41
- item?: Record<string, unknown>;
42
- context: AccessContext & {
43
- _isSudo?: boolean;
44
- };
45
- inputData?: Record<string, unknown>;
46
- }): Promise<boolean>;
47
- /**
48
- * Build Prisma include object with access control filters
49
- * This allows us to filter relationships at the database level instead of in memory
50
- */
51
- export declare function buildIncludeWithAccessControl(fieldConfigs: Record<string, FieldConfig>, args: {
52
- session: Session | null;
53
- context: AccessContext;
54
- }, config: OpenSaasConfig, depth?: number): Promise<Record<string, boolean | {
55
- where?: PrismaFilter;
56
- include?: Record<string, boolean | /*elided*/ any>;
57
- }> | undefined>;
58
- /**
59
- * Filter fields from an object based on read access
60
- * Recursively applies access control to nested relationships
61
- */
62
- export declare function filterReadableFields<T extends Record<string, unknown>>(item: T, fieldConfigs: Record<string, FieldConfig>, args: {
63
- session: Session | null;
64
- context: AccessContext & {
65
- _isSudo?: boolean;
66
- };
67
- }, config?: OpenSaasConfig, depth?: number, listKey?: string): Promise<Partial<T>>;
68
- /**
69
- * Filter fields from input data based on write access (create/update)
70
- */
71
- export declare function filterWritableFields<T extends Record<string, unknown>>(data: T, fieldConfigs: Record<string, {
72
- access?: FieldAccess;
73
- type?: string;
74
- }>, operation: 'create' | 'update', args: {
75
- session: Session | null;
76
- item?: Record<string, unknown>;
77
- context: AccessContext & {
78
- _isSudo?: boolean;
79
- };
80
- inputData?: Record<string, unknown>;
81
- }): Promise<Partial<T>>;
82
49
  //# sourceMappingURL=engine.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/access/engine.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/access/engine.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACrF,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAA;AAEpE;;;;;;;;;;;;;GAaG;AAEH;;GAEG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,OAAO,CAE1D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CAEpE;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,eAAe,EAAE,MAAM,EACvB,MAAM,EAAE,cAAc,GAErB;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,UAAU,CAAC,GAAG,CAAC,CAAA;CAAE,GAAG,IAAI,CAe1D;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,aAAa,EAAE,aAAa,CAAC,CAAC,CAAC,GAAG,SAAS,EAC3C,IAAI,EAAE;IACJ,OAAO,EAAE,OAAO,GAAG,IAAI,CAAA;IACvB,IAAI,CAAC,EAAE,CAAC,CAAA;IACR,OAAO,EAAE,aAAa,CAAA;CACvB,GACA,OAAO,CAAC,OAAO,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC,CAUpC;AAED;;GAEG;AACH,wBAAgB,YAAY,CAC1B,UAAU,EAAE,YAAY,GAAG,SAAS,EACpC,YAAY,EAAE,OAAO,GAAG,YAAY,GACnC,YAAY,GAAG,IAAI,CAoBrB"}