@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/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
- this.logger.debug('Registering app manifest', { id });
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 objects
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', { object: objDef.name, from: id });
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', { object: name, from: id });
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 contributions
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 { SchemaRegistry } from './registry.js';
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
- this.ql = new ObjectQL(this.hostContext);
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
- let view: any;
80
+ const fields = schema.fields || {};
81
+ const fieldKeys = Object.keys(fields);
82
+
81
83
  if (request.type === 'list') {
82
- view = {
83
- type: 'list',
84
- object: request.object,
85
- columns: Object.keys(schema.fields || {}).slice(0, 5).map(f => ({
86
- field: f,
87
- label: schema.fields[f].label || f
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
- view = {
92
- type: 'form',
93
- object: request.object,
94
- sections: [
95
- {
96
- label: 'General',
97
- fields: Object.keys(schema.fields || {}).map(f => ({
98
- field: f
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 }) {