@objectql/core 1.4.0 → 1.6.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/src/loader.ts DELETED
@@ -1,259 +0,0 @@
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';
6
-
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
- }
127
- }
128
-
129
- private runPlugin(plugin: LoaderPlugin, dir: string, packageName?: string) {
130
- // Enforce path conventions:
131
- // 1. Never scan node_modules (unless explicitly loaded via loadPackage which sets cwd inside it)
132
- // 2. Ignore build artifacts (dist, build, out) to avoid double-loading metadata if both src and dist exist.
133
- // Note: If you want to load from 'dist', you must explicitly point the loader to it (e.g. loader.load('./dist')).
134
- // In that case, the patterns won't match relative to the CWD.
135
- // Path conventions:
136
- // 1. Always ignore node_modules and .git
137
- const ignore = [
138
- '**/node_modules/**',
139
- '**/.git/**'
140
- ];
141
-
142
- // 2. Intelligent handling of build artifacts (dist/build)
143
- // If 'src' exists in the scan directory, we assume it's a Development Environment.
144
- // In Dev, we ignore 'dist' to avoid duplicate loading (ts in src vs js in dist).
145
- // In Production (no src), we must NOT ignore 'dist', otherwise we can't load compiled hooks/actions.
146
- const srcPath = path.join(dir, 'src');
147
- const hasSrc = fs.existsSync(srcPath) && fs.statSync(srcPath).isDirectory();
148
-
149
- if (hasSrc) {
150
- ignore.push('**/dist/**', '**/build/**', '**/out/**');
151
- }
152
-
153
- // 3. User instruction: "src 不行的" (src is not viable for metadata in production)
154
- // Metadata (.yml) should ideally be placed in 'objects/' or root, not 'src/',
155
- // to simplify packaging (so you don't need to copy assets from src to dist).
156
- // However, we do not strictly block 'src' scanning here to avoid breaking dev workflow.
157
- // The exclusion of 'dist' in dev mode (above) handles the code duality.
158
-
159
- const files = glob.sync(plugin.glob, {
160
- cwd: dir,
161
- absolute: true,
162
- ignore
163
- });
164
-
165
- for (const file of files) {
166
- try {
167
- const ctx: LoaderHandlerContext = {
168
- file,
169
- content: '',
170
- registry: this.registry,
171
- packageName
172
- };
173
-
174
- // Pre-read for convenience
175
- if (!file.match(/\.(js|ts|node)$/)) {
176
- ctx.content = fs.readFileSync(file, 'utf8');
177
- }
178
-
179
- plugin.handler(ctx);
180
-
181
- } catch (e) {
182
- console.error(`Error in loader plugin '${plugin.name}' processing ${file}:`, e);
183
- }
184
- }
185
- }
186
- }
187
-
188
- function registerObject(registry: MetadataRegistry, obj: any, file: string, packageName?: string) {
189
- // Normalize fields
190
- if (obj.fields) {
191
- for (const [key, field] of Object.entries(obj.fields)) {
192
- if (typeof field === 'object' && field !== null) {
193
- if (!(field as any).name) {
194
- (field as any).name = key;
195
- }
196
- }
197
- }
198
- }
199
-
200
- // Check for existing object to Merge
201
- const existing = registry.getEntry('object', obj.name);
202
- if (existing) {
203
- const base = existing.content;
204
-
205
- // Merge Fields: New fields overwrite old ones
206
- if (obj.fields) {
207
- base.fields = { ...base.fields, ...obj.fields };
208
- }
209
-
210
- // Merge Actions
211
- if (obj.actions) {
212
- base.actions = { ...base.actions, ...obj.actions };
213
- }
214
-
215
- // Merge Indexes
216
- if (obj.indexes) {
217
- base.indexes = { ...base.indexes, ...obj.indexes };
218
- }
219
-
220
- // Override Top-level Properties if provided
221
- if (obj.label) base.label = obj.label;
222
- if (obj.icon) base.icon = obj.icon;
223
- if (obj.description) base.description = obj.description;
224
- if (obj.datasource) base.datasource = obj.datasource;
225
-
226
- // Update the content reference
227
- existing.content = base;
228
- return;
229
- }
230
-
231
- registry.register('object', {
232
- type: 'object',
233
- id: obj.name,
234
- path: file,
235
- package: packageName,
236
- content: obj
237
- });
238
- }
239
-
240
- export function loadObjectConfigs(dir: string): Record<string, ObjectConfig> {
241
- const registry = new MetadataRegistry();
242
- const loader = new ObjectLoader(registry);
243
- loader.load(dir);
244
-
245
- // Merge actions into objects
246
- const actions = registry.list<any>('action');
247
- for (const act of actions) {
248
- const obj = registry.get<ObjectConfig>('object', act.id);
249
- if (obj) {
250
- obj.actions = act.content;
251
- }
252
- }
253
-
254
- const result: Record<string, ObjectConfig> = {};
255
- for (const obj of registry.list<ObjectConfig>('object')) {
256
- result[obj.name] = obj;
257
- }
258
- return result;
259
- }
package/src/plugin.ts DELETED
@@ -1,53 +0,0 @@
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 DELETED
@@ -1,50 +0,0 @@
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
- }
@@ -1,34 +0,0 @@
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
- });
@@ -1,8 +0,0 @@
1
- module.exports = {
2
- listenTo: 'project',
3
- closeProject: {
4
- handler: async function(ctx) {
5
- return { success: true };
6
- }
7
- }
8
- };
@@ -1,41 +0,0 @@
1
- name: project
2
- label: Project
3
- icon: standard:case
4
- enable_search: true
5
- fields:
6
- name:
7
- label: Project Name
8
- type: text
9
- required: true
10
- searchable: true
11
- index: true
12
-
13
- status:
14
- label: Status
15
- type: select
16
- options:
17
- - label: Planned
18
- value: planned
19
- - label: In Progress
20
- value: in_progress
21
- - label: Completed
22
- value: completed
23
- defaultValue: planned
24
-
25
- start_date:
26
- label: Start Date
27
- type: date
28
-
29
- owner:
30
- label: Project Manager
31
- type: lookup
32
- reference_to: users
33
-
34
- budget:
35
- label: Total Budget
36
- type: currency
37
- scale: 2
38
-
39
- description:
40
- label: Description
41
- type: textarea
@@ -1,15 +0,0 @@
1
- import { loadObjectConfigs } from '../src/loader';
2
- import * as path from 'path';
3
-
4
- describe('Loader', () => {
5
- it('should load object configs from directory', () => {
6
- const fixturesDir = path.join(__dirname, 'fixtures');
7
- const configs = loadObjectConfigs(fixturesDir);
8
- expect(configs).toBeDefined();
9
- expect(configs['project']).toBeDefined();
10
- expect(configs['project'].name).toBe('project');
11
- expect(configs['project'].fields).toBeDefined();
12
- expect(configs['project'].fields.name).toBeDefined();
13
- });
14
- });
15
-
@@ -1,49 +0,0 @@
1
- import { ObjectQL } from '../src/index';
2
- import { ObjectConfig } from '@objectql/types';
3
- import * as fs from 'fs';
4
- import * as path from 'path';
5
- import * as yaml from 'js-yaml';
6
-
7
- describe('Metadata Loading', () => {
8
-
9
- it('should load definitions from .object.yml file', () => {
10
- // 1. Read YAML file
11
- const yamlPath = path.join(__dirname, 'fixtures', 'project.object.yml');
12
- const fileContents = fs.readFileSync(yamlPath, 'utf8');
13
-
14
- // 2. Parse YAML
15
- const objectDef = yaml.load(fileContents) as ObjectConfig;
16
-
17
- // 3. Verify Structure
18
- expect(objectDef.name).toBe('project');
19
- expect(objectDef.fields.name.type).toBe('text');
20
- expect(objectDef.fields.status.options).toHaveLength(3);
21
- expect(objectDef.fields.budget.type).toBe('currency');
22
- expect(objectDef.fields.owner.reference_to).toBe('users');
23
-
24
- // 4. Register with ObjectQL
25
- const app = new ObjectQL({
26
- datasources: {}
27
- });
28
-
29
- app.registerObject(objectDef);
30
-
31
- // 5. Verify Registration
32
- const retrieved = app.getObject('project');
33
- expect(retrieved).toBeDefined();
34
- expect(retrieved?.label).toBe('Project');
35
- });
36
-
37
- it('should validate required properties (manual validation simulation)', () => {
38
- const yamlPath = path.join(__dirname, 'fixtures', 'project.object.yml');
39
- const fileContents = fs.readFileSync(yamlPath, 'utf8');
40
- const objectDef = yaml.load(fileContents) as ObjectConfig;
41
-
42
- function validateObject(obj: ObjectConfig) {
43
- if (!obj.name) throw new Error('Object name is required');
44
- if (!obj.fields) throw new Error('Object fields are required');
45
- }
46
-
47
- expect(() => validateObject(objectDef)).not.toThrow();
48
- });
49
- });
@@ -1,119 +0,0 @@
1
-
2
- import { ObjectQL } from '../src';
3
- import { ObjectConfig } from '@objectql/types';
4
-
5
- describe('ObjectQL Remote Federation', () => {
6
- let originalFetch: any;
7
-
8
- beforeAll(() => {
9
- originalFetch = global.fetch;
10
- });
11
-
12
- afterAll(() => {
13
- global.fetch = originalFetch;
14
- });
15
-
16
- it('should load remote objects and proxy queries', async () => {
17
- // 1. Mock Fetch
18
- const mockFetch = jest.fn();
19
- global.fetch = mockFetch;
20
-
21
- const remoteUrl = 'http://remote-service:3000';
22
-
23
- // Mock Responses
24
- mockFetch.mockImplementation(async (url: string, options: any) => {
25
- // A. Metadata List
26
- if (url === `${remoteUrl}/api/metadata/objects`) {
27
- return {
28
- ok: true,
29
- json: async () => ({
30
- objects: [
31
- { name: 'remote_user', label: 'Remote User' }
32
- ]
33
- })
34
- };
35
- }
36
-
37
- // B. Object Detail
38
- if (url === `${remoteUrl}/api/metadata/objects/remote_user`) {
39
- return {
40
- ok: true,
41
- json: async () => ({
42
- name: 'remote_user',
43
- fields: {
44
- name: { type: 'text' },
45
- email: { type: 'text' }
46
- }
47
- } as ObjectConfig)
48
- };
49
- }
50
-
51
- // C. Data Query (find)
52
- if (url === `${remoteUrl}/api/objectql`) {
53
- const body = JSON.parse(options.body);
54
- if (body.op === 'find' && body.object === 'remote_user') {
55
- return {
56
- ok: true,
57
- json: async () => ({
58
- data: [
59
- { id: 1, name: 'Alice', email: 'alice@example.com' }
60
- ]
61
- })
62
- };
63
- }
64
- }
65
-
66
- return { ok: false, status: 404 };
67
- });
68
-
69
- // 2. Init ObjectQL with remotes
70
- const app = new ObjectQL({
71
- remotes: [remoteUrl]
72
- });
73
-
74
- await app.init();
75
-
76
- // 3. Verify Schema is loaded
77
- const config = app.getObject('remote_user');
78
- expect(config).toBeDefined();
79
- expect(config?.datasource).toBe(`remote:${remoteUrl}`);
80
-
81
- // 4. Verify Query is proxied
82
- // Note: 'object()' is on Context, not App. We need to create a context first.
83
- const ctx = app.createContext({});
84
- const users = await ctx.object('remote_user').find();
85
-
86
- expect(users).toHaveLength(1);
87
- expect(users[0].name).toBe('Alice');
88
-
89
- // Verify fetch was called correctly
90
- expect(mockFetch).toHaveBeenCalledTimes(3);
91
- // 1. api/metadata/objects -> List
92
- // 2. api/metadata/objects/remote_user -> Detail
93
- // 3. api/objectql -> Query
94
- });
95
-
96
- it('should handle remote errors gracefully', async () => {
97
- const mockFetch = jest.fn();
98
- global.fetch = mockFetch;
99
- const remoteUrl = 'http://broken-service:3000';
100
-
101
- // Mock Failure
102
- mockFetch.mockResolvedValue({
103
- ok: false,
104
- status: 500,
105
- statusText: 'Internal Server Error'
106
- });
107
-
108
- const app = new ObjectQL({
109
- remotes: [remoteUrl]
110
- });
111
-
112
- // Should not throw, just log warning (which we can spy on if we want, but preventing crash is key)
113
- await expect(app.init()).resolves.not.toThrow();
114
-
115
- // Object shouldn't exist
116
- const config = app.getObject('remote_user');
117
- expect(config).toBeUndefined();
118
- });
119
- });