@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/CHANGELOG.md +8 -3
- package/dist/index.d.ts +19 -11
- package/dist/index.js +237 -68
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +24 -1
- package/dist/loader.js +152 -146
- package/dist/loader.js.map +1 -1
- package/dist/repository.d.ts +3 -5
- package/dist/repository.js +107 -112
- package/dist/repository.js.map +1 -1
- package/jest.config.js +3 -0
- package/package.json +10 -6
- package/src/index.ts +283 -53
- package/src/loader.ts +178 -166
- package/src/repository.ts +123 -127
- package/test/action.test.ts +58 -0
- package/test/hook.test.ts +60 -0
- package/test/utils.ts +54 -0
- package/tsconfig.json +4 -3
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/driver.d.ts +0 -17
- package/dist/driver.js +0 -3
- package/dist/driver.js.map +0 -1
- package/dist/metadata.d.ts +0 -104
- package/dist/metadata.js +0 -3
- package/dist/metadata.js.map +0 -1
- package/dist/query.d.ts +0 -10
- package/dist/query.js +0 -3
- package/dist/query.js.map +0 -1
- package/dist/types.d.ts +0 -77
- package/dist/types.js +0 -3
- package/dist/types.js.map +0 -1
- package/src/driver.ts +0 -24
- package/src/metadata.ts +0 -143
- package/src/query.ts +0 -11
- package/src/types.ts +0 -109
- package/test/fixtures/project.action.ts +0 -6
- package/test/fixtures/project.object.yml +0 -41
- package/test/loader.test.ts +0 -22
- package/test/metadata.test.ts +0 -49
- package/test/mock-driver.ts +0 -86
- package/test/repository.test.ts +0 -150
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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
absolute: true
|
|
14
|
-
});
|
|
7
|
+
export interface LoaderHandlerContext {
|
|
8
|
+
file: string;
|
|
9
|
+
content: string;
|
|
10
|
+
registry: ObjectRegistry;
|
|
11
|
+
packageName?: string;
|
|
12
|
+
}
|
|
15
13
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
}
|
|
114
|
-
console.error(`Error loading hook from ${file}:`, e);
|
|
115
|
-
}
|
|
122
|
+
});
|
|
116
123
|
}
|
|
117
124
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
143
|
+
// fallback to directory
|
|
144
|
+
this.load(packageName, packageName);
|
|
149
145
|
}
|
|
150
146
|
}
|
|
151
147
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
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
|
+
}
|