@objectstack/metadata 3.3.0 → 3.3.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/dist/index.cjs +2197 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +42 -82
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +2201 -0
- package/dist/node.cjs.map +1 -0
- package/dist/node.d.cts +65 -0
- package/dist/node.d.ts +65 -0
- package/dist/{index.mjs → node.js} +3 -1
- package/package.json +18 -13
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -504
- package/ROADMAP.md +0 -224
- package/src/index.ts +0 -68
- package/src/loaders/database-loader.test.ts +0 -559
- package/src/loaders/database-loader.ts +0 -352
- package/src/loaders/filesystem-loader.ts +0 -420
- package/src/loaders/loader-interface.ts +0 -89
- package/src/loaders/memory-loader.ts +0 -103
- package/src/loaders/remote-loader.ts +0 -140
- package/src/metadata-manager.ts +0 -1168
- package/src/metadata-service.test.ts +0 -965
- package/src/metadata.test.ts +0 -431
- package/src/migration/executor.ts +0 -54
- package/src/migration/index.ts +0 -3
- package/src/node-metadata-manager.ts +0 -126
- package/src/node.ts +0 -11
- package/src/objects/sys-metadata.object.ts +0 -188
- package/src/plugin.ts +0 -102
- package/src/serializers/json-serializer.ts +0 -73
- package/src/serializers/serializer-interface.ts +0 -65
- package/src/serializers/serializers.test.ts +0 -74
- package/src/serializers/typescript-serializer.ts +0 -127
- package/src/serializers/yaml-serializer.ts +0 -49
- package/tsconfig.json +0 -9
- package/vitest.config.ts +0 -23
- /package/dist/{index.d.mts → index.d.cts} +0 -0
- /package/dist/{index.mjs.map → node.js.map} +0 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
DatabaseLoader: () => DatabaseLoader,
|
|
34
|
+
JSONSerializer: () => JSONSerializer,
|
|
35
|
+
MemoryLoader: () => MemoryLoader,
|
|
36
|
+
MetadataManager: () => MetadataManager,
|
|
37
|
+
MetadataPlugin: () => MetadataPlugin,
|
|
38
|
+
Migration: () => migration_exports,
|
|
39
|
+
RemoteLoader: () => RemoteLoader,
|
|
40
|
+
SysMetadataObject: () => SysMetadataObject,
|
|
41
|
+
TypeScriptSerializer: () => TypeScriptSerializer,
|
|
42
|
+
YAMLSerializer: () => YAMLSerializer
|
|
43
|
+
});
|
|
44
|
+
module.exports = __toCommonJS(index_exports);
|
|
45
|
+
|
|
46
|
+
// src/metadata-manager.ts
|
|
47
|
+
var import_core = require("@objectstack/core");
|
|
48
|
+
|
|
49
|
+
// src/serializers/json-serializer.ts
|
|
50
|
+
var JSONSerializer = class {
|
|
51
|
+
serialize(item, options) {
|
|
52
|
+
const { prettify = true, indent = 2, sortKeys = false } = options || {};
|
|
53
|
+
if (sortKeys) {
|
|
54
|
+
const sorted = this.sortObjectKeys(item);
|
|
55
|
+
return prettify ? JSON.stringify(sorted, null, indent) : JSON.stringify(sorted);
|
|
56
|
+
}
|
|
57
|
+
return prettify ? JSON.stringify(item, null, indent) : JSON.stringify(item);
|
|
58
|
+
}
|
|
59
|
+
deserialize(content, schema) {
|
|
60
|
+
const parsed = JSON.parse(content);
|
|
61
|
+
if (schema) {
|
|
62
|
+
return schema.parse(parsed);
|
|
63
|
+
}
|
|
64
|
+
return parsed;
|
|
65
|
+
}
|
|
66
|
+
getExtension() {
|
|
67
|
+
return ".json";
|
|
68
|
+
}
|
|
69
|
+
canHandle(format) {
|
|
70
|
+
return format === "json";
|
|
71
|
+
}
|
|
72
|
+
getFormat() {
|
|
73
|
+
return "json";
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Recursively sort object keys
|
|
77
|
+
*/
|
|
78
|
+
sortObjectKeys(obj) {
|
|
79
|
+
if (obj === null || typeof obj !== "object") {
|
|
80
|
+
return obj;
|
|
81
|
+
}
|
|
82
|
+
if (Array.isArray(obj)) {
|
|
83
|
+
return obj.map((item) => this.sortObjectKeys(item));
|
|
84
|
+
}
|
|
85
|
+
const sorted = {};
|
|
86
|
+
const keys = Object.keys(obj).sort();
|
|
87
|
+
for (const key of keys) {
|
|
88
|
+
sorted[key] = this.sortObjectKeys(obj[key]);
|
|
89
|
+
}
|
|
90
|
+
return sorted;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// src/serializers/yaml-serializer.ts
|
|
95
|
+
var yaml = __toESM(require("js-yaml"), 1);
|
|
96
|
+
var YAMLSerializer = class {
|
|
97
|
+
serialize(item, options) {
|
|
98
|
+
const { indent = 2, sortKeys = false } = options || {};
|
|
99
|
+
return yaml.dump(item, {
|
|
100
|
+
indent,
|
|
101
|
+
sortKeys,
|
|
102
|
+
lineWidth: -1,
|
|
103
|
+
// Disable line wrapping
|
|
104
|
+
noRefs: true
|
|
105
|
+
// Disable YAML references
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
deserialize(content, schema) {
|
|
109
|
+
const parsed = yaml.load(content, { schema: yaml.JSON_SCHEMA });
|
|
110
|
+
if (schema) {
|
|
111
|
+
return schema.parse(parsed);
|
|
112
|
+
}
|
|
113
|
+
return parsed;
|
|
114
|
+
}
|
|
115
|
+
getExtension() {
|
|
116
|
+
return ".yaml";
|
|
117
|
+
}
|
|
118
|
+
canHandle(format) {
|
|
119
|
+
return format === "yaml";
|
|
120
|
+
}
|
|
121
|
+
getFormat() {
|
|
122
|
+
return "yaml";
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// src/serializers/typescript-serializer.ts
|
|
127
|
+
var TypeScriptSerializer = class {
|
|
128
|
+
constructor(format = "typescript") {
|
|
129
|
+
this.format = format;
|
|
130
|
+
}
|
|
131
|
+
serialize(item, options) {
|
|
132
|
+
const { prettify = true, indent = 2 } = options || {};
|
|
133
|
+
const jsonStr = JSON.stringify(item, null, prettify ? indent : 0);
|
|
134
|
+
if (this.format === "typescript") {
|
|
135
|
+
return `import type { ServiceObject } from '@objectstack/spec/data';
|
|
136
|
+
|
|
137
|
+
export const metadata: ServiceObject = ${jsonStr};
|
|
138
|
+
|
|
139
|
+
export default metadata;
|
|
140
|
+
`;
|
|
141
|
+
} else {
|
|
142
|
+
return `export const metadata = ${jsonStr};
|
|
143
|
+
|
|
144
|
+
export default metadata;
|
|
145
|
+
`;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
deserialize(content, schema) {
|
|
149
|
+
let objectStart = content.indexOf("export const");
|
|
150
|
+
if (objectStart === -1) {
|
|
151
|
+
objectStart = content.indexOf("export default");
|
|
152
|
+
}
|
|
153
|
+
if (objectStart === -1) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
'Could not parse TypeScript/JavaScript module. Expected export pattern: "export const metadata = {...};" or "export default {...};"'
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
const braceStart = content.indexOf("{", objectStart);
|
|
159
|
+
if (braceStart === -1) {
|
|
160
|
+
throw new Error("Could not find object literal in export statement");
|
|
161
|
+
}
|
|
162
|
+
let braceCount = 0;
|
|
163
|
+
let braceEnd = -1;
|
|
164
|
+
let inString = false;
|
|
165
|
+
let stringChar = "";
|
|
166
|
+
for (let i = braceStart; i < content.length; i++) {
|
|
167
|
+
const char = content[i];
|
|
168
|
+
const prevChar = i > 0 ? content[i - 1] : "";
|
|
169
|
+
if ((char === '"' || char === "'") && prevChar !== "\\") {
|
|
170
|
+
if (!inString) {
|
|
171
|
+
inString = true;
|
|
172
|
+
stringChar = char;
|
|
173
|
+
} else if (char === stringChar) {
|
|
174
|
+
inString = false;
|
|
175
|
+
stringChar = "";
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!inString) {
|
|
179
|
+
if (char === "{") braceCount++;
|
|
180
|
+
if (char === "}") {
|
|
181
|
+
braceCount--;
|
|
182
|
+
if (braceCount === 0) {
|
|
183
|
+
braceEnd = i;
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (braceEnd === -1) {
|
|
190
|
+
throw new Error("Could not find matching closing brace for object literal");
|
|
191
|
+
}
|
|
192
|
+
const objectLiteral = content.substring(braceStart, braceEnd + 1);
|
|
193
|
+
try {
|
|
194
|
+
const parsed = JSON.parse(objectLiteral);
|
|
195
|
+
if (schema) {
|
|
196
|
+
return schema.parse(parsed);
|
|
197
|
+
}
|
|
198
|
+
return parsed;
|
|
199
|
+
} catch (error) {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Failed to parse object literal as JSON: ${error instanceof Error ? error.message : String(error)}. Make sure the TypeScript/JavaScript object uses JSON-compatible syntax (no functions, comments, or trailing commas).`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
getExtension() {
|
|
206
|
+
return this.format === "typescript" ? ".ts" : ".js";
|
|
207
|
+
}
|
|
208
|
+
canHandle(format) {
|
|
209
|
+
return format === "typescript" || format === "javascript";
|
|
210
|
+
}
|
|
211
|
+
getFormat() {
|
|
212
|
+
return this.format;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// src/objects/sys-metadata.object.ts
|
|
217
|
+
var import_data = require("@objectstack/spec/data");
|
|
218
|
+
var SysMetadataObject = import_data.ObjectSchema.create({
|
|
219
|
+
namespace: "sys",
|
|
220
|
+
name: "metadata",
|
|
221
|
+
label: "System Metadata",
|
|
222
|
+
pluralLabel: "System Metadata",
|
|
223
|
+
icon: "settings",
|
|
224
|
+
isSystem: true,
|
|
225
|
+
description: "Stores platform and user-scope metadata records (objects, views, flows, etc.)",
|
|
226
|
+
fields: {
|
|
227
|
+
/** Primary Key (UUID) */
|
|
228
|
+
id: import_data.Field.text({
|
|
229
|
+
label: "ID",
|
|
230
|
+
required: true,
|
|
231
|
+
readonly: true
|
|
232
|
+
}),
|
|
233
|
+
/** Machine name — unique identifier used in code references */
|
|
234
|
+
name: import_data.Field.text({
|
|
235
|
+
label: "Name",
|
|
236
|
+
required: true,
|
|
237
|
+
searchable: true,
|
|
238
|
+
maxLength: 255
|
|
239
|
+
}),
|
|
240
|
+
/** Metadata type (e.g. "object", "view", "flow") */
|
|
241
|
+
type: import_data.Field.text({
|
|
242
|
+
label: "Metadata Type",
|
|
243
|
+
required: true,
|
|
244
|
+
searchable: true,
|
|
245
|
+
maxLength: 100
|
|
246
|
+
}),
|
|
247
|
+
/** Namespace / module grouping (e.g. "crm", "core") */
|
|
248
|
+
namespace: import_data.Field.text({
|
|
249
|
+
label: "Namespace",
|
|
250
|
+
required: false,
|
|
251
|
+
defaultValue: "default",
|
|
252
|
+
maxLength: 100
|
|
253
|
+
}),
|
|
254
|
+
/** Package that owns/delivered this metadata */
|
|
255
|
+
package_id: import_data.Field.text({
|
|
256
|
+
label: "Package ID",
|
|
257
|
+
required: false,
|
|
258
|
+
maxLength: 255
|
|
259
|
+
}),
|
|
260
|
+
/** Who manages this record: package, platform, or user */
|
|
261
|
+
managed_by: import_data.Field.select(["package", "platform", "user"], {
|
|
262
|
+
label: "Managed By",
|
|
263
|
+
required: false
|
|
264
|
+
}),
|
|
265
|
+
/** Scope: system (code), platform (admin DB), user (personal DB) */
|
|
266
|
+
scope: import_data.Field.select(["system", "platform", "user"], {
|
|
267
|
+
label: "Scope",
|
|
268
|
+
required: true,
|
|
269
|
+
defaultValue: "platform"
|
|
270
|
+
}),
|
|
271
|
+
/** JSON payload — the actual metadata configuration */
|
|
272
|
+
metadata: import_data.Field.textarea({
|
|
273
|
+
label: "Metadata",
|
|
274
|
+
required: true,
|
|
275
|
+
description: "JSON-serialized metadata payload"
|
|
276
|
+
}),
|
|
277
|
+
/** Parent metadata name for extension/override */
|
|
278
|
+
extends: import_data.Field.text({
|
|
279
|
+
label: "Extends",
|
|
280
|
+
required: false,
|
|
281
|
+
maxLength: 255
|
|
282
|
+
}),
|
|
283
|
+
/** Merge strategy when extending parent metadata */
|
|
284
|
+
strategy: import_data.Field.select(["merge", "replace"], {
|
|
285
|
+
label: "Strategy",
|
|
286
|
+
required: false,
|
|
287
|
+
defaultValue: "merge"
|
|
288
|
+
}),
|
|
289
|
+
/** Owner user ID (for user-scope items) */
|
|
290
|
+
owner: import_data.Field.text({
|
|
291
|
+
label: "Owner",
|
|
292
|
+
required: false,
|
|
293
|
+
maxLength: 255
|
|
294
|
+
}),
|
|
295
|
+
/** Lifecycle state */
|
|
296
|
+
state: import_data.Field.select(["draft", "active", "archived", "deprecated"], {
|
|
297
|
+
label: "State",
|
|
298
|
+
required: false,
|
|
299
|
+
defaultValue: "active"
|
|
300
|
+
}),
|
|
301
|
+
/** Tenant ID for multi-tenant isolation */
|
|
302
|
+
tenant_id: import_data.Field.text({
|
|
303
|
+
label: "Tenant ID",
|
|
304
|
+
required: false,
|
|
305
|
+
maxLength: 255
|
|
306
|
+
}),
|
|
307
|
+
/** Version number for optimistic concurrency */
|
|
308
|
+
version: import_data.Field.number({
|
|
309
|
+
label: "Version",
|
|
310
|
+
required: false,
|
|
311
|
+
defaultValue: 1
|
|
312
|
+
}),
|
|
313
|
+
/** Content checksum for change detection */
|
|
314
|
+
checksum: import_data.Field.text({
|
|
315
|
+
label: "Checksum",
|
|
316
|
+
required: false,
|
|
317
|
+
maxLength: 64
|
|
318
|
+
}),
|
|
319
|
+
/** Origin of this metadata record */
|
|
320
|
+
source: import_data.Field.select(["filesystem", "database", "api", "migration"], {
|
|
321
|
+
label: "Source",
|
|
322
|
+
required: false
|
|
323
|
+
}),
|
|
324
|
+
/** Classification tags (JSON array) */
|
|
325
|
+
tags: import_data.Field.textarea({
|
|
326
|
+
label: "Tags",
|
|
327
|
+
required: false,
|
|
328
|
+
description: "JSON-serialized array of classification tags"
|
|
329
|
+
}),
|
|
330
|
+
/** Audit fields */
|
|
331
|
+
created_by: import_data.Field.text({
|
|
332
|
+
label: "Created By",
|
|
333
|
+
required: false,
|
|
334
|
+
readonly: true,
|
|
335
|
+
maxLength: 255
|
|
336
|
+
}),
|
|
337
|
+
created_at: import_data.Field.datetime({
|
|
338
|
+
label: "Created At",
|
|
339
|
+
required: false,
|
|
340
|
+
readonly: true
|
|
341
|
+
}),
|
|
342
|
+
updated_by: import_data.Field.text({
|
|
343
|
+
label: "Updated By",
|
|
344
|
+
required: false,
|
|
345
|
+
maxLength: 255
|
|
346
|
+
}),
|
|
347
|
+
updated_at: import_data.Field.datetime({
|
|
348
|
+
label: "Updated At",
|
|
349
|
+
required: false
|
|
350
|
+
})
|
|
351
|
+
},
|
|
352
|
+
indexes: [
|
|
353
|
+
{ fields: ["type", "name"], unique: true },
|
|
354
|
+
{ fields: ["type", "scope"] },
|
|
355
|
+
{ fields: ["tenant_id"] },
|
|
356
|
+
{ fields: ["state"] },
|
|
357
|
+
{ fields: ["namespace"] }
|
|
358
|
+
],
|
|
359
|
+
enable: {
|
|
360
|
+
trackHistory: true,
|
|
361
|
+
searchable: false,
|
|
362
|
+
apiEnabled: true,
|
|
363
|
+
apiMethods: ["get", "list", "create", "update", "delete"],
|
|
364
|
+
trash: false
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// src/loaders/database-loader.ts
|
|
369
|
+
var DatabaseLoader = class {
|
|
370
|
+
constructor(options) {
|
|
371
|
+
this.contract = {
|
|
372
|
+
name: "database",
|
|
373
|
+
protocol: "datasource:",
|
|
374
|
+
capabilities: {
|
|
375
|
+
read: true,
|
|
376
|
+
write: true,
|
|
377
|
+
watch: false,
|
|
378
|
+
list: true
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
this.schemaReady = false;
|
|
382
|
+
this.driver = options.driver;
|
|
383
|
+
this.tableName = options.tableName ?? "sys_metadata";
|
|
384
|
+
this.tenantId = options.tenantId;
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Ensure the metadata table exists.
|
|
388
|
+
* Uses IDataDriver.syncSchema with the SysMetadataObject definition
|
|
389
|
+
* to idempotently create/update the table.
|
|
390
|
+
*/
|
|
391
|
+
async ensureSchema() {
|
|
392
|
+
if (this.schemaReady) return;
|
|
393
|
+
try {
|
|
394
|
+
await this.driver.syncSchema(this.tableName, {
|
|
395
|
+
...SysMetadataObject,
|
|
396
|
+
name: this.tableName
|
|
397
|
+
});
|
|
398
|
+
this.schemaReady = true;
|
|
399
|
+
} catch {
|
|
400
|
+
this.schemaReady = true;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Build base filter conditions for queries.
|
|
405
|
+
* Always includes tenantId when configured.
|
|
406
|
+
*/
|
|
407
|
+
baseFilter(type, name) {
|
|
408
|
+
const filter = { type };
|
|
409
|
+
if (name !== void 0) {
|
|
410
|
+
filter.name = name;
|
|
411
|
+
}
|
|
412
|
+
if (this.tenantId) {
|
|
413
|
+
filter.tenant_id = this.tenantId;
|
|
414
|
+
}
|
|
415
|
+
return filter;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Convert a database row to a metadata payload.
|
|
419
|
+
* Parses the JSON `metadata` column back into an object.
|
|
420
|
+
*/
|
|
421
|
+
rowToData(row) {
|
|
422
|
+
if (!row || !row.metadata) return null;
|
|
423
|
+
const payload = typeof row.metadata === "string" ? JSON.parse(row.metadata) : row.metadata;
|
|
424
|
+
return payload;
|
|
425
|
+
}
|
|
426
|
+
/**
|
|
427
|
+
* Convert a database row to a MetadataRecord-like object.
|
|
428
|
+
*/
|
|
429
|
+
rowToRecord(row) {
|
|
430
|
+
return {
|
|
431
|
+
id: row.id,
|
|
432
|
+
name: row.name,
|
|
433
|
+
type: row.type,
|
|
434
|
+
namespace: row.namespace ?? "default",
|
|
435
|
+
packageId: row.package_id,
|
|
436
|
+
managedBy: row.managed_by,
|
|
437
|
+
scope: row.scope ?? "platform",
|
|
438
|
+
metadata: this.rowToData(row) ?? {},
|
|
439
|
+
extends: row.extends,
|
|
440
|
+
strategy: row.strategy ?? "merge",
|
|
441
|
+
owner: row.owner,
|
|
442
|
+
state: row.state ?? "active",
|
|
443
|
+
tenantId: row.tenant_id,
|
|
444
|
+
version: row.version ?? 1,
|
|
445
|
+
checksum: row.checksum,
|
|
446
|
+
source: row.source,
|
|
447
|
+
tags: row.tags ? typeof row.tags === "string" ? JSON.parse(row.tags) : row.tags : void 0,
|
|
448
|
+
createdBy: row.created_by,
|
|
449
|
+
createdAt: row.created_at,
|
|
450
|
+
updatedBy: row.updated_by,
|
|
451
|
+
updatedAt: row.updated_at
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
// ==========================================
|
|
455
|
+
// MetadataLoader Interface Implementation
|
|
456
|
+
// ==========================================
|
|
457
|
+
async load(type, name, _options) {
|
|
458
|
+
const startTime = Date.now();
|
|
459
|
+
await this.ensureSchema();
|
|
460
|
+
try {
|
|
461
|
+
const row = await this.driver.findOne(this.tableName, {
|
|
462
|
+
object: this.tableName,
|
|
463
|
+
where: this.baseFilter(type, name)
|
|
464
|
+
});
|
|
465
|
+
if (!row) {
|
|
466
|
+
return {
|
|
467
|
+
data: null,
|
|
468
|
+
loadTime: Date.now() - startTime
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
const data = this.rowToData(row);
|
|
472
|
+
const record = this.rowToRecord(row);
|
|
473
|
+
return {
|
|
474
|
+
data,
|
|
475
|
+
source: "database",
|
|
476
|
+
format: "json",
|
|
477
|
+
etag: record.checksum,
|
|
478
|
+
loadTime: Date.now() - startTime
|
|
479
|
+
};
|
|
480
|
+
} catch {
|
|
481
|
+
return {
|
|
482
|
+
data: null,
|
|
483
|
+
loadTime: Date.now() - startTime
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
async loadMany(type, _options) {
|
|
488
|
+
await this.ensureSchema();
|
|
489
|
+
try {
|
|
490
|
+
const rows = await this.driver.find(this.tableName, {
|
|
491
|
+
object: this.tableName,
|
|
492
|
+
where: this.baseFilter(type)
|
|
493
|
+
});
|
|
494
|
+
return rows.map((row) => this.rowToData(row)).filter((data) => data !== null);
|
|
495
|
+
} catch {
|
|
496
|
+
return [];
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
async exists(type, name) {
|
|
500
|
+
await this.ensureSchema();
|
|
501
|
+
try {
|
|
502
|
+
const count = await this.driver.count(this.tableName, {
|
|
503
|
+
object: this.tableName,
|
|
504
|
+
where: this.baseFilter(type, name)
|
|
505
|
+
});
|
|
506
|
+
return count > 0;
|
|
507
|
+
} catch {
|
|
508
|
+
return false;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
async stat(type, name) {
|
|
512
|
+
await this.ensureSchema();
|
|
513
|
+
try {
|
|
514
|
+
const row = await this.driver.findOne(this.tableName, {
|
|
515
|
+
object: this.tableName,
|
|
516
|
+
where: this.baseFilter(type, name)
|
|
517
|
+
});
|
|
518
|
+
if (!row) return null;
|
|
519
|
+
const record = this.rowToRecord(row);
|
|
520
|
+
const metadataStr = typeof row.metadata === "string" ? row.metadata : JSON.stringify(row.metadata);
|
|
521
|
+
return {
|
|
522
|
+
size: metadataStr.length,
|
|
523
|
+
mtime: record.updatedAt ?? record.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
524
|
+
format: "json",
|
|
525
|
+
etag: record.checksum
|
|
526
|
+
};
|
|
527
|
+
} catch {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
async list(type) {
|
|
532
|
+
await this.ensureSchema();
|
|
533
|
+
try {
|
|
534
|
+
const rows = await this.driver.find(this.tableName, {
|
|
535
|
+
object: this.tableName,
|
|
536
|
+
where: this.baseFilter(type),
|
|
537
|
+
fields: ["name"]
|
|
538
|
+
});
|
|
539
|
+
return rows.map((row) => row.name).filter((name) => typeof name === "string");
|
|
540
|
+
} catch {
|
|
541
|
+
return [];
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
async save(type, name, data, _options) {
|
|
545
|
+
const startTime = Date.now();
|
|
546
|
+
await this.ensureSchema();
|
|
547
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
548
|
+
const metadataJson = JSON.stringify(data);
|
|
549
|
+
try {
|
|
550
|
+
const existing = await this.driver.findOne(this.tableName, {
|
|
551
|
+
object: this.tableName,
|
|
552
|
+
where: this.baseFilter(type, name)
|
|
553
|
+
});
|
|
554
|
+
if (existing) {
|
|
555
|
+
const version = (existing.version ?? 0) + 1;
|
|
556
|
+
await this.driver.update(this.tableName, existing.id, {
|
|
557
|
+
metadata: metadataJson,
|
|
558
|
+
version,
|
|
559
|
+
updated_at: now,
|
|
560
|
+
state: "active"
|
|
561
|
+
});
|
|
562
|
+
return {
|
|
563
|
+
success: true,
|
|
564
|
+
path: `datasource://${this.tableName}/${type}/${name}`,
|
|
565
|
+
size: metadataJson.length,
|
|
566
|
+
saveTime: Date.now() - startTime
|
|
567
|
+
};
|
|
568
|
+
} else {
|
|
569
|
+
const id = generateId();
|
|
570
|
+
await this.driver.create(this.tableName, {
|
|
571
|
+
id,
|
|
572
|
+
name,
|
|
573
|
+
type,
|
|
574
|
+
namespace: "default",
|
|
575
|
+
scope: data?.scope ?? "platform",
|
|
576
|
+
metadata: metadataJson,
|
|
577
|
+
strategy: "merge",
|
|
578
|
+
state: "active",
|
|
579
|
+
version: 1,
|
|
580
|
+
source: "database",
|
|
581
|
+
...this.tenantId ? { tenant_id: this.tenantId } : {},
|
|
582
|
+
created_at: now,
|
|
583
|
+
updated_at: now
|
|
584
|
+
});
|
|
585
|
+
return {
|
|
586
|
+
success: true,
|
|
587
|
+
path: `datasource://${this.tableName}/${type}/${name}`,
|
|
588
|
+
size: metadataJson.length,
|
|
589
|
+
saveTime: Date.now() - startTime
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
} catch (error) {
|
|
593
|
+
throw new Error(
|
|
594
|
+
`DatabaseLoader save failed for ${type}/${name}: ${error instanceof Error ? error.message : String(error)}`
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
function generateId() {
|
|
600
|
+
if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.randomUUID === "function") {
|
|
601
|
+
return globalThis.crypto.randomUUID();
|
|
602
|
+
}
|
|
603
|
+
return `meta_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// src/metadata-manager.ts
|
|
607
|
+
var MetadataManager = class {
|
|
608
|
+
constructor(config) {
|
|
609
|
+
this.loaders = /* @__PURE__ */ new Map();
|
|
610
|
+
this.watchCallbacks = /* @__PURE__ */ new Map();
|
|
611
|
+
// In-memory metadata registry: type -> name -> data
|
|
612
|
+
this.registry = /* @__PURE__ */ new Map();
|
|
613
|
+
// Overlay storage: "type:name:scope" -> MetadataOverlay
|
|
614
|
+
this.overlays = /* @__PURE__ */ new Map();
|
|
615
|
+
// Type registry for metadata type info
|
|
616
|
+
this.typeRegistry = [];
|
|
617
|
+
// Dependency tracking: "type:name" -> dependencies
|
|
618
|
+
this.dependencies = /* @__PURE__ */ new Map();
|
|
619
|
+
this.config = config;
|
|
620
|
+
this.logger = (0, import_core.createLogger)({ level: "info", format: "pretty" });
|
|
621
|
+
this.serializers = /* @__PURE__ */ new Map();
|
|
622
|
+
const formats = config.formats || ["typescript", "json", "yaml"];
|
|
623
|
+
if (formats.includes("json")) {
|
|
624
|
+
this.serializers.set("json", new JSONSerializer());
|
|
625
|
+
}
|
|
626
|
+
if (formats.includes("yaml")) {
|
|
627
|
+
this.serializers.set("yaml", new YAMLSerializer());
|
|
628
|
+
}
|
|
629
|
+
if (formats.includes("typescript")) {
|
|
630
|
+
this.serializers.set("typescript", new TypeScriptSerializer("typescript"));
|
|
631
|
+
}
|
|
632
|
+
if (formats.includes("javascript")) {
|
|
633
|
+
this.serializers.set("javascript", new TypeScriptSerializer("javascript"));
|
|
634
|
+
}
|
|
635
|
+
if (config.loaders && config.loaders.length > 0) {
|
|
636
|
+
config.loaders.forEach((loader) => this.registerLoader(loader));
|
|
637
|
+
}
|
|
638
|
+
if (config.datasource && config.driver) {
|
|
639
|
+
this.setDatabaseDriver(config.driver);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Set the type registry for metadata type discovery.
|
|
644
|
+
*/
|
|
645
|
+
setTypeRegistry(entries) {
|
|
646
|
+
this.typeRegistry = entries;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Configure and register a DatabaseLoader for database-backed metadata persistence.
|
|
650
|
+
* Can be called at any time to enable database storage (e.g. after kernel resolves the driver).
|
|
651
|
+
*
|
|
652
|
+
* @param driver - An IDataDriver instance for database operations
|
|
653
|
+
*/
|
|
654
|
+
setDatabaseDriver(driver) {
|
|
655
|
+
const tableName = this.config.tableName ?? "sys_metadata";
|
|
656
|
+
const dbLoader = new DatabaseLoader({
|
|
657
|
+
driver,
|
|
658
|
+
tableName
|
|
659
|
+
});
|
|
660
|
+
this.registerLoader(dbLoader);
|
|
661
|
+
this.logger.info("DatabaseLoader configured", { datasource: this.config.datasource, tableName });
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Register a new metadata loader (data source)
|
|
665
|
+
*/
|
|
666
|
+
registerLoader(loader) {
|
|
667
|
+
this.loaders.set(loader.contract.name, loader);
|
|
668
|
+
this.logger.info(`Registered metadata loader: ${loader.contract.name} (${loader.contract.protocol})`);
|
|
669
|
+
}
|
|
670
|
+
// ==========================================
|
|
671
|
+
// IMetadataService — Core CRUD Operations
|
|
672
|
+
// ==========================================
|
|
673
|
+
/**
|
|
674
|
+
* Register/save a metadata item by type
|
|
675
|
+
*/
|
|
676
|
+
async register(type, name, data) {
|
|
677
|
+
if (!this.registry.has(type)) {
|
|
678
|
+
this.registry.set(type, /* @__PURE__ */ new Map());
|
|
679
|
+
}
|
|
680
|
+
this.registry.get(type).set(name, data);
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Get a metadata item by type and name.
|
|
684
|
+
* Checks in-memory registry first, then falls back to loaders.
|
|
685
|
+
*/
|
|
686
|
+
async get(type, name) {
|
|
687
|
+
const typeStore = this.registry.get(type);
|
|
688
|
+
if (typeStore?.has(name)) {
|
|
689
|
+
return typeStore.get(name);
|
|
690
|
+
}
|
|
691
|
+
const result = await this.load(type, name);
|
|
692
|
+
return result ?? void 0;
|
|
693
|
+
}
|
|
694
|
+
/**
|
|
695
|
+
* List all metadata items of a given type
|
|
696
|
+
*/
|
|
697
|
+
async list(type) {
|
|
698
|
+
const items = /* @__PURE__ */ new Map();
|
|
699
|
+
const typeStore = this.registry.get(type);
|
|
700
|
+
if (typeStore) {
|
|
701
|
+
for (const [name, data] of typeStore) {
|
|
702
|
+
items.set(name, data);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
for (const loader of this.loaders.values()) {
|
|
706
|
+
try {
|
|
707
|
+
const loaderItems = await loader.loadMany(type);
|
|
708
|
+
for (const item of loaderItems) {
|
|
709
|
+
const itemAny = item;
|
|
710
|
+
if (itemAny && typeof itemAny.name === "string" && !items.has(itemAny.name)) {
|
|
711
|
+
items.set(itemAny.name, item);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
} catch (e) {
|
|
715
|
+
this.logger.warn(`Loader ${loader.contract.name} failed to loadMany ${type}`, { error: e });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return Array.from(items.values());
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Unregister/remove a metadata item by type and name
|
|
722
|
+
*/
|
|
723
|
+
async unregister(type, name) {
|
|
724
|
+
const typeStore = this.registry.get(type);
|
|
725
|
+
if (typeStore) {
|
|
726
|
+
typeStore.delete(name);
|
|
727
|
+
if (typeStore.size === 0) {
|
|
728
|
+
this.registry.delete(type);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Check if a metadata item exists
|
|
734
|
+
*/
|
|
735
|
+
async exists(type, name) {
|
|
736
|
+
if (this.registry.get(type)?.has(name)) {
|
|
737
|
+
return true;
|
|
738
|
+
}
|
|
739
|
+
for (const loader of this.loaders.values()) {
|
|
740
|
+
if (await loader.exists(type, name)) {
|
|
741
|
+
return true;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return false;
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* List all names of metadata items of a given type
|
|
748
|
+
*/
|
|
749
|
+
async listNames(type) {
|
|
750
|
+
const names = /* @__PURE__ */ new Set();
|
|
751
|
+
const typeStore = this.registry.get(type);
|
|
752
|
+
if (typeStore) {
|
|
753
|
+
for (const name of typeStore.keys()) {
|
|
754
|
+
names.add(name);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
for (const loader of this.loaders.values()) {
|
|
758
|
+
const result = await loader.list(type);
|
|
759
|
+
result.forEach((item) => names.add(item));
|
|
760
|
+
}
|
|
761
|
+
return Array.from(names);
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Convenience: get an object definition by name
|
|
765
|
+
*/
|
|
766
|
+
async getObject(name) {
|
|
767
|
+
return this.get("object", name);
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Convenience: list all object definitions
|
|
771
|
+
*/
|
|
772
|
+
async listObjects() {
|
|
773
|
+
return this.list("object");
|
|
774
|
+
}
|
|
775
|
+
// ==========================================
|
|
776
|
+
// Convenience: UI Metadata
|
|
777
|
+
// ==========================================
|
|
778
|
+
/**
|
|
779
|
+
* Convenience: get a view definition by name
|
|
780
|
+
*/
|
|
781
|
+
async getView(name) {
|
|
782
|
+
return this.get("view", name);
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Convenience: list view definitions, optionally filtered by object
|
|
786
|
+
*/
|
|
787
|
+
async listViews(object) {
|
|
788
|
+
const views = await this.list("view");
|
|
789
|
+
if (object) {
|
|
790
|
+
return views.filter((v) => v?.object === object);
|
|
791
|
+
}
|
|
792
|
+
return views;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Convenience: get a dashboard definition by name
|
|
796
|
+
*/
|
|
797
|
+
async getDashboard(name) {
|
|
798
|
+
return this.get("dashboard", name);
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Convenience: list all dashboard definitions
|
|
802
|
+
*/
|
|
803
|
+
async listDashboards() {
|
|
804
|
+
return this.list("dashboard");
|
|
805
|
+
}
|
|
806
|
+
// ==========================================
|
|
807
|
+
// Package Management
|
|
808
|
+
// ==========================================
|
|
809
|
+
/**
|
|
810
|
+
* Unregister all metadata items from a specific package
|
|
811
|
+
*/
|
|
812
|
+
async unregisterPackage(packageName) {
|
|
813
|
+
for (const [type, typeStore] of this.registry) {
|
|
814
|
+
const toDelete = [];
|
|
815
|
+
for (const [name, data] of typeStore) {
|
|
816
|
+
const meta = data;
|
|
817
|
+
if (meta?.packageId === packageName || meta?.package === packageName) {
|
|
818
|
+
toDelete.push(name);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
for (const name of toDelete) {
|
|
822
|
+
typeStore.delete(name);
|
|
823
|
+
}
|
|
824
|
+
if (typeStore.size === 0) {
|
|
825
|
+
this.registry.delete(type);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* Publish an entire package:
|
|
831
|
+
* 1. Validate all draft items
|
|
832
|
+
* 2. Snapshot all items in the package (publishedDefinition = clone(metadata))
|
|
833
|
+
* 3. Increment version
|
|
834
|
+
* 4. Set all items state → active
|
|
835
|
+
*/
|
|
836
|
+
async publishPackage(packageId, options) {
|
|
837
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
838
|
+
const shouldValidate = options?.validate !== false;
|
|
839
|
+
const publishedBy = options?.publishedBy;
|
|
840
|
+
const packageItems = [];
|
|
841
|
+
for (const [type, typeStore] of this.registry) {
|
|
842
|
+
for (const [name, data] of typeStore) {
|
|
843
|
+
const meta = data;
|
|
844
|
+
if (meta?.packageId === packageId || meta?.package === packageId) {
|
|
845
|
+
packageItems.push({ type, name, data: meta });
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
if (packageItems.length === 0) {
|
|
850
|
+
return {
|
|
851
|
+
success: false,
|
|
852
|
+
packageId,
|
|
853
|
+
version: 0,
|
|
854
|
+
publishedAt: now,
|
|
855
|
+
itemsPublished: 0,
|
|
856
|
+
validationErrors: [{ type: "", name: "", message: `No metadata items found for package '${packageId}'` }]
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
if (shouldValidate) {
|
|
860
|
+
const validationErrors = [];
|
|
861
|
+
for (const item of packageItems) {
|
|
862
|
+
const result = await this.validate(item.type, item.data);
|
|
863
|
+
if (!result.valid && result.errors) {
|
|
864
|
+
for (const err of result.errors) {
|
|
865
|
+
validationErrors.push({
|
|
866
|
+
type: item.type,
|
|
867
|
+
name: item.name,
|
|
868
|
+
message: err.message
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
const packageItemKeys = new Set(packageItems.map((i) => `${i.type}:${i.name}`));
|
|
874
|
+
for (const item of packageItems) {
|
|
875
|
+
const deps = await this.getDependencies(item.type, item.name);
|
|
876
|
+
for (const dep of deps) {
|
|
877
|
+
const depKey = `${dep.targetType}:${dep.targetName}`;
|
|
878
|
+
if (packageItemKeys.has(depKey)) continue;
|
|
879
|
+
const depItem = await this.get(dep.targetType, dep.targetName);
|
|
880
|
+
if (!depItem) {
|
|
881
|
+
validationErrors.push({
|
|
882
|
+
type: item.type,
|
|
883
|
+
name: item.name,
|
|
884
|
+
message: `Dependency '${dep.targetType}:${dep.targetName}' not found`
|
|
885
|
+
});
|
|
886
|
+
} else {
|
|
887
|
+
const depMeta = depItem;
|
|
888
|
+
if (depMeta.publishedDefinition === void 0 && depMeta.state !== "active") {
|
|
889
|
+
validationErrors.push({
|
|
890
|
+
type: item.type,
|
|
891
|
+
name: item.name,
|
|
892
|
+
message: `Dependency '${dep.targetType}:${dep.targetName}' is not published`
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (validationErrors.length > 0) {
|
|
899
|
+
return {
|
|
900
|
+
success: false,
|
|
901
|
+
packageId,
|
|
902
|
+
version: 0,
|
|
903
|
+
publishedAt: now,
|
|
904
|
+
itemsPublished: 0,
|
|
905
|
+
validationErrors
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
let maxVersion = 0;
|
|
910
|
+
for (const item of packageItems) {
|
|
911
|
+
const v = typeof item.data.version === "number" ? item.data.version : 0;
|
|
912
|
+
if (v > maxVersion) maxVersion = v;
|
|
913
|
+
}
|
|
914
|
+
const newVersion = maxVersion + 1;
|
|
915
|
+
for (const item of packageItems) {
|
|
916
|
+
const updated = {
|
|
917
|
+
...item.data,
|
|
918
|
+
publishedDefinition: structuredClone(item.data.metadata ?? item.data),
|
|
919
|
+
publishedAt: now,
|
|
920
|
+
publishedBy: publishedBy ?? item.data.publishedBy,
|
|
921
|
+
version: newVersion,
|
|
922
|
+
state: "active"
|
|
923
|
+
};
|
|
924
|
+
await this.register(item.type, item.name, updated);
|
|
925
|
+
}
|
|
926
|
+
return {
|
|
927
|
+
success: true,
|
|
928
|
+
packageId,
|
|
929
|
+
version: newVersion,
|
|
930
|
+
publishedAt: now,
|
|
931
|
+
itemsPublished: packageItems.length
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
/**
|
|
935
|
+
* Revert entire package to last published state.
|
|
936
|
+
* Restores all metadata definitions from their published snapshots.
|
|
937
|
+
*/
|
|
938
|
+
async revertPackage(packageId) {
|
|
939
|
+
const packageItems = [];
|
|
940
|
+
for (const [type, typeStore] of this.registry) {
|
|
941
|
+
for (const [name, data] of typeStore) {
|
|
942
|
+
const meta = data;
|
|
943
|
+
if (meta?.packageId === packageId || meta?.package === packageId) {
|
|
944
|
+
packageItems.push({ type, name, data: meta });
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
if (packageItems.length === 0) {
|
|
949
|
+
throw new Error(`No metadata items found for package '${packageId}'`);
|
|
950
|
+
}
|
|
951
|
+
const hasPublished = packageItems.some((item) => item.data.publishedDefinition !== void 0);
|
|
952
|
+
if (!hasPublished) {
|
|
953
|
+
throw new Error(`Package '${packageId}' has never been published`);
|
|
954
|
+
}
|
|
955
|
+
for (const item of packageItems) {
|
|
956
|
+
if (item.data.publishedDefinition !== void 0) {
|
|
957
|
+
const reverted = {
|
|
958
|
+
...item.data,
|
|
959
|
+
metadata: structuredClone(item.data.publishedDefinition),
|
|
960
|
+
state: "active"
|
|
961
|
+
};
|
|
962
|
+
await this.register(item.type, item.name, reverted);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Get the published version of any metadata item (for runtime serving).
|
|
968
|
+
* Returns publishedDefinition if exists, else current definition.
|
|
969
|
+
*/
|
|
970
|
+
async getPublished(type, name) {
|
|
971
|
+
const item = await this.get(type, name);
|
|
972
|
+
if (!item) return void 0;
|
|
973
|
+
const meta = item;
|
|
974
|
+
if (meta.publishedDefinition !== void 0) {
|
|
975
|
+
return meta.publishedDefinition;
|
|
976
|
+
}
|
|
977
|
+
return meta.metadata ?? item;
|
|
978
|
+
}
|
|
979
|
+
// ==========================================
|
|
980
|
+
// Query / Search
|
|
981
|
+
// ==========================================
|
|
982
|
+
/**
|
|
983
|
+
* Query metadata items with filtering, sorting, and pagination
|
|
984
|
+
*/
|
|
985
|
+
async query(query) {
|
|
986
|
+
const { types, search, page = 1, pageSize = 50, sortBy = "name", sortOrder = "asc" } = query;
|
|
987
|
+
const allItems = [];
|
|
988
|
+
const targetTypes = types && types.length > 0 ? types : Array.from(this.registry.keys());
|
|
989
|
+
for (const type of targetTypes) {
|
|
990
|
+
const items = await this.list(type);
|
|
991
|
+
for (const item of items) {
|
|
992
|
+
const meta = item;
|
|
993
|
+
allItems.push({
|
|
994
|
+
type,
|
|
995
|
+
name: meta?.name ?? "",
|
|
996
|
+
namespace: meta?.namespace,
|
|
997
|
+
label: meta?.label,
|
|
998
|
+
scope: meta?.scope,
|
|
999
|
+
state: meta?.state,
|
|
1000
|
+
packageId: meta?.packageId,
|
|
1001
|
+
updatedAt: meta?.updatedAt
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
let filtered = allItems;
|
|
1006
|
+
if (search) {
|
|
1007
|
+
const searchLower = search.toLowerCase();
|
|
1008
|
+
filtered = filtered.filter(
|
|
1009
|
+
(item) => item.name.toLowerCase().includes(searchLower) || item.label && item.label.toLowerCase().includes(searchLower)
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
if (query.scope) {
|
|
1013
|
+
filtered = filtered.filter((item) => item.scope === query.scope);
|
|
1014
|
+
}
|
|
1015
|
+
if (query.state) {
|
|
1016
|
+
filtered = filtered.filter((item) => item.state === query.state);
|
|
1017
|
+
}
|
|
1018
|
+
if (query.namespaces && query.namespaces.length > 0) {
|
|
1019
|
+
filtered = filtered.filter((item) => item.namespace && query.namespaces.includes(item.namespace));
|
|
1020
|
+
}
|
|
1021
|
+
if (query.packageId) {
|
|
1022
|
+
filtered = filtered.filter((item) => item.packageId === query.packageId);
|
|
1023
|
+
}
|
|
1024
|
+
if (query.tags && query.tags.length > 0) {
|
|
1025
|
+
filtered = filtered.filter((item) => {
|
|
1026
|
+
const meta = item;
|
|
1027
|
+
return meta?.tags && query.tags.some((t) => meta.tags.includes(t));
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
filtered.sort((a, b) => {
|
|
1031
|
+
const aVal = a[sortBy] ?? "";
|
|
1032
|
+
const bVal = b[sortBy] ?? "";
|
|
1033
|
+
const cmp = String(aVal).localeCompare(String(bVal));
|
|
1034
|
+
return sortOrder === "desc" ? -cmp : cmp;
|
|
1035
|
+
});
|
|
1036
|
+
const total = filtered.length;
|
|
1037
|
+
const start = (page - 1) * pageSize;
|
|
1038
|
+
const paged = filtered.slice(start, start + pageSize);
|
|
1039
|
+
return {
|
|
1040
|
+
items: paged,
|
|
1041
|
+
total,
|
|
1042
|
+
page,
|
|
1043
|
+
pageSize
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
// ==========================================
|
|
1047
|
+
// Bulk Operations
|
|
1048
|
+
// ==========================================
|
|
1049
|
+
/**
|
|
1050
|
+
* Register multiple metadata items in a single batch
|
|
1051
|
+
*/
|
|
1052
|
+
async bulkRegister(items, options) {
|
|
1053
|
+
const { continueOnError = false } = options ?? {};
|
|
1054
|
+
let succeeded = 0;
|
|
1055
|
+
let failed = 0;
|
|
1056
|
+
const errors = [];
|
|
1057
|
+
for (const item of items) {
|
|
1058
|
+
try {
|
|
1059
|
+
await this.register(item.type, item.name, item.data);
|
|
1060
|
+
succeeded++;
|
|
1061
|
+
} catch (e) {
|
|
1062
|
+
failed++;
|
|
1063
|
+
errors.push({
|
|
1064
|
+
type: item.type,
|
|
1065
|
+
name: item.name,
|
|
1066
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1067
|
+
});
|
|
1068
|
+
if (!continueOnError) break;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
return {
|
|
1072
|
+
total: items.length,
|
|
1073
|
+
succeeded,
|
|
1074
|
+
failed,
|
|
1075
|
+
errors: errors.length > 0 ? errors : void 0
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
/**
|
|
1079
|
+
* Unregister multiple metadata items in a single batch
|
|
1080
|
+
*/
|
|
1081
|
+
async bulkUnregister(items) {
|
|
1082
|
+
let succeeded = 0;
|
|
1083
|
+
let failed = 0;
|
|
1084
|
+
const errors = [];
|
|
1085
|
+
for (const item of items) {
|
|
1086
|
+
try {
|
|
1087
|
+
await this.unregister(item.type, item.name);
|
|
1088
|
+
succeeded++;
|
|
1089
|
+
} catch (e) {
|
|
1090
|
+
failed++;
|
|
1091
|
+
errors.push({
|
|
1092
|
+
type: item.type,
|
|
1093
|
+
name: item.name,
|
|
1094
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return {
|
|
1099
|
+
total: items.length,
|
|
1100
|
+
succeeded,
|
|
1101
|
+
failed,
|
|
1102
|
+
errors: errors.length > 0 ? errors : void 0
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
// ==========================================
|
|
1106
|
+
// Overlay / Customization Management
|
|
1107
|
+
// ==========================================
|
|
1108
|
+
overlayKey(type, name, scope = "platform") {
|
|
1109
|
+
return `${encodeURIComponent(type)}:${encodeURIComponent(name)}:${scope}`;
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Get the active overlay for a metadata item
|
|
1113
|
+
*/
|
|
1114
|
+
async getOverlay(type, name, scope) {
|
|
1115
|
+
return this.overlays.get(this.overlayKey(type, name, scope ?? "platform"));
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* Save/update an overlay for a metadata item
|
|
1119
|
+
*/
|
|
1120
|
+
async saveOverlay(overlay) {
|
|
1121
|
+
const key = this.overlayKey(overlay.baseType, overlay.baseName, overlay.scope);
|
|
1122
|
+
this.overlays.set(key, overlay);
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Remove an overlay, reverting to the base definition
|
|
1126
|
+
*/
|
|
1127
|
+
async removeOverlay(type, name, scope) {
|
|
1128
|
+
this.overlays.delete(this.overlayKey(type, name, scope ?? "platform"));
|
|
1129
|
+
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Get the effective (merged) metadata after applying all overlays.
|
|
1132
|
+
* Resolution order: system ← merge(platform) ← merge(user)
|
|
1133
|
+
*/
|
|
1134
|
+
async getEffective(type, name, context) {
|
|
1135
|
+
const base = await this.get(type, name);
|
|
1136
|
+
if (!base) return void 0;
|
|
1137
|
+
let effective = { ...base };
|
|
1138
|
+
const platformOverlay = await this.getOverlay(type, name, "platform");
|
|
1139
|
+
if (platformOverlay?.active && platformOverlay.patch) {
|
|
1140
|
+
effective = { ...effective, ...platformOverlay.patch };
|
|
1141
|
+
}
|
|
1142
|
+
if (context?.userId) {
|
|
1143
|
+
const userOverlayKey = this.overlayKey(type, name, "user") + `:${context.userId}`;
|
|
1144
|
+
const userOverlay = this.overlays.get(userOverlayKey) ?? await this.getOverlay(type, name, "user");
|
|
1145
|
+
if (userOverlay?.active && userOverlay.patch) {
|
|
1146
|
+
if (!userOverlay.owner || userOverlay.owner === context.userId) {
|
|
1147
|
+
effective = { ...effective, ...userOverlay.patch };
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
} else {
|
|
1151
|
+
const userOverlay = await this.getOverlay(type, name, "user");
|
|
1152
|
+
if (userOverlay?.active && userOverlay.patch && !userOverlay.owner) {
|
|
1153
|
+
effective = { ...effective, ...userOverlay.patch };
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
return effective;
|
|
1157
|
+
}
|
|
1158
|
+
// ==========================================
|
|
1159
|
+
// Watch / Subscribe (IMetadataService)
|
|
1160
|
+
// ==========================================
|
|
1161
|
+
/**
|
|
1162
|
+
* Watch for metadata changes (IMetadataService contract).
|
|
1163
|
+
* Returns a handle for unsubscribing.
|
|
1164
|
+
*/
|
|
1165
|
+
watchService(type, callback) {
|
|
1166
|
+
const wrappedCallback = (event) => {
|
|
1167
|
+
const mappedType = event.type === "added" ? "registered" : event.type === "deleted" ? "unregistered" : "updated";
|
|
1168
|
+
callback({
|
|
1169
|
+
type: mappedType,
|
|
1170
|
+
metadataType: event.metadataType ?? type,
|
|
1171
|
+
name: event.name ?? "",
|
|
1172
|
+
data: event.data
|
|
1173
|
+
});
|
|
1174
|
+
};
|
|
1175
|
+
this.addWatchCallback(type, wrappedCallback);
|
|
1176
|
+
return {
|
|
1177
|
+
unsubscribe: () => this.removeWatchCallback(type, wrappedCallback)
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
// ==========================================
|
|
1181
|
+
// Import / Export
|
|
1182
|
+
// ==========================================
|
|
1183
|
+
/**
|
|
1184
|
+
* Export metadata as a portable bundle
|
|
1185
|
+
*/
|
|
1186
|
+
async exportMetadata(options) {
|
|
1187
|
+
const bundle = {};
|
|
1188
|
+
const targetTypes = options?.types ?? Array.from(this.registry.keys());
|
|
1189
|
+
for (const type of targetTypes) {
|
|
1190
|
+
const items = await this.list(type);
|
|
1191
|
+
if (items.length > 0) {
|
|
1192
|
+
bundle[type] = items;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
return bundle;
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* Import metadata from a portable bundle
|
|
1199
|
+
*/
|
|
1200
|
+
async importMetadata(data, options) {
|
|
1201
|
+
const {
|
|
1202
|
+
conflictResolution = "skip",
|
|
1203
|
+
validate: _validate = true,
|
|
1204
|
+
dryRun = false
|
|
1205
|
+
} = options ?? {};
|
|
1206
|
+
const bundle = data;
|
|
1207
|
+
let total = 0;
|
|
1208
|
+
let imported = 0;
|
|
1209
|
+
let skipped = 0;
|
|
1210
|
+
let failed = 0;
|
|
1211
|
+
const errors = [];
|
|
1212
|
+
for (const [type, items] of Object.entries(bundle)) {
|
|
1213
|
+
if (!Array.isArray(items)) continue;
|
|
1214
|
+
for (const item of items) {
|
|
1215
|
+
total++;
|
|
1216
|
+
const meta = item;
|
|
1217
|
+
const name = meta?.name;
|
|
1218
|
+
if (!name) {
|
|
1219
|
+
failed++;
|
|
1220
|
+
errors.push({ type, name: "(unknown)", error: "Item missing name field" });
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
try {
|
|
1224
|
+
const itemExists = await this.exists(type, name);
|
|
1225
|
+
if (itemExists && conflictResolution === "skip") {
|
|
1226
|
+
skipped++;
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
if (!dryRun) {
|
|
1230
|
+
if (itemExists && conflictResolution === "merge") {
|
|
1231
|
+
const existing = await this.get(type, name);
|
|
1232
|
+
const merged = { ...existing, ...item };
|
|
1233
|
+
await this.register(type, name, merged);
|
|
1234
|
+
} else {
|
|
1235
|
+
await this.register(type, name, item);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
imported++;
|
|
1239
|
+
} catch (e) {
|
|
1240
|
+
failed++;
|
|
1241
|
+
errors.push({
|
|
1242
|
+
type,
|
|
1243
|
+
name,
|
|
1244
|
+
error: e instanceof Error ? e.message : String(e)
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
return {
|
|
1250
|
+
total,
|
|
1251
|
+
imported,
|
|
1252
|
+
skipped,
|
|
1253
|
+
failed,
|
|
1254
|
+
errors: errors.length > 0 ? errors : void 0
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
// ==========================================
|
|
1258
|
+
// Validation
|
|
1259
|
+
// ==========================================
|
|
1260
|
+
/**
|
|
1261
|
+
* Validate a metadata item against its type schema.
|
|
1262
|
+
* Returns validation result with errors and warnings.
|
|
1263
|
+
*/
|
|
1264
|
+
async validate(_type, data) {
|
|
1265
|
+
if (data === null || data === void 0) {
|
|
1266
|
+
return {
|
|
1267
|
+
valid: false,
|
|
1268
|
+
errors: [{ path: "", message: "Metadata data cannot be null or undefined" }]
|
|
1269
|
+
};
|
|
1270
|
+
}
|
|
1271
|
+
if (typeof data !== "object") {
|
|
1272
|
+
return {
|
|
1273
|
+
valid: false,
|
|
1274
|
+
errors: [{ path: "", message: "Metadata data must be an object" }]
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
const meta = data;
|
|
1278
|
+
const warnings = [];
|
|
1279
|
+
if (!meta.name) {
|
|
1280
|
+
return {
|
|
1281
|
+
valid: false,
|
|
1282
|
+
errors: [{ path: "name", message: "Metadata item must have a name field" }]
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
if (!meta.label) {
|
|
1286
|
+
warnings.push({ path: "label", message: "Missing label field (recommended)" });
|
|
1287
|
+
}
|
|
1288
|
+
return { valid: true, warnings: warnings.length > 0 ? warnings : void 0 };
|
|
1289
|
+
}
|
|
1290
|
+
// ==========================================
|
|
1291
|
+
// Type Registry
|
|
1292
|
+
// ==========================================
|
|
1293
|
+
/**
|
|
1294
|
+
* Get all registered metadata types
|
|
1295
|
+
*/
|
|
1296
|
+
async getRegisteredTypes() {
|
|
1297
|
+
const types = /* @__PURE__ */ new Set();
|
|
1298
|
+
for (const entry of this.typeRegistry) {
|
|
1299
|
+
types.add(entry.type);
|
|
1300
|
+
}
|
|
1301
|
+
for (const type of this.registry.keys()) {
|
|
1302
|
+
types.add(type);
|
|
1303
|
+
}
|
|
1304
|
+
return Array.from(types);
|
|
1305
|
+
}
|
|
1306
|
+
/**
|
|
1307
|
+
* Get detailed information about a metadata type
|
|
1308
|
+
*/
|
|
1309
|
+
async getTypeInfo(type) {
|
|
1310
|
+
const entry = this.typeRegistry.find((e) => e.type === type);
|
|
1311
|
+
if (!entry) return void 0;
|
|
1312
|
+
return {
|
|
1313
|
+
type: entry.type,
|
|
1314
|
+
label: entry.label,
|
|
1315
|
+
description: entry.description,
|
|
1316
|
+
filePatterns: entry.filePatterns,
|
|
1317
|
+
supportsOverlay: entry.supportsOverlay,
|
|
1318
|
+
domain: entry.domain
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
// ==========================================
|
|
1322
|
+
// Dependency Tracking
|
|
1323
|
+
// ==========================================
|
|
1324
|
+
/**
|
|
1325
|
+
* Get metadata items that this item depends on
|
|
1326
|
+
*/
|
|
1327
|
+
async getDependencies(type, name) {
|
|
1328
|
+
return this.dependencies.get(`${encodeURIComponent(type)}:${encodeURIComponent(name)}`) ?? [];
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Get metadata items that depend on this item
|
|
1332
|
+
*/
|
|
1333
|
+
async getDependents(type, name) {
|
|
1334
|
+
const dependents = [];
|
|
1335
|
+
for (const deps of this.dependencies.values()) {
|
|
1336
|
+
for (const dep of deps) {
|
|
1337
|
+
if (dep.targetType === type && dep.targetName === name) {
|
|
1338
|
+
dependents.push(dep);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
return dependents;
|
|
1343
|
+
}
|
|
1344
|
+
/**
|
|
1345
|
+
* Register a dependency between two metadata items.
|
|
1346
|
+
* Used internally to track cross-references.
|
|
1347
|
+
* Duplicate dependencies (same source, target, and kind) are ignored.
|
|
1348
|
+
*/
|
|
1349
|
+
addDependency(dep) {
|
|
1350
|
+
const key = `${encodeURIComponent(dep.sourceType)}:${encodeURIComponent(dep.sourceName)}`;
|
|
1351
|
+
if (!this.dependencies.has(key)) {
|
|
1352
|
+
this.dependencies.set(key, []);
|
|
1353
|
+
}
|
|
1354
|
+
const existing = this.dependencies.get(key);
|
|
1355
|
+
const isDuplicate = existing.some(
|
|
1356
|
+
(d) => d.targetType === dep.targetType && d.targetName === dep.targetName && d.kind === dep.kind
|
|
1357
|
+
);
|
|
1358
|
+
if (!isDuplicate) {
|
|
1359
|
+
existing.push(dep);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
// ==========================================
|
|
1363
|
+
// Legacy Loader API (backward compatible)
|
|
1364
|
+
// ==========================================
|
|
1365
|
+
/**
|
|
1366
|
+
* Load a single metadata item from loaders.
|
|
1367
|
+
* Iterates through registered loaders until found.
|
|
1368
|
+
*/
|
|
1369
|
+
async load(type, name, options) {
|
|
1370
|
+
for (const loader of this.loaders.values()) {
|
|
1371
|
+
try {
|
|
1372
|
+
const result = await loader.load(type, name, options);
|
|
1373
|
+
if (result.data) {
|
|
1374
|
+
return result.data;
|
|
1375
|
+
}
|
|
1376
|
+
} catch (e) {
|
|
1377
|
+
this.logger.warn(`Loader ${loader.contract.name} failed to load ${type}:${name}`, { error: e });
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
return null;
|
|
1381
|
+
}
|
|
1382
|
+
/**
|
|
1383
|
+
* Load multiple metadata items from loaders.
|
|
1384
|
+
* Aggregates results from all loaders.
|
|
1385
|
+
*/
|
|
1386
|
+
async loadMany(type, options) {
|
|
1387
|
+
const results = [];
|
|
1388
|
+
for (const loader of this.loaders.values()) {
|
|
1389
|
+
try {
|
|
1390
|
+
const items = await loader.loadMany(type, options);
|
|
1391
|
+
for (const item of items) {
|
|
1392
|
+
const itemAny = item;
|
|
1393
|
+
if (itemAny && typeof itemAny.name === "string") {
|
|
1394
|
+
const exists = results.some((r) => r && r.name === itemAny.name);
|
|
1395
|
+
if (exists) continue;
|
|
1396
|
+
}
|
|
1397
|
+
results.push(item);
|
|
1398
|
+
}
|
|
1399
|
+
} catch (e) {
|
|
1400
|
+
this.logger.warn(`Loader ${loader.contract.name} failed to loadMany ${type}`, { error: e });
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
return results;
|
|
1404
|
+
}
|
|
1405
|
+
/**
|
|
1406
|
+
* Save metadata item to a loader
|
|
1407
|
+
*/
|
|
1408
|
+
async save(type, name, data, options) {
|
|
1409
|
+
const targetLoader = options?.loader;
|
|
1410
|
+
let loader;
|
|
1411
|
+
if (targetLoader) {
|
|
1412
|
+
loader = this.loaders.get(targetLoader);
|
|
1413
|
+
if (!loader) {
|
|
1414
|
+
throw new Error(`Loader not found: ${targetLoader}`);
|
|
1415
|
+
}
|
|
1416
|
+
} else {
|
|
1417
|
+
for (const l of this.loaders.values()) {
|
|
1418
|
+
if (!l.save) continue;
|
|
1419
|
+
try {
|
|
1420
|
+
if (await l.exists(type, name)) {
|
|
1421
|
+
loader = l;
|
|
1422
|
+
this.logger.info(`Updating existing metadata in loader: ${l.contract.name}`);
|
|
1423
|
+
break;
|
|
1424
|
+
}
|
|
1425
|
+
} catch (e) {
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
if (!loader) {
|
|
1429
|
+
const fsLoader = this.loaders.get("filesystem");
|
|
1430
|
+
if (fsLoader && fsLoader.save) {
|
|
1431
|
+
loader = fsLoader;
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
if (!loader) {
|
|
1435
|
+
for (const l of this.loaders.values()) {
|
|
1436
|
+
if (l.save) {
|
|
1437
|
+
loader = l;
|
|
1438
|
+
break;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
if (!loader) {
|
|
1444
|
+
throw new Error(`No loader available for saving type: ${type}`);
|
|
1445
|
+
}
|
|
1446
|
+
if (!loader.save) {
|
|
1447
|
+
throw new Error(`Loader '${loader.contract?.name}' does not support saving`);
|
|
1448
|
+
}
|
|
1449
|
+
return loader.save(type, name, data, options);
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Register a watch callback for metadata changes
|
|
1453
|
+
*/
|
|
1454
|
+
addWatchCallback(type, callback) {
|
|
1455
|
+
if (!this.watchCallbacks.has(type)) {
|
|
1456
|
+
this.watchCallbacks.set(type, /* @__PURE__ */ new Set());
|
|
1457
|
+
}
|
|
1458
|
+
this.watchCallbacks.get(type).add(callback);
|
|
1459
|
+
}
|
|
1460
|
+
/**
|
|
1461
|
+
* Remove a watch callback for metadata changes
|
|
1462
|
+
*/
|
|
1463
|
+
removeWatchCallback(type, callback) {
|
|
1464
|
+
const callbacks = this.watchCallbacks.get(type);
|
|
1465
|
+
if (callbacks) {
|
|
1466
|
+
callbacks.delete(callback);
|
|
1467
|
+
if (callbacks.size === 0) {
|
|
1468
|
+
this.watchCallbacks.delete(type);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
/**
|
|
1473
|
+
* Stop all watching
|
|
1474
|
+
*/
|
|
1475
|
+
async stopWatching() {
|
|
1476
|
+
}
|
|
1477
|
+
notifyWatchers(type, event) {
|
|
1478
|
+
const callbacks = this.watchCallbacks.get(type);
|
|
1479
|
+
if (!callbacks) return;
|
|
1480
|
+
for (const callback of callbacks) {
|
|
1481
|
+
try {
|
|
1482
|
+
void callback(event);
|
|
1483
|
+
} catch (error) {
|
|
1484
|
+
this.logger.error("Watch callback error", void 0, {
|
|
1485
|
+
type,
|
|
1486
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
};
|
|
1492
|
+
|
|
1493
|
+
// src/node-metadata-manager.ts
|
|
1494
|
+
var path2 = __toESM(require("path"), 1);
|
|
1495
|
+
var import_chokidar = require("chokidar");
|
|
1496
|
+
|
|
1497
|
+
// src/loaders/filesystem-loader.ts
|
|
1498
|
+
var fs = __toESM(require("fs/promises"), 1);
|
|
1499
|
+
var path = __toESM(require("path"), 1);
|
|
1500
|
+
var import_glob = require("glob");
|
|
1501
|
+
var import_node_crypto = require("crypto");
|
|
1502
|
+
var FilesystemLoader = class {
|
|
1503
|
+
constructor(rootDir, serializers, logger) {
|
|
1504
|
+
this.rootDir = rootDir;
|
|
1505
|
+
this.serializers = serializers;
|
|
1506
|
+
this.logger = logger;
|
|
1507
|
+
this.contract = {
|
|
1508
|
+
name: "filesystem",
|
|
1509
|
+
protocol: "file:",
|
|
1510
|
+
capabilities: {
|
|
1511
|
+
read: true,
|
|
1512
|
+
write: true,
|
|
1513
|
+
watch: true,
|
|
1514
|
+
list: true
|
|
1515
|
+
},
|
|
1516
|
+
supportedFormats: ["json", "yaml", "typescript", "javascript"],
|
|
1517
|
+
supportsWatch: true,
|
|
1518
|
+
supportsWrite: true,
|
|
1519
|
+
supportsCache: true
|
|
1520
|
+
};
|
|
1521
|
+
this.cache = /* @__PURE__ */ new Map();
|
|
1522
|
+
}
|
|
1523
|
+
async load(type, name, options) {
|
|
1524
|
+
const startTime = Date.now();
|
|
1525
|
+
const { validate: _validate = true, useCache = true, ifNoneMatch } = options || {};
|
|
1526
|
+
try {
|
|
1527
|
+
const filePath = await this.findFile(type, name);
|
|
1528
|
+
if (!filePath) {
|
|
1529
|
+
return {
|
|
1530
|
+
data: null,
|
|
1531
|
+
fromCache: false,
|
|
1532
|
+
notModified: false,
|
|
1533
|
+
loadTime: Date.now() - startTime
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
const stats = await this.stat(type, name);
|
|
1537
|
+
if (!stats) {
|
|
1538
|
+
return {
|
|
1539
|
+
data: null,
|
|
1540
|
+
fromCache: false,
|
|
1541
|
+
notModified: false,
|
|
1542
|
+
loadTime: Date.now() - startTime
|
|
1543
|
+
};
|
|
1544
|
+
}
|
|
1545
|
+
if (useCache && ifNoneMatch && stats.etag === ifNoneMatch) {
|
|
1546
|
+
return {
|
|
1547
|
+
data: null,
|
|
1548
|
+
fromCache: true,
|
|
1549
|
+
notModified: true,
|
|
1550
|
+
etag: stats.etag,
|
|
1551
|
+
stats,
|
|
1552
|
+
loadTime: Date.now() - startTime
|
|
1553
|
+
};
|
|
1554
|
+
}
|
|
1555
|
+
const cacheKey = `${type}:${name}`;
|
|
1556
|
+
if (useCache && this.cache.has(cacheKey)) {
|
|
1557
|
+
const cached = this.cache.get(cacheKey);
|
|
1558
|
+
if (cached.etag === stats.etag) {
|
|
1559
|
+
return {
|
|
1560
|
+
data: cached.data,
|
|
1561
|
+
fromCache: true,
|
|
1562
|
+
notModified: false,
|
|
1563
|
+
etag: stats.etag,
|
|
1564
|
+
stats,
|
|
1565
|
+
loadTime: Date.now() - startTime
|
|
1566
|
+
};
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
1570
|
+
const serializer = this.getSerializer(stats.format);
|
|
1571
|
+
if (!serializer) {
|
|
1572
|
+
throw new Error(`No serializer found for format: ${stats.format}`);
|
|
1573
|
+
}
|
|
1574
|
+
const data = serializer.deserialize(content);
|
|
1575
|
+
if (useCache) {
|
|
1576
|
+
this.cache.set(cacheKey, {
|
|
1577
|
+
data,
|
|
1578
|
+
etag: stats.etag || "",
|
|
1579
|
+
timestamp: Date.now()
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
return {
|
|
1583
|
+
data,
|
|
1584
|
+
fromCache: false,
|
|
1585
|
+
notModified: false,
|
|
1586
|
+
etag: stats.etag,
|
|
1587
|
+
stats,
|
|
1588
|
+
loadTime: Date.now() - startTime
|
|
1589
|
+
};
|
|
1590
|
+
} catch (error) {
|
|
1591
|
+
this.logger?.error("Failed to load metadata", void 0, {
|
|
1592
|
+
type,
|
|
1593
|
+
name,
|
|
1594
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1595
|
+
});
|
|
1596
|
+
throw error;
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
async loadMany(type, options) {
|
|
1600
|
+
const { patterns = ["**/*"], recursive: _recursive = true, limit } = options || {};
|
|
1601
|
+
const typeDir = path.join(this.rootDir, type);
|
|
1602
|
+
const items = [];
|
|
1603
|
+
try {
|
|
1604
|
+
const globPatterns = patterns.map(
|
|
1605
|
+
(pattern) => path.join(typeDir, pattern)
|
|
1606
|
+
);
|
|
1607
|
+
for (const pattern of globPatterns) {
|
|
1608
|
+
const files = await (0, import_glob.glob)(pattern, {
|
|
1609
|
+
ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*"],
|
|
1610
|
+
nodir: true
|
|
1611
|
+
});
|
|
1612
|
+
for (const file of files) {
|
|
1613
|
+
if (limit && items.length >= limit) {
|
|
1614
|
+
break;
|
|
1615
|
+
}
|
|
1616
|
+
try {
|
|
1617
|
+
const content = await fs.readFile(file, "utf-8");
|
|
1618
|
+
const format = this.detectFormat(file);
|
|
1619
|
+
const serializer = this.getSerializer(format);
|
|
1620
|
+
if (serializer) {
|
|
1621
|
+
const data = serializer.deserialize(content);
|
|
1622
|
+
items.push(data);
|
|
1623
|
+
}
|
|
1624
|
+
} catch (error) {
|
|
1625
|
+
this.logger?.warn("Failed to load file", {
|
|
1626
|
+
file,
|
|
1627
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
if (limit && items.length >= limit) {
|
|
1632
|
+
break;
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
return items;
|
|
1636
|
+
} catch (error) {
|
|
1637
|
+
this.logger?.error("Failed to load many", void 0, {
|
|
1638
|
+
type,
|
|
1639
|
+
patterns,
|
|
1640
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1641
|
+
});
|
|
1642
|
+
throw error;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
async exists(type, name) {
|
|
1646
|
+
const filePath = await this.findFile(type, name);
|
|
1647
|
+
return filePath !== null;
|
|
1648
|
+
}
|
|
1649
|
+
async stat(type, name) {
|
|
1650
|
+
const filePath = await this.findFile(type, name);
|
|
1651
|
+
if (!filePath) {
|
|
1652
|
+
return null;
|
|
1653
|
+
}
|
|
1654
|
+
try {
|
|
1655
|
+
const stats = await fs.stat(filePath);
|
|
1656
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
1657
|
+
const etag = this.generateETag(content);
|
|
1658
|
+
const format = this.detectFormat(filePath);
|
|
1659
|
+
return {
|
|
1660
|
+
size: stats.size,
|
|
1661
|
+
modifiedAt: stats.mtime.toISOString(),
|
|
1662
|
+
etag,
|
|
1663
|
+
format,
|
|
1664
|
+
path: filePath
|
|
1665
|
+
};
|
|
1666
|
+
} catch (error) {
|
|
1667
|
+
this.logger?.error("Failed to stat file", void 0, {
|
|
1668
|
+
type,
|
|
1669
|
+
name,
|
|
1670
|
+
filePath,
|
|
1671
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1672
|
+
});
|
|
1673
|
+
return null;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
async list(type) {
|
|
1677
|
+
const typeDir = path.join(this.rootDir, type);
|
|
1678
|
+
try {
|
|
1679
|
+
const files = await (0, import_glob.glob)("**/*", {
|
|
1680
|
+
cwd: typeDir,
|
|
1681
|
+
ignore: ["**/node_modules/**", "**/*.test.*", "**/*.spec.*"],
|
|
1682
|
+
nodir: true
|
|
1683
|
+
});
|
|
1684
|
+
return files.map((file) => {
|
|
1685
|
+
const ext = path.extname(file);
|
|
1686
|
+
const basename3 = path.basename(file, ext);
|
|
1687
|
+
return basename3;
|
|
1688
|
+
});
|
|
1689
|
+
} catch (error) {
|
|
1690
|
+
this.logger?.error("Failed to list", void 0, {
|
|
1691
|
+
type,
|
|
1692
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1693
|
+
});
|
|
1694
|
+
return [];
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
async save(type, name, data, options) {
|
|
1698
|
+
const startTime = Date.now();
|
|
1699
|
+
const {
|
|
1700
|
+
format = "typescript",
|
|
1701
|
+
prettify = true,
|
|
1702
|
+
indent = 2,
|
|
1703
|
+
sortKeys = false,
|
|
1704
|
+
backup = false,
|
|
1705
|
+
overwrite = true,
|
|
1706
|
+
atomic = true,
|
|
1707
|
+
path: customPath
|
|
1708
|
+
} = options || {};
|
|
1709
|
+
try {
|
|
1710
|
+
const serializer = this.getSerializer(format);
|
|
1711
|
+
if (!serializer) {
|
|
1712
|
+
throw new Error(`No serializer found for format: ${format}`);
|
|
1713
|
+
}
|
|
1714
|
+
const typeDir = path.join(this.rootDir, type);
|
|
1715
|
+
const fileName = `${name}${serializer.getExtension()}`;
|
|
1716
|
+
const filePath = customPath || path.join(typeDir, fileName);
|
|
1717
|
+
if (!overwrite) {
|
|
1718
|
+
try {
|
|
1719
|
+
await fs.access(filePath);
|
|
1720
|
+
throw new Error(`File already exists: ${filePath}`);
|
|
1721
|
+
} catch (error) {
|
|
1722
|
+
if (error.code !== "ENOENT") {
|
|
1723
|
+
throw error;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
1728
|
+
let backupPath;
|
|
1729
|
+
if (backup) {
|
|
1730
|
+
try {
|
|
1731
|
+
await fs.access(filePath);
|
|
1732
|
+
backupPath = `${filePath}.bak`;
|
|
1733
|
+
await fs.copyFile(filePath, backupPath);
|
|
1734
|
+
} catch {
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
const content = serializer.serialize(data, {
|
|
1738
|
+
prettify,
|
|
1739
|
+
indent,
|
|
1740
|
+
sortKeys
|
|
1741
|
+
});
|
|
1742
|
+
if (atomic) {
|
|
1743
|
+
const tempPath = `${filePath}.tmp`;
|
|
1744
|
+
await fs.writeFile(tempPath, content, "utf-8");
|
|
1745
|
+
await fs.rename(tempPath, filePath);
|
|
1746
|
+
} else {
|
|
1747
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
1748
|
+
}
|
|
1749
|
+
return {
|
|
1750
|
+
success: true,
|
|
1751
|
+
path: filePath,
|
|
1752
|
+
// format, // Not in schema
|
|
1753
|
+
size: Buffer.byteLength(content, "utf-8"),
|
|
1754
|
+
backupPath,
|
|
1755
|
+
saveTime: Date.now() - startTime
|
|
1756
|
+
};
|
|
1757
|
+
} catch (error) {
|
|
1758
|
+
this.logger?.error("Failed to save metadata", void 0, {
|
|
1759
|
+
type,
|
|
1760
|
+
name,
|
|
1761
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1762
|
+
});
|
|
1763
|
+
throw error;
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* Find file for a given type and name
|
|
1768
|
+
*/
|
|
1769
|
+
async findFile(type, name) {
|
|
1770
|
+
const typeDir = path.join(this.rootDir, type);
|
|
1771
|
+
const extensions = [".json", ".yaml", ".yml", ".ts", ".js"];
|
|
1772
|
+
for (const ext of extensions) {
|
|
1773
|
+
const filePath = path.join(typeDir, `${name}${ext}`);
|
|
1774
|
+
try {
|
|
1775
|
+
await fs.access(filePath);
|
|
1776
|
+
return filePath;
|
|
1777
|
+
} catch {
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
return null;
|
|
1781
|
+
}
|
|
1782
|
+
/**
|
|
1783
|
+
* Detect format from file extension
|
|
1784
|
+
*/
|
|
1785
|
+
detectFormat(filePath) {
|
|
1786
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1787
|
+
switch (ext) {
|
|
1788
|
+
case ".json":
|
|
1789
|
+
return "json";
|
|
1790
|
+
case ".yaml":
|
|
1791
|
+
case ".yml":
|
|
1792
|
+
return "yaml";
|
|
1793
|
+
case ".ts":
|
|
1794
|
+
return "typescript";
|
|
1795
|
+
case ".js":
|
|
1796
|
+
return "javascript";
|
|
1797
|
+
default:
|
|
1798
|
+
return "json";
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
/**
|
|
1802
|
+
* Get serializer for format
|
|
1803
|
+
*/
|
|
1804
|
+
getSerializer(format) {
|
|
1805
|
+
return this.serializers.get(format);
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Generate ETag for content
|
|
1809
|
+
* Uses SHA-256 hash truncated to 32 characters for reasonable collision resistance
|
|
1810
|
+
* while keeping ETag headers compact (full 64-char hash is overkill for this use case)
|
|
1811
|
+
*/
|
|
1812
|
+
generateETag(content) {
|
|
1813
|
+
const hash = (0, import_node_crypto.createHash)("sha256").update(content).digest("hex").substring(0, 32);
|
|
1814
|
+
return `"${hash}"`;
|
|
1815
|
+
}
|
|
1816
|
+
};
|
|
1817
|
+
|
|
1818
|
+
// src/node-metadata-manager.ts
|
|
1819
|
+
var NodeMetadataManager = class extends MetadataManager {
|
|
1820
|
+
constructor(config) {
|
|
1821
|
+
super(config);
|
|
1822
|
+
if (!config.loaders || config.loaders.length === 0) {
|
|
1823
|
+
const rootDir = config.rootDir || process.cwd();
|
|
1824
|
+
this.registerLoader(new FilesystemLoader(rootDir, this.serializers, this.logger));
|
|
1825
|
+
}
|
|
1826
|
+
if (config.watch) {
|
|
1827
|
+
this.startWatching();
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
/**
|
|
1831
|
+
* Stop all watching
|
|
1832
|
+
*/
|
|
1833
|
+
async stopWatching() {
|
|
1834
|
+
if (this.watcher) {
|
|
1835
|
+
await this.watcher.close();
|
|
1836
|
+
this.watcher = void 0;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
/**
|
|
1840
|
+
* Start watching for file changes
|
|
1841
|
+
*/
|
|
1842
|
+
startWatching() {
|
|
1843
|
+
const rootDir = this.config.rootDir || process.cwd();
|
|
1844
|
+
const { ignored = ["**/node_modules/**", "**/*.test.*"], persistent = true } = this.config.watchOptions || {};
|
|
1845
|
+
this.watcher = (0, import_chokidar.watch)(rootDir, {
|
|
1846
|
+
ignored,
|
|
1847
|
+
persistent,
|
|
1848
|
+
ignoreInitial: true
|
|
1849
|
+
});
|
|
1850
|
+
this.watcher.on("add", async (filePath) => {
|
|
1851
|
+
await this.handleFileEvent("added", filePath);
|
|
1852
|
+
});
|
|
1853
|
+
this.watcher.on("change", async (filePath) => {
|
|
1854
|
+
await this.handleFileEvent("changed", filePath);
|
|
1855
|
+
});
|
|
1856
|
+
this.watcher.on("unlink", async (filePath) => {
|
|
1857
|
+
await this.handleFileEvent("deleted", filePath);
|
|
1858
|
+
});
|
|
1859
|
+
this.logger.info("File watcher started", { rootDir });
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* Handle file change events
|
|
1863
|
+
*/
|
|
1864
|
+
async handleFileEvent(eventType, filePath) {
|
|
1865
|
+
const rootDir = this.config.rootDir || process.cwd();
|
|
1866
|
+
const relativePath = path2.relative(rootDir, filePath);
|
|
1867
|
+
const parts = relativePath.split(path2.sep);
|
|
1868
|
+
if (parts.length < 2) {
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
const type = parts[0];
|
|
1872
|
+
const fileName = parts[parts.length - 1];
|
|
1873
|
+
const name = path2.basename(fileName, path2.extname(fileName));
|
|
1874
|
+
let data = void 0;
|
|
1875
|
+
if (eventType !== "deleted") {
|
|
1876
|
+
try {
|
|
1877
|
+
data = await this.load(type, name, { useCache: false });
|
|
1878
|
+
} catch (error) {
|
|
1879
|
+
this.logger.error("Failed to load changed file", void 0, {
|
|
1880
|
+
filePath,
|
|
1881
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1882
|
+
});
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
const event = {
|
|
1887
|
+
type: eventType,
|
|
1888
|
+
metadataType: type,
|
|
1889
|
+
name,
|
|
1890
|
+
path: filePath,
|
|
1891
|
+
data,
|
|
1892
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1893
|
+
};
|
|
1894
|
+
this.notifyWatchers(type, event);
|
|
1895
|
+
}
|
|
1896
|
+
};
|
|
1897
|
+
|
|
1898
|
+
// src/plugin.ts
|
|
1899
|
+
var import_kernel = require("@objectstack/spec/kernel");
|
|
1900
|
+
var MetadataPlugin = class {
|
|
1901
|
+
constructor(options = {}) {
|
|
1902
|
+
this.name = "com.objectstack.metadata";
|
|
1903
|
+
this.type = "standard";
|
|
1904
|
+
this.version = "1.0.0";
|
|
1905
|
+
this.init = async (ctx) => {
|
|
1906
|
+
ctx.logger.info("Initializing Metadata Manager", {
|
|
1907
|
+
root: this.options.rootDir || process.cwd(),
|
|
1908
|
+
watch: this.options.watch
|
|
1909
|
+
});
|
|
1910
|
+
ctx.registerService("metadata", this.manager);
|
|
1911
|
+
ctx.registerService("app.com.objectstack.metadata", {
|
|
1912
|
+
id: "com.objectstack.metadata",
|
|
1913
|
+
name: "Metadata",
|
|
1914
|
+
version: "1.0.0",
|
|
1915
|
+
type: "plugin",
|
|
1916
|
+
namespace: "sys",
|
|
1917
|
+
objects: [SysMetadataObject]
|
|
1918
|
+
});
|
|
1919
|
+
ctx.logger.info("MetadataPlugin providing metadata service (primary mode)", {
|
|
1920
|
+
mode: "file-system",
|
|
1921
|
+
features: ["watch", "persistence", "multi-format", "query", "overlay", "type-registry"]
|
|
1922
|
+
});
|
|
1923
|
+
};
|
|
1924
|
+
this.start = async (ctx) => {
|
|
1925
|
+
ctx.logger.info("Loading metadata from file system...");
|
|
1926
|
+
const sortedTypes = [...import_kernel.DEFAULT_METADATA_TYPE_REGISTRY].sort((a, b) => a.loadOrder - b.loadOrder);
|
|
1927
|
+
let totalLoaded = 0;
|
|
1928
|
+
for (const entry of sortedTypes) {
|
|
1929
|
+
try {
|
|
1930
|
+
const items = await this.manager.loadMany(entry.type, {
|
|
1931
|
+
recursive: true
|
|
1932
|
+
});
|
|
1933
|
+
if (items.length > 0) {
|
|
1934
|
+
for (const item of items) {
|
|
1935
|
+
const meta = item;
|
|
1936
|
+
if (meta?.name) {
|
|
1937
|
+
await this.manager.register(entry.type, meta.name, item);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
ctx.logger.info(`Loaded ${items.length} ${entry.type} from file system`);
|
|
1941
|
+
totalLoaded += items.length;
|
|
1942
|
+
}
|
|
1943
|
+
} catch (e) {
|
|
1944
|
+
ctx.logger.debug(`No ${entry.type} metadata found`, { error: e.message });
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
ctx.logger.info("Metadata loading complete", {
|
|
1948
|
+
totalItems: totalLoaded,
|
|
1949
|
+
registeredTypes: sortedTypes.length
|
|
1950
|
+
});
|
|
1951
|
+
};
|
|
1952
|
+
this.options = {
|
|
1953
|
+
watch: true,
|
|
1954
|
+
...options
|
|
1955
|
+
};
|
|
1956
|
+
const rootDir = this.options.rootDir || process.cwd();
|
|
1957
|
+
this.manager = new NodeMetadataManager({
|
|
1958
|
+
rootDir,
|
|
1959
|
+
watch: this.options.watch ?? true,
|
|
1960
|
+
formats: ["yaml", "json", "typescript", "javascript"]
|
|
1961
|
+
});
|
|
1962
|
+
this.manager.setTypeRegistry(import_kernel.DEFAULT_METADATA_TYPE_REGISTRY);
|
|
1963
|
+
}
|
|
1964
|
+
};
|
|
1965
|
+
|
|
1966
|
+
// src/loaders/memory-loader.ts
|
|
1967
|
+
var MemoryLoader = class {
|
|
1968
|
+
constructor() {
|
|
1969
|
+
this.contract = {
|
|
1970
|
+
name: "memory",
|
|
1971
|
+
protocol: "memory:",
|
|
1972
|
+
capabilities: {
|
|
1973
|
+
read: true,
|
|
1974
|
+
write: true,
|
|
1975
|
+
watch: false,
|
|
1976
|
+
list: true
|
|
1977
|
+
}
|
|
1978
|
+
};
|
|
1979
|
+
// Storage: Type -> Name -> Data
|
|
1980
|
+
this.storage = /* @__PURE__ */ new Map();
|
|
1981
|
+
}
|
|
1982
|
+
async load(type, name, _options) {
|
|
1983
|
+
const typeStore = this.storage.get(type);
|
|
1984
|
+
const data = typeStore?.get(name);
|
|
1985
|
+
if (data) {
|
|
1986
|
+
return {
|
|
1987
|
+
data,
|
|
1988
|
+
source: "memory",
|
|
1989
|
+
format: "json",
|
|
1990
|
+
loadTime: 0
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
return { data: null };
|
|
1994
|
+
}
|
|
1995
|
+
async loadMany(type, _options) {
|
|
1996
|
+
const typeStore = this.storage.get(type);
|
|
1997
|
+
if (!typeStore) return [];
|
|
1998
|
+
return Array.from(typeStore.values());
|
|
1999
|
+
}
|
|
2000
|
+
async exists(type, name) {
|
|
2001
|
+
return this.storage.get(type)?.has(name) ?? false;
|
|
2002
|
+
}
|
|
2003
|
+
async stat(type, name) {
|
|
2004
|
+
if (await this.exists(type, name)) {
|
|
2005
|
+
return {
|
|
2006
|
+
size: 0,
|
|
2007
|
+
// In-memory
|
|
2008
|
+
mtime: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2009
|
+
format: "json"
|
|
2010
|
+
};
|
|
2011
|
+
}
|
|
2012
|
+
return null;
|
|
2013
|
+
}
|
|
2014
|
+
async list(type) {
|
|
2015
|
+
const typeStore = this.storage.get(type);
|
|
2016
|
+
if (!typeStore) return [];
|
|
2017
|
+
return Array.from(typeStore.keys());
|
|
2018
|
+
}
|
|
2019
|
+
async save(type, name, data, _options) {
|
|
2020
|
+
if (!this.storage.has(type)) {
|
|
2021
|
+
this.storage.set(type, /* @__PURE__ */ new Map());
|
|
2022
|
+
}
|
|
2023
|
+
this.storage.get(type).set(name, data);
|
|
2024
|
+
return {
|
|
2025
|
+
success: true,
|
|
2026
|
+
path: `memory://${type}/${name}`,
|
|
2027
|
+
saveTime: 0
|
|
2028
|
+
};
|
|
2029
|
+
}
|
|
2030
|
+
};
|
|
2031
|
+
|
|
2032
|
+
// src/loaders/remote-loader.ts
|
|
2033
|
+
var RemoteLoader = class {
|
|
2034
|
+
constructor(baseUrl, authToken) {
|
|
2035
|
+
this.baseUrl = baseUrl;
|
|
2036
|
+
this.authToken = authToken;
|
|
2037
|
+
this.contract = {
|
|
2038
|
+
name: "remote",
|
|
2039
|
+
protocol: "http:",
|
|
2040
|
+
capabilities: {
|
|
2041
|
+
read: true,
|
|
2042
|
+
write: true,
|
|
2043
|
+
watch: false,
|
|
2044
|
+
// Could implement SSE/WebSocket in future
|
|
2045
|
+
list: true
|
|
2046
|
+
}
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
get headers() {
|
|
2050
|
+
return {
|
|
2051
|
+
"Content-Type": "application/json",
|
|
2052
|
+
...this.authToken ? { Authorization: `Bearer ${this.authToken}` } : {}
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
async load(type, name, _options) {
|
|
2056
|
+
try {
|
|
2057
|
+
const response = await fetch(`${this.baseUrl}/${type}/${name}`, {
|
|
2058
|
+
method: "GET",
|
|
2059
|
+
headers: this.headers
|
|
2060
|
+
});
|
|
2061
|
+
if (response.status === 404) {
|
|
2062
|
+
return { data: null };
|
|
2063
|
+
}
|
|
2064
|
+
if (!response.ok) {
|
|
2065
|
+
throw new Error(`Remote load failed: ${response.statusText}`);
|
|
2066
|
+
}
|
|
2067
|
+
const data = await response.json();
|
|
2068
|
+
return {
|
|
2069
|
+
data,
|
|
2070
|
+
source: this.baseUrl,
|
|
2071
|
+
format: "json",
|
|
2072
|
+
loadTime: 0
|
|
2073
|
+
};
|
|
2074
|
+
} catch (error) {
|
|
2075
|
+
console.error(`RemoteLoader error loading ${type}/${name}`, error);
|
|
2076
|
+
throw error;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
async loadMany(type, _options) {
|
|
2080
|
+
const response = await fetch(`${this.baseUrl}/${type}`, {
|
|
2081
|
+
method: "GET",
|
|
2082
|
+
headers: this.headers
|
|
2083
|
+
});
|
|
2084
|
+
if (!response.ok) {
|
|
2085
|
+
return [];
|
|
2086
|
+
}
|
|
2087
|
+
return await response.json();
|
|
2088
|
+
}
|
|
2089
|
+
async exists(type, name) {
|
|
2090
|
+
const response = await fetch(`${this.baseUrl}/${type}/${name}`, {
|
|
2091
|
+
method: "HEAD",
|
|
2092
|
+
headers: this.headers
|
|
2093
|
+
});
|
|
2094
|
+
return response.ok;
|
|
2095
|
+
}
|
|
2096
|
+
async stat(type, name) {
|
|
2097
|
+
const response = await fetch(`${this.baseUrl}/${type}/${name}`, {
|
|
2098
|
+
method: "HEAD",
|
|
2099
|
+
headers: this.headers
|
|
2100
|
+
});
|
|
2101
|
+
if (!response.ok) return null;
|
|
2102
|
+
return {
|
|
2103
|
+
size: Number(response.headers.get("content-length") || 0),
|
|
2104
|
+
mtime: new Date(response.headers.get("last-modified") || Date.now()).toISOString(),
|
|
2105
|
+
format: "json"
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
async list(type) {
|
|
2109
|
+
const items = await this.loadMany(type);
|
|
2110
|
+
return items.map((i) => i.name);
|
|
2111
|
+
}
|
|
2112
|
+
async save(type, name, data, _options) {
|
|
2113
|
+
const response = await fetch(`${this.baseUrl}/${type}/${name}`, {
|
|
2114
|
+
method: "PUT",
|
|
2115
|
+
headers: this.headers,
|
|
2116
|
+
body: JSON.stringify(data)
|
|
2117
|
+
});
|
|
2118
|
+
if (!response.ok) {
|
|
2119
|
+
throw new Error(`Remote save failed: ${response.statusText}`);
|
|
2120
|
+
}
|
|
2121
|
+
return {
|
|
2122
|
+
success: true,
|
|
2123
|
+
path: `${this.baseUrl}/${type}/${name}`,
|
|
2124
|
+
saveTime: 0
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
};
|
|
2128
|
+
|
|
2129
|
+
// src/migration/index.ts
|
|
2130
|
+
var migration_exports = {};
|
|
2131
|
+
__export(migration_exports, {
|
|
2132
|
+
MigrationExecutor: () => MigrationExecutor
|
|
2133
|
+
});
|
|
2134
|
+
|
|
2135
|
+
// src/migration/executor.ts
|
|
2136
|
+
var MigrationExecutor = class {
|
|
2137
|
+
constructor(driver) {
|
|
2138
|
+
this.driver = driver;
|
|
2139
|
+
}
|
|
2140
|
+
async executeChangeSet(changeSet) {
|
|
2141
|
+
console.log(`Executing ChangeSet: ${changeSet.name} (${changeSet.id})`);
|
|
2142
|
+
for (const op of changeSet.operations) {
|
|
2143
|
+
try {
|
|
2144
|
+
await this.executeOperation(op);
|
|
2145
|
+
} catch (e) {
|
|
2146
|
+
console.error(`Failed to execute operation ${op.type}:`, e);
|
|
2147
|
+
throw e;
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
async executeOperation(op) {
|
|
2152
|
+
switch (op.type) {
|
|
2153
|
+
case "create_object":
|
|
2154
|
+
console.log(` > Create Object: ${op.object.name}`);
|
|
2155
|
+
await this.driver.createCollection(op.object.name, op.object);
|
|
2156
|
+
break;
|
|
2157
|
+
case "add_field":
|
|
2158
|
+
console.log(` > Add Field: ${op.objectName}.${op.fieldName}`);
|
|
2159
|
+
await this.driver.addColumn(op.objectName, op.fieldName, op.field);
|
|
2160
|
+
break;
|
|
2161
|
+
case "remove_field":
|
|
2162
|
+
console.log(` > Remove Field: ${op.objectName}.${op.fieldName}`);
|
|
2163
|
+
await this.driver.dropColumn(op.objectName, op.fieldName);
|
|
2164
|
+
break;
|
|
2165
|
+
case "delete_object":
|
|
2166
|
+
console.log(` > Delete Object: ${op.objectName}`);
|
|
2167
|
+
await this.driver.dropCollection(op.objectName);
|
|
2168
|
+
break;
|
|
2169
|
+
case "execute_sql":
|
|
2170
|
+
console.log(` > Execute SQL`);
|
|
2171
|
+
await this.driver.executeRaw(op.sql);
|
|
2172
|
+
break;
|
|
2173
|
+
case "modify_field":
|
|
2174
|
+
console.warn(` ! Modify Field: ${op.objectName}.${op.fieldName} (Not fully implemented)`);
|
|
2175
|
+
break;
|
|
2176
|
+
case "rename_object":
|
|
2177
|
+
console.warn(` ! Rename Object: ${op.oldName} -> ${op.newName} (Not fully implemented)`);
|
|
2178
|
+
break;
|
|
2179
|
+
default:
|
|
2180
|
+
throw new Error(`Unknown operation type`);
|
|
2181
|
+
}
|
|
2182
|
+
}
|
|
2183
|
+
};
|
|
2184
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2185
|
+
0 && (module.exports = {
|
|
2186
|
+
DatabaseLoader,
|
|
2187
|
+
JSONSerializer,
|
|
2188
|
+
MemoryLoader,
|
|
2189
|
+
MetadataManager,
|
|
2190
|
+
MetadataPlugin,
|
|
2191
|
+
Migration,
|
|
2192
|
+
RemoteLoader,
|
|
2193
|
+
SysMetadataObject,
|
|
2194
|
+
TypeScriptSerializer,
|
|
2195
|
+
YAMLSerializer
|
|
2196
|
+
});
|
|
2197
|
+
//# sourceMappingURL=index.cjs.map
|