@rangka/core 0.1.0 → 0.1.2
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 -18
- 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
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
loadPlugins,
|
|
4
|
-
DuplicatePluginError,
|
|
5
|
-
PluginConfigError,
|
|
6
|
-
MissingAdapterImplementationError,
|
|
7
|
-
} from '../loader.js';
|
|
8
|
-
import type { PluginDefinition, DataAdapter } from '../types.js';
|
|
9
|
-
|
|
10
|
-
function makeAdapter(overrides?: Partial<DataAdapter>): DataAdapter {
|
|
11
|
-
return {
|
|
12
|
-
async get(_model: string, _id: string) {
|
|
13
|
-
return { id: '1' };
|
|
14
|
-
},
|
|
15
|
-
...overrides,
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
describe('loadPlugins', () => {
|
|
20
|
-
describe('basic boot', () => {
|
|
21
|
-
it('calls boot on each plugin', async () => {
|
|
22
|
-
const boot = vi.fn();
|
|
23
|
-
const plugin: PluginDefinition = {
|
|
24
|
-
name: 'test-plugin',
|
|
25
|
-
version: '1.0.0',
|
|
26
|
-
boot,
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
await loadPlugins({ plugins: [plugin] });
|
|
30
|
-
expect(boot).toHaveBeenCalledOnce();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('boots multiple plugins in order', async () => {
|
|
34
|
-
const order: string[] = [];
|
|
35
|
-
const pluginA: PluginDefinition = {
|
|
36
|
-
name: 'a',
|
|
37
|
-
version: '1.0.0',
|
|
38
|
-
boot() {
|
|
39
|
-
order.push('a');
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
const pluginB: PluginDefinition = {
|
|
43
|
-
name: 'b',
|
|
44
|
-
version: '1.0.0',
|
|
45
|
-
boot() {
|
|
46
|
-
order.push('b');
|
|
47
|
-
},
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
await loadPlugins({ plugins: [pluginA, pluginB] });
|
|
51
|
-
expect(order).toEqual(['a', 'b']);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('supports async boot functions', async () => {
|
|
55
|
-
const plugin: PluginDefinition = {
|
|
56
|
-
name: 'async-plugin',
|
|
57
|
-
version: '1.0.0',
|
|
58
|
-
async boot() {
|
|
59
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
await expect(loadPlugins({ plugins: [plugin] })).resolves.toBeDefined();
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe('config resolution', () => {
|
|
68
|
-
it('passes user config to boot context', async () => {
|
|
69
|
-
let receivedConfig: Record<string, unknown> = {};
|
|
70
|
-
const plugin: PluginDefinition = {
|
|
71
|
-
name: 'stripe',
|
|
72
|
-
version: '1.0.0',
|
|
73
|
-
config: {
|
|
74
|
-
secretKey: { type: 'string', required: true },
|
|
75
|
-
},
|
|
76
|
-
boot(ctx) {
|
|
77
|
-
receivedConfig = ctx.config;
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
await loadPlugins({
|
|
82
|
-
plugins: [plugin],
|
|
83
|
-
config: { stripe: { secretKey: 'sk_test_123' } },
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
expect(receivedConfig).toEqual({ secretKey: 'sk_test_123' });
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('applies default values when user config is missing', async () => {
|
|
90
|
-
let receivedConfig: Record<string, unknown> = {};
|
|
91
|
-
const plugin: PluginDefinition = {
|
|
92
|
-
name: 'stripe',
|
|
93
|
-
version: '1.0.0',
|
|
94
|
-
config: {
|
|
95
|
-
apiVersion: { type: 'string', default: '2024-12-18' },
|
|
96
|
-
},
|
|
97
|
-
boot(ctx) {
|
|
98
|
-
receivedConfig = ctx.config;
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
await loadPlugins({ plugins: [plugin] });
|
|
103
|
-
expect(receivedConfig).toEqual({ apiVersion: '2024-12-18' });
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('throws PluginConfigError when required field is missing', async () => {
|
|
107
|
-
const plugin: PluginDefinition = {
|
|
108
|
-
name: 'stripe',
|
|
109
|
-
version: '1.0.0',
|
|
110
|
-
config: {
|
|
111
|
-
secretKey: { type: 'string', required: true },
|
|
112
|
-
},
|
|
113
|
-
boot() {},
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
await expect(loadPlugins({ plugins: [plugin] })).rejects.toThrow(PluginConfigError);
|
|
117
|
-
await expect(loadPlugins({ plugins: [plugin] })).rejects.toThrow(
|
|
118
|
-
'Plugin "stripe" requires config field "secretKey"',
|
|
119
|
-
);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('allows extra config fields not in schema', async () => {
|
|
123
|
-
let receivedConfig: Record<string, unknown> = {};
|
|
124
|
-
const plugin: PluginDefinition = {
|
|
125
|
-
name: 'stripe',
|
|
126
|
-
version: '1.0.0',
|
|
127
|
-
config: {
|
|
128
|
-
secretKey: { type: 'string', required: true },
|
|
129
|
-
},
|
|
130
|
-
boot(ctx) {
|
|
131
|
-
receivedConfig = ctx.config;
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
await loadPlugins({
|
|
136
|
-
plugins: [plugin],
|
|
137
|
-
config: { stripe: { secretKey: 'sk_test', extra: 'value' } },
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
expect(receivedConfig).toEqual({ secretKey: 'sk_test', extra: 'value' });
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
describe('adapter registration', () => {
|
|
145
|
-
it('provides adapter implement function in boot context', async () => {
|
|
146
|
-
const adapter = makeAdapter();
|
|
147
|
-
const plugin: PluginDefinition = {
|
|
148
|
-
name: 'stripe',
|
|
149
|
-
version: '1.0.0',
|
|
150
|
-
provides: {
|
|
151
|
-
adapters: [{ name: 'stripe', capabilities: ['read', 'list'] }],
|
|
152
|
-
},
|
|
153
|
-
boot(ctx) {
|
|
154
|
-
ctx.adapters.stripe.implement(adapter);
|
|
155
|
-
},
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
const { adapterRegistry } = await loadPlugins({ plugins: [plugin] });
|
|
159
|
-
expect(adapterRegistry.has('stripe')).toBe(true);
|
|
160
|
-
expect(adapterRegistry.get('stripe')).toBe(adapter);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('supports multiple adapters from one plugin', async () => {
|
|
164
|
-
const plugin: PluginDefinition = {
|
|
165
|
-
name: 'multi',
|
|
166
|
-
version: '1.0.0',
|
|
167
|
-
provides: {
|
|
168
|
-
adapters: [
|
|
169
|
-
{ name: 'stripe', capabilities: ['read'] },
|
|
170
|
-
{ name: 'hubspot', capabilities: ['read', 'list'] },
|
|
171
|
-
],
|
|
172
|
-
},
|
|
173
|
-
boot(ctx) {
|
|
174
|
-
ctx.adapters.stripe.implement(makeAdapter());
|
|
175
|
-
ctx.adapters.hubspot.implement(makeAdapter());
|
|
176
|
-
},
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
const { adapterRegistry } = await loadPlugins({ plugins: [plugin] });
|
|
180
|
-
expect(adapterRegistry.has('stripe')).toBe(true);
|
|
181
|
-
expect(adapterRegistry.has('hubspot')).toBe(true);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('throws MissingAdapterImplementationError when adapter not implemented', async () => {
|
|
185
|
-
const plugin: PluginDefinition = {
|
|
186
|
-
name: 'stripe',
|
|
187
|
-
version: '1.0.0',
|
|
188
|
-
provides: {
|
|
189
|
-
adapters: [{ name: 'stripe', capabilities: ['read'] }],
|
|
190
|
-
},
|
|
191
|
-
boot() {
|
|
192
|
-
// does not call ctx.adapters.stripe.implement()
|
|
193
|
-
},
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
await expect(loadPlugins({ plugins: [plugin] })).rejects.toThrow(
|
|
197
|
-
MissingAdapterImplementationError,
|
|
198
|
-
);
|
|
199
|
-
await expect(loadPlugins({ plugins: [plugin] })).rejects.toThrow(
|
|
200
|
-
'Plugin "stripe" declares adapter "stripe" in provides but did not implement it during boot',
|
|
201
|
-
);
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
describe('lifecycle hooks', () => {
|
|
206
|
-
it('registers lifecycle handlers via ctx.on', async () => {
|
|
207
|
-
const handler = vi.fn().mockResolvedValue(undefined);
|
|
208
|
-
const plugin: PluginDefinition = {
|
|
209
|
-
name: 'test',
|
|
210
|
-
version: '1.0.0',
|
|
211
|
-
boot(ctx) {
|
|
212
|
-
ctx.on('afterBoot', handler);
|
|
213
|
-
},
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
const { lifecycleManager } = await loadPlugins({ plugins: [plugin] });
|
|
217
|
-
expect(lifecycleManager.getHandlers('afterBoot')).toContain(handler);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it('supports multiple lifecycle events from one plugin', async () => {
|
|
221
|
-
const beforeHandler = vi.fn().mockResolvedValue(undefined);
|
|
222
|
-
const afterHandler = vi.fn().mockResolvedValue(undefined);
|
|
223
|
-
const plugin: PluginDefinition = {
|
|
224
|
-
name: 'test',
|
|
225
|
-
version: '1.0.0',
|
|
226
|
-
boot(ctx) {
|
|
227
|
-
ctx.on('beforeBoot', beforeHandler);
|
|
228
|
-
ctx.on('afterBoot', afterHandler);
|
|
229
|
-
},
|
|
230
|
-
};
|
|
231
|
-
|
|
232
|
-
const { lifecycleManager } = await loadPlugins({ plugins: [plugin] });
|
|
233
|
-
expect(lifecycleManager.getHandlers('beforeBoot')).toContain(beforeHandler);
|
|
234
|
-
expect(lifecycleManager.getHandlers('afterBoot')).toContain(afterHandler);
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
describe('duplicate detection', () => {
|
|
239
|
-
it('throws DuplicatePluginError for same plugin name', async () => {
|
|
240
|
-
const pluginA: PluginDefinition = {
|
|
241
|
-
name: 'stripe',
|
|
242
|
-
version: '1.0.0',
|
|
243
|
-
boot() {},
|
|
244
|
-
};
|
|
245
|
-
const pluginB: PluginDefinition = {
|
|
246
|
-
name: 'stripe',
|
|
247
|
-
version: '2.0.0',
|
|
248
|
-
boot() {},
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
await expect(loadPlugins({ plugins: [pluginA, pluginB] })).rejects.toThrow(
|
|
252
|
-
DuplicatePluginError,
|
|
253
|
-
);
|
|
254
|
-
await expect(loadPlugins({ plugins: [pluginA, pluginB] })).rejects.toThrow(
|
|
255
|
-
'Plugin "stripe" is already registered',
|
|
256
|
-
);
|
|
257
|
-
});
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
describe('return value', () => {
|
|
261
|
-
it('returns adapterRegistry and lifecycleManager', async () => {
|
|
262
|
-
const plugin: PluginDefinition = {
|
|
263
|
-
name: 'test',
|
|
264
|
-
version: '1.0.0',
|
|
265
|
-
boot() {},
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
const result = await loadPlugins({ plugins: [plugin] });
|
|
269
|
-
expect(result).toHaveProperty('adapterRegistry');
|
|
270
|
-
expect(result).toHaveProperty('lifecycleManager');
|
|
271
|
-
});
|
|
272
|
-
});
|
|
273
|
-
});
|
|
@@ -1,275 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { validatePluginSetup } from '../validator.js';
|
|
3
|
-
import { AdapterRegistry } from '../adapter-registry.js';
|
|
4
|
-
import type { PluginDefinition, DataAdapter, AdapterCapability } from '../types.js';
|
|
5
|
-
import type { ResolvedModel } from '../../schema/types.js';
|
|
6
|
-
|
|
7
|
-
function makeAdapter(overrides?: Partial<DataAdapter>): DataAdapter {
|
|
8
|
-
return {
|
|
9
|
-
async get(_model, _id) {
|
|
10
|
-
return { id: '1' };
|
|
11
|
-
},
|
|
12
|
-
...overrides,
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function makeExternalModel(name: string, source: string): ResolvedModel {
|
|
17
|
-
return {
|
|
18
|
-
qualifiedName: `billing.${name}`,
|
|
19
|
-
app: 'billing-app',
|
|
20
|
-
module: 'billing',
|
|
21
|
-
name,
|
|
22
|
-
auditLog: false,
|
|
23
|
-
traits: [],
|
|
24
|
-
fields: [],
|
|
25
|
-
indexes: [],
|
|
26
|
-
source,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function makePlugin(name: string, adapters?: string[]): PluginDefinition {
|
|
31
|
-
return {
|
|
32
|
-
name,
|
|
33
|
-
version: '1.0.0',
|
|
34
|
-
provides: adapters
|
|
35
|
-
? { adapters: adapters.map((a) => ({ name: a, capabilities: ['read'] as const })) }
|
|
36
|
-
: undefined,
|
|
37
|
-
boot() {},
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
describe('validatePluginSetup', () => {
|
|
42
|
-
describe('valid setup', () => {
|
|
43
|
-
it('returns valid with no plugins and no external models', () => {
|
|
44
|
-
const registry = new AdapterRegistry();
|
|
45
|
-
const result = validatePluginSetup(registry, [], []);
|
|
46
|
-
expect(result.valid).toBe(true);
|
|
47
|
-
expect(result.errors).toEqual([]);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('returns valid when all adapters are registered and sources resolved', () => {
|
|
51
|
-
const registry = new AdapterRegistry();
|
|
52
|
-
registry.register('stripe', makeAdapter());
|
|
53
|
-
|
|
54
|
-
const models = [makeExternalModel('Customer', 'stripe')];
|
|
55
|
-
const plugins = [makePlugin('stripe', ['stripe'])];
|
|
56
|
-
|
|
57
|
-
const result = validatePluginSetup(registry, models, plugins);
|
|
58
|
-
expect(result.valid).toBe(true);
|
|
59
|
-
expect(result.errors).toEqual([]);
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
describe('duplicate plugin detection', () => {
|
|
64
|
-
it('detects duplicate plugin names', () => {
|
|
65
|
-
const registry = new AdapterRegistry();
|
|
66
|
-
const plugins = [makePlugin('stripe'), makePlugin('stripe')];
|
|
67
|
-
|
|
68
|
-
const result = validatePluginSetup(registry, [], plugins);
|
|
69
|
-
expect(result.valid).toBe(false);
|
|
70
|
-
expect(result.errors).toHaveLength(1);
|
|
71
|
-
expect(result.errors[0].type).toBe('DUPLICATE_PLUGIN');
|
|
72
|
-
expect(result.errors[0].message).toContain('stripe');
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('allows different plugin names', () => {
|
|
76
|
-
const registry = new AdapterRegistry();
|
|
77
|
-
const plugins = [makePlugin('stripe'), makePlugin('hubspot')];
|
|
78
|
-
|
|
79
|
-
const result = validatePluginSetup(registry, [], plugins);
|
|
80
|
-
expect(result.valid).toBe(true);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe('missing adapter implementation', () => {
|
|
85
|
-
it('detects when declared adapter is not registered', () => {
|
|
86
|
-
const registry = new AdapterRegistry();
|
|
87
|
-
const plugins = [makePlugin('stripe', ['stripe'])];
|
|
88
|
-
|
|
89
|
-
const result = validatePluginSetup(registry, [], plugins);
|
|
90
|
-
expect(result.valid).toBe(false);
|
|
91
|
-
expect(result.errors).toHaveLength(1);
|
|
92
|
-
expect(result.errors[0].type).toBe('MISSING_ADAPTER_IMPL');
|
|
93
|
-
expect(result.errors[0].plugin).toBe('stripe');
|
|
94
|
-
expect(result.errors[0].message).toContain('stripe');
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('passes when declared adapter is registered', () => {
|
|
98
|
-
const registry = new AdapterRegistry();
|
|
99
|
-
registry.register('stripe', makeAdapter());
|
|
100
|
-
const plugins = [makePlugin('stripe', ['stripe'])];
|
|
101
|
-
|
|
102
|
-
const result = validatePluginSetup(registry, [], plugins);
|
|
103
|
-
expect(result.valid).toBe(true);
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
describe('unresolved external model source', () => {
|
|
108
|
-
it('detects when external model references unregistered adapter', () => {
|
|
109
|
-
const registry = new AdapterRegistry();
|
|
110
|
-
const models = [makeExternalModel('Customer', 'stripe')];
|
|
111
|
-
|
|
112
|
-
const result = validatePluginSetup(registry, models, []);
|
|
113
|
-
expect(result.valid).toBe(false);
|
|
114
|
-
expect(result.errors).toHaveLength(1);
|
|
115
|
-
expect(result.errors[0].type).toBe('UNRESOLVED_SOURCE');
|
|
116
|
-
expect(result.errors[0].model).toBe('billing.Customer');
|
|
117
|
-
expect(result.errors[0].message).toContain('stripe');
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('passes when source adapter is registered', () => {
|
|
121
|
-
const registry = new AdapterRegistry();
|
|
122
|
-
registry.register('stripe', makeAdapter());
|
|
123
|
-
const models = [makeExternalModel('Customer', 'stripe')];
|
|
124
|
-
|
|
125
|
-
const result = validatePluginSetup(registry, models, []);
|
|
126
|
-
expect(result.valid).toBe(true);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('skips models without source', () => {
|
|
130
|
-
const registry = new AdapterRegistry();
|
|
131
|
-
const internalModel: ResolvedModel = {
|
|
132
|
-
qualifiedName: 'sales.Order',
|
|
133
|
-
app: 'sales-app',
|
|
134
|
-
module: 'sales',
|
|
135
|
-
name: 'Order',
|
|
136
|
-
auditLog: false,
|
|
137
|
-
traits: [],
|
|
138
|
-
fields: [],
|
|
139
|
-
indexes: [],
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
const result = validatePluginSetup(registry, [internalModel], []);
|
|
143
|
-
expect(result.valid).toBe(true);
|
|
144
|
-
});
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
describe('batchGet warnings', () => {
|
|
148
|
-
it('warns when adapter lacks batchGet', () => {
|
|
149
|
-
const registry = new AdapterRegistry();
|
|
150
|
-
registry.register('stripe', makeAdapter());
|
|
151
|
-
const models = [makeExternalModel('Customer', 'stripe')];
|
|
152
|
-
|
|
153
|
-
const result = validatePluginSetup(registry, models, []);
|
|
154
|
-
expect(result.warnings).toHaveLength(1);
|
|
155
|
-
expect(result.warnings[0]).toContain('batchGet');
|
|
156
|
-
expect(result.warnings[0]).toContain('N+1');
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('no warning when adapter has batchGet', () => {
|
|
160
|
-
const registry = new AdapterRegistry();
|
|
161
|
-
registry.register(
|
|
162
|
-
'stripe',
|
|
163
|
-
makeAdapter({
|
|
164
|
-
async batchGet(_model, ids) {
|
|
165
|
-
return ids.map((id) => ({ id }));
|
|
166
|
-
},
|
|
167
|
-
}),
|
|
168
|
-
);
|
|
169
|
-
const models = [makeExternalModel('Customer', 'stripe')];
|
|
170
|
-
|
|
171
|
-
const result = validatePluginSetup(registry, models, []);
|
|
172
|
-
expect(result.warnings).toHaveLength(0);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
it('skips warning when adapter is not registered', () => {
|
|
176
|
-
const registry = new AdapterRegistry();
|
|
177
|
-
const models = [makeExternalModel('Customer', 'missing')];
|
|
178
|
-
|
|
179
|
-
const result = validatePluginSetup(registry, models, []);
|
|
180
|
-
expect(result.warnings).toHaveLength(0);
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
describe('aggregated errors', () => {
|
|
185
|
-
it('collects multiple errors from different checks', () => {
|
|
186
|
-
const registry = new AdapterRegistry();
|
|
187
|
-
const plugins = [makePlugin('stripe', ['stripe']), makePlugin('stripe', ['hubspot'])];
|
|
188
|
-
const models = [makeExternalModel('Customer', 'missing')];
|
|
189
|
-
|
|
190
|
-
const result = validatePluginSetup(registry, models, plugins);
|
|
191
|
-
expect(result.valid).toBe(false);
|
|
192
|
-
expect(result.errors.length).toBeGreaterThan(1);
|
|
193
|
-
|
|
194
|
-
const types = result.errors.map((e) => e.type);
|
|
195
|
-
expect(types).toContain('DUPLICATE_PLUGIN');
|
|
196
|
-
expect(types).toContain('MISSING_ADAPTER_IMPL');
|
|
197
|
-
expect(types).toContain('UNRESOLVED_SOURCE');
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
describe('capability violations', () => {
|
|
202
|
-
it('errors when adapter lacks required read capability', () => {
|
|
203
|
-
const registry = new AdapterRegistry();
|
|
204
|
-
registry.register('stripe', makeAdapter());
|
|
205
|
-
const models = [makeExternalModel('Customer', 'stripe')];
|
|
206
|
-
const capabilities: Record<string, AdapterCapability[]> = { stripe: ['list'] };
|
|
207
|
-
|
|
208
|
-
const result = validatePluginSetup(registry, models, [], capabilities);
|
|
209
|
-
expect(result.valid).toBe(false);
|
|
210
|
-
expect(result.errors).toContainEqual(
|
|
211
|
-
expect.objectContaining({
|
|
212
|
-
type: 'CAPABILITY_VIOLATION',
|
|
213
|
-
model: 'billing.Customer',
|
|
214
|
-
message: expect.stringContaining('read'),
|
|
215
|
-
}),
|
|
216
|
-
);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
it('errors when adapter lacks list capability', () => {
|
|
220
|
-
const registry = new AdapterRegistry();
|
|
221
|
-
registry.register('stripe', makeAdapter());
|
|
222
|
-
const models = [makeExternalModel('Customer', 'stripe')];
|
|
223
|
-
const capabilities: Record<string, AdapterCapability[]> = { stripe: ['read'] };
|
|
224
|
-
|
|
225
|
-
const result = validatePluginSetup(registry, models, [], capabilities);
|
|
226
|
-
expect(result.valid).toBe(false);
|
|
227
|
-
expect(result.errors).toContainEqual(
|
|
228
|
-
expect.objectContaining({
|
|
229
|
-
type: 'CAPABILITY_VIOLATION',
|
|
230
|
-
message: expect.stringContaining('list'),
|
|
231
|
-
}),
|
|
232
|
-
);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it('passes when adapter has read and list', () => {
|
|
236
|
-
const registry = new AdapterRegistry();
|
|
237
|
-
registry.register('stripe', makeAdapter());
|
|
238
|
-
const models = [makeExternalModel('Customer', 'stripe')];
|
|
239
|
-
const capabilities: Record<string, AdapterCapability[]> = { stripe: ['read', 'list'] };
|
|
240
|
-
|
|
241
|
-
const result = validatePluginSetup(registry, models, [], capabilities);
|
|
242
|
-
const capErrors = result.errors.filter((e) => e.type === 'CAPABILITY_VIOLATION');
|
|
243
|
-
expect(capErrors).toHaveLength(0);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('skips check when no capabilities declared for adapter', () => {
|
|
247
|
-
const registry = new AdapterRegistry();
|
|
248
|
-
registry.register('stripe', makeAdapter());
|
|
249
|
-
const models = [makeExternalModel('Customer', 'stripe')];
|
|
250
|
-
|
|
251
|
-
const result = validatePluginSetup(registry, models, [], {});
|
|
252
|
-
const capErrors = result.errors.filter((e) => e.type === 'CAPABILITY_VIOLATION');
|
|
253
|
-
expect(capErrors).toHaveLength(0);
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it('skips internal models', () => {
|
|
257
|
-
const registry = new AdapterRegistry();
|
|
258
|
-
const internalModel: ResolvedModel = {
|
|
259
|
-
qualifiedName: 'sales.Order',
|
|
260
|
-
app: 'test',
|
|
261
|
-
module: 'sales',
|
|
262
|
-
name: 'Order',
|
|
263
|
-
auditLog: false,
|
|
264
|
-
traits: [],
|
|
265
|
-
fields: [],
|
|
266
|
-
indexes: [],
|
|
267
|
-
};
|
|
268
|
-
const capabilities: Record<string, AdapterCapability[]> = { stripe: ['read'] };
|
|
269
|
-
|
|
270
|
-
const result = validatePluginSetup(registry, [internalModel], [], capabilities);
|
|
271
|
-
const capErrors = result.errors.filter((e) => e.type === 'CAPABILITY_VIOLATION');
|
|
272
|
-
expect(capErrors).toHaveLength(0);
|
|
273
|
-
});
|
|
274
|
-
});
|
|
275
|
-
});
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { DataAdapter } from './types.js';
|
|
2
|
-
|
|
3
|
-
export class AdapterNotFoundError extends Error {
|
|
4
|
-
constructor(public readonly adapterName: string) {
|
|
5
|
-
super(`Adapter "${adapterName}" is not registered`);
|
|
6
|
-
this.name = 'AdapterNotFoundError';
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export class DuplicateAdapterError extends Error {
|
|
11
|
-
constructor(public readonly adapterName: string) {
|
|
12
|
-
super(`Adapter "${adapterName}" is already registered`);
|
|
13
|
-
this.name = 'DuplicateAdapterError';
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export class AdapterRegistry {
|
|
18
|
-
private adapters = new Map<string, DataAdapter>();
|
|
19
|
-
|
|
20
|
-
register(name: string, adapter: DataAdapter): void {
|
|
21
|
-
if (this.adapters.has(name)) {
|
|
22
|
-
throw new DuplicateAdapterError(name);
|
|
23
|
-
}
|
|
24
|
-
this.adapters.set(name, adapter);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
has(name: string): boolean {
|
|
28
|
-
return this.adapters.has(name);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
get(name: string): DataAdapter {
|
|
32
|
-
const adapter = this.adapters.get(name);
|
|
33
|
-
if (!adapter) {
|
|
34
|
-
throw new AdapterNotFoundError(name);
|
|
35
|
-
}
|
|
36
|
-
return adapter;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
getAll(): Array<{ name: string; adapter: DataAdapter }> {
|
|
40
|
-
return [...this.adapters.entries()].map(([name, adapter]) => ({ name, adapter }));
|
|
41
|
-
}
|
|
42
|
-
}
|
package/src/plugins/define.ts
DELETED
package/src/plugins/index.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
export { definePlugin } from './define.js';
|
|
2
|
-
export {
|
|
3
|
-
AdapterRegistry,
|
|
4
|
-
AdapterNotFoundError,
|
|
5
|
-
DuplicateAdapterError,
|
|
6
|
-
} from './adapter-registry.js';
|
|
7
|
-
export { PluginLifecycleManager } from './lifecycle.js';
|
|
8
|
-
export {
|
|
9
|
-
loadPlugins,
|
|
10
|
-
DuplicatePluginError,
|
|
11
|
-
PluginConfigError,
|
|
12
|
-
MissingAdapterImplementationError,
|
|
13
|
-
} from './loader.js';
|
|
14
|
-
export { validatePluginSetup } from './validator.js';
|
|
15
|
-
export type {
|
|
16
|
-
DataAdapter,
|
|
17
|
-
AdapterCapability,
|
|
18
|
-
PluginDefinition,
|
|
19
|
-
PluginBootContext,
|
|
20
|
-
PluginLifecycleEvent,
|
|
21
|
-
PluginProvides,
|
|
22
|
-
PluginConfigField,
|
|
23
|
-
ListQuery,
|
|
24
|
-
ListResult,
|
|
25
|
-
FilterExpression,
|
|
26
|
-
LifecycleHandler,
|
|
27
|
-
} from './types.js';
|
|
28
|
-
export type { ValidationResult, PluginValidationError } from './validator.js';
|
package/src/plugins/lifecycle.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { PluginLifecycleEvent, LifecycleHandler } from './types.js';
|
|
2
|
-
|
|
3
|
-
export class PluginLifecycleManager {
|
|
4
|
-
private handlers: Map<PluginLifecycleEvent, LifecycleHandler[]>;
|
|
5
|
-
|
|
6
|
-
constructor(handlers?: Map<PluginLifecycleEvent, LifecycleHandler[]>) {
|
|
7
|
-
this.handlers = handlers ?? new Map();
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
register(event: PluginLifecycleEvent, handler: LifecycleHandler): void {
|
|
11
|
-
const list = this.handlers.get(event) ?? [];
|
|
12
|
-
list.push(handler);
|
|
13
|
-
this.handlers.set(event, list);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async emit(event: PluginLifecycleEvent, ...args: unknown[]): Promise<void> {
|
|
17
|
-
const list = this.handlers.get(event);
|
|
18
|
-
if (!list) return;
|
|
19
|
-
for (const handler of list) {
|
|
20
|
-
await handler(...args);
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
getHandlers(event: PluginLifecycleEvent): LifecycleHandler[] {
|
|
25
|
-
return this.handlers.get(event) ?? [];
|
|
26
|
-
}
|
|
27
|
-
}
|