@objectql/core 1.1.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 -6
- 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 -17
- package/dist/driver.js +52 -0
- package/dist/driver.js.map +1 -1
- 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 +8 -25
- package/dist/index.js +8 -141
- package/dist/index.js.map +1 -1
- package/dist/loader.d.ts +9 -4
- package/dist/loader.js +206 -9
- 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/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 +11 -7
- package/src/action.ts +40 -0
- package/src/app.ts +257 -0
- package/src/driver.ts +51 -21
- package/src/hook.ts +42 -0
- package/src/index.ts +8 -158
- package/src/loader.ts +184 -9
- package/src/object.ts +26 -0
- package/src/plugin.ts +53 -0
- package/src/remote.ts +50 -0
- package/src/repository.ts +123 -127
- package/test/action.test.ts +58 -0
- package/test/fixtures/project.action.js +8 -0
- package/test/hook.test.ts +60 -0
- package/test/loader.test.ts +1 -8
- package/test/metadata.test.ts +1 -1
- package/test/mock-driver.ts +1 -1
- package/test/remote.test.ts +119 -0
- package/test/repository.test.ts +42 -49
- package/test/utils.ts +54 -0
- package/tsconfig.json +7 -3
- package/tsconfig.tsbuildinfo +1 -1
- package/README.md +0 -53
- 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/registry.d.ts +0 -4
- package/dist/registry.js +0 -8
- package/dist/registry.js.map +0 -1
- package/dist/types.d.ts +0 -83
- package/dist/types.js +0 -6
- package/dist/types.js.map +0 -1
- package/src/metadata.ts +0 -143
- package/src/query.ts +0 -11
- package/src/registry.ts +0 -6
- package/src/types.ts +0 -115
- package/test/fixtures/project.action.ts +0 -6
package/src/driver.ts
CHANGED
|
@@ -1,24 +1,54 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
count(objectName: string, filters: any, options?: any): Promise<number>;
|
|
8
|
-
|
|
9
|
-
// Advanced
|
|
10
|
-
aggregate?(objectName: string, query: any, options?: any): Promise<any>;
|
|
11
|
-
distinct?(objectName: string, field: string, filters?: any, options?: any): Promise<any[]>;
|
|
1
|
+
import { Driver } from '@objectql/types';
|
|
2
|
+
|
|
3
|
+
export function createDriverFromConnection(connection: string): Driver {
|
|
4
|
+
let driverPackage = '';
|
|
5
|
+
let driverClass = '';
|
|
6
|
+
let driverConfig: any = {};
|
|
12
7
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
+
}
|
|
18
42
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+
}
|
|
23
54
|
}
|
|
24
|
-
|
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
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,159 +1,9 @@
|
|
|
1
|
-
import * as path from 'path';
|
|
2
|
-
|
|
3
|
-
export * from './metadata';
|
|
4
|
-
export * from './types';
|
|
5
|
-
export * from './driver';
|
|
6
|
-
export * from './repository';
|
|
7
|
-
export * from './query';
|
|
8
|
-
export * from './registry';
|
|
9
1
|
export * from './loader';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export class ObjectQL implements IObjectQL {
|
|
19
|
-
public metadata: MetadataRegistry;
|
|
20
|
-
private loader: MetadataLoader;
|
|
21
|
-
private datasources: Record<string, Driver> = {};
|
|
22
|
-
|
|
23
|
-
constructor(config: ObjectQLConfig) {
|
|
24
|
-
this.metadata = config.registry || new MetadataRegistry();
|
|
25
|
-
this.loader = new MetadataLoader(this.metadata);
|
|
26
|
-
this.datasources = config.datasources;
|
|
27
|
-
|
|
28
|
-
if (config.objects) {
|
|
29
|
-
for (const [key, obj] of Object.entries(config.objects)) {
|
|
30
|
-
this.registerObject(obj);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
if (config.packages) {
|
|
34
|
-
for (const name of config.packages) {
|
|
35
|
-
this.addPackage(name);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
addPackage(name: string) {
|
|
41
|
-
this.loader.loadPackage(name);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
removePackage(name: string) {
|
|
46
|
-
this.metadata.unregisterPackage(name);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
loadFromDirectory(dir: string, packageName?: string) {
|
|
50
|
-
this.loader.load(dir, packageName);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
createContext(options: ObjectQLContextOptions): ObjectQLContext {
|
|
54
|
-
const ctx: ObjectQLContext = {
|
|
55
|
-
userId: options.userId,
|
|
56
|
-
spaceId: options.spaceId,
|
|
57
|
-
roles: options.roles || [],
|
|
58
|
-
isSystem: options.isSystem,
|
|
59
|
-
ignoreTriggers: options.ignoreTriggers,
|
|
60
|
-
object: (name: string) => {
|
|
61
|
-
return new ObjectRepository(name, ctx, this);
|
|
62
|
-
},
|
|
63
|
-
transaction: async (callback) => {
|
|
64
|
-
const driver = this.datasources['default'];
|
|
65
|
-
if (!driver || !driver.beginTransaction) {
|
|
66
|
-
return callback(ctx);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
let trx: any;
|
|
70
|
-
try {
|
|
71
|
-
trx = await driver.beginTransaction();
|
|
72
|
-
} catch (e) {
|
|
73
|
-
throw e;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const trxCtx: ObjectQLContext = {
|
|
77
|
-
...ctx,
|
|
78
|
-
transactionHandle: trx,
|
|
79
|
-
transaction: async (cb) => cb(trxCtx)
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const result = await callback(trxCtx);
|
|
84
|
-
if (driver.commitTransaction) await driver.commitTransaction(trx);
|
|
85
|
-
return result;
|
|
86
|
-
} catch (error) {
|
|
87
|
-
if (driver.rollbackTransaction) await driver.rollbackTransaction(trx);
|
|
88
|
-
throw error;
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
sudo: () => {
|
|
92
|
-
return this.createContext({ ...options, isSystem: true });
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
return ctx;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
registerObject(object: ObjectConfig) {
|
|
99
|
-
// Normalize fields
|
|
100
|
-
if (object.fields) {
|
|
101
|
-
for (const [key, field] of Object.entries(object.fields)) {
|
|
102
|
-
if (!field.name) {
|
|
103
|
-
field.name = key;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
this.metadata.register('object', {
|
|
108
|
-
type: 'object',
|
|
109
|
-
id: object.name,
|
|
110
|
-
content: object
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
getObject(name: string): ObjectConfig | undefined {
|
|
115
|
-
return this.metadata.get<ObjectConfig>('object', name);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
getConfigs(): Record<string, ObjectConfig> {
|
|
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;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
datasource(name: string): Driver {
|
|
128
|
-
const driver = this.datasources[name];
|
|
129
|
-
if (!driver) {
|
|
130
|
-
throw new Error(`Datasource '${name}' not found`);
|
|
131
|
-
}
|
|
132
|
-
return driver;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
async init() {
|
|
136
|
-
const ctx = this.createContext({ isSystem: true });
|
|
137
|
-
const objects = this.metadata.list<ObjectConfig>('object');
|
|
138
|
-
for (const obj of objects) {
|
|
139
|
-
if (obj.data && obj.data.length > 0) {
|
|
140
|
-
console.log(`Initializing data for object ${obj.name}...`);
|
|
141
|
-
const repo = ctx.object(obj.name);
|
|
142
|
-
for (const record of obj.data) {
|
|
143
|
-
try {
|
|
144
|
-
if (record._id) {
|
|
145
|
-
const existing = await repo.findOne(record._id);
|
|
146
|
-
if (existing) {
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
await repo.create(record);
|
|
151
|
-
console.log(`Inserted init data for ${obj.name}: ${record._id || 'unknown id'}`);
|
|
152
|
-
} catch (e) {
|
|
153
|
-
console.error(`Failed to insert init data for ${obj.name}:`, e);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
2
|
+
export * from './repository';
|
|
3
|
+
export * from './app';
|
|
4
|
+
export * from './plugin';
|
|
5
|
+
export * from './driver';
|
|
6
|
+
export * from './remote';
|
|
7
|
+
export * from './action';
|
|
8
|
+
export * from './hook';
|
|
9
|
+
export * from './object';
|
package/src/loader.ts
CHANGED
|
@@ -1,22 +1,197 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as glob from 'fast-glob';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { MetadataRegistry, ObjectConfig, LoaderPlugin, LoaderHandlerContext } from '@objectql/types';
|
|
5
|
+
import * as yaml from 'js-yaml';
|
|
4
6
|
|
|
5
|
-
export class
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
export class ObjectLoader {
|
|
8
|
+
private plugins: LoaderPlugin[] = [];
|
|
9
|
+
|
|
10
|
+
constructor(protected registry: MetadataRegistry) {
|
|
11
|
+
this.registerBuiltinPlugins();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
private registerBuiltinPlugins() {
|
|
15
|
+
// Objects
|
|
16
|
+
this.use({
|
|
17
|
+
name: 'object',
|
|
18
|
+
glob: ['**/*.object.yml', '**/*.object.yaml'],
|
|
19
|
+
handler: (ctx) => {
|
|
20
|
+
try {
|
|
21
|
+
const doc = yaml.load(ctx.content) as any;
|
|
22
|
+
if (!doc) return;
|
|
23
|
+
|
|
24
|
+
if (doc.name && doc.fields) {
|
|
25
|
+
registerObject(ctx.registry, doc, ctx.file, ctx.packageName || ctx.registry.getEntry('package-map', ctx.file)?.package);
|
|
26
|
+
} else {
|
|
27
|
+
for (const [key, value] of Object.entries(doc)) {
|
|
28
|
+
if (typeof value === 'object' && (value as any).fields) {
|
|
29
|
+
const obj = value as any;
|
|
30
|
+
if (!obj.name) obj.name = key;
|
|
31
|
+
registerObject(ctx.registry, obj, ctx.file, ctx.packageName);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.error(`Error loading object from ${ctx.file}:`, e);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Hooks
|
|
42
|
+
this.use({
|
|
43
|
+
name: 'hook',
|
|
44
|
+
glob: ['**/*.hook.ts', '**/*.hook.js'],
|
|
45
|
+
handler: (ctx) => {
|
|
46
|
+
const basename = path.basename(ctx.file);
|
|
47
|
+
// Extract object name from filename: user.hook.ts -> user
|
|
48
|
+
const objectName = basename.replace(/\.hook\.(ts|js)$/, '');
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const mod = require(ctx.file);
|
|
52
|
+
// Support default export or named exports
|
|
53
|
+
const hooks = mod.default || mod;
|
|
54
|
+
|
|
55
|
+
ctx.registry.register('hook', {
|
|
56
|
+
type: 'hook',
|
|
57
|
+
id: objectName, // Hook ID is the object name
|
|
58
|
+
path: ctx.file,
|
|
59
|
+
package: ctx.packageName,
|
|
60
|
+
content: hooks
|
|
61
|
+
});
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error(`Error loading hook from ${ctx.file}:`, e);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Actions
|
|
69
|
+
this.use({
|
|
70
|
+
name: 'action',
|
|
71
|
+
glob: ['**/*.action.ts', '**/*.action.js'],
|
|
72
|
+
handler: (ctx) => {
|
|
73
|
+
const basename = path.basename(ctx.file);
|
|
74
|
+
// Extract object name: invoice.action.ts -> invoice
|
|
75
|
+
const objectName = basename.replace(/\.action\.(ts|js)$/, '');
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const mod = require(ctx.file);
|
|
79
|
+
|
|
80
|
+
const actions: Record<string, any> = {};
|
|
81
|
+
|
|
82
|
+
for (const [key, value] of Object.entries(mod)) {
|
|
83
|
+
if (key === 'default') continue;
|
|
84
|
+
if (typeof value === 'object' && (value as any).handler) {
|
|
85
|
+
actions[key] = value;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (Object.keys(actions).length > 0) {
|
|
90
|
+
ctx.registry.register('action', {
|
|
91
|
+
type: 'action',
|
|
92
|
+
id: objectName, // Action collection ID is the object name
|
|
93
|
+
path: ctx.file,
|
|
94
|
+
package: ctx.packageName,
|
|
95
|
+
content: actions
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
} catch (e) {
|
|
100
|
+
console.error(`Error loading action from ${ctx.file}:`, e);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
use(plugin: LoaderPlugin) {
|
|
107
|
+
this.plugins.push(plugin);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
load(dir: string, packageName?: string) {
|
|
111
|
+
for (const plugin of this.plugins) {
|
|
112
|
+
this.runPlugin(plugin, dir, packageName);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
loadPackage(packageName: string) {
|
|
117
|
+
try {
|
|
118
|
+
const entryPath = require.resolve(packageName, { paths: [process.cwd()] });
|
|
119
|
+
// clean cache
|
|
120
|
+
delete require.cache[entryPath];
|
|
121
|
+
const packageDir = path.dirname(entryPath);
|
|
122
|
+
this.load(packageDir, packageName);
|
|
123
|
+
} catch (e) {
|
|
124
|
+
// fallback to directory
|
|
125
|
+
this.load(packageName, packageName);
|
|
126
|
+
}
|
|
9
127
|
}
|
|
128
|
+
|
|
129
|
+
private runPlugin(plugin: LoaderPlugin, dir: string, packageName?: string) {
|
|
130
|
+
const files = glob.sync(plugin.glob, {
|
|
131
|
+
cwd: dir,
|
|
132
|
+
absolute: true
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
for (const file of files) {
|
|
136
|
+
try {
|
|
137
|
+
const ctx: LoaderHandlerContext = {
|
|
138
|
+
file,
|
|
139
|
+
content: '',
|
|
140
|
+
registry: this.registry,
|
|
141
|
+
packageName
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// Pre-read for convenience
|
|
145
|
+
if (!file.match(/\.(js|ts|node)$/)) {
|
|
146
|
+
ctx.content = fs.readFileSync(file, 'utf8');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
plugin.handler(ctx);
|
|
150
|
+
|
|
151
|
+
} catch (e) {
|
|
152
|
+
console.error(`Error in loader plugin '${plugin.name}' processing ${file}:`, e);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function registerObject(registry: MetadataRegistry, obj: any, file: string, packageName?: string) {
|
|
159
|
+
// Normalize fields
|
|
160
|
+
if (obj.fields) {
|
|
161
|
+
for (const [key, field] of Object.entries(obj.fields)) {
|
|
162
|
+
if (typeof field === 'object' && field !== null) {
|
|
163
|
+
if (!(field as any).name) {
|
|
164
|
+
(field as any).name = key;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
registry.register('object', {
|
|
170
|
+
type: 'object',
|
|
171
|
+
id: obj.name,
|
|
172
|
+
path: file,
|
|
173
|
+
package: packageName,
|
|
174
|
+
content: obj
|
|
175
|
+
});
|
|
10
176
|
}
|
|
11
177
|
|
|
12
178
|
export function loadObjectConfigs(dir: string): Record<string, ObjectConfig> {
|
|
13
179
|
const registry = new MetadataRegistry();
|
|
14
|
-
const loader = new
|
|
180
|
+
const loader = new ObjectLoader(registry);
|
|
15
181
|
loader.load(dir);
|
|
182
|
+
|
|
183
|
+
// Merge actions into objects
|
|
184
|
+
const actions = registry.list<any>('action');
|
|
185
|
+
for (const act of actions) {
|
|
186
|
+
const obj = registry.get<ObjectConfig>('object', act.id);
|
|
187
|
+
if (obj) {
|
|
188
|
+
obj.actions = act.content;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
16
192
|
const result: Record<string, ObjectConfig> = {};
|
|
17
193
|
for (const obj of registry.list<ObjectConfig>('object')) {
|
|
18
194
|
result[obj.name] = obj;
|
|
19
195
|
}
|
|
20
196
|
return result;
|
|
21
197
|
}
|
|
22
|
-
|
package/src/object.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ObjectConfig, MetadataRegistry } from '@objectql/types';
|
|
2
|
+
|
|
3
|
+
export function registerObjectHelper(metadata: MetadataRegistry, object: ObjectConfig) {
|
|
4
|
+
// Normalize fields
|
|
5
|
+
if (object.fields) {
|
|
6
|
+
for (const [key, field] of Object.entries(object.fields)) {
|
|
7
|
+
if (!field.name) {
|
|
8
|
+
field.name = key;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
metadata.register('object', {
|
|
13
|
+
type: 'object',
|
|
14
|
+
id: object.name,
|
|
15
|
+
content: object
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getConfigsHelper(metadata: MetadataRegistry): Record<string, ObjectConfig> {
|
|
20
|
+
const result: Record<string, ObjectConfig> = {};
|
|
21
|
+
const objects = metadata.list<ObjectConfig>('object');
|
|
22
|
+
for (const obj of objects) {
|
|
23
|
+
result[obj.name] = obj;
|
|
24
|
+
}
|
|
25
|
+
return result;
|
|
26
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ObjectQLPlugin } from '@objectql/types';
|
|
2
|
+
|
|
3
|
+
export function loadPlugin(packageName: string): ObjectQLPlugin {
|
|
4
|
+
let mod: any;
|
|
5
|
+
try {
|
|
6
|
+
const modulePath = require.resolve(packageName, { paths: [process.cwd()] });
|
|
7
|
+
mod = require(modulePath);
|
|
8
|
+
} catch (e) {
|
|
9
|
+
throw new Error(`Failed to resolve plugin '${packageName}': ${e}`);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Helper to find plugin instance
|
|
13
|
+
const findPlugin = (candidate: any): ObjectQLPlugin | undefined => {
|
|
14
|
+
if (!candidate) return undefined;
|
|
15
|
+
|
|
16
|
+
// 1. Try treating as Class
|
|
17
|
+
if (typeof candidate === 'function') {
|
|
18
|
+
try {
|
|
19
|
+
const inst = new candidate();
|
|
20
|
+
if (inst && typeof inst.setup === 'function') {
|
|
21
|
+
return inst; // Found it!
|
|
22
|
+
}
|
|
23
|
+
} catch (e) {
|
|
24
|
+
// Not a constructor or instantiation failed
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 2. Try treating as Instance
|
|
29
|
+
if (candidate && typeof candidate.setup === 'function') {
|
|
30
|
+
if (candidate.name) return candidate;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Search in default, module root, and all named exports
|
|
36
|
+
let instance = findPlugin(mod.default) || findPlugin(mod);
|
|
37
|
+
|
|
38
|
+
if (!instance && mod && typeof mod === 'object') {
|
|
39
|
+
for (const key of Object.keys(mod)) {
|
|
40
|
+
if (key === 'default') continue;
|
|
41
|
+
instance = findPlugin(mod[key]);
|
|
42
|
+
if (instance) break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (instance) {
|
|
47
|
+
(instance as any)._packageName = packageName;
|
|
48
|
+
return instance;
|
|
49
|
+
} else {
|
|
50
|
+
console.error(`[PluginLoader] Failed to find ObjectQLPlugin in '${packageName}'. Exports:`, Object.keys(mod));
|
|
51
|
+
throw new Error(`Plugin '${packageName}' must export a class or object implementing ObjectQLPlugin.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/remote.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { RemoteDriver } from '@objectql/driver-remote';
|
|
2
|
+
import { ObjectConfig } from '@objectql/types';
|
|
3
|
+
|
|
4
|
+
export interface RemoteLoadResult {
|
|
5
|
+
driverName: string;
|
|
6
|
+
driver: RemoteDriver;
|
|
7
|
+
objects: ObjectConfig[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function loadRemoteFromUrl(url: string): Promise<RemoteLoadResult | null> {
|
|
11
|
+
try {
|
|
12
|
+
const baseUrl = url.replace(/\/$/, '');
|
|
13
|
+
const metadataUrl = `${baseUrl}/api/metadata/objects`;
|
|
14
|
+
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
16
|
+
// @ts-ignore - Fetch is available in Node 18+
|
|
17
|
+
const res = await fetch(metadataUrl);
|
|
18
|
+
if (!res.ok) {
|
|
19
|
+
console.warn(`[ObjectQL] Remote ${url} returned ${res.status}`);
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const data = await res.json() as any;
|
|
24
|
+
if (!data || !data.objects) return null;
|
|
25
|
+
|
|
26
|
+
const driverName = `remote:${baseUrl}`;
|
|
27
|
+
const driver = new RemoteDriver(baseUrl);
|
|
28
|
+
const objects: ObjectConfig[] = [];
|
|
29
|
+
|
|
30
|
+
await Promise.all(data.objects.map(async (summary: any) => {
|
|
31
|
+
try {
|
|
32
|
+
// @ts-ignore
|
|
33
|
+
const detailRes = await fetch(`${metadataUrl}/${summary.name}`);
|
|
34
|
+
if (detailRes.ok) {
|
|
35
|
+
const config = await detailRes.json() as ObjectConfig;
|
|
36
|
+
config.datasource = driverName;
|
|
37
|
+
objects.push(config);
|
|
38
|
+
}
|
|
39
|
+
} catch (e) {
|
|
40
|
+
console.warn(`[ObjectQL] Failed to load object ${summary.name} from ${url}`);
|
|
41
|
+
}
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
return { driverName, driver, objects };
|
|
45
|
+
|
|
46
|
+
} catch (e: any) {
|
|
47
|
+
console.warn(`[ObjectQL] Remote connection error ${url}: ${e.message}`);
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|