@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.
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +334 -0
- package/CLAUDE.md +29 -11
- package/dist/access/access-filter.d.ts +29 -0
- package/dist/access/access-filter.d.ts.map +1 -0
- package/dist/access/access-filter.js +68 -0
- package/dist/access/access-filter.js.map +1 -0
- package/dist/access/engine.d.ts +15 -48
- package/dist/access/engine.d.ts.map +1 -1
- package/dist/access/engine.js +14 -280
- package/dist/access/engine.js.map +1 -1
- package/dist/access/field-access.d.ts +44 -0
- package/dist/access/field-access.d.ts.map +1 -0
- package/dist/access/field-access.js +123 -0
- package/dist/access/field-access.js.map +1 -0
- package/dist/access/field-access.test.d.ts +2 -0
- package/dist/access/field-access.test.d.ts.map +1 -0
- package/dist/access/{engine.test.js → field-access.test.js} +2 -2
- package/dist/access/field-access.test.js.map +1 -0
- package/dist/access/field-visibility.d.ts +13 -0
- package/dist/access/field-visibility.d.ts.map +1 -0
- package/dist/access/field-visibility.js +178 -0
- package/dist/access/field-visibility.js.map +1 -0
- package/dist/access/index.d.ts +4 -1
- package/dist/access/index.d.ts.map +1 -1
- package/dist/access/index.js +8 -1
- package/dist/access/index.js.map +1 -1
- package/dist/access/multi-column-read-write.test.d.ts +2 -0
- package/dist/access/multi-column-read-write.test.d.ts.map +1 -0
- package/dist/access/multi-column-read-write.test.js +149 -0
- package/dist/access/multi-column-read-write.test.js.map +1 -0
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/types.d.ts +334 -5
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/hook-pipeline.d.ts +49 -0
- package/dist/context/hook-pipeline.d.ts.map +1 -0
- package/dist/context/hook-pipeline.js +75 -0
- package/dist/context/hook-pipeline.js.map +1 -0
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +30 -462
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.d.ts.map +1 -1
- package/dist/context/nested-operations.js +72 -68
- package/dist/context/nested-operations.js.map +1 -1
- package/dist/context/write-pipeline.d.ts +158 -0
- package/dist/context/write-pipeline.d.ts.map +1 -0
- package/dist/context/write-pipeline.js +306 -0
- package/dist/context/write-pipeline.js.map +1 -0
- package/dist/extend.d.ts +3 -0
- package/dist/extend.d.ts.map +1 -0
- package/dist/extend.js +10 -0
- package/dist/extend.js.map +1 -0
- package/dist/fields/format-prisma-default.d.ts +35 -0
- package/dist/fields/format-prisma-default.d.ts.map +1 -0
- package/dist/fields/format-prisma-default.js +52 -0
- package/dist/fields/format-prisma-default.js.map +1 -0
- package/dist/fields/format-prisma-default.test.d.ts +2 -0
- package/dist/fields/format-prisma-default.test.d.ts.map +1 -0
- package/dist/fields/format-prisma-default.test.js +54 -0
- package/dist/fields/format-prisma-default.test.js.map +1 -0
- package/dist/fields/index.d.ts +1 -0
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +267 -18
- package/dist/fields/index.js.map +1 -1
- package/dist/fields/select.test.js +85 -0
- package/dist/fields/select.test.js.map +1 -1
- package/dist/fields/text-keystone-compat.test.d.ts +2 -0
- package/dist/fields/text-keystone-compat.test.d.ts.map +1 -0
- package/dist/fields/text-keystone-compat.test.js +93 -0
- package/dist/fields/text-keystone-compat.test.js.map +1 -0
- package/dist/hooks/index.d.ts +20 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +246 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +6 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -9
- package/dist/index.js.map +1 -1
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +33 -0
- package/dist/index.test.js.map +1 -0
- package/dist/internal.d.ts +8 -0
- package/dist/internal.d.ts.map +1 -0
- package/dist/internal.js +16 -0
- package/dist/internal.js.map +1 -0
- package/dist/mcp/handler.js +0 -1
- package/dist/mcp/handler.js.map +1 -1
- package/dist/validation/field-config.d.ts +55 -0
- package/dist/validation/field-config.d.ts.map +1 -0
- package/dist/validation/field-config.js +100 -0
- package/dist/validation/field-config.js.map +1 -0
- package/dist/validation/field-config.test.d.ts +2 -0
- package/dist/validation/field-config.test.d.ts.map +1 -0
- package/dist/validation/field-config.test.js +159 -0
- package/dist/validation/field-config.test.js.map +1 -0
- package/package.json +11 -3
- package/src/access/access-filter.ts +97 -0
- package/src/access/engine.ts +13 -396
- package/src/access/{engine.test.ts → field-access.test.ts} +1 -1
- package/src/access/field-access.ts +159 -0
- package/src/access/field-visibility.ts +269 -0
- package/src/access/index.ts +7 -4
- package/src/access/multi-column-read-write.test.ts +255 -0
- package/src/config/index.ts +3 -0
- package/src/config/types.ts +342 -4
- package/src/context/hook-pipeline.ts +160 -0
- package/src/context/index.ts +29 -667
- package/src/context/nested-operations.ts +142 -111
- package/src/context/write-pipeline.ts +543 -0
- package/src/extend.ts +19 -0
- package/src/fields/format-prisma-default.test.ts +64 -0
- package/src/fields/format-prisma-default.ts +67 -0
- package/src/fields/index.ts +375 -20
- package/src/fields/select.test.ts +99 -0
- package/src/fields/text-keystone-compat.test.ts +126 -0
- package/src/hooks/index.ts +270 -0
- package/src/index.test.ts +50 -0
- package/src/index.ts +35 -82
- package/src/internal.ts +49 -0
- package/src/mcp/handler.ts +0 -2
- package/src/validation/field-config.test.ts +199 -0
- package/src/validation/field-config.ts +145 -0
- package/tests/access-relationships.test.ts +4 -4
- package/tests/access.test.ts +1 -1
- package/tests/field-hooks.test.ts +410 -0
- package/tests/field-types.test.ts +1 -1
- package/tests/hook-pipeline.test.ts +233 -0
- package/tests/nested-operation-registry.test.ts +206 -0
- package/tests/write-pipeline.test.ts +588 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vitest.config.ts +43 -1
- package/dist/access/engine.test.d.ts +0 -2
- package/dist/access/engine.test.d.ts.map +0 -1
- package/dist/access/engine.test.js.map +0 -1
package/.turbo/turbo-build.log
CHANGED
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 `
|
|
161
|
-
4. Field
|
|
162
|
-
5. Field
|
|
163
|
-
6. Field
|
|
164
|
-
7.
|
|
165
|
-
8.
|
|
166
|
-
9.
|
|
167
|
-
10.
|
|
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"}
|
package/dist/access/engine.d.ts
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import type { AccessControl, Session, AccessContext, PrismaFilter } from './types.js';
|
|
2
|
-
import type {
|
|
3
|
-
|
|
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":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AACrF,OAAO,KAAK,EAAE,
|
|
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"}
|