@objectstack/objectql 3.3.1 → 4.0.1
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 +25 -0
- package/dist/index.d.mts +27 -17
- package/dist/index.d.ts +27 -17
- package/dist/index.js +221 -109
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +221 -109
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
- package/src/engine.test.ts +13 -13
- package/src/engine.ts +36 -77
- package/src/plugin.ts +2 -2
- package/src/protocol-data.test.ts +41 -38
- package/src/protocol-discovery.test.ts +25 -25
- package/src/protocol-meta.test.ts +440 -0
- package/src/protocol.ts +258 -68
- package/tsconfig.json +2 -1
package/src/protocol.ts
CHANGED
|
@@ -76,7 +76,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
76
76
|
// Build dynamic service info with proper typing
|
|
77
77
|
const services: Record<string, ServiceInfo> = {
|
|
78
78
|
// --- Kernel-provided (objectql is an example kernel implementation) ---
|
|
79
|
-
metadata: { enabled: true, status: '
|
|
79
|
+
metadata: { enabled: true, status: 'available' as const, route: '/api/v1/meta', provider: 'objectql' },
|
|
80
80
|
data: { enabled: true, status: 'available' as const, route: '/api/v1/data', provider: 'objectql' },
|
|
81
81
|
analytics: { enabled: true, status: 'available' as const, route: '/api/v1/analytics', provider: 'objectql' },
|
|
82
82
|
};
|
|
@@ -151,8 +151,10 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
151
151
|
...optionalRoutes,
|
|
152
152
|
};
|
|
153
153
|
|
|
154
|
-
// Build well-known capabilities from registered services
|
|
155
|
-
|
|
154
|
+
// Build well-known capabilities from registered services.
|
|
155
|
+
// DiscoverySchema defines capabilities as Record<string, { enabled, features?, description? }>
|
|
156
|
+
// (hierarchical format). We also keep a flat WellKnownCapabilities for backward compat.
|
|
157
|
+
const wellKnown: WellKnownCapabilities = {
|
|
156
158
|
feed: registeredServices.has('feed'),
|
|
157
159
|
comments: registeredServices.has('feed'),
|
|
158
160
|
automation: registeredServices.has('automation'),
|
|
@@ -162,6 +164,12 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
162
164
|
chunkedUpload: registeredServices.has('file-storage'),
|
|
163
165
|
};
|
|
164
166
|
|
|
167
|
+
// Convert flat booleans → hierarchical capability objects
|
|
168
|
+
const capabilities: Record<string, { enabled: boolean; description?: string }> = {};
|
|
169
|
+
for (const [key, enabled] of Object.entries(wellKnown)) {
|
|
170
|
+
capabilities[key] = { enabled };
|
|
171
|
+
}
|
|
172
|
+
|
|
165
173
|
return {
|
|
166
174
|
version: '1.0',
|
|
167
175
|
apiName: 'ObjectStack API',
|
|
@@ -184,6 +192,43 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
184
192
|
const alt = request.type.endsWith('s') ? request.type.slice(0, -1) : request.type + 's';
|
|
185
193
|
items = SchemaRegistry.listItems(alt);
|
|
186
194
|
}
|
|
195
|
+
|
|
196
|
+
// Fallback to database if registry is empty for this type
|
|
197
|
+
if (items.length === 0) {
|
|
198
|
+
try {
|
|
199
|
+
const allRecords = await this.engine.find('sys_metadata', {
|
|
200
|
+
where: { type: request.type, state: 'active' }
|
|
201
|
+
});
|
|
202
|
+
if (allRecords && allRecords.length > 0) {
|
|
203
|
+
items = allRecords.map((record: any) => {
|
|
204
|
+
const data = typeof record.metadata === 'string'
|
|
205
|
+
? JSON.parse(record.metadata)
|
|
206
|
+
: record.metadata;
|
|
207
|
+
// Hydrate back into registry
|
|
208
|
+
SchemaRegistry.registerItem(request.type, data, 'name' as any);
|
|
209
|
+
return data;
|
|
210
|
+
});
|
|
211
|
+
} else {
|
|
212
|
+
// Try alternate type name in DB
|
|
213
|
+
const alt = request.type.endsWith('s') ? request.type.slice(0, -1) : request.type + 's';
|
|
214
|
+
const altRecords = await this.engine.find('sys_metadata', {
|
|
215
|
+
where: { type: alt, state: 'active' }
|
|
216
|
+
});
|
|
217
|
+
if (altRecords && altRecords.length > 0) {
|
|
218
|
+
items = altRecords.map((record: any) => {
|
|
219
|
+
const data = typeof record.metadata === 'string'
|
|
220
|
+
? JSON.parse(record.metadata)
|
|
221
|
+
: record.metadata;
|
|
222
|
+
SchemaRegistry.registerItem(request.type, data, 'name' as any);
|
|
223
|
+
return data;
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
// DB not available, return registry results (empty)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
187
232
|
return {
|
|
188
233
|
type: request.type,
|
|
189
234
|
items
|
|
@@ -197,6 +242,38 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
197
242
|
const alt = request.type.endsWith('s') ? request.type.slice(0, -1) : request.type + 's';
|
|
198
243
|
item = SchemaRegistry.getItem(alt, request.name);
|
|
199
244
|
}
|
|
245
|
+
|
|
246
|
+
// Fallback to database if not in registry
|
|
247
|
+
if (item === undefined) {
|
|
248
|
+
try {
|
|
249
|
+
const record = await this.engine.findOne('sys_metadata', {
|
|
250
|
+
where: { type: request.type, name: request.name, state: 'active' }
|
|
251
|
+
});
|
|
252
|
+
if (record) {
|
|
253
|
+
item = typeof record.metadata === 'string'
|
|
254
|
+
? JSON.parse(record.metadata)
|
|
255
|
+
: record.metadata;
|
|
256
|
+
// Hydrate back into registry for next time
|
|
257
|
+
SchemaRegistry.registerItem(request.type, item, 'name' as any);
|
|
258
|
+
} else {
|
|
259
|
+
// Try alternate type name
|
|
260
|
+
const alt = request.type.endsWith('s') ? request.type.slice(0, -1) : request.type + 's';
|
|
261
|
+
const altRecord = await this.engine.findOne('sys_metadata', {
|
|
262
|
+
where: { type: alt, name: request.name, state: 'active' }
|
|
263
|
+
});
|
|
264
|
+
if (altRecord) {
|
|
265
|
+
item = typeof altRecord.metadata === 'string'
|
|
266
|
+
? JSON.parse(altRecord.metadata)
|
|
267
|
+
: altRecord.metadata;
|
|
268
|
+
// Hydrate back into registry for next time
|
|
269
|
+
SchemaRegistry.registerItem(request.type, item, 'name' as any);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} catch {
|
|
273
|
+
// DB not available, return undefined
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
200
277
|
return {
|
|
201
278
|
type: request.type,
|
|
202
279
|
name: request.name,
|
|
@@ -279,17 +356,31 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
279
356
|
async findData(request: { object: string, query?: any }) {
|
|
280
357
|
const options: any = { ...request.query };
|
|
281
358
|
|
|
282
|
-
//
|
|
283
|
-
|
|
284
|
-
|
|
359
|
+
// ====================================================================
|
|
360
|
+
// Normalize legacy params → QueryAST standard (where/fields/orderBy/offset/expand)
|
|
361
|
+
// ====================================================================
|
|
362
|
+
|
|
363
|
+
// Numeric fields — normalize top → limit, skip → offset
|
|
364
|
+
if (options.top != null) {
|
|
365
|
+
options.limit = Number(options.top);
|
|
366
|
+
delete options.top;
|
|
367
|
+
}
|
|
368
|
+
if (options.skip != null) {
|
|
369
|
+
options.offset = Number(options.skip);
|
|
370
|
+
delete options.skip;
|
|
371
|
+
}
|
|
285
372
|
if (options.limit != null) options.limit = Number(options.limit);
|
|
373
|
+
if (options.offset != null) options.offset = Number(options.offset);
|
|
286
374
|
|
|
287
|
-
// Select: comma-separated string → array
|
|
375
|
+
// Select → fields: comma-separated string → array
|
|
288
376
|
if (typeof options.select === 'string') {
|
|
289
|
-
options.
|
|
377
|
+
options.fields = options.select.split(',').map((s: string) => s.trim()).filter(Boolean);
|
|
378
|
+
} else if (Array.isArray(options.select)) {
|
|
379
|
+
options.fields = options.select;
|
|
290
380
|
}
|
|
381
|
+
if (options.select !== undefined) delete options.select;
|
|
291
382
|
|
|
292
|
-
// Sort/orderBy: string →
|
|
383
|
+
// Sort/orderBy → orderBy: string → SortNode[] array
|
|
293
384
|
const sortValue = options.orderBy ?? options.sort;
|
|
294
385
|
if (typeof sortValue === 'string') {
|
|
295
386
|
const parsed = sortValue.split(',').map((part: string) => {
|
|
@@ -300,45 +391,62 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
300
391
|
const [field, order] = trimmed.split(/\s+/);
|
|
301
392
|
return { field, order: (order?.toLowerCase() === 'desc' ? 'desc' : 'asc') as 'asc' | 'desc' };
|
|
302
393
|
}).filter((s: any) => s.field);
|
|
303
|
-
options.
|
|
304
|
-
|
|
394
|
+
options.orderBy = parsed;
|
|
395
|
+
} else if (Array.isArray(sortValue)) {
|
|
396
|
+
options.orderBy = sortValue;
|
|
305
397
|
}
|
|
398
|
+
delete options.sort;
|
|
306
399
|
|
|
307
|
-
// Filter: normalize
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
options.filter = options.filters;
|
|
311
|
-
}
|
|
400
|
+
// Filter/filters/$filter → where: normalize all filter aliases
|
|
401
|
+
const filterValue = options.filter ?? options.filters ?? options.$filter ?? options.where;
|
|
402
|
+
delete options.filter;
|
|
312
403
|
delete options.filters;
|
|
404
|
+
delete options.$filter;
|
|
313
405
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
// Populate: comma-separated string → array
|
|
326
|
-
if (typeof options.populate === 'string') {
|
|
327
|
-
options.populate = options.populate.split(',').map((s: string) => s.trim()).filter(Boolean);
|
|
406
|
+
if (filterValue !== undefined) {
|
|
407
|
+
let parsedFilter = filterValue;
|
|
408
|
+
// JSON string → object
|
|
409
|
+
if (typeof parsedFilter === 'string') {
|
|
410
|
+
try { parsedFilter = JSON.parse(parsedFilter); } catch { /* keep as-is */ }
|
|
411
|
+
}
|
|
412
|
+
// Filter AST array → FilterCondition object
|
|
413
|
+
if (isFilterAST(parsedFilter)) {
|
|
414
|
+
parsedFilter = parseFilterAST(parsedFilter);
|
|
415
|
+
}
|
|
416
|
+
options.where = parsedFilter;
|
|
328
417
|
}
|
|
329
418
|
|
|
330
|
-
//
|
|
331
|
-
|
|
419
|
+
// Populate/expand/$expand → expand (Record<string, QueryAST>)
|
|
420
|
+
const populateValue = options.populate;
|
|
332
421
|
const expandValue = options.$expand ?? options.expand;
|
|
333
|
-
|
|
422
|
+
const expandNames: string[] = [];
|
|
423
|
+
if (typeof populateValue === 'string') {
|
|
424
|
+
expandNames.push(...populateValue.split(',').map((s: string) => s.trim()).filter(Boolean));
|
|
425
|
+
} else if (Array.isArray(populateValue)) {
|
|
426
|
+
expandNames.push(...populateValue);
|
|
427
|
+
}
|
|
428
|
+
if (!expandNames.length && expandValue) {
|
|
334
429
|
if (typeof expandValue === 'string') {
|
|
335
|
-
|
|
430
|
+
expandNames.push(...expandValue.split(',').map((s: string) => s.trim()).filter(Boolean));
|
|
336
431
|
} else if (Array.isArray(expandValue)) {
|
|
337
|
-
|
|
432
|
+
expandNames.push(...expandValue);
|
|
338
433
|
}
|
|
339
434
|
}
|
|
435
|
+
delete options.populate;
|
|
340
436
|
delete options.$expand;
|
|
341
|
-
|
|
437
|
+
// Clean up non-object expand (e.g. string) BEFORE the Record conversion
|
|
438
|
+
// below, so that populate-derived names can create the expand Record even
|
|
439
|
+
// when a legacy string expand was also present.
|
|
440
|
+
if (typeof options.expand !== 'object' || options.expand === null) {
|
|
441
|
+
delete options.expand;
|
|
442
|
+
}
|
|
443
|
+
// Only set expand if not already an object (advanced usage)
|
|
444
|
+
if (expandNames.length > 0 && !options.expand) {
|
|
445
|
+
options.expand = {} as Record<string, any>;
|
|
446
|
+
for (const rel of expandNames) {
|
|
447
|
+
options.expand[rel] = { object: rel };
|
|
448
|
+
}
|
|
449
|
+
}
|
|
342
450
|
|
|
343
451
|
// Boolean fields
|
|
344
452
|
for (const key of ['distinct', 'count']) {
|
|
@@ -348,20 +456,18 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
348
456
|
|
|
349
457
|
// Flat field filters: REST-style query params like ?id=abc&status=open
|
|
350
458
|
// After extracting all known query parameters, any remaining keys are
|
|
351
|
-
// treated as implicit field-level equality filters
|
|
352
|
-
// REST convention used by the client when serializing simple filter maps
|
|
353
|
-
// (e.g. `{ filters: { id: "..." } }` → `?id=...`).
|
|
459
|
+
// treated as implicit field-level equality filters merged into `where`.
|
|
354
460
|
const knownParams = new Set([
|
|
355
|
-
'top', '
|
|
356
|
-
'
|
|
357
|
-
'
|
|
358
|
-
'
|
|
359
|
-
'
|
|
461
|
+
'top', 'limit', 'offset',
|
|
462
|
+
'orderBy',
|
|
463
|
+
'fields',
|
|
464
|
+
'where',
|
|
465
|
+
'expand',
|
|
360
466
|
'distinct', 'count',
|
|
361
467
|
'aggregations', 'groupBy',
|
|
362
|
-
'search', 'context',
|
|
468
|
+
'search', 'context', 'cursor',
|
|
363
469
|
]);
|
|
364
|
-
if (!options.
|
|
470
|
+
if (!options.where) {
|
|
365
471
|
const implicitFilters: Record<string, unknown> = {};
|
|
366
472
|
for (const key of Object.keys(options)) {
|
|
367
473
|
if (!knownParams.has(key)) {
|
|
@@ -370,14 +476,14 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
370
476
|
}
|
|
371
477
|
}
|
|
372
478
|
if (Object.keys(implicitFilters).length > 0) {
|
|
373
|
-
options.
|
|
479
|
+
options.where = implicitFilters;
|
|
374
480
|
}
|
|
375
481
|
}
|
|
376
482
|
|
|
377
483
|
const records = await this.engine.find(request.object, options);
|
|
378
484
|
return {
|
|
379
485
|
object: request.object,
|
|
380
|
-
value: records, // OData
|
|
486
|
+
value: records, // OData compatibility
|
|
381
487
|
records, // Legacy
|
|
382
488
|
total: records.length,
|
|
383
489
|
hasMore: false
|
|
@@ -386,21 +492,25 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
386
492
|
|
|
387
493
|
async getData(request: { object: string, id: string, expand?: string | string[], select?: string | string[] }) {
|
|
388
494
|
const queryOptions: any = {
|
|
389
|
-
|
|
495
|
+
where: { id: request.id }
|
|
390
496
|
};
|
|
391
497
|
|
|
392
|
-
// Support
|
|
498
|
+
// Support fields for single-record retrieval
|
|
393
499
|
if (request.select) {
|
|
394
|
-
queryOptions.
|
|
500
|
+
queryOptions.fields = typeof request.select === 'string'
|
|
395
501
|
? request.select.split(',').map((s: string) => s.trim()).filter(Boolean)
|
|
396
502
|
: request.select;
|
|
397
503
|
}
|
|
398
504
|
|
|
399
505
|
// Support expand for single-record retrieval
|
|
400
506
|
if (request.expand) {
|
|
401
|
-
|
|
507
|
+
const expandNames = typeof request.expand === 'string'
|
|
402
508
|
? request.expand.split(',').map((s: string) => s.trim()).filter(Boolean)
|
|
403
509
|
: request.expand;
|
|
510
|
+
queryOptions.expand = {} as Record<string, any>;
|
|
511
|
+
for (const rel of expandNames) {
|
|
512
|
+
queryOptions.expand[rel] = { object: rel };
|
|
513
|
+
}
|
|
404
514
|
}
|
|
405
515
|
|
|
406
516
|
const result = await this.engine.findOne(request.object, queryOptions);
|
|
@@ -425,7 +535,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
425
535
|
|
|
426
536
|
async updateData(request: { object: string, id: string, data: any }) {
|
|
427
537
|
// Adapt: update(obj, id, data) -> update(obj, data, options)
|
|
428
|
-
const result = await this.engine.update(request.object, request.data, {
|
|
538
|
+
const result = await this.engine.update(request.object, request.data, { where: { id: request.id } });
|
|
429
539
|
return {
|
|
430
540
|
object: request.object,
|
|
431
541
|
id: request.id,
|
|
@@ -435,7 +545,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
435
545
|
|
|
436
546
|
async deleteData(request: { object: string, id: string }) {
|
|
437
547
|
// Adapt: delete(obj, id) -> delete(obj, options)
|
|
438
|
-
await this.engine.delete(request.object, {
|
|
548
|
+
await this.engine.delete(request.object, { where: { id: request.id } });
|
|
439
549
|
return {
|
|
440
550
|
object: request.object,
|
|
441
551
|
id: request.id,
|
|
@@ -509,7 +619,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
509
619
|
}
|
|
510
620
|
case 'update': {
|
|
511
621
|
if (!record.id) throw new Error('Record id is required for update');
|
|
512
|
-
const updated = await this.engine.update(object, record.data || {}, {
|
|
622
|
+
const updated = await this.engine.update(object, record.data || {}, { where: { id: record.id } });
|
|
513
623
|
results.push({ id: record.id, success: true, record: updated });
|
|
514
624
|
succeeded++;
|
|
515
625
|
break;
|
|
@@ -518,9 +628,9 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
518
628
|
// Try update first, then create if not found
|
|
519
629
|
if (record.id) {
|
|
520
630
|
try {
|
|
521
|
-
const existing = await this.engine.findOne(object, {
|
|
631
|
+
const existing = await this.engine.findOne(object, { where: { id: record.id } });
|
|
522
632
|
if (existing) {
|
|
523
|
-
const updated = await this.engine.update(object, record.data || {}, {
|
|
633
|
+
const updated = await this.engine.update(object, record.data || {}, { where: { id: record.id } });
|
|
524
634
|
results.push({ id: record.id, success: true, record: updated });
|
|
525
635
|
} else {
|
|
526
636
|
const created = await this.engine.insert(object, { id: record.id, ...(record.data || {}) });
|
|
@@ -539,7 +649,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
539
649
|
}
|
|
540
650
|
case 'delete': {
|
|
541
651
|
if (!record.id) throw new Error('Record id is required for delete');
|
|
542
|
-
await this.engine.delete(object, {
|
|
652
|
+
await this.engine.delete(object, { where: { id: record.id } });
|
|
543
653
|
results.push({ id: record.id, success: true });
|
|
544
654
|
succeeded++;
|
|
545
655
|
break;
|
|
@@ -588,7 +698,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
588
698
|
|
|
589
699
|
for (const record of records) {
|
|
590
700
|
try {
|
|
591
|
-
const updated = await this.engine.update(object, record.data, {
|
|
701
|
+
const updated = await this.engine.update(object, record.data, { where: { id: record.id } });
|
|
592
702
|
results.push({ id: record.id, success: true, record: updated });
|
|
593
703
|
succeeded++;
|
|
594
704
|
} catch (err: any) {
|
|
@@ -655,11 +765,11 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
655
765
|
|
|
656
766
|
// Execute via engine.aggregate (which delegates to driver.find with groupBy/aggregations)
|
|
657
767
|
const rows = await this.engine.aggregate(object, {
|
|
658
|
-
filter,
|
|
768
|
+
where: filter,
|
|
659
769
|
groupBy: groupBy.length > 0 ? groupBy : undefined,
|
|
660
770
|
aggregations: aggregations.length > 0
|
|
661
|
-
? aggregations.map(a => ({
|
|
662
|
-
: [{
|
|
771
|
+
? aggregations.map(a => ({ function: a.method as any, field: a.field, alias: a.alias }))
|
|
772
|
+
: [{ function: 'count' as any, alias: 'count' }],
|
|
663
773
|
});
|
|
664
774
|
|
|
665
775
|
// Build field metadata
|
|
@@ -790,7 +900,7 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
790
900
|
async deleteManyData(request: DeleteManyDataRequest): Promise<any> {
|
|
791
901
|
// This expects deleting by IDs.
|
|
792
902
|
return this.engine.delete(request.object, {
|
|
793
|
-
|
|
903
|
+
where: { id: { $in: request.ids } },
|
|
794
904
|
...request.options
|
|
795
905
|
});
|
|
796
906
|
}
|
|
@@ -799,12 +909,92 @@ export class ObjectStackProtocolImplementation implements ObjectStackProtocol {
|
|
|
799
909
|
if (!request.item) {
|
|
800
910
|
throw new Error('Item data is required');
|
|
801
911
|
}
|
|
802
|
-
|
|
912
|
+
|
|
913
|
+
// 1. Always update the in-memory registry (runtime cache)
|
|
803
914
|
SchemaRegistry.registerItem(request.type, request.item, 'name');
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
915
|
+
|
|
916
|
+
// 2. Persist to database via data engine
|
|
917
|
+
try {
|
|
918
|
+
const now = new Date().toISOString();
|
|
919
|
+
// Check if record exists
|
|
920
|
+
const existing = await this.engine.findOne('sys_metadata', {
|
|
921
|
+
where: { type: request.type, name: request.name }
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
if (existing) {
|
|
925
|
+
await this.engine.update('sys_metadata', {
|
|
926
|
+
metadata: JSON.stringify(request.item),
|
|
927
|
+
updated_at: now,
|
|
928
|
+
version: (existing.version || 0) + 1,
|
|
929
|
+
}, {
|
|
930
|
+
where: { id: existing.id }
|
|
931
|
+
});
|
|
932
|
+
} else {
|
|
933
|
+
// Use crypto.randomUUID() when available (modern browsers and Node ≥ 14.17);
|
|
934
|
+
// fall back to a time+random ID for older or restricted environments.
|
|
935
|
+
const id = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
936
|
+
? crypto.randomUUID()
|
|
937
|
+
: `meta_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
938
|
+
await this.engine.insert('sys_metadata', {
|
|
939
|
+
id,
|
|
940
|
+
name: request.name,
|
|
941
|
+
type: request.type,
|
|
942
|
+
scope: 'platform',
|
|
943
|
+
metadata: JSON.stringify(request.item),
|
|
944
|
+
state: 'active',
|
|
945
|
+
version: 1,
|
|
946
|
+
created_at: now,
|
|
947
|
+
updated_at: now,
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return {
|
|
952
|
+
success: true,
|
|
953
|
+
message: 'Saved to database and registry'
|
|
954
|
+
};
|
|
955
|
+
} catch (dbError: any) {
|
|
956
|
+
// DB write failed but in-memory registry was updated — degrade gracefully
|
|
957
|
+
console.warn(`[Protocol] DB persistence failed for ${request.type}/${request.name}: ${dbError.message}`);
|
|
958
|
+
return {
|
|
959
|
+
success: true,
|
|
960
|
+
message: 'Saved to memory registry (DB persistence unavailable)',
|
|
961
|
+
warning: dbError.message
|
|
962
|
+
};
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* Hydrate SchemaRegistry from the database on startup.
|
|
968
|
+
* Loads all active metadata records and registers them in the in-memory registry.
|
|
969
|
+
* Safe to call repeatedly — idempotent (latest DB record wins).
|
|
970
|
+
*/
|
|
971
|
+
async loadMetaFromDb(): Promise<{ loaded: number; errors: number }> {
|
|
972
|
+
let loaded = 0;
|
|
973
|
+
let errors = 0;
|
|
974
|
+
try {
|
|
975
|
+
const records = await this.engine.find('sys_metadata', {
|
|
976
|
+
where: { state: 'active' }
|
|
977
|
+
});
|
|
978
|
+
for (const record of records) {
|
|
979
|
+
try {
|
|
980
|
+
const data = typeof record.metadata === 'string'
|
|
981
|
+
? JSON.parse(record.metadata)
|
|
982
|
+
: record.metadata;
|
|
983
|
+
if (record.type === 'object') {
|
|
984
|
+
SchemaRegistry.registerObject(data as any, record.packageId || 'sys_metadata');
|
|
985
|
+
} else {
|
|
986
|
+
SchemaRegistry.registerItem(record.type, data, 'name' as any);
|
|
987
|
+
}
|
|
988
|
+
loaded++;
|
|
989
|
+
} catch (e) {
|
|
990
|
+
errors++;
|
|
991
|
+
console.warn(`[Protocol] Failed to hydrate ${record.type}/${record.name}: ${e instanceof Error ? e.message : String(e)}`);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
} catch (e: any) {
|
|
995
|
+
console.warn(`[Protocol] DB hydration skipped: ${e.message}`);
|
|
996
|
+
}
|
|
997
|
+
return { loaded, errors };
|
|
808
998
|
}
|
|
809
999
|
|
|
810
1000
|
// ==========================================
|