@utilarium/overcontext 0.0.4-dev.0 → 0.0.5-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/dist/api/query-builder.d.ts +3 -0
- package/dist/api/query-builder.js +11 -4
- package/dist/api/query-builder.js.map +1 -1
- package/dist/api/search.js +33 -3
- package/dist/api/search.js.map +1 -1
- package/dist/discovery/hierarchical-provider.js +10 -1
- package/dist/discovery/hierarchical-provider.js.map +1 -1
- package/dist/discovery/walker.js +14 -0
- package/dist/discovery/walker.js.map +1 -1
- package/dist/index.cjs +173 -20
- package/dist/index.cjs.map +1 -1
- package/dist/schema/base.js +3 -1
- package/dist/schema/base.js.map +1 -1
- package/dist/storage/filesystem.js +87 -7
- package/dist/storage/filesystem.js.map +1 -1
- package/dist/storage/memory.js +16 -4
- package/dist/storage/memory.js.map +1 -1
- package/dist/storage/observable.js +1 -0
- package/dist/storage/observable.js.map +1 -1
- package/package.json +1 -1
|
@@ -30,14 +30,17 @@ export declare class QueryBuilder {
|
|
|
30
30
|
sortBy(field: string, direction?: SortDirection): this;
|
|
31
31
|
/**
|
|
32
32
|
* Set result limit.
|
|
33
|
+
* Must be a positive integer (minimum 1).
|
|
33
34
|
*/
|
|
34
35
|
limit(n: number): this;
|
|
35
36
|
/**
|
|
36
37
|
* Set result offset.
|
|
38
|
+
* Must be a non-negative integer (minimum 0).
|
|
37
39
|
*/
|
|
38
40
|
offset(n: number): this;
|
|
39
41
|
/**
|
|
40
42
|
* Set page (calculates offset from limit).
|
|
43
|
+
* Page numbers are 1-indexed (first page is 1).
|
|
41
44
|
*/
|
|
42
45
|
page(pageNum: number, pageSize: number): this;
|
|
43
46
|
/**
|
|
@@ -59,21 +59,28 @@ function _define_property(obj, key, value) {
|
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
61
|
* Set result limit.
|
|
62
|
+
* Must be a positive integer (minimum 1).
|
|
62
63
|
*/ limit(n) {
|
|
63
|
-
this.options.limit = n;
|
|
64
|
+
this.options.limit = Math.max(1, Math.floor(n));
|
|
64
65
|
return this;
|
|
65
66
|
}
|
|
66
67
|
/**
|
|
67
68
|
* Set result offset.
|
|
69
|
+
* Must be a non-negative integer (minimum 0).
|
|
68
70
|
*/ offset(n) {
|
|
69
|
-
this.options.offset = n;
|
|
71
|
+
this.options.offset = Math.max(0, Math.floor(n));
|
|
70
72
|
return this;
|
|
71
73
|
}
|
|
72
74
|
/**
|
|
73
75
|
* Set page (calculates offset from limit).
|
|
76
|
+
* Page numbers are 1-indexed (first page is 1).
|
|
74
77
|
*/ page(pageNum, pageSize) {
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
// Ensure valid page number (minimum 1)
|
|
79
|
+
const safePage = Math.max(1, Math.floor(pageNum));
|
|
80
|
+
// Ensure valid page size (minimum 1)
|
|
81
|
+
const safeSize = Math.max(1, Math.floor(pageSize));
|
|
82
|
+
this.options.limit = safeSize;
|
|
83
|
+
this.options.offset = (safePage - 1) * safeSize;
|
|
77
84
|
return this;
|
|
78
85
|
}
|
|
79
86
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-builder.js","sources":["../../src/api/query-builder.ts"],"sourcesContent":["import { QueryOptions, SortDirection } from './query';\n\n/**\n * Fluent query builder for constructing search queries.\n */\nexport class QueryBuilder {\n private options: QueryOptions = {};\n\n /**\n * Filter by entity type(s).\n */\n type(type: string | string[]): this {\n this.options.type = type;\n return this;\n }\n\n /**\n * Filter by namespace(s).\n */\n namespace(ns: string | string[]): this {\n this.options.namespace = ns;\n return this;\n }\n\n /**\n * Filter by specific IDs.\n */\n ids(ids: string[]): this {\n this.options.ids = ids;\n return this;\n }\n\n /**\n * Add text search.\n */\n search(query: string, fields?: string[]): this {\n this.options.search = query;\n if (fields) this.options.searchFields = fields;\n return this;\n }\n\n /**\n * Enable case-sensitive search.\n */\n caseSensitive(enabled: boolean = true): this {\n this.options.caseSensitive = enabled;\n return this;\n }\n\n /**\n * Add sort field.\n */\n sortBy(field: string, direction: SortDirection = 'asc'): this {\n this.options.sort = [...(this.options.sort || []), { field, direction }];\n return this;\n }\n\n /**\n * Set result limit.\n */\n limit(n: number): this {\n this.options.limit = n;\n return this;\n }\n\n /**\n * Set result offset.\n */\n offset(n: number): this {\n this.options.offset = n;\n return this;\n }\n\n /**\n * Set page (calculates offset from limit).\n */\n page(pageNum: number, pageSize: number): this {\n this.options.limit =
|
|
1
|
+
{"version":3,"file":"query-builder.js","sources":["../../src/api/query-builder.ts"],"sourcesContent":["import { QueryOptions, SortDirection } from './query';\n\n/**\n * Fluent query builder for constructing search queries.\n */\nexport class QueryBuilder {\n private options: QueryOptions = {};\n\n /**\n * Filter by entity type(s).\n */\n type(type: string | string[]): this {\n this.options.type = type;\n return this;\n }\n\n /**\n * Filter by namespace(s).\n */\n namespace(ns: string | string[]): this {\n this.options.namespace = ns;\n return this;\n }\n\n /**\n * Filter by specific IDs.\n */\n ids(ids: string[]): this {\n this.options.ids = ids;\n return this;\n }\n\n /**\n * Add text search.\n */\n search(query: string, fields?: string[]): this {\n this.options.search = query;\n if (fields) this.options.searchFields = fields;\n return this;\n }\n\n /**\n * Enable case-sensitive search.\n */\n caseSensitive(enabled: boolean = true): this {\n this.options.caseSensitive = enabled;\n return this;\n }\n\n /**\n * Add sort field.\n */\n sortBy(field: string, direction: SortDirection = 'asc'): this {\n this.options.sort = [...(this.options.sort || []), { field, direction }];\n return this;\n }\n\n /**\n * Set result limit.\n * Must be a positive integer (minimum 1).\n */\n limit(n: number): this {\n this.options.limit = Math.max(1, Math.floor(n));\n return this;\n }\n\n /**\n * Set result offset.\n * Must be a non-negative integer (minimum 0).\n */\n offset(n: number): this {\n this.options.offset = Math.max(0, Math.floor(n));\n return this;\n }\n\n /**\n * Set page (calculates offset from limit).\n * Page numbers are 1-indexed (first page is 1).\n */\n page(pageNum: number, pageSize: number): this {\n // Ensure valid page number (minimum 1)\n const safePage = Math.max(1, Math.floor(pageNum));\n // Ensure valid page size (minimum 1)\n const safeSize = Math.max(1, Math.floor(pageSize));\n \n this.options.limit = safeSize;\n this.options.offset = (safePage - 1) * safeSize;\n return this;\n }\n\n /**\n * Build the query options.\n */\n build(): QueryOptions {\n return { ...this.options };\n }\n}\n\n/**\n * Start building a query.\n */\nexport const query = () => new QueryBuilder();\n"],"names":["QueryBuilder","type","options","namespace","ns","ids","search","query","fields","searchFields","caseSensitive","enabled","sortBy","field","direction","sort","limit","n","Math","max","floor","offset","page","pageNum","pageSize","safePage","safeSize","build"],"mappings":";;;;;;;;;;;;;AAEA;;AAEC,IACM,MAAMA,YAAAA,CAAAA;AAGT;;QAGAC,IAAAA,CAAKA,IAAuB,EAAQ;AAChC,QAAA,IAAI,CAACC,OAAO,CAACD,IAAI,GAAGA,IAAAA;AACpB,QAAA,OAAO,IAAI;AACf,IAAA;AAEA;;QAGAE,SAAAA,CAAUC,EAAqB,EAAQ;AACnC,QAAA,IAAI,CAACF,OAAO,CAACC,SAAS,GAAGC,EAAAA;AACzB,QAAA,OAAO,IAAI;AACf,IAAA;AAEA;;QAGAC,GAAAA,CAAIA,GAAa,EAAQ;AACrB,QAAA,IAAI,CAACH,OAAO,CAACG,GAAG,GAAGA,GAAAA;AACnB,QAAA,OAAO,IAAI;AACf,IAAA;AAEA;;AAEC,QACDC,MAAAA,CAAOC,KAAa,EAAEC,MAAiB,EAAQ;AAC3C,QAAA,IAAI,CAACN,OAAO,CAACI,MAAM,GAAGC,KAAAA;AACtB,QAAA,IAAIC,QAAQ,IAAI,CAACN,OAAO,CAACO,YAAY,GAAGD,MAAAA;AACxC,QAAA,OAAO,IAAI;AACf,IAAA;AAEA;;QAGAE,aAAAA,CAAcC,OAAAA,GAAmB,IAAI,EAAQ;AACzC,QAAA,IAAI,CAACT,OAAO,CAACQ,aAAa,GAAGC,OAAAA;AAC7B,QAAA,OAAO,IAAI;AACf,IAAA;AAEA;;AAEC,QACDC,MAAAA,CAAOC,KAAa,EAAEC,SAAAA,GAA2B,KAAK,EAAQ;AAC1D,QAAA,IAAI,CAACZ,OAAO,CAACa,IAAI,GAAG;AAAK,YAAA,GAAA,IAAI,CAACb,OAAO,CAACa,IAAI,IAAI,EAAE;AAAG,YAAA;AAAEF,gBAAAA,KAAAA;AAAOC,gBAAAA;AAAU;AAAE,SAAA;AACxE,QAAA,OAAO,IAAI;AACf,IAAA;AAEA;;;QAIAE,KAAAA,CAAMC,CAAS,EAAQ;QACnB,IAAI,CAACf,OAAO,CAACc,KAAK,GAAGE,IAAAA,CAAKC,GAAG,CAAC,CAAA,EAAGD,IAAAA,CAAKE,KAAK,CAACH,CAAAA,CAAAA,CAAAA;AAC5C,QAAA,OAAO,IAAI;AACf,IAAA;AAEA;;;QAIAI,MAAAA,CAAOJ,CAAS,EAAQ;QACpB,IAAI,CAACf,OAAO,CAACmB,MAAM,GAAGH,IAAAA,CAAKC,GAAG,CAAC,CAAA,EAAGD,IAAAA,CAAKE,KAAK,CAACH,CAAAA,CAAAA,CAAAA;AAC7C,QAAA,OAAO,IAAI;AACf,IAAA;AAEA;;;AAGC,QACDK,IAAAA,CAAKC,OAAe,EAAEC,QAAgB,EAAQ;;AAE1C,QAAA,MAAMC,WAAWP,IAAAA,CAAKC,GAAG,CAAC,CAAA,EAAGD,IAAAA,CAAKE,KAAK,CAACG,OAAAA,CAAAA,CAAAA;;AAExC,QAAA,MAAMG,WAAWR,IAAAA,CAAKC,GAAG,CAAC,CAAA,EAAGD,IAAAA,CAAKE,KAAK,CAACI,QAAAA,CAAAA,CAAAA;AAExC,QAAA,IAAI,CAACtB,OAAO,CAACc,KAAK,GAAGU,QAAAA;QACrB,IAAI,CAACxB,OAAO,CAACmB,MAAM,GAAG,CAACI,QAAAA,GAAW,CAAA,IAAKC,QAAAA;AACvC,QAAA,OAAO,IAAI;AACf,IAAA;AAEA;;AAEC,QACDC,KAAAA,GAAsB;QAClB,OAAO;YAAE,GAAG,IAAI,CAACzB;AAAQ,SAAA;AAC7B,IAAA;;AAzFA,QAAA,gBAAA,CAAA,IAAA,EAAQA,WAAwB,EAAC,CAAA;;AA0FrC;AAEA;;AAEC,IACM,MAAMK,KAAAA,GAAQ,IAAM,IAAIP,YAAAA;;;;"}
|
package/dist/api/search.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import { StorageAccessError } from '../storage/errors.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Maximum number of entities that can be loaded into memory during search.
|
|
5
|
+
* Prevents memory exhaustion with large datasets.
|
|
6
|
+
*/ const MAX_SEARCH_ENTITIES = 10000;
|
|
1
7
|
const createSearchEngine = (options)=>{
|
|
2
8
|
const { provider, registry, defaultNamespace } = options;
|
|
3
9
|
const textMatch = (text, query, caseSensitive)=>{
|
|
@@ -28,6 +34,23 @@ const createSearchEngine = (options)=>{
|
|
|
28
34
|
}
|
|
29
35
|
return false;
|
|
30
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* Check if a value looks like an ISO date string.
|
|
39
|
+
*/ const isISODateString = (value)=>{
|
|
40
|
+
if (typeof value !== 'string') return false;
|
|
41
|
+
// Match ISO 8601 date format (YAML deserializes dates as ISO strings)
|
|
42
|
+
return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value);
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Try to parse a value as a Date for comparison.
|
|
46
|
+
*/ const toDateValue = (value)=>{
|
|
47
|
+
if (value instanceof Date) return value.getTime();
|
|
48
|
+
if (isISODateString(value)) {
|
|
49
|
+
const parsed = new Date(value);
|
|
50
|
+
return isNaN(parsed.getTime()) ? null : parsed.getTime();
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
};
|
|
31
54
|
const sortEntities = (entities, sort)=>{
|
|
32
55
|
return [
|
|
33
56
|
...entities
|
|
@@ -39,10 +62,13 @@ const createSearchEngine = (options)=>{
|
|
|
39
62
|
if (aVal === undefined || aVal === null) return direction === 'asc' ? 1 : -1;
|
|
40
63
|
if (bVal === undefined || bVal === null) return direction === 'asc' ? -1 : 1;
|
|
41
64
|
let cmp;
|
|
42
|
-
|
|
65
|
+
// Try date comparison first (handles both Date objects and ISO strings)
|
|
66
|
+
const aDate = toDateValue(aVal);
|
|
67
|
+
const bDate = toDateValue(bVal);
|
|
68
|
+
if (aDate !== null && bDate !== null) {
|
|
69
|
+
cmp = aDate - bDate;
|
|
70
|
+
} else if (typeof aVal === 'string' && typeof bVal === 'string') {
|
|
43
71
|
cmp = aVal.localeCompare(bVal);
|
|
44
|
-
} else if (aVal instanceof Date && bVal instanceof Date) {
|
|
45
|
-
cmp = aVal.getTime() - bVal.getTime();
|
|
46
72
|
} else {
|
|
47
73
|
cmp = aVal < bVal ? -1 : 1;
|
|
48
74
|
}
|
|
@@ -77,6 +103,10 @@ const createSearchEngine = (options)=>{
|
|
|
77
103
|
allEntities = allEntities.concat(entities);
|
|
78
104
|
}
|
|
79
105
|
}
|
|
106
|
+
// Check for memory exhaustion risk
|
|
107
|
+
if (allEntities.length > MAX_SEARCH_ENTITIES) {
|
|
108
|
+
throw new StorageAccessError(`Search returned too many results (${allEntities.length}). ` + `Please narrow your query by specifying types, namespaces, or search terms. ` + `Maximum allowed: ${MAX_SEARCH_ENTITIES} entities.`);
|
|
109
|
+
}
|
|
80
110
|
// Apply ID filter
|
|
81
111
|
if (ids === null || ids === void 0 ? void 0 : ids.length) {
|
|
82
112
|
allEntities = allEntities.filter((e)=>ids.includes(e.id));
|
package/dist/api/search.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search.js","sources":["../../src/api/search.ts"],"sourcesContent":["import { BaseEntity } from '../schema/base';\nimport { SchemaRegistry } from '../schema/registry';\nimport { StorageProvider } from '../storage/interface';\nimport { QueryOptions, QueryResult, SortOption } from './query';\n\nexport interface SearchEngine {\n /**\n * Search entities with filtering, pagination, and sorting.\n */\n search<T extends BaseEntity>(options: QueryOptions): Promise<QueryResult<T>>;\n\n /**\n * Quick search by name across all types.\n */\n quickSearch<T extends BaseEntity>(\n query: string,\n options?: Pick<QueryOptions, 'type' | 'namespace' | 'limit'>\n ): Promise<T[]>;\n}\n\nexport interface SearchEngineOptions {\n provider: StorageProvider;\n registry: SchemaRegistry;\n defaultNamespace?: string;\n}\n\nexport const createSearchEngine = (options: SearchEngineOptions): SearchEngine => {\n const { provider, registry, defaultNamespace } = options;\n\n const textMatch = (\n text: string | undefined,\n query: string,\n caseSensitive: boolean\n ): boolean => {\n if (!text) return false;\n const a = caseSensitive ? text : text.toLowerCase();\n const b = caseSensitive ? query : query.toLowerCase();\n return a.includes(b);\n };\n\n const matchesSearch = (\n entity: BaseEntity,\n search: string,\n searchFields: string[],\n caseSensitive: boolean\n ): boolean => {\n // Always search name\n if (textMatch(entity.name, search, caseSensitive)) {\n return true;\n }\n\n // Search additional fields\n for (const field of searchFields) {\n const value = (entity as Record<string, unknown>)[field];\n if (typeof value === 'string' && textMatch(value, search, caseSensitive)) {\n return true;\n }\n // Handle arrays of strings (like sounds_like)\n if (Array.isArray(value)) {\n for (const item of value) {\n if (typeof item === 'string' && textMatch(item, search, caseSensitive)) {\n return true;\n }\n }\n }\n }\n\n return false;\n };\n\n const sortEntities = <T extends BaseEntity>(\n entities: T[],\n sort: SortOption[]\n ): T[] => {\n return [...entities].sort((a, b) => {\n for (const { field, direction } of sort) {\n const aVal = (a as Record<string, unknown>)[field];\n const bVal = (b as Record<string, unknown>)[field];\n\n if (aVal === bVal) continue;\n if (aVal === undefined || aVal === null) return direction === 'asc' ? 1 : -1;\n if (bVal === undefined || bVal === null) return direction === 'asc' ? -1 : 1;\n\n let cmp: number;\n if (typeof aVal === 'string' && typeof bVal === 'string') {\n cmp = aVal.localeCompare(bVal);\n } else if (aVal instanceof Date && bVal instanceof Date) {\n cmp = aVal.getTime() - bVal.getTime();\n } else {\n cmp = aVal < bVal ? -1 : 1;\n }\n\n return direction === 'asc' ? cmp : -cmp;\n }\n return 0;\n });\n };\n\n return {\n async search<T extends BaseEntity>(options: QueryOptions): Promise<QueryResult<T>> {\n const {\n type,\n namespace,\n ids,\n search,\n searchFields = [],\n caseSensitive = false,\n limit,\n offset = 0,\n sort = [{ field: 'name', direction: 'asc' }],\n } = options;\n\n // Determine which types to search\n const types = type\n ? (Array.isArray(type) ? type : [type])\n : registry.types();\n\n // Determine which namespaces to search\n const namespaces = namespace\n ? (Array.isArray(namespace) ? namespace : [namespace])\n : [defaultNamespace];\n\n // Collect entities\n let allEntities: T[] = [];\n\n for (const t of types) {\n for (const ns of namespaces) {\n const entities = await provider.getAll<T>(t, ns);\n allEntities = allEntities.concat(entities);\n }\n }\n\n // Apply ID filter\n if (ids?.length) {\n allEntities = allEntities.filter(e => ids.includes(e.id));\n }\n\n // Apply search filter\n if (search) {\n allEntities = allEntities.filter(e =>\n matchesSearch(e, search, searchFields, caseSensitive)\n );\n }\n\n // Apply sorting\n allEntities = sortEntities(allEntities, sort);\n\n // Get total before pagination\n const total = allEntities.length;\n\n // Apply pagination\n const paginated = allEntities.slice(offset, limit ? offset + limit : undefined);\n\n return {\n items: paginated,\n total,\n hasMore: limit ? offset + limit < total : false,\n query: options,\n };\n },\n\n async quickSearch<T extends BaseEntity>(\n query: string,\n options: Pick<QueryOptions, 'type' | 'namespace' | 'limit'> = {}\n ): Promise<T[]> {\n const result = await this.search<T>({\n ...options,\n search: query,\n });\n return result.items;\n },\n };\n};\n"],"names":["createSearchEngine","options","provider","registry","defaultNamespace","textMatch","text","query","caseSensitive","a","toLowerCase","b","includes","matchesSearch","entity","search","searchFields","name","field","value","Array","isArray","item","sortEntities","entities","sort","direction","aVal","bVal","undefined","cmp","localeCompare","Date","getTime","type","namespace","ids","limit","offset","types","namespaces","allEntities","t","ns","getAll","concat","length","filter","e","id","total","paginated","slice","items","hasMore","quickSearch","result"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"search.js","sources":["../../src/api/search.ts"],"sourcesContent":["import { BaseEntity } from '../schema/base';\nimport { SchemaRegistry } from '../schema/registry';\nimport { StorageProvider } from '../storage/interface';\nimport { StorageAccessError } from '../storage/errors';\nimport { QueryOptions, QueryResult, SortOption } from './query';\n\n/**\n * Maximum number of entities that can be loaded into memory during search.\n * Prevents memory exhaustion with large datasets.\n */\nconst MAX_SEARCH_ENTITIES = 10000;\n\nexport interface SearchEngine {\n /**\n * Search entities with filtering, pagination, and sorting.\n */\n search<T extends BaseEntity>(options: QueryOptions): Promise<QueryResult<T>>;\n\n /**\n * Quick search by name across all types.\n */\n quickSearch<T extends BaseEntity>(\n query: string,\n options?: Pick<QueryOptions, 'type' | 'namespace' | 'limit'>\n ): Promise<T[]>;\n}\n\nexport interface SearchEngineOptions {\n provider: StorageProvider;\n registry: SchemaRegistry;\n defaultNamespace?: string;\n}\n\nexport const createSearchEngine = (options: SearchEngineOptions): SearchEngine => {\n const { provider, registry, defaultNamespace } = options;\n\n const textMatch = (\n text: string | undefined,\n query: string,\n caseSensitive: boolean\n ): boolean => {\n if (!text) return false;\n const a = caseSensitive ? text : text.toLowerCase();\n const b = caseSensitive ? query : query.toLowerCase();\n return a.includes(b);\n };\n\n const matchesSearch = (\n entity: BaseEntity,\n search: string,\n searchFields: string[],\n caseSensitive: boolean\n ): boolean => {\n // Always search name\n if (textMatch(entity.name, search, caseSensitive)) {\n return true;\n }\n\n // Search additional fields\n for (const field of searchFields) {\n const value = (entity as Record<string, unknown>)[field];\n if (typeof value === 'string' && textMatch(value, search, caseSensitive)) {\n return true;\n }\n // Handle arrays of strings (like sounds_like)\n if (Array.isArray(value)) {\n for (const item of value) {\n if (typeof item === 'string' && textMatch(item, search, caseSensitive)) {\n return true;\n }\n }\n }\n }\n\n return false;\n };\n\n /**\n * Check if a value looks like an ISO date string.\n */\n const isISODateString = (value: unknown): value is string => {\n if (typeof value !== 'string') return false;\n // Match ISO 8601 date format (YAML deserializes dates as ISO strings)\n return /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}/.test(value);\n };\n\n /**\n * Try to parse a value as a Date for comparison.\n */\n const toDateValue = (value: unknown): number | null => {\n if (value instanceof Date) return value.getTime();\n if (isISODateString(value)) {\n const parsed = new Date(value);\n return isNaN(parsed.getTime()) ? null : parsed.getTime();\n }\n return null;\n };\n\n const sortEntities = <T extends BaseEntity>(\n entities: T[],\n sort: SortOption[]\n ): T[] => {\n return [...entities].sort((a, b) => {\n for (const { field, direction } of sort) {\n const aVal = (a as Record<string, unknown>)[field];\n const bVal = (b as Record<string, unknown>)[field];\n\n if (aVal === bVal) continue;\n if (aVal === undefined || aVal === null) return direction === 'asc' ? 1 : -1;\n if (bVal === undefined || bVal === null) return direction === 'asc' ? -1 : 1;\n\n let cmp: number;\n\n // Try date comparison first (handles both Date objects and ISO strings)\n const aDate = toDateValue(aVal);\n const bDate = toDateValue(bVal);\n if (aDate !== null && bDate !== null) {\n cmp = aDate - bDate;\n } else if (typeof aVal === 'string' && typeof bVal === 'string') {\n cmp = aVal.localeCompare(bVal);\n } else {\n cmp = aVal < bVal ? -1 : 1;\n }\n\n return direction === 'asc' ? cmp : -cmp;\n }\n return 0;\n });\n };\n\n return {\n async search<T extends BaseEntity>(options: QueryOptions): Promise<QueryResult<T>> {\n const {\n type,\n namespace,\n ids,\n search,\n searchFields = [],\n caseSensitive = false,\n limit,\n offset = 0,\n sort = [{ field: 'name', direction: 'asc' }],\n } = options;\n\n // Determine which types to search\n const types = type\n ? (Array.isArray(type) ? type : [type])\n : registry.types();\n\n // Determine which namespaces to search\n const namespaces = namespace\n ? (Array.isArray(namespace) ? namespace : [namespace])\n : [defaultNamespace];\n\n // Collect entities\n let allEntities: T[] = [];\n\n for (const t of types) {\n for (const ns of namespaces) {\n const entities = await provider.getAll<T>(t, ns);\n allEntities = allEntities.concat(entities);\n }\n }\n\n // Check for memory exhaustion risk\n if (allEntities.length > MAX_SEARCH_ENTITIES) {\n throw new StorageAccessError(\n `Search returned too many results (${allEntities.length}). ` +\n `Please narrow your query by specifying types, namespaces, or search terms. ` +\n `Maximum allowed: ${MAX_SEARCH_ENTITIES} entities.`\n );\n }\n\n // Apply ID filter\n if (ids?.length) {\n allEntities = allEntities.filter(e => ids.includes(e.id));\n }\n\n // Apply search filter\n if (search) {\n allEntities = allEntities.filter(e =>\n matchesSearch(e, search, searchFields, caseSensitive)\n );\n }\n\n // Apply sorting\n allEntities = sortEntities(allEntities, sort);\n\n // Get total before pagination\n const total = allEntities.length;\n\n // Apply pagination\n const paginated = allEntities.slice(offset, limit ? offset + limit : undefined);\n\n return {\n items: paginated,\n total,\n hasMore: limit ? offset + limit < total : false,\n query: options,\n };\n },\n\n async quickSearch<T extends BaseEntity>(\n query: string,\n options: Pick<QueryOptions, 'type' | 'namespace' | 'limit'> = {}\n ): Promise<T[]> {\n const result = await this.search<T>({\n ...options,\n search: query,\n });\n return result.items;\n },\n };\n};\n"],"names":["MAX_SEARCH_ENTITIES","createSearchEngine","options","provider","registry","defaultNamespace","textMatch","text","query","caseSensitive","a","toLowerCase","b","includes","matchesSearch","entity","search","searchFields","name","field","value","Array","isArray","item","isISODateString","test","toDateValue","Date","getTime","parsed","isNaN","sortEntities","entities","sort","direction","aVal","bVal","undefined","cmp","aDate","bDate","localeCompare","type","namespace","ids","limit","offset","types","namespaces","allEntities","t","ns","getAll","concat","length","StorageAccessError","filter","e","id","total","paginated","slice","items","hasMore","quickSearch","result"],"mappings":";;AAMA;;;AAGC,IACD,MAAMA,mBAAAA,GAAsB,KAAA;AAuBrB,MAAMC,qBAAqB,CAACC,OAAAA,GAAAA;AAC/B,IAAA,MAAM,EAAEC,QAAQ,EAAEC,QAAQ,EAAEC,gBAAgB,EAAE,GAAGH,OAAAA;IAEjD,MAAMI,SAAAA,GAAY,CACdC,IAAAA,EACAC,KAAAA,EACAC,aAAAA,GAAAA;QAEA,IAAI,CAACF,MAAM,OAAO,KAAA;AAClB,QAAA,MAAMG,CAAAA,GAAID,aAAAA,GAAgBF,IAAAA,GAAOA,IAAAA,CAAKI,WAAW,EAAA;AACjD,QAAA,MAAMC,CAAAA,GAAIH,aAAAA,GAAgBD,KAAAA,GAAQA,KAAAA,CAAMG,WAAW,EAAA;QACnD,OAAOD,CAAAA,CAAEG,QAAQ,CAACD,CAAAA,CAAAA;AACtB,IAAA,CAAA;AAEA,IAAA,MAAME,aAAAA,GAAgB,CAClBC,MAAAA,EACAC,MAAAA,EACAC,YAAAA,EACAR,aAAAA,GAAAA;;AAGA,QAAA,IAAIH,SAAAA,CAAUS,MAAAA,CAAOG,IAAI,EAAEF,QAAQP,aAAAA,CAAAA,EAAgB;YAC/C,OAAO,IAAA;AACX,QAAA;;QAGA,KAAK,MAAMU,SAASF,YAAAA,CAAc;AAC9B,YAAA,MAAMG,KAAAA,GAASL,MAAkC,CAACI,KAAAA,CAAM;AACxD,YAAA,IAAI,OAAOC,KAAAA,KAAU,QAAA,IAAYd,SAAAA,CAAUc,KAAAA,EAAOJ,QAAQP,aAAAA,CAAAA,EAAgB;gBACtE,OAAO,IAAA;AACX,YAAA;;YAEA,IAAIY,KAAAA,CAAMC,OAAO,CAACF,KAAAA,CAAAA,EAAQ;gBACtB,KAAK,MAAMG,QAAQH,KAAAA,CAAO;AACtB,oBAAA,IAAI,OAAOG,IAAAA,KAAS,QAAA,IAAYjB,SAAAA,CAAUiB,IAAAA,EAAMP,QAAQP,aAAAA,CAAAA,EAAgB;wBACpE,OAAO,IAAA;AACX,oBAAA;AACJ,gBAAA;AACJ,YAAA;AACJ,QAAA;QAEA,OAAO,KAAA;AACX,IAAA,CAAA;AAEA;;QAGA,MAAMe,kBAAkB,CAACJ,KAAAA,GAAAA;QACrB,IAAI,OAAOA,KAAAA,KAAU,QAAA,EAAU,OAAO,KAAA;;QAEtC,OAAO,sCAAA,CAAuCK,IAAI,CAACL,KAAAA,CAAAA;AACvD,IAAA,CAAA;AAEA;;QAGA,MAAMM,cAAc,CAACN,KAAAA,GAAAA;AACjB,QAAA,IAAIA,KAAAA,YAAiBO,IAAAA,EAAM,OAAOP,KAAAA,CAAMQ,OAAO,EAAA;AAC/C,QAAA,IAAIJ,gBAAgBJ,KAAAA,CAAAA,EAAQ;YACxB,MAAMS,MAAAA,GAAS,IAAIF,IAAAA,CAAKP,KAAAA,CAAAA;AACxB,YAAA,OAAOU,MAAMD,MAAAA,CAAOD,OAAO,EAAA,CAAA,GAAM,IAAA,GAAOC,OAAOD,OAAO,EAAA;AAC1D,QAAA;QACA,OAAO,IAAA;AACX,IAAA,CAAA;IAEA,MAAMG,YAAAA,GAAe,CACjBC,QAAAA,EACAC,IAAAA,GAAAA;QAEA,OAAO;AAAID,YAAAA,GAAAA;SAAS,CAACC,IAAI,CAAC,CAACvB,CAAAA,EAAGE,CAAAA,GAAAA;AAC1B,YAAA,KAAK,MAAM,EAAEO,KAAK,EAAEe,SAAS,EAAE,IAAID,IAAAA,CAAM;AACrC,gBAAA,MAAME,IAAAA,GAAQzB,CAA6B,CAACS,KAAAA,CAAM;AAClD,gBAAA,MAAMiB,IAAAA,GAAQxB,CAA6B,CAACO,KAAAA,CAAM;AAElD,gBAAA,IAAIgB,SAASC,IAAAA,EAAM;gBACnB,IAAID,IAAAA,KAASE,aAAaF,IAAAA,KAAS,IAAA,EAAM,OAAOD,SAAAA,KAAc,KAAA,GAAQ,IAAI,EAAC;gBAC3E,IAAIE,IAAAA,KAASC,aAAaD,IAAAA,KAAS,IAAA,EAAM,OAAOF,SAAAA,KAAc,KAAA,GAAQ,EAAC,GAAI,CAAA;gBAE3E,IAAII,GAAAA;;AAGJ,gBAAA,MAAMC,QAAQb,WAAAA,CAAYS,IAAAA,CAAAA;AAC1B,gBAAA,MAAMK,QAAQd,WAAAA,CAAYU,IAAAA,CAAAA;gBAC1B,IAAIG,KAAAA,KAAU,IAAA,IAAQC,KAAAA,KAAU,IAAA,EAAM;AAClCF,oBAAAA,GAAAA,GAAMC,KAAAA,GAAQC,KAAAA;AAClB,gBAAA,CAAA,MAAO,IAAI,OAAOL,IAAAA,KAAS,QAAA,IAAY,OAAOC,SAAS,QAAA,EAAU;oBAC7DE,GAAAA,GAAMH,IAAAA,CAAKM,aAAa,CAACL,IAAAA,CAAAA;gBAC7B,CAAA,MAAO;oBACHE,GAAAA,GAAMH,IAAAA,GAAOC,IAAAA,GAAO,EAAC,GAAI,CAAA;AAC7B,gBAAA;gBAEA,OAAOF,SAAAA,KAAc,KAAA,GAAQI,GAAAA,GAAM,CAACA,GAAAA;AACxC,YAAA;YACA,OAAO,CAAA;AACX,QAAA,CAAA,CAAA;AACJ,IAAA,CAAA;IAEA,OAAO;AACH,QAAA,MAAMtB,QAA6Bd,OAAqB,EAAA;YACpD,MAAM,EACFwC,IAAI,EACJC,SAAS,EACTC,GAAG,EACH5B,MAAM,EACNC,YAAAA,GAAe,EAAE,EACjBR,aAAAA,GAAgB,KAAK,EACrBoC,KAAK,EACLC,MAAAA,GAAS,CAAC,EACVb,IAAAA,GAAO;AAAC,gBAAA;oBAAEd,KAAAA,EAAO,MAAA;oBAAQe,SAAAA,EAAW;AAAM;AAAE,aAAA,EAC/C,GAAGhC,OAAAA;;AAGJ,YAAA,MAAM6C,QAAQL,IAAAA,GACPrB,KAAAA,CAAMC,OAAO,CAACoB,QAAQA,IAAAA,GAAO;AAACA,gBAAAA;AAAK,aAAA,GACpCtC,SAAS2C,KAAK,EAAA;;AAGpB,YAAA,MAAMC,aAAaL,SAAAA,GACZtB,KAAAA,CAAMC,OAAO,CAACqB,aAAaA,SAAAA,GAAY;AAACA,gBAAAA;aAAU,GACnD;AAACtC,gBAAAA;AAAiB,aAAA;;AAGxB,YAAA,IAAI4C,cAAmB,EAAE;YAEzB,KAAK,MAAMC,KAAKH,KAAAA,CAAO;gBACnB,KAAK,MAAMI,MAAMH,UAAAA,CAAY;AACzB,oBAAA,MAAMhB,QAAAA,GAAW,MAAM7B,QAAAA,CAASiD,MAAM,CAAIF,CAAAA,EAAGC,EAAAA,CAAAA;oBAC7CF,WAAAA,GAAcA,WAAAA,CAAYI,MAAM,CAACrB,QAAAA,CAAAA;AACrC,gBAAA;AACJ,YAAA;;YAGA,IAAIiB,WAAAA,CAAYK,MAAM,GAAGtD,mBAAAA,EAAqB;gBAC1C,MAAM,IAAIuD,mBACN,CAAC,kCAAkC,EAAEN,WAAAA,CAAYK,MAAM,CAAC,GAAG,CAAC,GAC5D,CAAC,2EAA2E,CAAC,GAC7E,CAAC,iBAAiB,EAAEtD,mBAAAA,CAAoB,UAAU,CAAC,CAAA;AAE3D,YAAA;;AAGA,YAAA,IAAI4C,GAAAA,KAAAA,IAAAA,IAAAA,GAAAA,KAAAA,MAAAA,GAAAA,MAAAA,GAAAA,GAAAA,CAAKU,MAAM,EAAE;gBACbL,WAAAA,GAAcA,WAAAA,CAAYO,MAAM,CAACC,CAAAA,IAAKb,GAAAA,CAAI/B,QAAQ,CAAC4C,CAAAA,CAAEC,EAAE,CAAA,CAAA;AAC3D,YAAA;;AAGA,YAAA,IAAI1C,MAAAA,EAAQ;gBACRiC,WAAAA,GAAcA,WAAAA,CAAYO,MAAM,CAACC,CAAAA,IAC7B3C,aAAAA,CAAc2C,CAAAA,EAAGzC,QAAQC,YAAAA,EAAcR,aAAAA,CAAAA,CAAAA;AAE/C,YAAA;;AAGAwC,YAAAA,WAAAA,GAAclB,aAAakB,WAAAA,EAAahB,IAAAA,CAAAA;;YAGxC,MAAM0B,KAAAA,GAAQV,YAAYK,MAAM;;AAGhC,YAAA,MAAMM,YAAYX,WAAAA,CAAYY,KAAK,CAACf,MAAAA,EAAQD,KAAAA,GAAQC,SAASD,KAAAA,GAAQR,SAAAA,CAAAA;YAErE,OAAO;gBACHyB,KAAAA,EAAOF,SAAAA;AACPD,gBAAAA,KAAAA;gBACAI,OAAAA,EAASlB,KAAAA,GAAQC,MAAAA,GAASD,KAAAA,GAAQc,KAAAA,GAAQ,KAAA;gBAC1CnD,KAAAA,EAAON;AACX,aAAA;AACJ,QAAA,CAAA;AAEA,QAAA,MAAM8D,WAAAA,CAAAA,CACFxD,KAAa,EACbN,OAAAA,GAA8D,EAAE,EAAA;AAEhE,YAAA,MAAM+D,MAAAA,GAAS,MAAM,IAAI,CAACjD,MAAM,CAAI;AAChC,gBAAA,GAAGd,OAAO;gBACVc,MAAAA,EAAQR;AACZ,aAAA,CAAA;AACA,YAAA,OAAOyD,OAAOH,KAAK;AACvB,QAAA;AACJ,KAAA;AACJ;;;;"}
|
|
@@ -30,15 +30,24 @@ import { createFileSystemProvider } from '../storage/filesystem.js';
|
|
|
30
30
|
await primaryProvider.initialize();
|
|
31
31
|
const findEntities = async (filter)=>{
|
|
32
32
|
const byId = new Map();
|
|
33
|
+
// Create a filter without pagination - we'll apply pagination after merging
|
|
34
|
+
// This prevents double-pagination (sub-providers paginating, then us paginating again)
|
|
35
|
+
const filterWithoutPagination = {
|
|
36
|
+
type: filter.type,
|
|
37
|
+
namespace: filter.namespace,
|
|
38
|
+
ids: filter.ids,
|
|
39
|
+
search: filter.search
|
|
40
|
+
};
|
|
33
41
|
for (const p of [
|
|
34
42
|
...readProviders
|
|
35
43
|
].reverse()){
|
|
36
|
-
const results = await p.find(
|
|
44
|
+
const results = await p.find(filterWithoutPagination);
|
|
37
45
|
for (const entity of results){
|
|
38
46
|
byId.set(entity.id, entity);
|
|
39
47
|
}
|
|
40
48
|
}
|
|
41
49
|
let results = Array.from(byId.values());
|
|
50
|
+
// Apply pagination once, after merging all results
|
|
42
51
|
if (filter.offset) results = results.slice(filter.offset);
|
|
43
52
|
if (filter.limit) results = results.slice(0, filter.limit);
|
|
44
53
|
return results;
|
|
@@ -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 for (const p of [...readProviders].reverse()) {\n const results = await p.find<T>(
|
|
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":"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"}
|
package/dist/discovery/walker.js
CHANGED
|
@@ -60,7 +60,21 @@ const createDirectoryWalker = (options)=>{
|
|
|
60
60
|
const discovered = [];
|
|
61
61
|
let currentDir = path.resolve(startDir);
|
|
62
62
|
let level = 0;
|
|
63
|
+
const visited = new Set();
|
|
63
64
|
while(level < maxLevels){
|
|
65
|
+
// Resolve real path to handle symlinks and prevent cycles
|
|
66
|
+
let realPath;
|
|
67
|
+
try {
|
|
68
|
+
realPath = await fs.realpath(currentDir);
|
|
69
|
+
} catch {
|
|
70
|
+
// If realpath fails, use resolved path (may happen with permissions)
|
|
71
|
+
realPath = currentDir;
|
|
72
|
+
}
|
|
73
|
+
// Check for symlink cycles
|
|
74
|
+
if (visited.has(realPath)) {
|
|
75
|
+
break; // Already visited this real path, prevent infinite loop
|
|
76
|
+
}
|
|
77
|
+
visited.add(realPath);
|
|
64
78
|
const contextDir = path.join(currentDir, contextDirName);
|
|
65
79
|
if (existsSync(contextDir)) {
|
|
66
80
|
const { namespaces, types } = await getNamespacesAndTypes(contextDir);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"walker.js","sources":["../../src/discovery/walker.ts"],"sourcesContent":["import * as fs from 'node:fs/promises';\nimport { existsSync } from 'node:fs';\nimport * as path from 'node:path';\nimport { SchemaRegistry } from '../schema/registry';\n\nexport interface DiscoveredContextDir {\n /** Absolute path to the context directory */\n path: string;\n\n /** Distance from starting point (0 = closest) */\n level: number;\n\n /** Namespaces found in this context dir */\n namespaces: string[];\n\n /** Entity types found (based on directory names) */\n types: string[];\n}\n\nexport interface WalkerOptions {\n /** Directory to start searching from */\n startDir: string;\n\n /** Name of context directory to look for */\n contextDirName: string;\n\n /** Maximum levels to walk up */\n maxLevels: number;\n\n /** Stop walking when this directory is reached */\n stopAt?: string;\n\n /** Stop when finding a marker file (e.g., '.git', 'package.json') */\n stopMarkers?: string[];\n\n /** Schema registry to identify entity type directories */\n registry?: SchemaRegistry;\n}\n\nexport interface DirectoryWalker {\n /**\n * Find all context directories walking up from startDir.\n */\n discover(): Promise<DiscoveredContextDir[]>;\n\n /**\n * Check if a specific directory contains context.\n */\n hasContext(dir: string): Promise<boolean>;\n}\n\nexport const createDirectoryWalker = (options: WalkerOptions): DirectoryWalker => {\n const {\n startDir,\n contextDirName,\n maxLevels,\n stopAt,\n stopMarkers = [],\n registry,\n } = options;\n\n const shouldStop = async (dir: string): Promise<boolean> => {\n if (stopAt && dir === stopAt) return true;\n\n for (const marker of stopMarkers) {\n if (existsSync(path.join(dir, marker))) {\n return true;\n }\n }\n\n const parent = path.dirname(dir);\n return parent === dir; // Root\n };\n\n const getNamespacesAndTypes = async (\n contextDir: string\n ): Promise<{ namespaces: string[]; types: string[] }> => {\n const namespaces: string[] = [];\n const types: string[] = [];\n\n try {\n const entries = await fs.readdir(contextDir, { withFileTypes: true });\n\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n\n const subPath = path.join(contextDir, entry.name);\n const subEntries = await fs.readdir(subPath, { withFileTypes: true });\n\n // Check if this is a type directory (has .yaml files)\n const hasYamlFiles = subEntries.some(\n sub => sub.isFile() && (sub.name.endsWith('.yaml') || sub.name.endsWith('.yml'))\n );\n\n // Check if this looks like a type directory (known to registry)\n const isTypeDir = registry\n ? !!registry.getTypeFromDirectory(entry.name)\n : hasYamlFiles;\n\n if (isTypeDir) {\n const typeName = registry?.getTypeFromDirectory(entry.name) || entry.name;\n if (!types.includes(typeName)) {\n types.push(typeName);\n }\n } else {\n // Check if it's a namespace (contains type directories)\n const hasTypeDirs = subEntries.some(sub => {\n if (!sub.isDirectory()) return false;\n return registry\n ? !!registry.getTypeFromDirectory(sub.name)\n : true; // Assume any subdir could be a type\n });\n\n if (hasTypeDirs) {\n namespaces.push(entry.name);\n }\n }\n }\n } catch {\n // Directory doesn't exist or can't be read\n }\n\n return { namespaces, types };\n };\n\n return {\n async discover(): Promise<DiscoveredContextDir[]> {\n const discovered: DiscoveredContextDir[] = [];\n let currentDir = path.resolve(startDir);\n let level = 0;\n\n while (level < maxLevels) {\n const contextDir = path.join(currentDir, contextDirName);\n\n if (existsSync(contextDir)) {\n const { namespaces, types } = await getNamespacesAndTypes(contextDir);\n discovered.push({ path: contextDir, level, namespaces, types });\n }\n\n if (await shouldStop(currentDir)) break;\n\n currentDir = path.dirname(currentDir);\n level++;\n }\n\n return discovered;\n },\n\n async hasContext(dir: string): Promise<boolean> {\n return existsSync(path.join(dir, contextDirName));\n },\n };\n};\n"],"names":["createDirectoryWalker","options","startDir","contextDirName","maxLevels","stopAt","stopMarkers","registry","shouldStop","dir","marker","existsSync","path","join","parent","dirname","getNamespacesAndTypes","contextDir","namespaces","types","entries","fs","readdir","withFileTypes","entry","isDirectory","subPath","name","subEntries","hasYamlFiles","some","sub","isFile","endsWith","isTypeDir","getTypeFromDirectory","typeName","includes","push","hasTypeDirs","discover","discovered","currentDir","resolve","level","hasContext"],"mappings":"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
|
|
1
|
+
{"version":3,"file":"walker.js","sources":["../../src/discovery/walker.ts"],"sourcesContent":["import * as fs from 'node:fs/promises';\nimport { existsSync } from 'node:fs';\nimport * as path from 'node:path';\nimport { SchemaRegistry } from '../schema/registry';\n\nexport interface DiscoveredContextDir {\n /** Absolute path to the context directory */\n path: string;\n\n /** Distance from starting point (0 = closest) */\n level: number;\n\n /** Namespaces found in this context dir */\n namespaces: string[];\n\n /** Entity types found (based on directory names) */\n types: string[];\n}\n\nexport interface WalkerOptions {\n /** Directory to start searching from */\n startDir: string;\n\n /** Name of context directory to look for */\n contextDirName: string;\n\n /** Maximum levels to walk up */\n maxLevels: number;\n\n /** Stop walking when this directory is reached */\n stopAt?: string;\n\n /** Stop when finding a marker file (e.g., '.git', 'package.json') */\n stopMarkers?: string[];\n\n /** Schema registry to identify entity type directories */\n registry?: SchemaRegistry;\n}\n\nexport interface DirectoryWalker {\n /**\n * Find all context directories walking up from startDir.\n */\n discover(): Promise<DiscoveredContextDir[]>;\n\n /**\n * Check if a specific directory contains context.\n */\n hasContext(dir: string): Promise<boolean>;\n}\n\nexport const createDirectoryWalker = (options: WalkerOptions): DirectoryWalker => {\n const {\n startDir,\n contextDirName,\n maxLevels,\n stopAt,\n stopMarkers = [],\n registry,\n } = options;\n\n const shouldStop = async (dir: string): Promise<boolean> => {\n if (stopAt && dir === stopAt) return true;\n\n for (const marker of stopMarkers) {\n if (existsSync(path.join(dir, marker))) {\n return true;\n }\n }\n\n const parent = path.dirname(dir);\n return parent === dir; // Root\n };\n\n const getNamespacesAndTypes = async (\n contextDir: string\n ): Promise<{ namespaces: string[]; types: string[] }> => {\n const namespaces: string[] = [];\n const types: string[] = [];\n\n try {\n const entries = await fs.readdir(contextDir, { withFileTypes: true });\n\n for (const entry of entries) {\n if (!entry.isDirectory()) continue;\n\n const subPath = path.join(contextDir, entry.name);\n const subEntries = await fs.readdir(subPath, { withFileTypes: true });\n\n // Check if this is a type directory (has .yaml files)\n const hasYamlFiles = subEntries.some(\n sub => sub.isFile() && (sub.name.endsWith('.yaml') || sub.name.endsWith('.yml'))\n );\n\n // Check if this looks like a type directory (known to registry)\n const isTypeDir = registry\n ? !!registry.getTypeFromDirectory(entry.name)\n : hasYamlFiles;\n\n if (isTypeDir) {\n const typeName = registry?.getTypeFromDirectory(entry.name) || entry.name;\n if (!types.includes(typeName)) {\n types.push(typeName);\n }\n } else {\n // Check if it's a namespace (contains type directories)\n const hasTypeDirs = subEntries.some(sub => {\n if (!sub.isDirectory()) return false;\n return registry\n ? !!registry.getTypeFromDirectory(sub.name)\n : true; // Assume any subdir could be a type\n });\n\n if (hasTypeDirs) {\n namespaces.push(entry.name);\n }\n }\n }\n } catch {\n // Directory doesn't exist or can't be read\n }\n\n return { namespaces, types };\n };\n\n return {\n async discover(): Promise<DiscoveredContextDir[]> {\n const discovered: DiscoveredContextDir[] = [];\n let currentDir = path.resolve(startDir);\n let level = 0;\n const visited = new Set<string>();\n\n while (level < maxLevels) {\n // Resolve real path to handle symlinks and prevent cycles\n let realPath: string;\n try {\n realPath = await fs.realpath(currentDir);\n } catch {\n // If realpath fails, use resolved path (may happen with permissions)\n realPath = currentDir;\n }\n\n // Check for symlink cycles\n if (visited.has(realPath)) {\n break; // Already visited this real path, prevent infinite loop\n }\n visited.add(realPath);\n\n const contextDir = path.join(currentDir, contextDirName);\n\n if (existsSync(contextDir)) {\n const { namespaces, types } = await getNamespacesAndTypes(contextDir);\n discovered.push({ path: contextDir, level, namespaces, types });\n }\n\n if (await shouldStop(currentDir)) break;\n\n currentDir = path.dirname(currentDir);\n level++;\n }\n\n return discovered;\n },\n\n async hasContext(dir: string): Promise<boolean> {\n return existsSync(path.join(dir, contextDirName));\n },\n };\n};\n"],"names":["createDirectoryWalker","options","startDir","contextDirName","maxLevels","stopAt","stopMarkers","registry","shouldStop","dir","marker","existsSync","path","join","parent","dirname","getNamespacesAndTypes","contextDir","namespaces","types","entries","fs","readdir","withFileTypes","entry","isDirectory","subPath","name","subEntries","hasYamlFiles","some","sub","isFile","endsWith","isTypeDir","getTypeFromDirectory","typeName","includes","push","hasTypeDirs","discover","discovered","currentDir","resolve","level","visited","Set","realPath","realpath","has","add","hasContext"],"mappings":"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"}
|