@utilarium/overcontext 0.0.4-dev.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/LICENSE +65 -0
- package/README.md +286 -0
- package/dist/api/builder.d.ts +26 -0
- package/dist/api/builder.js +24 -0
- package/dist/api/builder.js.map +1 -0
- package/dist/api/context.d.ts +90 -0
- package/dist/api/context.js +94 -0
- package/dist/api/context.js.map +1 -0
- package/dist/api/index.d.ts +6 -0
- package/dist/api/query-builder.d.ts +51 -0
- package/dist/api/query-builder.js +95 -0
- package/dist/api/query-builder.js.map +1 -0
- package/dist/api/query.d.ts +32 -0
- package/dist/api/search.d.ts +20 -0
- package/dist/api/search.js +112 -0
- package/dist/api/search.js.map +1 -0
- package/dist/api/slug.d.ts +10 -0
- package/dist/api/slug.js +55 -0
- package/dist/api/slug.js.map +1 -0
- package/dist/cli/builder.d.ts +74 -0
- package/dist/cli/builder.js +42 -0
- package/dist/cli/builder.js.map +1 -0
- package/dist/cli/commands.d.ts +53 -0
- package/dist/cli/commands.js +57 -0
- package/dist/cli/commands.js.map +1 -0
- package/dist/cli/formatters.d.ts +15 -0
- package/dist/cli/formatters.js +50 -0
- package/dist/cli/formatters.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/discovery/context-root.d.ts +31 -0
- package/dist/discovery/context-root.js +48 -0
- package/dist/discovery/context-root.js.map +1 -0
- package/dist/discovery/hierarchical-provider.d.ts +13 -0
- package/dist/discovery/hierarchical-provider.js +102 -0
- package/dist/discovery/hierarchical-provider.js.map +1 -0
- package/dist/discovery/index.d.ts +18 -0
- package/dist/discovery/index.js +47 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/discovery/walker.d.ts +36 -0
- package/dist/discovery/walker.js +87 -0
- package/dist/discovery/walker.js.map +1 -0
- package/dist/index.cjs +1763 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/namespace/index.d.ts +3 -0
- package/dist/namespace/multi-context.d.ts +40 -0
- package/dist/namespace/multi-context.js +72 -0
- package/dist/namespace/multi-context.js.map +1 -0
- package/dist/namespace/resolver.d.ts +33 -0
- package/dist/namespace/resolver.js +85 -0
- package/dist/namespace/resolver.js.map +1 -0
- package/dist/namespace/types.d.ts +41 -0
- package/dist/overcontext.d.ts +7 -0
- package/dist/schema/base.d.ts +29 -0
- package/dist/schema/base.js +24 -0
- package/dist/schema/base.js.map +1 -0
- package/dist/schema/builder.d.ts +28 -0
- package/dist/schema/builder.js +39 -0
- package/dist/schema/builder.js.map +1 -0
- package/dist/schema/index.d.ts +5 -0
- package/dist/schema/inference.d.ts +49 -0
- package/dist/schema/inference.js +15 -0
- package/dist/schema/inference.js.map +1 -0
- package/dist/schema/registry.d.ts +74 -0
- package/dist/schema/registry.js +117 -0
- package/dist/schema/registry.js.map +1 -0
- package/dist/schema/validation.d.ts +26 -0
- package/dist/schema/validation.js +51 -0
- package/dist/schema/validation.js.map +1 -0
- package/dist/storage/errors.d.ts +35 -0
- package/dist/storage/errors.js +58 -0
- package/dist/storage/errors.js.map +1 -0
- package/dist/storage/events.d.ts +50 -0
- package/dist/storage/filesystem.d.ts +10 -0
- package/dist/storage/filesystem.js +284 -0
- package/dist/storage/filesystem.js.map +1 -0
- package/dist/storage/index.d.ts +6 -0
- package/dist/storage/interface.d.ts +109 -0
- package/dist/storage/memory.d.ts +7 -0
- package/dist/storage/memory.js +128 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/storage/observable.d.ts +6 -0
- package/dist/storage/observable.js +98 -0
- package/dist/storage/observable.js.map +1 -0
- package/package.json +85 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1763 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
4
|
+
|
|
5
|
+
const zod = require('zod');
|
|
6
|
+
const fs = require('node:fs/promises');
|
|
7
|
+
const node_fs = require('node:fs');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
const yaml = require('js-yaml');
|
|
10
|
+
|
|
11
|
+
function _interopNamespaceDefault(e) {
|
|
12
|
+
const n = Object.create(null, { [Symbol.toStringTag]: { value: 'Module' } });
|
|
13
|
+
if (e) {
|
|
14
|
+
for (const k in e) {
|
|
15
|
+
if (k !== 'default') {
|
|
16
|
+
const d = Object.getOwnPropertyDescriptor(e, k);
|
|
17
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
get: () => e[k]
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
n.default = e;
|
|
25
|
+
return Object.freeze(n);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
|
|
29
|
+
const path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
|
|
30
|
+
const yaml__namespace = /*#__PURE__*/_interopNamespaceDefault(yaml);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Metadata that overcontext manages automatically.
|
|
34
|
+
* Consumers don't need to define these.
|
|
35
|
+
*/ const EntityMetadataSchema = zod.z.object({
|
|
36
|
+
createdAt: zod.z.date().optional(),
|
|
37
|
+
updatedAt: zod.z.date().optional(),
|
|
38
|
+
createdBy: zod.z.string().optional(),
|
|
39
|
+
namespace: zod.z.string().optional(),
|
|
40
|
+
source: zod.z.string().optional()
|
|
41
|
+
});
|
|
42
|
+
/**
|
|
43
|
+
* The minimal contract every entity must satisfy.
|
|
44
|
+
* Consuming libraries extend this with their own fields.
|
|
45
|
+
*/ const BaseEntitySchema = zod.z.object({
|
|
46
|
+
/** Unique identifier within the entity type (used as filename) */ id: zod.z.string().min(1),
|
|
47
|
+
/** Human-readable name (used for display and search) */ name: zod.z.string().min(1),
|
|
48
|
+
/** Entity type discriminator (must be a string literal in extensions) */ type: zod.z.string().min(1),
|
|
49
|
+
/** Optional notes - common enough to include in base */ notes: zod.z.string().optional()
|
|
50
|
+
}).merge(EntityMetadataSchema);
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Helper to create a properly typed entity schema.
|
|
54
|
+
* Ensures the schema extends BaseEntitySchema.
|
|
55
|
+
*/ const createEntitySchema = (typeName, extension)=>{
|
|
56
|
+
return BaseEntitySchema.extend({
|
|
57
|
+
type: zod.z.literal(typeName),
|
|
58
|
+
...extension
|
|
59
|
+
});
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Validate that an object satisfies at least the base entity contract.
|
|
64
|
+
*/ const validateBaseEntity = (data)=>{
|
|
65
|
+
const result = BaseEntitySchema.safeParse(data);
|
|
66
|
+
if (result.success) {
|
|
67
|
+
return {
|
|
68
|
+
success: true,
|
|
69
|
+
data: result.data
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
success: false,
|
|
74
|
+
errors: result.error.issues.map((e)=>({
|
|
75
|
+
path: e.path.join('.'),
|
|
76
|
+
message: e.message
|
|
77
|
+
}))
|
|
78
|
+
};
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Validate data against a specific schema.
|
|
82
|
+
*/ const validateEntity = (schema, data)=>{
|
|
83
|
+
const result = schema.safeParse(data);
|
|
84
|
+
if (result.success) {
|
|
85
|
+
return {
|
|
86
|
+
success: true,
|
|
87
|
+
data: result.data
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
success: false,
|
|
92
|
+
errors: result.error.issues.map((e)=>({
|
|
93
|
+
path: e.path.join('.'),
|
|
94
|
+
message: e.message
|
|
95
|
+
}))
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Check if data extends the base entity (has id, name, type).
|
|
100
|
+
*/ const isBaseEntity = (data)=>{
|
|
101
|
+
return validateBaseEntity(data).success;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Format Zod errors into a readable message.
|
|
105
|
+
*/ const formatValidationErrors = (errors)=>{
|
|
106
|
+
return errors.issues.map((e)=>`${e.path.join('.')}: ${e.message}`).join('; ');
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Default plural name derivation.
|
|
111
|
+
*/ const derivePluralName = (type)=>{
|
|
112
|
+
// Simple pluralization rules
|
|
113
|
+
if (type.endsWith('y')) {
|
|
114
|
+
return type.slice(0, -1) + 'ies';
|
|
115
|
+
}
|
|
116
|
+
if (type.endsWith('s') || type.endsWith('x') || type.endsWith('ch') || type.endsWith('sh')) {
|
|
117
|
+
return type + 'es';
|
|
118
|
+
}
|
|
119
|
+
return type + 's';
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Create a new schema registry.
|
|
123
|
+
*/ const createSchemaRegistry = ()=>{
|
|
124
|
+
const schemas = new Map();
|
|
125
|
+
const directoryToType = new Map();
|
|
126
|
+
const register = (options)=>{
|
|
127
|
+
const { type, schema, pluralName, customValidator } = options;
|
|
128
|
+
const plural = pluralName || derivePluralName(type);
|
|
129
|
+
const registered = {
|
|
130
|
+
type,
|
|
131
|
+
schema,
|
|
132
|
+
pluralName: plural,
|
|
133
|
+
customValidator
|
|
134
|
+
};
|
|
135
|
+
schemas.set(type, registered);
|
|
136
|
+
directoryToType.set(plural, type);
|
|
137
|
+
};
|
|
138
|
+
const registerAll = (schemaMap)=>{
|
|
139
|
+
for (const [type, schema] of Object.entries(schemaMap)){
|
|
140
|
+
register({
|
|
141
|
+
type,
|
|
142
|
+
schema
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const get = (type)=>{
|
|
147
|
+
return schemas.get(type);
|
|
148
|
+
};
|
|
149
|
+
const has = (type)=>{
|
|
150
|
+
return schemas.has(type);
|
|
151
|
+
};
|
|
152
|
+
const types = ()=>{
|
|
153
|
+
return Array.from(schemas.keys());
|
|
154
|
+
};
|
|
155
|
+
const getDirectoryName = (type)=>{
|
|
156
|
+
var _schemas_get;
|
|
157
|
+
return (_schemas_get = schemas.get(type)) === null || _schemas_get === void 0 ? void 0 : _schemas_get.pluralName;
|
|
158
|
+
};
|
|
159
|
+
const getTypeFromDirectory = (directory)=>{
|
|
160
|
+
return directoryToType.get(directory);
|
|
161
|
+
};
|
|
162
|
+
const validate = (entity)=>{
|
|
163
|
+
const registered = schemas.get(entity.type);
|
|
164
|
+
if (!registered) {
|
|
165
|
+
return {
|
|
166
|
+
success: false,
|
|
167
|
+
errors: [
|
|
168
|
+
{
|
|
169
|
+
path: 'type',
|
|
170
|
+
message: `Unknown entity type: ${entity.type}`
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
// Schema validation
|
|
176
|
+
const schemaResult = validateEntity(registered.schema, entity);
|
|
177
|
+
if (!schemaResult.success) {
|
|
178
|
+
return schemaResult;
|
|
179
|
+
}
|
|
180
|
+
// Custom validation
|
|
181
|
+
if (registered.customValidator) {
|
|
182
|
+
return registered.customValidator(entity);
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
success: true,
|
|
186
|
+
data: entity
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
const validateAs = (type, data)=>{
|
|
190
|
+
const registered = schemas.get(type);
|
|
191
|
+
if (!registered) {
|
|
192
|
+
return {
|
|
193
|
+
success: false,
|
|
194
|
+
errors: [
|
|
195
|
+
{
|
|
196
|
+
path: 'type',
|
|
197
|
+
message: `Unknown entity type: ${type}`
|
|
198
|
+
}
|
|
199
|
+
]
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
// Add type to data if missing (for convenience when loading from files)
|
|
203
|
+
const withType = typeof data === 'object' && data !== null ? {
|
|
204
|
+
...data,
|
|
205
|
+
type
|
|
206
|
+
} : data;
|
|
207
|
+
return validateEntity(registered.schema, withType);
|
|
208
|
+
};
|
|
209
|
+
return {
|
|
210
|
+
register,
|
|
211
|
+
registerAll,
|
|
212
|
+
get,
|
|
213
|
+
has,
|
|
214
|
+
types,
|
|
215
|
+
getDirectoryName,
|
|
216
|
+
getTypeFromDirectory,
|
|
217
|
+
validate,
|
|
218
|
+
validateAs
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Create a schema map with proper type inference.
|
|
224
|
+
*
|
|
225
|
+
* @example
|
|
226
|
+
* const { schemas, types } = defineSchemas({
|
|
227
|
+
* person: PersonSchema,
|
|
228
|
+
* project: ProjectSchema,
|
|
229
|
+
* });
|
|
230
|
+
*
|
|
231
|
+
* type Person = typeof types.person; // Inferred from PersonSchema
|
|
232
|
+
*/ const defineSchemas = (schemas)=>{
|
|
233
|
+
return {
|
|
234
|
+
schemas,
|
|
235
|
+
types: {}
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
/**
|
|
239
|
+
* Helper to check if a schema extends BaseEntity properly.
|
|
240
|
+
*/ const isValidEntitySchema = (schema)=>{
|
|
241
|
+
try {
|
|
242
|
+
// BaseEntitySchema allows extra fields, so we just need id, name, type
|
|
243
|
+
const result = schema.safeParse({
|
|
244
|
+
id: 'test',
|
|
245
|
+
name: 'Test',
|
|
246
|
+
type: 'test'
|
|
247
|
+
});
|
|
248
|
+
// If it fails, it might be because type is a literal
|
|
249
|
+
// Try without type and check if it has the base structure
|
|
250
|
+
if (!result.success) {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
return true;
|
|
254
|
+
} catch {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
function _define_property$1(obj, key, value) {
|
|
260
|
+
if (key in obj) {
|
|
261
|
+
Object.defineProperty(obj, key, {
|
|
262
|
+
value: value,
|
|
263
|
+
enumerable: true,
|
|
264
|
+
configurable: true,
|
|
265
|
+
writable: true
|
|
266
|
+
});
|
|
267
|
+
} else {
|
|
268
|
+
obj[key] = value;
|
|
269
|
+
}
|
|
270
|
+
return obj;
|
|
271
|
+
}
|
|
272
|
+
class StorageError extends Error {
|
|
273
|
+
constructor(message, code, cause){
|
|
274
|
+
super(message), _define_property$1(this, "code", void 0), _define_property$1(this, "cause", void 0), this.code = code, this.cause = cause;
|
|
275
|
+
this.name = 'StorageError';
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
class EntityNotFoundError extends StorageError {
|
|
279
|
+
constructor(entityType, entityId, namespace){
|
|
280
|
+
super(`Entity not found: ${entityType}/${entityId}${namespace ? ` in ${namespace}` : ''}`, 'ENTITY_NOT_FOUND'), _define_property$1(this, "entityType", void 0), _define_property$1(this, "entityId", void 0), _define_property$1(this, "namespace", void 0), this.entityType = entityType, this.entityId = entityId, this.namespace = namespace;
|
|
281
|
+
this.name = 'EntityNotFoundError';
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
class SchemaNotRegisteredError extends StorageError {
|
|
285
|
+
constructor(entityType){
|
|
286
|
+
super(`Schema not registered for type: ${entityType}`, 'SCHEMA_NOT_REGISTERED'), _define_property$1(this, "entityType", void 0), this.entityType = entityType;
|
|
287
|
+
this.name = 'SchemaNotRegisteredError';
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
class ValidationError extends StorageError {
|
|
291
|
+
constructor(message, validationErrors){
|
|
292
|
+
super(message, 'VALIDATION_ERROR'), _define_property$1(this, "validationErrors", void 0), this.validationErrors = validationErrors;
|
|
293
|
+
this.name = 'ValidationError';
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
class StorageAccessError extends StorageError {
|
|
297
|
+
constructor(message, cause){
|
|
298
|
+
super(message, 'STORAGE_ACCESS_ERROR', cause);
|
|
299
|
+
this.name = 'StorageAccessError';
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
class ReadonlyStorageError extends StorageError {
|
|
303
|
+
constructor(){
|
|
304
|
+
super('Storage is readonly', 'READONLY_STORAGE');
|
|
305
|
+
this.name = 'ReadonlyStorageError';
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
class NamespaceNotFoundError extends StorageError {
|
|
309
|
+
constructor(namespace){
|
|
310
|
+
super(`Namespace not found: ${namespace}`, 'NAMESPACE_NOT_FOUND'), _define_property$1(this, "namespace", void 0), this.namespace = namespace;
|
|
311
|
+
this.name = 'NamespaceNotFoundError';
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Wrap any StorageProvider to make it observable.
|
|
317
|
+
*/ const createObservableProvider = (provider)=>{
|
|
318
|
+
const handlers = new Set();
|
|
319
|
+
const emit = (event)=>{
|
|
320
|
+
handlers.forEach((handler)=>{
|
|
321
|
+
try {
|
|
322
|
+
handler(event);
|
|
323
|
+
} catch (error) {
|
|
324
|
+
// eslint-disable-next-line no-console
|
|
325
|
+
console.error('Storage event handler error:', error);
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
return {
|
|
330
|
+
...provider,
|
|
331
|
+
subscribe (handler) {
|
|
332
|
+
handlers.add(handler);
|
|
333
|
+
return ()=>handlers.delete(handler);
|
|
334
|
+
},
|
|
335
|
+
async initialize () {
|
|
336
|
+
await provider.initialize();
|
|
337
|
+
emit({
|
|
338
|
+
type: 'storage:initialized',
|
|
339
|
+
timestamp: new Date()
|
|
340
|
+
});
|
|
341
|
+
},
|
|
342
|
+
async dispose () {
|
|
343
|
+
emit({
|
|
344
|
+
type: 'storage:disposed',
|
|
345
|
+
timestamp: new Date()
|
|
346
|
+
});
|
|
347
|
+
await provider.dispose();
|
|
348
|
+
},
|
|
349
|
+
async save (entity, namespace) {
|
|
350
|
+
const existing = await provider.get(entity.type, entity.id, namespace);
|
|
351
|
+
const saved = await provider.save(entity, namespace);
|
|
352
|
+
if (existing) {
|
|
353
|
+
emit({
|
|
354
|
+
type: 'entity:updated',
|
|
355
|
+
timestamp: new Date(),
|
|
356
|
+
namespace,
|
|
357
|
+
entityType: entity.type,
|
|
358
|
+
entityId: entity.id,
|
|
359
|
+
entity: saved,
|
|
360
|
+
previousEntity: existing
|
|
361
|
+
});
|
|
362
|
+
} else {
|
|
363
|
+
emit({
|
|
364
|
+
type: 'entity:created',
|
|
365
|
+
timestamp: new Date(),
|
|
366
|
+
namespace,
|
|
367
|
+
entityType: entity.type,
|
|
368
|
+
entityId: entity.id,
|
|
369
|
+
entity: saved
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
return saved;
|
|
373
|
+
},
|
|
374
|
+
async delete (type, id, namespace) {
|
|
375
|
+
const deleted = await provider.delete(type, id, namespace);
|
|
376
|
+
if (deleted) {
|
|
377
|
+
emit({
|
|
378
|
+
type: 'entity:deleted',
|
|
379
|
+
timestamp: new Date(),
|
|
380
|
+
namespace,
|
|
381
|
+
entityType: type,
|
|
382
|
+
entityId: id
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
return deleted;
|
|
386
|
+
},
|
|
387
|
+
async saveBatch (entities, namespace) {
|
|
388
|
+
const saved = await provider.saveBatch(entities, namespace);
|
|
389
|
+
emit({
|
|
390
|
+
type: 'batch:saved',
|
|
391
|
+
timestamp: new Date(),
|
|
392
|
+
namespace,
|
|
393
|
+
entities: saved
|
|
394
|
+
});
|
|
395
|
+
return saved;
|
|
396
|
+
},
|
|
397
|
+
async deleteBatch (refs, namespace) {
|
|
398
|
+
const count = await provider.deleteBatch(refs, namespace);
|
|
399
|
+
emit({
|
|
400
|
+
type: 'batch:deleted',
|
|
401
|
+
timestamp: new Date(),
|
|
402
|
+
namespace,
|
|
403
|
+
refs,
|
|
404
|
+
deletedCount: count
|
|
405
|
+
});
|
|
406
|
+
return count;
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const createFileSystemProvider = async (options)=>{
|
|
412
|
+
const { basePath, registry, createIfMissing = true, extension = '.yaml', readonly = false, defaultNamespace } = options;
|
|
413
|
+
// --- Helper Functions ---
|
|
414
|
+
const getEntityDir = (type, namespace)=>{
|
|
415
|
+
const dirName = registry.getDirectoryName(type);
|
|
416
|
+
if (!dirName) {
|
|
417
|
+
throw new SchemaNotRegisteredError(type);
|
|
418
|
+
}
|
|
419
|
+
if (namespace) {
|
|
420
|
+
return path__namespace.join(basePath, namespace, dirName);
|
|
421
|
+
}
|
|
422
|
+
return path__namespace.join(basePath, dirName);
|
|
423
|
+
};
|
|
424
|
+
const getEntityPath = (type, id, namespace)=>{
|
|
425
|
+
return path__namespace.join(getEntityDir(type, namespace), `${id}${extension}`);
|
|
426
|
+
};
|
|
427
|
+
const ensureDir = async (dir)=>{
|
|
428
|
+
if (!node_fs.existsSync(dir) && createIfMissing && !readonly) {
|
|
429
|
+
await fs__namespace.mkdir(dir, {
|
|
430
|
+
recursive: true
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
const readEntity = async (filePath, type)=>{
|
|
435
|
+
try {
|
|
436
|
+
const content = await fs__namespace.readFile(filePath, 'utf-8');
|
|
437
|
+
let parsed;
|
|
438
|
+
try {
|
|
439
|
+
parsed = yaml__namespace.load(content);
|
|
440
|
+
} catch (yamlError) {
|
|
441
|
+
// eslint-disable-next-line no-console
|
|
442
|
+
console.warn(`Invalid YAML at ${filePath}:`, yamlError);
|
|
443
|
+
return undefined;
|
|
444
|
+
}
|
|
445
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
// Validate against registered schema
|
|
449
|
+
const result = registry.validateAs(type, {
|
|
450
|
+
...parsed,
|
|
451
|
+
source: filePath
|
|
452
|
+
});
|
|
453
|
+
if (!result.success) {
|
|
454
|
+
// eslint-disable-next-line no-console
|
|
455
|
+
console.warn(`Invalid entity at ${filePath}:`, result.errors);
|
|
456
|
+
return undefined;
|
|
457
|
+
}
|
|
458
|
+
return result.data;
|
|
459
|
+
} catch (error) {
|
|
460
|
+
if (error.code === 'ENOENT') {
|
|
461
|
+
return undefined;
|
|
462
|
+
}
|
|
463
|
+
throw new StorageAccessError(`Failed to read ${filePath}`, error);
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
const writeEntity = async (entity, namespace)=>{
|
|
467
|
+
if (readonly) {
|
|
468
|
+
throw new ReadonlyStorageError();
|
|
469
|
+
}
|
|
470
|
+
// Validate against schema
|
|
471
|
+
const validationResult = registry.validate(entity);
|
|
472
|
+
if (!validationResult.success) {
|
|
473
|
+
throw new ValidationError('Entity validation failed', validationResult.errors || []);
|
|
474
|
+
}
|
|
475
|
+
const dir = getEntityDir(entity.type, namespace);
|
|
476
|
+
await ensureDir(dir);
|
|
477
|
+
const filePath = getEntityPath(entity.type, entity.id, namespace);
|
|
478
|
+
// Remove framework-managed fields from saved YAML
|
|
479
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
480
|
+
const { type: _type, source: _source, ...entityToSave } = entity;
|
|
481
|
+
// Update metadata
|
|
482
|
+
const now = new Date();
|
|
483
|
+
const toSave = {
|
|
484
|
+
...entityToSave,
|
|
485
|
+
updatedAt: now,
|
|
486
|
+
createdAt: entityToSave.createdAt || now
|
|
487
|
+
};
|
|
488
|
+
const content = yaml__namespace.dump(toSave, {
|
|
489
|
+
lineWidth: -1,
|
|
490
|
+
sortKeys: false
|
|
491
|
+
});
|
|
492
|
+
await fs__namespace.writeFile(filePath, content, 'utf-8');
|
|
493
|
+
return {
|
|
494
|
+
...entity,
|
|
495
|
+
...toSave,
|
|
496
|
+
type: entity.type,
|
|
497
|
+
source: filePath
|
|
498
|
+
};
|
|
499
|
+
};
|
|
500
|
+
const listDirectoryTypes = async (basePath)=>{
|
|
501
|
+
const types = [];
|
|
502
|
+
try {
|
|
503
|
+
const entries = await fs__namespace.readdir(basePath, {
|
|
504
|
+
withFileTypes: true
|
|
505
|
+
});
|
|
506
|
+
for (const entry of entries){
|
|
507
|
+
if (entry.isDirectory()) {
|
|
508
|
+
const type = registry.getTypeFromDirectory(entry.name);
|
|
509
|
+
if (type) {
|
|
510
|
+
types.push(type);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
} catch {
|
|
515
|
+
// Directory doesn't exist
|
|
516
|
+
}
|
|
517
|
+
return types;
|
|
518
|
+
};
|
|
519
|
+
// --- StorageProvider Implementation ---
|
|
520
|
+
const provider = {
|
|
521
|
+
name: 'filesystem',
|
|
522
|
+
location: basePath,
|
|
523
|
+
registry,
|
|
524
|
+
async initialize () {
|
|
525
|
+
if (createIfMissing && !readonly) {
|
|
526
|
+
await ensureDir(basePath);
|
|
527
|
+
}
|
|
528
|
+
if (!node_fs.existsSync(basePath)) {
|
|
529
|
+
throw new StorageAccessError(`Context path does not exist: ${basePath}`);
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
async dispose () {
|
|
533
|
+
// No cleanup needed for filesystem
|
|
534
|
+
},
|
|
535
|
+
async isAvailable () {
|
|
536
|
+
try {
|
|
537
|
+
const stat = node_fs.statSync(basePath);
|
|
538
|
+
return stat.isDirectory();
|
|
539
|
+
} catch {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
async get (type, id, namespace) {
|
|
544
|
+
const ns = namespace !== null && namespace !== void 0 ? namespace : defaultNamespace;
|
|
545
|
+
const filePath = getEntityPath(type, id, ns);
|
|
546
|
+
return readEntity(filePath, type);
|
|
547
|
+
},
|
|
548
|
+
async getAll (type, namespace) {
|
|
549
|
+
const ns = namespace !== null && namespace !== void 0 ? namespace : defaultNamespace;
|
|
550
|
+
let dir;
|
|
551
|
+
try {
|
|
552
|
+
dir = getEntityDir(type, ns);
|
|
553
|
+
} catch (error) {
|
|
554
|
+
if (error instanceof SchemaNotRegisteredError) {
|
|
555
|
+
return [];
|
|
556
|
+
}
|
|
557
|
+
throw error;
|
|
558
|
+
}
|
|
559
|
+
if (!node_fs.existsSync(dir)) {
|
|
560
|
+
return [];
|
|
561
|
+
}
|
|
562
|
+
const files = await fs__namespace.readdir(dir);
|
|
563
|
+
const entities = [];
|
|
564
|
+
for (const file of files){
|
|
565
|
+
if (!file.endsWith('.yaml') && !file.endsWith('.yml')) {
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
const entity = await readEntity(path__namespace.join(dir, file), type);
|
|
569
|
+
if (entity) {
|
|
570
|
+
entities.push(entity);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return entities;
|
|
574
|
+
},
|
|
575
|
+
async find (filter) {
|
|
576
|
+
var _filter_namespace;
|
|
577
|
+
var _filter_ids;
|
|
578
|
+
let results = [];
|
|
579
|
+
const types = filter.type ? Array.isArray(filter.type) ? filter.type : [
|
|
580
|
+
filter.type
|
|
581
|
+
] : registry.types();
|
|
582
|
+
const namespace = (_filter_namespace = filter.namespace) !== null && _filter_namespace !== void 0 ? _filter_namespace : defaultNamespace;
|
|
583
|
+
for (const type of types){
|
|
584
|
+
const entities = await this.getAll(type, namespace);
|
|
585
|
+
results = results.concat(entities);
|
|
586
|
+
}
|
|
587
|
+
// Apply ID filter
|
|
588
|
+
if ((_filter_ids = filter.ids) === null || _filter_ids === void 0 ? void 0 : _filter_ids.length) {
|
|
589
|
+
results = results.filter((e)=>filter.ids.includes(e.id));
|
|
590
|
+
}
|
|
591
|
+
// Apply search filter
|
|
592
|
+
if (filter.search) {
|
|
593
|
+
const searchLower = filter.search.toLowerCase();
|
|
594
|
+
results = results.filter((e)=>e.name.toLowerCase().includes(searchLower));
|
|
595
|
+
}
|
|
596
|
+
// Apply pagination
|
|
597
|
+
if (filter.offset) {
|
|
598
|
+
results = results.slice(filter.offset);
|
|
599
|
+
}
|
|
600
|
+
if (filter.limit) {
|
|
601
|
+
results = results.slice(0, filter.limit);
|
|
602
|
+
}
|
|
603
|
+
return results;
|
|
604
|
+
},
|
|
605
|
+
async exists (type, id, namespace) {
|
|
606
|
+
const ns = namespace !== null && namespace !== void 0 ? namespace : defaultNamespace;
|
|
607
|
+
try {
|
|
608
|
+
const filePath = getEntityPath(type, id, ns);
|
|
609
|
+
return node_fs.existsSync(filePath);
|
|
610
|
+
} catch {
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
async count (filter) {
|
|
615
|
+
const results = await this.find(filter);
|
|
616
|
+
return results.length;
|
|
617
|
+
},
|
|
618
|
+
async save (entity, namespace) {
|
|
619
|
+
const ns = namespace !== null && namespace !== void 0 ? namespace : defaultNamespace;
|
|
620
|
+
return writeEntity(entity, ns);
|
|
621
|
+
},
|
|
622
|
+
async delete (type, id, namespace) {
|
|
623
|
+
if (readonly) {
|
|
624
|
+
throw new ReadonlyStorageError();
|
|
625
|
+
}
|
|
626
|
+
const ns = namespace !== null && namespace !== void 0 ? namespace : defaultNamespace;
|
|
627
|
+
try {
|
|
628
|
+
const filePath = getEntityPath(type, id, ns);
|
|
629
|
+
await fs__namespace.unlink(filePath);
|
|
630
|
+
return true;
|
|
631
|
+
} catch (error) {
|
|
632
|
+
if (error.code === 'ENOENT') {
|
|
633
|
+
return false;
|
|
634
|
+
}
|
|
635
|
+
throw error;
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
async saveBatch (entities, namespace) {
|
|
639
|
+
const saved = [];
|
|
640
|
+
for (const entity of entities){
|
|
641
|
+
saved.push(await this.save(entity, namespace));
|
|
642
|
+
}
|
|
643
|
+
return saved;
|
|
644
|
+
},
|
|
645
|
+
async deleteBatch (refs, namespace) {
|
|
646
|
+
let count = 0;
|
|
647
|
+
for (const ref of refs){
|
|
648
|
+
if (await this.delete(ref.type, ref.id, namespace)) {
|
|
649
|
+
count++;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return count;
|
|
653
|
+
},
|
|
654
|
+
async listNamespaces () {
|
|
655
|
+
const namespaces = [];
|
|
656
|
+
if (!node_fs.existsSync(basePath)) {
|
|
657
|
+
return namespaces;
|
|
658
|
+
}
|
|
659
|
+
const entries = await fs__namespace.readdir(basePath, {
|
|
660
|
+
withFileTypes: true
|
|
661
|
+
});
|
|
662
|
+
for (const entry of entries){
|
|
663
|
+
if (!entry.isDirectory()) continue;
|
|
664
|
+
// Check if it's a namespace (contains entity type directories)
|
|
665
|
+
// vs being an entity type directory itself
|
|
666
|
+
const subPath = path__namespace.join(basePath, entry.name);
|
|
667
|
+
const subTypes = await listDirectoryTypes(subPath);
|
|
668
|
+
if (subTypes.length > 0) {
|
|
669
|
+
namespaces.push(entry.name);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return namespaces;
|
|
673
|
+
},
|
|
674
|
+
async namespaceExists (namespace) {
|
|
675
|
+
const namespacePath = path__namespace.join(basePath, namespace);
|
|
676
|
+
return node_fs.existsSync(namespacePath);
|
|
677
|
+
},
|
|
678
|
+
async listTypes (namespace) {
|
|
679
|
+
const ns = namespace !== null && namespace !== void 0 ? namespace : defaultNamespace;
|
|
680
|
+
const searchPath = ns ? path__namespace.join(basePath, ns) : basePath;
|
|
681
|
+
return listDirectoryTypes(searchPath);
|
|
682
|
+
}
|
|
683
|
+
};
|
|
684
|
+
return provider;
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
const createMemoryProvider = (options)=>{
|
|
688
|
+
const { registry, readonly = false, defaultNamespace, initialData = [] } = options;
|
|
689
|
+
// Storage: namespace -> type -> id -> entity
|
|
690
|
+
const store = new Map();
|
|
691
|
+
const getNamespaceStore = (namespace)=>{
|
|
692
|
+
if (!store.has(namespace)) {
|
|
693
|
+
store.set(namespace, new Map());
|
|
694
|
+
}
|
|
695
|
+
return store.get(namespace);
|
|
696
|
+
};
|
|
697
|
+
const getTypeStore = (namespace, type)=>{
|
|
698
|
+
const nsStore = getNamespaceStore(namespace);
|
|
699
|
+
if (!nsStore.has(type)) {
|
|
700
|
+
nsStore.set(type, new Map());
|
|
701
|
+
}
|
|
702
|
+
return nsStore.get(type);
|
|
703
|
+
};
|
|
704
|
+
const resolveNamespace = (ns)=>{
|
|
705
|
+
var _ref;
|
|
706
|
+
return (_ref = ns !== null && ns !== void 0 ? ns : defaultNamespace) !== null && _ref !== void 0 ? _ref : '_default';
|
|
707
|
+
};
|
|
708
|
+
// Initialize with any provided data
|
|
709
|
+
for (const entity of initialData){
|
|
710
|
+
const ns = resolveNamespace(entity.namespace);
|
|
711
|
+
getTypeStore(ns, entity.type).set(entity.id, entity);
|
|
712
|
+
}
|
|
713
|
+
return {
|
|
714
|
+
name: 'memory',
|
|
715
|
+
location: 'in-memory',
|
|
716
|
+
registry,
|
|
717
|
+
async initialize () {},
|
|
718
|
+
async dispose () {
|
|
719
|
+
store.clear();
|
|
720
|
+
},
|
|
721
|
+
async isAvailable () {
|
|
722
|
+
return true;
|
|
723
|
+
},
|
|
724
|
+
async get (type, id, namespace) {
|
|
725
|
+
const ns = resolveNamespace(namespace);
|
|
726
|
+
return getTypeStore(ns, type).get(id);
|
|
727
|
+
},
|
|
728
|
+
async getAll (type, namespace) {
|
|
729
|
+
const ns = resolveNamespace(namespace);
|
|
730
|
+
return Array.from(getTypeStore(ns, type).values());
|
|
731
|
+
},
|
|
732
|
+
async find (filter) {
|
|
733
|
+
var _filter_ids;
|
|
734
|
+
let results = [];
|
|
735
|
+
const types = filter.type ? Array.isArray(filter.type) ? filter.type : [
|
|
736
|
+
filter.type
|
|
737
|
+
] : registry.types();
|
|
738
|
+
const ns = resolveNamespace(filter.namespace);
|
|
739
|
+
for (const type of types){
|
|
740
|
+
results = results.concat(await this.getAll(type, ns));
|
|
741
|
+
}
|
|
742
|
+
if ((_filter_ids = filter.ids) === null || _filter_ids === void 0 ? void 0 : _filter_ids.length) {
|
|
743
|
+
results = results.filter((e)=>filter.ids.includes(e.id));
|
|
744
|
+
}
|
|
745
|
+
if (filter.search) {
|
|
746
|
+
const s = filter.search.toLowerCase();
|
|
747
|
+
results = results.filter((e)=>e.name.toLowerCase().includes(s));
|
|
748
|
+
}
|
|
749
|
+
if (filter.offset) results = results.slice(filter.offset);
|
|
750
|
+
if (filter.limit) results = results.slice(0, filter.limit);
|
|
751
|
+
return results;
|
|
752
|
+
},
|
|
753
|
+
async exists (type, id, namespace) {
|
|
754
|
+
const ns = resolveNamespace(namespace);
|
|
755
|
+
return getTypeStore(ns, type).has(id);
|
|
756
|
+
},
|
|
757
|
+
async count (filter) {
|
|
758
|
+
return (await this.find(filter)).length;
|
|
759
|
+
},
|
|
760
|
+
async save (entity, namespace) {
|
|
761
|
+
if (readonly) {
|
|
762
|
+
throw new ReadonlyStorageError();
|
|
763
|
+
}
|
|
764
|
+
const validationResult = registry.validate(entity);
|
|
765
|
+
if (!validationResult.success) {
|
|
766
|
+
throw new ValidationError('Validation failed', validationResult.errors || []);
|
|
767
|
+
}
|
|
768
|
+
const ns = resolveNamespace(namespace);
|
|
769
|
+
const now = new Date();
|
|
770
|
+
const existing = getTypeStore(ns, entity.type).get(entity.id);
|
|
771
|
+
const saved = {
|
|
772
|
+
...entity,
|
|
773
|
+
namespace: ns,
|
|
774
|
+
createdAt: (existing === null || existing === void 0 ? void 0 : existing.createdAt) || now,
|
|
775
|
+
updatedAt: now
|
|
776
|
+
};
|
|
777
|
+
getTypeStore(ns, entity.type).set(entity.id, saved);
|
|
778
|
+
return saved;
|
|
779
|
+
},
|
|
780
|
+
async delete (type, id, namespace) {
|
|
781
|
+
if (readonly) {
|
|
782
|
+
throw new ReadonlyStorageError();
|
|
783
|
+
}
|
|
784
|
+
const ns = resolveNamespace(namespace);
|
|
785
|
+
return getTypeStore(ns, type).delete(id);
|
|
786
|
+
},
|
|
787
|
+
async saveBatch (entities, namespace) {
|
|
788
|
+
return Promise.all(entities.map((e)=>this.save(e, namespace)));
|
|
789
|
+
},
|
|
790
|
+
async deleteBatch (refs, namespace) {
|
|
791
|
+
let count = 0;
|
|
792
|
+
for (const ref of refs){
|
|
793
|
+
if (await this.delete(ref.type, ref.id, namespace)) count++;
|
|
794
|
+
}
|
|
795
|
+
return count;
|
|
796
|
+
},
|
|
797
|
+
async listNamespaces () {
|
|
798
|
+
return Array.from(store.keys()).filter((ns)=>ns !== '_default');
|
|
799
|
+
},
|
|
800
|
+
async namespaceExists (namespace) {
|
|
801
|
+
return store.has(namespace);
|
|
802
|
+
},
|
|
803
|
+
async listTypes (namespace) {
|
|
804
|
+
const ns = resolveNamespace(namespace);
|
|
805
|
+
const nsStore = store.get(ns);
|
|
806
|
+
return nsStore ? Array.from(nsStore.keys()) : [];
|
|
807
|
+
}
|
|
808
|
+
};
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* Generate a slug from a name.
|
|
813
|
+
* "John Doe" -> "john-doe"
|
|
814
|
+
* "API Reference" -> "api-reference"
|
|
815
|
+
*/ const slugify = (name)=>{
|
|
816
|
+
const cleaned = name.toLowerCase().trim().replace(/[^\w\s-]/g, '') // Remove non-word chars
|
|
817
|
+
.replace(/[\s_]+/g, '-'); // Replace spaces/underscores
|
|
818
|
+
// Single-pass algorithm to collapse hyphens and trim (avoid ReDoS)
|
|
819
|
+
const result = [];
|
|
820
|
+
let prevWasHyphen = false;
|
|
821
|
+
let startIdx = 0;
|
|
822
|
+
let endIdx = cleaned.length;
|
|
823
|
+
// Skip leading hyphens
|
|
824
|
+
while(startIdx < cleaned.length && cleaned[startIdx] === '-'){
|
|
825
|
+
startIdx++;
|
|
826
|
+
}
|
|
827
|
+
// Skip trailing hyphens
|
|
828
|
+
while(endIdx > startIdx && cleaned[endIdx - 1] === '-'){
|
|
829
|
+
endIdx--;
|
|
830
|
+
}
|
|
831
|
+
// Build result, collapsing consecutive hyphens
|
|
832
|
+
for(let i = startIdx; i < endIdx; i++){
|
|
833
|
+
const char = cleaned[i];
|
|
834
|
+
if (char === '-') {
|
|
835
|
+
if (!prevWasHyphen) {
|
|
836
|
+
result.push('-');
|
|
837
|
+
prevWasHyphen = true;
|
|
838
|
+
}
|
|
839
|
+
} else {
|
|
840
|
+
result.push(char);
|
|
841
|
+
prevWasHyphen = false;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
return result.join('');
|
|
845
|
+
};
|
|
846
|
+
/**
|
|
847
|
+
* Generate a unique ID, appending a number if needed.
|
|
848
|
+
*/ const generateUniqueId = async (baseName, exists)=>{
|
|
849
|
+
const baseId = slugify(baseName);
|
|
850
|
+
if (!await exists(baseId)) {
|
|
851
|
+
return baseId;
|
|
852
|
+
}
|
|
853
|
+
// Try appending numbers
|
|
854
|
+
for(let i = 2; i <= 100; i++){
|
|
855
|
+
const candidateId = `${baseId}-${i}`;
|
|
856
|
+
if (!await exists(candidateId)) {
|
|
857
|
+
return candidateId;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
// Fallback to timestamp
|
|
861
|
+
return `${baseId}-${Date.now()}`;
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
const createSearchEngine = (options)=>{
|
|
865
|
+
const { provider, registry, defaultNamespace } = options;
|
|
866
|
+
const textMatch = (text, query, caseSensitive)=>{
|
|
867
|
+
if (!text) return false;
|
|
868
|
+
const a = caseSensitive ? text : text.toLowerCase();
|
|
869
|
+
const b = caseSensitive ? query : query.toLowerCase();
|
|
870
|
+
return a.includes(b);
|
|
871
|
+
};
|
|
872
|
+
const matchesSearch = (entity, search, searchFields, caseSensitive)=>{
|
|
873
|
+
// Always search name
|
|
874
|
+
if (textMatch(entity.name, search, caseSensitive)) {
|
|
875
|
+
return true;
|
|
876
|
+
}
|
|
877
|
+
// Search additional fields
|
|
878
|
+
for (const field of searchFields){
|
|
879
|
+
const value = entity[field];
|
|
880
|
+
if (typeof value === 'string' && textMatch(value, search, caseSensitive)) {
|
|
881
|
+
return true;
|
|
882
|
+
}
|
|
883
|
+
// Handle arrays of strings (like sounds_like)
|
|
884
|
+
if (Array.isArray(value)) {
|
|
885
|
+
for (const item of value){
|
|
886
|
+
if (typeof item === 'string' && textMatch(item, search, caseSensitive)) {
|
|
887
|
+
return true;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return false;
|
|
893
|
+
};
|
|
894
|
+
const sortEntities = (entities, sort)=>{
|
|
895
|
+
return [
|
|
896
|
+
...entities
|
|
897
|
+
].sort((a, b)=>{
|
|
898
|
+
for (const { field, direction } of sort){
|
|
899
|
+
const aVal = a[field];
|
|
900
|
+
const bVal = b[field];
|
|
901
|
+
if (aVal === bVal) continue;
|
|
902
|
+
if (aVal === undefined || aVal === null) return direction === 'asc' ? 1 : -1;
|
|
903
|
+
if (bVal === undefined || bVal === null) return direction === 'asc' ? -1 : 1;
|
|
904
|
+
let cmp;
|
|
905
|
+
if (typeof aVal === 'string' && typeof bVal === 'string') {
|
|
906
|
+
cmp = aVal.localeCompare(bVal);
|
|
907
|
+
} else if (aVal instanceof Date && bVal instanceof Date) {
|
|
908
|
+
cmp = aVal.getTime() - bVal.getTime();
|
|
909
|
+
} else {
|
|
910
|
+
cmp = aVal < bVal ? -1 : 1;
|
|
911
|
+
}
|
|
912
|
+
return direction === 'asc' ? cmp : -cmp;
|
|
913
|
+
}
|
|
914
|
+
return 0;
|
|
915
|
+
});
|
|
916
|
+
};
|
|
917
|
+
return {
|
|
918
|
+
async search (options) {
|
|
919
|
+
const { type, namespace, ids, search, searchFields = [], caseSensitive = false, limit, offset = 0, sort = [
|
|
920
|
+
{
|
|
921
|
+
field: 'name',
|
|
922
|
+
direction: 'asc'
|
|
923
|
+
}
|
|
924
|
+
] } = options;
|
|
925
|
+
// Determine which types to search
|
|
926
|
+
const types = type ? Array.isArray(type) ? type : [
|
|
927
|
+
type
|
|
928
|
+
] : registry.types();
|
|
929
|
+
// Determine which namespaces to search
|
|
930
|
+
const namespaces = namespace ? Array.isArray(namespace) ? namespace : [
|
|
931
|
+
namespace
|
|
932
|
+
] : [
|
|
933
|
+
defaultNamespace
|
|
934
|
+
];
|
|
935
|
+
// Collect entities
|
|
936
|
+
let allEntities = [];
|
|
937
|
+
for (const t of types){
|
|
938
|
+
for (const ns of namespaces){
|
|
939
|
+
const entities = await provider.getAll(t, ns);
|
|
940
|
+
allEntities = allEntities.concat(entities);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
// Apply ID filter
|
|
944
|
+
if (ids === null || ids === void 0 ? void 0 : ids.length) {
|
|
945
|
+
allEntities = allEntities.filter((e)=>ids.includes(e.id));
|
|
946
|
+
}
|
|
947
|
+
// Apply search filter
|
|
948
|
+
if (search) {
|
|
949
|
+
allEntities = allEntities.filter((e)=>matchesSearch(e, search, searchFields, caseSensitive));
|
|
950
|
+
}
|
|
951
|
+
// Apply sorting
|
|
952
|
+
allEntities = sortEntities(allEntities, sort);
|
|
953
|
+
// Get total before pagination
|
|
954
|
+
const total = allEntities.length;
|
|
955
|
+
// Apply pagination
|
|
956
|
+
const paginated = allEntities.slice(offset, limit ? offset + limit : undefined);
|
|
957
|
+
return {
|
|
958
|
+
items: paginated,
|
|
959
|
+
total,
|
|
960
|
+
hasMore: limit ? offset + limit < total : false,
|
|
961
|
+
query: options
|
|
962
|
+
};
|
|
963
|
+
},
|
|
964
|
+
async quickSearch (query, options = {}) {
|
|
965
|
+
const result = await this.search({
|
|
966
|
+
...options,
|
|
967
|
+
search: query
|
|
968
|
+
});
|
|
969
|
+
return result.items;
|
|
970
|
+
}
|
|
971
|
+
};
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
const createContext = (options)=>{
|
|
975
|
+
const { provider, registry, defaultNamespace } = options;
|
|
976
|
+
const resolveNamespace = (ns)=>ns !== null && ns !== void 0 ? ns : defaultNamespace;
|
|
977
|
+
const searchEngine = createSearchEngine({
|
|
978
|
+
provider,
|
|
979
|
+
registry,
|
|
980
|
+
defaultNamespace
|
|
981
|
+
});
|
|
982
|
+
const api = {
|
|
983
|
+
provider,
|
|
984
|
+
registry,
|
|
985
|
+
defaultNamespace,
|
|
986
|
+
async get (type, id, namespace) {
|
|
987
|
+
return provider.get(type, id, resolveNamespace(namespace));
|
|
988
|
+
},
|
|
989
|
+
async getAll (type, namespace) {
|
|
990
|
+
return provider.getAll(type, resolveNamespace(namespace));
|
|
991
|
+
},
|
|
992
|
+
async exists (type, id, namespace) {
|
|
993
|
+
return provider.exists(type, id, resolveNamespace(namespace));
|
|
994
|
+
},
|
|
995
|
+
async create (type, data, options = {}) {
|
|
996
|
+
var _options_generateUniqueId;
|
|
997
|
+
const namespace = resolveNamespace(options.namespace);
|
|
998
|
+
const shouldGenerateUnique = (_options_generateUniqueId = options.generateUniqueId) !== null && _options_generateUniqueId !== void 0 ? _options_generateUniqueId : true;
|
|
999
|
+
let id;
|
|
1000
|
+
if (options.id) {
|
|
1001
|
+
id = options.id;
|
|
1002
|
+
} else if (shouldGenerateUnique) {
|
|
1003
|
+
id = await generateUniqueId(data.name, (testId)=>provider.exists(type, testId, namespace));
|
|
1004
|
+
} else {
|
|
1005
|
+
id = slugify(data.name);
|
|
1006
|
+
}
|
|
1007
|
+
const entity = {
|
|
1008
|
+
...data,
|
|
1009
|
+
id,
|
|
1010
|
+
type,
|
|
1011
|
+
createdAt: new Date(),
|
|
1012
|
+
updatedAt: new Date()
|
|
1013
|
+
};
|
|
1014
|
+
return provider.save(entity, namespace);
|
|
1015
|
+
},
|
|
1016
|
+
async update (type, id, updates, namespace) {
|
|
1017
|
+
const ns = resolveNamespace(namespace);
|
|
1018
|
+
const existing = await provider.get(type, id, ns);
|
|
1019
|
+
if (!existing) {
|
|
1020
|
+
throw new EntityNotFoundError(type, id, ns);
|
|
1021
|
+
}
|
|
1022
|
+
const updated = {
|
|
1023
|
+
...existing,
|
|
1024
|
+
...updates,
|
|
1025
|
+
id,
|
|
1026
|
+
type,
|
|
1027
|
+
updatedAt: new Date()
|
|
1028
|
+
};
|
|
1029
|
+
return provider.save(updated, ns);
|
|
1030
|
+
},
|
|
1031
|
+
async upsert (type, entity, namespace) {
|
|
1032
|
+
const ns = resolveNamespace(namespace);
|
|
1033
|
+
const existing = await provider.get(type, entity.id, ns);
|
|
1034
|
+
const now = new Date();
|
|
1035
|
+
const toSave = {
|
|
1036
|
+
...existing,
|
|
1037
|
+
...entity,
|
|
1038
|
+
type,
|
|
1039
|
+
createdAt: (existing === null || existing === void 0 ? void 0 : existing.createdAt) || now,
|
|
1040
|
+
updatedAt: now
|
|
1041
|
+
};
|
|
1042
|
+
return provider.save(toSave, ns);
|
|
1043
|
+
},
|
|
1044
|
+
async delete (type, id, namespace) {
|
|
1045
|
+
return provider.delete(type, id, resolveNamespace(namespace));
|
|
1046
|
+
},
|
|
1047
|
+
types () {
|
|
1048
|
+
return registry.types();
|
|
1049
|
+
},
|
|
1050
|
+
withNamespace (namespace) {
|
|
1051
|
+
return createContext({
|
|
1052
|
+
...options,
|
|
1053
|
+
defaultNamespace: namespace
|
|
1054
|
+
});
|
|
1055
|
+
},
|
|
1056
|
+
search: searchEngine.search.bind(searchEngine),
|
|
1057
|
+
quickSearch: searchEngine.quickSearch.bind(searchEngine)
|
|
1058
|
+
};
|
|
1059
|
+
return api;
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
const createTypedAPI = (options)=>{
|
|
1063
|
+
const { schemas, provider, defaultNamespace, pluralNames = {} } = options;
|
|
1064
|
+
// Create registry from schemas
|
|
1065
|
+
const registry = createSchemaRegistry();
|
|
1066
|
+
for (const [type, schema] of Object.entries(schemas)){
|
|
1067
|
+
registry.register({
|
|
1068
|
+
type,
|
|
1069
|
+
schema: schema,
|
|
1070
|
+
pluralName: pluralNames[type]
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
return createContext({
|
|
1074
|
+
provider,
|
|
1075
|
+
registry,
|
|
1076
|
+
schemas,
|
|
1077
|
+
defaultNamespace
|
|
1078
|
+
});
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
function _define_property(obj, key, value) {
|
|
1082
|
+
if (key in obj) {
|
|
1083
|
+
Object.defineProperty(obj, key, {
|
|
1084
|
+
value: value,
|
|
1085
|
+
enumerable: true,
|
|
1086
|
+
configurable: true,
|
|
1087
|
+
writable: true
|
|
1088
|
+
});
|
|
1089
|
+
} else {
|
|
1090
|
+
obj[key] = value;
|
|
1091
|
+
}
|
|
1092
|
+
return obj;
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Fluent query builder for constructing search queries.
|
|
1096
|
+
*/ class QueryBuilder {
|
|
1097
|
+
/**
|
|
1098
|
+
* Filter by entity type(s).
|
|
1099
|
+
*/ type(type) {
|
|
1100
|
+
this.options.type = type;
|
|
1101
|
+
return this;
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Filter by namespace(s).
|
|
1105
|
+
*/ namespace(ns) {
|
|
1106
|
+
this.options.namespace = ns;
|
|
1107
|
+
return this;
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* Filter by specific IDs.
|
|
1111
|
+
*/ ids(ids) {
|
|
1112
|
+
this.options.ids = ids;
|
|
1113
|
+
return this;
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Add text search.
|
|
1117
|
+
*/ search(query, fields) {
|
|
1118
|
+
this.options.search = query;
|
|
1119
|
+
if (fields) this.options.searchFields = fields;
|
|
1120
|
+
return this;
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Enable case-sensitive search.
|
|
1124
|
+
*/ caseSensitive(enabled = true) {
|
|
1125
|
+
this.options.caseSensitive = enabled;
|
|
1126
|
+
return this;
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Add sort field.
|
|
1130
|
+
*/ sortBy(field, direction = 'asc') {
|
|
1131
|
+
this.options.sort = [
|
|
1132
|
+
...this.options.sort || [],
|
|
1133
|
+
{
|
|
1134
|
+
field,
|
|
1135
|
+
direction
|
|
1136
|
+
}
|
|
1137
|
+
];
|
|
1138
|
+
return this;
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Set result limit.
|
|
1142
|
+
*/ limit(n) {
|
|
1143
|
+
this.options.limit = n;
|
|
1144
|
+
return this;
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Set result offset.
|
|
1148
|
+
*/ offset(n) {
|
|
1149
|
+
this.options.offset = n;
|
|
1150
|
+
return this;
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Set page (calculates offset from limit).
|
|
1154
|
+
*/ page(pageNum, pageSize) {
|
|
1155
|
+
this.options.limit = pageSize;
|
|
1156
|
+
this.options.offset = (pageNum - 1) * pageSize;
|
|
1157
|
+
return this;
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* Build the query options.
|
|
1161
|
+
*/ build() {
|
|
1162
|
+
return {
|
|
1163
|
+
...this.options
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
constructor(){
|
|
1167
|
+
_define_property(this, "options", {});
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Start building a query.
|
|
1172
|
+
*/ const query = ()=>new QueryBuilder();
|
|
1173
|
+
|
|
1174
|
+
const createNamespaceResolver = (options)=>{
|
|
1175
|
+
const { provider, defaultNamespace = '_default' } = options;
|
|
1176
|
+
const configuredNamespaces = new Map();
|
|
1177
|
+
// Load initial configurations
|
|
1178
|
+
if (options.namespaces) {
|
|
1179
|
+
for (const ns of options.namespaces){
|
|
1180
|
+
configuredNamespaces.set(ns.id, ns);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
return {
|
|
1184
|
+
async resolve (requested) {
|
|
1185
|
+
let namespaceIds;
|
|
1186
|
+
if (!requested) {
|
|
1187
|
+
namespaceIds = [
|
|
1188
|
+
defaultNamespace
|
|
1189
|
+
];
|
|
1190
|
+
} else if (typeof requested === 'string') {
|
|
1191
|
+
namespaceIds = [
|
|
1192
|
+
requested
|
|
1193
|
+
];
|
|
1194
|
+
} else {
|
|
1195
|
+
namespaceIds = requested;
|
|
1196
|
+
}
|
|
1197
|
+
const references = [];
|
|
1198
|
+
const readable = [];
|
|
1199
|
+
let primary;
|
|
1200
|
+
for(let i = 0; i < namespaceIds.length; i++){
|
|
1201
|
+
const nsId = namespaceIds[i];
|
|
1202
|
+
const config = configuredNamespaces.get(nsId);
|
|
1203
|
+
// Include all requested namespaces, even if they don't exist yet
|
|
1204
|
+
const ref = {
|
|
1205
|
+
namespace: nsId,
|
|
1206
|
+
priority: namespaceIds.length - i,
|
|
1207
|
+
searchable: (config === null || config === void 0 ? void 0 : config.active) !== false,
|
|
1208
|
+
writable: i === 0
|
|
1209
|
+
};
|
|
1210
|
+
references.push(ref);
|
|
1211
|
+
readable.push(nsId);
|
|
1212
|
+
if (!primary) {
|
|
1213
|
+
primary = nsId;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
if (!primary) {
|
|
1217
|
+
primary = defaultNamespace;
|
|
1218
|
+
readable.push(defaultNamespace);
|
|
1219
|
+
}
|
|
1220
|
+
return {
|
|
1221
|
+
primary,
|
|
1222
|
+
readable,
|
|
1223
|
+
references
|
|
1224
|
+
};
|
|
1225
|
+
},
|
|
1226
|
+
async listAll () {
|
|
1227
|
+
const discovered = await provider.listNamespaces();
|
|
1228
|
+
const all = new Map();
|
|
1229
|
+
// Add configured namespaces
|
|
1230
|
+
for (const [id, config] of configuredNamespaces){
|
|
1231
|
+
all.set(id, config);
|
|
1232
|
+
}
|
|
1233
|
+
// Add discovered namespaces
|
|
1234
|
+
for (const nsId of discovered){
|
|
1235
|
+
if (!all.has(nsId)) {
|
|
1236
|
+
all.set(nsId, {
|
|
1237
|
+
id: nsId,
|
|
1238
|
+
name: nsId,
|
|
1239
|
+
active: true
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
return Array.from(all.values());
|
|
1244
|
+
},
|
|
1245
|
+
register (config) {
|
|
1246
|
+
configuredNamespaces.set(config.id, config);
|
|
1247
|
+
},
|
|
1248
|
+
getPrimary () {
|
|
1249
|
+
return defaultNamespace;
|
|
1250
|
+
},
|
|
1251
|
+
async exists (namespace) {
|
|
1252
|
+
return configuredNamespaces.has(namespace) || await provider.namespaceExists(namespace);
|
|
1253
|
+
}
|
|
1254
|
+
};
|
|
1255
|
+
};
|
|
1256
|
+
|
|
1257
|
+
const createMultiNamespaceContext = async (options, namespaces)=>{
|
|
1258
|
+
const { api, resolver } = options;
|
|
1259
|
+
const { provider } = api;
|
|
1260
|
+
const resolution = await resolver.resolve(namespaces);
|
|
1261
|
+
return {
|
|
1262
|
+
// Forward base API methods
|
|
1263
|
+
provider: api.provider,
|
|
1264
|
+
registry: api.registry,
|
|
1265
|
+
defaultNamespace: resolution.primary,
|
|
1266
|
+
get: (type, id, namespace)=>api.get(type, id, namespace !== null && namespace !== void 0 ? namespace : resolution.primary),
|
|
1267
|
+
getAll: (type, namespace)=>api.getAll(type, namespace !== null && namespace !== void 0 ? namespace : resolution.primary),
|
|
1268
|
+
exists: (type, id, namespace)=>api.exists(type, id, namespace !== null && namespace !== void 0 ? namespace : resolution.primary),
|
|
1269
|
+
create: (type, data, opts)=>{
|
|
1270
|
+
var _ref;
|
|
1271
|
+
return api.create(type, data, {
|
|
1272
|
+
...opts,
|
|
1273
|
+
namespace: (_ref = opts === null || opts === void 0 ? void 0 : opts.namespace) !== null && _ref !== void 0 ? _ref : resolution.primary
|
|
1274
|
+
});
|
|
1275
|
+
},
|
|
1276
|
+
update: (type, id, updates, namespace)=>api.update(type, id, updates, namespace !== null && namespace !== void 0 ? namespace : resolution.primary),
|
|
1277
|
+
upsert: (type, entity, namespace)=>api.upsert(type, entity, namespace !== null && namespace !== void 0 ? namespace : resolution.primary),
|
|
1278
|
+
delete: (type, id, namespace)=>api.delete(type, id, namespace !== null && namespace !== void 0 ? namespace : resolution.primary),
|
|
1279
|
+
types: ()=>api.types(),
|
|
1280
|
+
search: (opts)=>api.search(opts),
|
|
1281
|
+
quickSearch: (q, opts)=>api.quickSearch(q, opts),
|
|
1282
|
+
withNamespace: (ns)=>api.withNamespace(ns),
|
|
1283
|
+
// Multi-namespace methods
|
|
1284
|
+
async getFromAny (type, id) {
|
|
1285
|
+
for (const ns of resolution.readable){
|
|
1286
|
+
const entity = await provider.get(type, id, ns);
|
|
1287
|
+
if (entity) {
|
|
1288
|
+
return {
|
|
1289
|
+
entity: entity,
|
|
1290
|
+
namespace: ns
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
return undefined;
|
|
1295
|
+
},
|
|
1296
|
+
async getAllMerged (type) {
|
|
1297
|
+
const byId = new Map();
|
|
1298
|
+
// Process in reverse priority order so higher priority wins
|
|
1299
|
+
const reversed = [
|
|
1300
|
+
...resolution.readable
|
|
1301
|
+
].reverse();
|
|
1302
|
+
for (const ns of reversed){
|
|
1303
|
+
const entities = await provider.getAll(type, ns);
|
|
1304
|
+
for (const entity of entities){
|
|
1305
|
+
byId.set(entity.id, entity);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
return Array.from(byId.values());
|
|
1309
|
+
},
|
|
1310
|
+
async locateEntity (type, id) {
|
|
1311
|
+
for (const ns of resolution.readable){
|
|
1312
|
+
if (await provider.exists(type, id, ns)) {
|
|
1313
|
+
return ns;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
return undefined;
|
|
1317
|
+
},
|
|
1318
|
+
getResolution () {
|
|
1319
|
+
return resolution;
|
|
1320
|
+
},
|
|
1321
|
+
async withNamespaces (ns) {
|
|
1322
|
+
return createMultiNamespaceContext(options, ns);
|
|
1323
|
+
}
|
|
1324
|
+
};
|
|
1325
|
+
};
|
|
1326
|
+
|
|
1327
|
+
const createDirectoryWalker = (options)=>{
|
|
1328
|
+
const { startDir, contextDirName, maxLevels, stopAt, stopMarkers = [], registry } = options;
|
|
1329
|
+
const shouldStop = async (dir)=>{
|
|
1330
|
+
if (stopAt && dir === stopAt) return true;
|
|
1331
|
+
for (const marker of stopMarkers){
|
|
1332
|
+
if (node_fs.existsSync(path__namespace.join(dir, marker))) {
|
|
1333
|
+
return true;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
const parent = path__namespace.dirname(dir);
|
|
1337
|
+
return parent === dir; // Root
|
|
1338
|
+
};
|
|
1339
|
+
const getNamespacesAndTypes = async (contextDir)=>{
|
|
1340
|
+
const namespaces = [];
|
|
1341
|
+
const types = [];
|
|
1342
|
+
try {
|
|
1343
|
+
const entries = await fs__namespace.readdir(contextDir, {
|
|
1344
|
+
withFileTypes: true
|
|
1345
|
+
});
|
|
1346
|
+
for (const entry of entries){
|
|
1347
|
+
if (!entry.isDirectory()) continue;
|
|
1348
|
+
const subPath = path__namespace.join(contextDir, entry.name);
|
|
1349
|
+
const subEntries = await fs__namespace.readdir(subPath, {
|
|
1350
|
+
withFileTypes: true
|
|
1351
|
+
});
|
|
1352
|
+
// Check if this is a type directory (has .yaml files)
|
|
1353
|
+
const hasYamlFiles = subEntries.some((sub)=>sub.isFile() && (sub.name.endsWith('.yaml') || sub.name.endsWith('.yml')));
|
|
1354
|
+
// Check if this looks like a type directory (known to registry)
|
|
1355
|
+
const isTypeDir = registry ? !!registry.getTypeFromDirectory(entry.name) : hasYamlFiles;
|
|
1356
|
+
if (isTypeDir) {
|
|
1357
|
+
const typeName = (registry === null || registry === void 0 ? void 0 : registry.getTypeFromDirectory(entry.name)) || entry.name;
|
|
1358
|
+
if (!types.includes(typeName)) {
|
|
1359
|
+
types.push(typeName);
|
|
1360
|
+
}
|
|
1361
|
+
} else {
|
|
1362
|
+
// Check if it's a namespace (contains type directories)
|
|
1363
|
+
const hasTypeDirs = subEntries.some((sub)=>{
|
|
1364
|
+
if (!sub.isDirectory()) return false;
|
|
1365
|
+
return registry ? !!registry.getTypeFromDirectory(sub.name) : true; // Assume any subdir could be a type
|
|
1366
|
+
});
|
|
1367
|
+
if (hasTypeDirs) {
|
|
1368
|
+
namespaces.push(entry.name);
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
} catch {
|
|
1373
|
+
// Directory doesn't exist or can't be read
|
|
1374
|
+
}
|
|
1375
|
+
return {
|
|
1376
|
+
namespaces,
|
|
1377
|
+
types
|
|
1378
|
+
};
|
|
1379
|
+
};
|
|
1380
|
+
return {
|
|
1381
|
+
async discover () {
|
|
1382
|
+
const discovered = [];
|
|
1383
|
+
let currentDir = path__namespace.resolve(startDir);
|
|
1384
|
+
let level = 0;
|
|
1385
|
+
while(level < maxLevels){
|
|
1386
|
+
const contextDir = path__namespace.join(currentDir, contextDirName);
|
|
1387
|
+
if (node_fs.existsSync(contextDir)) {
|
|
1388
|
+
const { namespaces, types } = await getNamespacesAndTypes(contextDir);
|
|
1389
|
+
discovered.push({
|
|
1390
|
+
path: contextDir,
|
|
1391
|
+
level,
|
|
1392
|
+
namespaces,
|
|
1393
|
+
types
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
if (await shouldStop(currentDir)) break;
|
|
1397
|
+
currentDir = path__namespace.dirname(currentDir);
|
|
1398
|
+
level++;
|
|
1399
|
+
}
|
|
1400
|
+
return discovered;
|
|
1401
|
+
},
|
|
1402
|
+
async hasContext (dir) {
|
|
1403
|
+
return node_fs.existsSync(path__namespace.join(dir, contextDirName));
|
|
1404
|
+
}
|
|
1405
|
+
};
|
|
1406
|
+
};
|
|
1407
|
+
|
|
1408
|
+
const discoverContextRoot = async (options = {})=>{
|
|
1409
|
+
var _directories_;
|
|
1410
|
+
const { startDir = process.cwd(), contextDirName = 'context', maxLevels = 10, projectMarkers = [
|
|
1411
|
+
'.git',
|
|
1412
|
+
'package.json'
|
|
1413
|
+
], registry } = options;
|
|
1414
|
+
const walker = createDirectoryWalker({
|
|
1415
|
+
startDir,
|
|
1416
|
+
contextDirName,
|
|
1417
|
+
maxLevels,
|
|
1418
|
+
stopMarkers: projectMarkers,
|
|
1419
|
+
registry
|
|
1420
|
+
});
|
|
1421
|
+
const directories = await walker.discover();
|
|
1422
|
+
// Collect all namespaces and types (deduped)
|
|
1423
|
+
const namespaceSet = new Set();
|
|
1424
|
+
const typeSet = new Set();
|
|
1425
|
+
for (const dir of directories){
|
|
1426
|
+
for (const ns of dir.namespaces)namespaceSet.add(ns);
|
|
1427
|
+
for (const type of dir.types)typeSet.add(type);
|
|
1428
|
+
}
|
|
1429
|
+
return {
|
|
1430
|
+
directories,
|
|
1431
|
+
primary: (_directories_ = directories[0]) === null || _directories_ === void 0 ? void 0 : _directories_.path,
|
|
1432
|
+
contextPaths: directories.map((d)=>d.path),
|
|
1433
|
+
allNamespaces: Array.from(namespaceSet),
|
|
1434
|
+
allTypes: Array.from(typeSet)
|
|
1435
|
+
};
|
|
1436
|
+
};
|
|
1437
|
+
/**
|
|
1438
|
+
* Create or ensure a context directory exists.
|
|
1439
|
+
*/ const ensureContextRoot = async (projectDir, contextDirName = 'context')=>{
|
|
1440
|
+
const contextPath = path__namespace.join(projectDir, contextDirName);
|
|
1441
|
+
if (!node_fs.existsSync(contextPath)) {
|
|
1442
|
+
const fs = await import('node:fs/promises');
|
|
1443
|
+
await fs.mkdir(contextPath, {
|
|
1444
|
+
recursive: true
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
return contextPath;
|
|
1448
|
+
};
|
|
1449
|
+
|
|
1450
|
+
/**
|
|
1451
|
+
* Storage provider that reads from multiple context directories
|
|
1452
|
+
* and writes to the primary (closest) directory.
|
|
1453
|
+
*/ const createHierarchicalProvider = async (options)=>{
|
|
1454
|
+
const { contextRoot, registry, readonly = false } = options;
|
|
1455
|
+
if (contextRoot.contextPaths.length === 0) {
|
|
1456
|
+
throw new Error('No context directories found');
|
|
1457
|
+
}
|
|
1458
|
+
// Create read-only providers for each context directory
|
|
1459
|
+
const readProviders = [];
|
|
1460
|
+
for (const contextPath of contextRoot.contextPaths){
|
|
1461
|
+
const fsProvider = await createFileSystemProvider({
|
|
1462
|
+
basePath: contextPath,
|
|
1463
|
+
registry,
|
|
1464
|
+
createIfMissing: false,
|
|
1465
|
+
readonly: true
|
|
1466
|
+
});
|
|
1467
|
+
await fsProvider.initialize();
|
|
1468
|
+
readProviders.push(fsProvider);
|
|
1469
|
+
}
|
|
1470
|
+
// Primary provider for writes
|
|
1471
|
+
const primaryProvider = await createFileSystemProvider({
|
|
1472
|
+
basePath: contextRoot.primary,
|
|
1473
|
+
registry,
|
|
1474
|
+
createIfMissing: true,
|
|
1475
|
+
readonly
|
|
1476
|
+
});
|
|
1477
|
+
await primaryProvider.initialize();
|
|
1478
|
+
const findEntities = async (filter)=>{
|
|
1479
|
+
const byId = new Map();
|
|
1480
|
+
for (const p of [
|
|
1481
|
+
...readProviders
|
|
1482
|
+
].reverse()){
|
|
1483
|
+
const results = await p.find(filter);
|
|
1484
|
+
for (const entity of results){
|
|
1485
|
+
byId.set(entity.id, entity);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
let results = Array.from(byId.values());
|
|
1489
|
+
if (filter.offset) results = results.slice(filter.offset);
|
|
1490
|
+
if (filter.limit) results = results.slice(0, filter.limit);
|
|
1491
|
+
return results;
|
|
1492
|
+
};
|
|
1493
|
+
return {
|
|
1494
|
+
name: 'hierarchical',
|
|
1495
|
+
location: contextRoot.primary,
|
|
1496
|
+
registry,
|
|
1497
|
+
async initialize () {},
|
|
1498
|
+
async dispose () {
|
|
1499
|
+
for (const p of readProviders)await p.dispose();
|
|
1500
|
+
await primaryProvider.dispose();
|
|
1501
|
+
},
|
|
1502
|
+
async isAvailable () {
|
|
1503
|
+
return primaryProvider.isAvailable();
|
|
1504
|
+
},
|
|
1505
|
+
// Read operations search all providers (closest first)
|
|
1506
|
+
async get (type, id, namespace) {
|
|
1507
|
+
for (const p of readProviders){
|
|
1508
|
+
const entity = await p.get(type, id, namespace);
|
|
1509
|
+
if (entity) return entity;
|
|
1510
|
+
}
|
|
1511
|
+
return undefined;
|
|
1512
|
+
},
|
|
1513
|
+
async getAll (type, namespace) {
|
|
1514
|
+
const byId = new Map();
|
|
1515
|
+
// Process in reverse order so closest overwrites
|
|
1516
|
+
for (const p of [
|
|
1517
|
+
...readProviders
|
|
1518
|
+
].reverse()){
|
|
1519
|
+
const entities = await p.getAll(type, namespace);
|
|
1520
|
+
for (const entity of entities){
|
|
1521
|
+
byId.set(entity.id, entity);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
return Array.from(byId.values());
|
|
1525
|
+
},
|
|
1526
|
+
find: findEntities,
|
|
1527
|
+
async exists (type, id, namespace) {
|
|
1528
|
+
for (const p of readProviders){
|
|
1529
|
+
if (await p.exists(type, id, namespace)) return true;
|
|
1530
|
+
}
|
|
1531
|
+
return false;
|
|
1532
|
+
},
|
|
1533
|
+
async count (filter) {
|
|
1534
|
+
const results = await findEntities(filter);
|
|
1535
|
+
return results.length;
|
|
1536
|
+
},
|
|
1537
|
+
// Write operations go to primary
|
|
1538
|
+
save: (entity, namespace)=>primaryProvider.save(entity, namespace),
|
|
1539
|
+
delete: (type, id, namespace)=>primaryProvider.delete(type, id, namespace),
|
|
1540
|
+
saveBatch: (entities, namespace)=>primaryProvider.saveBatch(entities, namespace),
|
|
1541
|
+
deleteBatch: (refs, namespace)=>primaryProvider.deleteBatch(refs, namespace),
|
|
1542
|
+
listNamespaces: ()=>Promise.resolve(contextRoot.allNamespaces),
|
|
1543
|
+
namespaceExists: (ns)=>Promise.resolve(contextRoot.allNamespaces.includes(ns)),
|
|
1544
|
+
listTypes: ()=>Promise.resolve(contextRoot.allTypes)
|
|
1545
|
+
};
|
|
1546
|
+
};
|
|
1547
|
+
|
|
1548
|
+
/**
|
|
1549
|
+
* Discover context directories and create an API.
|
|
1550
|
+
*/ const discoverOvercontext = async (options)=>{
|
|
1551
|
+
const { schemas, pluralNames = {}, readonly, ...rootOptions } = options;
|
|
1552
|
+
// Create registry
|
|
1553
|
+
const registry = createSchemaRegistry();
|
|
1554
|
+
for (const [type, schema] of Object.entries(schemas)){
|
|
1555
|
+
registry.register({
|
|
1556
|
+
type,
|
|
1557
|
+
schema: schema,
|
|
1558
|
+
pluralName: pluralNames[type]
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
// Discover context directories
|
|
1562
|
+
const contextRoot = await discoverContextRoot({
|
|
1563
|
+
...rootOptions,
|
|
1564
|
+
registry
|
|
1565
|
+
});
|
|
1566
|
+
if (!contextRoot.primary) {
|
|
1567
|
+
throw new Error('No context directory found');
|
|
1568
|
+
}
|
|
1569
|
+
// Create hierarchical provider
|
|
1570
|
+
const provider = await createHierarchicalProvider({
|
|
1571
|
+
contextRoot,
|
|
1572
|
+
registry,
|
|
1573
|
+
readonly
|
|
1574
|
+
});
|
|
1575
|
+
// Create context API
|
|
1576
|
+
return createContext({
|
|
1577
|
+
provider,
|
|
1578
|
+
registry,
|
|
1579
|
+
schemas,
|
|
1580
|
+
defaultNamespace: undefined
|
|
1581
|
+
});
|
|
1582
|
+
};
|
|
1583
|
+
|
|
1584
|
+
/**
|
|
1585
|
+
* Format entities for CLI output.
|
|
1586
|
+
*/ const formatEntities = (entities, options)=>{
|
|
1587
|
+
const { format, fields, noHeaders } = options;
|
|
1588
|
+
if (format === 'json') {
|
|
1589
|
+
return JSON.stringify(entities, null, 2);
|
|
1590
|
+
}
|
|
1591
|
+
if (format === 'yaml') {
|
|
1592
|
+
return yaml__namespace.dump(entities);
|
|
1593
|
+
}
|
|
1594
|
+
// Table format
|
|
1595
|
+
if (entities.length === 0) {
|
|
1596
|
+
return 'No entities found.';
|
|
1597
|
+
}
|
|
1598
|
+
const displayFields = fields || [
|
|
1599
|
+
'id',
|
|
1600
|
+
'name',
|
|
1601
|
+
'type'
|
|
1602
|
+
];
|
|
1603
|
+
const rows = [];
|
|
1604
|
+
if (!noHeaders) {
|
|
1605
|
+
rows.push(displayFields.map((f)=>f.toUpperCase()));
|
|
1606
|
+
}
|
|
1607
|
+
for (const entity of entities){
|
|
1608
|
+
const row = displayFields.map((f)=>{
|
|
1609
|
+
const value = entity[f];
|
|
1610
|
+
if (value === undefined || value === null) return '-';
|
|
1611
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
1612
|
+
return String(value);
|
|
1613
|
+
});
|
|
1614
|
+
rows.push(row);
|
|
1615
|
+
}
|
|
1616
|
+
// Calculate column widths
|
|
1617
|
+
const widths = displayFields.map((_, i)=>Math.max(...rows.map((row)=>row[i].length)));
|
|
1618
|
+
// Format rows
|
|
1619
|
+
return rows.map((row)=>row.map((cell, i)=>cell.padEnd(widths[i])).join(' ')).join('\n');
|
|
1620
|
+
};
|
|
1621
|
+
/**
|
|
1622
|
+
* Format a single entity for display.
|
|
1623
|
+
*/ const formatEntity = (entity, options)=>{
|
|
1624
|
+
if (options.format === 'json') {
|
|
1625
|
+
return JSON.stringify(entity, null, 2);
|
|
1626
|
+
}
|
|
1627
|
+
return yaml__namespace.dump(entity);
|
|
1628
|
+
};
|
|
1629
|
+
|
|
1630
|
+
/**
|
|
1631
|
+
* Build a list command handler.
|
|
1632
|
+
*/ const listCommand = async (ctx, options)=>{
|
|
1633
|
+
const result = await ctx.api.search({
|
|
1634
|
+
type: options.type,
|
|
1635
|
+
namespace: ctx.namespace,
|
|
1636
|
+
search: options.search,
|
|
1637
|
+
limit: options.limit || 20
|
|
1638
|
+
});
|
|
1639
|
+
return formatEntities(result.items, {
|
|
1640
|
+
format: ctx.outputFormat,
|
|
1641
|
+
fields: options.fields
|
|
1642
|
+
});
|
|
1643
|
+
};
|
|
1644
|
+
/**
|
|
1645
|
+
* Build a get command handler.
|
|
1646
|
+
*/ const getCommand = async (ctx, options)=>{
|
|
1647
|
+
const entity = await ctx.api.get(options.type, options.id, ctx.namespace);
|
|
1648
|
+
if (!entity) {
|
|
1649
|
+
throw new Error(`Entity not found: ${options.type}/${options.id}`);
|
|
1650
|
+
}
|
|
1651
|
+
return formatEntity(entity, {
|
|
1652
|
+
format: ctx.outputFormat
|
|
1653
|
+
});
|
|
1654
|
+
};
|
|
1655
|
+
/**
|
|
1656
|
+
* Build a create command handler.
|
|
1657
|
+
*/ const createCommand = async (ctx, options)=>{
|
|
1658
|
+
const entity = await ctx.api.create(options.type, {
|
|
1659
|
+
name: options.name,
|
|
1660
|
+
...options.data
|
|
1661
|
+
}, {
|
|
1662
|
+
id: options.id,
|
|
1663
|
+
namespace: ctx.namespace
|
|
1664
|
+
});
|
|
1665
|
+
return `Created ${options.type}: ${entity.id}`;
|
|
1666
|
+
};
|
|
1667
|
+
/**
|
|
1668
|
+
* Build an update command handler.
|
|
1669
|
+
*/ const updateCommand = async (ctx, options)=>{
|
|
1670
|
+
await ctx.api.update(options.type, options.id, options.data, ctx.namespace);
|
|
1671
|
+
return `Updated ${options.type}: ${options.id}`;
|
|
1672
|
+
};
|
|
1673
|
+
/**
|
|
1674
|
+
* Build a delete command handler.
|
|
1675
|
+
*/ const deleteCommand = async (ctx, options)=>{
|
|
1676
|
+
const deleted = await ctx.api.delete(options.type, options.id, ctx.namespace);
|
|
1677
|
+
if (!deleted) {
|
|
1678
|
+
throw new Error(`Entity not found: ${options.type}/${options.id}`);
|
|
1679
|
+
}
|
|
1680
|
+
return `Deleted ${options.type}: ${options.id}`;
|
|
1681
|
+
};
|
|
1682
|
+
|
|
1683
|
+
/**
|
|
1684
|
+
* CLI builder for consumers to create their own CLIs.
|
|
1685
|
+
*
|
|
1686
|
+
* @example
|
|
1687
|
+
* const cli = createCLIBuilder({ api });
|
|
1688
|
+
*
|
|
1689
|
+
* // In your CLI tool:
|
|
1690
|
+
* const result = await cli.list({ type: 'person' });
|
|
1691
|
+
* console.log(result);
|
|
1692
|
+
*/ const createCLIBuilder = (options)=>{
|
|
1693
|
+
const { api, defaultFormat = 'table' } = options;
|
|
1694
|
+
const createContext = (format, namespace)=>({
|
|
1695
|
+
api,
|
|
1696
|
+
outputFormat: format || defaultFormat,
|
|
1697
|
+
namespace
|
|
1698
|
+
});
|
|
1699
|
+
return {
|
|
1700
|
+
/**
|
|
1701
|
+
* List entities.
|
|
1702
|
+
*/ list: (opts)=>listCommand(createContext(opts.format, opts.namespace), opts),
|
|
1703
|
+
/**
|
|
1704
|
+
* Get a single entity.
|
|
1705
|
+
*/ get: (opts)=>getCommand(createContext(opts.format, opts.namespace), opts),
|
|
1706
|
+
/**
|
|
1707
|
+
* Create an entity.
|
|
1708
|
+
*/ create: (opts)=>createCommand(createContext(opts.format, opts.namespace), opts),
|
|
1709
|
+
/**
|
|
1710
|
+
* Update an entity.
|
|
1711
|
+
*/ update: (opts)=>updateCommand(createContext(opts.format, opts.namespace), opts),
|
|
1712
|
+
/**
|
|
1713
|
+
* Delete an entity.
|
|
1714
|
+
*/ delete: (opts)=>deleteCommand(createContext(opts.format, opts.namespace), opts),
|
|
1715
|
+
/**
|
|
1716
|
+
* List available entity types.
|
|
1717
|
+
*/ types: ()=>api.types()
|
|
1718
|
+
};
|
|
1719
|
+
};
|
|
1720
|
+
|
|
1721
|
+
exports.BaseEntitySchema = BaseEntitySchema;
|
|
1722
|
+
exports.EntityMetadataSchema = EntityMetadataSchema;
|
|
1723
|
+
exports.EntityNotFoundError = EntityNotFoundError;
|
|
1724
|
+
exports.NamespaceNotFoundError = NamespaceNotFoundError;
|
|
1725
|
+
exports.QueryBuilder = QueryBuilder;
|
|
1726
|
+
exports.ReadonlyStorageError = ReadonlyStorageError;
|
|
1727
|
+
exports.SchemaNotRegisteredError = SchemaNotRegisteredError;
|
|
1728
|
+
exports.StorageAccessError = StorageAccessError;
|
|
1729
|
+
exports.StorageError = StorageError;
|
|
1730
|
+
exports.ValidationError = ValidationError;
|
|
1731
|
+
exports.createCLIBuilder = createCLIBuilder;
|
|
1732
|
+
exports.createCommand = createCommand;
|
|
1733
|
+
exports.createContext = createContext;
|
|
1734
|
+
exports.createDirectoryWalker = createDirectoryWalker;
|
|
1735
|
+
exports.createEntitySchema = createEntitySchema;
|
|
1736
|
+
exports.createFileSystemProvider = createFileSystemProvider;
|
|
1737
|
+
exports.createHierarchicalProvider = createHierarchicalProvider;
|
|
1738
|
+
exports.createMemoryProvider = createMemoryProvider;
|
|
1739
|
+
exports.createMultiNamespaceContext = createMultiNamespaceContext;
|
|
1740
|
+
exports.createNamespaceResolver = createNamespaceResolver;
|
|
1741
|
+
exports.createObservableProvider = createObservableProvider;
|
|
1742
|
+
exports.createSchemaRegistry = createSchemaRegistry;
|
|
1743
|
+
exports.createSearchEngine = createSearchEngine;
|
|
1744
|
+
exports.createTypedAPI = createTypedAPI;
|
|
1745
|
+
exports.defineSchemas = defineSchemas;
|
|
1746
|
+
exports.deleteCommand = deleteCommand;
|
|
1747
|
+
exports.discoverContextRoot = discoverContextRoot;
|
|
1748
|
+
exports.discoverOvercontext = discoverOvercontext;
|
|
1749
|
+
exports.ensureContextRoot = ensureContextRoot;
|
|
1750
|
+
exports.formatEntities = formatEntities;
|
|
1751
|
+
exports.formatEntity = formatEntity;
|
|
1752
|
+
exports.formatValidationErrors = formatValidationErrors;
|
|
1753
|
+
exports.generateUniqueId = generateUniqueId;
|
|
1754
|
+
exports.getCommand = getCommand;
|
|
1755
|
+
exports.isBaseEntity = isBaseEntity;
|
|
1756
|
+
exports.isValidEntitySchema = isValidEntitySchema;
|
|
1757
|
+
exports.listCommand = listCommand;
|
|
1758
|
+
exports.query = query;
|
|
1759
|
+
exports.slugify = slugify;
|
|
1760
|
+
exports.updateCommand = updateCommand;
|
|
1761
|
+
exports.validateBaseEntity = validateBaseEntity;
|
|
1762
|
+
exports.validateEntity = validateEntity;
|
|
1763
|
+
//# sourceMappingURL=index.cjs.map
|