@objectql/core 1.1.0 → 1.2.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/CHANGELOG.md +4 -10
- package/dist/index.d.ts +14 -11
- package/dist/index.js +222 -34
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +23 -5
- package/dist/loader.js +201 -9
- package/dist/loader.js.map +1 -1
- package/dist/repository.d.ts +3 -5
- package/dist/repository.js +107 -112
- package/dist/repository.js.map +1 -1
- package/jest.config.js +3 -0
- package/package.json +10 -7
- package/src/index.ts +261 -41
- package/src/loader.ts +194 -10
- package/src/repository.ts +123 -127
- package/test/action.test.ts +58 -0
- package/test/hook.test.ts +60 -0
- package/test/utils.ts +54 -0
- package/tsconfig.json +4 -3
- package/tsconfig.tsbuildinfo +1 -1
- package/README.md +0 -53
- package/dist/driver.d.ts +0 -17
- package/dist/driver.js +0 -3
- package/dist/driver.js.map +0 -1
- package/dist/metadata.d.ts +0 -104
- package/dist/metadata.js +0 -3
- package/dist/metadata.js.map +0 -1
- package/dist/query.d.ts +0 -10
- package/dist/query.js +0 -3
- package/dist/query.js.map +0 -1
- package/dist/registry.d.ts +0 -4
- package/dist/registry.js +0 -8
- package/dist/registry.js.map +0 -1
- package/dist/types.d.ts +0 -83
- package/dist/types.js +0 -6
- package/dist/types.js.map +0 -1
- package/src/driver.ts +0 -24
- package/src/metadata.ts +0 -143
- package/src/query.ts +0 -11
- package/src/registry.ts +0 -6
- package/src/types.ts +0 -115
- package/test/dynamic.test.ts +0 -34
- package/test/fixtures/project.action.ts +0 -6
- package/test/fixtures/project.object.yml +0 -41
- package/test/loader.test.ts +0 -22
- package/test/metadata.test.ts +0 -49
- package/test/mock-driver.ts +0 -86
- package/test/repository.test.ts +0 -150
package/src/index.ts
CHANGED
|
@@ -1,49 +1,196 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import {
|
|
2
|
+
ObjectRegistry,
|
|
3
|
+
Driver,
|
|
4
|
+
ObjectConfig,
|
|
5
|
+
ObjectQLContext,
|
|
6
|
+
ObjectQLContextOptions,
|
|
7
|
+
IObjectQL,
|
|
8
|
+
ObjectQLConfig,
|
|
9
|
+
ObjectQLPlugin,
|
|
10
|
+
HookName,
|
|
11
|
+
HookHandler,
|
|
12
|
+
HookContext,
|
|
13
|
+
ActionHandler,
|
|
14
|
+
ActionContext
|
|
15
|
+
} from '@objectql/types';
|
|
16
|
+
import { ObjectLoader } from './loader';
|
|
9
17
|
export * from './loader';
|
|
10
|
-
|
|
11
|
-
import { ObjectConfig } from './metadata';
|
|
12
|
-
import { ObjectQLContext, ObjectQLContextOptions, IObjectQL, ObjectQLConfig } from './types';
|
|
13
18
|
import { ObjectRepository } from './repository';
|
|
14
|
-
import { Driver } from './driver';
|
|
15
|
-
import { MetadataLoader } from './loader';
|
|
16
|
-
import { MetadataRegistry } from './registry';
|
|
17
19
|
|
|
18
20
|
export class ObjectQL implements IObjectQL {
|
|
19
|
-
public metadata:
|
|
20
|
-
private loader:
|
|
21
|
+
public metadata: ObjectRegistry;
|
|
22
|
+
private loader: ObjectLoader;
|
|
21
23
|
private datasources: Record<string, Driver> = {};
|
|
24
|
+
private hooks: Record<string, Array<{ objectName: string, handler: HookHandler, packageName?: string }>> = {};
|
|
25
|
+
private actions: Record<string, { handler: ActionHandler, packageName?: string }> = {};
|
|
26
|
+
private pluginsList: ObjectQLPlugin[] = [];
|
|
22
27
|
|
|
23
28
|
constructor(config: ObjectQLConfig) {
|
|
24
|
-
this.metadata = config.registry || new
|
|
25
|
-
this.loader = new
|
|
26
|
-
this.datasources = config.datasources;
|
|
29
|
+
this.metadata = config.registry || new ObjectRegistry();
|
|
30
|
+
this.loader = new ObjectLoader(this.metadata);
|
|
31
|
+
this.datasources = config.datasources || {};
|
|
32
|
+
|
|
33
|
+
if (config.connection) {
|
|
34
|
+
this.loadDriverFromConnection(config.connection);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 1. Load Presets/Packages first (Base Layer)
|
|
38
|
+
if (config.packages) {
|
|
39
|
+
for (const name of config.packages) {
|
|
40
|
+
this.addPackage(name);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (config.presets) {
|
|
44
|
+
for (const name of config.presets) {
|
|
45
|
+
this.addPackage(name);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (config.plugins) {
|
|
50
|
+
for (const plugin of config.plugins) {
|
|
51
|
+
if (typeof plugin === 'string') {
|
|
52
|
+
this.loadPluginFromPackage(plugin);
|
|
53
|
+
} else {
|
|
54
|
+
this.use(plugin);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
27
58
|
|
|
59
|
+
// 2. Load Local Sources (Application Layer - can override presets)
|
|
60
|
+
if (config.source) {
|
|
61
|
+
const sources = Array.isArray(config.source) ? config.source : [config.source];
|
|
62
|
+
for (const src of sources) {
|
|
63
|
+
this.loader.load(src);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 3. Load In-Memory Objects (Dynamic Layer - highest priority)
|
|
28
68
|
if (config.objects) {
|
|
29
69
|
for (const [key, obj] of Object.entries(config.objects)) {
|
|
30
70
|
this.registerObject(obj);
|
|
31
71
|
}
|
|
32
72
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private loadPluginFromPackage(packageName: string) {
|
|
76
|
+
let mod: any;
|
|
77
|
+
try {
|
|
78
|
+
const modulePath = require.resolve(packageName, { paths: [process.cwd()] });
|
|
79
|
+
mod = require(modulePath);
|
|
80
|
+
} catch (e) {
|
|
81
|
+
throw new Error(`Failed to resolve plugin '${packageName}': ${e}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Helper to find plugin instance
|
|
85
|
+
const findPlugin = (candidate: any): ObjectQLPlugin | undefined => {
|
|
86
|
+
if (!candidate) return undefined;
|
|
87
|
+
|
|
88
|
+
// 1. Try treating as Class
|
|
89
|
+
if (typeof candidate === 'function') {
|
|
90
|
+
try {
|
|
91
|
+
const inst = new candidate();
|
|
92
|
+
if (inst && typeof inst.setup === 'function') {
|
|
93
|
+
return inst; // Found it!
|
|
94
|
+
}
|
|
95
|
+
} catch (e) {
|
|
96
|
+
// Not a constructor or instantiation failed
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 2. Try treating as Instance
|
|
101
|
+
if (candidate && typeof candidate.setup === 'function') {
|
|
102
|
+
if (candidate.name) return candidate;
|
|
103
|
+
}
|
|
104
|
+
return undefined;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Search in default, module root, and all named exports
|
|
108
|
+
let instance = findPlugin(mod.default) || findPlugin(mod);
|
|
109
|
+
|
|
110
|
+
if (!instance && mod && typeof mod === 'object') {
|
|
111
|
+
for (const key of Object.keys(mod)) {
|
|
112
|
+
if (key === 'default') continue;
|
|
113
|
+
instance = findPlugin(mod[key]);
|
|
114
|
+
if (instance) break;
|
|
36
115
|
}
|
|
37
116
|
}
|
|
117
|
+
|
|
118
|
+
if (instance) {
|
|
119
|
+
(instance as any)._packageName = packageName;
|
|
120
|
+
this.use(instance);
|
|
121
|
+
} else {
|
|
122
|
+
console.error(`[PluginLoader] Failed to find ObjectQLPlugin in '${packageName}'. Exports:`, Object.keys(mod));
|
|
123
|
+
throw new Error(`Plugin '${packageName}' must export a class or object implementing ObjectQLPlugin.`);
|
|
124
|
+
}
|
|
38
125
|
}
|
|
39
126
|
|
|
40
127
|
addPackage(name: string) {
|
|
41
128
|
this.loader.loadPackage(name);
|
|
42
129
|
}
|
|
43
130
|
|
|
131
|
+
use(plugin: ObjectQLPlugin) {
|
|
132
|
+
this.pluginsList.push(plugin);
|
|
133
|
+
}
|
|
44
134
|
|
|
45
135
|
removePackage(name: string) {
|
|
46
136
|
this.metadata.unregisterPackage(name);
|
|
137
|
+
|
|
138
|
+
// Remove hooks
|
|
139
|
+
for (const event of Object.keys(this.hooks)) {
|
|
140
|
+
this.hooks[event] = this.hooks[event].filter(h => h.packageName !== name);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Remove actions
|
|
144
|
+
for (const key of Object.keys(this.actions)) {
|
|
145
|
+
if (this.actions[key].packageName === name) {
|
|
146
|
+
delete this.actions[key];
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
on(event: HookName, objectName: string, handler: HookHandler, packageName?: string) {
|
|
152
|
+
if (!this.hooks[event]) {
|
|
153
|
+
this.hooks[event] = [];
|
|
154
|
+
}
|
|
155
|
+
this.hooks[event].push({ objectName, handler, packageName });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async triggerHook(event: HookName, objectName: string, ctx: HookContext) {
|
|
159
|
+
// 1. Registry Hooks (File-based)
|
|
160
|
+
const fileHooks = this.metadata.get<any>('hook', objectName);
|
|
161
|
+
if (fileHooks && typeof fileHooks[event] === 'function') {
|
|
162
|
+
await fileHooks[event](ctx);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 2. Programmatic Hooks
|
|
166
|
+
const hooks = this.hooks[event] || [];
|
|
167
|
+
for (const hook of hooks) {
|
|
168
|
+
if (hook.objectName === '*' || hook.objectName === objectName) {
|
|
169
|
+
await hook.handler(ctx);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
registerAction(objectName: string, actionName: string, handler: ActionHandler, packageName?: string) {
|
|
175
|
+
const key = `${objectName}:${actionName}`;
|
|
176
|
+
this.actions[key] = { handler, packageName };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async executeAction(objectName: string, actionName: string, ctx: ActionContext) {
|
|
180
|
+
// 1. Programmatic
|
|
181
|
+
const key = `${objectName}:${actionName}`;
|
|
182
|
+
const actionEntry = this.actions[key];
|
|
183
|
+
if (actionEntry) {
|
|
184
|
+
return await actionEntry.handler(ctx);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 2. Registry (File-based)
|
|
188
|
+
const fileActions = this.metadata.get<any>('action', objectName);
|
|
189
|
+
if (fileActions && typeof fileActions[actionName] === 'function') {
|
|
190
|
+
return await fileActions[actionName](ctx);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
throw new Error(`Action '${actionName}' not found for object '${objectName}'`);
|
|
47
194
|
}
|
|
48
195
|
|
|
49
196
|
loadFromDirectory(dir: string, packageName?: string) {
|
|
@@ -56,7 +203,6 @@ export class ObjectQL implements IObjectQL {
|
|
|
56
203
|
spaceId: options.spaceId,
|
|
57
204
|
roles: options.roles || [],
|
|
58
205
|
isSystem: options.isSystem,
|
|
59
|
-
ignoreTriggers: options.ignoreTriggers,
|
|
60
206
|
object: (name: string) => {
|
|
61
207
|
return new ObjectRepository(name, ctx, this);
|
|
62
208
|
},
|
|
@@ -111,6 +257,10 @@ export class ObjectQL implements IObjectQL {
|
|
|
111
257
|
});
|
|
112
258
|
}
|
|
113
259
|
|
|
260
|
+
unregisterObject(name: string) {
|
|
261
|
+
this.metadata.unregister('object', name);
|
|
262
|
+
}
|
|
263
|
+
|
|
114
264
|
getObject(name: string): ObjectConfig | undefined {
|
|
115
265
|
return this.metadata.get<ObjectConfig>('object', name);
|
|
116
266
|
}
|
|
@@ -133,27 +283,97 @@ export class ObjectQL implements IObjectQL {
|
|
|
133
283
|
}
|
|
134
284
|
|
|
135
285
|
async init() {
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
286
|
+
// 0. Init Plugins
|
|
287
|
+
for (const plugin of this.pluginsList) {
|
|
288
|
+
console.log(`Initializing plugin '${plugin.name}'...`);
|
|
289
|
+
|
|
290
|
+
let app: IObjectQL = this;
|
|
291
|
+
const pkgName = (plugin as any)._packageName;
|
|
292
|
+
|
|
293
|
+
if (pkgName) {
|
|
294
|
+
app = new Proxy(this, {
|
|
295
|
+
get(target, prop) {
|
|
296
|
+
if (prop === 'on') {
|
|
297
|
+
return (event: HookName, obj: string, handler: HookHandler) =>
|
|
298
|
+
target.on(event, obj, handler, pkgName);
|
|
299
|
+
}
|
|
300
|
+
if (prop === 'registerAction') {
|
|
301
|
+
return (obj: string, act: string, handler: ActionHandler) =>
|
|
302
|
+
target.registerAction(obj, act, handler, pkgName);
|
|
149
303
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
} catch (e) {
|
|
153
|
-
console.error(`Failed to insert init data for ${obj.name}:`, e);
|
|
304
|
+
const value = (target as any)[prop];
|
|
305
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
154
306
|
}
|
|
155
|
-
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
await plugin.setup(app);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const objects = this.metadata.list<ObjectConfig>('object');
|
|
314
|
+
|
|
315
|
+
// 1. Init Drivers (e.g. Sync Schema)
|
|
316
|
+
// Let's pass all objects to all configured drivers.
|
|
317
|
+
for (const [name, driver] of Object.entries(this.datasources)) {
|
|
318
|
+
if (driver.init) {
|
|
319
|
+
console.log(`Initializing driver '${name}'...`);
|
|
320
|
+
await driver.init(objects);
|
|
156
321
|
}
|
|
157
322
|
}
|
|
158
323
|
}
|
|
324
|
+
|
|
325
|
+
private loadDriverFromConnection(connection: string) {
|
|
326
|
+
let driverPackage = '';
|
|
327
|
+
let driverClass = '';
|
|
328
|
+
let driverConfig: any = {};
|
|
329
|
+
|
|
330
|
+
if (connection.startsWith('mongodb://')) {
|
|
331
|
+
driverPackage = '@objectql/driver-mongo';
|
|
332
|
+
driverClass = 'MongoDriver';
|
|
333
|
+
driverConfig = { url: connection };
|
|
334
|
+
}
|
|
335
|
+
else if (connection.startsWith('sqlite://')) {
|
|
336
|
+
driverPackage = '@objectql/driver-knex';
|
|
337
|
+
driverClass = 'KnexDriver';
|
|
338
|
+
const filename = connection.replace('sqlite://', '');
|
|
339
|
+
driverConfig = {
|
|
340
|
+
client: 'sqlite3',
|
|
341
|
+
connection: { filename },
|
|
342
|
+
useNullAsDefault: true
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
else if (connection.startsWith('postgres://') || connection.startsWith('postgresql://')) {
|
|
346
|
+
driverPackage = '@objectql/driver-knex';
|
|
347
|
+
driverClass = 'KnexDriver';
|
|
348
|
+
driverConfig = {
|
|
349
|
+
client: 'pg',
|
|
350
|
+
connection: connection
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
else if (connection.startsWith('mysql://')) {
|
|
354
|
+
driverPackage = '@objectql/driver-knex';
|
|
355
|
+
driverClass = 'KnexDriver';
|
|
356
|
+
driverConfig = {
|
|
357
|
+
client: 'mysql2',
|
|
358
|
+
connection: connection
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
throw new Error(`Unsupported connection protocol: ${connection}`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
367
|
+
const pkg = require(driverPackage);
|
|
368
|
+
const DriverClass = pkg[driverClass];
|
|
369
|
+
if (!DriverClass) {
|
|
370
|
+
throw new Error(`${driverClass} not found in ${driverPackage}`);
|
|
371
|
+
}
|
|
372
|
+
this.datasources['default'] = new DriverClass(driverConfig);
|
|
373
|
+
} catch (e: any) {
|
|
374
|
+
throw new Error(`Failed to load driver ${driverPackage}. Please install it: npm install ${driverPackage}. Error: ${e.message}`);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
159
377
|
}
|
|
378
|
+
|
|
379
|
+
export * from './repository';
|
package/src/loader.ts
CHANGED
|
@@ -1,17 +1,202 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as glob from 'fast-glob';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { ObjectRegistry, ObjectConfig } from '@objectql/types';
|
|
5
|
+
import * as yaml from 'js-yaml';
|
|
4
6
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
export interface LoaderHandlerContext {
|
|
8
|
+
file: string;
|
|
9
|
+
content: string;
|
|
10
|
+
registry: ObjectRegistry;
|
|
11
|
+
packageName?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type LoaderHandler = (ctx: LoaderHandlerContext) => void;
|
|
15
|
+
|
|
16
|
+
export interface LoaderPlugin {
|
|
17
|
+
name: string;
|
|
18
|
+
glob: string[];
|
|
19
|
+
handler: LoaderHandler;
|
|
20
|
+
options?: any;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class ObjectLoader {
|
|
24
|
+
private plugins: LoaderPlugin[] = [];
|
|
25
|
+
|
|
26
|
+
constructor(protected registry: ObjectRegistry) {
|
|
27
|
+
this.registerBuiltinPlugins();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private registerBuiltinPlugins() {
|
|
31
|
+
// Objects
|
|
32
|
+
this.use({
|
|
33
|
+
name: 'object',
|
|
34
|
+
glob: ['**/*.object.yml', '**/*.object.yaml'],
|
|
35
|
+
handler: (ctx) => {
|
|
36
|
+
try {
|
|
37
|
+
const doc = yaml.load(ctx.content) as any;
|
|
38
|
+
if (!doc) return;
|
|
39
|
+
|
|
40
|
+
if (doc.name && doc.fields) {
|
|
41
|
+
registerObject(ctx.registry, doc, ctx.file, ctx.packageName || ctx.registry.getEntry('package-map', ctx.file)?.package);
|
|
42
|
+
} else {
|
|
43
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
44
|
+
if (typeof value === 'object' && (value as any).fields) {
|
|
45
|
+
const obj = value as any;
|
|
46
|
+
if (!obj.name) obj.name = key;
|
|
47
|
+
registerObject(ctx.registry, obj, ctx.file, ctx.packageName);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.error(`Error loading object from ${ctx.file}:`, e);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Hooks
|
|
58
|
+
this.use({
|
|
59
|
+
name: 'hook',
|
|
60
|
+
glob: ['**/*.hook.ts', '**/*.hook.js'],
|
|
61
|
+
handler: (ctx) => {
|
|
62
|
+
const basename = path.basename(ctx.file);
|
|
63
|
+
// Extract object name from filename: user.hook.ts -> user
|
|
64
|
+
const objectName = basename.replace(/\.hook\.(ts|js)$/, '');
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const mod = require(ctx.file);
|
|
68
|
+
// Support default export or named exports
|
|
69
|
+
const hooks = mod.default || mod;
|
|
70
|
+
|
|
71
|
+
ctx.registry.register('hook', {
|
|
72
|
+
type: 'hook',
|
|
73
|
+
id: objectName, // Hook ID is the object name
|
|
74
|
+
path: ctx.file,
|
|
75
|
+
package: ctx.packageName,
|
|
76
|
+
content: hooks
|
|
77
|
+
});
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.error(`Error loading hook from ${ctx.file}:`, e);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Actions
|
|
85
|
+
this.use({
|
|
86
|
+
name: 'action',
|
|
87
|
+
glob: ['**/*.action.ts', '**/*.action.js'],
|
|
88
|
+
handler: (ctx) => {
|
|
89
|
+
const basename = path.basename(ctx.file);
|
|
90
|
+
// Extract object name: invoice.action.ts -> invoice
|
|
91
|
+
const objectName = basename.replace(/\.action\.(ts|js)$/, '');
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const mod = require(ctx.file);
|
|
95
|
+
// Action file exports multiple actions
|
|
96
|
+
// export const approve = { ... };
|
|
97
|
+
// export const reject = { ... };
|
|
98
|
+
|
|
99
|
+
const actions: Record<string, any> = {};
|
|
100
|
+
|
|
101
|
+
for (const [key, value] of Object.entries(mod)) {
|
|
102
|
+
if (key === 'default') continue;
|
|
103
|
+
if (typeof value === 'object' && (value as any).handler) {
|
|
104
|
+
actions[key] = value;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (Object.keys(actions).length > 0) {
|
|
109
|
+
ctx.registry.register('action', {
|
|
110
|
+
type: 'action',
|
|
111
|
+
id: objectName, // Action collection ID is the object name
|
|
112
|
+
path: ctx.file,
|
|
113
|
+
package: ctx.packageName,
|
|
114
|
+
content: actions
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
} catch (e) {
|
|
119
|
+
console.error(`Error loading action from ${ctx.file}:`, e);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
use(plugin: LoaderPlugin) {
|
|
126
|
+
this.plugins.push(plugin);
|
|
9
127
|
}
|
|
128
|
+
|
|
129
|
+
load(dir: string, packageName?: string) {
|
|
130
|
+
for (const plugin of this.plugins) {
|
|
131
|
+
this.runPlugin(plugin, dir, packageName);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
loadPackage(packageName: string) {
|
|
136
|
+
try {
|
|
137
|
+
const entryPath = require.resolve(packageName, { paths: [process.cwd()] });
|
|
138
|
+
// clean cache
|
|
139
|
+
delete require.cache[entryPath];
|
|
140
|
+
const packageDir = path.dirname(entryPath);
|
|
141
|
+
this.load(packageDir, packageName);
|
|
142
|
+
} catch (e) {
|
|
143
|
+
// fallback to directory
|
|
144
|
+
this.load(packageName, packageName);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private runPlugin(plugin: LoaderPlugin, dir: string, packageName?: string) {
|
|
149
|
+
const files = glob.sync(plugin.glob, {
|
|
150
|
+
cwd: dir,
|
|
151
|
+
absolute: true
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
for (const file of files) {
|
|
155
|
+
try {
|
|
156
|
+
const ctx: LoaderHandlerContext = {
|
|
157
|
+
file,
|
|
158
|
+
content: '',
|
|
159
|
+
registry: this.registry,
|
|
160
|
+
packageName
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Pre-read for convenience
|
|
164
|
+
if (!file.match(/\.(js|ts|node)$/)) {
|
|
165
|
+
ctx.content = fs.readFileSync(file, 'utf8');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
plugin.handler(ctx);
|
|
169
|
+
|
|
170
|
+
} catch (e) {
|
|
171
|
+
console.error(`Error in loader plugin '${plugin.name}' processing ${file}:`, e);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function registerObject(registry: ObjectRegistry, obj: any, file: string, packageName?: string) {
|
|
178
|
+
// Normalize fields
|
|
179
|
+
if (obj.fields) {
|
|
180
|
+
for (const [key, field] of Object.entries(obj.fields)) {
|
|
181
|
+
if (typeof field === 'object' && field !== null) {
|
|
182
|
+
if (!(field as any).name) {
|
|
183
|
+
(field as any).name = key;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
registry.register('object', {
|
|
189
|
+
type: 'object',
|
|
190
|
+
id: obj.name,
|
|
191
|
+
path: file,
|
|
192
|
+
package: packageName,
|
|
193
|
+
content: obj
|
|
194
|
+
});
|
|
10
195
|
}
|
|
11
196
|
|
|
12
197
|
export function loadObjectConfigs(dir: string): Record<string, ObjectConfig> {
|
|
13
|
-
const registry = new
|
|
14
|
-
const loader = new
|
|
198
|
+
const registry = new ObjectRegistry();
|
|
199
|
+
const loader = new ObjectLoader(registry);
|
|
15
200
|
loader.load(dir);
|
|
16
201
|
const result: Record<string, ObjectConfig> = {};
|
|
17
202
|
for (const obj of registry.list<ObjectConfig>('object')) {
|
|
@@ -19,4 +204,3 @@ export function loadObjectConfigs(dir: string): Record<string, ObjectConfig> {
|
|
|
19
204
|
}
|
|
20
205
|
return result;
|
|
21
206
|
}
|
|
22
|
-
|