@utilarium/overcontext 0.0.5 → 0.0.6-dev.20260218224640.3065466
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/dist/discovery/hierarchical-provider.d.ts +3 -0
- package/dist/discovery/hierarchical-provider.js +5 -3
- package/dist/discovery/hierarchical-provider.js.map +1 -1
- package/dist/discovery/index.d.ts +7 -0
- package/dist/discovery/index.js +3 -2
- package/dist/discovery/index.js.map +1 -1
- package/dist/index.cjs +81 -7
- package/dist/index.cjs.map +1 -1
- package/dist/storage/filesystem.d.ts +9 -0
- package/dist/storage/filesystem.js +73 -2
- package/dist/storage/filesystem.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -126,6 +126,22 @@ const workPeople = await ctx.getAll('person', 'work');
|
|
|
126
126
|
- **Hierarchical**: Multi-level discovery with override behavior
|
|
127
127
|
- **Custom**: Implement your own
|
|
128
128
|
|
|
129
|
+
### Custom Filenames
|
|
130
|
+
|
|
131
|
+
Control how entity files are named on disk with a filename strategy:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
const ctx = await discoverOvercontext({
|
|
135
|
+
schemas: { person: PersonSchema },
|
|
136
|
+
filenameStrategy: (entity) => {
|
|
137
|
+
const slug = (entity as any).slug;
|
|
138
|
+
return slug ? `${entity.id.substring(0, 8)}-${slug}` : entity.id;
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
This produces files like `d00acdc4-gerald-corson.yaml` instead of `d00acdc4-5678-9abc-def0-111111111111.yaml`. Lookups, existence checks, and deletes work transparently regardless of the filename on disk. See the [Storage Providers](./guide/storage-providers.md#custom-filename-strategy) guide for details.
|
|
144
|
+
|
|
129
145
|
## API Overview
|
|
130
146
|
|
|
131
147
|
### CRUD Operations
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { StorageProvider } from '../storage/interface';
|
|
2
|
+
import { BaseEntity } from '../schema/base';
|
|
2
3
|
import { SchemaRegistry } from '../schema/registry';
|
|
3
4
|
import { ContextRoot } from './context-root';
|
|
4
5
|
export interface HierarchicalProviderOptions {
|
|
5
6
|
contextRoot: ContextRoot;
|
|
6
7
|
registry: SchemaRegistry;
|
|
7
8
|
readonly?: boolean;
|
|
9
|
+
/** Custom filename strategy passed through to filesystem providers */
|
|
10
|
+
filenameStrategy?: (entity: BaseEntity) => string;
|
|
8
11
|
}
|
|
9
12
|
/**
|
|
10
13
|
* Storage provider that reads from multiple context directories
|
|
@@ -4,7 +4,7 @@ import { createFileSystemProvider } from '../storage/filesystem.js';
|
|
|
4
4
|
* Storage provider that reads from multiple context directories
|
|
5
5
|
* and writes to the primary (closest) directory.
|
|
6
6
|
*/ const createHierarchicalProvider = async (options)=>{
|
|
7
|
-
const { contextRoot, registry, readonly = false } = options;
|
|
7
|
+
const { contextRoot, registry, readonly = false, filenameStrategy } = options;
|
|
8
8
|
if (contextRoot.contextPaths.length === 0) {
|
|
9
9
|
throw new Error('No context directories found');
|
|
10
10
|
}
|
|
@@ -15,7 +15,8 @@ import { createFileSystemProvider } from '../storage/filesystem.js';
|
|
|
15
15
|
basePath: contextPath,
|
|
16
16
|
registry,
|
|
17
17
|
createIfMissing: false,
|
|
18
|
-
readonly: true
|
|
18
|
+
readonly: true,
|
|
19
|
+
filenameStrategy
|
|
19
20
|
});
|
|
20
21
|
await fsProvider.initialize();
|
|
21
22
|
readProviders.push(fsProvider);
|
|
@@ -25,7 +26,8 @@ import { createFileSystemProvider } from '../storage/filesystem.js';
|
|
|
25
26
|
basePath: contextRoot.primary,
|
|
26
27
|
registry,
|
|
27
28
|
createIfMissing: true,
|
|
28
|
-
readonly
|
|
29
|
+
readonly,
|
|
30
|
+
filenameStrategy
|
|
29
31
|
});
|
|
30
32
|
await primaryProvider.initialize();
|
|
31
33
|
const findEntities = async (filter)=>{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"hierarchical-provider.js","sources":["../../src/discovery/hierarchical-provider.ts"],"sourcesContent":["import { StorageProvider, EntityFilter } from '../storage/interface';\nimport { BaseEntity } from '../schema/base';\nimport { SchemaRegistry } from '../schema/registry';\nimport { createFileSystemProvider } from '../storage/filesystem';\nimport { ContextRoot } from './context-root';\n\nexport interface HierarchicalProviderOptions {\n contextRoot: ContextRoot;\n registry: SchemaRegistry;\n readonly?: boolean;\n}\n\n/**\n * Storage provider that reads from multiple context directories\n * and writes to the primary (closest) directory.\n */\nexport const createHierarchicalProvider = async (\n options: HierarchicalProviderOptions\n): Promise<StorageProvider> => {\n const { contextRoot, registry, readonly = false } = options;\n\n if (contextRoot.contextPaths.length === 0) {\n throw new Error('No context directories found');\n }\n\n // Create read-only providers for each context directory\n const readProviders: StorageProvider[] = [];\n for (const contextPath of contextRoot.contextPaths) {\n const fsProvider = await createFileSystemProvider({\n basePath: contextPath,\n registry,\n createIfMissing: false,\n readonly: true,\n });\n await fsProvider.initialize();\n readProviders.push(fsProvider);\n }\n\n // Primary provider for writes\n const primaryProvider = await createFileSystemProvider({\n basePath: contextRoot.primary!,\n registry,\n createIfMissing: true,\n readonly,\n });\n await primaryProvider.initialize();\n\n const findEntities = async <T extends BaseEntity>(filter: EntityFilter): Promise<T[]> => {\n const byId = new Map<string, T>();\n\n // Create a filter without pagination - we'll apply pagination after merging\n // This prevents double-pagination (sub-providers paginating, then us paginating again)\n const filterWithoutPagination: EntityFilter = {\n type: filter.type,\n namespace: filter.namespace,\n ids: filter.ids,\n search: filter.search,\n };\n\n for (const p of [...readProviders].reverse()) {\n const results = await p.find<T>(filterWithoutPagination);\n for (const entity of results) {\n byId.set(entity.id, entity);\n }\n }\n\n let results = Array.from(byId.values());\n\n // Apply pagination once, after merging all results\n if (filter.offset) results = results.slice(filter.offset);\n if (filter.limit) results = results.slice(0, filter.limit);\n\n return results;\n };\n\n return {\n name: 'hierarchical',\n location: contextRoot.primary!,\n registry,\n\n async initialize() { },\n\n async dispose() {\n for (const p of readProviders) await p.dispose();\n await primaryProvider.dispose();\n },\n\n async isAvailable() {\n return primaryProvider.isAvailable();\n },\n\n // Read operations search all providers (closest first)\n async get<T extends BaseEntity>(type: string, id: string, namespace?: string) {\n for (const p of readProviders) {\n const entity = await p.get<T>(type, id, namespace);\n if (entity) return entity;\n }\n return undefined;\n },\n\n async getAll<T extends BaseEntity>(type: string, namespace?: string) {\n const byId = new Map<string, T>();\n\n // Process in reverse order so closest overwrites\n for (const p of [...readProviders].reverse()) {\n const entities = await p.getAll<T>(type, namespace);\n for (const entity of entities) {\n byId.set(entity.id, entity);\n }\n }\n\n return Array.from(byId.values());\n },\n\n find: findEntities,\n\n async exists(type: string, id: string, namespace?: string) {\n for (const p of readProviders) {\n if (await p.exists(type, id, namespace)) return true;\n }\n return false;\n },\n\n async count(filter: EntityFilter) {\n const results = await findEntities(filter);\n return results.length;\n },\n\n // Write operations go to primary\n save: (entity, namespace) => primaryProvider.save(entity, namespace),\n delete: (type, id, namespace) => primaryProvider.delete(type, id, namespace),\n saveBatch: (entities, namespace) => primaryProvider.saveBatch(entities, namespace),\n deleteBatch: (refs, namespace) => primaryProvider.deleteBatch(refs, namespace),\n\n listNamespaces: () => Promise.resolve(contextRoot.allNamespaces),\n namespaceExists: (ns) => Promise.resolve(contextRoot.allNamespaces.includes(ns)),\n listTypes: () => Promise.resolve(contextRoot.allTypes),\n };\n};\n"],"names":["createHierarchicalProvider","options","contextRoot","registry","readonly","contextPaths","length","Error","readProviders","contextPath","fsProvider","createFileSystemProvider","basePath","createIfMissing","initialize","push","primaryProvider","primary","findEntities","filter","byId","Map","filterWithoutPagination","type","namespace","ids","search","p","reverse","results","find","entity","set","id","Array","from","values","offset","slice","limit","name","location","dispose","isAvailable","get","undefined","getAll","entities","exists","count","save","delete","saveBatch","deleteBatch","refs","listNamespaces","Promise","resolve","allNamespaces","namespaceExists","ns","includes","listTypes","allTypes"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"hierarchical-provider.js","sources":["../../src/discovery/hierarchical-provider.ts"],"sourcesContent":["import { StorageProvider, EntityFilter } from '../storage/interface';\nimport { BaseEntity } from '../schema/base';\nimport { SchemaRegistry } from '../schema/registry';\nimport { createFileSystemProvider } from '../storage/filesystem';\nimport { ContextRoot } from './context-root';\n\nexport interface HierarchicalProviderOptions {\n contextRoot: ContextRoot;\n registry: SchemaRegistry;\n readonly?: boolean;\n /** Custom filename strategy passed through to filesystem providers */\n filenameStrategy?: (entity: BaseEntity) => string;\n}\n\n/**\n * Storage provider that reads from multiple context directories\n * and writes to the primary (closest) directory.\n */\nexport const createHierarchicalProvider = async (\n options: HierarchicalProviderOptions\n): Promise<StorageProvider> => {\n const { contextRoot, registry, readonly = false, filenameStrategy } = options;\n\n if (contextRoot.contextPaths.length === 0) {\n throw new Error('No context directories found');\n }\n\n // Create read-only providers for each context directory\n const readProviders: StorageProvider[] = [];\n for (const contextPath of contextRoot.contextPaths) {\n const fsProvider = await createFileSystemProvider({\n basePath: contextPath,\n registry,\n createIfMissing: false,\n readonly: true,\n filenameStrategy,\n });\n await fsProvider.initialize();\n readProviders.push(fsProvider);\n }\n\n // Primary provider for writes\n const primaryProvider = await createFileSystemProvider({\n basePath: contextRoot.primary!,\n registry,\n createIfMissing: true,\n readonly,\n filenameStrategy,\n });\n await primaryProvider.initialize();\n\n const findEntities = async <T extends BaseEntity>(filter: EntityFilter): Promise<T[]> => {\n const byId = new Map<string, T>();\n\n // Create a filter without pagination - we'll apply pagination after merging\n // This prevents double-pagination (sub-providers paginating, then us paginating again)\n const filterWithoutPagination: EntityFilter = {\n type: filter.type,\n namespace: filter.namespace,\n ids: filter.ids,\n search: filter.search,\n };\n\n for (const p of [...readProviders].reverse()) {\n const results = await p.find<T>(filterWithoutPagination);\n for (const entity of results) {\n byId.set(entity.id, entity);\n }\n }\n\n let results = Array.from(byId.values());\n\n // Apply pagination once, after merging all results\n if (filter.offset) results = results.slice(filter.offset);\n if (filter.limit) results = results.slice(0, filter.limit);\n\n return results;\n };\n\n return {\n name: 'hierarchical',\n location: contextRoot.primary!,\n registry,\n\n async initialize() { },\n\n async dispose() {\n for (const p of readProviders) await p.dispose();\n await primaryProvider.dispose();\n },\n\n async isAvailable() {\n return primaryProvider.isAvailable();\n },\n\n // Read operations search all providers (closest first)\n async get<T extends BaseEntity>(type: string, id: string, namespace?: string) {\n for (const p of readProviders) {\n const entity = await p.get<T>(type, id, namespace);\n if (entity) return entity;\n }\n return undefined;\n },\n\n async getAll<T extends BaseEntity>(type: string, namespace?: string) {\n const byId = new Map<string, T>();\n\n // Process in reverse order so closest overwrites\n for (const p of [...readProviders].reverse()) {\n const entities = await p.getAll<T>(type, namespace);\n for (const entity of entities) {\n byId.set(entity.id, entity);\n }\n }\n\n return Array.from(byId.values());\n },\n\n find: findEntities,\n\n async exists(type: string, id: string, namespace?: string) {\n for (const p of readProviders) {\n if (await p.exists(type, id, namespace)) return true;\n }\n return false;\n },\n\n async count(filter: EntityFilter) {\n const results = await findEntities(filter);\n return results.length;\n },\n\n // Write operations go to primary\n save: (entity, namespace) => primaryProvider.save(entity, namespace),\n delete: (type, id, namespace) => primaryProvider.delete(type, id, namespace),\n saveBatch: (entities, namespace) => primaryProvider.saveBatch(entities, namespace),\n deleteBatch: (refs, namespace) => primaryProvider.deleteBatch(refs, namespace),\n\n listNamespaces: () => Promise.resolve(contextRoot.allNamespaces),\n namespaceExists: (ns) => Promise.resolve(contextRoot.allNamespaces.includes(ns)),\n listTypes: () => Promise.resolve(contextRoot.allTypes),\n };\n};\n"],"names":["createHierarchicalProvider","options","contextRoot","registry","readonly","filenameStrategy","contextPaths","length","Error","readProviders","contextPath","fsProvider","createFileSystemProvider","basePath","createIfMissing","initialize","push","primaryProvider","primary","findEntities","filter","byId","Map","filterWithoutPagination","type","namespace","ids","search","p","reverse","results","find","entity","set","id","Array","from","values","offset","slice","limit","name","location","dispose","isAvailable","get","undefined","getAll","entities","exists","count","save","delete","saveBatch","deleteBatch","refs","listNamespaces","Promise","resolve","allNamespaces","namespaceExists","ns","includes","listTypes","allTypes"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SchemaMap } from '../schema/registry';
|
|
2
|
+
import { BaseEntity } from '../schema/base';
|
|
2
3
|
import { ContextRootOptions } from './context-root';
|
|
3
4
|
import { OvercontextAPI } from '../api/context';
|
|
4
5
|
export * from './walker';
|
|
@@ -11,6 +12,12 @@ export interface DiscoverOptions<TSchemas extends SchemaMap> extends ContextRoot
|
|
|
11
12
|
pluralNames?: Partial<Record<keyof TSchemas, string>>;
|
|
12
13
|
/** Whether context is readonly */
|
|
13
14
|
readonly?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Custom filename strategy for entity files.
|
|
17
|
+
* Given an entity, returns the filename stem (without extension).
|
|
18
|
+
* When not provided, entity.id is used as the filename.
|
|
19
|
+
*/
|
|
20
|
+
filenameStrategy?: (entity: BaseEntity) => string;
|
|
14
21
|
}
|
|
15
22
|
/**
|
|
16
23
|
* Discover context directories and create an API.
|
package/dist/discovery/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { createContext } from '../api/context.js';
|
|
|
10
10
|
/**
|
|
11
11
|
* Discover context directories and create an API.
|
|
12
12
|
*/ const discoverOvercontext = async (options)=>{
|
|
13
|
-
const { schemas, pluralNames = {}, readonly, ...rootOptions } = options;
|
|
13
|
+
const { schemas, pluralNames = {}, readonly, filenameStrategy, ...rootOptions } = options;
|
|
14
14
|
// Create registry
|
|
15
15
|
const registry = createSchemaRegistry();
|
|
16
16
|
for (const [type, schema] of Object.entries(schemas)){
|
|
@@ -32,7 +32,8 @@ import { createContext } from '../api/context.js';
|
|
|
32
32
|
const provider = await createHierarchicalProvider({
|
|
33
33
|
contextRoot,
|
|
34
34
|
registry,
|
|
35
|
-
readonly
|
|
35
|
+
readonly,
|
|
36
|
+
filenameStrategy
|
|
36
37
|
});
|
|
37
38
|
// Create context API
|
|
38
39
|
return createContext({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../src/discovery/index.ts"],"sourcesContent":["export * from './walker';\nexport * from './context-root';\nexport * from './hierarchical-provider';\n\nimport { z } from 'zod';\nimport { SchemaMap, createSchemaRegistry } from '../schema/registry';\nimport { BaseEntity } from '../schema/base';\nimport { discoverContextRoot, ContextRootOptions } from './context-root';\nimport { createHierarchicalProvider } from './hierarchical-provider';\nimport { createContext, OvercontextAPI } from '../api/context';\n\nexport interface DiscoverOptions<TSchemas extends SchemaMap> extends ContextRootOptions {\n /** Schemas to register */\n schemas: TSchemas;\n\n /** Custom plural names for types */\n pluralNames?: Partial<Record<keyof TSchemas, string>>;\n\n /** Whether context is readonly */\n readonly?: boolean;\n}\n\n/**\n * Discover context directories and create an API.\n */\nexport const discoverOvercontext = async <TSchemas extends SchemaMap>(\n options: DiscoverOptions<TSchemas>\n): Promise<OvercontextAPI<TSchemas>> => {\n const { schemas, pluralNames = {}, readonly, ...rootOptions } = options;\n\n // Create registry\n const registry = createSchemaRegistry();\n for (const [type, schema] of Object.entries(schemas)) {\n registry.register({\n type,\n schema: schema as z.ZodType<BaseEntity>,\n pluralName: (pluralNames as Record<string, string>)[type],\n });\n }\n\n // Discover context directories\n const contextRoot = await discoverContextRoot({\n ...rootOptions,\n registry,\n });\n\n if (!contextRoot.primary) {\n throw new Error('No context directory found');\n }\n\n // Create hierarchical provider\n const provider = await createHierarchicalProvider({\n contextRoot,\n registry,\n readonly,\n });\n\n // Create context API\n return createContext({\n provider,\n registry,\n schemas,\n defaultNamespace: undefined,\n });\n};\n"],"names":["discoverOvercontext","options","schemas","pluralNames","readonly","rootOptions","registry","createSchemaRegistry","type","schema","Object","entries","register","pluralName","contextRoot","discoverContextRoot","primary","Error","provider","createHierarchicalProvider","createContext","defaultNamespace","undefined"],"mappings":";;;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../src/discovery/index.ts"],"sourcesContent":["export * from './walker';\nexport * from './context-root';\nexport * from './hierarchical-provider';\n\nimport { z } from 'zod';\nimport { SchemaMap, createSchemaRegistry } from '../schema/registry';\nimport { BaseEntity } from '../schema/base';\nimport { discoverContextRoot, ContextRootOptions } from './context-root';\nimport { createHierarchicalProvider } from './hierarchical-provider';\nimport { createContext, OvercontextAPI } from '../api/context';\n\nexport interface DiscoverOptions<TSchemas extends SchemaMap> extends ContextRootOptions {\n /** Schemas to register */\n schemas: TSchemas;\n\n /** Custom plural names for types */\n pluralNames?: Partial<Record<keyof TSchemas, string>>;\n\n /** Whether context is readonly */\n readonly?: boolean;\n\n /**\n * Custom filename strategy for entity files.\n * Given an entity, returns the filename stem (without extension).\n * When not provided, entity.id is used as the filename.\n */\n filenameStrategy?: (entity: BaseEntity) => string;\n}\n\n/**\n * Discover context directories and create an API.\n */\nexport const discoverOvercontext = async <TSchemas extends SchemaMap>(\n options: DiscoverOptions<TSchemas>\n): Promise<OvercontextAPI<TSchemas>> => {\n const { schemas, pluralNames = {}, readonly, filenameStrategy, ...rootOptions } = options;\n\n // Create registry\n const registry = createSchemaRegistry();\n for (const [type, schema] of Object.entries(schemas)) {\n registry.register({\n type,\n schema: schema as z.ZodType<BaseEntity>,\n pluralName: (pluralNames as Record<string, string>)[type],\n });\n }\n\n // Discover context directories\n const contextRoot = await discoverContextRoot({\n ...rootOptions,\n registry,\n });\n\n if (!contextRoot.primary) {\n throw new Error('No context directory found');\n }\n\n // Create hierarchical provider\n const provider = await createHierarchicalProvider({\n contextRoot,\n registry,\n readonly,\n filenameStrategy,\n });\n\n // Create context API\n return createContext({\n provider,\n registry,\n schemas,\n defaultNamespace: undefined,\n });\n};\n"],"names":["discoverOvercontext","options","schemas","pluralNames","readonly","filenameStrategy","rootOptions","registry","createSchemaRegistry","type","schema","Object","entries","register","pluralName","contextRoot","discoverContextRoot","primary","Error","provider","createHierarchicalProvider","createContext","defaultNamespace","undefined"],"mappings":";;;;;;;;;AA6BA;;IAGO,MAAMA,mBAAAA,GAAsB,OAC/BC,OAAAA,GAAAA;AAEA,IAAA,MAAM,EAAEC,OAAO,EAAEC,WAAAA,GAAc,EAAE,EAAEC,QAAQ,EAAEC,gBAAgB,EAAE,GAAGC,aAAa,GAAGL,OAAAA;;AAGlF,IAAA,MAAMM,QAAAA,GAAWC,oBAAAA,EAAAA;IACjB,KAAK,MAAM,CAACC,IAAAA,EAAMC,MAAAA,CAAO,IAAIC,MAAAA,CAAOC,OAAO,CAACV,OAAAA,CAAAA,CAAU;AAClDK,QAAAA,QAAAA,CAASM,QAAQ,CAAC;AACdJ,YAAAA,IAAAA;YACAC,MAAAA,EAAQA,MAAAA;YACRI,UAAAA,EAAaX,WAAsC,CAACM,IAAAA;AACxD,SAAA,CAAA;AACJ,IAAA;;IAGA,MAAMM,WAAAA,GAAc,MAAMC,mBAAAA,CAAoB;AAC1C,QAAA,GAAGV,WAAW;AACdC,QAAAA;AACJ,KAAA,CAAA;IAEA,IAAI,CAACQ,WAAAA,CAAYE,OAAO,EAAE;AACtB,QAAA,MAAM,IAAIC,KAAAA,CAAM,4BAAA,CAAA;AACpB,IAAA;;IAGA,MAAMC,QAAAA,GAAW,MAAMC,0BAAAA,CAA2B;AAC9CL,QAAAA,WAAAA;AACAR,QAAAA,QAAAA;AACAH,QAAAA,QAAAA;AACAC,QAAAA;AACJ,KAAA,CAAA;;AAGA,IAAA,OAAOgB,aAAAA,CAAc;AACjBF,QAAAA,QAAAA;AACAZ,QAAAA,QAAAA;AACAL,QAAAA,OAAAA;QACAoB,gBAAAA,EAAkBC;AACtB,KAAA,CAAA;AACJ;;;;"}
|
package/dist/index.cjs
CHANGED
|
@@ -412,7 +412,7 @@ class NamespaceNotFoundError extends StorageError {
|
|
|
412
412
|
};
|
|
413
413
|
|
|
414
414
|
const createFileSystemProvider = async (options)=>{
|
|
415
|
-
const { basePath, registry, createIfMissing = true, extension = '.yaml', readonly = false, defaultNamespace } = options;
|
|
415
|
+
const { basePath, registry, createIfMissing = true, extension = '.yaml', readonly = false, defaultNamespace, filenameStrategy } = options;
|
|
416
416
|
// --- Helper Functions ---
|
|
417
417
|
/**
|
|
418
418
|
* Sanitize a path component to prevent directory traversal attacks.
|
|
@@ -487,6 +487,53 @@ const createFileSystemProvider = async (options)=>{
|
|
|
487
487
|
verifyPathWithinBase(fullPath);
|
|
488
488
|
return fullPath;
|
|
489
489
|
};
|
|
490
|
+
const getEntityPathForWrite = (entity, namespace)=>{
|
|
491
|
+
if (!filenameStrategy) {
|
|
492
|
+
return getEntityPath(entity.type, entity.id, namespace);
|
|
493
|
+
}
|
|
494
|
+
const filename = filenameStrategy(entity);
|
|
495
|
+
const safeFilename = sanitizePathComponent(filename, 'filename');
|
|
496
|
+
const dir = getEntityDir(entity.type, namespace);
|
|
497
|
+
const fullPath = path__namespace.join(dir, `${safeFilename}${extension}`);
|
|
498
|
+
verifyPathWithinBase(fullPath);
|
|
499
|
+
return fullPath;
|
|
500
|
+
};
|
|
501
|
+
/**
|
|
502
|
+
* Find the actual file path for an entity by id, handling custom filename strategies.
|
|
503
|
+
* Tries direct {id}{ext} first, then scans the directory using id prefix matching.
|
|
504
|
+
*/ const findEntityFileById = async (type, id, namespace)=>{
|
|
505
|
+
const directPath = getEntityPath(type, id, namespace);
|
|
506
|
+
if (node_fs.existsSync(directPath)) {
|
|
507
|
+
return directPath;
|
|
508
|
+
}
|
|
509
|
+
if (!filenameStrategy) {
|
|
510
|
+
return undefined;
|
|
511
|
+
}
|
|
512
|
+
let dir;
|
|
513
|
+
try {
|
|
514
|
+
dir = getEntityDir(type, namespace);
|
|
515
|
+
} catch {
|
|
516
|
+
return undefined;
|
|
517
|
+
}
|
|
518
|
+
if (!node_fs.existsSync(dir)) {
|
|
519
|
+
return undefined;
|
|
520
|
+
}
|
|
521
|
+
const prefix = id.substring(0, Math.min(8, id.length));
|
|
522
|
+
const files = await fs__namespace.readdir(dir);
|
|
523
|
+
const candidates = files.filter((f)=>(f.endsWith('.yaml') || f.endsWith('.yml')) && f.startsWith(prefix));
|
|
524
|
+
if (candidates.length === 1) {
|
|
525
|
+
return path__namespace.join(dir, candidates[0]);
|
|
526
|
+
}
|
|
527
|
+
// Multiple prefix matches — read and verify the id field
|
|
528
|
+
for (const file of candidates){
|
|
529
|
+
const filePath = path__namespace.join(dir, file);
|
|
530
|
+
const entity = await readEntity(filePath, type);
|
|
531
|
+
if (entity && entity.id === id) {
|
|
532
|
+
return filePath;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return undefined;
|
|
536
|
+
};
|
|
490
537
|
const ensureDir = async (dir)=>{
|
|
491
538
|
if (!node_fs.existsSync(dir) && createIfMissing && !readonly) {
|
|
492
539
|
await fs__namespace.mkdir(dir, {
|
|
@@ -545,7 +592,16 @@ const createFileSystemProvider = async (options)=>{
|
|
|
545
592
|
}
|
|
546
593
|
const dir = getEntityDir(entity.type, namespace);
|
|
547
594
|
await ensureDir(dir);
|
|
548
|
-
const filePath =
|
|
595
|
+
const filePath = getEntityPathForWrite(entity, namespace);
|
|
596
|
+
// If filenameStrategy is set, clean up any old file with a different name for the same id
|
|
597
|
+
if (filenameStrategy) {
|
|
598
|
+
const oldPath = await findEntityFileById(entity.type, entity.id, namespace);
|
|
599
|
+
if (oldPath && path__namespace.resolve(oldPath) !== path__namespace.resolve(filePath)) {
|
|
600
|
+
try {
|
|
601
|
+
await fs__namespace.unlink(oldPath);
|
|
602
|
+
} catch {}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
549
605
|
// Remove framework-managed fields from saved YAML
|
|
550
606
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
551
607
|
const { type: _type, source: _source, ...entityToSave } = entity;
|
|
@@ -613,6 +669,11 @@ const createFileSystemProvider = async (options)=>{
|
|
|
613
669
|
},
|
|
614
670
|
async get (type, id, namespace) {
|
|
615
671
|
const ns = namespace !== null && namespace !== void 0 ? namespace : defaultNamespace;
|
|
672
|
+
if (filenameStrategy) {
|
|
673
|
+
const filePath = await findEntityFileById(type, id, ns);
|
|
674
|
+
if (!filePath) return undefined;
|
|
675
|
+
return readEntity(filePath, type);
|
|
676
|
+
}
|
|
616
677
|
const filePath = getEntityPath(type, id, ns);
|
|
617
678
|
return readEntity(filePath, type);
|
|
618
679
|
},
|
|
@@ -676,6 +737,10 @@ const createFileSystemProvider = async (options)=>{
|
|
|
676
737
|
async exists (type, id, namespace) {
|
|
677
738
|
const ns = namespace !== null && namespace !== void 0 ? namespace : defaultNamespace;
|
|
678
739
|
try {
|
|
740
|
+
if (filenameStrategy) {
|
|
741
|
+
const filePath = await findEntityFileById(type, id, ns);
|
|
742
|
+
return filePath !== undefined;
|
|
743
|
+
}
|
|
679
744
|
const filePath = getEntityPath(type, id, ns);
|
|
680
745
|
return node_fs.existsSync(filePath);
|
|
681
746
|
} catch {
|
|
@@ -696,6 +761,12 @@ const createFileSystemProvider = async (options)=>{
|
|
|
696
761
|
}
|
|
697
762
|
const ns = namespace !== null && namespace !== void 0 ? namespace : defaultNamespace;
|
|
698
763
|
try {
|
|
764
|
+
if (filenameStrategy) {
|
|
765
|
+
const filePath = await findEntityFileById(type, id, ns);
|
|
766
|
+
if (!filePath) return false;
|
|
767
|
+
await fs__namespace.unlink(filePath);
|
|
768
|
+
return true;
|
|
769
|
+
}
|
|
699
770
|
const filePath = getEntityPath(type, id, ns);
|
|
700
771
|
await fs__namespace.unlink(filePath);
|
|
701
772
|
return true;
|
|
@@ -1595,7 +1666,7 @@ const discoverContextRoot = async (options = {})=>{
|
|
|
1595
1666
|
* Storage provider that reads from multiple context directories
|
|
1596
1667
|
* and writes to the primary (closest) directory.
|
|
1597
1668
|
*/ const createHierarchicalProvider = async (options)=>{
|
|
1598
|
-
const { contextRoot, registry, readonly = false } = options;
|
|
1669
|
+
const { contextRoot, registry, readonly = false, filenameStrategy } = options;
|
|
1599
1670
|
if (contextRoot.contextPaths.length === 0) {
|
|
1600
1671
|
throw new Error('No context directories found');
|
|
1601
1672
|
}
|
|
@@ -1606,7 +1677,8 @@ const discoverContextRoot = async (options = {})=>{
|
|
|
1606
1677
|
basePath: contextPath,
|
|
1607
1678
|
registry,
|
|
1608
1679
|
createIfMissing: false,
|
|
1609
|
-
readonly: true
|
|
1680
|
+
readonly: true,
|
|
1681
|
+
filenameStrategy
|
|
1610
1682
|
});
|
|
1611
1683
|
await fsProvider.initialize();
|
|
1612
1684
|
readProviders.push(fsProvider);
|
|
@@ -1616,7 +1688,8 @@ const discoverContextRoot = async (options = {})=>{
|
|
|
1616
1688
|
basePath: contextRoot.primary,
|
|
1617
1689
|
registry,
|
|
1618
1690
|
createIfMissing: true,
|
|
1619
|
-
readonly
|
|
1691
|
+
readonly,
|
|
1692
|
+
filenameStrategy
|
|
1620
1693
|
});
|
|
1621
1694
|
await primaryProvider.initialize();
|
|
1622
1695
|
const findEntities = async (filter)=>{
|
|
@@ -1701,7 +1774,7 @@ const discoverContextRoot = async (options = {})=>{
|
|
|
1701
1774
|
/**
|
|
1702
1775
|
* Discover context directories and create an API.
|
|
1703
1776
|
*/ const discoverOvercontext = async (options)=>{
|
|
1704
|
-
const { schemas, pluralNames = {}, readonly, ...rootOptions } = options;
|
|
1777
|
+
const { schemas, pluralNames = {}, readonly, filenameStrategy, ...rootOptions } = options;
|
|
1705
1778
|
// Create registry
|
|
1706
1779
|
const registry = createSchemaRegistry();
|
|
1707
1780
|
for (const [type, schema] of Object.entries(schemas)){
|
|
@@ -1723,7 +1796,8 @@ const discoverContextRoot = async (options = {})=>{
|
|
|
1723
1796
|
const provider = await createHierarchicalProvider({
|
|
1724
1797
|
contextRoot,
|
|
1725
1798
|
registry,
|
|
1726
|
-
readonly
|
|
1799
|
+
readonly,
|
|
1800
|
+
filenameStrategy
|
|
1727
1801
|
});
|
|
1728
1802
|
// Create context API
|
|
1729
1803
|
return createContext({
|