@x12i/static-memorix-explorer-api 1.1.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -0
- package/dist/config.d.ts +38 -0
- package/dist/config.js +18 -0
- package/dist/engine/aliases.d.ts +32 -0
- package/dist/engine/associations.d.ts +26 -0
- package/dist/engine/identity.d.ts +22 -0
- package/dist/engine/inventory.d.ts +29 -0
- package/dist/engine/inventory.js +4 -4
- package/dist/engine/lists.d.ts +18 -0
- package/dist/engine/lists.js +2 -6
- package/dist/engine/narratives.d.ts +16 -0
- package/dist/engine/narratives.js +6 -16
- package/dist/engine/objectTypes.d.ts +11 -0
- package/dist/engine/query.d.ts +38 -0
- package/dist/engine/records.d.ts +23 -0
- package/dist/engine/snapshots.d.ts +17 -0
- package/dist/engine/write.d.ts +30 -0
- package/dist/engine/write.js +2 -6
- package/dist/routes/helpers.d.ts +4 -0
- package/dist/routes/index.d.ts +2 -0
- package/dist/routes/index.js +7 -1
- package/dist/server.d.ts +12 -0
- package/dist/server.js +30 -2
- package/dist/storage/InMemoryStore.d.ts +29 -0
- package/dist/storage/InMemoryStore.js +35 -16
- package/dist/storage/fs.d.ts +6 -0
- package/dist/types.d.ts +88 -0
- package/guides/managing-json-files.md +56 -0
- package/guides/running-the-service.md +27 -3
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -35,6 +35,12 @@ Installed CLI, using the default port `5030`:
|
|
|
35
35
|
npx @x12i/static-memorix-explorer-api
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
Or, after a global install:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
static-memorix-explorer-api --port 5030 --host 127.0.0.1
|
|
42
|
+
```
|
|
43
|
+
|
|
38
44
|
Override the port from the shell:
|
|
39
45
|
|
|
40
46
|
```bash
|
|
@@ -201,6 +207,28 @@ These strings are derived from `MEMORIX_ORG_ID`, `MEMORIX_AGENT_ID`, and the
|
|
|
201
207
|
`MEMORIX_DEPLOYMENT_PROFILE` (which, when set to `ebook`, forces both IDs to
|
|
202
208
|
`ebooks`).
|
|
203
209
|
|
|
210
|
+
### File-backed routing
|
|
211
|
+
|
|
212
|
+
Those routed database names also select JSON files. The separator is `--`:
|
|
213
|
+
|
|
214
|
+
```text
|
|
215
|
+
mocks/data/product/neo-memorix-entities--snapshots.json
|
|
216
|
+
mocks/data/product/neo-memorix-events--events.json
|
|
217
|
+
mocks/data/memory/neo-agent-memorix-catalox--memory.json
|
|
218
|
+
mocks/metadata/lists/neo-agent-memorix-catalox--my-plate.json
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
For `MEMORIX_ORG_ID=neo` and `MEMORIX_AGENT_ID=neo-agent`, entity-like
|
|
222
|
+
collections use the first prefix, events use the second, and memory plus
|
|
223
|
+
metadata use the Catalox prefix. If a routed file does not exist, the server
|
|
224
|
+
loads its unprefixed counterpart as seed data, but all mutations are written
|
|
225
|
+
to the routed filename. This provides database-style isolation without a
|
|
226
|
+
database and preserves existing fixtures.
|
|
227
|
+
|
|
228
|
+
The health response exposes the resolved values under `fileRouting`.
|
|
229
|
+
See [guides/managing-json-files.md](./guides/managing-json-files.md) for the
|
|
230
|
+
complete precedence and migration rules.
|
|
231
|
+
|
|
204
232
|
## Feature Flag Enforcement
|
|
205
233
|
|
|
206
234
|
The mock server enforces the following feature-flag gates with error responses:
|
|
@@ -241,6 +269,24 @@ and `knowledgeId`.
|
|
|
241
269
|
|
|
242
270
|
## Release history
|
|
243
271
|
|
|
272
|
+
### v1.3.0
|
|
273
|
+
- Added database-routing parity through deterministic JSON filename prefixes.
|
|
274
|
+
- Added separate entity, event, and Catalox/metadata prefix resolution.
|
|
275
|
+
- Added migration-safe fallback from missing routed files to unprefixed seeds;
|
|
276
|
+
writes always persist to the selected routed file.
|
|
277
|
+
- Added resolved `fileRouting` prefixes to the health response.
|
|
278
|
+
- Updated metadata discovery to ignore other routed tenants.
|
|
279
|
+
- Added routed persistence and prefix regression coverage.
|
|
280
|
+
|
|
281
|
+
### v1.2.0
|
|
282
|
+
- Added native `--port` and `--host` CLI options.
|
|
283
|
+
- Added the canonical `static-memorix-explorer-api` executable while retaining
|
|
284
|
+
`mock-memorix-explorer-api` as a compatibility alias.
|
|
285
|
+
- Published TypeScript declarations for `buildServer()`, `startServer()`,
|
|
286
|
+
configuration exports, and server options.
|
|
287
|
+
- Corrected the supported Node.js runtime to Node 20 or newer for Fastify 5.
|
|
288
|
+
- Expanded CLI, programmatic, test, precedence, and lifecycle documentation.
|
|
289
|
+
|
|
244
290
|
### v1.1.1
|
|
245
291
|
- Changed the default listen port from `4300` to `5030`.
|
|
246
292
|
- Added safe programmatic APIs: `buildServer()` and `startServer(options)`.
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export declare const ROOT_DIR: string;
|
|
2
|
+
export declare const MOCKS_DIR: string;
|
|
3
|
+
export declare const METADATA_DIR: string;
|
|
4
|
+
export declare const DATA_DIR: string;
|
|
5
|
+
export declare const PORT: number;
|
|
6
|
+
export declare const HOST: string;
|
|
7
|
+
export declare const DISK_FLUSH_DEBOUNCE_MS: number;
|
|
8
|
+
export declare const MEMORIX_ORG_ID: string;
|
|
9
|
+
export declare const MEMORIX_AGENT_ID: string;
|
|
10
|
+
export declare const MEMORIX_DEPLOYMENT_PROFILE: string;
|
|
11
|
+
/**
|
|
12
|
+
* Construct the router string the frontend expects.
|
|
13
|
+
* e.g. "neo-memorix-entities + neo-memorix-events" for MEMORIX_ORG_ID=neo
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildMemorixDbString(): string;
|
|
16
|
+
/**
|
|
17
|
+
* Construct the agent-scoped Catalox DB name.
|
|
18
|
+
* e.g. "ebooks-memorix-catalox"
|
|
19
|
+
*/
|
|
20
|
+
export declare function buildCataloxDbString(): string;
|
|
21
|
+
/**
|
|
22
|
+
* Files emulate routed databases by using the resolved database name as a
|
|
23
|
+
* prefix. The `--` separator is reserved for routing and must not be used in
|
|
24
|
+
* an unprefixed fixture basename.
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildDataFilePrefix(contentType: string): string;
|
|
27
|
+
export declare function buildMetadataFilePrefix(): string;
|
|
28
|
+
export declare function prefixedJsonFilename(prefix: string, basename: string): string;
|
|
29
|
+
export declare const FLAGS: {
|
|
30
|
+
METADATA_WRITES: boolean;
|
|
31
|
+
PIPELINE_WRITES: boolean;
|
|
32
|
+
REGISTRY_WRITES: boolean;
|
|
33
|
+
};
|
|
34
|
+
export declare function metadataWritesEnabled(): boolean;
|
|
35
|
+
export declare function pipelineWritesEnabled(): boolean;
|
|
36
|
+
export declare function registryWritesEnabled(): boolean;
|
|
37
|
+
export declare function metadataPath(...segments: string[]): string;
|
|
38
|
+
export declare function dataPath(...segments: string[]): string;
|
package/dist/config.js
CHANGED
|
@@ -46,6 +46,24 @@ export function buildMemorixDbString() {
|
|
|
46
46
|
export function buildCataloxDbString() {
|
|
47
47
|
return `${MEMORIX_AGENT_ID}-memorix-catalox`;
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Files emulate routed databases by using the resolved database name as a
|
|
51
|
+
* prefix. The `--` separator is reserved for routing and must not be used in
|
|
52
|
+
* an unprefixed fixture basename.
|
|
53
|
+
*/
|
|
54
|
+
export function buildDataFilePrefix(contentType) {
|
|
55
|
+
if (contentType === "events")
|
|
56
|
+
return `${MEMORIX_ORG_ID}-memorix-events`;
|
|
57
|
+
if (contentType === "memory")
|
|
58
|
+
return buildCataloxDbString();
|
|
59
|
+
return `${MEMORIX_ORG_ID}-memorix-entities`;
|
|
60
|
+
}
|
|
61
|
+
export function buildMetadataFilePrefix() {
|
|
62
|
+
return buildCataloxDbString();
|
|
63
|
+
}
|
|
64
|
+
export function prefixedJsonFilename(prefix, basename) {
|
|
65
|
+
return `${prefix}--${basename}.json`;
|
|
66
|
+
}
|
|
49
67
|
// ---------------------------------------------------------------------------
|
|
50
68
|
// Feature Flag Enforcement
|
|
51
69
|
// ---------------------------------------------------------------------------
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { IncludeFlag } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Storage Alias Translation Map
|
|
4
|
+
* Client API key -> storage property / fallback.
|
|
5
|
+
*/
|
|
6
|
+
export interface AliasEntry {
|
|
7
|
+
clientKey: string;
|
|
8
|
+
storageProp: string;
|
|
9
|
+
fallbacks: string[];
|
|
10
|
+
}
|
|
11
|
+
export declare const STORAGE_ALIASES: Record<string, AliasEntry>;
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a normalized associated bucket from a raw document.
|
|
14
|
+
* Strips raw `associated*` fields and reconstructs the `associated` bucket
|
|
15
|
+
* using the alias map. Custom associated properties follow the pattern
|
|
16
|
+
* associated{Custom}.
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildAssociatedBucket(rawDoc: Record<string, unknown>): {
|
|
19
|
+
associated: Record<string, unknown>;
|
|
20
|
+
strippedDoc: Record<string, unknown>;
|
|
21
|
+
properties: Array<{
|
|
22
|
+
propertyName: string;
|
|
23
|
+
source: string;
|
|
24
|
+
count: number;
|
|
25
|
+
}>;
|
|
26
|
+
};
|
|
27
|
+
export declare function aliasStorageToClient(storageKey: string): string;
|
|
28
|
+
export declare function aliasClientToStorage(clientKey: string): {
|
|
29
|
+
primary: string;
|
|
30
|
+
fallbacks: string[];
|
|
31
|
+
};
|
|
32
|
+
export declare function resolveInclude(include?: string | string[]): Set<IncludeFlag> | null;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export interface AssociationPlan {
|
|
2
|
+
objectType: string;
|
|
3
|
+
expectedPlanFingerprint: string;
|
|
4
|
+
proposedMutations: Array<{
|
|
5
|
+
recordId: string;
|
|
6
|
+
targetArray: string;
|
|
7
|
+
op: string;
|
|
8
|
+
count: number;
|
|
9
|
+
}>;
|
|
10
|
+
}
|
|
11
|
+
export declare function planAssociations(objectType?: string): AssociationPlan[];
|
|
12
|
+
export interface ApplyResult {
|
|
13
|
+
objectType: string;
|
|
14
|
+
applied: boolean;
|
|
15
|
+
expectedPlanFingerprint: string;
|
|
16
|
+
actualFingerprint: string;
|
|
17
|
+
mismatch: boolean;
|
|
18
|
+
count: number;
|
|
19
|
+
}
|
|
20
|
+
export declare function applyAssociations(expectedPlanFingerprint: string, objectType?: string): ApplyResult[];
|
|
21
|
+
export declare function verifyAssociations(expectedPlanFingerprint: string, objectType?: string): {
|
|
22
|
+
objectType: string;
|
|
23
|
+
expectedPlanFingerprint: string;
|
|
24
|
+
actualFingerprint: string;
|
|
25
|
+
verified: boolean;
|
|
26
|
+
}[];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ContentType, IdentityKey } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the primary identity field for a given object type / content type.
|
|
4
|
+
* `memory` collections are keyed by `memoryId`; everything else falls back to
|
|
5
|
+
* the per-document heuristic used elsewhere.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveIdField(ct: ContentType, sample?: any): IdentityKey;
|
|
8
|
+
/**
|
|
9
|
+
* Mock of the production `MemorixRecordArraySchema`. Validates that each record
|
|
10
|
+
* in a collection array carries the collection's canonical identity key.
|
|
11
|
+
* Additional identity-shaped fields are allowed because they can represent
|
|
12
|
+
* relationships (for example a memory has `memoryId` and may reference an
|
|
13
|
+
* `entityId` or `eventId`).
|
|
14
|
+
*/
|
|
15
|
+
export declare function validateRecordArray(objectType: string, ct: ContentType, records: any[]): {
|
|
16
|
+
ok: boolean;
|
|
17
|
+
errors: string[];
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Find a record by any of the supported identity keys.
|
|
21
|
+
*/
|
|
22
|
+
export declare function findByIdentity(records: any[], identity: Partial<Record<IdentityKey, string>>): any | undefined;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ExplorerQueryParams } from "../types.js";
|
|
2
|
+
export interface InventoryRow {
|
|
3
|
+
objectType: string;
|
|
4
|
+
contentType: string;
|
|
5
|
+
collection: string;
|
|
6
|
+
count: number;
|
|
7
|
+
status: "populated" | "empty" | "orphan";
|
|
8
|
+
provenance: {
|
|
9
|
+
source: "catalog" | "orphan";
|
|
10
|
+
};
|
|
11
|
+
missingCollections?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function listObjectTypes(): string[];
|
|
14
|
+
export declare function listDataObjectTypes(): string[];
|
|
15
|
+
export declare function computeInventory(params: ExplorerQueryParams): InventoryRow[];
|
|
16
|
+
export declare function computeInventorySummary(rows: InventoryRow[], params: ExplorerQueryParams): Record<string, unknown>;
|
|
17
|
+
export declare function computeInventoryIssues(rows: InventoryRow[]): InventoryRow[];
|
|
18
|
+
export declare function computeGraph(rows: InventoryRow[]): {
|
|
19
|
+
nodes: {
|
|
20
|
+
id: string;
|
|
21
|
+
objectType: string;
|
|
22
|
+
populated: boolean;
|
|
23
|
+
}[];
|
|
24
|
+
edges: {
|
|
25
|
+
from: string;
|
|
26
|
+
to: string;
|
|
27
|
+
relationType?: string;
|
|
28
|
+
}[];
|
|
29
|
+
};
|
package/dist/engine/inventory.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { store } from "../storage/InMemoryStore.js";
|
|
3
3
|
import { CONTENT_TYPES } from "../types.js";
|
|
4
|
-
import {
|
|
5
|
-
import { METADATA_DIR, DATA_DIR } from "../config.js";
|
|
4
|
+
import { DATA_DIR } from "../config.js";
|
|
6
5
|
import fs from "node:fs";
|
|
7
6
|
import { isDir } from "../storage/fs.js";
|
|
8
7
|
export function listObjectTypes() {
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
return store.getMetaKeys()
|
|
9
|
+
.filter((key) => key.startsWith("object-types/"))
|
|
10
|
+
.map((key) => key.slice("object-types/".length));
|
|
11
11
|
}
|
|
12
12
|
export function listDataObjectTypes() {
|
|
13
13
|
if (!isDir(DATA_DIR))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type ListDescriptor, type ExplorerQueryParams } from "../types.js";
|
|
2
|
+
export declare function listListDescriptors(entityName?: string): ListDescriptor[];
|
|
3
|
+
export declare function getListDescriptor(listId: string): ListDescriptor | undefined;
|
|
4
|
+
/**
|
|
5
|
+
* Executes a list descriptor query pipeline against local data arrays.
|
|
6
|
+
*/
|
|
7
|
+
export declare function executeListDescriptor(desc: ListDescriptor, params?: ExplorerQueryParams): Promise<any>;
|
|
8
|
+
/**
|
|
9
|
+
* Suggest extensions / analytics for unjoined content types.
|
|
10
|
+
*/
|
|
11
|
+
export declare function suggestListExtensions(entityName: string): {
|
|
12
|
+
entityName: string;
|
|
13
|
+
suggestions: {
|
|
14
|
+
contentType: "events" | "memory" | "analysis" | "decisions";
|
|
15
|
+
collection: string;
|
|
16
|
+
suggestion: string;
|
|
17
|
+
}[];
|
|
18
|
+
};
|
package/dist/engine/lists.js
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
1
|
import { store } from "../storage/InMemoryStore.js";
|
|
3
2
|
import { CONTENT_TYPES } from "../types.js";
|
|
4
|
-
import { listJsonFiles, basenameNoExt } from "../storage/fs.js";
|
|
5
|
-
import { METADATA_DIR } from "../config.js";
|
|
6
3
|
import { queryMockCollection } from "../engine/query.js";
|
|
7
4
|
export function listListDescriptors(entityName) {
|
|
8
|
-
const dir = path.join(METADATA_DIR, "lists");
|
|
9
5
|
const out = [];
|
|
10
|
-
for (const
|
|
11
|
-
const desc = store.getMeta(
|
|
6
|
+
for (const key of store.getMetaKeys().filter((item) => item.startsWith("lists/"))) {
|
|
7
|
+
const desc = store.getMeta(key);
|
|
12
8
|
if (desc && (!entityName || desc.entity === entityName || desc.entityName === entityName)) {
|
|
13
9
|
out.push(desc);
|
|
14
10
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type NarrativeDefinition } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Discover narrative keys tagged on records via doc.narratives.*.
|
|
4
|
+
*/
|
|
5
|
+
export declare function discoverRecordNarrativeTags(entityName: string): string[];
|
|
6
|
+
export declare function mergeCatalogWithSignals(authored: Record<string, NarrativeDefinition>, recordTags: string[]): Array<NarrativeDefinition & {
|
|
7
|
+
signaled: boolean;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function getMergedNarratives(entityName?: string): (NarrativeDefinition & {
|
|
10
|
+
signaled: boolean;
|
|
11
|
+
})[] | Record<string, (NarrativeDefinition & {
|
|
12
|
+
signaled: boolean;
|
|
13
|
+
})[]>;
|
|
14
|
+
export declare function getRawNarratives(entityName: string): Record<string, NarrativeDefinition>;
|
|
15
|
+
export declare function queryNarrativeRecords(entityName: string, key: string): any[];
|
|
16
|
+
export declare function listNarrativeEntities(): string[];
|
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
1
|
import { store } from "../storage/InMemoryStore.js";
|
|
3
2
|
import { CONTENT_TYPES } from "../types.js";
|
|
4
|
-
import { listJsonFiles, basenameNoExt, readJson } from "../storage/fs.js";
|
|
5
|
-
import { METADATA_DIR } from "../config.js";
|
|
6
|
-
import { isDir } from "../storage/fs.js";
|
|
7
|
-
import fs from "node:fs";
|
|
8
3
|
/**
|
|
9
4
|
* Discover narrative keys tagged on records via doc.narratives.*.
|
|
10
5
|
*/
|
|
@@ -40,11 +35,10 @@ export function getMergedNarratives(entityName) {
|
|
|
40
35
|
return mergeCatalogWithSignals(authored, recordTags);
|
|
41
36
|
}
|
|
42
37
|
// global across all entities
|
|
43
|
-
const dir = path.join(METADATA_DIR, "narratives");
|
|
44
38
|
const out = {};
|
|
45
|
-
for (const
|
|
46
|
-
const name =
|
|
47
|
-
const authored =
|
|
39
|
+
for (const metaKey of store.getMetaKeys().filter((key) => key.startsWith("narratives/"))) {
|
|
40
|
+
const name = metaKey.slice("narratives/".length);
|
|
41
|
+
const authored = store.getMeta(metaKey) || {};
|
|
48
42
|
const recordTags = discoverRecordNarrativeTags(name);
|
|
49
43
|
out[name] = mergeCatalogWithSignals(authored, recordTags);
|
|
50
44
|
}
|
|
@@ -67,11 +61,7 @@ export function queryNarrativeRecords(entityName, key) {
|
|
|
67
61
|
return results;
|
|
68
62
|
}
|
|
69
63
|
export function listNarrativeEntities() {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return fs
|
|
74
|
-
.readdirSync(dir)
|
|
75
|
-
.filter((f) => f.endsWith(".json"))
|
|
76
|
-
.map((f) => basenameNoExt(f));
|
|
64
|
+
return store.getMetaKeys()
|
|
65
|
+
.filter((key) => key.startsWith("narratives/"))
|
|
66
|
+
.map((key) => key.slice("narratives/".length));
|
|
77
67
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface RootPropertyEntry {
|
|
2
|
+
path: string;
|
|
3
|
+
occurrencePct: number;
|
|
4
|
+
types: string[];
|
|
5
|
+
sampleValues: unknown[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Full-scan all record arrays in ./data/{name}/*.json, compute property
|
|
9
|
+
* occurrence percentages, data types, and paths.
|
|
10
|
+
*/
|
|
11
|
+
export declare function computeRootPropertyCatalog(objectType: string): RootPropertyEntry[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { ExplorerQueryParams } from "../types.js";
|
|
2
|
+
export type FilterOp = "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "in" | "nin" | "exists" | "regex";
|
|
3
|
+
export interface ParsedFilter {
|
|
4
|
+
field: string;
|
|
5
|
+
op: FilterOp;
|
|
6
|
+
value: unknown;
|
|
7
|
+
raw: string;
|
|
8
|
+
isVirtual?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function parseFilterToken(token: string): ParsedFilter | null;
|
|
11
|
+
export declare function coerce(value: string): unknown;
|
|
12
|
+
/**
|
|
13
|
+
* Parse the filter param. Supports repeated values (array) and `;` separators.
|
|
14
|
+
* Returns virtual narrative filters separately so the caller can rewrite them.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseExplorerFilters(filter?: string | string[]): {
|
|
17
|
+
criteria: Record<string, unknown>;
|
|
18
|
+
virtual: ParsedFilter[];
|
|
19
|
+
};
|
|
20
|
+
export declare function parseSortParams(sort?: string | string[]): Record<string, 1 | -1>;
|
|
21
|
+
/**
|
|
22
|
+
* Resolve virtual narrative filters into a predicate that checks
|
|
23
|
+
* doc.narratives[key] presence / truthiness.
|
|
24
|
+
*/
|
|
25
|
+
export declare function applyVirtualNarrativeFilters(records: any[], virtual: ParsedFilter[]): any[];
|
|
26
|
+
export interface QueryResult {
|
|
27
|
+
rows: any[];
|
|
28
|
+
page: {
|
|
29
|
+
offset: number;
|
|
30
|
+
limit: number;
|
|
31
|
+
total?: number;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Execute a query against an in-memory record array using mingo, with
|
|
36
|
+
* full-text search, virtual narrative filters, sorting, and pagination.
|
|
37
|
+
*/
|
|
38
|
+
export declare function queryMockCollection(records: any[], queryParams: ExplorerQueryParams): QueryResult;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type ContentType, type ExplorerQueryParams, type IdentityKey } from "../types.js";
|
|
2
|
+
export declare function inferIdField(entityName: string, ct: ContentType): string;
|
|
3
|
+
/**
|
|
4
|
+
* Find a record within a collection using an identity map (recordId, entityId,
|
|
5
|
+
* eventId, knowledgeId, or memoryId). At least one identity key is required.
|
|
6
|
+
*/
|
|
7
|
+
export declare function findRecordByIdentity(entityName: string, ct: ContentType, identity: Partial<Record<IdentityKey, string>>): any | undefined;
|
|
8
|
+
export declare function findRecord(entityName: string, ct: ContentType, recordId: string): any | undefined;
|
|
9
|
+
export declare function getRecordsCollection(params: ExplorerQueryParams): Promise<{
|
|
10
|
+
rows: any[];
|
|
11
|
+
page: any;
|
|
12
|
+
}>;
|
|
13
|
+
export declare function getRecordsFull(entityName: string, recordId: string): any;
|
|
14
|
+
export declare function getRecordsItem(entityName: string, recordId: string, itemDescriptor?: any): any;
|
|
15
|
+
/**
|
|
16
|
+
* Fetch a single content record by id. Supports the memory tier via memoryId.
|
|
17
|
+
* If entityName is empty and contentType is "memory", defaults to "memory".
|
|
18
|
+
* Otherwise, searches across all object types in the store.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getRecordContent(entityName: string, contentType: ContentType, identity: Partial<Record<IdentityKey, string>>): any | undefined;
|
|
21
|
+
export declare function getRawCollection(entityName: string, collectionName: string): any[];
|
|
22
|
+
export declare function getRawItem(collectionName: string, recordId: string): any | undefined;
|
|
23
|
+
export declare function getWorkspaceRecords(params: ExplorerQueryParams): Promise<any>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare function getSnapshotRaw(objectType: string, recordId: string): {
|
|
2
|
+
rawDoc: any;
|
|
3
|
+
idField: string;
|
|
4
|
+
} | undefined;
|
|
5
|
+
export declare function getSnapshotNormalized(objectType: string, recordId: string, include?: string | string[]): any | undefined;
|
|
6
|
+
export declare function getSnapshotAssociated(objectType: string, recordId: string): any | undefined;
|
|
7
|
+
export declare function getSnapshotAssociatedProperty(objectType: string, recordId: string, propertyName: string): any | undefined;
|
|
8
|
+
export declare function getAssociatedProperties(objectType: string): Array<{
|
|
9
|
+
propertyName: string;
|
|
10
|
+
source: string;
|
|
11
|
+
count: number;
|
|
12
|
+
}>;
|
|
13
|
+
/**
|
|
14
|
+
* Deterministic fingerprint for association plan/apply/verify.
|
|
15
|
+
*/
|
|
16
|
+
export declare function computeAssociationFingerprint(objectType: string): string;
|
|
17
|
+
export declare function hashString(s: string): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ContentType, WriteDescriptor } from "../types.js";
|
|
2
|
+
export type WriteOperation = "add" | "upsert" | "patch" | "replace" | "delete";
|
|
3
|
+
export declare function loadWriteDescriptor(writeDescriptorId: string): WriteDescriptor;
|
|
4
|
+
export interface ValidationResult {
|
|
5
|
+
ok: boolean;
|
|
6
|
+
errors: string[];
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Validate input against the write descriptor schema (lightweight AJV-free check).
|
|
10
|
+
*
|
|
11
|
+
* `operation` controls how `required` is enforced:
|
|
12
|
+
* - `add` / `replace`: every field in `schema.required` must be present.
|
|
13
|
+
* - `upsert` / `patch` / `delete`: only the collection's identity field
|
|
14
|
+
* (e.g. `recordId`, `memoryId`) is required; other `required` entries are
|
|
15
|
+
* ignored because partial updates are allowed.
|
|
16
|
+
*
|
|
17
|
+
* Type checks on whatever fields ARE present always run.
|
|
18
|
+
*/
|
|
19
|
+
export declare function validateInputSchema(writeSpec: WriteDescriptor, input: any, operation?: WriteOperation): ValidationResult;
|
|
20
|
+
export declare function parseTargetCollection(target: string): {
|
|
21
|
+
objectType: string;
|
|
22
|
+
ct: ContentType;
|
|
23
|
+
};
|
|
24
|
+
export declare function executeOperation(params: {
|
|
25
|
+
target: string;
|
|
26
|
+
op: WriteOperation;
|
|
27
|
+
data: any;
|
|
28
|
+
persist?: boolean;
|
|
29
|
+
}): any[];
|
|
30
|
+
export declare function listWriteDescriptors(): WriteDescriptor[];
|
package/dist/engine/write.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
1
|
import { store } from "../storage/InMemoryStore.js";
|
|
3
|
-
import { basenameNoExt, listJsonFiles } from "../storage/fs.js";
|
|
4
|
-
import { METADATA_DIR } from "../config.js";
|
|
5
2
|
export function loadWriteDescriptor(writeDescriptorId) {
|
|
6
3
|
const desc = store.getMeta(`write-descriptors/${writeDescriptorId}`);
|
|
7
4
|
if (!desc) {
|
|
@@ -156,10 +153,9 @@ function getNextId(collection, idField = "recordId") {
|
|
|
156
153
|
return () => String(counter++);
|
|
157
154
|
}
|
|
158
155
|
export function listWriteDescriptors() {
|
|
159
|
-
const dir = path.join(METADATA_DIR, "write-descriptors");
|
|
160
156
|
const out = [];
|
|
161
|
-
for (const
|
|
162
|
-
const desc = store.getMeta(
|
|
157
|
+
for (const key of store.getMetaKeys().filter((item) => item.startsWith("write-descriptors/"))) {
|
|
158
|
+
const desc = store.getMeta(key);
|
|
163
159
|
if (desc)
|
|
164
160
|
out.push(desc);
|
|
165
161
|
}
|
package/dist/routes/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { store } from "../storage/InMemoryStore.js";
|
|
2
|
-
import { buildMemorixDbString, buildCataloxDbString, metadataWritesEnabled, } from "../config.js";
|
|
2
|
+
import { buildMemorixDbString, buildCataloxDbString, buildDataFilePrefix, buildMetadataFilePrefix, metadataWritesEnabled, } from "../config.js";
|
|
3
3
|
import { listObjectTypes } from "../engine/inventory.js";
|
|
4
4
|
import { computeInventory, computeInventorySummary, computeInventoryIssues, computeGraph, } from "../engine/inventory.js";
|
|
5
5
|
import { qp, str } from "./helpers.js";
|
|
@@ -25,6 +25,12 @@ export async function registerRoutes(app) {
|
|
|
25
25
|
discoverySample: objectTypes,
|
|
26
26
|
memorixDb: buildMemorixDbString(),
|
|
27
27
|
cataloxDb: buildCataloxDbString(),
|
|
28
|
+
fileRouting: {
|
|
29
|
+
entitiesPrefix: buildDataFilePrefix("snapshots"),
|
|
30
|
+
eventsPrefix: buildDataFilePrefix("events"),
|
|
31
|
+
memoryPrefix: buildDataFilePrefix("memory"),
|
|
32
|
+
metadataPrefix: buildMetadataFilePrefix(),
|
|
33
|
+
},
|
|
28
34
|
};
|
|
29
35
|
if (includeInventory) {
|
|
30
36
|
const rows = computeInventory({ sourceLens: "catalox-first" });
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export interface StartServerOptions {
|
|
3
|
+
port?: number;
|
|
4
|
+
host?: string;
|
|
5
|
+
logger?: boolean;
|
|
6
|
+
installSignalHandlers?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function parseCliArgs(args: string[]): StartServerOptions;
|
|
9
|
+
/** Build a loaded Fastify instance without opening a network port. */
|
|
10
|
+
export declare function buildServer(options?: Pick<StartServerOptions, "logger">): Promise<import("fastify").FastifyInstance<import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>, import("http").IncomingMessage, import("http").ServerResponse<import("http").IncomingMessage>, import("fastify").FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>>;
|
|
11
|
+
/** Build and listen. Options override PORT/HOST environment configuration. */
|
|
12
|
+
export declare function startServer(options?: StartServerOptions): Promise<import("fastify").FastifyInstance<import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>, import("http").IncomingMessage, import("http").ServerResponse<import("http").IncomingMessage>, import("fastify").FastifyBaseLogger, import("fastify").FastifyTypeProviderDefault>>;
|
package/dist/server.js
CHANGED
|
@@ -5,6 +5,32 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { store } from "./storage/InMemoryStore.js";
|
|
6
6
|
import { registerRoutes } from "./routes/index.js";
|
|
7
7
|
import { PORT, HOST, MOCKS_DIR } from "./config.js";
|
|
8
|
+
export function parseCliArgs(args) {
|
|
9
|
+
const options = {};
|
|
10
|
+
for (let index = 0; index < args.length; index++) {
|
|
11
|
+
const arg = args[index];
|
|
12
|
+
if (arg === "--port") {
|
|
13
|
+
const raw = args[++index];
|
|
14
|
+
const port = Number(raw);
|
|
15
|
+
if (!raw || !Number.isInteger(port) || port < 0 || port > 65535) {
|
|
16
|
+
throw Object.assign(new Error("--port must be an integer from 0 to 65535"), {
|
|
17
|
+
statusCode: 400,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
options.port = port;
|
|
21
|
+
}
|
|
22
|
+
else if (arg === "--host") {
|
|
23
|
+
const host = args[++index];
|
|
24
|
+
if (!host)
|
|
25
|
+
throw Object.assign(new Error("--host requires a value"), { statusCode: 400 });
|
|
26
|
+
options.host = host;
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
throw Object.assign(new Error(`Unknown CLI argument: ${arg}`), { statusCode: 400 });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return options;
|
|
33
|
+
}
|
|
8
34
|
/** Build a loaded Fastify instance without opening a network port. */
|
|
9
35
|
export async function buildServer(options = {}) {
|
|
10
36
|
const app = Fastify({ logger: options.logger ?? false });
|
|
@@ -31,8 +57,10 @@ export async function startServer(options = {}) {
|
|
|
31
57
|
const port = options.port ?? PORT;
|
|
32
58
|
const host = options.host ?? HOST;
|
|
33
59
|
await app.listen({ port, host });
|
|
60
|
+
const address = app.server.address();
|
|
61
|
+
const boundPort = typeof address === "object" && address ? address.port : port;
|
|
34
62
|
// eslint-disable-next-line no-console
|
|
35
|
-
console.log(`[mock-memorix-explorer-api] listening on ${host}:${
|
|
63
|
+
console.log(`[mock-memorix-explorer-api] listening on ${host}:${boundPort}`);
|
|
36
64
|
console.log(`[mock-memorix-explorer-api] mocks dir: ${MOCKS_DIR}`);
|
|
37
65
|
if (options.installSignalHandlers ?? true) {
|
|
38
66
|
const shutdown = async () => {
|
|
@@ -50,7 +78,7 @@ const isCliEntry = process.argv[1]
|
|
|
50
78
|
? path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
51
79
|
: false;
|
|
52
80
|
if (isCliEntry) {
|
|
53
|
-
startServer().catch((err) => {
|
|
81
|
+
Promise.resolve().then(() => startServer(parseCliArgs(process.argv.slice(2)))).catch((err) => {
|
|
54
82
|
// eslint-disable-next-line no-console
|
|
55
83
|
console.error(err);
|
|
56
84
|
process.exit(1);
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ContentType } from "../types.js";
|
|
2
|
+
export declare class InMemoryStore {
|
|
3
|
+
private collections;
|
|
4
|
+
private metadata;
|
|
5
|
+
private flushTimers;
|
|
6
|
+
private loaded;
|
|
7
|
+
/**
|
|
8
|
+
* Load everything from disk into memory. Idempotent.
|
|
9
|
+
*/
|
|
10
|
+
load(): Promise<void>;
|
|
11
|
+
private loadMetadataCatalog;
|
|
12
|
+
private loadDataCollections;
|
|
13
|
+
getMeta<T = any>(key: string): T | undefined;
|
|
14
|
+
setMeta(key: string, data: any, persist?: boolean): void;
|
|
15
|
+
getMetaKeys(): string[];
|
|
16
|
+
getCollection(objectType: string, ct: ContentType): any[];
|
|
17
|
+
setCollection(objectType: string, ct: ContentType, data: any[]): void;
|
|
18
|
+
getCollectionKeys(): string[];
|
|
19
|
+
private scheduleCollectionFlush;
|
|
20
|
+
private scheduleMetaFlush;
|
|
21
|
+
flushCollectionNow(key: string): void;
|
|
22
|
+
flushMetaNow(key: string): void;
|
|
23
|
+
/**
|
|
24
|
+
* Flush every dirty file immediately and clear timers.
|
|
25
|
+
*/
|
|
26
|
+
flushAll(): Promise<void>;
|
|
27
|
+
get mocksDir(): string;
|
|
28
|
+
}
|
|
29
|
+
export declare const store: InMemoryStore;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { CONTENT_TYPES } from "../types.js";
|
|
4
|
-
import { MOCKS_DIR, METADATA_DIR, DATA_DIR } from "../config.js";
|
|
4
|
+
import { MOCKS_DIR, METADATA_DIR, DATA_DIR, buildDataFilePrefix, buildMetadataFilePrefix, prefixedJsonFilename, } from "../config.js";
|
|
5
5
|
import { validateRecordArray } from "../engine/identity.js";
|
|
6
6
|
import { isDir, listJsonFiles, readJson, writeJson, basenameNoExt, } from "./fs.js";
|
|
7
7
|
import { DISK_FLUSH_DEBOUNCE_MS } from "../config.js";
|
|
@@ -28,18 +28,33 @@ export class InMemoryStore {
|
|
|
28
28
|
"write-descriptors": "write-descriptors",
|
|
29
29
|
narratives: "narratives",
|
|
30
30
|
};
|
|
31
|
+
const prefix = buildMetadataFilePrefix();
|
|
31
32
|
for (const [subdir] of Object.entries(sections)) {
|
|
32
33
|
const dir = path.join(METADATA_DIR, subdir);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
const files = listJsonFiles(dir);
|
|
35
|
+
const logicalNames = new Set();
|
|
36
|
+
for (const file of files) {
|
|
37
|
+
const name = basenameNoExt(file);
|
|
38
|
+
if (!name.includes("--"))
|
|
39
|
+
logicalNames.add(name);
|
|
40
|
+
if (name.startsWith(`${prefix}--`))
|
|
41
|
+
logicalNames.add(name.slice(prefix.length + 2));
|
|
42
|
+
}
|
|
43
|
+
for (const name of logicalNames) {
|
|
44
|
+
const baseFile = path.join(dir, `${name}.json`);
|
|
45
|
+
const routedFile = path.join(dir, prefixedJsonFilename(prefix, name));
|
|
46
|
+
const source = fs.existsSync(routedFile) ? routedFile : baseFile;
|
|
47
|
+
const key = `${subdir}/${name}`;
|
|
48
|
+
this.metadata.set(key, { path: routedFile, data: readJson(source, {}) });
|
|
36
49
|
}
|
|
37
50
|
}
|
|
38
|
-
const
|
|
39
|
-
|
|
51
|
+
const baseAgentsFile = path.join(METADATA_DIR, "agents.json");
|
|
52
|
+
const routedAgentsFile = path.join(METADATA_DIR, prefixedJsonFilename(prefix, "agents"));
|
|
53
|
+
const agentsSource = fs.existsSync(routedAgentsFile) ? routedAgentsFile : baseAgentsFile;
|
|
54
|
+
if (fs.existsSync(agentsSource)) {
|
|
40
55
|
this.metadata.set("agents", {
|
|
41
|
-
path:
|
|
42
|
-
data: readJson(
|
|
56
|
+
path: routedAgentsFile,
|
|
57
|
+
data: readJson(agentsSource, { agents: [] }),
|
|
43
58
|
});
|
|
44
59
|
}
|
|
45
60
|
}
|
|
@@ -51,9 +66,11 @@ export class InMemoryStore {
|
|
|
51
66
|
if (!isDir(otDir))
|
|
52
67
|
continue;
|
|
53
68
|
for (const ct of CONTENT_TYPES) {
|
|
54
|
-
const
|
|
69
|
+
const baseFile = path.join(otDir, `${ct}.json`);
|
|
70
|
+
const file = path.join(otDir, prefixedJsonFilename(buildDataFilePrefix(ct), ct));
|
|
55
71
|
const key = `${objectType}/${ct}`;
|
|
56
|
-
const
|
|
72
|
+
const source = fs.existsSync(file) ? file : baseFile;
|
|
73
|
+
const data = readJson(source, []);
|
|
57
74
|
// Startup validation: ensure records carry exactly one identity key
|
|
58
75
|
// (MemorixRecordArraySchema mock). Memory collections use memoryId.
|
|
59
76
|
const result = validateRecordArray(objectType, ct, data);
|
|
@@ -68,11 +85,13 @@ export class InMemoryStore {
|
|
|
68
85
|
});
|
|
69
86
|
}
|
|
70
87
|
// system overrides
|
|
71
|
-
const
|
|
72
|
-
|
|
88
|
+
const baseInvFile = path.join(DATA_DIR, "system", "inventory.json");
|
|
89
|
+
const invFile = path.join(DATA_DIR, "system", prefixedJsonFilename(buildDataFilePrefix("snapshots"), "inventory"));
|
|
90
|
+
const invSource = fs.existsSync(invFile) ? invFile : baseInvFile;
|
|
91
|
+
if (objectType === "system" && fs.existsSync(invSource)) {
|
|
73
92
|
this.collections.set("system/inventory", {
|
|
74
93
|
path: invFile,
|
|
75
|
-
data: readJson(
|
|
94
|
+
data: readJson(invSource, []),
|
|
76
95
|
});
|
|
77
96
|
}
|
|
78
97
|
}
|
|
@@ -89,7 +108,7 @@ export class InMemoryStore {
|
|
|
89
108
|
}
|
|
90
109
|
else {
|
|
91
110
|
this.metadata.set(key, {
|
|
92
|
-
path: path.join(METADATA_DIR, `${key}.
|
|
111
|
+
path: path.join(METADATA_DIR, `${path.dirname(key)}/${prefixedJsonFilename(buildMetadataFilePrefix(), path.basename(key))}`),
|
|
93
112
|
data,
|
|
94
113
|
[DIRTY]: true,
|
|
95
114
|
});
|
|
@@ -106,7 +125,7 @@ export class InMemoryStore {
|
|
|
106
125
|
const c = this.collections.get(key);
|
|
107
126
|
if (!c) {
|
|
108
127
|
const newC = {
|
|
109
|
-
path: path.join(DATA_DIR, objectType,
|
|
128
|
+
path: path.join(DATA_DIR, objectType, prefixedJsonFilename(buildDataFilePrefix(ct), ct)),
|
|
110
129
|
data: [],
|
|
111
130
|
};
|
|
112
131
|
this.collections.set(key, newC);
|
|
@@ -123,7 +142,7 @@ export class InMemoryStore {
|
|
|
123
142
|
}
|
|
124
143
|
else {
|
|
125
144
|
this.collections.set(key, {
|
|
126
|
-
path: path.join(DATA_DIR, objectType,
|
|
145
|
+
path: path.join(DATA_DIR, objectType, prefixedJsonFilename(buildDataFilePrefix(ct), ct)),
|
|
127
146
|
data,
|
|
128
147
|
[DIRTY]: true,
|
|
129
148
|
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function ensureDir(dir: string): void;
|
|
2
|
+
export declare function isDir(p: string): boolean;
|
|
3
|
+
export declare function readJson<T = any>(p: string, fallback: T): T;
|
|
4
|
+
export declare function writeJson(p: string, data: unknown): void;
|
|
5
|
+
export declare function listJsonFiles(dir: string): string[];
|
|
6
|
+
export declare function basenameNoExt(p: string): string;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export type ContentType = "snapshots" | "analysis" | "decisions" | "events" | "memory";
|
|
2
|
+
export declare const CONTENT_TYPES: ContentType[];
|
|
3
|
+
/**
|
|
4
|
+
* The fourth tier of data alongside entities, events, and knowledge.
|
|
5
|
+
* `memory` collections are keyed by `memoryId`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const MEMORY_CONTENT_TYPE: ContentType;
|
|
8
|
+
/** Primary identity keys accepted across fetch/write engines. */
|
|
9
|
+
export declare const IDENTITY_KEYS: readonly ["recordId", "entityId", "eventId", "knowledgeId", "memoryId"];
|
|
10
|
+
export type IdentityKey = (typeof IDENTITY_KEYS)[number];
|
|
11
|
+
/** Map a target type to its canonical id field. */
|
|
12
|
+
export declare const TARGET_TO_ID_FIELD: Record<string, IdentityKey>;
|
|
13
|
+
export declare const INCLUDE_FLAGS: readonly ["data", "analytics", "enrichment", "insights", "system", "associated", "discovery", "analysis"];
|
|
14
|
+
export type IncludeFlag = (typeof INCLUDE_FLAGS)[number];
|
|
15
|
+
export interface ExplorerQueryParams {
|
|
16
|
+
filter?: string | string[];
|
|
17
|
+
sort?: string | string[];
|
|
18
|
+
searchText?: string;
|
|
19
|
+
q?: string;
|
|
20
|
+
limit?: string | number;
|
|
21
|
+
offset?: string | number;
|
|
22
|
+
skip?: string | number;
|
|
23
|
+
includeTotal?: boolean;
|
|
24
|
+
include?: string | string[];
|
|
25
|
+
contentType?: string;
|
|
26
|
+
mode?: "descriptor" | "raw" | "auto";
|
|
27
|
+
listId?: string;
|
|
28
|
+
entityName?: string;
|
|
29
|
+
sourceLens?: "db-first" | "catalox-first";
|
|
30
|
+
target?: "entity" | "event" | "knowledge" | "memory" | string;
|
|
31
|
+
includeExactCounts?: boolean;
|
|
32
|
+
includeInventory?: boolean | string;
|
|
33
|
+
memoryId?: string;
|
|
34
|
+
recordId?: string;
|
|
35
|
+
entityId?: string;
|
|
36
|
+
eventId?: string;
|
|
37
|
+
knowledgeId?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface CatalogRelation {
|
|
40
|
+
sourceProperty?: string;
|
|
41
|
+
targetObject?: string;
|
|
42
|
+
targetProperty?: string;
|
|
43
|
+
relationType?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface ObjectTypeMeta {
|
|
46
|
+
name: string;
|
|
47
|
+
summary?: string;
|
|
48
|
+
rootPropertyCatalog?: RootPropertyCatalogEntry[];
|
|
49
|
+
catalogRelations?: CatalogRelation[];
|
|
50
|
+
[key: string]: unknown;
|
|
51
|
+
}
|
|
52
|
+
export interface RootPropertyCatalogEntry {
|
|
53
|
+
path: string;
|
|
54
|
+
occurrencePct?: number;
|
|
55
|
+
types?: string[];
|
|
56
|
+
sampleValues?: unknown[];
|
|
57
|
+
[key: string]: unknown;
|
|
58
|
+
}
|
|
59
|
+
export interface ListDescriptor {
|
|
60
|
+
id?: string;
|
|
61
|
+
listId?: string;
|
|
62
|
+
entity?: string;
|
|
63
|
+
entityName?: string;
|
|
64
|
+
fields?: string[];
|
|
65
|
+
projections?: Record<string, unknown>;
|
|
66
|
+
extensions?: string[];
|
|
67
|
+
sort?: Record<string, 1 | -1>;
|
|
68
|
+
query?: Record<string, unknown>;
|
|
69
|
+
[key: string]: unknown;
|
|
70
|
+
}
|
|
71
|
+
export interface WriteDescriptor {
|
|
72
|
+
writeDescriptorId?: string;
|
|
73
|
+
id?: string;
|
|
74
|
+
targetCollection?: string;
|
|
75
|
+
schema?: unknown;
|
|
76
|
+
[key: string]: unknown;
|
|
77
|
+
}
|
|
78
|
+
export interface NarrativeDefinition {
|
|
79
|
+
key: string;
|
|
80
|
+
label?: string;
|
|
81
|
+
description?: string;
|
|
82
|
+
[key: string]: unknown;
|
|
83
|
+
}
|
|
84
|
+
export interface AgentEntry {
|
|
85
|
+
id?: string;
|
|
86
|
+
name?: string;
|
|
87
|
+
[key: string]: unknown;
|
|
88
|
+
}
|
|
@@ -1,5 +1,61 @@
|
|
|
1
1
|
# Managing JSON Files
|
|
2
2
|
|
|
3
|
+
## File-backed database routing
|
|
4
|
+
|
|
5
|
+
The static server has no database, so database routing is represented by a
|
|
6
|
+
reserved filename prefix followed by `--`.
|
|
7
|
+
|
|
8
|
+
With:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
MEMORIX_ORG_ID=neo
|
|
12
|
+
MEMORIX_AGENT_ID=neo-agent
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
the routing matrix is:
|
|
16
|
+
|
|
17
|
+
| Data kind | Filename prefix | Example |
|
|
18
|
+
| --- | --- | --- |
|
|
19
|
+
| Snapshots, analysis, decisions | `neo-memorix-entities--` | `data/product/neo-memorix-entities--snapshots.json` |
|
|
20
|
+
| Events | `neo-memorix-events--` | `data/product/neo-memorix-events--events.json` |
|
|
21
|
+
| Memory | `neo-agent-memorix-catalox--` | `data/memory/neo-agent-memorix-catalox--memory.json` |
|
|
22
|
+
| Metadata | `neo-agent-memorix-catalox--` | `metadata/lists/neo-agent-memorix-catalox--my-plate.json` |
|
|
23
|
+
|
|
24
|
+
### Read and write precedence
|
|
25
|
+
|
|
26
|
+
1. The server first looks for the routed, prefixed file.
|
|
27
|
+
2. If it does not exist, it reads the matching unprefixed file as seed data.
|
|
28
|
+
3. Any mutation writes the complete resulting collection to the routed file.
|
|
29
|
+
4. Unprefixed seed files are never overwritten while routed mode is active.
|
|
30
|
+
5. Files belonging to a different prefix are ignored by the active process.
|
|
31
|
+
|
|
32
|
+
This makes onboarding migration-safe: existing files such as
|
|
33
|
+
`product/snapshots.json` continue to seed a new tenant, while the first write
|
|
34
|
+
creates `product/neo-memorix-entities--snapshots.json`. Starting with another
|
|
35
|
+
organization ID selects a different file and therefore different state.
|
|
36
|
+
|
|
37
|
+
The delimiter `--` is reserved. Do not use it inside ordinary, unprefixed
|
|
38
|
+
fixture basenames.
|
|
39
|
+
|
|
40
|
+
### Inspect the active route
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
curl http://localhost:5030/api/explorer/health
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The response includes:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"fileRouting": {
|
|
51
|
+
"entitiesPrefix": "neo-memorix-entities",
|
|
52
|
+
"eventsPrefix": "neo-memorix-events",
|
|
53
|
+
"memoryPrefix": "neo-agent-memorix-catalox",
|
|
54
|
+
"metadataPrefix": "neo-agent-memorix-catalox"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
3
59
|
The mock server stores all state in JSON files under the `mocks/` directory. At
|
|
4
60
|
startup, every file is loaded into an in-memory store. Mutations happen in
|
|
5
61
|
memory and are debounced-flushed back to disk automatically.
|
|
@@ -13,6 +13,8 @@ npm install -g @x12i/static-memorix-explorer-api
|
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
This makes the `mock-memorix-explorer-api` command available globally.
|
|
16
|
+
The preferred command name is `static-memorix-explorer-api`; the older
|
|
17
|
+
`mock-memorix-explorer-api` name remains as a compatibility alias.
|
|
16
18
|
|
|
17
19
|
### From source (git clone)
|
|
18
20
|
|
|
@@ -43,7 +45,7 @@ iteration, but not suitable for distribution.
|
|
|
43
45
|
### As a global CLI
|
|
44
46
|
|
|
45
47
|
```bash
|
|
46
|
-
|
|
48
|
+
static-memorix-explorer-api
|
|
47
49
|
```
|
|
48
50
|
|
|
49
51
|
After `npm install -g`, the CLI starts the server with default settings. The
|
|
@@ -55,6 +57,14 @@ mocks directory defaults to a `mocks/` folder relative to the installed package.
|
|
|
55
57
|
npx @x12i/static-memorix-explorer-api
|
|
56
58
|
```
|
|
57
59
|
|
|
60
|
+
Choose a port with a CLI option or environment variable. CLI options take
|
|
61
|
+
precedence:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
static-memorix-explorer-api --port 5030 --host 127.0.0.1
|
|
65
|
+
PORT=5030 HOST=127.0.0.1 static-memorix-explorer-api
|
|
66
|
+
```
|
|
67
|
+
|
|
58
68
|
### From application code
|
|
59
69
|
|
|
60
70
|
Importing the package does not open a port. Call `startServer()` explicitly:
|
|
@@ -111,11 +121,25 @@ correct tenant.
|
|
|
111
121
|
The health endpoint returns:
|
|
112
122
|
- `memorixDb`: `"<orgId>-memorix-entities + <orgId>-memorix-events"`
|
|
113
123
|
- `cataloxDb`: `"<agentId>-memorix-catalox"`
|
|
124
|
+
- `fileRouting`: the concrete entity, event, memory, and metadata filename
|
|
125
|
+
prefixes selected for this process
|
|
126
|
+
|
|
127
|
+
These are functional routes, not display-only labels. For example:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
MEMORIX_ORG_ID=neo MEMORIX_AGENT_ID=neo-agent npm start
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
selects filenames beginning with `neo-memorix-entities--`,
|
|
134
|
+
`neo-memorix-events--`, and `neo-agent-memorix-catalox--`. See
|
|
135
|
+
[managing-json-files.md](./managing-json-files.md#file-backed-database-routing)
|
|
136
|
+
for exact examples and fallback behavior.
|
|
114
137
|
|
|
115
138
|
### Feature flags
|
|
116
139
|
|
|
117
|
-
|
|
118
|
-
|
|
140
|
+
Metadata mutations on lists and narratives are gated behind feature flags.
|
|
141
|
+
Record mutations are governed by write descriptors and are available by
|
|
142
|
+
default.
|
|
119
143
|
|
|
120
144
|
| Variable | Controls |
|
|
121
145
|
| ------------------------------------------- | --------------------- |
|
package/package.json
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x12i/static-memorix-explorer-api",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Static mock server providing full API parity for the Memorix Explorer API.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/server.js",
|
|
7
7
|
"exports": {
|
|
8
8
|
".": {
|
|
9
|
+
"types": "./dist/server.d.ts",
|
|
9
10
|
"import": "./dist/server.js"
|
|
10
11
|
},
|
|
11
12
|
"./config": {
|
|
13
|
+
"types": "./dist/config.d.ts",
|
|
12
14
|
"import": "./dist/config.js"
|
|
13
15
|
}
|
|
14
16
|
},
|
|
15
17
|
"bin": {
|
|
18
|
+
"static-memorix-explorer-api": "dist/server.js",
|
|
16
19
|
"mock-memorix-explorer-api": "dist/server.js"
|
|
17
20
|
},
|
|
18
21
|
"scripts": {
|
|
@@ -59,6 +62,6 @@
|
|
|
59
62
|
"static"
|
|
60
63
|
],
|
|
61
64
|
"engines": {
|
|
62
|
-
"node": ">=
|
|
65
|
+
"node": ">=20.0.0"
|
|
63
66
|
}
|
|
64
67
|
}
|