@objectql/core 1.5.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/CHANGELOG.md +11 -0
- package/dist/app.d.ts +1 -6
- package/dist/app.js +19 -131
- package/dist/app.js.map +1 -1
- package/dist/index.d.ts +2 -4
- package/dist/index.js +2 -4
- package/dist/index.js.map +1 -1
- package/dist/util.d.ts +1 -0
- package/dist/util.js +9 -0
- package/dist/util.js.map +1 -0
- package/dist/validator.js +8 -5
- package/dist/validator.js.map +1 -1
- package/package.json +2 -5
- package/src/app.ts +22 -111
- package/src/index.ts +4 -4
- package/src/util.ts +5 -0
- package/tsconfig.json +2 -3
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/driver.d.ts +0 -2
- package/dist/driver.js +0 -55
- package/dist/driver.js.map +0 -1
- package/dist/loader.d.ts +0 -12
- package/dist/loader.js +0 -318
- package/dist/loader.js.map +0 -1
- package/dist/plugin.d.ts +0 -2
- package/dist/plugin.js +0 -56
- package/dist/plugin.js.map +0 -1
- package/dist/remote.d.ts +0 -8
- package/dist/remote.js +0 -43
- package/dist/remote.js.map +0 -1
- package/src/driver.ts +0 -54
- package/src/loader.ts +0 -304
- package/src/plugin.ts +0 -53
- package/src/remote.ts +0 -50
- package/test/dynamic.test.ts +0 -34
- package/test/fixtures/project-with-validation.object.yml +0 -124
- package/test/fixtures/project.action.js +0 -8
- package/test/fixtures/project.object.yml +0 -41
- package/test/loader.test.ts +0 -15
- package/test/metadata.test.ts +0 -49
- package/test/remote.test.ts +0 -119
- package/test/validation.test.ts +0 -486
package/src/loader.ts
DELETED
|
@@ -1,304 +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
|
-
// Generic YAML Metadata Loaders
|
|
106
|
-
const metaTypes = ['view', 'form', 'menu', 'permission', 'report', 'workflow', 'validation', 'data'];
|
|
107
|
-
|
|
108
|
-
for (const type of metaTypes) {
|
|
109
|
-
this.use({
|
|
110
|
-
name: type,
|
|
111
|
-
glob: [`**/*.${type}.yml`, `**/*.${type}.yaml`],
|
|
112
|
-
handler: (ctx) => {
|
|
113
|
-
try {
|
|
114
|
-
const doc = yaml.load(ctx.content) as any;
|
|
115
|
-
if (!doc) return;
|
|
116
|
-
|
|
117
|
-
// Use 'name' from doc, or filename base (without extension)
|
|
118
|
-
let id = doc.name;
|
|
119
|
-
if (!id && type !== 'data') {
|
|
120
|
-
const basename = path.basename(ctx.file);
|
|
121
|
-
// e.g. "my-view.view.yml" -> "my-view"
|
|
122
|
-
// Regex: remove .type.yml or .type.yaml
|
|
123
|
-
const re = new RegExp(`\\.${type}\\.(yml|yaml)$`);
|
|
124
|
-
id = basename.replace(re, '');
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Data entries might not need a name, but for registry we need an ID.
|
|
128
|
-
// For data, we can use filename if not present.
|
|
129
|
-
if (!id && type === 'data') {
|
|
130
|
-
id = path.basename(ctx.file);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Ensure name is in the doc for consistency
|
|
134
|
-
if (!doc.name) doc.name = id;
|
|
135
|
-
|
|
136
|
-
ctx.registry.register(type, {
|
|
137
|
-
type: type,
|
|
138
|
-
id: id,
|
|
139
|
-
path: ctx.file,
|
|
140
|
-
package: ctx.packageName,
|
|
141
|
-
content: doc
|
|
142
|
-
});
|
|
143
|
-
} catch (e) {
|
|
144
|
-
console.error(`Error loading ${type} from ${ctx.file}:`, e);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
})
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
use(plugin: LoaderPlugin) {
|
|
152
|
-
this.plugins.push(plugin);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
load(dir: string, packageName?: string) {
|
|
156
|
-
for (const plugin of this.plugins) {
|
|
157
|
-
this.runPlugin(plugin, dir, packageName);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
loadPackage(packageName: string) {
|
|
162
|
-
try {
|
|
163
|
-
const entryPath = require.resolve(packageName, { paths: [process.cwd()] });
|
|
164
|
-
// clean cache
|
|
165
|
-
delete require.cache[entryPath];
|
|
166
|
-
const packageDir = path.dirname(entryPath);
|
|
167
|
-
this.load(packageDir, packageName);
|
|
168
|
-
} catch (e) {
|
|
169
|
-
// fallback to directory
|
|
170
|
-
this.load(packageName, packageName);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
private runPlugin(plugin: LoaderPlugin, dir: string, packageName?: string) {
|
|
175
|
-
// Enforce path conventions:
|
|
176
|
-
// 1. Never scan node_modules (unless explicitly loaded via loadPackage which sets cwd inside it)
|
|
177
|
-
// 2. Ignore build artifacts (dist, build, out) to avoid double-loading metadata if both src and dist exist.
|
|
178
|
-
// Note: If you want to load from 'dist', you must explicitly point the loader to it (e.g. loader.load('./dist')).
|
|
179
|
-
// In that case, the patterns won't match relative to the CWD.
|
|
180
|
-
// Path conventions:
|
|
181
|
-
// 1. Always ignore node_modules and .git
|
|
182
|
-
const ignore = [
|
|
183
|
-
'**/node_modules/**',
|
|
184
|
-
'**/.git/**'
|
|
185
|
-
];
|
|
186
|
-
|
|
187
|
-
// 2. Intelligent handling of build artifacts (dist/build)
|
|
188
|
-
// If 'src' exists in the scan directory, we assume it's a Development Environment.
|
|
189
|
-
// In Dev, we ignore 'dist' to avoid duplicate loading (ts in src vs js in dist).
|
|
190
|
-
// In Production (no src), we must NOT ignore 'dist', otherwise we can't load compiled hooks/actions.
|
|
191
|
-
const srcPath = path.join(dir, 'src');
|
|
192
|
-
const hasSrc = fs.existsSync(srcPath) && fs.statSync(srcPath).isDirectory();
|
|
193
|
-
|
|
194
|
-
if (hasSrc) {
|
|
195
|
-
ignore.push('**/dist/**', '**/build/**', '**/out/**');
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// 3. User instruction: "src 不行的" (src is not viable for metadata in production)
|
|
199
|
-
// Metadata (.yml) should ideally be placed in 'objects/' or root, not 'src/',
|
|
200
|
-
// to simplify packaging (so you don't need to copy assets from src to dist).
|
|
201
|
-
// However, we do not strictly block 'src' scanning here to avoid breaking dev workflow.
|
|
202
|
-
// The exclusion of 'dist' in dev mode (above) handles the code duality.
|
|
203
|
-
|
|
204
|
-
const files = glob.sync(plugin.glob, {
|
|
205
|
-
cwd: dir,
|
|
206
|
-
absolute: true,
|
|
207
|
-
ignore
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
for (const file of files) {
|
|
211
|
-
try {
|
|
212
|
-
const ctx: LoaderHandlerContext = {
|
|
213
|
-
file,
|
|
214
|
-
content: '',
|
|
215
|
-
registry: this.registry,
|
|
216
|
-
packageName
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
// Pre-read for convenience
|
|
220
|
-
if (!file.match(/\.(js|ts|node)$/)) {
|
|
221
|
-
ctx.content = fs.readFileSync(file, 'utf8');
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
plugin.handler(ctx);
|
|
225
|
-
|
|
226
|
-
} catch (e) {
|
|
227
|
-
console.error(`Error in loader plugin '${plugin.name}' processing ${file}:`, e);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
function registerObject(registry: MetadataRegistry, obj: any, file: string, packageName?: string) {
|
|
234
|
-
// Normalize fields
|
|
235
|
-
if (obj.fields) {
|
|
236
|
-
for (const [key, field] of Object.entries(obj.fields)) {
|
|
237
|
-
if (typeof field === 'object' && field !== null) {
|
|
238
|
-
if (!(field as any).name) {
|
|
239
|
-
(field as any).name = key;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// Check for existing object to Merge
|
|
246
|
-
const existing = registry.getEntry('object', obj.name);
|
|
247
|
-
if (existing) {
|
|
248
|
-
const base = existing.content;
|
|
249
|
-
|
|
250
|
-
// Merge Fields: New fields overwrite old ones
|
|
251
|
-
if (obj.fields) {
|
|
252
|
-
base.fields = { ...base.fields, ...obj.fields };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// Merge Actions
|
|
256
|
-
if (obj.actions) {
|
|
257
|
-
base.actions = { ...base.actions, ...obj.actions };
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Merge Indexes
|
|
261
|
-
if (obj.indexes) {
|
|
262
|
-
base.indexes = { ...base.indexes, ...obj.indexes };
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Override Top-level Properties if provided
|
|
266
|
-
if (obj.label) base.label = obj.label;
|
|
267
|
-
if (obj.icon) base.icon = obj.icon;
|
|
268
|
-
if (obj.description) base.description = obj.description;
|
|
269
|
-
if (obj.datasource) base.datasource = obj.datasource;
|
|
270
|
-
|
|
271
|
-
// Update the content reference
|
|
272
|
-
existing.content = base;
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
registry.register('object', {
|
|
277
|
-
type: 'object',
|
|
278
|
-
id: obj.name,
|
|
279
|
-
path: file,
|
|
280
|
-
package: packageName,
|
|
281
|
-
content: obj
|
|
282
|
-
});
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
export function loadObjectConfigs(dir: string): Record<string, ObjectConfig> {
|
|
286
|
-
const registry = new MetadataRegistry();
|
|
287
|
-
const loader = new ObjectLoader(registry);
|
|
288
|
-
loader.load(dir);
|
|
289
|
-
|
|
290
|
-
// Merge actions into objects
|
|
291
|
-
const actions = registry.list<any>('action');
|
|
292
|
-
for (const act of actions) {
|
|
293
|
-
const obj = registry.get<ObjectConfig>('object', act.id);
|
|
294
|
-
if (obj) {
|
|
295
|
-
obj.actions = act.content;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const result: Record<string, ObjectConfig> = {};
|
|
300
|
-
for (const obj of registry.list<ObjectConfig>('object')) {
|
|
301
|
-
result[obj.name] = obj;
|
|
302
|
-
}
|
|
303
|
-
return result;
|
|
304
|
-
}
|
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
|
-
}
|
package/test/dynamic.test.ts
DELETED
|
@@ -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,124 +0,0 @@
|
|
|
1
|
-
name: project
|
|
2
|
-
label: Project
|
|
3
|
-
description: Project object with validation rules
|
|
4
|
-
|
|
5
|
-
fields:
|
|
6
|
-
name:
|
|
7
|
-
type: text
|
|
8
|
-
label: Project Name
|
|
9
|
-
required: true
|
|
10
|
-
validation:
|
|
11
|
-
min_length: 3
|
|
12
|
-
max_length: 100
|
|
13
|
-
message: Project name must be between 3 and 100 characters
|
|
14
|
-
ai_context:
|
|
15
|
-
intent: Unique identifier for the project
|
|
16
|
-
|
|
17
|
-
description:
|
|
18
|
-
type: textarea
|
|
19
|
-
label: Description
|
|
20
|
-
|
|
21
|
-
status:
|
|
22
|
-
type: select
|
|
23
|
-
label: Status
|
|
24
|
-
required: true
|
|
25
|
-
defaultValue: planning
|
|
26
|
-
options:
|
|
27
|
-
- label: Planning
|
|
28
|
-
value: planning
|
|
29
|
-
- label: Active
|
|
30
|
-
value: active
|
|
31
|
-
- label: On Hold
|
|
32
|
-
value: on_hold
|
|
33
|
-
- label: Completed
|
|
34
|
-
value: completed
|
|
35
|
-
- label: Cancelled
|
|
36
|
-
value: cancelled
|
|
37
|
-
ai_context:
|
|
38
|
-
intent: Track project through its lifecycle
|
|
39
|
-
is_state_machine: true
|
|
40
|
-
|
|
41
|
-
budget:
|
|
42
|
-
type: currency
|
|
43
|
-
label: Budget
|
|
44
|
-
validation:
|
|
45
|
-
min: 0
|
|
46
|
-
max: 10000000
|
|
47
|
-
message: Budget must be between 0 and 10,000,000
|
|
48
|
-
|
|
49
|
-
start_date:
|
|
50
|
-
type: date
|
|
51
|
-
label: Start Date
|
|
52
|
-
required: true
|
|
53
|
-
|
|
54
|
-
end_date:
|
|
55
|
-
type: date
|
|
56
|
-
label: End Date
|
|
57
|
-
|
|
58
|
-
email:
|
|
59
|
-
type: email
|
|
60
|
-
label: Contact Email
|
|
61
|
-
validation:
|
|
62
|
-
format: email
|
|
63
|
-
message: Please enter a valid email address
|
|
64
|
-
|
|
65
|
-
website:
|
|
66
|
-
type: url
|
|
67
|
-
label: Website
|
|
68
|
-
validation:
|
|
69
|
-
format: url
|
|
70
|
-
protocols: [http, https]
|
|
71
|
-
message: Please enter a valid URL
|
|
72
|
-
|
|
73
|
-
validation:
|
|
74
|
-
ai_context:
|
|
75
|
-
intent: Ensure project data integrity and enforce business rules
|
|
76
|
-
validation_strategy: Fail fast with clear error messages
|
|
77
|
-
|
|
78
|
-
rules:
|
|
79
|
-
# Cross-field validation: End date must be after start date
|
|
80
|
-
- name: valid_date_range
|
|
81
|
-
type: cross_field
|
|
82
|
-
ai_context:
|
|
83
|
-
intent: Ensure timeline makes logical sense
|
|
84
|
-
business_rule: Projects cannot end before they start
|
|
85
|
-
error_impact: high
|
|
86
|
-
rule:
|
|
87
|
-
field: end_date
|
|
88
|
-
operator: ">="
|
|
89
|
-
compare_to: start_date
|
|
90
|
-
message: End date must be on or after start date
|
|
91
|
-
error_code: INVALID_DATE_RANGE
|
|
92
|
-
|
|
93
|
-
# State machine validation
|
|
94
|
-
- name: status_transition
|
|
95
|
-
type: state_machine
|
|
96
|
-
field: status
|
|
97
|
-
ai_context:
|
|
98
|
-
intent: Control valid status transitions throughout project lifecycle
|
|
99
|
-
business_rule: Projects follow a controlled workflow
|
|
100
|
-
transitions:
|
|
101
|
-
planning:
|
|
102
|
-
allowed_next: [active, cancelled]
|
|
103
|
-
ai_context:
|
|
104
|
-
rationale: Can start work or cancel before beginning
|
|
105
|
-
active:
|
|
106
|
-
allowed_next: [on_hold, completed, cancelled]
|
|
107
|
-
ai_context:
|
|
108
|
-
rationale: Can pause, finish, or cancel ongoing work
|
|
109
|
-
on_hold:
|
|
110
|
-
allowed_next: [active, cancelled]
|
|
111
|
-
ai_context:
|
|
112
|
-
rationale: Can resume or cancel paused projects
|
|
113
|
-
completed:
|
|
114
|
-
allowed_next: []
|
|
115
|
-
is_terminal: true
|
|
116
|
-
ai_context:
|
|
117
|
-
rationale: Finished projects cannot change state
|
|
118
|
-
cancelled:
|
|
119
|
-
allowed_next: []
|
|
120
|
-
is_terminal: true
|
|
121
|
-
ai_context:
|
|
122
|
-
rationale: Cancelled projects are final
|
|
123
|
-
message: "Invalid status transition from {{old_status}} to {{new_status}}"
|
|
124
|
-
error_code: INVALID_STATE_TRANSITION
|
|
@@ -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
|
package/test/loader.test.ts
DELETED
|
@@ -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
|
-
|
package/test/metadata.test.ts
DELETED
|
@@ -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
|
-
});
|