@objectql/core 1.2.0 → 1.3.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 +15 -0
- package/dist/action.d.ts +7 -0
- package/dist/action.js +23 -0
- package/dist/action.js.map +1 -0
- package/dist/app.d.ts +28 -0
- package/dist/app.js +211 -0
- package/dist/app.js.map +1 -0
- package/dist/driver.d.ts +2 -0
- package/dist/driver.js +55 -0
- package/dist/driver.js.map +1 -0
- package/dist/hook.d.ts +8 -0
- package/dist/hook.js +25 -0
- package/dist/hook.js.map +1 -0
- package/dist/index.d.ts +7 -27
- package/dist/index.js +7 -328
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +3 -16
- package/dist/loader.js +9 -4
- package/dist/loader.js.map +1 -1
- package/dist/object.d.ts +3 -0
- package/dist/object.js +28 -0
- package/dist/object.js.map +1 -0
- package/dist/plugin.d.ts +2 -0
- package/dist/plugin.js +56 -0
- package/dist/plugin.js.map +1 -0
- package/dist/remote.d.ts +8 -0
- package/dist/remote.js +43 -0
- package/dist/remote.js.map +1 -0
- package/package.json +3 -2
- package/src/action.ts +40 -0
- package/src/app.ts +257 -0
- package/src/driver.ts +54 -0
- package/src/hook.ts +42 -0
- package/src/index.ts +7 -377
- package/src/loader.ts +15 -24
- package/src/object.ts +26 -0
- package/src/plugin.ts +53 -0
- package/src/remote.ts +50 -0
- package/test/action.test.ts +1 -1
- package/test/dynamic.test.ts +34 -0
- package/test/fixtures/project.action.js +8 -0
- package/test/fixtures/project.object.yml +41 -0
- package/test/hook.test.ts +1 -1
- package/test/loader.test.ts +15 -0
- package/test/metadata.test.ts +49 -0
- package/test/mock-driver.ts +86 -0
- package/test/remote.test.ts +119 -0
- package/test/repository.test.ts +143 -0
- package/tsconfig.json +4 -1
- 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
|
+
}
|