@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/CHANGELOG.md +11 -0
- package/README.md +53 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.js +31 -50
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +5 -0
- package/dist/loader.js +17 -203
- package/dist/loader.js.map +1 -1
- package/dist/registry.d.ts +4 -0
- package/dist/registry.js +8 -0
- package/dist/registry.js.map +1 -0
- package/dist/types.d.ts +6 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -1
- package/package.json +3 -2
- package/src/index.ts +37 -27
- package/src/loader.ts +17 -189
- package/src/registry.ts +6 -0
- package/src/types.ts +6 -0
- package/test/dynamic.test.ts +34 -0
- package/tsconfig.tsbuildinfo +1 -1
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 {
|
|
15
|
+
import { MetadataLoader } from './loader';
|
|
16
|
+
import { MetadataRegistry } from './registry';
|
|
14
17
|
|
|
15
18
|
export class ObjectQL implements IObjectQL {
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
const packageDir = path.dirname(entryPath);
|
|
40
|
-
this.loadFromDirectory(packageDir);
|
|
40
|
+
addPackage(name: string) {
|
|
41
|
+
this.loader.loadPackage(name);
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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.
|
|
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.
|
|
115
|
+
return this.metadata.get<ObjectConfig>('object', name);
|
|
111
116
|
}
|
|
112
117
|
|
|
113
118
|
getConfigs(): Record<string, ObjectConfig> {
|
|
114
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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 ${
|
|
131
|
-
const repo = ctx.object(
|
|
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 ${
|
|
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 ${
|
|
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
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
package/src/registry.ts
ADDED
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
|
+
});
|