@objectql/core 1.2.0 → 1.3.1

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 (50) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/action.d.ts +7 -0
  3. package/dist/action.js +23 -0
  4. package/dist/action.js.map +1 -0
  5. package/dist/app.d.ts +28 -0
  6. package/dist/app.js +211 -0
  7. package/dist/app.js.map +1 -0
  8. package/dist/driver.d.ts +2 -0
  9. package/dist/driver.js +55 -0
  10. package/dist/driver.js.map +1 -0
  11. package/dist/hook.d.ts +8 -0
  12. package/dist/hook.js +25 -0
  13. package/dist/hook.js.map +1 -0
  14. package/dist/index.d.ts +7 -27
  15. package/dist/index.js +7 -328
  16. package/dist/index.js.map +1 -1
  17. package/dist/loader.d.ts +3 -16
  18. package/dist/loader.js +9 -4
  19. package/dist/loader.js.map +1 -1
  20. package/dist/object.d.ts +3 -0
  21. package/dist/object.js +28 -0
  22. package/dist/object.js.map +1 -0
  23. package/dist/plugin.d.ts +2 -0
  24. package/dist/plugin.js +56 -0
  25. package/dist/plugin.js.map +1 -0
  26. package/dist/remote.d.ts +8 -0
  27. package/dist/remote.js +43 -0
  28. package/dist/remote.js.map +1 -0
  29. package/package.json +3 -2
  30. package/src/action.ts +40 -0
  31. package/src/app.ts +257 -0
  32. package/src/driver.ts +54 -0
  33. package/src/hook.ts +42 -0
  34. package/src/index.ts +7 -377
  35. package/src/loader.ts +15 -24
  36. package/src/object.ts +26 -0
  37. package/src/plugin.ts +53 -0
  38. package/src/remote.ts +50 -0
  39. package/test/action.test.ts +1 -1
  40. package/test/dynamic.test.ts +34 -0
  41. package/test/fixtures/project.action.js +8 -0
  42. package/test/fixtures/project.object.yml +41 -0
  43. package/test/hook.test.ts +1 -1
  44. package/test/loader.test.ts +15 -0
  45. package/test/metadata.test.ts +49 -0
  46. package/test/mock-driver.ts +86 -0
  47. package/test/remote.test.ts +119 -0
  48. package/test/repository.test.ts +143 -0
  49. package/tsconfig.json +4 -1
  50. package/tsconfig.tsbuildinfo +1 -1
