@opensaas/stack-core 0.37.0 → 0.39.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 +191 -0
- package/CLAUDE.md +42 -3
- package/dist/access/access-filter.d.ts +70 -113
- package/dist/access/access-filter.d.ts.map +1 -1
- package/dist/access/access-filter.js +183 -210
- package/dist/access/access-filter.js.map +1 -1
- package/dist/access/access-filter.test.js +148 -188
- package/dist/access/access-filter.test.js.map +1 -1
- package/dist/access/declared-dependencies.d.ts +66 -26
- package/dist/access/declared-dependencies.d.ts.map +1 -1
- package/dist/access/declared-dependencies.js +62 -17
- package/dist/access/declared-dependencies.js.map +1 -1
- package/dist/access/declared-dependencies.test.d.ts +2 -0
- package/dist/access/declared-dependencies.test.d.ts.map +1 -0
- package/dist/access/declared-dependencies.test.js +226 -0
- package/dist/access/declared-dependencies.test.js.map +1 -0
- package/dist/access/depth-limits.d.ts +8 -7
- package/dist/access/depth-limits.d.ts.map +1 -1
- package/dist/access/depth-limits.js +8 -7
- package/dist/access/depth-limits.js.map +1 -1
- package/dist/access/engine.d.ts +0 -20
- package/dist/access/engine.d.ts.map +1 -1
- package/dist/access/engine.js +0 -25
- package/dist/access/engine.js.map +1 -1
- package/dist/access/errors.d.ts +61 -8
- package/dist/access/errors.d.ts.map +1 -1
- package/dist/access/errors.js +91 -12
- package/dist/access/errors.js.map +1 -1
- package/dist/access/field-access.d.ts +39 -1
- package/dist/access/field-access.d.ts.map +1 -1
- package/dist/access/field-access.js +108 -42
- package/dist/access/field-access.js.map +1 -1
- package/dist/access/field-access.test.js +229 -2
- package/dist/access/field-access.test.js.map +1 -1
- package/dist/access/field-transforms.d.ts +0 -27
- package/dist/access/field-transforms.d.ts.map +1 -1
- package/dist/access/field-visibility.d.ts +2 -5
- package/dist/access/field-visibility.d.ts.map +1 -1
- package/dist/access/field-visibility.js +89 -41
- package/dist/access/field-visibility.js.map +1 -1
- package/dist/access/index.d.ts +6 -3
- package/dist/access/index.d.ts.map +1 -1
- package/dist/access/index.js +12 -2
- package/dist/access/index.js.map +1 -1
- package/dist/access/query-validation.d.ts +121 -0
- package/dist/access/query-validation.d.ts.map +1 -0
- package/dist/access/query-validation.js +292 -0
- package/dist/access/query-validation.js.map +1 -0
- package/dist/access/relationship-count.d.ts +1 -1
- package/dist/access/relationship-count.d.ts.map +1 -1
- package/dist/access/relationship-count.js +2 -10
- package/dist/access/relationship-count.js.map +1 -1
- package/dist/access/relationship-label-filter.d.ts +23 -23
- package/dist/access/relationship-label-filter.d.ts.map +1 -1
- package/dist/access/relationship-label-filter.js +5 -76
- package/dist/access/relationship-label-filter.js.map +1 -1
- package/dist/access/relationship-label-filter.test.js +19 -56
- package/dist/access/relationship-label-filter.test.js.map +1 -1
- package/dist/access/transaction-registry.d.ts +45 -0
- package/dist/access/transaction-registry.d.ts.map +1 -0
- package/dist/access/transaction-registry.js +37 -0
- package/dist/access/transaction-registry.js.map +1 -0
- package/dist/access/types.d.ts +55 -59
- package/dist/access/types.d.ts.map +1 -1
- package/dist/config/index.d.ts +9 -7
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +10 -18
- package/dist/config/index.js.map +1 -1
- package/dist/config/label.d.ts +6 -13
- package/dist/config/label.d.ts.map +1 -1
- package/dist/config/label.js +6 -13
- package/dist/config/label.js.map +1 -1
- package/dist/config/nav-count.d.ts +11 -19
- package/dist/config/nav-count.d.ts.map +1 -1
- package/dist/config/nav-count.js +11 -19
- package/dist/config/nav-count.js.map +1 -1
- package/dist/config/plugin-engine.d.ts +0 -10
- package/dist/config/plugin-engine.d.ts.map +1 -1
- package/dist/config/plugin-engine.js +1 -37
- package/dist/config/plugin-engine.js.map +1 -1
- package/dist/config/types.d.ts +94 -109
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/apply-defaults.d.ts +8 -31
- package/dist/context/apply-defaults.d.ts.map +1 -1
- package/dist/context/apply-defaults.js +12 -41
- package/dist/context/apply-defaults.js.map +1 -1
- package/dist/context/hook-pipeline.d.ts +4 -29
- package/dist/context/hook-pipeline.d.ts.map +1 -1
- package/dist/context/hook-pipeline.js +8 -41
- package/dist/context/hook-pipeline.js.map +1 -1
- package/dist/context/index.d.ts +4 -15
- package/dist/context/index.d.ts.map +1 -1
- package/dist/context/index.js +223 -215
- package/dist/context/index.js.map +1 -1
- package/dist/context/nested-operations.d.ts +7 -26
- package/dist/context/nested-operations.d.ts.map +1 -1
- package/dist/context/nested-operations.js +73 -171
- package/dist/context/nested-operations.js.map +1 -1
- package/dist/context/transaction-boundary.d.ts +36 -47
- package/dist/context/transaction-boundary.d.ts.map +1 -1
- package/dist/context/transaction-boundary.js +92 -79
- package/dist/context/transaction-boundary.js.map +1 -1
- package/dist/context/write-pipeline.d.ts +25 -64
- package/dist/context/write-pipeline.d.ts.map +1 -1
- package/dist/context/write-pipeline.js +81 -111
- package/dist/context/write-pipeline.js.map +1 -1
- package/dist/fields/format-prisma-default.d.ts.map +1 -1
- package/dist/fields/format-prisma-default.js +4 -7
- package/dist/fields/format-prisma-default.js.map +1 -1
- package/dist/fields/index.d.ts +17 -2
- package/dist/fields/index.d.ts.map +1 -1
- package/dist/fields/index.js +136 -147
- package/dist/fields/index.js.map +1 -1
- package/dist/filter/collect.d.ts +26 -3
- package/dist/filter/collect.d.ts.map +1 -1
- package/dist/filter/collect.js +30 -12
- package/dist/filter/collect.js.map +1 -1
- package/dist/filter/filter.test.js +98 -15
- package/dist/filter/filter.test.js.map +1 -1
- package/dist/filter/index.d.ts +1 -0
- package/dist/filter/index.d.ts.map +1 -1
- package/dist/filter/index.js.map +1 -1
- package/dist/filter/map.d.ts.map +1 -1
- package/dist/filter/map.js +0 -5
- package/dist/filter/map.js.map +1 -1
- package/dist/filter/parse.d.ts.map +1 -1
- package/dist/filter/parse.js +2 -9
- package/dist/filter/parse.js.map +1 -1
- package/dist/filter/serialize.d.ts.map +1 -1
- package/dist/filter/serialize.js +2 -4
- package/dist/filter/serialize.js.map +1 -1
- package/dist/hooks/index.d.ts +4 -42
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +5 -59
- package/dist/hooks/index.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -4
- package/dist/index.js.map +1 -1
- package/dist/internal.d.ts +1 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +4 -0
- package/dist/internal.js.map +1 -1
- package/dist/lib/case-utils.d.ts +0 -32
- package/dist/lib/case-utils.d.ts.map +1 -1
- package/dist/lib/case-utils.js +0 -32
- package/dist/lib/case-utils.js.map +1 -1
- package/dist/mcp/handler.d.ts +0 -4
- package/dist/mcp/handler.d.ts.map +1 -1
- package/dist/mcp/handler.js +12 -69
- package/dist/mcp/handler.js.map +1 -1
- package/dist/query/index.d.ts +55 -227
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/index.js +41 -117
- package/dist/query/index.js.map +1 -1
- package/dist/query/relationship-options.d.ts +1 -1
- package/dist/query/relationship-options.js +1 -1
- package/dist/utils/password.d.ts +0 -68
- package/dist/utils/password.d.ts.map +1 -1
- package/dist/utils/password.js +7 -76
- package/dist/utils/password.js.map +1 -1
- package/dist/validation/field-config.d.ts.map +1 -1
- package/dist/validation/field-config.js +0 -6
- package/dist/validation/field-config.js.map +1 -1
- package/dist/validation/schema.d.ts +0 -7
- package/dist/validation/schema.d.ts.map +1 -1
- package/dist/validation/schema.js +0 -10
- package/dist/validation/schema.js.map +1 -1
- package/package.json +1 -1
- package/src/access/access-filter.test.ts +205 -275
- package/src/access/access-filter.ts +236 -272
- package/src/access/declared-dependencies.test.ts +277 -0
- package/src/access/declared-dependencies.ts +117 -37
- package/src/access/depth-limits.ts +8 -7
- package/src/access/engine.ts +0 -25
- package/src/access/errors.ts +97 -12
- package/src/access/field-access.test.ts +312 -2
- package/src/access/field-access.ts +118 -42
- package/src/access/field-transforms.ts +8 -39
- package/src/access/field-visibility.ts +99 -45
- package/src/access/index.ts +17 -5
- package/src/access/query-validation.ts +359 -0
- package/src/access/relationship-count.ts +3 -11
- package/src/access/relationship-label-filter.test.ts +19 -68
- package/src/access/relationship-label-filter.ts +27 -104
- package/src/access/transaction-registry.ts +58 -0
- package/src/access/types.ts +83 -86
- package/src/config/index.ts +10 -22
- package/src/config/label.ts +6 -13
- package/src/config/nav-count.ts +11 -19
- package/src/config/plugin-engine.ts +1 -37
- package/src/config/types.ts +95 -110
- package/src/context/apply-defaults.ts +12 -44
- package/src/context/hook-pipeline.ts +12 -67
- package/src/context/index.ts +311 -276
- package/src/context/nested-operations.ts +94 -226
- package/src/context/transaction-boundary.ts +113 -109
- package/src/context/write-pipeline.ts +99 -155
- package/src/fields/format-prisma-default.ts +4 -7
- package/src/fields/index.ts +155 -147
- package/src/filter/collect.ts +48 -12
- package/src/filter/filter.test.ts +153 -15
- package/src/filter/index.ts +1 -0
- package/src/filter/map.ts +0 -5
- package/src/filter/parse.ts +2 -9
- package/src/filter/serialize.ts +2 -4
- package/src/hooks/index.ts +5 -59
- package/src/index.ts +23 -4
- package/src/internal.ts +5 -0
- package/src/lib/case-utils.ts +0 -32
- package/src/mcp/handler.ts +13 -71
- package/src/query/index.ts +82 -235
- package/src/query/relationship-options.ts +1 -1
- package/src/utils/password.ts +7 -76
- package/src/validation/field-config.ts +0 -6
- package/src/validation/schema.ts +0 -10
- package/tests/access-relationships.test.ts +18 -16
- package/tests/computed-field-selective-evaluation.test.ts +418 -0
- package/tests/context.test.ts +727 -1
- package/tests/field-types.test.ts +222 -0
- package/tests/mcp-handler.test.ts +34 -0
- package/tests/needs-declared-dependencies.test.ts +7 -4
- package/tests/resolve-chain.test.ts +11 -11
- package/tests/transaction-boundary-outermost.test.ts +530 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/config/nav-count.ts
CHANGED
|
@@ -13,13 +13,12 @@ type CountDelegate = {
|
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Whether a list's query access is **statically denied** — provably no session
|
|
16
|
-
* can read any row without evaluating a session-dependent function.
|
|
16
|
+
* can read any row, without evaluating a session-dependent function.
|
|
17
17
|
*
|
|
18
|
-
* Mirrors the list view's `canDeleteList` static check (issue #733):
|
|
19
|
-
* access is
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* context, which re-applies the real rule per row.
|
|
18
|
+
* Mirrors the list view's `canDeleteList` static check (issue #733): absent
|
|
19
|
+
* access is deny-by-default (matching the access engine), so only an explicit
|
|
20
|
+
* `false` counts as statically denied here — a function or `true` falls
|
|
21
|
+
* through to a real per-row check via the secured context.
|
|
23
22
|
*
|
|
24
23
|
* A statically-denied list must render no nav count at all: a `0` there would
|
|
25
24
|
* imply "this list is empty" when the truth is "you may see none of it".
|
|
@@ -35,20 +34,13 @@ export function isListQueryStaticallyDenied(
|
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
/**
|
|
38
|
-
* Resolve the access-scoped nav counts for
|
|
39
|
-
* `ui.navCount` (issue #735).
|
|
37
|
+
* Resolve the access-scoped nav counts for lists that opt in via
|
|
38
|
+
* `ui.navCount` (issue #735). A list gets an entry only if it opts in, isn't
|
|
39
|
+
* a singleton (no meaningful count), and isn't {@link isListQueryStaticallyDenied}
|
|
40
|
+
* — everything else is omitted from the map, never zeroed.
|
|
40
41
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* 1. opts in (`ui.navCount === true`) — no count query runs for any other list,
|
|
45
|
-
* 2. is not a singleton (a single-record list has no meaningful count), and
|
|
46
|
-
* 3. does not have statically-denied query access (see
|
|
47
|
-
* {@link isListQueryStaticallyDenied}).
|
|
48
|
-
*
|
|
49
|
-
* Each count is read through the secured `context.db`, whose `count` applies the
|
|
50
|
-
* access filter, so the number reflects exactly what the current session may
|
|
51
|
-
* see (a denied row is not counted). Counts are fetched concurrently.
|
|
42
|
+
* Each count is read through the secured `context.db`, so it reflects
|
|
43
|
+
* exactly what the current session may see.
|
|
52
44
|
*/
|
|
53
45
|
export async function resolveNavCounts(
|
|
54
46
|
context: AccessContext,
|
|
@@ -8,17 +8,12 @@ import type {
|
|
|
8
8
|
BaseFieldConfig,
|
|
9
9
|
} from './types.js'
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
* Topological sort for plugin dependency resolution
|
|
13
|
-
* Ensures plugins execute in correct order based on dependencies
|
|
14
|
-
*/
|
|
15
11
|
function sortPluginsByDependencies(plugins: Plugin[]): Plugin[] {
|
|
16
12
|
const pluginMap = new Map<string, Plugin>()
|
|
17
13
|
const visited = new Set<string>()
|
|
18
14
|
const visiting = new Set<string>()
|
|
19
15
|
const sorted: Plugin[] = []
|
|
20
16
|
|
|
21
|
-
// Build plugin map
|
|
22
17
|
for (const plugin of plugins) {
|
|
23
18
|
if (pluginMap.has(plugin.name)) {
|
|
24
19
|
throw new Error(`Duplicate plugin name: ${plugin.name}`)
|
|
@@ -42,7 +37,6 @@ function sortPluginsByDependencies(plugins: Plugin[]): Plugin[] {
|
|
|
42
37
|
|
|
43
38
|
visiting.add(pluginName)
|
|
44
39
|
|
|
45
|
-
// Visit dependencies first
|
|
46
40
|
if (plugin.dependencies) {
|
|
47
41
|
for (const dep of plugin.dependencies) {
|
|
48
42
|
visit(dep, [...path, pluginName])
|
|
@@ -54,7 +48,6 @@ function sortPluginsByDependencies(plugins: Plugin[]): Plugin[] {
|
|
|
54
48
|
sorted.push(plugin)
|
|
55
49
|
}
|
|
56
50
|
|
|
57
|
-
// Visit all plugins
|
|
58
51
|
for (const plugin of plugins) {
|
|
59
52
|
visit(plugin.name)
|
|
60
53
|
}
|
|
@@ -62,17 +55,12 @@ function sortPluginsByDependencies(plugins: Plugin[]): Plugin[] {
|
|
|
62
55
|
return sorted
|
|
63
56
|
}
|
|
64
57
|
|
|
65
|
-
/**
|
|
66
|
-
* Merge hooks from extension into existing hooks
|
|
67
|
-
* Combines hook arrays so multiple plugins can add hooks
|
|
68
|
-
*/
|
|
69
58
|
function mergeHooks(existing: Hooks | undefined, extension: Hooks | undefined): Hooks | undefined {
|
|
70
59
|
if (!extension) return existing
|
|
71
60
|
if (!existing) return extension
|
|
72
61
|
|
|
73
62
|
const merged: Partial<Hooks> = {}
|
|
74
63
|
|
|
75
|
-
// Merge resolveInput hooks (chain them, passing resolvedData through)
|
|
76
64
|
if (existing.resolveInput || extension.resolveInput) {
|
|
77
65
|
if (existing.resolveInput && extension.resolveInput) {
|
|
78
66
|
merged.resolveInput = async (args) => {
|
|
@@ -85,7 +73,6 @@ function mergeHooks(existing: Hooks | undefined, extension: Hooks | undefined):
|
|
|
85
73
|
}
|
|
86
74
|
}
|
|
87
75
|
|
|
88
|
-
// Merge validateInput hooks (run both)
|
|
89
76
|
if (existing.validateInput || extension.validateInput) {
|
|
90
77
|
if (existing.validateInput && extension.validateInput) {
|
|
91
78
|
merged.validateInput = async (args) => {
|
|
@@ -97,7 +84,6 @@ function mergeHooks(existing: Hooks | undefined, extension: Hooks | undefined):
|
|
|
97
84
|
}
|
|
98
85
|
}
|
|
99
86
|
|
|
100
|
-
// Merge beforeOperation hooks (run both in sequence)
|
|
101
87
|
if (existing.beforeOperation || extension.beforeOperation) {
|
|
102
88
|
if (existing.beforeOperation && extension.beforeOperation) {
|
|
103
89
|
merged.beforeOperation = async (args) => {
|
|
@@ -109,7 +95,6 @@ function mergeHooks(existing: Hooks | undefined, extension: Hooks | undefined):
|
|
|
109
95
|
}
|
|
110
96
|
}
|
|
111
97
|
|
|
112
|
-
// Merge afterOperation hooks (run both in sequence)
|
|
113
98
|
if (existing.afterOperation || extension.afterOperation) {
|
|
114
99
|
if (existing.afterOperation && extension.afterOperation) {
|
|
115
100
|
merged.afterOperation = async (args) => {
|
|
@@ -124,33 +109,24 @@ function mergeHooks(existing: Hooks | undefined, extension: Hooks | undefined):
|
|
|
124
109
|
return Object.keys(merged).length > 0 ? (merged as Hooks) : undefined
|
|
125
110
|
}
|
|
126
111
|
|
|
127
|
-
/**
|
|
128
|
-
* Execute plugins and transform config
|
|
129
|
-
* Returns modified config with plugin data attached
|
|
130
|
-
*/
|
|
131
112
|
export async function executePlugins(config: OpenSaasConfig): Promise<OpenSaasConfig> {
|
|
132
113
|
if (!config.plugins || config.plugins.length === 0) {
|
|
133
114
|
return config
|
|
134
115
|
}
|
|
135
116
|
|
|
136
|
-
// Sort plugins by dependencies
|
|
137
117
|
const sortedPlugins = sortPluginsByDependencies(config.plugins)
|
|
138
118
|
|
|
139
|
-
// Initialize mutable config state - preserve all config properties
|
|
140
119
|
let currentConfig: OpenSaasConfig = {
|
|
141
120
|
...config,
|
|
142
121
|
lists: { ...config.lists }, // Clone lists object to avoid mutating original
|
|
143
122
|
_pluginData: {},
|
|
144
123
|
}
|
|
145
124
|
|
|
146
|
-
// Field type registry (for third-party fields)
|
|
147
125
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Registry must accept any field config builder
|
|
148
126
|
const fieldTypeRegistry = new Map<string, (options?: unknown) => BaseFieldConfig<any>>()
|
|
149
127
|
|
|
150
|
-
// MCP tools registry
|
|
151
128
|
const mcpToolsRegistry: McpCustomTool[] = []
|
|
152
129
|
|
|
153
|
-
// Execute each plugin
|
|
154
130
|
for (const plugin of sortedPlugins) {
|
|
155
131
|
const context: PluginContext = {
|
|
156
132
|
config: currentConfig,
|
|
@@ -186,16 +162,13 @@ export async function executePlugins(config: OpenSaasConfig): Promise<OpenSaasCo
|
|
|
186
162
|
)
|
|
187
163
|
}
|
|
188
164
|
|
|
189
|
-
// Deep merge fields
|
|
190
165
|
const mergedFields = {
|
|
191
166
|
...existing.fields,
|
|
192
167
|
...extension.fields,
|
|
193
168
|
}
|
|
194
169
|
|
|
195
|
-
// Merge hooks
|
|
196
170
|
const mergedHooks = mergeHooks(existing.hooks, extension.hooks)
|
|
197
171
|
|
|
198
|
-
// Merge MCP config
|
|
199
172
|
const mergedMcp = extension.mcp
|
|
200
173
|
? {
|
|
201
174
|
...existing.mcp,
|
|
@@ -233,11 +206,9 @@ export async function executePlugins(config: OpenSaasConfig): Promise<OpenSaasCo
|
|
|
233
206
|
},
|
|
234
207
|
}
|
|
235
208
|
|
|
236
|
-
// Execute plugin init
|
|
237
209
|
await plugin.init(context)
|
|
238
210
|
}
|
|
239
211
|
|
|
240
|
-
// Store registered MCP tools in config (if any)
|
|
241
212
|
if (mcpToolsRegistry.length > 0) {
|
|
242
213
|
if (!currentConfig._pluginData) {
|
|
243
214
|
currentConfig._pluginData = {}
|
|
@@ -245,8 +216,7 @@ export async function executePlugins(config: OpenSaasConfig): Promise<OpenSaasCo
|
|
|
245
216
|
currentConfig._pluginData.__mcpTools = mcpToolsRegistry
|
|
246
217
|
}
|
|
247
218
|
|
|
248
|
-
//
|
|
249
|
-
// This allows context creation to call plugin.runtime() functions
|
|
219
|
+
// Stored so context creation can call each plugin's runtime() function.
|
|
250
220
|
if (!currentConfig._plugins) {
|
|
251
221
|
currentConfig._plugins = []
|
|
252
222
|
}
|
|
@@ -255,9 +225,6 @@ export async function executePlugins(config: OpenSaasConfig): Promise<OpenSaasCo
|
|
|
255
225
|
return currentConfig
|
|
256
226
|
}
|
|
257
227
|
|
|
258
|
-
/**
|
|
259
|
-
* Execute beforeGenerate hooks from plugins
|
|
260
|
-
*/
|
|
261
228
|
export async function executeBeforeGenerateHooks(config: OpenSaasConfig): Promise<OpenSaasConfig> {
|
|
262
229
|
if (!config.plugins || config.plugins.length === 0) {
|
|
263
230
|
return config
|
|
@@ -274,9 +241,6 @@ export async function executeBeforeGenerateHooks(config: OpenSaasConfig): Promis
|
|
|
274
241
|
return currentConfig
|
|
275
242
|
}
|
|
276
243
|
|
|
277
|
-
/**
|
|
278
|
-
* Execute afterGenerate hooks from plugins
|
|
279
|
-
*/
|
|
280
244
|
export async function executeAfterGenerateHooks(
|
|
281
245
|
config: OpenSaasConfig,
|
|
282
246
|
files: { prismaSchema: string; types: string; context: string; [key: string]: string },
|
package/src/config/types.ts
CHANGED
|
@@ -2,9 +2,6 @@ import type { AccessControl, FieldAccess } from '../access/types.js'
|
|
|
2
2
|
import type { FilterSpec } from '../filter/types.js'
|
|
3
3
|
import type { z } from 'zod'
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Field configuration types
|
|
7
|
-
*/
|
|
8
5
|
export type FieldType =
|
|
9
6
|
'text' | 'integer' | 'checkbox' | 'timestamp' | 'password' | 'select' | 'relationship' | string // Allow custom field types from third-party packages
|
|
10
7
|
|
|
@@ -12,10 +9,7 @@ export type FieldType =
|
|
|
12
9
|
* Field-level hook argument types (exported for user annotations)
|
|
13
10
|
*/
|
|
14
11
|
|
|
15
|
-
/**
|
|
16
|
-
* Arguments for field-level resolveInput hook
|
|
17
|
-
* Used to transform field values before database write
|
|
18
|
-
*/
|
|
12
|
+
/** Arguments for {@link FieldHooks.resolveInput}. */
|
|
19
13
|
export type FieldResolveInputHookArgs<
|
|
20
14
|
TTypeInfo extends TypeInfo,
|
|
21
15
|
TFieldKey extends FieldKeys<TTypeInfo['fields']> = FieldKeys<TTypeInfo['fields']>,
|
|
@@ -39,10 +33,7 @@ export type FieldResolveInputHookArgs<
|
|
|
39
33
|
context: import('../access/types.js').AccessContext
|
|
40
34
|
}
|
|
41
35
|
|
|
42
|
-
/**
|
|
43
|
-
* Arguments for field-level validate hook
|
|
44
|
-
* Used for custom validation logic
|
|
45
|
-
*/
|
|
36
|
+
/** Arguments for {@link FieldHooks.validate} (and its deprecated `validateInput` alias). */
|
|
46
37
|
export type FieldValidateHookArgs<
|
|
47
38
|
TTypeInfo extends TypeInfo,
|
|
48
39
|
TFieldKey extends FieldKeys<TTypeInfo['fields']> = FieldKeys<TTypeInfo['fields']>,
|
|
@@ -76,10 +67,7 @@ export type FieldValidateHookArgs<
|
|
|
76
67
|
addValidationError: (msg: string) => void
|
|
77
68
|
}
|
|
78
69
|
|
|
79
|
-
/**
|
|
80
|
-
* Arguments for field-level beforeOperation hook
|
|
81
|
-
* Used for side effects before database write
|
|
82
|
-
*/
|
|
70
|
+
/** Arguments for {@link FieldHooks.beforeOperation}. */
|
|
83
71
|
export type FieldBeforeOperationHookArgs<
|
|
84
72
|
TTypeInfo extends TypeInfo,
|
|
85
73
|
TFieldKey extends FieldKeys<TTypeInfo['fields']> = FieldKeys<TTypeInfo['fields']>,
|
|
@@ -109,10 +97,7 @@ export type FieldBeforeOperationHookArgs<
|
|
|
109
97
|
context: import('../access/types.js').AccessContext
|
|
110
98
|
}
|
|
111
99
|
|
|
112
|
-
/**
|
|
113
|
-
* Arguments for field-level afterOperation hook
|
|
114
|
-
* Used for side effects after database operation
|
|
115
|
-
*/
|
|
100
|
+
/** Arguments for {@link FieldHooks.afterOperation}. */
|
|
116
101
|
export type FieldAfterOperationHookArgs<
|
|
117
102
|
TTypeInfo extends TypeInfo,
|
|
118
103
|
TFieldKey extends FieldKeys<TTypeInfo['fields']> = FieldKeys<TTypeInfo['fields']>,
|
|
@@ -263,10 +248,7 @@ export type FieldAfterTransactionHookArgs<
|
|
|
263
248
|
context: import('../access/types.js').AccessContext
|
|
264
249
|
}
|
|
265
250
|
|
|
266
|
-
/**
|
|
267
|
-
* Arguments for field-level resolveOutput hook
|
|
268
|
-
* Used to transform field values after database read
|
|
269
|
-
*/
|
|
251
|
+
/** Arguments for {@link FieldHooks.resolveOutput}. */
|
|
270
252
|
export type FieldResolveOutputHookArgs<
|
|
271
253
|
TTypeInfo extends TypeInfo,
|
|
272
254
|
TFieldKey extends FieldKeys<TTypeInfo['fields']> = FieldKeys<TTypeInfo['fields']>,
|
|
@@ -507,10 +489,10 @@ export type BaseFieldConfig<TTypeInfo extends TypeInfo> = {
|
|
|
507
489
|
defaultValue?: unknown
|
|
508
490
|
hooks?: FieldHooks<TTypeInfo>
|
|
509
491
|
/**
|
|
510
|
-
* Marks this field as virtual
|
|
511
|
-
*
|
|
512
|
-
*
|
|
513
|
-
*
|
|
492
|
+
* Marks this field as virtual — not stored in database, computed via
|
|
493
|
+
* `resolveInput`/`resolveOutput` hooks, and excluded from the Prisma
|
|
494
|
+
* schema and input types. Computed whenever the read is going to return
|
|
495
|
+
* it (ADR-0027) — not gated behind an explicit `include`/selection.
|
|
514
496
|
*/
|
|
515
497
|
virtual?: boolean
|
|
516
498
|
/**
|
|
@@ -877,6 +859,25 @@ export type DecimalField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConfi
|
|
|
877
859
|
isIndexed?: boolean | 'unique'
|
|
878
860
|
}
|
|
879
861
|
|
|
862
|
+
/**
|
|
863
|
+
* 64-bit integer field (Prisma `BigInt`, TypeScript `bigint`) — for values that
|
|
864
|
+
* overflow `integer()`'s 32-bit `Int` (e.g. a millisecond epoch).
|
|
865
|
+
*
|
|
866
|
+
* Wire representation (ADR-0029): `bigint` in application code, a decimal
|
|
867
|
+
* string over MCP — `bigint` is not JSON-serialisable, so the MCP handler
|
|
868
|
+
* renders it as a string rather than throwing.
|
|
869
|
+
*/
|
|
870
|
+
export type BigIntField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConfig<TTypeInfo> & {
|
|
871
|
+
type: 'bigInt'
|
|
872
|
+
defaultValue?: bigint | number | string
|
|
873
|
+
validation?: {
|
|
874
|
+
isRequired?: boolean
|
|
875
|
+
min?: bigint
|
|
876
|
+
max?: bigint
|
|
877
|
+
}
|
|
878
|
+
isIndexed?: boolean | 'unique'
|
|
879
|
+
}
|
|
880
|
+
|
|
880
881
|
export type CheckboxField<TTypeInfo extends TypeInfo = TypeInfo> = BaseFieldConfig<TTypeInfo> & {
|
|
881
882
|
type: 'checkbox'
|
|
882
883
|
}
|
|
@@ -1358,20 +1359,8 @@ export type VirtualField<TTypeInfo extends TypeInfo> = BaseFieldConfig<TTypeInfo
|
|
|
1358
1359
|
*/
|
|
1359
1360
|
export type FieldConfig = BaseFieldConfig<TypeInfo>
|
|
1360
1361
|
|
|
1361
|
-
/**
|
|
1362
|
-
* List configuration types
|
|
1363
|
-
*/
|
|
1364
|
-
|
|
1365
|
-
/**
|
|
1366
|
-
* Utility type to inject TypeInfo into a single field config
|
|
1367
|
-
* Extracts TInput and TOutput from BaseFieldConfig and reconstructs with new TypeInfo
|
|
1368
|
-
*/
|
|
1369
1362
|
type WithTypeInfo<TTypeInfo extends TypeInfo> = BaseFieldConfig<TTypeInfo>
|
|
1370
1363
|
|
|
1371
|
-
/**
|
|
1372
|
-
* Utility type to transform all fields in a record to inject TypeInfo
|
|
1373
|
-
* Maps over each field and applies WithTypeInfo transformation
|
|
1374
|
-
*/
|
|
1375
1364
|
export type FieldsWithTypeInfo<TTypeInfo extends TypeInfo> = {
|
|
1376
1365
|
[key: string]: WithTypeInfo<TTypeInfo>
|
|
1377
1366
|
}
|
|
@@ -1547,8 +1536,7 @@ export type ListAccessControl<T = any> =
|
|
|
1547
1536
|
}
|
|
1548
1537
|
|
|
1549
1538
|
/**
|
|
1550
|
-
* Hook arguments for resolveInput hook
|
|
1551
|
-
* Uses discriminated union to provide proper types based on operation
|
|
1539
|
+
* Hook arguments for the list-level `resolveInput` hook.
|
|
1552
1540
|
* - create: resolvedData is CreateInput, item is undefined
|
|
1553
1541
|
* - update: resolvedData is UpdateInput, item is the existing record
|
|
1554
1542
|
*/
|
|
@@ -1575,8 +1563,7 @@ export type ResolveInputHookArgs<
|
|
|
1575
1563
|
}
|
|
1576
1564
|
|
|
1577
1565
|
/**
|
|
1578
|
-
* Hook arguments for validate hook (renamed from validateInput for Keystone compatibility)
|
|
1579
|
-
* Uses discriminated union to provide proper types based on operation
|
|
1566
|
+
* Hook arguments for the list-level `validate` hook (renamed from `validateInput` for Keystone compatibility).
|
|
1580
1567
|
* - create: resolvedData is CreateInput, item is undefined
|
|
1581
1568
|
* - update: resolvedData is UpdateInput, item is the existing record
|
|
1582
1569
|
* - delete: item is the item being deleted
|
|
@@ -1613,8 +1600,7 @@ export type ValidateHookArgs<
|
|
|
1613
1600
|
}
|
|
1614
1601
|
|
|
1615
1602
|
/**
|
|
1616
|
-
* Hook arguments for beforeOperation hook
|
|
1617
|
-
* Uses discriminated union to provide proper types based on operation
|
|
1603
|
+
* Hook arguments for the list-level `beforeOperation` hook.
|
|
1618
1604
|
* - create: has inputData and resolvedData, no item
|
|
1619
1605
|
* - update: has inputData, resolvedData, and item
|
|
1620
1606
|
* - delete: has item only
|
|
@@ -1647,8 +1633,7 @@ export type BeforeOperationHookArgs<
|
|
|
1647
1633
|
}
|
|
1648
1634
|
|
|
1649
1635
|
/**
|
|
1650
|
-
* Hook arguments for afterOperation hook
|
|
1651
|
-
* Uses discriminated union to provide proper types based on operation
|
|
1636
|
+
* Hook arguments for the list-level `afterOperation` hook.
|
|
1652
1637
|
* - create: has item, inputData, and resolvedData, no originalItem
|
|
1653
1638
|
* - update: has item, originalItem, inputData, and resolvedData
|
|
1654
1639
|
* - delete: has originalItem only
|
|
@@ -1848,14 +1833,20 @@ export type ListIndexFieldRef =
|
|
|
1848
1833
|
}
|
|
1849
1834
|
|
|
1850
1835
|
/**
|
|
1851
|
-
* A model-level
|
|
1852
|
-
*
|
|
1853
|
-
*
|
|
1836
|
+
* A model-level `@@unique`/`@@index` constraint spanning one or more of a
|
|
1837
|
+
* list's own fields. See {@link ListConfig.db}'s `indexes` for the full
|
|
1838
|
+
* explanation and examples (#864, #918).
|
|
1854
1839
|
*/
|
|
1855
1840
|
export type ListIndex = {
|
|
1856
1841
|
/**
|
|
1857
1842
|
* The fields participating in this index/constraint, in declaration order.
|
|
1858
1843
|
* OpenSaaS field names, not raw database column names.
|
|
1844
|
+
*
|
|
1845
|
+
* One or more fields — a named single-column constraint is as legitimate
|
|
1846
|
+
* as a composite one, and is the form to reach for when a live table's
|
|
1847
|
+
* constraint carries a name Prisma wouldn't derive on its own. Field-level
|
|
1848
|
+
* `isIndexed` remains the sugar for the unnamed single-column case; this
|
|
1849
|
+
* is the full form when a name or a sort direction is needed.
|
|
1859
1850
|
*/
|
|
1860
1851
|
fields: ListIndexFieldRef[]
|
|
1861
1852
|
/**
|
|
@@ -1949,31 +1940,40 @@ export type ListConfig<TTypeInfo extends TypeInfo> = {
|
|
|
1949
1940
|
*/
|
|
1950
1941
|
timestamps?: boolean
|
|
1951
1942
|
/**
|
|
1952
|
-
* Model-level
|
|
1953
|
-
*
|
|
1943
|
+
* Model-level `@@unique`/`@@index` constraints spanning one or more of
|
|
1944
|
+
* this list's own fields (#864, #918). See the [reference
|
|
1945
|
+
* docs](https://stack.opensaas.au/docs/reference/config-api#dbindexes)
|
|
1946
|
+
* for the full explanation, arity, and error conditions.
|
|
1954
1947
|
*
|
|
1955
|
-
* Field-level `isIndexed` (on a scalar or relationship field)
|
|
1956
|
-
*
|
|
1957
|
-
*
|
|
1958
|
-
*
|
|
1959
|
-
*
|
|
1960
|
-
*
|
|
1961
|
-
*
|
|
1962
|
-
*
|
|
1963
|
-
*
|
|
1948
|
+
* Field-level `isIndexed` (on a scalar or relationship field) is the
|
|
1949
|
+
* sugar for the unnamed single-column case. `db.indexes` is the full
|
|
1950
|
+
* form — reach for it when a constraint needs a `name` (Prisma's `map:`,
|
|
1951
|
+
* for adopting an existing live constraint), a `sort` direction, or spans
|
|
1952
|
+
* more than one column. Arity is incidental: each entry names one or
|
|
1953
|
+
* more of the list's own OpenSaaS field names, not raw database column
|
|
1954
|
+
* names. The generator resolves each to its underlying Prisma column — a
|
|
1955
|
+
* scalar field's own name (its Prisma field name is unaffected by
|
|
1956
|
+
* `db.map`), or a relationship field's foreign key column (`<field>Id`)
|
|
1957
|
+
* when this side owns it.
|
|
1964
1958
|
*
|
|
1965
|
-
* A
|
|
1966
|
-
*
|
|
1967
|
-
*
|
|
1968
|
-
*
|
|
1969
|
-
*
|
|
1970
|
-
*
|
|
1971
|
-
*
|
|
1959
|
+
* A **unique** entry is the load-bearing case: it's the database-level
|
|
1960
|
+
* backstop for a business rule concurrent writes could otherwise both
|
|
1961
|
+
* slip past (e.g. "one booking per student per production", or a
|
|
1962
|
+
* single-column unique a live table already enforces under a name Prisma
|
|
1963
|
+
* wouldn't derive) — something a hook's existence check cannot close on
|
|
1964
|
+
* its own, and can't be retrofitted once duplicate rows exist. An
|
|
1965
|
+
* **index** (non-unique) entry is the equivalent performance-only case
|
|
1966
|
+
* (a hot lookup path).
|
|
1972
1967
|
*
|
|
1973
|
-
*
|
|
1974
|
-
*
|
|
1975
|
-
*
|
|
1976
|
-
*
|
|
1968
|
+
* Two conditions fail `pnpm generate` with an error naming the list and
|
|
1969
|
+
* the entry: an empty `fields` array, and a single-field entry that
|
|
1970
|
+
* indexes the exact column a field-level `isIndexed` on the same list
|
|
1971
|
+
* already indexes (the error also names that field and its `isIndexed`
|
|
1972
|
+
* value — one of the two should be removed). An entry naming a field the
|
|
1973
|
+
* list doesn't have, a virtual field, a to-many relationship, or the
|
|
1974
|
+
* non-FK side of a one-to-one relationship fails the same way, naming
|
|
1975
|
+
* the bad field too — no entry is ever silently dropped or emitted as
|
|
1976
|
+
* invalid Prisma.
|
|
1977
1977
|
*
|
|
1978
1978
|
* @example One audition per student per production (composite unique)
|
|
1979
1979
|
* ```typescript
|
|
@@ -2007,12 +2007,22 @@ export type ListConfig<TTypeInfo extends TypeInfo> = {
|
|
|
2007
2007
|
* })
|
|
2008
2008
|
* // Generates: @@index([identifier, createdAt(sort: Desc)], map: "AuthVerification_identifier_createdAt_idx")
|
|
2009
2009
|
* ```
|
|
2010
|
+
*
|
|
2011
|
+
* @example Naming a single-column unique constraint (adopting a live table's existing name)
|
|
2012
|
+
* ```typescript
|
|
2013
|
+
* RateLimit: list({
|
|
2014
|
+
* fields: { key: text() }, // no isIndexed here — db.indexes owns this column instead
|
|
2015
|
+
* db: {
|
|
2016
|
+
* indexes: [{ fields: ['key'], unique: true, name: 'RateLimit_key_key' }],
|
|
2017
|
+
* },
|
|
2018
|
+
* })
|
|
2019
|
+
* // Generates: @@unique([key], map: "RateLimit_key_key")
|
|
2020
|
+
* // Setting key's own isIndexed too would duplicate this constraint and
|
|
2021
|
+
* // fail generation — one of the two must own the column.
|
|
2022
|
+
* ```
|
|
2010
2023
|
*/
|
|
2011
2024
|
indexes?: ListIndex[]
|
|
2012
2025
|
}
|
|
2013
|
-
/**
|
|
2014
|
-
* MCP server configuration for this list
|
|
2015
|
-
*/
|
|
2016
2026
|
mcp?: ListMcpConfig
|
|
2017
2027
|
/**
|
|
2018
2028
|
* Restricts this list to a single record (singleton pattern)
|
|
@@ -2319,9 +2329,6 @@ export type ListConfigInput<TTypeInfo extends TypeInfo> = Omit<ListConfig<TTypeI
|
|
|
2319
2329
|
access?: ListAccessControl<TTypeInfo['item']>
|
|
2320
2330
|
}
|
|
2321
2331
|
|
|
2322
|
-
/**
|
|
2323
|
-
* Database configuration
|
|
2324
|
-
*/
|
|
2325
2332
|
export type DatabaseConfig = {
|
|
2326
2333
|
provider: 'postgresql' | 'mysql' | 'sqlite'
|
|
2327
2334
|
/**
|
|
@@ -2540,9 +2547,6 @@ export type DatabaseConfig = {
|
|
|
2540
2547
|
}
|
|
2541
2548
|
}
|
|
2542
2549
|
|
|
2543
|
-
/**
|
|
2544
|
-
* Session configuration
|
|
2545
|
-
*/
|
|
2546
2550
|
export type SessionConfig = {
|
|
2547
2551
|
// Uses `any` return type because session structure is user-defined and varies per application
|
|
2548
2552
|
// The stack doesn't enforce a specific session shape - users can use NextAuth, Clerk, etc.
|
|
@@ -2550,9 +2554,6 @@ export type SessionConfig = {
|
|
|
2550
2554
|
getSession: () => Promise<any>
|
|
2551
2555
|
}
|
|
2552
2556
|
|
|
2553
|
-
/**
|
|
2554
|
-
* Theme preset options
|
|
2555
|
-
*/
|
|
2556
2557
|
export type ThemePreset = 'modern' | 'classic' | 'neon'
|
|
2557
2558
|
|
|
2558
2559
|
/**
|
|
@@ -2648,14 +2649,8 @@ export type ThemeConfig = {
|
|
|
2648
2649
|
shadows?: ThemeShadows
|
|
2649
2650
|
}
|
|
2650
2651
|
|
|
2651
|
-
/**
|
|
2652
|
-
* UI configuration
|
|
2653
|
-
*/
|
|
2654
2652
|
export type UIConfig = {
|
|
2655
2653
|
basePath?: string
|
|
2656
|
-
/**
|
|
2657
|
-
* Theme configuration for the admin UI
|
|
2658
|
-
*/
|
|
2659
2654
|
theme?: ThemeConfig
|
|
2660
2655
|
}
|
|
2661
2656
|
|
|
@@ -2698,18 +2693,12 @@ export type McpCustomTool = {
|
|
|
2698
2693
|
* Unique name for the tool
|
|
2699
2694
|
*/
|
|
2700
2695
|
name: string
|
|
2701
|
-
/**
|
|
2702
|
-
* Description of what the tool does
|
|
2703
|
-
*/
|
|
2704
2696
|
description: string
|
|
2705
2697
|
/**
|
|
2706
2698
|
* Input schema (Zod schema)
|
|
2707
2699
|
*/
|
|
2708
2700
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2709
2701
|
inputSchema: any
|
|
2710
|
-
/**
|
|
2711
|
-
* Handler function that executes the tool
|
|
2712
|
-
*/
|
|
2713
2702
|
handler: (args: {
|
|
2714
2703
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2715
2704
|
input: any
|
|
@@ -2717,9 +2706,6 @@ export type McpCustomTool = {
|
|
|
2717
2706
|
}) => Promise<unknown>
|
|
2718
2707
|
}
|
|
2719
2708
|
|
|
2720
|
-
/**
|
|
2721
|
-
* List-level MCP configuration
|
|
2722
|
-
*/
|
|
2723
2709
|
export type ListMcpConfig = {
|
|
2724
2710
|
/**
|
|
2725
2711
|
* Enable MCP tools for this list
|
|
@@ -2790,9 +2776,6 @@ export type McpAuthConfig =
|
|
|
2790
2776
|
* Authentication type - custom auth provider
|
|
2791
2777
|
*/
|
|
2792
2778
|
type: string
|
|
2793
|
-
/**
|
|
2794
|
-
* Additional auth-specific configuration
|
|
2795
|
-
*/
|
|
2796
2779
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Allows custom auth provider configuration
|
|
2797
2780
|
[key: string]: any
|
|
2798
2781
|
}
|
|
@@ -2920,15 +2903,17 @@ export type PluginContext = {
|
|
|
2920
2903
|
|
|
2921
2904
|
/**
|
|
2922
2905
|
* Add a new list to the config
|
|
2923
|
-
* Throws error if list already exists
|
|
2906
|
+
* Throws error if list already exists
|
|
2924
2907
|
*/
|
|
2925
2908
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Plugin API must accept any list config
|
|
2926
2909
|
addList: (name: string, listConfig: ListConfig<any>) => void
|
|
2927
2910
|
|
|
2928
2911
|
/**
|
|
2929
|
-
* Extend an existing list with additional fields, hooks, or
|
|
2930
|
-
*
|
|
2931
|
-
*
|
|
2912
|
+
* Extend an existing list with additional fields, hooks, or MCP config.
|
|
2913
|
+
* Merges fields, hooks, and MCP config. Throws if the list doesn't exist,
|
|
2914
|
+
* or if the extension sets operation-level `access` — access control
|
|
2915
|
+
* belongs to whoever created the list, never a plugin extending it
|
|
2916
|
+
* (ADR-0013).
|
|
2932
2917
|
*/
|
|
2933
2918
|
extendList: (
|
|
2934
2919
|
name: string,
|
|
@@ -3047,10 +3032,6 @@ export type Plugin = {
|
|
|
3047
3032
|
}
|
|
3048
3033
|
}
|
|
3049
3034
|
|
|
3050
|
-
/**
|
|
3051
|
-
* Main configuration type
|
|
3052
|
-
* Using interface instead of type to allow module augmentation
|
|
3053
|
-
*/
|
|
3054
3035
|
/**
|
|
3055
3036
|
* Configurable generator output locations.
|
|
3056
3037
|
*
|
|
@@ -3112,6 +3093,10 @@ export interface OutputConfig {
|
|
|
3112
3093
|
buildTarget?: 'node'
|
|
3113
3094
|
}
|
|
3114
3095
|
|
|
3096
|
+
/**
|
|
3097
|
+
* Main configuration type.
|
|
3098
|
+
* Uses an interface, not a type alias, so it can be extended via module augmentation.
|
|
3099
|
+
*/
|
|
3115
3100
|
export interface OpenSaasConfig {
|
|
3116
3101
|
db: DatabaseConfig
|
|
3117
3102
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Config must accept any list configuration
|