@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/loader.ts CHANGED
@@ -1,194 +1,206 @@
1
1
  import * as fs from 'fs';
2
+ import * as glob from 'fast-glob';
2
3
  import * as path from 'path';
4
+ import { ObjectRegistry, ObjectConfig } from '@objectql/types';
3
5
  import * as yaml from 'js-yaml';
4
- import * as glob from 'fast-glob';
5
- import { ObjectQLConfig, ObjectConfig } from './types';
6
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
- });
7
+ export interface LoaderHandlerContext {
8
+ file: string;
9
+ content: string;
10
+ registry: ObjectRegistry;
11
+ packageName?: string;
12
+ }
15
13
 
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;
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
+ }
28
50
  }
51
+ } catch (e) {
52
+ console.error(`Error loading object from ${ctx.file}:`, e);
29
53
  }
30
54
  }
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];
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);
65
80
  }
66
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
+ }
67
117
 
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
- }
118
+ } catch (e) {
119
+ console.error(`Error loading action from ${ctx.file}:`, e);
120
+ }
112
121
  }
113
- } catch (e) {
114
- console.error(`Error loading hook from ${file}:`, e);
115
- }
122
+ });
116
123
  }
117
124
 
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
- }
125
+ use(plugin: LoaderPlugin) {
126
+ this.plugins.push(plugin);
127
+ }
133
128
 
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
- }
129
+ load(dir: string, packageName?: string) {
130
+ for (const plugin of this.plugins) {
131
+ this.runPlugin(plugin, dir, packageName);
132
+ }
133
+ }
146
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);
147
142
  } catch (e) {
148
- console.error(`Error loading data from ${file}:`, e);
143
+ // fallback to directory
144
+ this.load(packageName, packageName);
149
145
  }
150
146
  }
151
147
 
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];
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');
168
166
  }
167
+
168
+ plugin.handler(ctx);
169
+
170
+ } catch (e) {
171
+ console.error(`Error in loader plugin '${plugin.name}' processing ${file}:`, e);
169
172
  }
173
+ }
174
+ }
175
+ }
170
176
 
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
- }
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
+ }
186
185
  }
187
- } catch (e) {
188
- console.error(`Error loading action from ${file}:`, e);
189
186
  }
190
187
  }
191
-
192
- return configs;
188
+ registry.register('object', {
189
+ type: 'object',
190
+ id: obj.name,
191
+ path: file,
192
+ package: packageName,
193
+ content: obj
194
+ });
193
195
  }
194
196
 
197
+ export function loadObjectConfigs(dir: string): Record<string, ObjectConfig> {
198
+ const registry = new ObjectRegistry();
199
+ const loader = new ObjectLoader(registry);
200
+ loader.load(dir);
201
+ const result: Record<string, ObjectConfig> = {};
202
+ for (const obj of registry.list<ObjectConfig>('object')) {
203
+ result[obj.name] = obj;
204
+ }
205
+ return result;
206
+ }