@x12i/static-memorix-explorer-api 1.1.0 → 1.2.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 +63 -5
- package/dist/config.d.ts +30 -0
- package/dist/config.js +1 -1
- 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/lists.d.ts +18 -0
- package/dist/engine/narratives.d.ts +16 -0
- 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/routes/helpers.d.ts +4 -0
- package/dist/routes/index.d.ts +2 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.js +63 -18
- package/dist/storage/InMemoryStore.d.ts +29 -0
- package/dist/storage/fs.d.ts +6 -0
- package/dist/types.d.ts +88 -0
- package/guides/demo-app.md +6 -6
- package/guides/running-the-service.md +53 -7
- package/guides/using-the-api.md +1 -1
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -13,20 +13,63 @@ See [guides/demo-app.md](./guides/demo-app.md) for the full walkthrough.
|
|
|
13
13
|
```bash
|
|
14
14
|
npm install
|
|
15
15
|
npm run build
|
|
16
|
-
npm start # listens on :
|
|
16
|
+
npm start # listens on :5030 (PORT / HOST / MOCKS_DIR env-overridable)
|
|
17
17
|
# dev (no build): npm run dev
|
|
18
18
|
|
|
19
19
|
# open the demo UI in your browser
|
|
20
|
-
open http://localhost:
|
|
20
|
+
open http://localhost:5030/demo
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Health:
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
curl localhost:
|
|
27
|
-
curl "localhost:
|
|
26
|
+
curl localhost:5030/health
|
|
27
|
+
curl "localhost:5030/api/explorer/health?includeInventory=1"
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
## Run from CLI or code
|
|
31
|
+
|
|
32
|
+
Installed CLI, using the default port `5030`:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx @x12i/static-memorix-explorer-api
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Or, after a global install:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
static-memorix-explorer-api --port 5030 --host 127.0.0.1
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Override the port from the shell:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
PORT=8080 npx @x12i/static-memorix-explorer-api
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Start it programmatically (explicit options take precedence over environment
|
|
51
|
+
defaults):
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
import { startServer } from "@x12i/static-memorix-explorer-api";
|
|
55
|
+
|
|
56
|
+
const app = await startServer({ port: 5030, host: "127.0.0.1" });
|
|
57
|
+
// await app.close();
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
For code-level tests without binding a port:
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
import { buildServer } from "@x12i/static-memorix-explorer-api";
|
|
64
|
+
|
|
65
|
+
const app = await buildServer();
|
|
66
|
+
const response = await app.inject({ method: "GET", url: "/health" });
|
|
67
|
+
await app.close();
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
See [guides/running-the-service.md](./guides/running-the-service.md) for all
|
|
71
|
+
CLI, environment, lifecycle, and programmatic examples.
|
|
72
|
+
|
|
30
73
|
## Architecture
|
|
31
74
|
|
|
32
75
|
```
|
|
@@ -139,7 +182,7 @@ Full implementation walkthrough: [guides/demo-app.md](./guides/demo-app.md).
|
|
|
139
182
|
|
|
140
183
|
| Var | Default | Notes |
|
|
141
184
|
|-----|---------|-------|
|
|
142
|
-
| `PORT` | `
|
|
185
|
+
| `PORT` | `5030` | Listen port |
|
|
143
186
|
| `HOST` | `0.0.0.0` | Listen host |
|
|
144
187
|
| `MOCKS_DIR` | `<project>/mocks` | Fixture root |
|
|
145
188
|
| `DISK_FLUSH_DEBOUNCE_MS` | `300` | Debounce before disk flush |
|
|
@@ -204,6 +247,21 @@ and `knowledgeId`.
|
|
|
204
247
|
|
|
205
248
|
## Release history
|
|
206
249
|
|
|
250
|
+
### v1.2.0
|
|
251
|
+
- Added native `--port` and `--host` CLI options.
|
|
252
|
+
- Added the canonical `static-memorix-explorer-api` executable while retaining
|
|
253
|
+
`mock-memorix-explorer-api` as a compatibility alias.
|
|
254
|
+
- Published TypeScript declarations for `buildServer()`, `startServer()`,
|
|
255
|
+
configuration exports, and server options.
|
|
256
|
+
- Corrected the supported Node.js runtime to Node 20 or newer for Fastify 5.
|
|
257
|
+
- Expanded CLI, programmatic, test, precedence, and lifecycle documentation.
|
|
258
|
+
|
|
259
|
+
### v1.1.1
|
|
260
|
+
- Changed the default listen port from `4300` to `5030`.
|
|
261
|
+
- Added safe programmatic APIs: `buildServer()` and `startServer(options)`.
|
|
262
|
+
- Importing the package no longer starts a listener as a side effect.
|
|
263
|
+
- Added CLI, environment override, programmatic startup, and injected-test examples.
|
|
264
|
+
|
|
207
265
|
### v1.1.0
|
|
208
266
|
- **Added**: `delete` operation to the write engine
|
|
209
267
|
(`POST /records/write` body `operation:"delete"`).
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
export declare const FLAGS: {
|
|
22
|
+
METADATA_WRITES: boolean;
|
|
23
|
+
PIPELINE_WRITES: boolean;
|
|
24
|
+
REGISTRY_WRITES: boolean;
|
|
25
|
+
};
|
|
26
|
+
export declare function metadataWritesEnabled(): boolean;
|
|
27
|
+
export declare function pipelineWritesEnabled(): boolean;
|
|
28
|
+
export declare function registryWritesEnabled(): boolean;
|
|
29
|
+
export declare function metadataPath(...segments: string[]): string;
|
|
30
|
+
export declare function dataPath(...segments: string[]): string;
|
package/dist/config.js
CHANGED
|
@@ -8,7 +8,7 @@ export const MOCKS_DIR = process.env.MOCKS_DIR
|
|
|
8
8
|
: path.resolve(ROOT_DIR, "..", "mocks");
|
|
9
9
|
export const METADATA_DIR = path.join(MOCKS_DIR, "metadata");
|
|
10
10
|
export const DATA_DIR = path.join(MOCKS_DIR, "data");
|
|
11
|
-
export const PORT = Number(process.env.PORT ||
|
|
11
|
+
export const PORT = Number(process.env.PORT || 5030);
|
|
12
12
|
export const HOST = process.env.HOST || "0.0.0.0";
|
|
13
13
|
export const DISK_FLUSH_DEBOUNCE_MS = Number(process.env.DISK_FLUSH_DEBOUNCE_MS || 300);
|
|
14
14
|
// ---------------------------------------------------------------------------
|
|
@@ -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
|
+
};
|
|
@@ -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: "analysis" | "decisions" | "events" | "memory";
|
|
15
|
+
collection: string;
|
|
16
|
+
suggestion: string;
|
|
17
|
+
}[];
|
|
18
|
+
};
|
|
@@ -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[];
|
|
@@ -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/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
|
@@ -1,10 +1,39 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import Fastify from "fastify";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
3
5
|
import { store } from "./storage/InMemoryStore.js";
|
|
4
6
|
import { registerRoutes } from "./routes/index.js";
|
|
5
7
|
import { PORT, HOST, MOCKS_DIR } from "./config.js";
|
|
6
|
-
|
|
7
|
-
const
|
|
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
|
+
}
|
|
34
|
+
/** Build a loaded Fastify instance without opening a network port. */
|
|
35
|
+
export async function buildServer(options = {}) {
|
|
36
|
+
const app = Fastify({ logger: options.logger ?? false });
|
|
8
37
|
// Global error handler matching predictable error shapes.
|
|
9
38
|
app.setErrorHandler((error, _request, reply) => {
|
|
10
39
|
const normalized = error instanceof Error
|
|
@@ -20,22 +49,38 @@ async function main() {
|
|
|
20
49
|
});
|
|
21
50
|
await store.load();
|
|
22
51
|
await registerRoutes(app);
|
|
23
|
-
|
|
24
|
-
|
|
52
|
+
return app;
|
|
53
|
+
}
|
|
54
|
+
/** Build and listen. Options override PORT/HOST environment configuration. */
|
|
55
|
+
export async function startServer(options = {}) {
|
|
56
|
+
const app = await buildServer(options);
|
|
57
|
+
const port = options.port ?? PORT;
|
|
58
|
+
const host = options.host ?? HOST;
|
|
59
|
+
await app.listen({ port, host });
|
|
60
|
+
const address = app.server.address();
|
|
61
|
+
const boundPort = typeof address === "object" && address ? address.port : port;
|
|
25
62
|
// eslint-disable-next-line no-console
|
|
26
|
-
console.log(`[mock-memorix-explorer-api] listening on ${
|
|
63
|
+
console.log(`[mock-memorix-explorer-api] listening on ${host}:${boundPort}`);
|
|
27
64
|
console.log(`[mock-memorix-explorer-api] mocks dir: ${MOCKS_DIR}`);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
65
|
+
if (options.installSignalHandlers ?? true) {
|
|
66
|
+
const shutdown = async () => {
|
|
67
|
+
console.log("\n[mock-memorix-explorer-api] flushing + shutting down");
|
|
68
|
+
await store.flushAll();
|
|
69
|
+
await app.close();
|
|
70
|
+
process.exit(0);
|
|
71
|
+
};
|
|
72
|
+
process.once("SIGINT", shutdown);
|
|
73
|
+
process.once("SIGTERM", shutdown);
|
|
74
|
+
}
|
|
75
|
+
return app;
|
|
76
|
+
}
|
|
77
|
+
const isCliEntry = process.argv[1]
|
|
78
|
+
? path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
79
|
+
: false;
|
|
80
|
+
if (isCliEntry) {
|
|
81
|
+
Promise.resolve().then(() => startServer(parseCliArgs(process.argv.slice(2)))).catch((err) => {
|
|
82
|
+
// eslint-disable-next-line no-console
|
|
83
|
+
console.error(err);
|
|
84
|
+
process.exit(1);
|
|
85
|
+
});
|
|
36
86
|
}
|
|
37
|
-
main().catch((err) => {
|
|
38
|
-
// eslint-disable-next-line no-console
|
|
39
|
-
console.error(err);
|
|
40
|
-
process.exit(1);
|
|
41
|
-
});
|
|
@@ -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;
|
|
@@ -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
|
+
}
|
package/guides/demo-app.md
CHANGED
|
@@ -17,7 +17,7 @@ calls.
|
|
|
17
17
|
```bash
|
|
18
18
|
npm run build
|
|
19
19
|
npm start
|
|
20
|
-
# open http://localhost:
|
|
20
|
+
# open http://localhost:5030/demo
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Every mutation you make in the UI is persisted back to `mocks/data/**/*.json`
|
|
@@ -454,23 +454,23 @@ A 30-second smoke test, runnable from the repo root:
|
|
|
454
454
|
npm run build && npm start &
|
|
455
455
|
|
|
456
456
|
# 2. Read
|
|
457
|
-
curl -s "localhost:
|
|
457
|
+
curl -s "localhost:5030/api/explorer/records/collection?entityName=users&limit=10"
|
|
458
458
|
|
|
459
459
|
# 3. Create
|
|
460
|
-
curl -s -X POST localhost:
|
|
460
|
+
curl -s -X POST localhost:5030/api/explorer/records/write \
|
|
461
461
|
-H "Content-Type: application/json" \
|
|
462
462
|
-d '{"writeDescriptorId":"product-task-write","operation":"add","input":{"recordId":"t-smoke-1","title":"Smoke","area":"product","status":"todo","priority":"normal"}}'
|
|
463
463
|
|
|
464
464
|
# 4. Update (partial)
|
|
465
|
-
curl -s -X POST localhost:
|
|
465
|
+
curl -s -X POST localhost:5030/api/explorer/records/write \
|
|
466
466
|
-H "Content-Type: application/json" \
|
|
467
467
|
-d '{"writeDescriptorId":"product-task-write","operation":"upsert","input":{"recordId":"t-smoke-1","status":"doing"}}'
|
|
468
468
|
|
|
469
469
|
# 5. Delete
|
|
470
|
-
curl -s -X DELETE "localhost:
|
|
470
|
+
curl -s -X DELETE "localhost:5030/api/explorer/records/item?entityName=product&recordId=t-smoke-1&writeDescriptorId=product-task-write"
|
|
471
471
|
|
|
472
472
|
# 6. Open the UI
|
|
473
|
-
open http://localhost:
|
|
473
|
+
open http://localhost:5030/demo
|
|
474
474
|
```
|
|
475
475
|
|
|
476
476
|
---
|
|
@@ -9,10 +9,12 @@ from in-memory state backed by JSON files on disk.
|
|
|
9
9
|
### From npm (global)
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
npm install -g x12i/static-memorix-explorer-api
|
|
12
|
+
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,12 +45,55 @@ 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
|
|
50
52
|
mocks directory defaults to a `mocks/` folder relative to the installed package.
|
|
51
53
|
|
|
54
|
+
### With `npx` (no global install)
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
npx @x12i/static-memorix-explorer-api
|
|
58
|
+
```
|
|
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
|
+
|
|
68
|
+
### From application code
|
|
69
|
+
|
|
70
|
+
Importing the package does not open a port. Call `startServer()` explicitly:
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
import { startServer } from "@x12i/static-memorix-explorer-api";
|
|
74
|
+
|
|
75
|
+
const app = await startServer({
|
|
76
|
+
port: 5030,
|
|
77
|
+
host: "127.0.0.1",
|
|
78
|
+
installSignalHandlers: false,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Later, for example during test teardown:
|
|
82
|
+
await app.close();
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
For tests that do not need a real TCP listener, build the Fastify app and use
|
|
86
|
+
its injection API:
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
import { buildServer } from "@x12i/static-memorix-explorer-api";
|
|
90
|
+
|
|
91
|
+
const app = await buildServer();
|
|
92
|
+
const response = await app.inject({ method: "GET", url: "/health" });
|
|
93
|
+
console.log(response.json()); // { ok: true }
|
|
94
|
+
await app.close();
|
|
95
|
+
```
|
|
96
|
+
|
|
52
97
|
## Configuration
|
|
53
98
|
|
|
54
99
|
All configuration is via environment variables. Defaults work out of the box.
|
|
@@ -57,7 +102,7 @@ All configuration is via environment variables. Defaults work out of the box.
|
|
|
57
102
|
|
|
58
103
|
| Variable | Default | Description |
|
|
59
104
|
| --------- | ----------- | ------------------------------ |
|
|
60
|
-
| `PORT` | `
|
|
105
|
+
| `PORT` | `5030` | HTTP listen port |
|
|
61
106
|
| `HOST` | `0.0.0.0` | HTTP listen host |
|
|
62
107
|
| `MOCKS_DIR` | `<project>/mocks` | Root directory for JSON fixtures |
|
|
63
108
|
|
|
@@ -79,8 +124,9 @@ The health endpoint returns:
|
|
|
79
124
|
|
|
80
125
|
### Feature flags
|
|
81
126
|
|
|
82
|
-
|
|
83
|
-
|
|
127
|
+
Metadata mutations on lists and narratives are gated behind feature flags.
|
|
128
|
+
Record mutations are governed by write descriptors and are available by
|
|
129
|
+
default.
|
|
84
130
|
|
|
85
131
|
| Variable | Controls |
|
|
86
132
|
| ------------------------------------------- | --------------------- |
|
|
@@ -117,10 +163,10 @@ mock-memorix-explorer-api
|
|
|
117
163
|
Once running, verify the server is up:
|
|
118
164
|
|
|
119
165
|
```bash
|
|
120
|
-
curl localhost:
|
|
166
|
+
curl localhost:5030/health
|
|
121
167
|
# {"ok":true}
|
|
122
168
|
|
|
123
|
-
curl "localhost:
|
|
169
|
+
curl "localhost:5030/api/explorer/health?includeInventory=1"
|
|
124
170
|
# {"ok":true,"mongoUriConfigured":true,"discoverySample":["assets","findings","memory"],...}
|
|
125
171
|
```
|
|
126
172
|
|
package/guides/using-the-api.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
The mock server exposes the full Memorix Explorer API under `/api/explorer/`.
|
|
4
4
|
All responses are JSON. Errors follow the shape `{ ok: false, error: "..." }`.
|
|
5
5
|
|
|
6
|
-
Base URL defaults to `http://localhost:
|
|
6
|
+
Base URL defaults to `http://localhost:5030`.
|
|
7
7
|
|
|
8
8
|
## Endpoints
|
|
9
9
|
|
package/package.json
CHANGED
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x12i/static-memorix-explorer-api",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.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
|
}
|