@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.
Files changed (48) hide show
  1. package/CHANGELOG.md +4 -10
  2. package/dist/index.d.ts +14 -11
  3. package/dist/index.js +222 -34
  4. package/dist/index.js.map +1 -1
  5. package/dist/loader.d.ts +23 -5
  6. package/dist/loader.js +201 -9
  7. package/dist/loader.js.map +1 -1
  8. package/dist/repository.d.ts +3 -5
  9. package/dist/repository.js +107 -112
  10. package/dist/repository.js.map +1 -1
  11. package/jest.config.js +3 -0
  12. package/package.json +10 -7
  13. package/src/index.ts +261 -41
  14. package/src/loader.ts +194 -10
  15. package/src/repository.ts +123 -127
  16. package/test/action.test.ts +58 -0
  17. package/test/hook.test.ts +60 -0
  18. package/test/utils.ts +54 -0
  19. package/tsconfig.json +4 -3
  20. package/tsconfig.tsbuildinfo +1 -1
  21. package/README.md +0 -53
  22. package/dist/driver.d.ts +0 -17
  23. package/dist/driver.js +0 -3
  24. package/dist/driver.js.map +0 -1
  25. package/dist/metadata.d.ts +0 -104
  26. package/dist/metadata.js +0 -3
  27. package/dist/metadata.js.map +0 -1
  28. package/dist/query.d.ts +0 -10
  29. package/dist/query.js +0 -3
  30. package/dist/query.js.map +0 -1
  31. package/dist/registry.d.ts +0 -4
  32. package/dist/registry.js +0 -8
  33. package/dist/registry.js.map +0 -1
  34. package/dist/types.d.ts +0 -83
  35. package/dist/types.js +0 -6
  36. package/dist/types.js.map +0 -1
  37. package/src/driver.ts +0 -24
  38. package/src/metadata.ts +0 -143
  39. package/src/query.ts +0 -11
  40. package/src/registry.ts +0 -6
  41. package/src/types.ts +0 -115
  42. package/test/dynamic.test.ts +0 -34
  43. package/test/fixtures/project.action.ts +0 -6
  44. package/test/fixtures/project.object.yml +0 -41
  45. package/test/loader.test.ts +0 -22
  46. package/test/metadata.test.ts +0 -49
  47. package/test/mock-driver.ts +0 -86
  48. package/test/repository.test.ts +0 -150
package/src/index.ts CHANGED
@@ -1,49 +1,196 @@
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
- export * from './registry';
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: MetadataRegistry;
20
- private loader: MetadataLoader;
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 MetadataRegistry();
25
- this.loader = new MetadataLoader(this.metadata);
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
- if (config.packages) {
34
- for (const name of config.packages) {
35
- this.addPackage(name);
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
- const ctx = this.createContext({ isSystem: true });
137
- const objects = this.metadata.list<ObjectConfig>('object');
138
- for (const obj of objects) {
139
- if (obj.data && obj.data.length > 0) {
140
- console.log(`Initializing data for object ${obj.name}...`);
141
- const repo = ctx.object(obj.name);
142
- for (const record of obj.data) {
143
- try {
144
- if (record._id) {
145
- const existing = await repo.findOne(record._id);
146
- if (existing) {
147
- continue;
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
- await repo.create(record);
151
- console.log(`Inserted init data for ${obj.name}: ${record._id || 'unknown id'}`);
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 { MetadataRegistry } from './registry';
2
- import { ObjectConfig } from './types';
3
- import { MetadataLoader as BaseLoader, registerObjectQLPlugins } from '@objectql/metadata';
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 class MetadataLoader extends BaseLoader {
6
- constructor(registry: MetadataRegistry) {
7
- super(registry);
8
- registerObjectQLPlugins(this);
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 MetadataRegistry();
14
- const loader = new MetadataLoader(registry);
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
-