@objectql/core 1.2.0 → 1.3.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 +15 -0
- package/dist/action.d.ts +7 -0
- package/dist/action.js +23 -0
- package/dist/action.js.map +1 -0
- package/dist/app.d.ts +28 -0
- package/dist/app.js +211 -0
- package/dist/app.js.map +1 -0
- package/dist/driver.d.ts +2 -0
- package/dist/driver.js +55 -0
- package/dist/driver.js.map +1 -0
- package/dist/hook.d.ts +8 -0
- package/dist/hook.js +25 -0
- package/dist/hook.js.map +1 -0
- package/dist/index.d.ts +7 -27
- package/dist/index.js +7 -328
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +3 -16
- package/dist/loader.js +9 -4
- package/dist/loader.js.map +1 -1
- package/dist/object.d.ts +3 -0
- package/dist/object.js +28 -0
- package/dist/object.js.map +1 -0
- package/dist/plugin.d.ts +2 -0
- package/dist/plugin.js +56 -0
- package/dist/plugin.js.map +1 -0
- package/dist/remote.d.ts +8 -0
- package/dist/remote.js +43 -0
- package/dist/remote.js.map +1 -0
- package/package.json +3 -2
- package/src/action.ts +40 -0
- package/src/app.ts +257 -0
- package/src/driver.ts +54 -0
- package/src/hook.ts +42 -0
- package/src/index.ts +7 -377
- package/src/loader.ts +15 -24
- package/src/object.ts +26 -0
- package/src/plugin.ts +53 -0
- package/src/remote.ts +50 -0
- package/test/action.test.ts +1 -1
- package/test/dynamic.test.ts +34 -0
- package/test/fixtures/project.action.js +8 -0
- package/test/fixtures/project.object.yml +41 -0
- package/test/hook.test.ts +1 -1
- package/test/loader.test.ts +15 -0
- package/test/metadata.test.ts +49 -0
- package/test/mock-driver.ts +86 -0
- package/test/remote.test.ts +119 -0
- package/test/repository.test.ts +143 -0
- package/tsconfig.json +4 -1
- package/tsconfig.tsbuildinfo +1 -1
package/src/index.ts
CHANGED
|
@@ -1,379 +1,9 @@
|
|
|
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';
|
|
17
1
|
export * from './loader';
|
|
18
|
-
import { ObjectRepository } from './repository';
|
|
19
|
-
|
|
20
|
-
export class ObjectQL implements IObjectQL {
|
|
21
|
-
public metadata: ObjectRegistry;
|
|
22
|
-
private loader: ObjectLoader;
|
|
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[] = [];
|
|
27
|
-
|
|
28
|
-
constructor(config: ObjectQLConfig) {
|
|
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
|
-
}
|
|
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)
|
|
68
|
-
if (config.objects) {
|
|
69
|
-
for (const [key, obj] of Object.entries(config.objects)) {
|
|
70
|
-
this.registerObject(obj);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
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;
|
|
115
|
-
}
|
|
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
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
addPackage(name: string) {
|
|
128
|
-
this.loader.loadPackage(name);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
use(plugin: ObjectQLPlugin) {
|
|
132
|
-
this.pluginsList.push(plugin);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
removePackage(name: string) {
|
|
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}'`);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
loadFromDirectory(dir: string, packageName?: string) {
|
|
197
|
-
this.loader.load(dir, packageName);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
createContext(options: ObjectQLContextOptions): ObjectQLContext {
|
|
201
|
-
const ctx: ObjectQLContext = {
|
|
202
|
-
userId: options.userId,
|
|
203
|
-
spaceId: options.spaceId,
|
|
204
|
-
roles: options.roles || [],
|
|
205
|
-
isSystem: options.isSystem,
|
|
206
|
-
object: (name: string) => {
|
|
207
|
-
return new ObjectRepository(name, ctx, this);
|
|
208
|
-
},
|
|
209
|
-
transaction: async (callback) => {
|
|
210
|
-
const driver = this.datasources['default'];
|
|
211
|
-
if (!driver || !driver.beginTransaction) {
|
|
212
|
-
return callback(ctx);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
let trx: any;
|
|
216
|
-
try {
|
|
217
|
-
trx = await driver.beginTransaction();
|
|
218
|
-
} catch (e) {
|
|
219
|
-
throw e;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
const trxCtx: ObjectQLContext = {
|
|
223
|
-
...ctx,
|
|
224
|
-
transactionHandle: trx,
|
|
225
|
-
transaction: async (cb) => cb(trxCtx)
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
try {
|
|
229
|
-
const result = await callback(trxCtx);
|
|
230
|
-
if (driver.commitTransaction) await driver.commitTransaction(trx);
|
|
231
|
-
return result;
|
|
232
|
-
} catch (error) {
|
|
233
|
-
if (driver.rollbackTransaction) await driver.rollbackTransaction(trx);
|
|
234
|
-
throw error;
|
|
235
|
-
}
|
|
236
|
-
},
|
|
237
|
-
sudo: () => {
|
|
238
|
-
return this.createContext({ ...options, isSystem: true });
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
return ctx;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
registerObject(object: ObjectConfig) {
|
|
245
|
-
// Normalize fields
|
|
246
|
-
if (object.fields) {
|
|
247
|
-
for (const [key, field] of Object.entries(object.fields)) {
|
|
248
|
-
if (!field.name) {
|
|
249
|
-
field.name = key;
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
this.metadata.register('object', {
|
|
254
|
-
type: 'object',
|
|
255
|
-
id: object.name,
|
|
256
|
-
content: object
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
unregisterObject(name: string) {
|
|
261
|
-
this.metadata.unregister('object', name);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
getObject(name: string): ObjectConfig | undefined {
|
|
265
|
-
return this.metadata.get<ObjectConfig>('object', name);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
getConfigs(): Record<string, ObjectConfig> {
|
|
269
|
-
const result: Record<string, ObjectConfig> = {};
|
|
270
|
-
const objects = this.metadata.list<ObjectConfig>('object');
|
|
271
|
-
for (const obj of objects) {
|
|
272
|
-
result[obj.name] = obj;
|
|
273
|
-
}
|
|
274
|
-
return result;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
datasource(name: string): Driver {
|
|
278
|
-
const driver = this.datasources[name];
|
|
279
|
-
if (!driver) {
|
|
280
|
-
throw new Error(`Datasource '${name}' not found`);
|
|
281
|
-
}
|
|
282
|
-
return driver;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
async init() {
|
|
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);
|
|
303
|
-
}
|
|
304
|
-
const value = (target as any)[prop];
|
|
305
|
-
return typeof value === 'function' ? value.bind(target) : value;
|
|
306
|
-
}
|
|
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);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
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
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
2
|
export * from './repository';
|
|
3
|
+
export * from './app';
|
|
4
|
+
export * from './plugin';
|
|
5
|
+
export * from './driver';
|
|
6
|
+
export * from './remote';
|
|
7
|
+
export * from './action';
|
|
8
|
+
export * from './hook';
|
|
9
|
+
export * from './object';
|
package/src/loader.ts
CHANGED
|
@@ -1,29 +1,13 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as glob from 'fast-glob';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
-
import {
|
|
4
|
+
import { MetadataRegistry, ObjectConfig, LoaderPlugin, LoaderHandlerContext } from '@objectql/types';
|
|
5
5
|
import * as yaml from 'js-yaml';
|
|
6
6
|
|
|
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
7
|
export class ObjectLoader {
|
|
24
8
|
private plugins: LoaderPlugin[] = [];
|
|
25
9
|
|
|
26
|
-
constructor(protected registry:
|
|
10
|
+
constructor(protected registry: MetadataRegistry) {
|
|
27
11
|
this.registerBuiltinPlugins();
|
|
28
12
|
}
|
|
29
13
|
|
|
@@ -92,10 +76,7 @@ export class ObjectLoader {
|
|
|
92
76
|
|
|
93
77
|
try {
|
|
94
78
|
const mod = require(ctx.file);
|
|
95
|
-
|
|
96
|
-
// export const approve = { ... };
|
|
97
|
-
// export const reject = { ... };
|
|
98
|
-
|
|
79
|
+
|
|
99
80
|
const actions: Record<string, any> = {};
|
|
100
81
|
|
|
101
82
|
for (const [key, value] of Object.entries(mod)) {
|
|
@@ -174,7 +155,7 @@ export class ObjectLoader {
|
|
|
174
155
|
}
|
|
175
156
|
}
|
|
176
157
|
|
|
177
|
-
function registerObject(registry:
|
|
158
|
+
function registerObject(registry: MetadataRegistry, obj: any, file: string, packageName?: string) {
|
|
178
159
|
// Normalize fields
|
|
179
160
|
if (obj.fields) {
|
|
180
161
|
for (const [key, field] of Object.entries(obj.fields)) {
|
|
@@ -195,9 +176,19 @@ function registerObject(registry: ObjectRegistry, obj: any, file: string, packag
|
|
|
195
176
|
}
|
|
196
177
|
|
|
197
178
|
export function loadObjectConfigs(dir: string): Record<string, ObjectConfig> {
|
|
198
|
-
const registry = new
|
|
179
|
+
const registry = new MetadataRegistry();
|
|
199
180
|
const loader = new ObjectLoader(registry);
|
|
200
181
|
loader.load(dir);
|
|
182
|
+
|
|
183
|
+
// Merge actions into objects
|
|
184
|
+
const actions = registry.list<any>('action');
|
|
185
|
+
for (const act of actions) {
|
|
186
|
+
const obj = registry.get<ObjectConfig>('object', act.id);
|
|
187
|
+
if (obj) {
|
|
188
|
+
obj.actions = act.content;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
201
192
|
const result: Record<string, ObjectConfig> = {};
|
|
202
193
|
for (const obj of registry.list<ObjectConfig>('object')) {
|
|
203
194
|
result[obj.name] = obj;
|
package/src/object.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ObjectConfig, MetadataRegistry } from '@objectql/types';
|
|
2
|
+
|
|
3
|
+
export function registerObjectHelper(metadata: MetadataRegistry, object: ObjectConfig) {
|
|
4
|
+
// Normalize fields
|
|
5
|
+
if (object.fields) {
|
|
6
|
+
for (const [key, field] of Object.entries(object.fields)) {
|
|
7
|
+
if (!field.name) {
|
|
8
|
+
field.name = key;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
metadata.register('object', {
|
|
13
|
+
type: 'object',
|
|
14
|
+
id: object.name,
|
|
15
|
+
content: object
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getConfigsHelper(metadata: MetadataRegistry): Record<string, ObjectConfig> {
|
|
20
|
+
const result: Record<string, ObjectConfig> = {};
|
|
21
|
+
const objects = metadata.list<ObjectConfig>('object');
|
|
22
|
+
for (const obj of objects) {
|
|
23
|
+
result[obj.name] = obj;
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ObjectQLPlugin } from '@objectql/types';
|
|
2
|
+
|
|
3
|
+
export function loadPlugin(packageName: string): ObjectQLPlugin {
|
|
4
|
+
let mod: any;
|
|
5
|
+
try {
|
|
6
|
+
const modulePath = require.resolve(packageName, { paths: [process.cwd()] });
|
|
7
|
+
mod = require(modulePath);
|
|
8
|
+
} catch (e) {
|
|
9
|
+
throw new Error(`Failed to resolve plugin '${packageName}': ${e}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Helper to find plugin instance
|
|
13
|
+
const findPlugin = (candidate: any): ObjectQLPlugin | undefined => {
|
|
14
|
+
if (!candidate) return undefined;
|
|
15
|
+
|
|
16
|
+
// 1. Try treating as Class
|
|
17
|
+
if (typeof candidate === 'function') {
|
|
18
|
+
try {
|
|
19
|
+
const inst = new candidate();
|
|
20
|
+
if (inst && typeof inst.setup === 'function') {
|
|
21
|
+
return inst; // Found it!
|
|
22
|
+
}
|
|
23
|
+
} catch (e) {
|
|
24
|
+
// Not a constructor or instantiation failed
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 2. Try treating as Instance
|
|
29
|
+
if (candidate && typeof candidate.setup === 'function') {
|
|
30
|
+
if (candidate.name) return candidate;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Search in default, module root, and all named exports
|
|
36
|
+
let instance = findPlugin(mod.default) || findPlugin(mod);
|
|
37
|
+
|
|
38
|
+
if (!instance && mod && typeof mod === 'object') {
|
|
39
|
+
for (const key of Object.keys(mod)) {
|
|
40
|
+
if (key === 'default') continue;
|
|
41
|
+
instance = findPlugin(mod[key]);
|
|
42
|
+
if (instance) break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (instance) {
|
|
47
|
+
(instance as any)._packageName = packageName;
|
|
48
|
+
return instance;
|
|
49
|
+
} else {
|
|
50
|
+
console.error(`[PluginLoader] Failed to find ObjectQLPlugin in '${packageName}'. Exports:`, Object.keys(mod));
|
|
51
|
+
throw new Error(`Plugin '${packageName}' must export a class or object implementing ObjectQLPlugin.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/remote.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { RemoteDriver } from '@objectql/driver-remote';
|
|
2
|
+
import { ObjectConfig } from '@objectql/types';
|
|
3
|
+
|
|
4
|
+
export interface RemoteLoadResult {
|
|
5
|
+
driverName: string;
|
|
6
|
+
driver: RemoteDriver;
|
|
7
|
+
objects: ObjectConfig[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function loadRemoteFromUrl(url: string): Promise<RemoteLoadResult | null> {
|
|
11
|
+
try {
|
|
12
|
+
const baseUrl = url.replace(/\/$/, '');
|
|
13
|
+
const metadataUrl = `${baseUrl}/api/metadata/objects`;
|
|
14
|
+
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
16
|
+
// @ts-ignore - Fetch is available in Node 18+
|
|
17
|
+
const res = await fetch(metadataUrl);
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
console.warn(`[ObjectQL] Remote ${url} returned ${res.status}`);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const data = await res.json() as any;
|
|
24
|
+
if (!data || !data.objects) return null;
|
|
25
|
+
|
|
26
|
+
const driverName = `remote:${baseUrl}`;
|
|
27
|
+
const driver = new RemoteDriver(baseUrl);
|
|
28
|
+
const objects: ObjectConfig[] = [];
|
|
29
|
+
|
|
30
|
+
await Promise.all(data.objects.map(async (summary: any) => {
|
|
31
|
+
try {
|
|
32
|
+
// @ts-ignore
|
|
33
|
+
const detailRes = await fetch(`${metadataUrl}/${summary.name}`);
|
|
34
|
+
if (detailRes.ok) {
|
|
35
|
+
const config = await detailRes.json() as ObjectConfig;
|
|
36
|
+
config.datasource = driverName;
|
|
37
|
+
objects.push(config);
|
|
38
|
+
}
|
|
39
|
+
} catch (e) {
|
|
40
|
+
console.warn(`[ObjectQL] Failed to load object ${summary.name} from ${url}`);
|
|
41
|
+
}
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
return { driverName, driver, objects };
|
|
45
|
+
|
|
46
|
+
} catch (e: any) {
|
|
47
|
+
console.warn(`[ObjectQL] Remote connection error ${url}: ${e.message}`);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
package/test/action.test.ts
CHANGED
|
@@ -41,7 +41,7 @@ describe('ObjectQL Actions', () => {
|
|
|
41
41
|
expect(ctx.objectName).toBe('invoice');
|
|
42
42
|
expect(ctx.actionName).toBe('pay');
|
|
43
43
|
expect(ctx.id).toBe('inv-123');
|
|
44
|
-
expect(ctx.
|
|
44
|
+
expect(ctx.input.method).toBe('credit_card');
|
|
45
45
|
return { success: true, paid: true };
|
|
46
46
|
});
|
|
47
47
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ObjectQL } from '../src/index';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
|
|
4
|
+
describe('Dynamic Package Loading', () => {
|
|
5
|
+
let objectql: ObjectQL;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
objectql = new ObjectQL({
|
|
9
|
+
datasources: {}
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('should load directory manually', () => {
|
|
14
|
+
const fixtureDir = path.join(__dirname, 'fixtures');
|
|
15
|
+
objectql.loadFromDirectory(fixtureDir, 'test-pkg');
|
|
16
|
+
|
|
17
|
+
expect(objectql.getObject('project')).toBeDefined();
|
|
18
|
+
// Since 'test-pkg' is passed, it should be tracked
|
|
19
|
+
// but packageObjects is private, so we test behavior by removal
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('should remove package objects', () => {
|
|
23
|
+
const fixtureDir = path.join(__dirname, 'fixtures');
|
|
24
|
+
objectql.loadFromDirectory(fixtureDir, 'test-pkg');
|
|
25
|
+
|
|
26
|
+
expect(objectql.getObject('project')).toBeDefined();
|
|
27
|
+
|
|
28
|
+
objectql.removePackage('test-pkg');
|
|
29
|
+
expect(objectql.getObject('project')).toBeUndefined();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Mocking require for loadFromPackage is harder in jest without creating a real node module.
|
|
33
|
+
// relying on loadFromDirectory with packageName argument is sufficient to test the tracking logic.
|
|
34
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: project
|
|
2
|
+
label: Project
|
|
3
|
+
icon: standard:case
|
|
4
|
+
enable_search: true
|
|
5
|
+
fields:
|
|
6
|
+
name:
|
|
7
|
+
label: Project Name
|
|
8
|
+
type: text
|
|
9
|
+
required: true
|
|
10
|
+
searchable: true
|
|
11
|
+
index: true
|
|
12
|
+
|
|
13
|
+
status:
|
|
14
|
+
label: Status
|
|
15
|
+
type: select
|
|
16
|
+
options:
|
|
17
|
+
- label: Planned
|
|
18
|
+
value: planned
|
|
19
|
+
- label: In Progress
|
|
20
|
+
value: in_progress
|
|
21
|
+
- label: Completed
|
|
22
|
+
value: completed
|
|
23
|
+
defaultValue: planned
|
|
24
|
+
|
|
25
|
+
start_date:
|
|
26
|
+
label: Start Date
|
|
27
|
+
type: date
|
|
28
|
+
|
|
29
|
+
owner:
|
|
30
|
+
label: Project Manager
|
|
31
|
+
type: lookup
|
|
32
|
+
reference_to: users
|
|
33
|
+
|
|
34
|
+
budget:
|
|
35
|
+
label: Total Budget
|
|
36
|
+
type: currency
|
|
37
|
+
scale: 2
|
|
38
|
+
|
|
39
|
+
description:
|
|
40
|
+
label: Description
|
|
41
|
+
type: textarea
|
package/test/hook.test.ts
CHANGED
|
@@ -30,7 +30,7 @@ describe('ObjectQL Hooks', () => {
|
|
|
30
30
|
let hookTriggered = false;
|
|
31
31
|
app.on('beforeFind', 'post', async (ctx) => {
|
|
32
32
|
hookTriggered = true;
|
|
33
|
-
ctx.query = { ...ctx.query, filters: [['status', '=', 'published']] };
|
|
33
|
+
(ctx as any).query = { ...(ctx as any).query, filters: [['status', '=', 'published']] };
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
// Mock driver find to check query
|