package/src/action.ts ADDED
@@ -0,0 +1,40 @@
1
+ import { ActionContext, ActionHandler, MetadataRegistry } from '@objectql/types';
2
+
3
+ export interface ActionEntry {
4
+ handler: ActionHandler;
5
+ packageName?: string;
6
+ }
7
+
8
+ export function registerActionHelper(
9
+ actions: Record<string, ActionEntry>,
10
+ objectName: string,
11
+ actionName: string,
12
+ handler: ActionHandler,
13
+ packageName?: string
14
+ ) {
15
+ const key = `${objectName}:${actionName}`;
16
+ actions[key] = { handler, packageName };
17
+ }
18
+
19
+ export async function executeActionHelper(
20
+ metadata: MetadataRegistry,
21
+ runtimeActions: Record<string, ActionEntry>,
22
+ objectName: string,
23
+ actionName: string,
24
+ ctx: ActionContext
25
+ ) {
26
+ // 1. Programmatic
27
+ const key = `${objectName}:${actionName}`;
28
+ const actionEntry = runtimeActions[key];
29
+ if (actionEntry) {
30
+ return await actionEntry.handler(ctx);
31
+ }
32
+
33
+ // 2. Registry (File-based)
34
+ const fileActions = metadata.get<any>('action', objectName);
35
+ if (fileActions && typeof fileActions[actionName] === 'function') {
36
+ return await fileActions[actionName](ctx);
37
+ }
38
+
39
+ throw new Error(`Action '${actionName}' not found for object '${objectName}'`);
40
+ }
package/src/app.ts ADDED
@@ -0,0 +1,257 @@
1
+ import {
2
+ MetadataRegistry,
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
+ LoaderPlugin
16
+ } from '@objectql/types';
17
+ import { ObjectLoader } from './loader';
18
+ import { ObjectRepository } from './repository';
19
+ import { loadPlugin } from './plugin';
20
+ import { createDriverFromConnection } from './driver';
21
+ import { loadRemoteFromUrl } from './remote';
22
+ import { executeActionHelper, registerActionHelper, ActionEntry } from './action';
23
+ import { registerHookHelper, triggerHookHelper, HookEntry } from './hook';
24
+ import { registerObjectHelper, getConfigsHelper } from './object';
25
+
26
+ export class ObjectQL implements IObjectQL {
27
+ public metadata: MetadataRegistry;
28
+ private loader: ObjectLoader;
29
+ private datasources: Record<string, Driver> = {};
30
+ private remotes: string[] = [];
31
+ private hooks: Record<string, HookEntry[]> = {};
32
+ private actions: Record<string, ActionEntry> = {};
33
+ private pluginsList: ObjectQLPlugin[] = [];
34
+
35
+ // Store config for lazy loading in init()
36
+ private config: ObjectQLConfig;
37
+
38
+ constructor(config: ObjectQLConfig) {
39
+ this.config = config;
40
+ this.metadata = config.registry || new MetadataRegistry();
41
+ this.loader = new ObjectLoader(this.metadata);
42
+ this.datasources = config.datasources || {};
43
+ this.remotes = config.remotes || [];
44
+
45
+ if (config.connection) {
46
+ this.datasources['default'] = createDriverFromConnection(config.connection);
47
+ }
48
+
49
+ // Initialize Plugin List (but don't setup yet)
50
+ if (config.plugins) {
51
+ for (const plugin of config.plugins) {
52
+ if (typeof plugin === 'string') {
53
+ this.use(loadPlugin(plugin));
54
+ } else {
55
+ this.use(plugin);
56
+ }
57
+ }
58
+ }
59
+ }
60
+
61
+ addPackage(name: string) {
62
+ this.loader.loadPackage(name);
63
+ }
64
+
65
+ use(plugin: ObjectQLPlugin) {
66
+ this.pluginsList.push(plugin);
67
+ }
68
+
69
+ removePackage(name: string) {
70
+ this.metadata.unregisterPackage(name);
71
+
72
+ // Remove hooks
73
+ for (const event of Object.keys(this.hooks)) {
74
+ this.hooks[event] = this.hooks[event].filter(h => h.packageName !== name);
75
+ }
76
+
77
+ // Remove actions
78
+ for (const key of Object.keys(this.actions)) {
79
+ if (this.actions[key].packageName === name) {
80
+ delete this.actions[key];
81
+ }
82
+ }
83
+ }
84
+
85
+ on(event: HookName, objectName: string, handler: HookHandler, packageName?: string) {
86
+ registerHookHelper(this.hooks, event, objectName, handler, packageName);
87
+ }
88
+
89
+ async triggerHook(event: HookName, objectName: string, ctx: HookContext) {
90
+ await triggerHookHelper(this.metadata, this.hooks, event, objectName, ctx);
91
+ }
92
+
93
+ registerAction(objectName: string, actionName: string, handler: ActionHandler, packageName?: string) {
94
+ registerActionHelper(this.actions, objectName, actionName, handler, packageName);
95
+ }
96
+
97
+ async executeAction(objectName: string, actionName: string, ctx: ActionContext) {
98
+ return await executeActionHelper(this.metadata, this.actions, objectName, actionName, ctx);
99
+ }
100
+
101
+ loadFromDirectory(dir: string, packageName?: string) {
102
+ this.loader.load(dir, packageName);
103
+ }
104
+
105
+ addLoader(plugin: LoaderPlugin) {
106
+ this.loader.use(plugin);
107
+ }
108
+
109
+ createContext(options: ObjectQLContextOptions): ObjectQLContext {
110
+ const ctx: ObjectQLContext = {
111
+ userId: options.userId,
112
+ spaceId: options.spaceId,
113
+ roles: options.roles || [],
114
+ isSystem: options.isSystem,
115
+ object: (name: string) => {
116
+ return new ObjectRepository(name, ctx, this);
117
+ },
118
+ transaction: async (callback) => {
119
+ const driver = this.datasources['default'];
120
+ if (!driver || !driver.beginTransaction) {
121
+ return callback(ctx);
122
+ }
123
+
124
+ let trx: any;
125
+ try {
126
+ trx = await driver.beginTransaction();
127
+ } catch (e) {
128
+ throw e;
129
+ }
130
+
131
+ const trxCtx: ObjectQLContext = {
132
+ ...ctx,
133
+ transactionHandle: trx,
134
+ transaction: async (cb) => cb(trxCtx)
135
+ };
136
+
137
+ try {
138
+ const result = await callback(trxCtx);
139
+ if (driver.commitTransaction) await driver.commitTransaction(trx);
140
+ return result;
141
+ } catch (error) {
142
+ if (driver.rollbackTransaction) await driver.rollbackTransaction(trx);
143
+ throw error;
144
+ }
145
+ },
146
+ sudo: () => {
147
+ return this.createContext({ ...options, isSystem: true });
148
+ }
149
+ };
150
+ return ctx;
151
+ }
152
+
153
+ registerObject(object: ObjectConfig) {
154
+ registerObjectHelper(this.metadata, object);
155
+ }
156
+
157
+ unregisterObject(name: string) {
158
+ this.metadata.unregister('object', name);
159
+ }
160
+
161
+ getObject(name: string): ObjectConfig | undefined {
162
+ return this.metadata.get<ObjectConfig>('object', name);
163
+ }
164
+
165
+ getConfigs(): Record<string, ObjectConfig> {
166
+ return getConfigsHelper(this.metadata);
167
+ }
168
+
169
+ datasource(name: string): Driver {
170
+ const driver = this.datasources[name];
171
+ if (!driver) {
172
+ throw new Error(`Datasource '${name}' not found`);
173
+ }
174
+ return driver;
175
+ }
176
+
177
+ async init() {
178
+ // 0. Init Plugins (This allows plugins to register custom loaders)
179
+ for (const plugin of this.pluginsList) {
180
+ console.log(`Initializing plugin '${plugin.name}'...`);
181
+
182
+ let app: IObjectQL = this;
183
+ const pkgName = (plugin as any)._packageName;
184
+
185
+ if (pkgName) {
186
+ app = new Proxy(this, {
187
+ get(target, prop) {
188
+ if (prop === 'on') {
189
+ return (event: HookName, obj: string, handler: HookHandler) =>
190
+ target.on(event, obj, handler, pkgName);
191
+ }
192
+ if (prop === 'registerAction') {
193
+ return (obj: string, act: string, handler: ActionHandler) =>
194
+ target.registerAction(obj, act, handler, pkgName);
195
+ }
196
+ const value = (target as any)[prop];
197
+ return typeof value === 'function' ? value.bind(target) : value;
198
+ }
199
+ });
200
+ }
201
+
202
+ await plugin.setup(app);
203
+ }
204
+
205
+ // 1. Load Presets/Packages (Base Layer) - AFTER plugins, so they can use new loaders
206
+ if (this.config.packages) {
207
+ for (const name of this.config.packages) {
208
+ this.addPackage(name);
209
+ }
210
+ }
211
+ if (this.config.presets) {
212
+ for (const name of this.config.presets) {
213
+ this.addPackage(name);
214
+ }
215
+ }
216
+
217
+ // 2. Load Local Sources (Application Layer)
218
+ if (this.config.source) {
219
+ const sources = Array.isArray(this.config.source) ? this.config.source : [this.config.source];
220
+ for (const src of sources) {
221
+ this.loader.load(src);
222
+ }
223
+ }
224
+
225
+ // 3. Load In-Memory Objects (Dynamic Layer)
226
+ if (this.config.objects) {
227
+ for (const [key, obj] of Object.entries(this.config.objects)) {
228
+ this.registerObject(obj);
229
+ }
230
+ }
231
+
232
+ // 4. Load Remotes
233
+ if (this.remotes.length > 0) {
234
+ console.log(`Loading ${this.remotes.length} remotes...`);
235
+ const results = await Promise.all(this.remotes.map(url => loadRemoteFromUrl(url)));
236
+ for (const res of results) {
237
+ if (res) {
238
+ this.datasources[res.driverName] = res.driver;
239
+ for (const obj of res.objects) {
240
+ this.registerObject(obj);
241
+ }
242
+ }
243
+ }
244
+ }
245
+
246
+ const objects = this.metadata.list<ObjectConfig>('object');
247
+
248
+ // 5. Init Drivers (e.g. Sync Schema)
249
+ // Let's pass all objects to all configured drivers.
250
+ for (const [name, driver] of Object.entries(this.datasources)) {
251
+ if (driver.init) {
252
+ console.log(`Initializing driver '${name}'...`);
253
+ await driver.init(objects);
254
+ }
255
+ }
256
+ }
257
+ }
package/src/driver.ts ADDED
@@ -0,0 +1,54 @@
1
+ import { Driver } from '@objectql/types';
2
+
3
+ export function createDriverFromConnection(connection: string): Driver {
4
+ let driverPackage = '';
5
+ let driverClass = '';
6
+ let driverConfig: any = {};
7
+
8
+ if (connection.startsWith('mongodb://')) {
9
+ driverPackage = '@objectql/driver-mongo';
10
+ driverClass = 'MongoDriver';
11
+ driverConfig = { url: connection };
12
+ }
13
+ else if (connection.startsWith('sqlite://')) {
14
+ driverPackage = '@objectql/driver-knex';
15
+ driverClass = 'KnexDriver';
16
+ const filename = connection.replace('sqlite://', '');
17
+ driverConfig = {
18
+ client: 'sqlite3',
19
+ connection: { filename },
20
+ useNullAsDefault: true
21
+ };
22
+ }
23
+ else if (connection.startsWith('postgres://') || connection.startsWith('postgresql://')) {
24
+ driverPackage = '@objectql/driver-knex';
25
+ driverClass = 'KnexDriver';
26
+ driverConfig = {
27
+ client: 'pg',
28
+ connection: connection
29
+ };
30
+ }
31
+ else if (connection.startsWith('mysql://')) {
32
+ driverPackage = '@objectql/driver-knex';
33
+ driverClass = 'KnexDriver';
34
+ driverConfig = {
35
+ client: 'mysql2',
36
+ connection: connection
37
+ };
38
+ }
39
+ else {
40
+ throw new Error(`Unsupported connection protocol: ${connection}`);
41
+ }
42
+
43
+ try {
44
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
45
+ const pkg = require(driverPackage);
46
+ const DriverClass = pkg[driverClass];
47
+ if (!DriverClass) {
48
+ throw new Error(`${driverClass} not found in ${driverPackage}`);
49
+ }
50
+ return new DriverClass(driverConfig);
51
+ } catch (e: any) {
52
+ throw new Error(`Failed to load driver ${driverPackage}. Please install it: npm install ${driverPackage}. Error: ${e.message}`);
53
+ }
54
+ }
package/src/hook.ts ADDED
@@ -0,0 +1,42 @@
1
+ import { HookContext, HookHandler, HookName, MetadataRegistry } from '@objectql/types';
2
+
3
+ export interface HookEntry {
4
+ objectName: string;
5
+ handler: HookHandler;
6
+ packageName?: string;
7
+ }
8
+
9
+ export function registerHookHelper(
10
+ hooks: Record<string, HookEntry[]>,
11
+ event: HookName,
12
+ objectName: string,
13
+ handler: HookHandler,
14
+ packageName?: string
15
+ ) {
16
+ if (!hooks[event]) {
17
+ hooks[event] = [];
18
+ }
19
+ hooks[event].push({ objectName, handler, packageName });
20
+ }
21
+
22
+ export async function triggerHookHelper(
23
+ metadata: MetadataRegistry,
24
+ runtimeHooks: Record<string, HookEntry[]>,
25
+ event: HookName,
26
+ objectName: string,
27
+ ctx: HookContext
28
+ ) {
29
+ // 1. Registry Hooks (File-based)
30
+ const fileHooks = metadata.get<any>('hook', objectName);
31
+ if (fileHooks && typeof fileHooks[event] === 'function') {
32
+ await fileHooks[event](ctx);
33
+ }
34
+
35
+ // 2. Programmatic Hooks
36
+ const hooks = runtimeHooks[event] || [];
37
+ for (const hook of hooks) {
38
+ if (hook.objectName === '*' || hook.objectName === objectName) {
39
+ await hook.handler(ctx);
40
+ }
41
+ }
42
+ }