@rangka/core 0.1.1 → 0.1.3
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/package.json +6 -2
- package/.claude/skills/extend-core/SKILL.md +0 -133
- package/.turbo/turbo-build.log +0 -4
- package/CHANGELOG.md +0 -25
- package/CLAUDE.md +0 -180
- package/src/__tests__/coerce.test.ts +0 -154
- package/src/__tests__/context.test.ts +0 -111
- package/src/__tests__/helpers.ts +0 -21
- package/src/__tests__/index.test.ts +0 -7
- package/src/__tests__/widgets.test.ts +0 -197
- package/src/api/__tests__/handlers.test.ts +0 -389
- package/src/api/__tests__/include-resolver.test.ts +0 -393
- package/src/api/__tests__/middleware.test.ts +0 -100
- package/src/api/__tests__/openapi-schema.test.ts +0 -210
- package/src/api/__tests__/query-parser.test.ts +0 -291
- package/src/api/__tests__/route-generator.test.ts +0 -137
- package/src/api/__tests__/server.test.ts +0 -73
- package/src/api/__tests__/swagger.test.ts +0 -166
- package/src/api/handlers.ts +0 -274
- package/src/api/include-resolver.ts +0 -27
- package/src/api/index.ts +0 -4
- package/src/api/meta-handler.ts +0 -254
- package/src/api/openapi-schema.ts +0 -99
- package/src/api/query-parser.ts +0 -315
- package/src/api/route-generator.ts +0 -448
- package/src/api/server.ts +0 -147
- package/src/api/types.ts +0 -16
- package/src/audit/__tests__/audit.test.ts +0 -144
- package/src/audit/index.ts +0 -3
- package/src/audit/record.ts +0 -69
- package/src/audit/tables.ts +0 -48
- package/src/audit/types.ts +0 -26
- package/src/auth/__tests__/core-module.test.ts +0 -54
- package/src/auth/__tests__/debug.test.ts +0 -47
- package/src/auth/__tests__/field-permissions.test.ts +0 -245
- package/src/auth/__tests__/integration.test.ts +0 -208
- package/src/auth/__tests__/meta-boot.test.ts +0 -538
- package/src/auth/__tests__/model-permissions.test.ts +0 -205
- package/src/auth/__tests__/password.test.ts +0 -29
- package/src/auth/__tests__/permission-registry.test.ts +0 -313
- package/src/auth/__tests__/scope-hook.test.ts +0 -509
- package/src/auth/__tests__/scope-registry.test.ts +0 -297
- package/src/auth/__tests__/scopes.test.ts +0 -66
- package/src/auth/__tests__/session.test.ts +0 -214
- package/src/auth/core-models.ts +0 -52
- package/src/auth/core-module.ts +0 -59
- package/src/auth/debug.ts +0 -157
- package/src/auth/field-permissions.ts +0 -116
- package/src/auth/index.ts +0 -37
- package/src/auth/model-permissions.ts +0 -59
- package/src/auth/password.ts +0 -22
- package/src/auth/permission-registry.ts +0 -171
- package/src/auth/scope-filters.ts +0 -11
- package/src/auth/scope-registry.ts +0 -121
- package/src/auth/scopes.ts +0 -146
- package/src/auth/seed.ts +0 -44
- package/src/auth/session.ts +0 -178
- package/src/auth/types.ts +0 -50
- package/src/boot/__tests__/page-scanning.test.ts +0 -170
- package/src/boot/__tests__/page-utils.test.ts +0 -225
- package/src/boot/__tests__/project-scanner.test.ts +0 -88
- package/src/boot/dependency-sort.ts +0 -82
- package/src/boot/discovery.ts +0 -85
- package/src/boot/index.ts +0 -457
- package/src/boot/page-utils.ts +0 -110
- package/src/boot/project-scanner.ts +0 -397
- package/src/boot/schema-loader.ts +0 -26
- package/src/boot/schema-merger.ts +0 -125
- package/src/boot/traits.ts +0 -25
- package/src/boot/types.ts +0 -73
- package/src/context.ts +0 -105
- package/src/db/__tests__/cascade-delete.test.ts +0 -182
- package/src/db/__tests__/desired-state.test.ts +0 -136
- package/src/db/__tests__/diff-engine.test.ts +0 -635
- package/src/db/__tests__/field-mapper.test.ts +0 -355
- package/src/db/__tests__/introspect.test.ts +0 -70
- package/src/db/__tests__/search-filter.test.ts +0 -45
- package/src/db/__tests__/sequence.test.ts +0 -221
- package/src/db/auto-sync.ts +0 -133
- package/src/db/client.ts +0 -147
- package/src/db/desired-state.ts +0 -98
- package/src/db/diff-engine.ts +0 -305
- package/src/db/field-mapper.ts +0 -504
- package/src/db/filter-applier.ts +0 -89
- package/src/db/include-resolver.ts +0 -40
- package/src/db/index.ts +0 -23
- package/src/db/introspect.ts +0 -265
- package/src/db/model-include-resolver.ts +0 -327
- package/src/db/model-ops.ts +0 -281
- package/src/db/scope-enforcer.ts +0 -37
- package/src/db/types.ts +0 -98
- package/src/errors.ts +0 -41
- package/src/events/__tests__/bus.test.ts +0 -105
- package/src/events/bus.ts +0 -89
- package/src/events/index.ts +0 -2
- package/src/events/types.ts +0 -9
- package/src/external-model/__tests__/computed-fields.test.ts +0 -106
- package/src/external-model/__tests__/field-mapper.test.ts +0 -160
- package/src/external-model/__tests__/in-memory-ops.test.ts +0 -247
- package/src/external-model/__tests__/mutation-executor.test.ts +0 -160
- package/src/external-model/__tests__/query-executor.test.ts +0 -284
- package/src/external-model/__tests__/schema-converter.test.ts +0 -174
- package/src/external-model/computed-fields.ts +0 -15
- package/src/external-model/define.ts +0 -5
- package/src/external-model/external-model-ops.ts +0 -108
- package/src/external-model/field-mapper.ts +0 -66
- package/src/external-model/in-memory-ops.ts +0 -107
- package/src/external-model/index.ts +0 -7
- package/src/external-model/mutation-executor.ts +0 -71
- package/src/external-model/query-executor.ts +0 -100
- package/src/external-model/schema-converter.ts +0 -53
- package/src/external-model/types.ts +0 -32
- package/src/fixtures/__tests__/fixtures.test.ts +0 -203
- package/src/fixtures/index.ts +0 -10
- package/src/fixtures/loader.ts +0 -196
- package/src/fixtures/registry.ts +0 -125
- package/src/fixtures/types.ts +0 -33
- package/src/helpers/assert-ownership.ts +0 -19
- package/src/helpers/coerce.ts +0 -28
- package/src/helpers/stamping.ts +0 -28
- package/src/helpers/validation.ts +0 -14
- package/src/hooks/__tests__/context.test.ts +0 -73
- package/src/hooks/__tests__/executor.test.ts +0 -433
- package/src/hooks/__tests__/middleware.test.ts +0 -224
- package/src/hooks/__tests__/registry.test.ts +0 -50
- package/src/hooks/context.ts +0 -89
- package/src/hooks/errors.ts +0 -11
- package/src/hooks/executor.ts +0 -115
- package/src/hooks/index.ts +0 -10
- package/src/hooks/middleware.ts +0 -220
- package/src/hooks/registry.ts +0 -20
- package/src/hooks/types.ts +0 -32
- package/src/index.ts +0 -172
- package/src/jobs/__tests__/enqueue.test.ts +0 -77
- package/src/jobs/__tests__/integration.test.ts +0 -71
- package/src/jobs/__tests__/registry.test.ts +0 -103
- package/src/jobs/__tests__/scheduler.test.ts +0 -92
- package/src/jobs/__tests__/worker-execution.test.ts +0 -202
- package/src/jobs/__tests__/worker.test.ts +0 -119
- package/src/jobs/enqueue.ts +0 -93
- package/src/jobs/index.ts +0 -14
- package/src/jobs/registry.ts +0 -92
- package/src/jobs/scheduler.ts +0 -205
- package/src/jobs/tables.ts +0 -132
- package/src/jobs/types.ts +0 -62
- package/src/jobs/worker.ts +0 -272
- package/src/model-api/__tests__/cross-boundary-includes.test.ts +0 -366
- package/src/model-api/__tests__/extended-api.test.ts +0 -244
- package/src/model-api/__tests__/filter-applier.test.ts +0 -177
- package/src/model-api/__tests__/filter-translator.test.ts +0 -186
- package/src/model-api/__tests__/include-resolver.test.ts +0 -226
- package/src/model-api/__tests__/model-access.test.ts +0 -284
- package/src/model-api/__tests__/query-builder.test.ts +0 -224
- package/src/model-api/__tests__/scope-enforcer.test.ts +0 -268
- package/src/model-api/field-access.ts +0 -28
- package/src/model-api/filter-applier.ts +0 -1
- package/src/model-api/filter-translator.ts +0 -67
- package/src/model-api/include-resolver.ts +0 -2
- package/src/model-api/index.ts +0 -86
- package/src/model-api/query-builder.ts +0 -155
- package/src/model-api/scope-enforcer.ts +0 -3
- package/src/model-api/types.ts +0 -139
- package/src/plugins/__tests__/adapter-registry.test.ts +0 -92
- package/src/plugins/__tests__/lifecycle.test.ts +0 -96
- package/src/plugins/__tests__/loader.test.ts +0 -273
- package/src/plugins/__tests__/validator.test.ts +0 -275
- package/src/plugins/adapter-registry.ts +0 -42
- package/src/plugins/define.ts +0 -5
- package/src/plugins/index.ts +0 -28
- package/src/plugins/lifecycle.ts +0 -27
- package/src/plugins/loader.ts +0 -126
- package/src/plugins/types.ts +0 -76
- package/src/plugins/validator.ts +0 -141
- package/src/schema/__tests__/registry-models-by-module.test.ts +0 -58
- package/src/schema/registry.ts +0 -93
- package/src/schema/relationships.ts +0 -93
- package/src/schema/types.ts +0 -43
- package/src/services/__tests__/integration.test.ts +0 -63
- package/src/services/__tests__/registry.test.ts +0 -175
- package/src/services/index.ts +0 -13
- package/src/services/registry.ts +0 -156
- package/src/services/types.ts +0 -27
- package/src/validation/__tests__/field-validator.test.ts +0 -195
- package/src/validation/field-validator.ts +0 -113
- package/src/validation/index.ts +0 -1
- package/src/widgets/index.ts +0 -3
- package/src/widgets/slot-validator.ts +0 -87
- package/src/widgets/widget-registry.ts +0 -32
- package/tests/boot.test.ts +0 -323
- package/tests/dependency-sort.test.ts +0 -99
- package/tests/discovery.test.ts +0 -126
- package/tests/registry.test.ts +0 -216
- package/tests/schema-loader.test.ts +0 -52
- package/tests/schema-merger.test.ts +0 -180
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -14
package/src/plugins/loader.ts
DELETED
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
PluginDefinition,
|
|
3
|
-
PluginBootContext,
|
|
4
|
-
DataAdapter,
|
|
5
|
-
PluginLifecycleEvent,
|
|
6
|
-
LifecycleHandler,
|
|
7
|
-
} from './types.js';
|
|
8
|
-
import { AdapterRegistry } from './adapter-registry.js';
|
|
9
|
-
import { PluginLifecycleManager } from './lifecycle.js';
|
|
10
|
-
|
|
11
|
-
export class DuplicatePluginError extends Error {
|
|
12
|
-
constructor(public readonly pluginName: string) {
|
|
13
|
-
super(`Plugin "${pluginName}" is already registered`);
|
|
14
|
-
this.name = 'DuplicatePluginError';
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export class PluginConfigError extends Error {
|
|
19
|
-
constructor(
|
|
20
|
-
public readonly pluginName: string,
|
|
21
|
-
public readonly field: string,
|
|
22
|
-
) {
|
|
23
|
-
super(`Plugin "${pluginName}" requires config field "${field}"`);
|
|
24
|
-
this.name = 'PluginConfigError';
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export class MissingAdapterImplementationError extends Error {
|
|
29
|
-
constructor(
|
|
30
|
-
public readonly pluginName: string,
|
|
31
|
-
public readonly adapterName: string,
|
|
32
|
-
) {
|
|
33
|
-
super(
|
|
34
|
-
`Plugin "${pluginName}" declares adapter "${adapterName}" in provides but did not implement it during boot`,
|
|
35
|
-
);
|
|
36
|
-
this.name = 'MissingAdapterImplementationError';
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface LoadPluginsOptions {
|
|
41
|
-
plugins: PluginDefinition[];
|
|
42
|
-
config?: Record<string, Record<string, unknown>>;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface LoadPluginsResult {
|
|
46
|
-
adapterRegistry: AdapterRegistry;
|
|
47
|
-
lifecycleManager: PluginLifecycleManager;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export async function loadPlugins(options: LoadPluginsOptions): Promise<LoadPluginsResult> {
|
|
51
|
-
const { plugins, config = {} } = options;
|
|
52
|
-
const adapterRegistry = new AdapterRegistry();
|
|
53
|
-
const lifecycleManager = new PluginLifecycleManager();
|
|
54
|
-
|
|
55
|
-
const seen = new Set<string>();
|
|
56
|
-
for (const plugin of plugins) {
|
|
57
|
-
if (seen.has(plugin.name)) {
|
|
58
|
-
throw new DuplicatePluginError(plugin.name);
|
|
59
|
-
}
|
|
60
|
-
seen.add(plugin.name);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
for (const plugin of plugins) {
|
|
64
|
-
const pluginConfig = resolveConfig(plugin, config[plugin.name] ?? {});
|
|
65
|
-
const implemented = new Set<string>();
|
|
66
|
-
|
|
67
|
-
const adapterProxies: Record<string, { implement(impl: DataAdapter): void }> = {};
|
|
68
|
-
if (plugin.provides?.adapters) {
|
|
69
|
-
for (const decl of plugin.provides.adapters) {
|
|
70
|
-
adapterProxies[decl.name] = {
|
|
71
|
-
implement(impl: DataAdapter) {
|
|
72
|
-
adapterRegistry.register(decl.name, impl);
|
|
73
|
-
implemented.add(decl.name);
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const ctx: PluginBootContext = {
|
|
80
|
-
config: pluginConfig,
|
|
81
|
-
adapters: adapterProxies,
|
|
82
|
-
on(event: PluginLifecycleEvent, handler: LifecycleHandler) {
|
|
83
|
-
lifecycleManager.register(event, handler);
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
await plugin.boot(ctx);
|
|
88
|
-
|
|
89
|
-
if (plugin.provides?.adapters) {
|
|
90
|
-
for (const decl of plugin.provides.adapters) {
|
|
91
|
-
if (!implemented.has(decl.name)) {
|
|
92
|
-
throw new MissingAdapterImplementationError(plugin.name, decl.name);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return { adapterRegistry, lifecycleManager };
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function resolveConfig(
|
|
102
|
-
plugin: PluginDefinition,
|
|
103
|
-
userConfig: Record<string, unknown>,
|
|
104
|
-
): Record<string, unknown> {
|
|
105
|
-
const result: Record<string, unknown> = {};
|
|
106
|
-
|
|
107
|
-
if (!plugin.config) return userConfig;
|
|
108
|
-
|
|
109
|
-
for (const [key, schema] of Object.entries(plugin.config)) {
|
|
110
|
-
if (userConfig[key] !== undefined) {
|
|
111
|
-
result[key] = userConfig[key];
|
|
112
|
-
} else if (schema.default !== undefined) {
|
|
113
|
-
result[key] = schema.default;
|
|
114
|
-
} else if (schema.required) {
|
|
115
|
-
throw new PluginConfigError(plugin.name, key);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
for (const [key, value] of Object.entries(userConfig)) {
|
|
120
|
-
if (!(key in result)) {
|
|
121
|
-
result[key] = value;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return result;
|
|
126
|
-
}
|
package/src/plugins/types.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
|
|
3
|
-
export type AdapterCapability =
|
|
4
|
-
| 'read'
|
|
5
|
-
| 'list'
|
|
6
|
-
| 'filter'
|
|
7
|
-
| 'sort'
|
|
8
|
-
| 'create'
|
|
9
|
-
| 'update'
|
|
10
|
-
| 'delete';
|
|
11
|
-
|
|
12
|
-
export interface FilterExpression {
|
|
13
|
-
field: string;
|
|
14
|
-
operator: string;
|
|
15
|
-
value: unknown;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface ListQuery {
|
|
19
|
-
page?: number;
|
|
20
|
-
pageSize?: number;
|
|
21
|
-
sort?: { field: string; direction: 'asc' | 'desc' };
|
|
22
|
-
filters?: FilterExpression[];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export interface ListResult {
|
|
26
|
-
data: Record<string, unknown>[];
|
|
27
|
-
total?: number;
|
|
28
|
-
hasMore?: boolean;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface DataAdapter {
|
|
32
|
-
get(model: string, id: string): Promise<Record<string, unknown> | null>;
|
|
33
|
-
list?(model: string, query: ListQuery): Promise<ListResult>;
|
|
34
|
-
create?(model: string, data: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
35
|
-
update?(
|
|
36
|
-
model: string,
|
|
37
|
-
id: string,
|
|
38
|
-
data: Record<string, unknown>,
|
|
39
|
-
): Promise<Record<string, unknown>>;
|
|
40
|
-
delete?(model: string, id: string): Promise<void>;
|
|
41
|
-
filter?(model: string, filters: FilterExpression[]): Promise<ListResult>;
|
|
42
|
-
batchGet?(model: string, ids: string[]): Promise<Record<string, unknown>[]>;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export type PluginLifecycleEvent =
|
|
46
|
-
| 'beforeBoot'
|
|
47
|
-
| 'afterBoot'
|
|
48
|
-
| 'beforeRequest'
|
|
49
|
-
| 'afterRequest'
|
|
50
|
-
| 'beforeShutdown';
|
|
51
|
-
|
|
52
|
-
export interface PluginConfigField {
|
|
53
|
-
type: string;
|
|
54
|
-
required?: boolean;
|
|
55
|
-
default?: unknown;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export interface PluginProvides {
|
|
59
|
-
adapters?: Array<{ name: string; capabilities: AdapterCapability[] }>;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export interface PluginBootContext {
|
|
63
|
-
config: Record<string, unknown>;
|
|
64
|
-
adapters: Record<string, { implement(impl: DataAdapter): void }>;
|
|
65
|
-
on(event: PluginLifecycleEvent, handler: (...args: any[]) => Promise<void>): void;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface PluginDefinition {
|
|
69
|
-
name: string;
|
|
70
|
-
version: string;
|
|
71
|
-
config?: Record<string, PluginConfigField>;
|
|
72
|
-
provides?: PluginProvides;
|
|
73
|
-
boot(ctx: PluginBootContext): void | Promise<void>;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export type LifecycleHandler = (...args: any[]) => Promise<void>;
|
package/src/plugins/validator.ts
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import type { AdapterRegistry } from './adapter-registry.js';
|
|
2
|
-
import type { AdapterCapability, PluginDefinition } from './types.js';
|
|
3
|
-
import type { ResolvedModel } from '../schema/types.js';
|
|
4
|
-
|
|
5
|
-
export interface PluginValidationError {
|
|
6
|
-
type:
|
|
7
|
-
| 'MISSING_ADAPTER_IMPL'
|
|
8
|
-
| 'UNRESOLVED_SOURCE'
|
|
9
|
-
| 'CONFIG_REQUIRED'
|
|
10
|
-
| 'DUPLICATE_PLUGIN'
|
|
11
|
-
| 'CAPABILITY_VIOLATION';
|
|
12
|
-
plugin?: string;
|
|
13
|
-
model?: string;
|
|
14
|
-
message: string;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface ValidationResult {
|
|
18
|
-
valid: boolean;
|
|
19
|
-
errors: PluginValidationError[];
|
|
20
|
-
warnings: string[];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function validatePluginSetup(
|
|
24
|
-
adapterRegistry: AdapterRegistry,
|
|
25
|
-
externalModels: ResolvedModel[],
|
|
26
|
-
pluginDefinitions: PluginDefinition[],
|
|
27
|
-
adapterCapabilities?: Record<string, AdapterCapability[]>,
|
|
28
|
-
): ValidationResult {
|
|
29
|
-
const errors: PluginValidationError[] = [];
|
|
30
|
-
const warnings: string[] = [];
|
|
31
|
-
|
|
32
|
-
checkDuplicatePlugins(pluginDefinitions, errors);
|
|
33
|
-
checkAdapterImplementations(pluginDefinitions, adapterRegistry, errors);
|
|
34
|
-
checkExternalModelSources(externalModels, adapterRegistry, errors);
|
|
35
|
-
checkCapabilityViolations(externalModels, adapterCapabilities ?? {}, errors);
|
|
36
|
-
checkBatchGetSupport(externalModels, adapterRegistry, warnings);
|
|
37
|
-
|
|
38
|
-
return {
|
|
39
|
-
valid: errors.length === 0,
|
|
40
|
-
errors,
|
|
41
|
-
warnings,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function checkDuplicatePlugins(plugins: PluginDefinition[], errors: PluginValidationError[]): void {
|
|
46
|
-
const seen = new Set<string>();
|
|
47
|
-
for (const plugin of plugins) {
|
|
48
|
-
if (seen.has(plugin.name)) {
|
|
49
|
-
errors.push({
|
|
50
|
-
type: 'DUPLICATE_PLUGIN',
|
|
51
|
-
plugin: plugin.name,
|
|
52
|
-
message: `Duplicate plugin name: "${plugin.name}"`,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
seen.add(plugin.name);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function checkAdapterImplementations(
|
|
60
|
-
plugins: PluginDefinition[],
|
|
61
|
-
adapterRegistry: AdapterRegistry,
|
|
62
|
-
errors: PluginValidationError[],
|
|
63
|
-
): void {
|
|
64
|
-
for (const plugin of plugins) {
|
|
65
|
-
if (!plugin.provides?.adapters) continue;
|
|
66
|
-
for (const decl of plugin.provides.adapters) {
|
|
67
|
-
if (!adapterRegistry.has(decl.name)) {
|
|
68
|
-
errors.push({
|
|
69
|
-
type: 'MISSING_ADAPTER_IMPL',
|
|
70
|
-
plugin: plugin.name,
|
|
71
|
-
message: `Plugin "${plugin.name}" declares adapter "${decl.name}" but it is not registered`,
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function checkExternalModelSources(
|
|
79
|
-
models: ResolvedModel[],
|
|
80
|
-
adapterRegistry: AdapterRegistry,
|
|
81
|
-
errors: PluginValidationError[],
|
|
82
|
-
): void {
|
|
83
|
-
for (const model of models) {
|
|
84
|
-
if (!model.source) continue;
|
|
85
|
-
if (!adapterRegistry.has(model.source)) {
|
|
86
|
-
errors.push({
|
|
87
|
-
type: 'UNRESOLVED_SOURCE',
|
|
88
|
-
model: model.qualifiedName,
|
|
89
|
-
message: `External model "${model.qualifiedName}" references adapter "${model.source}" which is not registered`,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function checkBatchGetSupport(
|
|
96
|
-
models: ResolvedModel[],
|
|
97
|
-
adapterRegistry: AdapterRegistry,
|
|
98
|
-
warnings: string[],
|
|
99
|
-
): void {
|
|
100
|
-
for (const model of models) {
|
|
101
|
-
if (!model.source) continue;
|
|
102
|
-
if (!adapterRegistry.has(model.source)) continue;
|
|
103
|
-
|
|
104
|
-
const adapter = adapterRegistry.get(model.source);
|
|
105
|
-
if (!adapter.batchGet) {
|
|
106
|
-
warnings.push(
|
|
107
|
-
`Adapter "${model.source}" used by "${model.qualifiedName}" does not implement batchGet. Relationship resolution will use N+1 get calls.`,
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function checkCapabilityViolations(
|
|
114
|
-
models: ResolvedModel[],
|
|
115
|
-
adapterCapabilities: Record<string, AdapterCapability[]>,
|
|
116
|
-
errors: PluginValidationError[],
|
|
117
|
-
): void {
|
|
118
|
-
for (const model of models) {
|
|
119
|
-
if (!model.source) continue;
|
|
120
|
-
const declared = adapterCapabilities[model.source];
|
|
121
|
-
if (!declared) continue;
|
|
122
|
-
|
|
123
|
-
const capabilities = new Set(declared);
|
|
124
|
-
|
|
125
|
-
if (!capabilities.has('read')) {
|
|
126
|
-
errors.push({
|
|
127
|
-
type: 'CAPABILITY_VIOLATION',
|
|
128
|
-
model: model.qualifiedName,
|
|
129
|
-
message: `External model "${model.qualifiedName}" uses adapter "${model.source}" which does not declare the required "read" capability`,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (!capabilities.has('list')) {
|
|
134
|
-
errors.push({
|
|
135
|
-
type: 'CAPABILITY_VIOLATION',
|
|
136
|
-
model: model.qualifiedName,
|
|
137
|
-
message: `External model "${model.qualifiedName}" uses adapter "${model.source}" which does not declare "list" capability. List endpoints will not be generated.`,
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { SchemaRegistry } from '../registry.js';
|
|
3
|
-
import type { ResolvedModel } from '../types.js';
|
|
4
|
-
|
|
5
|
-
function makeModel(module: string, name: string): ResolvedModel {
|
|
6
|
-
return {
|
|
7
|
-
qualifiedName: `${module}.${name}`,
|
|
8
|
-
app: 'test',
|
|
9
|
-
module,
|
|
10
|
-
name,
|
|
11
|
-
auditLog: false,
|
|
12
|
-
traits: [],
|
|
13
|
-
fields: [{ name: 'id', config: { type: 'string' }, provenance: { source: 'base' } }],
|
|
14
|
-
indexes: [],
|
|
15
|
-
};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe('SchemaRegistry.getModelsByModule', () => {
|
|
19
|
-
it('returns empty map for empty registry', () => {
|
|
20
|
-
const registry = new SchemaRegistry([]);
|
|
21
|
-
const result = registry.getModelsByModule();
|
|
22
|
-
expect(result.size).toBe(0);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('groups models by module', () => {
|
|
26
|
-
const models = [
|
|
27
|
-
makeModel('sales', 'invoice'),
|
|
28
|
-
makeModel('sales', 'customer'),
|
|
29
|
-
makeModel('accounting', 'journal_entry'),
|
|
30
|
-
];
|
|
31
|
-
const registry = new SchemaRegistry(models);
|
|
32
|
-
const result = registry.getModelsByModule();
|
|
33
|
-
|
|
34
|
-
expect(result.size).toBe(2);
|
|
35
|
-
expect(result.get('sales')).toHaveLength(2);
|
|
36
|
-
expect(result.get('accounting')).toHaveLength(1);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('preserves model data', () => {
|
|
40
|
-
const models = [makeModel('sales', 'invoice')];
|
|
41
|
-
const registry = new SchemaRegistry(models);
|
|
42
|
-
const result = registry.getModelsByModule();
|
|
43
|
-
const salesModels = result.get('sales')!;
|
|
44
|
-
expect(salesModels[0].qualifiedName).toBe('sales.invoice');
|
|
45
|
-
expect(salesModels[0].name).toBe('invoice');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('handles single module with multiple models', () => {
|
|
49
|
-
const models = [
|
|
50
|
-
makeModel('hr', 'employee'),
|
|
51
|
-
makeModel('hr', 'department'),
|
|
52
|
-
makeModel('hr', 'leave_request'),
|
|
53
|
-
];
|
|
54
|
-
const registry = new SchemaRegistry(models);
|
|
55
|
-
const result = registry.getModelsByModule();
|
|
56
|
-
expect(result.get('hr')).toHaveLength(3);
|
|
57
|
-
});
|
|
58
|
-
});
|
package/src/schema/registry.ts
DELETED
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import type { ResolvedModel, ResolvedField, ModelRelationship, ExtensionSource } from './types.js';
|
|
2
|
-
import { buildRelationships } from './relationships.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Central registry providing lookup access to all resolved models,
|
|
6
|
-
* their relationships, and extension metadata.
|
|
7
|
-
*/
|
|
8
|
-
export class SchemaRegistry {
|
|
9
|
-
private readonly modelMap: Map<string, ResolvedModel>;
|
|
10
|
-
private readonly relationships: ModelRelationship[];
|
|
11
|
-
private readonly extensionSourceMap: Map<string, ExtensionSource[]>;
|
|
12
|
-
|
|
13
|
-
constructor(models: ResolvedModel[]) {
|
|
14
|
-
this.modelMap = new Map();
|
|
15
|
-
for (const model of models) {
|
|
16
|
-
this.modelMap.set(model.qualifiedName, model);
|
|
17
|
-
}
|
|
18
|
-
this.relationships = buildRelationships(models);
|
|
19
|
-
this.extensionSourceMap = this.buildExtensionSourceMap(models);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/** Retrieve a single model by its fully qualified name (e.g. "sales.Invoice"). */
|
|
23
|
-
getModel(qualifiedName: string): ResolvedModel | undefined {
|
|
24
|
-
return this.modelMap.get(qualifiedName);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Return all registered models. */
|
|
28
|
-
getAllModels(): ResolvedModel[] {
|
|
29
|
-
return Array.from(this.modelMap.values());
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/** Return the field definitions for a given model. */
|
|
33
|
-
getFieldsForModel(qualifiedName: string): ResolvedField[] {
|
|
34
|
-
const model = this.modelMap.get(qualifiedName);
|
|
35
|
-
if (!model) return [];
|
|
36
|
-
return model.fields;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Return all computed relationships across the schema. */
|
|
40
|
-
getRelationships(): ModelRelationship[] {
|
|
41
|
-
return this.relationships;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Return relationships originating from a specific model. */
|
|
45
|
-
getRelationshipsForModel(qualifiedName: string): ModelRelationship[] {
|
|
46
|
-
return this.relationships.filter((r) => r.from === qualifiedName);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Return extension source metadata for a model (which apps contributed fields). */
|
|
50
|
-
getExtensionSources(qualifiedName: string): ExtensionSource[] {
|
|
51
|
-
return this.extensionSourceMap.get(qualifiedName) ?? [];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Group all models by their module name. */
|
|
55
|
-
getModelsByModule(): Map<string, ResolvedModel[]> {
|
|
56
|
-
const moduleToModels = new Map<string, ResolvedModel[]>();
|
|
57
|
-
for (const model of this.modelMap.values()) {
|
|
58
|
-
const group = moduleToModels.get(model.module) ?? [];
|
|
59
|
-
group.push(model);
|
|
60
|
-
moduleToModels.set(model.module, group);
|
|
61
|
-
}
|
|
62
|
-
return moduleToModels;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Builds a map from model name to the list of apps that extended it with extra fields.
|
|
67
|
-
*/
|
|
68
|
-
private buildExtensionSourceMap(models: ResolvedModel[]): Map<string, ExtensionSource[]> {
|
|
69
|
-
const map = new Map<string, ExtensionSource[]>();
|
|
70
|
-
|
|
71
|
-
for (const model of models) {
|
|
72
|
-
const extensionFields = model.fields.filter((f) => f.provenance.source === 'extension');
|
|
73
|
-
if (extensionFields.length === 0) continue;
|
|
74
|
-
|
|
75
|
-
// Group extension fields by the app that contributed them
|
|
76
|
-
const fieldsByApp = new Map<string, string[]>();
|
|
77
|
-
for (const field of extensionFields) {
|
|
78
|
-
const contributingApp = field.provenance.app!;
|
|
79
|
-
const fieldNames = fieldsByApp.get(contributingApp) ?? [];
|
|
80
|
-
fieldNames.push(field.name);
|
|
81
|
-
fieldsByApp.set(contributingApp, fieldNames);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const sources: ExtensionSource[] = [];
|
|
85
|
-
for (const [app, fields] of fieldsByApp) {
|
|
86
|
-
sources.push({ app, fields });
|
|
87
|
-
}
|
|
88
|
-
map.set(model.qualifiedName, sources);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return map;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import type { ResolvedModel, ModelRelationship } from './types.js';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Scans all models for relational fields and produces a flat list of relationships.
|
|
6
|
-
*/
|
|
7
|
-
export function buildRelationships(models: ResolvedModel[]): ModelRelationship[] {
|
|
8
|
-
const relationships: ModelRelationship[] = [];
|
|
9
|
-
|
|
10
|
-
for (const model of models) {
|
|
11
|
-
for (const field of model.fields) {
|
|
12
|
-
const relationship = fieldToRelationship(field.config, field.name, model, models);
|
|
13
|
-
if (relationship) {
|
|
14
|
-
relationships.push(relationship);
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return relationships;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Converts a single field config into a ModelRelationship, or returns null
|
|
24
|
-
* if the field is not a relational type.
|
|
25
|
-
*/
|
|
26
|
-
function fieldToRelationship(
|
|
27
|
-
config: any,
|
|
28
|
-
fieldName: string,
|
|
29
|
-
sourceModel: ResolvedModel,
|
|
30
|
-
allModels: ResolvedModel[],
|
|
31
|
-
): ModelRelationship | null {
|
|
32
|
-
const from = sourceModel.qualifiedName;
|
|
33
|
-
|
|
34
|
-
switch (config.type) {
|
|
35
|
-
case 'link': {
|
|
36
|
-
const targetModel = resolveModelName(config.model, sourceModel.module, allModels);
|
|
37
|
-
return { type: 'link', from, field: fieldName, to: targetModel };
|
|
38
|
-
}
|
|
39
|
-
case 'hasMany': {
|
|
40
|
-
const targetModel = resolveModelName(config.model, sourceModel.module, allModels);
|
|
41
|
-
return {
|
|
42
|
-
type: 'hasMany',
|
|
43
|
-
from,
|
|
44
|
-
field: fieldName,
|
|
45
|
-
to: targetModel,
|
|
46
|
-
foreignKey: config.foreignKey,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
case 'children': {
|
|
50
|
-
const targetModel = resolveModelName(config.model, sourceModel.module, allModels);
|
|
51
|
-
return {
|
|
52
|
-
type: 'children',
|
|
53
|
-
from,
|
|
54
|
-
field: fieldName,
|
|
55
|
-
to: targetModel,
|
|
56
|
-
foreignKey: config.foreignKey,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
case 'manyToMany': {
|
|
60
|
-
const targetModel = resolveModelName(config.model, sourceModel.module, allModels);
|
|
61
|
-
const throughModel = resolveModelName(config.through, sourceModel.module, allModels);
|
|
62
|
-
return { type: 'manyToMany', from, field: fieldName, to: targetModel, through: throughModel };
|
|
63
|
-
}
|
|
64
|
-
case 'dynamicLink': {
|
|
65
|
-
return {
|
|
66
|
-
type: 'dynamicLink',
|
|
67
|
-
from,
|
|
68
|
-
field: fieldName,
|
|
69
|
-
to: '*',
|
|
70
|
-
modelField: config.modelField,
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
default:
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Resolves an unqualified model name (e.g. "Invoice") to its fully qualified form
|
|
80
|
-
* (e.g. "accounting.Invoice") by checking the current app first, then all models.
|
|
81
|
-
*/
|
|
82
|
-
function resolveModelName(name: string, currentModule: string, models: ResolvedModel[]): string {
|
|
83
|
-
if (name.includes('.')) return name;
|
|
84
|
-
|
|
85
|
-
const qualifiedInCurrentModule = `${currentModule}.${name}`;
|
|
86
|
-
const foundInCurrentModule = models.find((m) => m.qualifiedName === qualifiedInCurrentModule);
|
|
87
|
-
if (foundInCurrentModule) return qualifiedInCurrentModule;
|
|
88
|
-
|
|
89
|
-
const foundElsewhere = models.find((m) => m.name === name);
|
|
90
|
-
if (foundElsewhere) return foundElsewhere.qualifiedName;
|
|
91
|
-
|
|
92
|
-
return qualifiedInCurrentModule;
|
|
93
|
-
}
|
package/src/schema/types.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import type { FieldConfig, ModelConfig, ScopeConfig } from '@rangka/shared';
|
|
2
|
-
|
|
3
|
-
export interface FieldProvenance {
|
|
4
|
-
source: 'base' | 'trait' | 'extension';
|
|
5
|
-
app?: string;
|
|
6
|
-
trait?: string;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export interface ResolvedField {
|
|
10
|
-
name: string;
|
|
11
|
-
config: FieldConfig;
|
|
12
|
-
provenance: FieldProvenance;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface ResolvedModel {
|
|
16
|
-
qualifiedName: string;
|
|
17
|
-
app: string;
|
|
18
|
-
module: string;
|
|
19
|
-
name: string;
|
|
20
|
-
label?: string;
|
|
21
|
-
naming?: ModelConfig['naming'];
|
|
22
|
-
scope?: ScopeConfig;
|
|
23
|
-
auditLog: boolean;
|
|
24
|
-
traits: string[];
|
|
25
|
-
fields: ResolvedField[];
|
|
26
|
-
indexes: ModelConfig['indexes'];
|
|
27
|
-
source?: string;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface ModelRelationship {
|
|
31
|
-
type: 'link' | 'hasMany' | 'children' | 'manyToMany' | 'dynamicLink';
|
|
32
|
-
from: string;
|
|
33
|
-
field: string;
|
|
34
|
-
to: string;
|
|
35
|
-
foreignKey?: string;
|
|
36
|
-
through?: string;
|
|
37
|
-
modelField?: string;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface ExtensionSource {
|
|
41
|
-
app: string;
|
|
42
|
-
fields: string[];
|
|
43
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { ServiceRegistry } from '../registry.js';
|
|
3
|
-
import { HookRegistry } from '../../hooks/registry.js';
|
|
4
|
-
import { executeHookPipeline } from '../../hooks/executor.js';
|
|
5
|
-
import { SchemaRegistry } from '../../schema/registry.js';
|
|
6
|
-
import { EventBus } from '../../events/bus.js';
|
|
7
|
-
|
|
8
|
-
describe('hook → service → db integration', () => {
|
|
9
|
-
it('hook calls ctx.service() which accesses ctx.db', async () => {
|
|
10
|
-
const serviceRegistry = new ServiceRegistry();
|
|
11
|
-
const dbCalls: string[] = [];
|
|
12
|
-
|
|
13
|
-
serviceRegistry.register({
|
|
14
|
-
name: 'pricing',
|
|
15
|
-
factory: (_ctx) => ({
|
|
16
|
-
getDiscount: async (customerId: unknown) => {
|
|
17
|
-
dbCalls.push(`query:${customerId}`);
|
|
18
|
-
return 0.1;
|
|
19
|
-
},
|
|
20
|
-
}),
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const hookRegistry = new HookRegistry();
|
|
24
|
-
hookRegistry.register(
|
|
25
|
-
'sales.order',
|
|
26
|
-
{
|
|
27
|
-
beforeSave: async (doc: any, ctx: any) => {
|
|
28
|
-
const pricing = ctx.service('pricing') as any;
|
|
29
|
-
const discount = await pricing.getDiscount(doc.customer_id);
|
|
30
|
-
doc.discount = discount;
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
'sales',
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
const schema = new SchemaRegistry([]);
|
|
37
|
-
const eventBus = new EventBus();
|
|
38
|
-
const auth = { user: { id: '1' }, roles: ['Admin'], scopes: {} } as any;
|
|
39
|
-
|
|
40
|
-
const mockTrx = {
|
|
41
|
-
transaction: () => ({
|
|
42
|
-
execute: async (fn: any) => fn(mockTrx),
|
|
43
|
-
}),
|
|
44
|
-
} as any;
|
|
45
|
-
|
|
46
|
-
const chain = hookRegistry.getChain('sales.order')!;
|
|
47
|
-
const result = await executeHookPipeline({
|
|
48
|
-
model: 'sales.order',
|
|
49
|
-
operation: 'create',
|
|
50
|
-
chain,
|
|
51
|
-
doc: { customer_id: 'cust-1', total: 100 } as any,
|
|
52
|
-
db: mockTrx,
|
|
53
|
-
schema,
|
|
54
|
-
auth,
|
|
55
|
-
eventBus,
|
|
56
|
-
serviceRegistry,
|
|
57
|
-
execute: async (doc) => doc,
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
expect(result.discount).toBe(0.1);
|
|
61
|
-
expect(dbCalls).toEqual(['query:cust-1']);
|
|
62
|
-
});
|
|
63
|
-
});
|