@opensaas/stack-core 0.38.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 +123 -0
- package/CLAUDE.md +21 -0
- package/dist/access/access-filter.d.ts +46 -1
- package/dist/access/access-filter.d.ts.map +1 -1
- package/dist/access/access-filter.js +113 -4
- package/dist/access/access-filter.js.map +1 -1
- package/dist/access/declared-dependencies.d.ts.map +1 -1
- package/dist/access/declared-dependencies.js +4 -9
- package/dist/access/declared-dependencies.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 +49 -0
- package/dist/access/errors.d.ts.map +1 -1
- package/dist/access/errors.js +75 -0
- 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 +0 -4
- package/dist/access/field-visibility.d.ts.map +1 -1
- package/dist/access/field-visibility.js +13 -39
- package/dist/access/field-visibility.js.map +1 -1
- package/dist/access/index.d.ts +6 -2
- 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.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 +190 -193
- 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 +26 -227
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/index.js +14 -117
- package/dist/query/index.js.map +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.ts +152 -5
- package/src/access/declared-dependencies.ts +4 -9
- package/src/access/engine.ts +0 -25
- package/src/access/errors.ts +81 -0
- 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 +16 -47
- package/src/access/index.ts +20 -2
- package/src/access/query-validation.ts +359 -0
- package/src/access/relationship-count.ts +2 -10
- 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 +263 -247
- 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 +29 -235
- package/src/utils/password.ts +7 -76
- package/src/validation/field-config.ts +0 -6
- package/src/validation/schema.ts +0 -10
- package/tests/context.test.ts +700 -1
- package/tests/field-types.test.ts +222 -0
- package/tests/mcp-handler.test.ts +34 -0
- package/tests/transaction-boundary-outermost.test.ts +530 -0
- package/tsconfig.tsbuildinfo +1 -1
package/src/config/index.ts
CHANGED
|
@@ -8,16 +8,11 @@ import type {
|
|
|
8
8
|
import { executePlugins } from './plugin-engine.js'
|
|
9
9
|
import type { AccessControl } from '../access/types.js'
|
|
10
10
|
|
|
11
|
-
/**
|
|
12
|
-
* Normalize access control shorthand to object form
|
|
13
|
-
* Converts function shorthand to { operation: { query, create, update, delete } } form
|
|
14
|
-
*/
|
|
15
11
|
function normalizeListAccess<T>(
|
|
16
12
|
access: ListAccessControl<T> | undefined,
|
|
17
13
|
): { operation?: OperationAccess<T> } | undefined {
|
|
18
14
|
if (!access) return undefined
|
|
19
15
|
|
|
20
|
-
// If it's a function, convert to object form applying to all operations
|
|
21
16
|
if (typeof access === 'function') {
|
|
22
17
|
const fn = access as AccessControl<T>
|
|
23
18
|
return {
|
|
@@ -30,32 +25,31 @@ function normalizeListAccess<T>(
|
|
|
30
25
|
}
|
|
31
26
|
}
|
|
32
27
|
|
|
33
|
-
// Already in object form
|
|
34
28
|
return access
|
|
35
29
|
}
|
|
36
30
|
|
|
37
31
|
/**
|
|
38
|
-
*
|
|
39
|
-
* Executes plugins if present in config.plugins array
|
|
32
|
+
* Define an OpenSaas config, executing `plugins` if present.
|
|
40
33
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
34
|
+
* Returns synchronously when there are no plugins (backward compatible);
|
|
35
|
+
* returns a `Promise` when plugins are present, since plugin execution is
|
|
36
|
+
* async.
|
|
43
37
|
*/
|
|
44
38
|
export function config(userConfig: OpenSaasConfig): OpenSaasConfig | Promise<OpenSaasConfig> {
|
|
45
|
-
// If no plugins, return config as-is (synchronous, backward compatible)
|
|
46
39
|
if (!userConfig.plugins || userConfig.plugins.length === 0) {
|
|
47
40
|
return userConfig
|
|
48
41
|
}
|
|
49
42
|
|
|
50
|
-
// Execute plugins and return promise
|
|
51
43
|
return executePlugins(userConfig)
|
|
52
44
|
}
|
|
53
45
|
|
|
54
46
|
/**
|
|
55
|
-
*
|
|
47
|
+
* Define a list with type safety.
|
|
56
48
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
49
|
+
* `ListConfigInput<TTypeInfo>` accepts raw field configs; the return type
|
|
50
|
+
* `ListConfig<TTypeInfo>` injects `TTypeInfo` so hook/field callbacks (e.g.
|
|
51
|
+
* `resolveInput`'s `item`) are properly typed once `TTypeInfo` is supplied
|
|
52
|
+
* from generated types.
|
|
59
53
|
*
|
|
60
54
|
* @example
|
|
61
55
|
* ```typescript
|
|
@@ -108,18 +102,15 @@ export function config(userConfig: OpenSaasConfig): OpenSaasConfig | Promise<Ope
|
|
|
108
102
|
export function list<TTypeInfo extends import('./types.js').TypeInfo>(
|
|
109
103
|
config: ListConfigInput<TTypeInfo>,
|
|
110
104
|
): ListConfig<TTypeInfo> {
|
|
111
|
-
// Normalize access control shorthand to object form
|
|
112
105
|
const normalizedConfig = {
|
|
113
106
|
...config,
|
|
114
107
|
access: normalizeListAccess(config.access),
|
|
115
108
|
}
|
|
116
109
|
|
|
117
|
-
//
|
|
118
|
-
// At type level, they're transformed to inject TypeInfo types
|
|
110
|
+
// Runtime shape is unchanged; the cast only narrows the TS type to inject TTypeInfo.
|
|
119
111
|
return normalizedConfig as ListConfig<TTypeInfo>
|
|
120
112
|
}
|
|
121
113
|
|
|
122
|
-
// Re-export all types
|
|
123
114
|
export type {
|
|
124
115
|
OpenSaasConfig,
|
|
125
116
|
OutputConfig,
|
|
@@ -171,16 +162,13 @@ export type {
|
|
|
171
162
|
FileMetadata,
|
|
172
163
|
ImageMetadata,
|
|
173
164
|
ImageTransformationResult,
|
|
174
|
-
// Plugin system types
|
|
175
165
|
Plugin,
|
|
176
166
|
PluginContext,
|
|
177
167
|
GeneratedFiles,
|
|
178
|
-
// List-level hook argument types
|
|
179
168
|
ResolveInputHookArgs,
|
|
180
169
|
ValidateHookArgs,
|
|
181
170
|
BeforeOperationHookArgs,
|
|
182
171
|
AfterOperationHookArgs,
|
|
183
|
-
// Field-level hook argument types
|
|
184
172
|
FieldResolveInputHookArgs,
|
|
185
173
|
FieldValidateHookArgs,
|
|
186
174
|
FieldBeforeOperationHookArgs,
|
package/src/config/label.ts
CHANGED
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
import type { ListConfig } from './types.js'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Resolve the field name
|
|
5
|
-
*
|
|
6
|
-
* field this returns, so the field chosen for projection can never drift
|
|
7
|
-
* from the field used for rendering.
|
|
4
|
+
* Resolve the field name used as a list's Label field: `ui.labelField` if
|
|
5
|
+
* set, else `name`, else `title`, else `id`.
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
* (
|
|
11
|
-
*
|
|
12
|
-
* @throws if `ui.labelField` is set but does not reference a declared,
|
|
13
|
-
* non-relationship field on the list.
|
|
7
|
+
* @throws if `ui.labelField` names a field not declared on the list, or a
|
|
8
|
+
* relationship field (the Label field must be a scalar).
|
|
14
9
|
*/
|
|
15
10
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
|
16
11
|
export function getLabelFieldName(listConfig: ListConfig<any>): string {
|
|
@@ -37,10 +32,8 @@ export function getLabelFieldName(listConfig: ListConfig<any>): string {
|
|
|
37
32
|
}
|
|
38
33
|
|
|
39
34
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* when that field is missing from the row (e.g. stripped by field-level
|
|
43
|
-
* access).
|
|
35
|
+
* Render a row's Label field ({@link getLabelFieldName}) as text, falling
|
|
36
|
+
* back to `item.id` when the value is `null`/`undefined`.
|
|
44
37
|
*/
|
|
45
38
|
export function getItemLabel(
|
|
46
39
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ListConfig must accept any TypeInfo
|
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 },
|