@objectstack/metadata 4.0.4 → 4.1.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/README.md +99 -12
- package/dist/index.cjs +793 -534
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +208 -6151
- package/dist/index.d.ts +208 -6151
- package/dist/index.js +794 -532
- package/dist/index.js.map +1 -1
- package/dist/migrations/index.cjs +156 -0
- package/dist/migrations/index.cjs.map +1 -0
- package/dist/migrations/index.d.cts +103 -0
- package/dist/migrations/index.d.ts +103 -0
- package/dist/migrations/index.js +127 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/node.cjs +793 -534
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +2 -3
- package/dist/node.d.ts +2 -3
- package/dist/node.js +794 -532
- package/dist/node.js.map +1 -1
- package/package.json +28 -8
package/dist/node.js
CHANGED
|
@@ -174,277 +174,8 @@ export default metadata;
|
|
|
174
174
|
}
|
|
175
175
|
};
|
|
176
176
|
|
|
177
|
-
// src/
|
|
178
|
-
import {
|
|
179
|
-
var SysMetadataObject = ObjectSchema.create({
|
|
180
|
-
namespace: "sys",
|
|
181
|
-
name: "metadata",
|
|
182
|
-
label: "System Metadata",
|
|
183
|
-
pluralLabel: "System Metadata",
|
|
184
|
-
icon: "settings",
|
|
185
|
-
isSystem: true,
|
|
186
|
-
description: "Stores platform and user-scope metadata records (objects, views, flows, etc.)",
|
|
187
|
-
fields: {
|
|
188
|
-
/** Primary Key (UUID) */
|
|
189
|
-
id: Field.text({
|
|
190
|
-
label: "ID",
|
|
191
|
-
required: true,
|
|
192
|
-
readonly: true
|
|
193
|
-
}),
|
|
194
|
-
/** Machine name — unique identifier used in code references */
|
|
195
|
-
name: Field.text({
|
|
196
|
-
label: "Name",
|
|
197
|
-
required: true,
|
|
198
|
-
searchable: true,
|
|
199
|
-
maxLength: 255
|
|
200
|
-
}),
|
|
201
|
-
/** Metadata type (e.g. "object", "view", "flow") */
|
|
202
|
-
type: Field.text({
|
|
203
|
-
label: "Metadata Type",
|
|
204
|
-
required: true,
|
|
205
|
-
searchable: true,
|
|
206
|
-
maxLength: 100
|
|
207
|
-
}),
|
|
208
|
-
/** Namespace / module grouping (e.g. "crm", "core") */
|
|
209
|
-
namespace: Field.text({
|
|
210
|
-
label: "Namespace",
|
|
211
|
-
required: false,
|
|
212
|
-
defaultValue: "default",
|
|
213
|
-
maxLength: 100
|
|
214
|
-
}),
|
|
215
|
-
/** Package that owns/delivered this metadata */
|
|
216
|
-
package_id: Field.text({
|
|
217
|
-
label: "Package ID",
|
|
218
|
-
required: false,
|
|
219
|
-
maxLength: 255
|
|
220
|
-
}),
|
|
221
|
-
/** Who manages this record: package, platform, or user */
|
|
222
|
-
managed_by: Field.select(["package", "platform", "user"], {
|
|
223
|
-
label: "Managed By",
|
|
224
|
-
required: false
|
|
225
|
-
}),
|
|
226
|
-
/** Scope: system (code), platform (admin DB), user (personal DB) */
|
|
227
|
-
scope: Field.select(["system", "platform", "user"], {
|
|
228
|
-
label: "Scope",
|
|
229
|
-
required: true,
|
|
230
|
-
defaultValue: "platform"
|
|
231
|
-
}),
|
|
232
|
-
/** JSON payload — the actual metadata configuration */
|
|
233
|
-
metadata: Field.textarea({
|
|
234
|
-
label: "Metadata",
|
|
235
|
-
required: true,
|
|
236
|
-
description: "JSON-serialized metadata payload"
|
|
237
|
-
}),
|
|
238
|
-
/** Parent metadata name for extension/override */
|
|
239
|
-
extends: Field.text({
|
|
240
|
-
label: "Extends",
|
|
241
|
-
required: false,
|
|
242
|
-
maxLength: 255
|
|
243
|
-
}),
|
|
244
|
-
/** Merge strategy when extending parent metadata */
|
|
245
|
-
strategy: Field.select(["merge", "replace"], {
|
|
246
|
-
label: "Strategy",
|
|
247
|
-
required: false,
|
|
248
|
-
defaultValue: "merge"
|
|
249
|
-
}),
|
|
250
|
-
/** Owner user ID (for user-scope items) */
|
|
251
|
-
owner: Field.text({
|
|
252
|
-
label: "Owner",
|
|
253
|
-
required: false,
|
|
254
|
-
maxLength: 255
|
|
255
|
-
}),
|
|
256
|
-
/** Lifecycle state */
|
|
257
|
-
state: Field.select(["draft", "active", "archived", "deprecated"], {
|
|
258
|
-
label: "State",
|
|
259
|
-
required: false,
|
|
260
|
-
defaultValue: "active"
|
|
261
|
-
}),
|
|
262
|
-
/** Tenant ID for multi-tenant isolation */
|
|
263
|
-
tenant_id: Field.text({
|
|
264
|
-
label: "Tenant ID",
|
|
265
|
-
required: false,
|
|
266
|
-
maxLength: 255
|
|
267
|
-
}),
|
|
268
|
-
/** Version number for optimistic concurrency */
|
|
269
|
-
version: Field.number({
|
|
270
|
-
label: "Version",
|
|
271
|
-
required: false,
|
|
272
|
-
defaultValue: 1
|
|
273
|
-
}),
|
|
274
|
-
/** Content checksum for change detection */
|
|
275
|
-
checksum: Field.text({
|
|
276
|
-
label: "Checksum",
|
|
277
|
-
required: false,
|
|
278
|
-
maxLength: 64
|
|
279
|
-
}),
|
|
280
|
-
/** Origin of this metadata record */
|
|
281
|
-
source: Field.select(["filesystem", "database", "api", "migration"], {
|
|
282
|
-
label: "Source",
|
|
283
|
-
required: false
|
|
284
|
-
}),
|
|
285
|
-
/** Classification tags (JSON array) */
|
|
286
|
-
tags: Field.textarea({
|
|
287
|
-
label: "Tags",
|
|
288
|
-
required: false,
|
|
289
|
-
description: "JSON-serialized array of classification tags"
|
|
290
|
-
}),
|
|
291
|
-
/** Audit fields */
|
|
292
|
-
created_by: Field.text({
|
|
293
|
-
label: "Created By",
|
|
294
|
-
required: false,
|
|
295
|
-
readonly: true,
|
|
296
|
-
maxLength: 255
|
|
297
|
-
}),
|
|
298
|
-
created_at: Field.datetime({
|
|
299
|
-
label: "Created At",
|
|
300
|
-
required: false,
|
|
301
|
-
readonly: true
|
|
302
|
-
}),
|
|
303
|
-
updated_by: Field.text({
|
|
304
|
-
label: "Updated By",
|
|
305
|
-
required: false,
|
|
306
|
-
maxLength: 255
|
|
307
|
-
}),
|
|
308
|
-
updated_at: Field.datetime({
|
|
309
|
-
label: "Updated At",
|
|
310
|
-
required: false
|
|
311
|
-
})
|
|
312
|
-
},
|
|
313
|
-
indexes: [
|
|
314
|
-
{ fields: ["type", "name"], unique: true },
|
|
315
|
-
{ fields: ["type", "scope"] },
|
|
316
|
-
{ fields: ["tenant_id"] },
|
|
317
|
-
{ fields: ["state"] },
|
|
318
|
-
{ fields: ["namespace"] }
|
|
319
|
-
],
|
|
320
|
-
enable: {
|
|
321
|
-
trackHistory: true,
|
|
322
|
-
searchable: false,
|
|
323
|
-
apiEnabled: true,
|
|
324
|
-
apiMethods: ["get", "list", "create", "update", "delete"],
|
|
325
|
-
trash: false
|
|
326
|
-
}
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
// src/objects/sys-metadata-history.object.ts
|
|
330
|
-
import { ObjectSchema as ObjectSchema2, Field as Field2 } from "@objectstack/spec/data";
|
|
331
|
-
var SysMetadataHistoryObject = ObjectSchema2.create({
|
|
332
|
-
namespace: "sys",
|
|
333
|
-
name: "metadata_history",
|
|
334
|
-
label: "Metadata History",
|
|
335
|
-
pluralLabel: "Metadata History",
|
|
336
|
-
icon: "history",
|
|
337
|
-
isSystem: true,
|
|
338
|
-
description: "Version history and audit trail for metadata changes",
|
|
339
|
-
fields: {
|
|
340
|
-
/** Primary Key (UUID) */
|
|
341
|
-
id: Field2.text({
|
|
342
|
-
label: "ID",
|
|
343
|
-
required: true,
|
|
344
|
-
readonly: true
|
|
345
|
-
}),
|
|
346
|
-
/** Foreign key to sys_metadata.id */
|
|
347
|
-
metadata_id: Field2.text({
|
|
348
|
-
label: "Metadata ID",
|
|
349
|
-
required: true,
|
|
350
|
-
readonly: true,
|
|
351
|
-
maxLength: 255
|
|
352
|
-
}),
|
|
353
|
-
/** Machine name (denormalized for easier querying) */
|
|
354
|
-
name: Field2.text({
|
|
355
|
-
label: "Name",
|
|
356
|
-
required: true,
|
|
357
|
-
searchable: true,
|
|
358
|
-
readonly: true,
|
|
359
|
-
maxLength: 255
|
|
360
|
-
}),
|
|
361
|
-
/** Metadata type (denormalized for easier querying) */
|
|
362
|
-
type: Field2.text({
|
|
363
|
-
label: "Metadata Type",
|
|
364
|
-
required: true,
|
|
365
|
-
searchable: true,
|
|
366
|
-
readonly: true,
|
|
367
|
-
maxLength: 100
|
|
368
|
-
}),
|
|
369
|
-
/** Version number at this snapshot */
|
|
370
|
-
version: Field2.number({
|
|
371
|
-
label: "Version",
|
|
372
|
-
required: true,
|
|
373
|
-
readonly: true
|
|
374
|
-
}),
|
|
375
|
-
/** Type of operation that created this history entry */
|
|
376
|
-
operation_type: Field2.select(["create", "update", "publish", "revert", "delete"], {
|
|
377
|
-
label: "Operation Type",
|
|
378
|
-
required: true,
|
|
379
|
-
readonly: true
|
|
380
|
-
}),
|
|
381
|
-
/** Historical metadata snapshot (JSON payload) */
|
|
382
|
-
metadata: Field2.textarea({
|
|
383
|
-
label: "Metadata",
|
|
384
|
-
required: true,
|
|
385
|
-
readonly: true,
|
|
386
|
-
description: "JSON-serialized metadata snapshot at this version"
|
|
387
|
-
}),
|
|
388
|
-
/** SHA-256 checksum of metadata content */
|
|
389
|
-
checksum: Field2.text({
|
|
390
|
-
label: "Checksum",
|
|
391
|
-
required: true,
|
|
392
|
-
readonly: true,
|
|
393
|
-
maxLength: 64
|
|
394
|
-
}),
|
|
395
|
-
/** Checksum of the previous version */
|
|
396
|
-
previous_checksum: Field2.text({
|
|
397
|
-
label: "Previous Checksum",
|
|
398
|
-
required: false,
|
|
399
|
-
readonly: true,
|
|
400
|
-
maxLength: 64
|
|
401
|
-
}),
|
|
402
|
-
/** Human-readable description of changes */
|
|
403
|
-
change_note: Field2.textarea({
|
|
404
|
-
label: "Change Note",
|
|
405
|
-
required: false,
|
|
406
|
-
readonly: true,
|
|
407
|
-
description: "Description of what changed in this version"
|
|
408
|
-
}),
|
|
409
|
-
/** Tenant ID for multi-tenant isolation */
|
|
410
|
-
tenant_id: Field2.text({
|
|
411
|
-
label: "Tenant ID",
|
|
412
|
-
required: false,
|
|
413
|
-
readonly: true,
|
|
414
|
-
maxLength: 255
|
|
415
|
-
}),
|
|
416
|
-
/** User who made this change */
|
|
417
|
-
recorded_by: Field2.text({
|
|
418
|
-
label: "Recorded By",
|
|
419
|
-
required: false,
|
|
420
|
-
readonly: true,
|
|
421
|
-
maxLength: 255
|
|
422
|
-
}),
|
|
423
|
-
/** When was this version recorded */
|
|
424
|
-
recorded_at: Field2.datetime({
|
|
425
|
-
label: "Recorded At",
|
|
426
|
-
required: true,
|
|
427
|
-
readonly: true
|
|
428
|
-
})
|
|
429
|
-
},
|
|
430
|
-
indexes: [
|
|
431
|
-
{ fields: ["metadata_id", "version"], unique: true },
|
|
432
|
-
{ fields: ["metadata_id", "recorded_at"] },
|
|
433
|
-
{ fields: ["type", "name"] },
|
|
434
|
-
{ fields: ["recorded_at"] },
|
|
435
|
-
{ fields: ["operation_type"] },
|
|
436
|
-
{ fields: ["tenant_id"] }
|
|
437
|
-
],
|
|
438
|
-
enable: {
|
|
439
|
-
trackHistory: false,
|
|
440
|
-
// Don't track history of history records
|
|
441
|
-
searchable: false,
|
|
442
|
-
apiEnabled: true,
|
|
443
|
-
apiMethods: ["get", "list"],
|
|
444
|
-
// Read-only via API
|
|
445
|
-
trash: false
|
|
446
|
-
}
|
|
447
|
-
});
|
|
177
|
+
// src/loaders/database-loader.ts
|
|
178
|
+
import { SysMetadataObject, SysMetadataHistoryObject } from "@objectstack/platform-objects/metadata";
|
|
448
179
|
|
|
449
180
|
// src/utils/metadata-history-utils.ts
|
|
450
181
|
async function calculateChecksum(metadata) {
|
|
@@ -544,6 +275,114 @@ function generateDiffSummary(diff) {
|
|
|
544
275
|
return summary.join(", ");
|
|
545
276
|
}
|
|
546
277
|
|
|
278
|
+
// src/utils/lru-cache.ts
|
|
279
|
+
var LRUCache = class {
|
|
280
|
+
constructor(options = {}) {
|
|
281
|
+
this.map = /* @__PURE__ */ new Map();
|
|
282
|
+
this.hits = 0;
|
|
283
|
+
this.misses = 0;
|
|
284
|
+
this.maxSize = options.maxSize && options.maxSize > 0 ? options.maxSize : 0;
|
|
285
|
+
this.ttl = options.ttl && options.ttl > 0 ? options.ttl : 0;
|
|
286
|
+
}
|
|
287
|
+
get(key) {
|
|
288
|
+
const entry = this.map.get(key);
|
|
289
|
+
if (!entry) {
|
|
290
|
+
this.misses++;
|
|
291
|
+
return void 0;
|
|
292
|
+
}
|
|
293
|
+
if (entry.expiresAt !== 0 && entry.expiresAt <= Date.now()) {
|
|
294
|
+
this.map.delete(key);
|
|
295
|
+
this.misses++;
|
|
296
|
+
return void 0;
|
|
297
|
+
}
|
|
298
|
+
this.map.delete(key);
|
|
299
|
+
this.map.set(key, entry);
|
|
300
|
+
this.hits++;
|
|
301
|
+
return entry.value;
|
|
302
|
+
}
|
|
303
|
+
set(key, value) {
|
|
304
|
+
if (this.map.has(key)) {
|
|
305
|
+
this.map.delete(key);
|
|
306
|
+
} else if (this.maxSize > 0 && this.map.size >= this.maxSize) {
|
|
307
|
+
const oldest = this.map.keys().next();
|
|
308
|
+
if (!oldest.done) this.map.delete(oldest.value);
|
|
309
|
+
}
|
|
310
|
+
this.map.set(key, {
|
|
311
|
+
value,
|
|
312
|
+
expiresAt: this.ttl > 0 ? Date.now() + this.ttl : 0
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
has(key) {
|
|
316
|
+
return this.get(key) !== void 0;
|
|
317
|
+
}
|
|
318
|
+
delete(key) {
|
|
319
|
+
return this.map.delete(key);
|
|
320
|
+
}
|
|
321
|
+
clear() {
|
|
322
|
+
this.map.clear();
|
|
323
|
+
}
|
|
324
|
+
get size() {
|
|
325
|
+
return this.map.size;
|
|
326
|
+
}
|
|
327
|
+
/** Diagnostic counters — useful for `metrics` endpoints. */
|
|
328
|
+
stats() {
|
|
329
|
+
const total = this.hits + this.misses;
|
|
330
|
+
return {
|
|
331
|
+
size: this.map.size,
|
|
332
|
+
hits: this.hits,
|
|
333
|
+
misses: this.misses,
|
|
334
|
+
hitRate: total === 0 ? 0 : this.hits / total
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
/** Resets hit/miss counters without dropping cached entries. */
|
|
338
|
+
resetStats() {
|
|
339
|
+
this.hits = 0;
|
|
340
|
+
this.misses = 0;
|
|
341
|
+
}
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
// src/migrations/add-sys-metadata-overlay-index.ts
|
|
345
|
+
var INDEX_NAME = "idx_sys_metadata_overlay_active";
|
|
346
|
+
var TABLE = "sys_metadata";
|
|
347
|
+
var COLUMNS = "(type, name, organization_id, project_id, scope)";
|
|
348
|
+
var WHERE = "state = 'active'";
|
|
349
|
+
async function addSysMetadataOverlayIndex(driver) {
|
|
350
|
+
const driverAny = driver;
|
|
351
|
+
const exec = async (sql) => {
|
|
352
|
+
if (typeof driverAny.raw === "function") {
|
|
353
|
+
await driverAny.raw(sql);
|
|
354
|
+
} else if (typeof driverAny.execute === "function") {
|
|
355
|
+
await driverAny.execute(sql);
|
|
356
|
+
} else {
|
|
357
|
+
throw new Error("driver has neither raw nor execute");
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
const partialSql = `CREATE UNIQUE INDEX IF NOT EXISTS ${INDEX_NAME} ON ${TABLE} ${COLUMNS} WHERE ${WHERE}`;
|
|
361
|
+
const fallbackSql = `CREATE INDEX IF NOT EXISTS ${INDEX_NAME} ON ${TABLE} ${COLUMNS}`;
|
|
362
|
+
try {
|
|
363
|
+
await exec(partialSql);
|
|
364
|
+
return { index: INDEX_NAME, status: "created" };
|
|
365
|
+
} catch (err) {
|
|
366
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
367
|
+
if (/partial|where clause|syntax/i.test(msg)) {
|
|
368
|
+
try {
|
|
369
|
+
await exec(fallbackSql);
|
|
370
|
+
return { index: INDEX_NAME, status: "fallback_non_unique" };
|
|
371
|
+
} catch (fallbackErr) {
|
|
372
|
+
return {
|
|
373
|
+
index: INDEX_NAME,
|
|
374
|
+
status: "error",
|
|
375
|
+
error: fallbackErr instanceof Error ? fallbackErr.message : String(fallbackErr)
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
if (/already exists/i.test(msg)) {
|
|
380
|
+
return { index: INDEX_NAME, status: "already_exists" };
|
|
381
|
+
}
|
|
382
|
+
return { index: INDEX_NAME, status: "error", error: msg };
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
547
386
|
// src/loaders/database-loader.ts
|
|
548
387
|
var DatabaseLoader = class {
|
|
549
388
|
constructor(options) {
|
|
@@ -559,11 +398,103 @@ var DatabaseLoader = class {
|
|
|
559
398
|
};
|
|
560
399
|
this.schemaReady = false;
|
|
561
400
|
this.historySchemaReady = false;
|
|
401
|
+
if (!options.driver && !options.engine) {
|
|
402
|
+
throw new Error("DatabaseLoader requires either a driver or engine");
|
|
403
|
+
}
|
|
562
404
|
this.driver = options.driver;
|
|
405
|
+
this.engine = options.engine;
|
|
563
406
|
this.tableName = options.tableName ?? "sys_metadata";
|
|
564
407
|
this.historyTableName = options.historyTableName ?? "sys_metadata_history";
|
|
565
|
-
this.
|
|
408
|
+
this.organizationId = options.organizationId;
|
|
409
|
+
this.projectId = options.projectId;
|
|
566
410
|
this.trackHistory = options.trackHistory !== false;
|
|
411
|
+
const cacheOpts = options.cache;
|
|
412
|
+
const cacheEnabled = cacheOpts?.enabled !== false;
|
|
413
|
+
if (cacheEnabled) {
|
|
414
|
+
const lruOpts = {
|
|
415
|
+
maxSize: cacheOpts?.maxSize ?? 500,
|
|
416
|
+
ttl: cacheOpts?.ttl ?? 6e4
|
|
417
|
+
};
|
|
418
|
+
this.loadCache = new LRUCache(lruOpts);
|
|
419
|
+
this.loadManyCache = new LRUCache(lruOpts);
|
|
420
|
+
this.listCache = new LRUCache(lruOpts);
|
|
421
|
+
this.statCache = new LRUCache(lruOpts);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// ==========================================
|
|
425
|
+
// Cache helpers
|
|
426
|
+
// ==========================================
|
|
427
|
+
cacheKey(type, name) {
|
|
428
|
+
return `${type}::${name}`;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Invalidate all cached entries for a specific (type, name) pair plus
|
|
432
|
+
* the type-level aggregates (`loadMany`, `list`). Called from every write
|
|
433
|
+
* path (`save`, `delete`, `registerRollback`).
|
|
434
|
+
*/
|
|
435
|
+
invalidate(type, name) {
|
|
436
|
+
if (!this.loadCache) return;
|
|
437
|
+
const key = this.cacheKey(type, name);
|
|
438
|
+
this.loadCache.delete(key);
|
|
439
|
+
this.statCache?.delete(key);
|
|
440
|
+
this.loadManyCache?.delete(type);
|
|
441
|
+
this.listCache?.delete(type);
|
|
442
|
+
}
|
|
443
|
+
/** Drop the entire cache — useful after bulk imports or schema changes. */
|
|
444
|
+
invalidateAll() {
|
|
445
|
+
this.loadCache?.clear();
|
|
446
|
+
this.loadManyCache?.clear();
|
|
447
|
+
this.listCache?.clear();
|
|
448
|
+
this.statCache?.clear();
|
|
449
|
+
}
|
|
450
|
+
/** Diagnostic: aggregated cache statistics for `metrics` endpoints. */
|
|
451
|
+
getCacheStats() {
|
|
452
|
+
return {
|
|
453
|
+
enabled: this.loadCache !== void 0,
|
|
454
|
+
load: this.loadCache?.stats() ?? null,
|
|
455
|
+
loadMany: this.loadManyCache?.stats() ?? null,
|
|
456
|
+
list: this.listCache?.stats() ?? null,
|
|
457
|
+
stat: this.statCache?.stats() ?? null
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
// ==========================================
|
|
461
|
+
// Internal CRUD helpers (driver vs engine)
|
|
462
|
+
// ==========================================
|
|
463
|
+
async _find(table, query) {
|
|
464
|
+
if (this.engine) {
|
|
465
|
+
return this.engine.find(table, query);
|
|
466
|
+
}
|
|
467
|
+
return this.driver.find(table, { object: table, ...query });
|
|
468
|
+
}
|
|
469
|
+
async _findOne(table, query) {
|
|
470
|
+
if (this.engine) {
|
|
471
|
+
return this.engine.findOne(table, query);
|
|
472
|
+
}
|
|
473
|
+
return this.driver.findOne(table, { object: table, ...query });
|
|
474
|
+
}
|
|
475
|
+
async _count(table, query) {
|
|
476
|
+
if (this.engine) {
|
|
477
|
+
return this.engine.count(table, query);
|
|
478
|
+
}
|
|
479
|
+
return this.driver.count(table, { object: table, ...query });
|
|
480
|
+
}
|
|
481
|
+
async _create(table, data) {
|
|
482
|
+
if (this.engine) {
|
|
483
|
+
return this.engine.insert(table, data);
|
|
484
|
+
}
|
|
485
|
+
return this.driver.create(table, data);
|
|
486
|
+
}
|
|
487
|
+
async _update(table, id, data) {
|
|
488
|
+
if (this.engine) {
|
|
489
|
+
return this.engine.update(table, { id, ...data });
|
|
490
|
+
}
|
|
491
|
+
return this.driver.update(table, id, data);
|
|
492
|
+
}
|
|
493
|
+
async _delete(table, id) {
|
|
494
|
+
if (this.engine) {
|
|
495
|
+
return this.engine.delete(table, { where: { id } });
|
|
496
|
+
}
|
|
497
|
+
return this.driver.delete(table, id);
|
|
567
498
|
}
|
|
568
499
|
/**
|
|
569
500
|
* Ensure the metadata table exists.
|
|
@@ -572,12 +503,37 @@ var DatabaseLoader = class {
|
|
|
572
503
|
*/
|
|
573
504
|
async ensureSchema() {
|
|
574
505
|
if (this.schemaReady) return;
|
|
506
|
+
if (this.engine) {
|
|
507
|
+
this.schemaReady = true;
|
|
508
|
+
try {
|
|
509
|
+
const engineAny = this.engine;
|
|
510
|
+
let driver = engineAny?.driver ?? engineAny?.getDriver?.();
|
|
511
|
+
if (!driver && engineAny?.drivers instanceof Map) {
|
|
512
|
+
for (const candidate of engineAny.drivers.values()) {
|
|
513
|
+
const c = candidate;
|
|
514
|
+
if (c && (typeof c.raw === "function" || typeof c.execute === "function")) {
|
|
515
|
+
driver = candidate;
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (driver) {
|
|
521
|
+
await addSysMetadataOverlayIndex(driver);
|
|
522
|
+
}
|
|
523
|
+
} catch {
|
|
524
|
+
}
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
575
527
|
try {
|
|
576
528
|
await this.driver.syncSchema(this.tableName, {
|
|
577
529
|
...SysMetadataObject,
|
|
578
530
|
name: this.tableName
|
|
579
531
|
});
|
|
580
532
|
this.schemaReady = true;
|
|
533
|
+
try {
|
|
534
|
+
await addSysMetadataOverlayIndex(this.driver);
|
|
535
|
+
} catch {
|
|
536
|
+
}
|
|
581
537
|
} catch {
|
|
582
538
|
this.schemaReady = true;
|
|
583
539
|
}
|
|
@@ -588,6 +544,10 @@ var DatabaseLoader = class {
|
|
|
588
544
|
*/
|
|
589
545
|
async ensureHistorySchema() {
|
|
590
546
|
if (!this.trackHistory || this.historySchemaReady) return;
|
|
547
|
+
if (this.engine) {
|
|
548
|
+
this.historySchemaReady = true;
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
591
551
|
try {
|
|
592
552
|
await this.driver.syncSchema(this.historyTableName, {
|
|
593
553
|
...SysMetadataHistoryObject,
|
|
@@ -600,16 +560,18 @@ var DatabaseLoader = class {
|
|
|
600
560
|
}
|
|
601
561
|
/**
|
|
602
562
|
* Build base filter conditions for queries.
|
|
603
|
-
*
|
|
563
|
+
* Filters by organizationId when configured; project_id when projectId is set,
|
|
564
|
+
* or null (platform-global) when not set.
|
|
604
565
|
*/
|
|
605
566
|
baseFilter(type, name) {
|
|
606
567
|
const filter = { type };
|
|
607
568
|
if (name !== void 0) {
|
|
608
569
|
filter.name = name;
|
|
609
570
|
}
|
|
610
|
-
if (this.
|
|
611
|
-
filter.
|
|
571
|
+
if (this.organizationId) {
|
|
572
|
+
filter.organization_id = this.organizationId;
|
|
612
573
|
}
|
|
574
|
+
filter.project_id = this.projectId ?? null;
|
|
613
575
|
return filter;
|
|
614
576
|
}
|
|
615
577
|
/**
|
|
@@ -648,10 +610,11 @@ var DatabaseLoader = class {
|
|
|
648
610
|
changeNote,
|
|
649
611
|
recordedBy,
|
|
650
612
|
recordedAt: now,
|
|
651
|
-
...this.
|
|
613
|
+
...this.organizationId ? { organizationId: this.organizationId } : {},
|
|
614
|
+
...this.projectId !== void 0 ? { projectId: this.projectId } : {}
|
|
652
615
|
};
|
|
653
616
|
try {
|
|
654
|
-
await this.
|
|
617
|
+
await this._create(this.historyTableName, {
|
|
655
618
|
id: historyRecord.id,
|
|
656
619
|
metadata_id: historyRecord.metadataId,
|
|
657
620
|
name: historyRecord.name,
|
|
@@ -664,7 +627,8 @@ var DatabaseLoader = class {
|
|
|
664
627
|
change_note: historyRecord.changeNote,
|
|
665
628
|
recorded_by: historyRecord.recordedBy,
|
|
666
629
|
recorded_at: historyRecord.recordedAt,
|
|
667
|
-
...this.
|
|
630
|
+
...this.organizationId ? { organization_id: this.organizationId } : {},
|
|
631
|
+
...this.projectId !== void 0 ? { project_id: this.projectId } : {}
|
|
668
632
|
});
|
|
669
633
|
} catch (error) {
|
|
670
634
|
console.error(`Failed to create history record for ${type}/${name}:`, error);
|
|
@@ -696,7 +660,8 @@ var DatabaseLoader = class {
|
|
|
696
660
|
strategy: row.strategy ?? "merge",
|
|
697
661
|
owner: row.owner,
|
|
698
662
|
state: row.state ?? "active",
|
|
699
|
-
|
|
663
|
+
organizationId: row.organization_id,
|
|
664
|
+
projectId: row.project_id,
|
|
700
665
|
version: row.version ?? 1,
|
|
701
666
|
checksum: row.checksum,
|
|
702
667
|
source: row.source,
|
|
@@ -713,12 +678,24 @@ var DatabaseLoader = class {
|
|
|
713
678
|
async load(type, name, _options) {
|
|
714
679
|
const startTime = Date.now();
|
|
715
680
|
await this.ensureSchema();
|
|
681
|
+
const key = this.cacheKey(type, name);
|
|
682
|
+
if (this.loadCache) {
|
|
683
|
+
const cached = this.loadCache.get(key);
|
|
684
|
+
if (cached !== void 0) {
|
|
685
|
+
return {
|
|
686
|
+
data: cached,
|
|
687
|
+
source: "database",
|
|
688
|
+
format: "json",
|
|
689
|
+
loadTime: Date.now() - startTime
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
}
|
|
716
693
|
try {
|
|
717
|
-
const row = await this.
|
|
718
|
-
object: this.tableName,
|
|
694
|
+
const row = await this._findOne(this.tableName, {
|
|
719
695
|
where: this.baseFilter(type, name)
|
|
720
696
|
});
|
|
721
697
|
if (!row) {
|
|
698
|
+
this.loadCache?.set(key, null);
|
|
722
699
|
return {
|
|
723
700
|
data: null,
|
|
724
701
|
loadTime: Date.now() - startTime
|
|
@@ -726,6 +703,7 @@ var DatabaseLoader = class {
|
|
|
726
703
|
}
|
|
727
704
|
const data = this.rowToData(row);
|
|
728
705
|
const record = this.rowToRecord(row);
|
|
706
|
+
this.loadCache?.set(key, data);
|
|
729
707
|
return {
|
|
730
708
|
data,
|
|
731
709
|
source: "database",
|
|
@@ -742,21 +720,29 @@ var DatabaseLoader = class {
|
|
|
742
720
|
}
|
|
743
721
|
async loadMany(type, _options) {
|
|
744
722
|
await this.ensureSchema();
|
|
723
|
+
if (this.loadManyCache) {
|
|
724
|
+
const cached = this.loadManyCache.get(type);
|
|
725
|
+
if (cached !== void 0) return cached;
|
|
726
|
+
}
|
|
745
727
|
try {
|
|
746
|
-
const rows = await this.
|
|
747
|
-
object: this.tableName,
|
|
728
|
+
const rows = await this._find(this.tableName, {
|
|
748
729
|
where: this.baseFilter(type)
|
|
749
730
|
});
|
|
750
|
-
|
|
731
|
+
const result = rows.map((row) => this.rowToData(row)).filter((data) => data !== null);
|
|
732
|
+
this.loadManyCache?.set(type, result);
|
|
733
|
+
return result;
|
|
751
734
|
} catch {
|
|
752
735
|
return [];
|
|
753
736
|
}
|
|
754
737
|
}
|
|
755
738
|
async exists(type, name) {
|
|
756
739
|
await this.ensureSchema();
|
|
740
|
+
if (this.loadCache) {
|
|
741
|
+
const cached = this.loadCache.get(this.cacheKey(type, name));
|
|
742
|
+
if (cached !== void 0) return cached !== null;
|
|
743
|
+
}
|
|
757
744
|
try {
|
|
758
|
-
const count = await this.
|
|
759
|
-
object: this.tableName,
|
|
745
|
+
const count = await this._count(this.tableName, {
|
|
760
746
|
where: this.baseFilter(type, name)
|
|
761
747
|
});
|
|
762
748
|
return count > 0;
|
|
@@ -766,33 +752,47 @@ var DatabaseLoader = class {
|
|
|
766
752
|
}
|
|
767
753
|
async stat(type, name) {
|
|
768
754
|
await this.ensureSchema();
|
|
755
|
+
const key = this.cacheKey(type, name);
|
|
756
|
+
if (this.statCache) {
|
|
757
|
+
const cached = this.statCache.get(key);
|
|
758
|
+
if (cached !== void 0) return cached;
|
|
759
|
+
}
|
|
769
760
|
try {
|
|
770
|
-
const row = await this.
|
|
771
|
-
object: this.tableName,
|
|
761
|
+
const row = await this._findOne(this.tableName, {
|
|
772
762
|
where: this.baseFilter(type, name)
|
|
773
763
|
});
|
|
774
|
-
if (!row)
|
|
764
|
+
if (!row) {
|
|
765
|
+
this.statCache?.set(key, null);
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
775
768
|
const record = this.rowToRecord(row);
|
|
776
769
|
const metadataStr = typeof row.metadata === "string" ? row.metadata : JSON.stringify(row.metadata);
|
|
777
|
-
|
|
770
|
+
const stats = {
|
|
778
771
|
size: metadataStr.length,
|
|
779
772
|
mtime: record.updatedAt ?? record.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
780
773
|
format: "json",
|
|
781
774
|
etag: record.checksum
|
|
782
775
|
};
|
|
776
|
+
this.statCache?.set(key, stats);
|
|
777
|
+
return stats;
|
|
783
778
|
} catch {
|
|
784
779
|
return null;
|
|
785
780
|
}
|
|
786
781
|
}
|
|
787
782
|
async list(type) {
|
|
788
783
|
await this.ensureSchema();
|
|
784
|
+
if (this.listCache) {
|
|
785
|
+
const cached = this.listCache.get(type);
|
|
786
|
+
if (cached !== void 0) return cached;
|
|
787
|
+
}
|
|
789
788
|
try {
|
|
790
|
-
const rows = await this.
|
|
791
|
-
object: this.tableName,
|
|
789
|
+
const rows = await this._find(this.tableName, {
|
|
792
790
|
where: this.baseFilter(type),
|
|
793
791
|
fields: ["name"]
|
|
794
792
|
});
|
|
795
|
-
|
|
793
|
+
const names = rows.map((row) => row.name).filter((name) => typeof name === "string");
|
|
794
|
+
this.listCache?.set(type, names);
|
|
795
|
+
return names;
|
|
796
796
|
} catch {
|
|
797
797
|
return [];
|
|
798
798
|
}
|
|
@@ -804,8 +804,7 @@ var DatabaseLoader = class {
|
|
|
804
804
|
async getHistoryRecord(type, name, version) {
|
|
805
805
|
if (!this.trackHistory) return null;
|
|
806
806
|
await this.ensureHistorySchema();
|
|
807
|
-
const metadataRow = await this.
|
|
808
|
-
object: this.tableName,
|
|
807
|
+
const metadataRow = await this._findOne(this.tableName, {
|
|
809
808
|
where: this.baseFilter(type, name)
|
|
810
809
|
});
|
|
811
810
|
if (!metadataRow) return null;
|
|
@@ -813,11 +812,11 @@ var DatabaseLoader = class {
|
|
|
813
812
|
metadata_id: metadataRow.id,
|
|
814
813
|
version
|
|
815
814
|
};
|
|
816
|
-
if (this.
|
|
817
|
-
filter.
|
|
815
|
+
if (this.organizationId) {
|
|
816
|
+
filter.organization_id = this.organizationId;
|
|
818
817
|
}
|
|
819
|
-
|
|
820
|
-
|
|
818
|
+
filter.project_id = this.projectId ?? null;
|
|
819
|
+
const row = await this._findOne(this.historyTableName, {
|
|
821
820
|
where: filter
|
|
822
821
|
});
|
|
823
822
|
if (!row) return null;
|
|
@@ -832,11 +831,80 @@ var DatabaseLoader = class {
|
|
|
832
831
|
checksum: row.checksum,
|
|
833
832
|
previousChecksum: row.previous_checksum,
|
|
834
833
|
changeNote: row.change_note,
|
|
835
|
-
|
|
834
|
+
organizationId: row.organization_id,
|
|
835
|
+
projectId: row.project_id,
|
|
836
836
|
recordedBy: row.recorded_by,
|
|
837
837
|
recordedAt: row.recorded_at
|
|
838
838
|
};
|
|
839
839
|
}
|
|
840
|
+
/**
|
|
841
|
+
* Query history records with pagination and filtering.
|
|
842
|
+
* Encapsulates history table queries so MetadataManager doesn't need
|
|
843
|
+
* direct driver access.
|
|
844
|
+
*/
|
|
845
|
+
async queryHistory(type, name, options) {
|
|
846
|
+
if (!this.trackHistory) {
|
|
847
|
+
return { records: [], total: 0, hasMore: false };
|
|
848
|
+
}
|
|
849
|
+
await this.ensureSchema();
|
|
850
|
+
await this.ensureHistorySchema();
|
|
851
|
+
const filter = { type, name };
|
|
852
|
+
if (this.organizationId) filter.organization_id = this.organizationId;
|
|
853
|
+
filter.project_id = this.projectId ?? null;
|
|
854
|
+
const metadataRecord = await this._findOne(this.tableName, { where: filter });
|
|
855
|
+
if (!metadataRecord) {
|
|
856
|
+
return { records: [], total: 0, hasMore: false };
|
|
857
|
+
}
|
|
858
|
+
const historyFilter = {
|
|
859
|
+
metadata_id: metadataRecord.id
|
|
860
|
+
};
|
|
861
|
+
if (this.organizationId) historyFilter.organization_id = this.organizationId;
|
|
862
|
+
historyFilter.project_id = this.projectId ?? null;
|
|
863
|
+
if (options?.operationType) historyFilter.operation_type = options.operationType;
|
|
864
|
+
if (options?.since) historyFilter.recorded_at = { $gte: options.since };
|
|
865
|
+
if (options?.until) {
|
|
866
|
+
if (historyFilter.recorded_at) {
|
|
867
|
+
historyFilter.recorded_at.$lte = options.until;
|
|
868
|
+
} else {
|
|
869
|
+
historyFilter.recorded_at = { $lte: options.until };
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
const limit = options?.limit ?? 50;
|
|
873
|
+
const offset = options?.offset ?? 0;
|
|
874
|
+
const historyRecords = await this._find(this.historyTableName, {
|
|
875
|
+
where: historyFilter,
|
|
876
|
+
orderBy: [
|
|
877
|
+
{ field: "recorded_at", order: "desc" },
|
|
878
|
+
{ field: "version", order: "desc" }
|
|
879
|
+
],
|
|
880
|
+
limit: limit + 1,
|
|
881
|
+
offset
|
|
882
|
+
});
|
|
883
|
+
const hasMore = historyRecords.length > limit;
|
|
884
|
+
const records = historyRecords.slice(0, limit);
|
|
885
|
+
const total = await this._count(this.historyTableName, { where: historyFilter });
|
|
886
|
+
const includeMetadata = options?.includeMetadata !== false;
|
|
887
|
+
const result = records.map((row) => {
|
|
888
|
+
const parsedMetadata = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
|
|
889
|
+
return {
|
|
890
|
+
id: row.id,
|
|
891
|
+
metadataId: row.metadata_id,
|
|
892
|
+
name: row.name,
|
|
893
|
+
type: row.type,
|
|
894
|
+
version: row.version,
|
|
895
|
+
operationType: row.operation_type,
|
|
896
|
+
metadata: includeMetadata ? parsedMetadata : null,
|
|
897
|
+
checksum: row.checksum,
|
|
898
|
+
previousChecksum: row.previous_checksum,
|
|
899
|
+
changeNote: row.change_note,
|
|
900
|
+
organizationId: row.organization_id,
|
|
901
|
+
projectId: row.project_id,
|
|
902
|
+
recordedBy: row.recorded_by,
|
|
903
|
+
recordedAt: row.recorded_at
|
|
904
|
+
};
|
|
905
|
+
});
|
|
906
|
+
return { records: result, total, hasMore };
|
|
907
|
+
}
|
|
840
908
|
/**
|
|
841
909
|
* Perform a rollback: persist `restoredData` as the new current state and record a
|
|
842
910
|
* single 'revert' history entry (instead of the usual 'update' entry that `save()`
|
|
@@ -849,8 +917,7 @@ var DatabaseLoader = class {
|
|
|
849
917
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
850
918
|
const metadataJson = JSON.stringify(restoredData);
|
|
851
919
|
const newChecksum = await calculateChecksum(restoredData);
|
|
852
|
-
const existing = await this.
|
|
853
|
-
object: this.tableName,
|
|
920
|
+
const existing = await this._findOne(this.tableName, {
|
|
854
921
|
where: this.baseFilter(type, name)
|
|
855
922
|
});
|
|
856
923
|
if (!existing) {
|
|
@@ -858,13 +925,14 @@ var DatabaseLoader = class {
|
|
|
858
925
|
}
|
|
859
926
|
const previousChecksum = existing.checksum;
|
|
860
927
|
const newVersion = (existing.version ?? 0) + 1;
|
|
861
|
-
await this.
|
|
928
|
+
await this._update(this.tableName, existing.id, {
|
|
862
929
|
metadata: metadataJson,
|
|
863
930
|
version: newVersion,
|
|
864
931
|
checksum: newChecksum,
|
|
865
932
|
updated_at: now,
|
|
866
933
|
state: "active"
|
|
867
934
|
});
|
|
935
|
+
this.invalidate(type, name);
|
|
868
936
|
await this.createHistoryRecord(
|
|
869
937
|
existing.id,
|
|
870
938
|
type,
|
|
@@ -884,13 +952,13 @@ var DatabaseLoader = class {
|
|
|
884
952
|
const metadataJson = JSON.stringify(data);
|
|
885
953
|
const newChecksum = await calculateChecksum(data);
|
|
886
954
|
try {
|
|
887
|
-
const existing = await this.
|
|
888
|
-
object: this.tableName,
|
|
955
|
+
const existing = await this._findOne(this.tableName, {
|
|
889
956
|
where: this.baseFilter(type, name)
|
|
890
957
|
});
|
|
891
958
|
if (existing) {
|
|
892
959
|
const previousChecksum = existing.checksum;
|
|
893
960
|
if (newChecksum === previousChecksum) {
|
|
961
|
+
this.loadCache?.set(this.cacheKey(type, name), data);
|
|
894
962
|
return {
|
|
895
963
|
success: true,
|
|
896
964
|
path: `datasource://${this.tableName}/${type}/${name}`,
|
|
@@ -899,13 +967,14 @@ var DatabaseLoader = class {
|
|
|
899
967
|
};
|
|
900
968
|
}
|
|
901
969
|
const version = (existing.version ?? 0) + 1;
|
|
902
|
-
await this.
|
|
970
|
+
await this._update(this.tableName, existing.id, {
|
|
903
971
|
metadata: metadataJson,
|
|
904
972
|
version,
|
|
905
973
|
checksum: newChecksum,
|
|
906
974
|
updated_at: now,
|
|
907
975
|
state: "active"
|
|
908
976
|
});
|
|
977
|
+
this.invalidate(type, name);
|
|
909
978
|
await this.createHistoryRecord(
|
|
910
979
|
existing.id,
|
|
911
980
|
type,
|
|
@@ -923,7 +992,7 @@ var DatabaseLoader = class {
|
|
|
923
992
|
};
|
|
924
993
|
} else {
|
|
925
994
|
const id = generateId();
|
|
926
|
-
await this.
|
|
995
|
+
await this._create(this.tableName, {
|
|
927
996
|
id,
|
|
928
997
|
name,
|
|
929
998
|
type,
|
|
@@ -935,10 +1004,12 @@ var DatabaseLoader = class {
|
|
|
935
1004
|
state: "active",
|
|
936
1005
|
version: 1,
|
|
937
1006
|
source: "database",
|
|
938
|
-
...this.
|
|
1007
|
+
...this.organizationId ? { organization_id: this.organizationId } : {},
|
|
1008
|
+
...this.projectId !== void 0 ? { project_id: this.projectId } : { project_id: null },
|
|
939
1009
|
created_at: now,
|
|
940
1010
|
updated_at: now
|
|
941
1011
|
});
|
|
1012
|
+
this.invalidate(type, name);
|
|
942
1013
|
await this.createHistoryRecord(
|
|
943
1014
|
id,
|
|
944
1015
|
type,
|
|
@@ -965,14 +1036,14 @@ var DatabaseLoader = class {
|
|
|
965
1036
|
*/
|
|
966
1037
|
async delete(type, name) {
|
|
967
1038
|
await this.ensureSchema();
|
|
968
|
-
const existing = await this.
|
|
969
|
-
object: this.tableName,
|
|
1039
|
+
const existing = await this._findOne(this.tableName, {
|
|
970
1040
|
where: this.baseFilter(type, name)
|
|
971
1041
|
});
|
|
972
1042
|
if (!existing) {
|
|
973
1043
|
return;
|
|
974
1044
|
}
|
|
975
|
-
await this.
|
|
1045
|
+
await this._delete(this.tableName, existing.id);
|
|
1046
|
+
this.invalidate(type, name);
|
|
976
1047
|
}
|
|
977
1048
|
};
|
|
978
1049
|
function generateId() {
|
|
@@ -983,7 +1054,7 @@ function generateId() {
|
|
|
983
1054
|
}
|
|
984
1055
|
|
|
985
1056
|
// src/metadata-manager.ts
|
|
986
|
-
var
|
|
1057
|
+
var _MetadataManager = class _MetadataManager {
|
|
987
1058
|
constructor(config) {
|
|
988
1059
|
this.loaders = /* @__PURE__ */ new Map();
|
|
989
1060
|
this.watchCallbacks = /* @__PURE__ */ new Map();
|
|
@@ -995,6 +1066,18 @@ var MetadataManager = class {
|
|
|
995
1066
|
this.typeRegistry = [];
|
|
996
1067
|
// Dependency tracking: "type:name" -> dependencies
|
|
997
1068
|
this.dependencies = /* @__PURE__ */ new Map();
|
|
1069
|
+
// Short-lived cache for list() results. Built primarily to break the
|
|
1070
|
+
// deadlock that occurs when security/permission middleware calls
|
|
1071
|
+
// `list('permission')` from inside a user-initiated DB transaction: the
|
|
1072
|
+
// DatabaseLoader's `engine.find('sys_metadata', ...)` would then try to
|
|
1073
|
+
// acquire a fresh knex connection while the transaction is still holding
|
|
1074
|
+
// SQLite's single connection — knex waits the full `acquireConnectionTimeout`
|
|
1075
|
+
// (60s) before returning []. The cache absorbs the repeated lookups so the
|
|
1076
|
+
// loader is only hit once per TTL window.
|
|
1077
|
+
//
|
|
1078
|
+
// Invalidated on every `register()` / `unregister()` to keep CRUD writes
|
|
1079
|
+
// visible to subsequent reads.
|
|
1080
|
+
this.listCache = /* @__PURE__ */ new Map();
|
|
998
1081
|
this.config = config;
|
|
999
1082
|
this.logger = createLogger({ level: "info", format: "pretty" });
|
|
1000
1083
|
this.serializers = /* @__PURE__ */ new Map();
|
|
@@ -1029,16 +1112,57 @@ var MetadataManager = class {
|
|
|
1029
1112
|
* Can be called at any time to enable database storage (e.g. after kernel resolves the driver).
|
|
1030
1113
|
*
|
|
1031
1114
|
* @param driver - An IDataDriver instance for database operations
|
|
1032
|
-
|
|
1033
|
-
|
|
1115
|
+
* @param organizationId - Organization ID for multi-tenant isolation
|
|
1116
|
+
* @param projectId - Project ID (undefined = platform-global)
|
|
1117
|
+
*/
|
|
1118
|
+
setDatabaseDriver(driver, organizationId, projectId) {
|
|
1119
|
+
if (projectId !== void 0) {
|
|
1120
|
+
this.logger.info("Project kernel \u2014 skipping DatabaseLoader for sys_metadata (control-plane only)", {
|
|
1121
|
+
organizationId,
|
|
1122
|
+
projectId
|
|
1123
|
+
});
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1034
1126
|
const tableName = this.config.tableName ?? "sys_metadata";
|
|
1035
1127
|
const dbLoader = new DatabaseLoader({
|
|
1036
1128
|
driver,
|
|
1037
|
-
tableName
|
|
1129
|
+
tableName,
|
|
1130
|
+
organizationId,
|
|
1131
|
+
projectId,
|
|
1132
|
+
cache: this.config.cache?.databaseLoader
|
|
1038
1133
|
});
|
|
1039
1134
|
this.registerLoader(dbLoader);
|
|
1040
1135
|
this.logger.info("DatabaseLoader configured", { datasource: this.config.datasource, tableName });
|
|
1041
1136
|
}
|
|
1137
|
+
/**
|
|
1138
|
+
* Configure and register a DatabaseLoader backed by an IDataEngine (ObjectQL).
|
|
1139
|
+
* The engine handles datasource routing automatically — sys_metadata will
|
|
1140
|
+
* be routed to the correct driver via the standard namespace mapping.
|
|
1141
|
+
* No manual driver resolution needed.
|
|
1142
|
+
*
|
|
1143
|
+
* @param engine - An IDataEngine instance (typically the ObjectQL service)
|
|
1144
|
+
* @param organizationId - Organization ID for multi-tenant isolation
|
|
1145
|
+
* @param projectId - Project ID (undefined = platform-global)
|
|
1146
|
+
*/
|
|
1147
|
+
setDataEngine(engine, organizationId, projectId) {
|
|
1148
|
+
if (projectId !== void 0) {
|
|
1149
|
+
this.logger.info("Project kernel \u2014 skipping DatabaseLoader for sys_metadata (control-plane only)", {
|
|
1150
|
+
organizationId,
|
|
1151
|
+
projectId
|
|
1152
|
+
});
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
const tableName = this.config.tableName ?? "sys_metadata";
|
|
1156
|
+
const dbLoader = new DatabaseLoader({
|
|
1157
|
+
engine,
|
|
1158
|
+
tableName,
|
|
1159
|
+
organizationId,
|
|
1160
|
+
projectId,
|
|
1161
|
+
cache: this.config.cache?.databaseLoader
|
|
1162
|
+
});
|
|
1163
|
+
this.registerLoader(dbLoader);
|
|
1164
|
+
this.logger.info("DatabaseLoader configured via DataEngine", { tableName });
|
|
1165
|
+
}
|
|
1042
1166
|
/**
|
|
1043
1167
|
* Set the realtime service for publishing metadata change events.
|
|
1044
1168
|
* Should be called after kernel resolves the realtime service.
|
|
@@ -1066,10 +1190,19 @@ var MetadataManager = class {
|
|
|
1066
1190
|
* should not be written to during runtime registration.
|
|
1067
1191
|
*/
|
|
1068
1192
|
async register(type, name, data) {
|
|
1193
|
+
if (this.config.persistence?.writable === false) {
|
|
1194
|
+
const msg = `MetadataManager is read-only (persistence.writable=false); refusing to register ${type}/${name}`;
|
|
1195
|
+
if (this.config.validation?.throwOnError) {
|
|
1196
|
+
throw new Error(msg);
|
|
1197
|
+
}
|
|
1198
|
+
this.logger.warn(msg);
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1069
1201
|
if (!this.registry.has(type)) {
|
|
1070
1202
|
this.registry.set(type, /* @__PURE__ */ new Map());
|
|
1071
1203
|
}
|
|
1072
1204
|
this.registry.get(type).set(name, data);
|
|
1205
|
+
this.invalidateListCache(type);
|
|
1073
1206
|
for (const loader of this.loaders.values()) {
|
|
1074
1207
|
if (loader.save && loader.contract.protocol === "datasource:" && loader.contract.capabilities.write) {
|
|
1075
1208
|
await loader.save(type, name, data);
|
|
@@ -1111,6 +1244,10 @@ var MetadataManager = class {
|
|
|
1111
1244
|
* List all metadata items of a given type
|
|
1112
1245
|
*/
|
|
1113
1246
|
async list(type) {
|
|
1247
|
+
const cached = this.listCache.get(type);
|
|
1248
|
+
if (cached && Date.now() - cached.ts < _MetadataManager.LIST_CACHE_TTL_MS) {
|
|
1249
|
+
return cached.items;
|
|
1250
|
+
}
|
|
1114
1251
|
const items = /* @__PURE__ */ new Map();
|
|
1115
1252
|
const typeStore = this.registry.get(type);
|
|
1116
1253
|
if (typeStore) {
|
|
@@ -1131,7 +1268,16 @@ var MetadataManager = class {
|
|
|
1131
1268
|
this.logger.warn(`Loader ${loader.contract.name} failed to loadMany ${type}`, { error: e });
|
|
1132
1269
|
}
|
|
1133
1270
|
}
|
|
1134
|
-
|
|
1271
|
+
const result = Array.from(items.values());
|
|
1272
|
+
this.cacheListResult(type, result);
|
|
1273
|
+
return result;
|
|
1274
|
+
}
|
|
1275
|
+
cacheListResult(type, items) {
|
|
1276
|
+
this.listCache.set(type, { ts: Date.now(), items });
|
|
1277
|
+
}
|
|
1278
|
+
/** Internal helper: drop the cached `list()` result for a type. */
|
|
1279
|
+
invalidateListCache(type) {
|
|
1280
|
+
this.listCache.delete(type);
|
|
1135
1281
|
}
|
|
1136
1282
|
/**
|
|
1137
1283
|
* Unregister/remove a metadata item by type and name.
|
|
@@ -1145,6 +1291,7 @@ var MetadataManager = class {
|
|
|
1145
1291
|
this.registry.delete(type);
|
|
1146
1292
|
}
|
|
1147
1293
|
}
|
|
1294
|
+
this.invalidateListCache(type);
|
|
1148
1295
|
for (const loader of this.loaders.values()) {
|
|
1149
1296
|
if (loader.contract.protocol !== "datasource:" || !loader.contract.capabilities.write) continue;
|
|
1150
1297
|
if (typeof loader.delete === "function") {
|
|
@@ -1559,6 +1706,14 @@ var MetadataManager = class {
|
|
|
1559
1706
|
* Save/update an overlay for a metadata item
|
|
1560
1707
|
*/
|
|
1561
1708
|
async saveOverlay(overlay) {
|
|
1709
|
+
if (this.config.persistence?.overlayWritable === false) {
|
|
1710
|
+
const msg = `MetadataManager overlays are read-only (persistence.overlayWritable=false); refusing to save overlay for ${overlay.baseType}/${overlay.baseName}`;
|
|
1711
|
+
if (this.config.validation?.throwOnError) {
|
|
1712
|
+
throw new Error(msg);
|
|
1713
|
+
}
|
|
1714
|
+
this.logger.warn(msg);
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1562
1717
|
const key = this.overlayKey(overlay.baseType, overlay.baseName, overlay.scope);
|
|
1563
1718
|
this.overlays.set(key, overlay);
|
|
1564
1719
|
}
|
|
@@ -1952,84 +2107,14 @@ var MetadataManager = class {
|
|
|
1952
2107
|
if (!dbLoader) {
|
|
1953
2108
|
throw new Error("History tracking requires a database loader to be configured");
|
|
1954
2109
|
}
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
}
|
|
1963
|
-
const metadataRecord = await driver.findOne(tableName, {
|
|
1964
|
-
object: tableName,
|
|
1965
|
-
where: filter
|
|
1966
|
-
});
|
|
1967
|
-
if (!metadataRecord) {
|
|
1968
|
-
return {
|
|
1969
|
-
records: [],
|
|
1970
|
-
total: 0,
|
|
1971
|
-
hasMore: false
|
|
1972
|
-
};
|
|
1973
|
-
}
|
|
1974
|
-
const historyFilter = {
|
|
1975
|
-
metadata_id: metadataRecord.id
|
|
1976
|
-
};
|
|
1977
|
-
if (tenantId) {
|
|
1978
|
-
historyFilter.tenant_id = tenantId;
|
|
1979
|
-
}
|
|
1980
|
-
if (options?.operationType) {
|
|
1981
|
-
historyFilter.operation_type = options.operationType;
|
|
1982
|
-
}
|
|
1983
|
-
if (options?.since) {
|
|
1984
|
-
historyFilter.recorded_at = { $gte: options.since };
|
|
1985
|
-
}
|
|
1986
|
-
if (options?.until) {
|
|
1987
|
-
if (historyFilter.recorded_at) {
|
|
1988
|
-
historyFilter.recorded_at.$lte = options.until;
|
|
1989
|
-
} else {
|
|
1990
|
-
historyFilter.recorded_at = { $lte: options.until };
|
|
1991
|
-
}
|
|
1992
|
-
}
|
|
1993
|
-
const limit = options?.limit ?? 50;
|
|
1994
|
-
const offset = options?.offset ?? 0;
|
|
1995
|
-
const historyRecords = await driver.find(historyTableName, {
|
|
1996
|
-
object: historyTableName,
|
|
1997
|
-
where: historyFilter,
|
|
1998
|
-
orderBy: [{ field: "recorded_at", order: "desc" }],
|
|
1999
|
-
limit: limit + 1,
|
|
2000
|
-
// Fetch one extra to determine hasMore
|
|
2001
|
-
offset
|
|
2002
|
-
});
|
|
2003
|
-
const hasMore = historyRecords.length > limit;
|
|
2004
|
-
const records = historyRecords.slice(0, limit);
|
|
2005
|
-
const total = await driver.count(historyTableName, {
|
|
2006
|
-
object: historyTableName,
|
|
2007
|
-
where: historyFilter
|
|
2008
|
-
});
|
|
2009
|
-
const includeMetadata = options?.includeMetadata !== false;
|
|
2010
|
-
const historyResult = records.map((row) => {
|
|
2011
|
-
const parsedMetadata = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
|
|
2012
|
-
return {
|
|
2013
|
-
id: row.id,
|
|
2014
|
-
metadataId: row.metadata_id,
|
|
2015
|
-
name: row.name,
|
|
2016
|
-
type: row.type,
|
|
2017
|
-
version: row.version,
|
|
2018
|
-
operationType: row.operation_type,
|
|
2019
|
-
metadata: includeMetadata ? parsedMetadata : null,
|
|
2020
|
-
checksum: row.checksum,
|
|
2021
|
-
previousChecksum: row.previous_checksum,
|
|
2022
|
-
changeNote: row.change_note,
|
|
2023
|
-
tenantId: row.tenant_id,
|
|
2024
|
-
recordedBy: row.recorded_by,
|
|
2025
|
-
recordedAt: row.recorded_at
|
|
2026
|
-
};
|
|
2110
|
+
return dbLoader.queryHistory(type, name, {
|
|
2111
|
+
operationType: options?.operationType,
|
|
2112
|
+
since: options?.since,
|
|
2113
|
+
until: options?.until,
|
|
2114
|
+
limit: options?.limit,
|
|
2115
|
+
offset: options?.offset,
|
|
2116
|
+
includeMetadata: options?.includeMetadata
|
|
2027
2117
|
});
|
|
2028
|
-
return {
|
|
2029
|
-
records: historyResult,
|
|
2030
|
-
total,
|
|
2031
|
-
hasMore
|
|
2032
|
-
};
|
|
2033
2118
|
}
|
|
2034
2119
|
/**
|
|
2035
2120
|
* Rollback a metadata item to a specific version.
|
|
@@ -2098,6 +2183,12 @@ var MetadataManager = class {
|
|
|
2098
2183
|
};
|
|
2099
2184
|
}
|
|
2100
2185
|
};
|
|
2186
|
+
_MetadataManager.LIST_CACHE_TTL_MS = 3e4;
|
|
2187
|
+
var MetadataManager = _MetadataManager;
|
|
2188
|
+
|
|
2189
|
+
// src/plugin.ts
|
|
2190
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
2191
|
+
import { createHash as createHash2 } from "crypto";
|
|
2101
2192
|
|
|
2102
2193
|
// src/node-metadata-manager.ts
|
|
2103
2194
|
import * as path2 from "path";
|
|
@@ -2215,7 +2306,7 @@ var FilesystemLoader = class {
|
|
|
2215
2306
|
);
|
|
2216
2307
|
for (const pattern of globPatterns) {
|
|
2217
2308
|
const files = await glob(pattern, {
|
|
2218
|
-
ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*"],
|
|
2309
|
+
ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*", "**/*[*]*"],
|
|
2219
2310
|
nodir: true
|
|
2220
2311
|
});
|
|
2221
2312
|
for (const file of files) {
|
|
@@ -2504,124 +2595,6 @@ var NodeMetadataManager = class extends MetadataManager {
|
|
|
2504
2595
|
}
|
|
2505
2596
|
};
|
|
2506
2597
|
|
|
2507
|
-
// src/plugin.ts
|
|
2508
|
-
import { DEFAULT_METADATA_TYPE_REGISTRY } from "@objectstack/spec/kernel";
|
|
2509
|
-
var MetadataPlugin = class {
|
|
2510
|
-
constructor(options = {}) {
|
|
2511
|
-
this.name = "com.objectstack.metadata";
|
|
2512
|
-
this.type = "standard";
|
|
2513
|
-
this.version = "1.0.0";
|
|
2514
|
-
this.init = async (ctx) => {
|
|
2515
|
-
ctx.logger.info("Initializing Metadata Manager", {
|
|
2516
|
-
root: this.options.rootDir || process.cwd(),
|
|
2517
|
-
watch: this.options.watch
|
|
2518
|
-
});
|
|
2519
|
-
ctx.registerService("metadata", this.manager);
|
|
2520
|
-
console.log("[MetadataPlugin] Registered metadata service, has getRegisteredTypes:", typeof this.manager.getRegisteredTypes);
|
|
2521
|
-
try {
|
|
2522
|
-
ctx.getService("manifest").register({
|
|
2523
|
-
id: "com.objectstack.metadata",
|
|
2524
|
-
name: "Metadata",
|
|
2525
|
-
version: "1.0.0",
|
|
2526
|
-
type: "plugin",
|
|
2527
|
-
namespace: "sys",
|
|
2528
|
-
objects: [SysMetadataObject]
|
|
2529
|
-
});
|
|
2530
|
-
} catch {
|
|
2531
|
-
}
|
|
2532
|
-
ctx.logger.info("MetadataPlugin providing metadata service (primary mode)", {
|
|
2533
|
-
mode: "file-system",
|
|
2534
|
-
features: ["watch", "persistence", "multi-format", "query", "overlay", "type-registry"]
|
|
2535
|
-
});
|
|
2536
|
-
};
|
|
2537
|
-
this.start = async (ctx) => {
|
|
2538
|
-
ctx.logger.info("Loading metadata from file system...");
|
|
2539
|
-
const sortedTypes = [...DEFAULT_METADATA_TYPE_REGISTRY].sort((a, b) => a.loadOrder - b.loadOrder);
|
|
2540
|
-
let totalLoaded = 0;
|
|
2541
|
-
for (const entry of sortedTypes) {
|
|
2542
|
-
try {
|
|
2543
|
-
const items = await this.manager.loadMany(entry.type, {
|
|
2544
|
-
recursive: true
|
|
2545
|
-
});
|
|
2546
|
-
if (items.length > 0) {
|
|
2547
|
-
for (const item of items) {
|
|
2548
|
-
const meta = item;
|
|
2549
|
-
if (meta?.name) {
|
|
2550
|
-
await this.manager.register(entry.type, meta.name, item);
|
|
2551
|
-
}
|
|
2552
|
-
}
|
|
2553
|
-
ctx.logger.info(`Loaded ${items.length} ${entry.type} from file system`);
|
|
2554
|
-
totalLoaded += items.length;
|
|
2555
|
-
}
|
|
2556
|
-
} catch (e) {
|
|
2557
|
-
ctx.logger.debug(`No ${entry.type} metadata found`, { error: e.message });
|
|
2558
|
-
}
|
|
2559
|
-
}
|
|
2560
|
-
ctx.logger.info("Metadata loading complete", {
|
|
2561
|
-
totalItems: totalLoaded,
|
|
2562
|
-
registeredTypes: sortedTypes.length
|
|
2563
|
-
});
|
|
2564
|
-
let driverBridged = false;
|
|
2565
|
-
try {
|
|
2566
|
-
const ql = ctx.getService("objectql");
|
|
2567
|
-
if (ql) {
|
|
2568
|
-
const tableName = this.manager["config"]?.tableName ?? "sys_metadata";
|
|
2569
|
-
const driver = ql.getDriverForObject?.(tableName);
|
|
2570
|
-
if (driver) {
|
|
2571
|
-
ctx.logger.info("[MetadataPlugin] Bridging driver to MetadataManager via ObjectQL routing", {
|
|
2572
|
-
tableName,
|
|
2573
|
-
driver: driver.name
|
|
2574
|
-
});
|
|
2575
|
-
this.manager.setDatabaseDriver(driver);
|
|
2576
|
-
driverBridged = true;
|
|
2577
|
-
} else {
|
|
2578
|
-
ctx.logger.debug("[MetadataPlugin] ObjectQL could not resolve driver for metadata table", { tableName });
|
|
2579
|
-
}
|
|
2580
|
-
}
|
|
2581
|
-
} catch {
|
|
2582
|
-
}
|
|
2583
|
-
if (!driverBridged) {
|
|
2584
|
-
try {
|
|
2585
|
-
const services = ctx.getServices();
|
|
2586
|
-
for (const [serviceName, service] of services) {
|
|
2587
|
-
if (serviceName.startsWith("driver.") && service) {
|
|
2588
|
-
ctx.logger.info("[MetadataPlugin] Bridging driver to MetadataManager (fallback: first driver)", {
|
|
2589
|
-
driverService: serviceName
|
|
2590
|
-
});
|
|
2591
|
-
this.manager.setDatabaseDriver(service);
|
|
2592
|
-
break;
|
|
2593
|
-
}
|
|
2594
|
-
}
|
|
2595
|
-
} catch (e) {
|
|
2596
|
-
ctx.logger.debug("[MetadataPlugin] No driver service found", { error: e.message });
|
|
2597
|
-
}
|
|
2598
|
-
}
|
|
2599
|
-
try {
|
|
2600
|
-
const realtimeService = ctx.getService("realtime");
|
|
2601
|
-
if (realtimeService && typeof realtimeService === "object" && "publish" in realtimeService) {
|
|
2602
|
-
ctx.logger.info("[MetadataPlugin] Bridging realtime service to MetadataManager for event publishing");
|
|
2603
|
-
this.manager.setRealtimeService(realtimeService);
|
|
2604
|
-
}
|
|
2605
|
-
} catch (e) {
|
|
2606
|
-
ctx.logger.debug("[MetadataPlugin] No realtime service found \u2014 metadata events will not be published", {
|
|
2607
|
-
error: e.message
|
|
2608
|
-
});
|
|
2609
|
-
}
|
|
2610
|
-
};
|
|
2611
|
-
this.options = {
|
|
2612
|
-
watch: true,
|
|
2613
|
-
...options
|
|
2614
|
-
};
|
|
2615
|
-
const rootDir = this.options.rootDir || process.cwd();
|
|
2616
|
-
this.manager = new NodeMetadataManager({
|
|
2617
|
-
rootDir,
|
|
2618
|
-
watch: this.options.watch ?? true,
|
|
2619
|
-
formats: ["yaml", "json", "typescript", "javascript"]
|
|
2620
|
-
});
|
|
2621
|
-
this.manager.setTypeRegistry(DEFAULT_METADATA_TYPE_REGISTRY);
|
|
2622
|
-
}
|
|
2623
|
-
};
|
|
2624
|
-
|
|
2625
2598
|
// src/loaders/memory-loader.ts
|
|
2626
2599
|
var MemoryLoader = class {
|
|
2627
2600
|
constructor() {
|
|
@@ -2700,6 +2673,289 @@ var MemoryLoader = class {
|
|
|
2700
2673
|
}
|
|
2701
2674
|
};
|
|
2702
2675
|
|
|
2676
|
+
// src/plugin.ts
|
|
2677
|
+
import { DEFAULT_METADATA_TYPE_REGISTRY } from "@objectstack/spec/kernel";
|
|
2678
|
+
import {
|
|
2679
|
+
SysMetadataObject as SysMetadataObject2,
|
|
2680
|
+
SysMetadataHistoryObject as SysMetadataHistoryObject2
|
|
2681
|
+
} from "@objectstack/platform-objects/metadata";
|
|
2682
|
+
var queryableMetadataObjects = [
|
|
2683
|
+
SysMetadataObject2,
|
|
2684
|
+
SysMetadataHistoryObject2
|
|
2685
|
+
];
|
|
2686
|
+
var ARTIFACT_FIELD_TO_TYPE = {
|
|
2687
|
+
objects: "object",
|
|
2688
|
+
objectExtensions: "object_extension",
|
|
2689
|
+
apps: "app",
|
|
2690
|
+
views: "view",
|
|
2691
|
+
pages: "page",
|
|
2692
|
+
dashboards: "dashboard",
|
|
2693
|
+
reports: "report",
|
|
2694
|
+
actions: "action",
|
|
2695
|
+
themes: "theme",
|
|
2696
|
+
workflows: "workflow",
|
|
2697
|
+
approvals: "approval",
|
|
2698
|
+
flows: "flow",
|
|
2699
|
+
roles: "role",
|
|
2700
|
+
permissions: "permission",
|
|
2701
|
+
sharingRules: "sharing_rule",
|
|
2702
|
+
policies: "policy",
|
|
2703
|
+
apis: "api",
|
|
2704
|
+
webhooks: "webhook",
|
|
2705
|
+
agents: "agent",
|
|
2706
|
+
skills: "skill",
|
|
2707
|
+
ragPipelines: "rag_pipeline",
|
|
2708
|
+
hooks: "hook",
|
|
2709
|
+
mappings: "mapping",
|
|
2710
|
+
analyticsCubes: "analytics_cube",
|
|
2711
|
+
connectors: "connector",
|
|
2712
|
+
data: "dataset"
|
|
2713
|
+
};
|
|
2714
|
+
var MetadataPlugin = class {
|
|
2715
|
+
constructor(options = {}) {
|
|
2716
|
+
this.name = "com.objectstack.metadata";
|
|
2717
|
+
this.type = "standard";
|
|
2718
|
+
this.version = "1.0.0";
|
|
2719
|
+
this.init = async (ctx) => {
|
|
2720
|
+
ctx.logger.info("Initializing Metadata Manager", {
|
|
2721
|
+
root: this.options.rootDir || process.cwd(),
|
|
2722
|
+
watch: this.options.watch,
|
|
2723
|
+
artifactSource: this.options.artifactSource?.mode
|
|
2724
|
+
});
|
|
2725
|
+
ctx.registerService("metadata", this.manager);
|
|
2726
|
+
console.log("[MetadataPlugin] Registered metadata service, has getRegisteredTypes:", typeof this.manager.getRegisteredTypes);
|
|
2727
|
+
const registerSysObjects = this.options.registerSystemObjects !== false;
|
|
2728
|
+
if (registerSysObjects) {
|
|
2729
|
+
try {
|
|
2730
|
+
const manifestService = ctx.getService("manifest");
|
|
2731
|
+
manifestService.register({
|
|
2732
|
+
id: "com.objectstack.metadata-objects",
|
|
2733
|
+
name: "Metadata Platform Objects",
|
|
2734
|
+
version: "1.0.0",
|
|
2735
|
+
type: "plugin",
|
|
2736
|
+
scope: "system",
|
|
2737
|
+
defaultDatasource: "cloud",
|
|
2738
|
+
objects: queryableMetadataObjects
|
|
2739
|
+
});
|
|
2740
|
+
ctx.logger.info("Registered system metadata objects", {
|
|
2741
|
+
queryable: queryableMetadataObjects.map((object) => object.name)
|
|
2742
|
+
});
|
|
2743
|
+
} catch {
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
ctx.logger.info("MetadataPlugin providing metadata service (primary mode)", {
|
|
2747
|
+
mode: this.options.artifactSource?.mode ?? "file-system",
|
|
2748
|
+
features: ["watch", "multi-format", "query", "overlay", "type-registry"]
|
|
2749
|
+
});
|
|
2750
|
+
};
|
|
2751
|
+
this.start = async (ctx) => {
|
|
2752
|
+
const src = this.options.artifactSource;
|
|
2753
|
+
const mode = this.options.config?.bootstrap ?? "eager";
|
|
2754
|
+
ctx.logger.info("[MetadataPlugin] Bootstrapping metadata", {
|
|
2755
|
+
bootstrap: mode,
|
|
2756
|
+
artifactSource: src?.mode ?? "none"
|
|
2757
|
+
});
|
|
2758
|
+
if (mode === "artifact-only") {
|
|
2759
|
+
if (src?.mode === "local-file") {
|
|
2760
|
+
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
|
|
2761
|
+
} else if (src?.mode === "artifact-api") {
|
|
2762
|
+
await this._loadFromArtifactApi(ctx, src);
|
|
2763
|
+
} else {
|
|
2764
|
+
throw new Error("[MetadataPlugin] bootstrap=artifact-only requires options.artifactSource to be set");
|
|
2765
|
+
}
|
|
2766
|
+
} else if (mode === "lazy") {
|
|
2767
|
+
if (src?.mode === "local-file") {
|
|
2768
|
+
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
|
|
2769
|
+
} else if (src?.mode === "artifact-api") {
|
|
2770
|
+
await this._loadFromArtifactApi(ctx, src);
|
|
2771
|
+
} else {
|
|
2772
|
+
ctx.logger.info("[MetadataPlugin] lazy bootstrap \u2014 skipping filesystem priming; metadata loads on demand");
|
|
2773
|
+
}
|
|
2774
|
+
} else {
|
|
2775
|
+
if (src?.mode === "local-file") {
|
|
2776
|
+
await this._loadFromLocalFile(ctx, src.path, src.fetchTimeoutMs);
|
|
2777
|
+
} else if (src?.mode === "artifact-api") {
|
|
2778
|
+
await this._loadFromArtifactApi(ctx, src);
|
|
2779
|
+
} else {
|
|
2780
|
+
await this._loadFromFileSystem(ctx);
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
try {
|
|
2784
|
+
const realtimeService = ctx.getService("realtime");
|
|
2785
|
+
if (realtimeService && typeof realtimeService === "object" && "publish" in realtimeService) {
|
|
2786
|
+
ctx.logger.info("[MetadataPlugin] Bridging realtime service to MetadataManager for event publishing");
|
|
2787
|
+
this.manager.setRealtimeService(realtimeService);
|
|
2788
|
+
}
|
|
2789
|
+
} catch (e) {
|
|
2790
|
+
ctx.logger.debug("[MetadataPlugin] No realtime service found \u2014 metadata events will not be published", {
|
|
2791
|
+
error: e.message
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
};
|
|
2795
|
+
this.options = {
|
|
2796
|
+
watch: true,
|
|
2797
|
+
...options
|
|
2798
|
+
};
|
|
2799
|
+
const rootDir = this.options.rootDir || process.cwd();
|
|
2800
|
+
const bootstrapMode = this.options.config?.bootstrap ?? "eager";
|
|
2801
|
+
const effectiveWatch = bootstrapMode === "artifact-only" ? false : this.options.watch ?? true;
|
|
2802
|
+
this.manager = new NodeMetadataManager({
|
|
2803
|
+
rootDir,
|
|
2804
|
+
watch: effectiveWatch,
|
|
2805
|
+
formats: ["yaml", "json", "typescript", "javascript"]
|
|
2806
|
+
});
|
|
2807
|
+
this.manager.setTypeRegistry(DEFAULT_METADATA_TYPE_REGISTRY);
|
|
2808
|
+
}
|
|
2809
|
+
/**
|
|
2810
|
+
* Fetch JSON content from a URL with configurable timeout.
|
|
2811
|
+
*/
|
|
2812
|
+
async _fetchJson(url, fetchTimeoutMs, token) {
|
|
2813
|
+
const envTimeout = Number(process.env.OS_ARTIFACT_FETCH_TIMEOUT_MS);
|
|
2814
|
+
const timeoutMs = fetchTimeoutMs ?? (Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : void 0) ?? 6e4;
|
|
2815
|
+
const controller = new AbortController();
|
|
2816
|
+
const timer = timeoutMs > 0 ? setTimeout(() => controller.abort(), timeoutMs) : void 0;
|
|
2817
|
+
try {
|
|
2818
|
+
const headers = { Accept: "application/json, */*;q=0.5" };
|
|
2819
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
2820
|
+
const res = await fetch(url, { redirect: "follow", signal: controller.signal, headers });
|
|
2821
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`);
|
|
2822
|
+
const content = await res.text();
|
|
2823
|
+
return JSON.parse(content);
|
|
2824
|
+
} catch (e) {
|
|
2825
|
+
if (e?.name === "AbortError") {
|
|
2826
|
+
throw new Error(
|
|
2827
|
+
`fetch timed out after ${timeoutMs}ms \u2014 set artifactSource.fetchTimeoutMs or OS_ARTIFACT_FETCH_TIMEOUT_MS to extend it (0 disables)`
|
|
2828
|
+
);
|
|
2829
|
+
}
|
|
2830
|
+
throw e;
|
|
2831
|
+
} finally {
|
|
2832
|
+
if (timer) clearTimeout(timer);
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
/**
|
|
2836
|
+
* Parse raw artifact JSON (envelope or bare definition) and register all
|
|
2837
|
+
* metadata items into the MetadataManager.
|
|
2838
|
+
*/
|
|
2839
|
+
async _parseAndRegisterArtifact(ctx, raw, label) {
|
|
2840
|
+
const { ProjectArtifactSchema } = await import("@objectstack/spec/cloud");
|
|
2841
|
+
const { ObjectStackDefinitionSchema } = await import("@objectstack/spec");
|
|
2842
|
+
let metadata;
|
|
2843
|
+
const obj = raw;
|
|
2844
|
+
if (obj?.schemaVersion && obj?.commitId && obj?.metadata !== void 0) {
|
|
2845
|
+
const artifact = ProjectArtifactSchema.parse(obj);
|
|
2846
|
+
metadata = artifact.metadata;
|
|
2847
|
+
} else if (obj?.success && obj?.data?.metadata) {
|
|
2848
|
+
const artifact = ProjectArtifactSchema.parse(obj.data);
|
|
2849
|
+
metadata = artifact.metadata;
|
|
2850
|
+
} else {
|
|
2851
|
+
const def = ObjectStackDefinitionSchema.parse(obj);
|
|
2852
|
+
const canonical = JSON.stringify(def, Object.keys(def).sort());
|
|
2853
|
+
const checksum = createHash2("sha256").update(canonical).digest("hex");
|
|
2854
|
+
const projectId = this.options.projectId ?? "proj_local";
|
|
2855
|
+
ProjectArtifactSchema.parse({
|
|
2856
|
+
schemaVersion: "0.1",
|
|
2857
|
+
projectId,
|
|
2858
|
+
commitId: "local-dev",
|
|
2859
|
+
checksum,
|
|
2860
|
+
metadata: def
|
|
2861
|
+
});
|
|
2862
|
+
metadata = def;
|
|
2863
|
+
}
|
|
2864
|
+
const memLoader = new MemoryLoader();
|
|
2865
|
+
const manifestPackageId = metadata?.manifest?.id ?? metadata?.id ?? void 0;
|
|
2866
|
+
let totalRegistered = 0;
|
|
2867
|
+
for (const [field, metaType] of Object.entries(ARTIFACT_FIELD_TO_TYPE)) {
|
|
2868
|
+
const items = metadata[field];
|
|
2869
|
+
if (!Array.isArray(items) || items.length === 0) continue;
|
|
2870
|
+
for (const item of items) {
|
|
2871
|
+
const name = item?.name;
|
|
2872
|
+
if (!name) continue;
|
|
2873
|
+
if (manifestPackageId && item._packageId === void 0) {
|
|
2874
|
+
item._packageId = manifestPackageId;
|
|
2875
|
+
}
|
|
2876
|
+
await memLoader.save(metaType, name, item);
|
|
2877
|
+
await this.manager.register(metaType, name, item);
|
|
2878
|
+
totalRegistered++;
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
this.manager.registerLoader(memLoader);
|
|
2882
|
+
ctx.logger.info("[MetadataPlugin] Artifact metadata loaded", { source: label, totalRegistered });
|
|
2883
|
+
return totalRegistered;
|
|
2884
|
+
}
|
|
2885
|
+
async _loadFromLocalFile(ctx, filePath, fetchTimeoutMs) {
|
|
2886
|
+
const isUrl = /^https?:\/\//i.test(filePath);
|
|
2887
|
+
ctx.logger.info(
|
|
2888
|
+
`[MetadataPlugin] Loading metadata from ${isUrl ? "remote URL" : "local artifact file"}`,
|
|
2889
|
+
{ path: filePath }
|
|
2890
|
+
);
|
|
2891
|
+
let raw;
|
|
2892
|
+
try {
|
|
2893
|
+
if (isUrl) {
|
|
2894
|
+
raw = await this._fetchJson(filePath, fetchTimeoutMs);
|
|
2895
|
+
} else {
|
|
2896
|
+
const content = await readFile2(filePath, "utf8");
|
|
2897
|
+
raw = JSON.parse(content);
|
|
2898
|
+
}
|
|
2899
|
+
} catch (e) {
|
|
2900
|
+
throw new Error(`[MetadataPlugin] Cannot read artifact ${isUrl ? "URL" : "file"} at "${filePath}": ${e.message}`);
|
|
2901
|
+
}
|
|
2902
|
+
await this._parseAndRegisterArtifact(ctx, raw, filePath);
|
|
2903
|
+
}
|
|
2904
|
+
/**
|
|
2905
|
+
* P2: Load metadata from the cloud artifact API endpoint.
|
|
2906
|
+
*/
|
|
2907
|
+
async _loadFromArtifactApi(ctx, src) {
|
|
2908
|
+
const projectId = this.options.projectId;
|
|
2909
|
+
if (!projectId) {
|
|
2910
|
+
throw new Error("[MetadataPlugin] artifact-api source requires options.projectId to be set");
|
|
2911
|
+
}
|
|
2912
|
+
let artifactUrl = src.url.replace(/\/+$/, "");
|
|
2913
|
+
if (!/\/api\/v\d+\/cloud\/projects\//i.test(artifactUrl)) {
|
|
2914
|
+
artifactUrl = `${artifactUrl}/api/v1/cloud/projects/${projectId}/artifact`;
|
|
2915
|
+
}
|
|
2916
|
+
if (src.commitId) {
|
|
2917
|
+
artifactUrl += `${artifactUrl.includes("?") ? "&" : "?"}commit=${encodeURIComponent(src.commitId)}`;
|
|
2918
|
+
}
|
|
2919
|
+
ctx.logger.info("[MetadataPlugin] Loading metadata from artifact API", { url: artifactUrl });
|
|
2920
|
+
let raw;
|
|
2921
|
+
try {
|
|
2922
|
+
raw = await this._fetchJson(artifactUrl, src.fetchTimeoutMs, src.token);
|
|
2923
|
+
} catch (e) {
|
|
2924
|
+
throw new Error(`[MetadataPlugin] Cannot load artifact from API "${artifactUrl}": ${e.message}`);
|
|
2925
|
+
}
|
|
2926
|
+
await this._parseAndRegisterArtifact(ctx, raw, artifactUrl);
|
|
2927
|
+
}
|
|
2928
|
+
async _loadFromFileSystem(ctx) {
|
|
2929
|
+
ctx.logger.info("Loading metadata from file system...");
|
|
2930
|
+
const sortedTypes = [...DEFAULT_METADATA_TYPE_REGISTRY].sort((a, b) => a.loadOrder - b.loadOrder);
|
|
2931
|
+
let totalLoaded = 0;
|
|
2932
|
+
for (const entry of sortedTypes) {
|
|
2933
|
+
try {
|
|
2934
|
+
const items = await this.manager.loadMany(entry.type, {
|
|
2935
|
+
recursive: true,
|
|
2936
|
+
patterns: entry.filePatterns
|
|
2937
|
+
});
|
|
2938
|
+
if (items.length > 0) {
|
|
2939
|
+
for (const item of items) {
|
|
2940
|
+
const meta = item;
|
|
2941
|
+
if (meta?.name) {
|
|
2942
|
+
await this.manager.register(entry.type, meta.name, item);
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
ctx.logger.info(`Loaded ${items.length} ${entry.type} from file system`);
|
|
2946
|
+
totalLoaded += items.length;
|
|
2947
|
+
}
|
|
2948
|
+
} catch (e) {
|
|
2949
|
+
ctx.logger.debug(`No ${entry.type} metadata found`, { error: e.message });
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
ctx.logger.info("Metadata loading complete", {
|
|
2953
|
+
totalItems: totalLoaded,
|
|
2954
|
+
registeredTypes: sortedTypes.length
|
|
2955
|
+
});
|
|
2956
|
+
}
|
|
2957
|
+
};
|
|
2958
|
+
|
|
2703
2959
|
// src/loaders/remote-loader.ts
|
|
2704
2960
|
var RemoteLoader = class {
|
|
2705
2961
|
constructor(baseUrl, authToken) {
|
|
@@ -2797,6 +3053,9 @@ var RemoteLoader = class {
|
|
|
2797
3053
|
}
|
|
2798
3054
|
};
|
|
2799
3055
|
|
|
3056
|
+
// src/index.ts
|
|
3057
|
+
import { SysMetadataObject as SysMetadataObject3, SysMetadataHistoryObject as SysMetadataHistoryObject3 } from "@objectstack/platform-objects/metadata";
|
|
3058
|
+
|
|
2800
3059
|
// src/routes/history-routes.ts
|
|
2801
3060
|
function registerMetadataHistoryRoutes(app, metadataService) {
|
|
2802
3061
|
app.get("/api/v1/metadata/:type/:name/history", async (c) => {
|
|
@@ -2952,7 +3211,8 @@ var HistoryCleanupManager = class {
|
|
|
2952
3211
|
async runCleanup() {
|
|
2953
3212
|
const driver = this.dbLoader.driver;
|
|
2954
3213
|
const historyTableName = this.dbLoader.historyTableName;
|
|
2955
|
-
const
|
|
3214
|
+
const organizationId = this.dbLoader.organizationId;
|
|
3215
|
+
const projectId = this.dbLoader.projectId;
|
|
2956
3216
|
let deleted = 0;
|
|
2957
3217
|
let errors = 0;
|
|
2958
3218
|
try {
|
|
@@ -2963,8 +3223,11 @@ var HistoryCleanupManager = class {
|
|
|
2963
3223
|
const filter = {
|
|
2964
3224
|
recorded_at: { $lt: cutoffISO }
|
|
2965
3225
|
};
|
|
2966
|
-
if (
|
|
2967
|
-
filter.
|
|
3226
|
+
if (organizationId) {
|
|
3227
|
+
filter.organization_id = organizationId;
|
|
3228
|
+
}
|
|
3229
|
+
if (projectId !== void 0) {
|
|
3230
|
+
filter.project_id = projectId;
|
|
2968
3231
|
}
|
|
2969
3232
|
try {
|
|
2970
3233
|
const result = await this.bulkDeleteByFilter(driver, historyTableName, filter);
|
|
@@ -2976,9 +3239,12 @@ var HistoryCleanupManager = class {
|
|
|
2976
3239
|
}
|
|
2977
3240
|
if (this.policy.maxVersions) {
|
|
2978
3241
|
try {
|
|
3242
|
+
const baseWhere = {};
|
|
3243
|
+
if (organizationId) baseWhere.organization_id = organizationId;
|
|
3244
|
+
if (projectId !== void 0) baseWhere.project_id = projectId;
|
|
2979
3245
|
const metadataIds = await driver.find(historyTableName, {
|
|
2980
3246
|
object: historyTableName,
|
|
2981
|
-
where:
|
|
3247
|
+
where: baseWhere,
|
|
2982
3248
|
fields: ["metadata_id"]
|
|
2983
3249
|
});
|
|
2984
3250
|
const uniqueIds = /* @__PURE__ */ new Set();
|
|
@@ -2988,10 +3254,7 @@ var HistoryCleanupManager = class {
|
|
|
2988
3254
|
}
|
|
2989
3255
|
}
|
|
2990
3256
|
for (const metadataId of uniqueIds) {
|
|
2991
|
-
const filter = { metadata_id: metadataId };
|
|
2992
|
-
if (tenantId) {
|
|
2993
|
-
filter.tenant_id = tenantId;
|
|
2994
|
-
}
|
|
3257
|
+
const filter = { metadata_id: metadataId, ...baseWhere };
|
|
2995
3258
|
try {
|
|
2996
3259
|
const historyRecords = await driver.find(historyTableName, {
|
|
2997
3260
|
object: historyTableName,
|
|
@@ -3065,20 +3328,22 @@ var HistoryCleanupManager = class {
|
|
|
3065
3328
|
async getCleanupStats() {
|
|
3066
3329
|
const driver = this.dbLoader.driver;
|
|
3067
3330
|
const historyTableName = this.dbLoader.historyTableName;
|
|
3068
|
-
const
|
|
3331
|
+
const organizationId = this.dbLoader.organizationId;
|
|
3332
|
+
const projectId = this.dbLoader.projectId;
|
|
3069
3333
|
let recordsByAge = 0;
|
|
3070
3334
|
let recordsByCount = 0;
|
|
3071
3335
|
try {
|
|
3336
|
+
const baseWhere = {};
|
|
3337
|
+
if (organizationId) baseWhere.organization_id = organizationId;
|
|
3338
|
+
if (projectId !== void 0) baseWhere.project_id = projectId;
|
|
3072
3339
|
if (this.policy.maxAgeDays) {
|
|
3073
3340
|
const cutoffDate = /* @__PURE__ */ new Date();
|
|
3074
3341
|
cutoffDate.setDate(cutoffDate.getDate() - this.policy.maxAgeDays);
|
|
3075
3342
|
const cutoffISO = cutoffDate.toISOString();
|
|
3076
3343
|
const filter = {
|
|
3077
|
-
recorded_at: { $lt: cutoffISO }
|
|
3344
|
+
recorded_at: { $lt: cutoffISO },
|
|
3345
|
+
...baseWhere
|
|
3078
3346
|
};
|
|
3079
|
-
if (tenantId) {
|
|
3080
|
-
filter.tenant_id = tenantId;
|
|
3081
|
-
}
|
|
3082
3347
|
recordsByAge = await driver.count(historyTableName, {
|
|
3083
3348
|
object: historyTableName,
|
|
3084
3349
|
where: filter
|
|
@@ -3087,7 +3352,7 @@ var HistoryCleanupManager = class {
|
|
|
3087
3352
|
if (this.policy.maxVersions) {
|
|
3088
3353
|
const metadataIds = await driver.find(historyTableName, {
|
|
3089
3354
|
object: historyTableName,
|
|
3090
|
-
where:
|
|
3355
|
+
where: baseWhere,
|
|
3091
3356
|
fields: ["metadata_id"]
|
|
3092
3357
|
});
|
|
3093
3358
|
const uniqueIds = /* @__PURE__ */ new Set();
|
|
@@ -3097,10 +3362,7 @@ var HistoryCleanupManager = class {
|
|
|
3097
3362
|
}
|
|
3098
3363
|
}
|
|
3099
3364
|
for (const metadataId of uniqueIds) {
|
|
3100
|
-
const filter = { metadata_id: metadataId };
|
|
3101
|
-
if (tenantId) {
|
|
3102
|
-
filter.tenant_id = tenantId;
|
|
3103
|
-
}
|
|
3365
|
+
const filter = { metadata_id: metadataId, ...baseWhere };
|
|
3104
3366
|
const count = await driver.count(historyTableName, {
|
|
3105
3367
|
object: historyTableName,
|
|
3106
3368
|
where: filter
|
|
@@ -3187,8 +3449,8 @@ export {
|
|
|
3187
3449
|
migration_exports as Migration,
|
|
3188
3450
|
NodeMetadataManager,
|
|
3189
3451
|
RemoteLoader,
|
|
3190
|
-
SysMetadataHistoryObject,
|
|
3191
|
-
SysMetadataObject,
|
|
3452
|
+
SysMetadataHistoryObject3 as SysMetadataHistoryObject,
|
|
3453
|
+
SysMetadataObject3 as SysMetadataObject,
|
|
3192
3454
|
TypeScriptSerializer,
|
|
3193
3455
|
YAMLSerializer,
|
|
3194
3456
|
calculateChecksum,
|