@objectql/core 1.0.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/src/index.ts CHANGED
@@ -1,59 +1,208 @@
1
- import * as path from 'path';
2
-
3
- export * from './metadata';
4
- export * from './types';
5
- export * from './driver';
6
- export * from './repository';
7
- export * from './query';
8
-
9
- import { ObjectConfig } from './metadata';
10
- import { ObjectQLContext, ObjectQLContextOptions, IObjectQL, ObjectQLConfig } from './types';
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
+ export * from './loader';
11
18
  import { ObjectRepository } from './repository';
12
- import { Driver } from './driver';
13
- import { loadObjectConfigs } from './loader';
14
19
 
15
20
  export class ObjectQL implements IObjectQL {
16
- private objects: Record<string, ObjectConfig> = {};
21
+ public metadata: ObjectRegistry;
22
+ private loader: ObjectLoader;
17
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[] = [];
18
27
 
19
28
  constructor(config: ObjectQLConfig) {
20
- 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
+ }
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)
21
68
  if (config.objects) {
22
69
  for (const [key, obj] of Object.entries(config.objects)) {
23
70
  this.registerObject(obj);
24
71
  }
25
72
  }
26
- if (config.packages) {
27
- for (const name of config.packages) {
28
- try {
29
- this.loadFromPackage(name);
30
- } catch (e) {
31
- this.loadFromDirectory(name);
32
- }
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;
33
115
  }
34
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);
35
129
  }
36
130
 
37
- loadFromPackage(name: string) {
38
- const entryPath = require.resolve(name, { paths: [process.cwd()] });
39
- const packageDir = path.dirname(entryPath);
40
- this.loadFromDirectory(packageDir);
131
+ use(plugin: ObjectQLPlugin) {
132
+ this.pluginsList.push(plugin);
41
133
  }
42
134
 
43
- loadFromDirectory(dir: string) {
44
- const objects = loadObjectConfigs(dir);
45
- for (const obj of Object.values(objects)) {
46
- this.registerObject(obj);
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
+ }
47
148
  }
48
149
  }
49
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
+
50
200
  createContext(options: ObjectQLContextOptions): ObjectQLContext {
51
201
  const ctx: ObjectQLContext = {
52
202
  userId: options.userId,
53
203
  spaceId: options.spaceId,
54
204
  roles: options.roles || [],
55
205
  isSystem: options.isSystem,
56
- ignoreTriggers: options.ignoreTriggers,
57
206
  object: (name: string) => {
58
207
  return new ObjectRepository(name, ctx, this);
59
208
  },
@@ -67,14 +216,12 @@ export class ObjectQL implements IObjectQL {
67
216
  try {
68
217
  trx = await driver.beginTransaction();
69
218
  } catch (e) {
70
- // If beginTransaction fails, fail.
71
219
  throw e;
72
220
  }
73
221
 
74
222
  const trxCtx: ObjectQLContext = {
75
223
  ...ctx,
76
224
  transactionHandle: trx,
77
- // Nested transaction simply reuses the current one (flat transaction)
78
225
  transaction: async (cb) => cb(trxCtx)
79
226
  };
80
227
 
@@ -103,15 +250,28 @@ export class ObjectQL implements IObjectQL {
103
250
  }
104
251
  }
105
252
  }
106
- this.objects[object.name] = object;
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);
107
262
  }
108
263
 
109
264
  getObject(name: string): ObjectConfig | undefined {
110
- return this.objects[name];
265
+ return this.metadata.get<ObjectConfig>('object', name);
111
266
  }
112
267
 
113
268
  getConfigs(): Record<string, ObjectConfig> {
114
- return this.objects;
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;
115
275
  }
116
276
 
117
277
  datasource(name: string): Driver {
@@ -123,27 +283,97 @@ export class ObjectQL implements IObjectQL {
123
283
  }
124
284
 
125
285
  async init() {
126
- const ctx = this.createContext({ isSystem: true });
127
- for (const objectName in this.objects) {
128
- const obj = this.objects[objectName];
129
- if (obj.data && obj.data.length > 0) {
130
- console.log(`Initializing data for object ${objectName}...`);
131
- const repo = ctx.object(objectName);
132
- for (const record of obj.data) {
133
- try {
134
- if (record._id) {
135
- const existing = await repo.findOne(record._id);
136
- if (existing) {
137
- continue;
138
- }
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);
139
303
  }
140
- await repo.create(record);
141
- console.log(`Inserted init data for ${objectName}: ${record._id || 'unknown id'}`);
142
- } catch (e) {
143
- console.error(`Failed to insert init data for ${objectName}:`, e);
304
+ const value = (target as any)[prop];
305
+ return typeof value === 'function' ? value.bind(target) : value;
144
306
  }
145
- }
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);
146
321
  }
147
322
  }
148
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
+ }
149
377
  }
378
+
379
+ export * from './repository';