Skip to content

Commit 2e4a5e6

Browse files
committed
重构 ObjectQL 类,添加插件、驱动、远程和动作支持,优化钩子和对象注册逻辑
1 parent 3298c46 commit 2e4a5e6

8 files changed

Lines changed: 296 additions & 201 deletions

File tree

packages/core/src/action.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { ActionContext, ActionHandler, ObjectRegistry } 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: ObjectRegistry,
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+
}

packages/core/src/app.ts

Lines changed: 25 additions & 201 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,20 @@ import {
1515
} from '@objectql/types';
1616
import { ObjectLoader } from './loader';
1717
import { ObjectRepository } from './repository';
18-
import { RemoteDriver } from '@objectql/driver-remote';
18+
import { loadPlugin } from './plugin';
19+
import { createDriverFromConnection } from './driver';
20+
import { loadRemoteFromUrl } from './remote';
21+
import { executeActionHelper, registerActionHelper, ActionEntry } from './action';
22+
import { registerHookHelper, triggerHookHelper, HookEntry } from './hook';
23+
import { registerObjectHelper, getConfigsHelper } from './object';
1924

2025
export class ObjectQL implements IObjectQL {
2126
public metadata: ObjectRegistry;
2227
private loader: ObjectLoader;
2328
private datasources: Record<string, Driver> = {};
2429
private remotes: string[] = [];
25-
private hooks: Record<string, Array<{ objectName: string, handler: HookHandler, packageName?: string }>> = {};
26-
private actions: Record<string, { handler: ActionHandler, packageName?: string }> = {};
30+
private hooks: Record<string, HookEntry[]> = {};
31+
private actions: Record<string, ActionEntry> = {};
2732
private pluginsList: ObjectQLPlugin[] = [];
2833

2934
constructor(config: ObjectQLConfig) {
@@ -33,7 +38,7 @@ export class ObjectQL implements IObjectQL {
3338
this.remotes = config.remotes || [];
3439

3540
if (config.connection) {
36-
this.loadDriverFromConnection(config.connection);
41+
this.datasources['default'] = createDriverFromConnection(config.connection);
3742
}
3843

3944
// 1. Load Presets/Packages first (Base Layer)
@@ -51,7 +56,7 @@ export class ObjectQL implements IObjectQL {
5156
if (config.plugins) {
5257
for (const plugin of config.plugins) {
5358
if (typeof plugin === 'string') {
54-
this.loadPluginFromPackage(plugin);
59+
this.use(loadPlugin(plugin));
5560
} else {
5661
this.use(plugin);
5762
}
@@ -74,58 +79,6 @@ export class ObjectQL implements IObjectQL {
7479
}
7580
}
7681

77-
private loadPluginFromPackage(packageName: string) {
78-
let mod: any;
79-
try {
80-
const modulePath = require.resolve(packageName, { paths: [process.cwd()] });
81-
mod = require(modulePath);
82-
} catch (e) {
83-
throw new Error(`Failed to resolve plugin '${packageName}': ${e}`);
84-
}
85-
86-
// Helper to find plugin instance
87-
const findPlugin = (candidate: any): ObjectQLPlugin | undefined => {
88-
if (!candidate) return undefined;
89-
90-
// 1. Try treating as Class
91-
if (typeof candidate === 'function') {
92-
try {
93-
const inst = new candidate();
94-
if (inst && typeof inst.setup === 'function') {
95-
return inst; // Found it!
96-
}
97-
} catch (e) {
98-
// Not a constructor or instantiation failed
99-
}
100-
}
101-
102-
// 2. Try treating as Instance
103-
if (candidate && typeof candidate.setup === 'function') {
104-
if (candidate.name) return candidate;
105-
}
106-
return undefined;
107-
};
108-
109-
// Search in default, module root, and all named exports
110-
let instance = findPlugin(mod.default) || findPlugin(mod);
111-
112-
if (!instance && mod && typeof mod === 'object') {
113-
for (const key of Object.keys(mod)) {
114-
if (key === 'default') continue;
115-
instance = findPlugin(mod[key]);
116-
if (instance) break;
117-
}
118-
}
119-
120-
if (instance) {
121-
(instance as any)._packageName = packageName;
122-
this.use(instance);
123-
} else {
124-
console.error(`[PluginLoader] Failed to find ObjectQLPlugin in '${packageName}'. Exports:`, Object.keys(mod));
125-
throw new Error(`Plugin '${packageName}' must export a class or object implementing ObjectQLPlugin.`);
126-
}
127-
}
128-
12982
addPackage(name: string) {
13083
this.loader.loadPackage(name);
13184
}
@@ -151,48 +104,19 @@ export class ObjectQL implements IObjectQL {
151104
}
152105

153106
on(event: HookName, objectName: string, handler: HookHandler, packageName?: string) {
154-
if (!this.hooks[event]) {
155-
this.hooks[event] = [];
156-
}
157-
this.hooks[event].push({ objectName, handler, packageName });
107+
registerHookHelper(this.hooks, event, objectName, handler, packageName);
158108
}
159109

160110
async triggerHook(event: HookName, objectName: string, ctx: HookContext) {
161-
// 1. Registry Hooks (File-based)
162-
const fileHooks = this.metadata.get<any>('hook', objectName);
163-
if (fileHooks && typeof fileHooks[event] === 'function') {
164-
await fileHooks[event](ctx);
165-
}
166-
167-
// 2. Programmatic Hooks
168-
const hooks = this.hooks[event] || [];
169-
for (const hook of hooks) {
170-
if (hook.objectName === '*' || hook.objectName === objectName) {
171-
await hook.handler(ctx);
172-
}
173-
}
111+
await triggerHookHelper(this.metadata, this.hooks, event, objectName, ctx);
174112
}
175113

176114
registerAction(objectName: string, actionName: string, handler: ActionHandler, packageName?: string) {
177-
const key = `${objectName}:${actionName}`;
178-
this.actions[key] = { handler, packageName };
115+
registerActionHelper(this.actions, objectName, actionName, handler, packageName);
179116
}
180117

181118
async executeAction(objectName: string, actionName: string, ctx: ActionContext) {
182-
// 1. Programmatic
183-
const key = `${objectName}:${actionName}`;
184-
const actionEntry = this.actions[key];
185-
if (actionEntry) {
186-
return await actionEntry.handler(ctx);
187-
}
188-
189-
// 2. Registry (File-based)
190-
const fileActions = this.metadata.get<any>('action', objectName);
191-
if (fileActions && typeof fileActions[actionName] === 'function') {
192-
return await fileActions[actionName](ctx);
193-
}
194-
195-
throw new Error(`Action '${actionName}' not found for object '${objectName}'`);
119+
return await executeActionHelper(this.metadata, this.actions, objectName, actionName, ctx);
196120
}
197121

198122
loadFromDirectory(dir: string, packageName?: string) {
@@ -244,19 +168,7 @@ export class ObjectQL implements IObjectQL {
244168
}
245169

246170
registerObject(object: ObjectConfig) {
247-
// Normalize fields
248-
if (object.fields) {
249-
for (const [key, field] of Object.entries(object.fields)) {
250-
if (!field.name) {
251-
field.name = key;
252-
}
253-
}
254-
}
255-
this.metadata.register('object', {
256-
type: 'object',
257-
id: object.name,
258-
content: object
259-
});
171+
registerObjectHelper(this.metadata, object);
260172
}
261173

262174
unregisterObject(name: string) {
@@ -268,12 +180,7 @@ export class ObjectQL implements IObjectQL {
268180
}
269181

270182
getConfigs(): Record<string, ObjectConfig> {
271-
const result: Record<string, ObjectConfig> = {};
272-
const objects = this.metadata.list<ObjectConfig>('object');
273-
for (const obj of objects) {
274-
result[obj.name] = obj;
275-
}
276-
return result;
183+
return getConfigsHelper(this.metadata);
277184
}
278185

279186
datasource(name: string): Driver {
@@ -288,7 +195,15 @@ export class ObjectQL implements IObjectQL {
288195
// -1. Load Remotes
289196
if (this.remotes.length > 0) {
290197
console.log(`Loading ${this.remotes.length} remotes...`);
291-
await Promise.all(this.remotes.map(url => this.loadRemote(url)));
198+
const results = await Promise.all(this.remotes.map(url => loadRemoteFromUrl(url)));
199+
for (const res of results) {
200+
if (res) {
201+
this.datasources[res.driverName] = res.driver;
202+
for (const obj of res.objects) {
203+
this.registerObject(obj);
204+
}
205+
}
206+
}
292207
}
293208

294209
// 0. Init Plugins
@@ -329,95 +244,4 @@ export class ObjectQL implements IObjectQL {
329244
}
330245
}
331246
}
332-
333-
private loadDriverFromConnection(connection: string) {
334-
let driverPackage = '';
335-
let driverClass = '';
336-
let driverConfig: any = {};
337-
338-
if (connection.startsWith('mongodb://')) {
339-
driverPackage = '@objectql/driver-mongo';
340-
driverClass = 'MongoDriver';
341-
driverConfig = { url: connection };
342-
}
343-
else if (connection.startsWith('sqlite://')) {
344-
driverPackage = '@objectql/driver-knex';
345-
driverClass = 'KnexDriver';
346-
const filename = connection.replace('sqlite://', '');
347-
driverConfig = {
348-
client: 'sqlite3',
349-
connection: { filename },
350-
useNullAsDefault: true
351-
};
352-
}
353-
else if (connection.startsWith('postgres://') || connection.startsWith('postgresql://')) {
354-
driverPackage = '@objectql/driver-knex';
355-
driverClass = 'KnexDriver';
356-
driverConfig = {
357-
client: 'pg',
358-
connection: connection
359-
};
360-
}
361-
else if (connection.startsWith('mysql://')) {
362-
driverPackage = '@objectql/driver-knex';
363-
driverClass = 'KnexDriver';
364-
driverConfig = {
365-
client: 'mysql2',
366-
connection: connection
367-
};
368-
}
369-
else {
370-
throw new Error(`Unsupported connection protocol: ${connection}`);
371-
}
372-
373-
try {
374-
// eslint-disable-next-line @typescript-eslint/no-var-requires
375-
const pkg = require(driverPackage);
376-
const DriverClass = pkg[driverClass];
377-
if (!DriverClass) {
378-
throw new Error(`${driverClass} not found in ${driverPackage}`);
379-
}
380-
this.datasources['default'] = new DriverClass(driverConfig);
381-
} catch (e: any) {
382-
throw new Error(`Failed to load driver ${driverPackage}. Please install it: npm install ${driverPackage}. Error: ${e.message}`);
383-
}
384-
}
385-
386-
private async loadRemote(url: string) {
387-
try {
388-
const baseUrl = url.replace(/\/$/, '');
389-
const metadataUrl = `${baseUrl}/api/metadata/objects`;
390-
391-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
392-
// @ts-ignore - Fetch is available in Node 18+
393-
const res = await fetch(metadataUrl);
394-
if (!res.ok) {
395-
console.warn(`[ObjectQL] Remote ${url} returned ${res.status}`);
396-
return;
397-
}
398-
399-
const data = await res.json() as any;
400-
if (!data || !data.objects) return;
401-
402-
const driverName = `remote:${baseUrl}`;
403-
this.datasources[driverName] = new RemoteDriver(baseUrl);
404-
405-
await Promise.all(data.objects.map(async (summary: any) => {
406-
try {
407-
// @ts-ignore
408-
const detailRes = await fetch(`${metadataUrl}/${summary.name}`);
409-
if (detailRes.ok) {
410-
const config = await detailRes.json() as ObjectConfig;
411-
config.datasource = driverName;
412-
this.registerObject(config);
413-
}
414-
} catch (e) {
415-
console.warn(`[ObjectQL] Failed to load object ${summary.name} from ${url}`);
416-
}
417-
}));
418-
419-
} catch (e: any) {
420-
console.warn(`[ObjectQL] Remote connection error ${url}: ${e.message}`);
421-
}
422-
}
423247
}

packages/core/src/driver.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

0 commit comments

Comments
 (0)