@objectql/core 1.0.0 → 1.1.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
@@ -5,19 +5,26 @@ export * from './types';
5
5
  export * from './driver';
6
6
  export * from './repository';
7
7
  export * from './query';
8
+ export * from './registry';
9
+ export * from './loader';
8
10
 
9
11
  import { ObjectConfig } from './metadata';
10
12
  import { ObjectQLContext, ObjectQLContextOptions, IObjectQL, ObjectQLConfig } from './types';
11
13
  import { ObjectRepository } from './repository';
12
14
  import { Driver } from './driver';
13
- import { loadObjectConfigs } from './loader';
15
+ import { MetadataLoader } from './loader';
16
+ import { MetadataRegistry } from './registry';
14
17
 
15
18
  export class ObjectQL implements IObjectQL {
16
- private objects: Record<string, ObjectConfig> = {};
19
+ public metadata: MetadataRegistry;
20
+ private loader: MetadataLoader;
17
21
  private datasources: Record<string, Driver> = {};
18
22
 
19
23
  constructor(config: ObjectQLConfig) {
24
+ this.metadata = config.registry || new MetadataRegistry();
25
+ this.loader = new MetadataLoader(this.metadata);
20
26
  this.datasources = config.datasources;
27
+
21
28
  if (config.objects) {
22
29
  for (const [key, obj] of Object.entries(config.objects)) {
23
30
  this.registerObject(obj);
@@ -25,26 +32,22 @@ export class ObjectQL implements IObjectQL {
25
32
  }
26
33
  if (config.packages) {
27
34
  for (const name of config.packages) {
28
- try {
29
- this.loadFromPackage(name);
30
- } catch (e) {
31
- this.loadFromDirectory(name);
32
- }
35
+ this.addPackage(name);
33
36
  }
34
37
  }
35
38
  }
36
39
 
37
- loadFromPackage(name: string) {
38
- const entryPath = require.resolve(name, { paths: [process.cwd()] });
39
- const packageDir = path.dirname(entryPath);
40
- this.loadFromDirectory(packageDir);
40
+ addPackage(name: string) {
41
+ this.loader.loadPackage(name);
41
42
  }
42
43
 
43
- loadFromDirectory(dir: string) {
44
- const objects = loadObjectConfigs(dir);
45
- for (const obj of Object.values(objects)) {
46
- this.registerObject(obj);
47
- }
44
+
45
+ removePackage(name: string) {
46
+ this.metadata.unregisterPackage(name);
47
+ }
48
+
49
+ loadFromDirectory(dir: string, packageName?: string) {
50
+ this.loader.load(dir, packageName);
48
51
  }
49
52
 
50
53
  createContext(options: ObjectQLContextOptions): ObjectQLContext {
@@ -67,14 +70,12 @@ export class ObjectQL implements IObjectQL {
67
70
  try {
68
71
  trx = await driver.beginTransaction();
69
72
  } catch (e) {
70
- // If beginTransaction fails, fail.
71
73
  throw e;
72
74
  }
73
75
 
74
76
  const trxCtx: ObjectQLContext = {
75
77
  ...ctx,
76
78
  transactionHandle: trx,
77
- // Nested transaction simply reuses the current one (flat transaction)
78
79
  transaction: async (cb) => cb(trxCtx)
79
80
  };
80
81
 
@@ -103,15 +104,24 @@ export class ObjectQL implements IObjectQL {
103
104
  }
104
105
  }
105
106
  }
106
- this.objects[object.name] = object;
107
+ this.metadata.register('object', {
108
+ type: 'object',
109
+ id: object.name,
110
+ content: object
111
+ });
107
112
  }
108
113
 
109
114
  getObject(name: string): ObjectConfig | undefined {
110
- return this.objects[name];
115
+ return this.metadata.get<ObjectConfig>('object', name);
111
116
  }
112
117
 
113
118
  getConfigs(): Record<string, ObjectConfig> {
114
- return this.objects;
119
+ const result: Record<string, ObjectConfig> = {};
120
+ const objects = this.metadata.list<ObjectConfig>('object');
121
+ for (const obj of objects) {
122
+ result[obj.name] = obj;
123
+ }
124
+ return result;
115
125
  }
116
126
 
117
127
  datasource(name: string): Driver {
@@ -124,11 +134,11 @@ export class ObjectQL implements IObjectQL {
124
134
 
125
135
  async init() {
126
136
  const ctx = this.createContext({ isSystem: true });
127
- for (const objectName in this.objects) {
128
- const obj = this.objects[objectName];
137
+ const objects = this.metadata.list<ObjectConfig>('object');
138
+ for (const obj of objects) {
129
139
  if (obj.data && obj.data.length > 0) {
130
- console.log(`Initializing data for object ${objectName}...`);
131
- const repo = ctx.object(objectName);
140
+ console.log(`Initializing data for object ${obj.name}...`);
141
+ const repo = ctx.object(obj.name);
132
142
  for (const record of obj.data) {
133
143
  try {
134
144
  if (record._id) {
@@ -138,9 +148,9 @@ export class ObjectQL implements IObjectQL {
138
148
  }
139
149
  }
140
150
  await repo.create(record);
141
- console.log(`Inserted init data for ${objectName}: ${record._id || 'unknown id'}`);
151
+ console.log(`Inserted init data for ${obj.name}: ${record._id || 'unknown id'}`);
142
152
  } catch (e) {
143
- console.error(`Failed to insert init data for ${objectName}:`, e);
153
+ console.error(`Failed to insert init data for ${obj.name}:`, e);
144
154
  }
145
155
  }
146
156
  }
package/src/loader.ts CHANGED
@@ -1,194 +1,22 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import * as yaml from 'js-yaml';
4
- import * as glob from 'fast-glob';
5
- import { ObjectQLConfig, ObjectConfig } from './types';
6
-
7
- export function loadObjectConfigs(dir: string): Record<string, ObjectConfig> {
8
- const configs: Record<string, ObjectConfig> = {};
9
-
10
- // 1. Load YAML Configs
11
- const files = glob.sync(['**/*.object.yml', '**/*.object.yaml'], {
12
- cwd: dir,
13
- absolute: true
14
- });
15
-
16
- for (const file of files) {
17
- try {
18
- const content = fs.readFileSync(file, 'utf8');
19
- const doc = yaml.load(content) as any;
20
-
21
- if (doc.name && doc.fields) {
22
- configs[doc.name] = doc as ObjectConfig;
23
- } else {
24
- for (const [key, value] of Object.entries(doc)) {
25
- if (typeof value === 'object' && (value as any).fields) {
26
- configs[key] = value as ObjectConfig;
27
- if (!configs[key].name) configs[key].name = key;
28
- }
29
- }
30
- }
31
- } catch (e) {
32
- console.error(`Error loading object config from ${file}:`, e);
33
- }
34
- }
35
-
36
- // 2. Load Hooks (.hook.js, .hook.ts)
37
- // We only load .js if running in node, or .ts if ts-node/register is present.
38
- // simpler: look for both, require will handle extension resolution if we are careful.
39
- // Actually, in `dist` we only find .js. In `src` (test) we find .ts.
40
- const hookFiles = glob.sync(['**/*.hook.{js,ts}'], {
41
- cwd: dir,
42
- absolute: true
43
- });
44
-
45
- for (const file of hookFiles) {
46
- try {
47
- // Check if we should ignore .ts if .js exists?
48
- // Or assume env handles it.
49
- // If we are in `dist`, `src` shouldn't be there usually.
50
-
51
- const hookModule = require(file);
52
- // Default export or named exports?
53
- // Convention: export const listenTo = 'objectName';
54
- // or filename based: 'project.hook.js' -> 'project' (flaky)
55
-
56
- let objectName = hookModule.listenTo;
57
-
58
- if (!objectName) {
59
- // Try to guess from filename?
60
- // project.hook.ts -> project
61
- const basename = path.basename(file);
62
- const match = basename.match(/^(.+)\.hook\.(ts|js)$/);
63
- if (match) {
64
- objectName = match[1];
65
- }
66
- }
67
-
68
- if (objectName && configs[objectName]) {
69
- if (!configs[objectName].listeners) {
70
- configs[objectName].listeners = {};
71
- }
72
- const listeners = configs[objectName].listeners!;
73
-
74
- // Merge exported functions into listeners
75
- // Common hooks: beforeFind, afterFind, beforeCreate, etc.
76
- const hookNames = [
77
- 'beforeFind', 'afterFind',
78
- 'beforeCreate', 'afterCreate',
79
- 'beforeUpdate', 'afterUpdate',
80
- 'beforeDelete', 'afterDelete'
81
- ];
82
-
83
- for (const name of hookNames) {
84
- if (typeof hookModule[name] === 'function') {
85
- listeners[name as keyof typeof listeners] = hookModule[name];
86
- }
87
- }
88
- // Support default export having listeners object?
89
- if (hookModule.default && typeof hookModule.default === 'object') {
90
- Object.assign(listeners, hookModule.default);
91
- }
92
-
93
- // Load Actions
94
- // Convention: export const actions = { myAction: (ctx, params) => ... }
95
- // OR export function myAction(ctx, params) ... (Ambiguous with hooks? No, hooks have explicit names)
96
- // Safer: look for `actions` export.
97
-
98
- if (hookModule.actions && typeof hookModule.actions === 'object') {
99
- if (!configs[objectName].actions) {
100
- configs[objectName].actions = {};
101
- }
102
-
103
- for (const [actionName, handler] of Object.entries(hookModule.actions)) {
104
- // We might have metadata from YAML already
105
- if (!configs[objectName].actions![actionName]) {
106
- configs[objectName].actions![actionName] = { };
107
- }
108
- // Attach handler
109
- configs[objectName].actions![actionName].handler = handler as any;
110
- }
111
- }
112
- }
113
- } catch (e) {
114
- console.error(`Error loading hook from ${file}:`, e);
115
- }
116
- }
117
-
118
- // 3. Load Data (.data.yml, .data.yaml)
119
- const dataFiles = glob.sync(['**/*.data.yml', '**/*.data.yaml'], {
120
- cwd: dir,
121
- absolute: true
122
- });
123
-
124
- for (const file of dataFiles) {
125
- try {
126
- const content = fs.readFileSync(file, 'utf8');
127
- const data = yaml.load(content);
128
-
129
- if (!Array.isArray(data)) {
130
- console.warn(`Data file ${file} does not contain an array. Skipping.`);
131
- continue;
132
- }
133
-
134
- // Guess object name from filename
135
- // project.data.yml -> project
136
- const basename = path.basename(file);
137
- const objectName = basename.replace(/\.data\.ya?ml$/, '');
138
-
139
- if (configs[objectName]) {
140
- configs[objectName].data = data;
141
- } else {
142
- // Maybe the object config hasn't been found yet?
143
- // loadObjectConfigs runs glob for objects first, so configs should be populated.
144
- console.warn(`Found data for unknown object '${objectName}' in ${file}`);
145
- }
146
-
147
- } catch (e) {
148
- console.error(`Error loading data from ${file}:`, e);
149
- }
1
+ import { MetadataRegistry } from './registry';
2
+ import { ObjectConfig } from './types';
3
+ import { MetadataLoader as BaseLoader, registerObjectQLPlugins } from '@objectql/metadata';
4
+
5
+ export class MetadataLoader extends BaseLoader {
6
+ constructor(registry: MetadataRegistry) {
7
+ super(registry);
8
+ registerObjectQLPlugins(this);
150
9
  }
10
+ }
151
11
 
152
- // 4. Load Actions (.action.js, .action.ts)
153
- const actionFiles = glob.sync(['**/*.action.{js,ts}'], {
154
- cwd: dir,
155
- absolute: true
156
- });
157
-
158
- for (const file of actionFiles) {
159
- try {
160
- const actionModule = require(file);
161
- let objectName = actionModule.listenTo;
162
-
163
- if (!objectName) {
164
- const basename = path.basename(file);
165
- const match = basename.match(/^(.+)\.action\.(ts|js)$/);
166
- if (match) {
167
- objectName = match[1];
168
- }
169
- }
170
-
171
- if (objectName && configs[objectName]) {
172
- if (!configs[objectName].actions) {
173
- configs[objectName].actions = {};
174
- }
175
-
176
- // Treat all exported functions as actions
177
- for (const [key, value] of Object.entries(actionModule)) {
178
- if (key === 'listenTo') continue;
179
- if (typeof value === 'function') {
180
- if (!configs[objectName].actions![key]) {
181
- configs[objectName].actions![key] = {};
182
- }
183
- configs[objectName].actions![key].handler = value as any;
184
- }
185
- }
186
- }
187
- } catch (e) {
188
- console.error(`Error loading action from ${file}:`, e);
189
- }
12
+ export function loadObjectConfigs(dir: string): Record<string, ObjectConfig> {
13
+ const registry = new MetadataRegistry();
14
+ const loader = new MetadataLoader(registry);
15
+ loader.load(dir);
16
+ const result: Record<string, ObjectConfig> = {};
17
+ for (const obj of registry.list<ObjectConfig>('object')) {
18
+ result[obj.name] = obj;
190
19
  }
191
-
192
- return configs;
20
+ return result;
193
21
  }
194
22
 
@@ -0,0 +1,6 @@
1
+ import { MetadataRegistry as BaseRegistry, Metadata } from '@objectql/metadata';
2
+
3
+ export { Metadata };
4
+ export class MetadataRegistry extends BaseRegistry {
5
+ // Re-export or extend if needed
6
+ }
package/src/types.ts CHANGED
@@ -2,10 +2,13 @@ import { ObjectRepository } from "./repository";
2
2
  import { ObjectConfig } from "./metadata";
3
3
  import { Driver } from "./driver";
4
4
  import { UnifiedQuery, FilterCriterion } from "./query";
5
+ import { MetadataRegistry } from "./registry";
5
6
 
6
7
  export { ObjectConfig } from "./metadata";
8
+ export { MetadataRegistry } from "./registry";
7
9
 
8
10
  export interface ObjectQLConfig {
11
+ registry?: MetadataRegistry;
9
12
  datasources: Record<string, Driver>;
10
13
  objects?: Record<string, ObjectConfig>;
11
14
  packages?: string[];
@@ -16,6 +19,9 @@ export interface IObjectQL {
16
19
  getConfigs(): Record<string, ObjectConfig>;
17
20
  datasource(name: string): Driver;
18
21
  init(): Promise<void>;
22
+ addPackage(name: string): void;
23
+ removePackage(name: string): void;
24
+ metadata: MetadataRegistry;
19
25
  }
20
26
 
21
27
  export interface HookContext<T = any> {
@@ -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
+ });