@objectstack/objectql 1.0.10 → 1.0.12
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +17 -0
- package/dist/index.d.mts +713 -33
- package/dist/index.d.ts +713 -33
- package/dist/index.js +585 -67
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +580 -67
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -4
- package/src/engine.test.ts +60 -3
- package/src/engine.ts +115 -8
- package/src/index.ts +9 -1
- package/src/plugin.ts +3 -1
- package/src/protocol.ts +63 -22
- package/src/registry.test.ts +456 -25
- package/src/registry.ts +609 -53
package/src/engine.ts
CHANGED
|
@@ -140,31 +140,113 @@ export class ObjectQL implements IDataEngine {
|
|
|
140
140
|
|
|
141
141
|
/**
|
|
142
142
|
* Register contribution (Manifest)
|
|
143
|
+
*
|
|
144
|
+
* Installs the manifest as a Package (the unit of installation),
|
|
145
|
+
* then decomposes it into individual metadata items (objects, apps, actions, etc.)
|
|
146
|
+
* and registers each into the SchemaRegistry.
|
|
147
|
+
*
|
|
148
|
+
* Key: Package ≠ App. The manifest is the package. The apps[] array inside
|
|
149
|
+
* the manifest contains UI navigation definitions (AppSchema).
|
|
143
150
|
*/
|
|
144
151
|
registerApp(manifest: any) {
|
|
145
|
-
const id = manifest.id;
|
|
146
|
-
|
|
152
|
+
const id = manifest.id || manifest.name;
|
|
153
|
+
const namespace = manifest.namespace as string | undefined;
|
|
154
|
+
this.logger.debug('Registering package manifest', { id, namespace });
|
|
147
155
|
|
|
148
|
-
// Register
|
|
156
|
+
// 1. Register the Package (manifest + lifecycle state)
|
|
157
|
+
SchemaRegistry.installPackage(manifest);
|
|
158
|
+
this.logger.debug('Installed Package', { id: manifest.id, name: manifest.name, namespace });
|
|
159
|
+
|
|
160
|
+
// 2. Register owned objects
|
|
149
161
|
if (manifest.objects) {
|
|
150
162
|
if (Array.isArray(manifest.objects)) {
|
|
151
163
|
this.logger.debug('Registering objects from manifest (Array)', { id, objectCount: manifest.objects.length });
|
|
152
164
|
for (const objDef of manifest.objects) {
|
|
153
|
-
SchemaRegistry.registerObject(objDef);
|
|
154
|
-
this.logger.debug('Registered Object', {
|
|
165
|
+
const fqn = SchemaRegistry.registerObject(objDef, id, namespace, 'own');
|
|
166
|
+
this.logger.debug('Registered Object', { fqn, from: id });
|
|
155
167
|
}
|
|
156
168
|
} else {
|
|
157
169
|
this.logger.debug('Registering objects from manifest (Map)', { id, objectCount: Object.keys(manifest.objects).length });
|
|
158
170
|
for (const [name, objDef] of Object.entries(manifest.objects)) {
|
|
159
171
|
// Ensure name in definition matches key
|
|
160
172
|
(objDef as any).name = name;
|
|
161
|
-
SchemaRegistry.registerObject(objDef as any);
|
|
162
|
-
this.logger.debug('Registered Object', {
|
|
173
|
+
const fqn = SchemaRegistry.registerObject(objDef as any, id, namespace, 'own');
|
|
174
|
+
this.logger.debug('Registered Object', { fqn, from: id });
|
|
163
175
|
}
|
|
164
176
|
}
|
|
165
177
|
}
|
|
166
178
|
|
|
167
|
-
// Register
|
|
179
|
+
// 2b. Register object extensions (fields added to objects owned by other packages)
|
|
180
|
+
if (Array.isArray(manifest.objectExtensions) && manifest.objectExtensions.length > 0) {
|
|
181
|
+
this.logger.debug('Registering object extensions', { id, count: manifest.objectExtensions.length });
|
|
182
|
+
for (const ext of manifest.objectExtensions) {
|
|
183
|
+
const targetFqn = ext.extend;
|
|
184
|
+
const priority = ext.priority ?? 200;
|
|
185
|
+
// Create a partial object definition for the extension
|
|
186
|
+
const extDef = {
|
|
187
|
+
name: targetFqn, // Use the target FQN as name
|
|
188
|
+
fields: ext.fields,
|
|
189
|
+
label: ext.label,
|
|
190
|
+
pluralLabel: ext.pluralLabel,
|
|
191
|
+
description: ext.description,
|
|
192
|
+
validations: ext.validations,
|
|
193
|
+
indexes: ext.indexes,
|
|
194
|
+
};
|
|
195
|
+
// Register as extension (namespace is undefined since we're targeting by FQN)
|
|
196
|
+
SchemaRegistry.registerObject(extDef as any, id, undefined, 'extend', priority);
|
|
197
|
+
this.logger.debug('Registered Object Extension', { target: targetFqn, priority, from: id });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 3. Register apps (UI navigation definitions) as their own metadata type
|
|
202
|
+
if (Array.isArray(manifest.apps) && manifest.apps.length > 0) {
|
|
203
|
+
this.logger.debug('Registering apps from manifest', { id, count: manifest.apps.length });
|
|
204
|
+
for (const app of manifest.apps) {
|
|
205
|
+
const appName = app.name || app.id;
|
|
206
|
+
if (appName) {
|
|
207
|
+
SchemaRegistry.registerApp(app, id);
|
|
208
|
+
this.logger.debug('Registered App', { app: appName, from: id });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 4. If manifest itself looks like an App (has navigation), also register as app
|
|
214
|
+
// This handles the case where the manifest IS the app definition (legacy/simple packages)
|
|
215
|
+
if (manifest.name && manifest.navigation && !manifest.apps?.length) {
|
|
216
|
+
SchemaRegistry.registerApp(manifest, id);
|
|
217
|
+
this.logger.debug('Registered manifest-as-app', { app: manifest.name, from: id });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// 5. Register all other metadata types generically
|
|
221
|
+
const metadataArrayKeys = [
|
|
222
|
+
'actions', 'dashboards', 'reports', 'flows', 'agents',
|
|
223
|
+
'apis', 'ragPipelines', 'profiles', 'sharingRules'
|
|
224
|
+
];
|
|
225
|
+
for (const key of metadataArrayKeys) {
|
|
226
|
+
const items = (manifest as any)[key];
|
|
227
|
+
if (Array.isArray(items) && items.length > 0) {
|
|
228
|
+
this.logger.debug(`Registering ${key} from manifest`, { id, count: items.length });
|
|
229
|
+
for (const item of items) {
|
|
230
|
+
const itemName = item.name || item.id;
|
|
231
|
+
if (itemName) {
|
|
232
|
+
SchemaRegistry.registerItem(key, item, 'name' as any, id);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// 6. Register seed data as metadata (keyed by target object name)
|
|
239
|
+
const seedData = (manifest as any).data;
|
|
240
|
+
if (Array.isArray(seedData) && seedData.length > 0) {
|
|
241
|
+
this.logger.debug('Registering seed data datasets', { id, count: seedData.length });
|
|
242
|
+
for (const dataset of seedData) {
|
|
243
|
+
if (dataset.object) {
|
|
244
|
+
SchemaRegistry.registerItem('data', dataset, 'object' as any, id);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 6. Register contributions
|
|
168
250
|
if (manifest.contributes?.kinds) {
|
|
169
251
|
this.logger.debug('Registering kinds from manifest', { id, kindCount: manifest.contributes.kinds.length });
|
|
170
252
|
for (const kind of manifest.contributes.kinds) {
|
|
@@ -202,6 +284,24 @@ export class ObjectQL implements IDataEngine {
|
|
|
202
284
|
return SchemaRegistry.getObject(objectName);
|
|
203
285
|
}
|
|
204
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Resolve an object name to its Fully Qualified Name (FQN).
|
|
289
|
+
*
|
|
290
|
+
* Short names like 'task' are resolved to FQN like 'todo__task'
|
|
291
|
+
* via SchemaRegistry lookup. If no match is found, the name is
|
|
292
|
+
* returned as-is (for ad-hoc / unregistered objects).
|
|
293
|
+
*
|
|
294
|
+
* This ensures that all driver operations use a consistent key
|
|
295
|
+
* regardless of whether the caller uses the short name or FQN.
|
|
296
|
+
*/
|
|
297
|
+
private resolveObjectName(name: string): string {
|
|
298
|
+
const schema = SchemaRegistry.getObject(name);
|
|
299
|
+
if (schema) {
|
|
300
|
+
return schema.name; // FQN from registry (e.g., 'todo__task')
|
|
301
|
+
}
|
|
302
|
+
return name; // Ad-hoc object, keep as-is
|
|
303
|
+
}
|
|
304
|
+
|
|
205
305
|
/**
|
|
206
306
|
* Helper to get the target driver
|
|
207
307
|
*/
|
|
@@ -311,6 +411,7 @@ export class ObjectQL implements IDataEngine {
|
|
|
311
411
|
// ============================================
|
|
312
412
|
|
|
313
413
|
async find(object: string, query?: DataEngineQueryOptions): Promise<any[]> {
|
|
414
|
+
object = this.resolveObjectName(object);
|
|
314
415
|
this.logger.debug('Find operation starting', { object, query });
|
|
315
416
|
const driver = this.getDriver(object);
|
|
316
417
|
const ast = this.toQueryAST(object, query);
|
|
@@ -338,6 +439,7 @@ export class ObjectQL implements IDataEngine {
|
|
|
338
439
|
}
|
|
339
440
|
|
|
340
441
|
async findOne(objectName: string, query?: DataEngineQueryOptions): Promise<any> {
|
|
442
|
+
objectName = this.resolveObjectName(objectName);
|
|
341
443
|
this.logger.debug('FindOne operation', { objectName });
|
|
342
444
|
const driver = this.getDriver(objectName);
|
|
343
445
|
const ast = this.toQueryAST(objectName, query);
|
|
@@ -349,6 +451,7 @@ export class ObjectQL implements IDataEngine {
|
|
|
349
451
|
}
|
|
350
452
|
|
|
351
453
|
async insert(object: string, data: any | any[], options?: DataEngineInsertOptions): Promise<any> {
|
|
454
|
+
object = this.resolveObjectName(object);
|
|
352
455
|
this.logger.debug('Insert operation starting', { object, isBatch: Array.isArray(data) });
|
|
353
456
|
const driver = this.getDriver(object);
|
|
354
457
|
|
|
@@ -386,6 +489,7 @@ export class ObjectQL implements IDataEngine {
|
|
|
386
489
|
}
|
|
387
490
|
|
|
388
491
|
async update(object: string, data: any, options?: DataEngineUpdateOptions): Promise<any> {
|
|
492
|
+
object = this.resolveObjectName(object);
|
|
389
493
|
// NOTE: This signature is tricky because Driver expects (obj, id, data) usually.
|
|
390
494
|
// DataEngine protocol puts filter in options.
|
|
391
495
|
this.logger.debug('Update operation starting', { object });
|
|
@@ -433,6 +537,7 @@ export class ObjectQL implements IDataEngine {
|
|
|
433
537
|
}
|
|
434
538
|
|
|
435
539
|
async delete(object: string, options?: DataEngineDeleteOptions): Promise<any> {
|
|
540
|
+
object = this.resolveObjectName(object);
|
|
436
541
|
this.logger.debug('Delete operation starting', { object });
|
|
437
542
|
const driver = this.getDriver(object);
|
|
438
543
|
|
|
@@ -474,6 +579,7 @@ export class ObjectQL implements IDataEngine {
|
|
|
474
579
|
}
|
|
475
580
|
|
|
476
581
|
async count(object: string, query?: DataEngineCountOptions): Promise<number> {
|
|
582
|
+
object = this.resolveObjectName(object);
|
|
477
583
|
const driver = this.getDriver(object);
|
|
478
584
|
if (driver.count) {
|
|
479
585
|
const ast = this.toQueryAST(object, { filter: query?.filter });
|
|
@@ -485,6 +591,7 @@ export class ObjectQL implements IDataEngine {
|
|
|
485
591
|
}
|
|
486
592
|
|
|
487
593
|
async aggregate(object: string, query: DataEngineAggregateOptions): Promise<any[]> {
|
|
594
|
+
object = this.resolveObjectName(object);
|
|
488
595
|
const driver = this.getDriver(object);
|
|
489
596
|
this.logger.debug(`Aggregate on ${object} using ${driver.name}`, query);
|
|
490
597
|
// Driver needs support for raw aggregation or mapped aggregation
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
// Export Registry
|
|
2
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
SchemaRegistry,
|
|
4
|
+
computeFQN,
|
|
5
|
+
parseFQN,
|
|
6
|
+
RESERVED_NAMESPACES,
|
|
7
|
+
DEFAULT_OWNER_PRIORITY,
|
|
8
|
+
DEFAULT_EXTENDER_PRIORITY,
|
|
9
|
+
} from './registry.js';
|
|
10
|
+
export type { ObjectContributor } from './registry.js';
|
|
3
11
|
|
|
4
12
|
// Export Protocol Implementation
|
|
5
13
|
export { ObjectStackProtocolImplementation } from './protocol.js';
|
package/src/plugin.ts
CHANGED
|
@@ -23,7 +23,9 @@ export class ObjectQLPlugin implements Plugin {
|
|
|
23
23
|
|
|
24
24
|
init = async (ctx: PluginContext) => {
|
|
25
25
|
if (!this.ql) {
|
|
26
|
-
|
|
26
|
+
// Pass kernel logger to engine to avoid creating a separate pino instance
|
|
27
|
+
const hostCtx = { ...this.hostContext, logger: ctx.logger };
|
|
28
|
+
this.ql = new ObjectQL(hostCtx);
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
// Register as provider for Core Kernel Services
|
package/src/protocol.ts
CHANGED
|
@@ -58,10 +58,10 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
async getMetaItems(request: { type: string }) {
|
|
61
|
+
async getMetaItems(request: { type: string; packageId?: string }) {
|
|
62
62
|
return {
|
|
63
63
|
type: request.type,
|
|
64
|
-
items: SchemaRegistry.listItems(request.type)
|
|
64
|
+
items: SchemaRegistry.listItems(request.type, request.packageId)
|
|
65
65
|
};
|
|
66
66
|
}
|
|
67
67
|
|
|
@@ -77,31 +77,72 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
77
77
|
const schema = SchemaRegistry.getObject(request.object);
|
|
78
78
|
if (!schema) throw new Error(`Object ${request.object} not found`);
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
const fields = schema.fields || {};
|
|
81
|
+
const fieldKeys = Object.keys(fields);
|
|
82
|
+
|
|
81
83
|
if (request.type === 'list') {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
// Intelligent Column Selection
|
|
85
|
+
// 1. Always include 'name' or name-like fields
|
|
86
|
+
// 2. Limit to 6 columns by default
|
|
87
|
+
const priorityFields = ['name', 'title', 'label', 'subject', 'email', 'status', 'type', 'category', 'created_at'];
|
|
88
|
+
|
|
89
|
+
let columns = fieldKeys.filter(k => priorityFields.includes(k));
|
|
90
|
+
|
|
91
|
+
// If few priority fields, add others until 5
|
|
92
|
+
if (columns.length < 5) {
|
|
93
|
+
const remaining = fieldKeys.filter(k => !columns.includes(k) && k !== 'id' && !fields[k].hidden);
|
|
94
|
+
columns = [...columns, ...remaining.slice(0, 5 - columns.length)];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Sort columns by priority then alphabet or schema order
|
|
98
|
+
// For now, just keep them roughly in order they appear in schema or priority list
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
list: {
|
|
102
|
+
type: 'grid' as const,
|
|
103
|
+
object: request.object,
|
|
104
|
+
label: schema.label || schema.name,
|
|
105
|
+
columns: columns.map(f => ({
|
|
106
|
+
field: f,
|
|
107
|
+
label: fields[f]?.label || f,
|
|
108
|
+
sortable: true
|
|
109
|
+
})),
|
|
110
|
+
sort: fields['created_at'] ? ([{ field: 'created_at', order: 'desc' }] as any) : undefined,
|
|
111
|
+
searchableFields: columns.slice(0, 3) // Make first few textual columns searchable
|
|
112
|
+
}
|
|
89
113
|
};
|
|
90
114
|
} else {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
115
|
+
// Form View Generation
|
|
116
|
+
// Simple single-section layout for now
|
|
117
|
+
const formFields = fieldKeys
|
|
118
|
+
.filter(k => k !== 'id' && k !== 'created_at' && k !== 'modified_at' && !fields[k].hidden)
|
|
119
|
+
.map(f => ({
|
|
120
|
+
field: f,
|
|
121
|
+
label: fields[f]?.label,
|
|
122
|
+
required: fields[f]?.required,
|
|
123
|
+
readonly: fields[f]?.readonly,
|
|
124
|
+
type: fields[f]?.type,
|
|
125
|
+
// Default to 2 columns for most, 1 for textareas
|
|
126
|
+
colSpan: (fields[f]?.type === 'textarea' || fields[f]?.type === 'html') ? 2 : 1
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
form: {
|
|
131
|
+
type: 'simple' as const,
|
|
132
|
+
object: request.object,
|
|
133
|
+
label: `Edit ${schema.label || schema.name}`,
|
|
134
|
+
sections: [
|
|
135
|
+
{
|
|
136
|
+
label: 'General Information',
|
|
137
|
+
columns: 2 as const,
|
|
138
|
+
collapsible: false,
|
|
139
|
+
collapsed: false,
|
|
140
|
+
fields: formFields
|
|
141
|
+
}
|
|
142
|
+
]
|
|
143
|
+
}
|
|
102
144
|
};
|
|
103
145
|
}
|
|
104
|
-
return view;
|
|
105
146
|
}
|
|
106
147
|
|
|
107
148
|
async findData(request: { object: string, query?: any }) {
|