@x12i/static-memorix-explorer-api 1.3.0 → 1.3.2
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 +49 -3
- package/dist/config.d.ts +1 -0
- package/dist/config.js +18 -2
- package/dist/routes/index.js +2 -1
- package/dist/server.d.ts +3 -0
- package/dist/server.js +54 -10
- package/dist/storage/InMemoryStore.d.ts +2 -0
- package/dist/storage/InMemoryStore.js +37 -2
- package/dist/storage/fs.js +19 -4
- package/guides/demo-app.md +6 -3
- package/guides/managing-json-files.md +18 -0
- package/guides/running-the-service.md +37 -10
- package/guides/using-the-api.md +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -10,6 +10,27 @@ See [guides/demo-app.md](./guides/demo-app.md) for the full walkthrough.
|
|
|
10
10
|
|
|
11
11
|
## Quick start
|
|
12
12
|
|
|
13
|
+
Install once, then run one command:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install --global @x12i/static-memorix-explorer-api
|
|
17
|
+
memorix-explorer
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Then open **http://localhost:5030/demo**. The command prints the Demo, API,
|
|
21
|
+
Health, and JSON paths when it starts. No database or additional configuration
|
|
22
|
+
is required.
|
|
23
|
+
|
|
24
|
+
Useful options:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
memorix-explorer --port 8080
|
|
28
|
+
memorix-explorer --help
|
|
29
|
+
memorix-explorer --version
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Run from this repository
|
|
33
|
+
|
|
13
34
|
```bash
|
|
14
35
|
npm install
|
|
15
36
|
npm run build
|
|
@@ -38,7 +59,7 @@ npx @x12i/static-memorix-explorer-api
|
|
|
38
59
|
Or, after a global install:
|
|
39
60
|
|
|
40
61
|
```bash
|
|
41
|
-
|
|
62
|
+
memorix-explorer
|
|
42
63
|
```
|
|
43
64
|
|
|
44
65
|
Override the port from the shell:
|
|
@@ -121,7 +142,9 @@ The mock server accepts the following identity keys across fetch/write engines:
|
|
|
121
142
|
- `memoryId` — memory
|
|
122
143
|
|
|
123
144
|
Endpoints like `/records/full` and `/records/content` require exactly one identity key
|
|
124
|
-
per request. Memory collections are keyed by `memoryId
|
|
145
|
+
per request. Memory collections are keyed by `memoryId`; the unprefixed
|
|
146
|
+
`./mocks/data/memory/memory.json` is the seed and routed writes use the active
|
|
147
|
+
Catalox-prefixed filename.
|
|
125
148
|
|
|
126
149
|
## Snapshot alias translation
|
|
127
150
|
|
|
@@ -184,7 +207,7 @@ Full implementation walkthrough: [guides/demo-app.md](./guides/demo-app.md).
|
|
|
184
207
|
|-----|---------|-------|
|
|
185
208
|
| `PORT` | `5030` | Listen port |
|
|
186
209
|
| `HOST` | `0.0.0.0` | Listen host |
|
|
187
|
-
| `MOCKS_DIR` | `<
|
|
210
|
+
| `MOCKS_DIR` | bundled `<package>/mocks` | Fixture root |
|
|
188
211
|
| `DISK_FLUSH_DEBOUNCE_MS` | `300` | Debounce before disk flush |
|
|
189
212
|
| `MEMORIX_ORG_ID` | `memorix` | Organization prefix for DB routing |
|
|
190
213
|
| `MEMORIX_AGENT_ID` | `default-agent` | Agent prefix for catalox DB routing |
|
|
@@ -193,6 +216,11 @@ Full implementation walkthrough: [guides/demo-app.md](./guides/demo-app.md).
|
|
|
193
216
|
| `MEMORIX_EXPLORER_ENABLE_PIPELINE_WRITES` | `false` | Allow pipeline writes |
|
|
194
217
|
| `MEMORIX_EXPLORER_ENABLE_REGISTRY_WRITES` | `false` | Allow registry writes |
|
|
195
218
|
|
|
219
|
+
For mutation-capable installed deployments, set `MOCKS_DIR` to a writable,
|
|
220
|
+
persistent directory. Routing configuration is process-scoped and resolved on
|
|
221
|
+
first import; use one process per organization/agent context and one writer per
|
|
222
|
+
prefix.
|
|
223
|
+
|
|
196
224
|
## DB Router Simulation
|
|
197
225
|
|
|
198
226
|
The mock server does not connect to MongoDB, but it **tricks the frontend** into
|
|
@@ -253,6 +281,8 @@ The mock's querying and writing engines now support `target=memory` and
|
|
|
253
281
|
and `knowledgeId`.
|
|
254
282
|
|
|
255
283
|
- Folder structure: `./mocks/data/memory/memory.json` (seeded with example memories).
|
|
284
|
+
- Runtime memory writes persist to the active
|
|
285
|
+
`<agent>-memorix-catalox--memory.json` route.
|
|
256
286
|
- Inventory engine accepts `target=memory` and includes memory collections in
|
|
257
287
|
the simulated `inventorySummary`.
|
|
258
288
|
- Records engine accepts `target=memory` and supports `memoryId` for fetching.
|
|
@@ -269,6 +299,22 @@ and `knowledgeId`.
|
|
|
269
299
|
|
|
270
300
|
## Release history
|
|
271
301
|
|
|
302
|
+
### v1.3.2
|
|
303
|
+
- Added the short `memorix-explorer` CLI command.
|
|
304
|
+
- Added `--help`/`-h` and `--version`/`-v` commands.
|
|
305
|
+
- Startup now prints clickable Demo, API, Health, and JSON locations.
|
|
306
|
+
- Reworked Quick Start documentation around install-once, run-one-command use.
|
|
307
|
+
|
|
308
|
+
### v1.3.1
|
|
309
|
+
- Rejected unsafe routing IDs, object types, and metadata keys before path use.
|
|
310
|
+
- Changed malformed and empty JSON handling from silent fallback to explicit
|
|
311
|
+
startup failure, preventing accidental data loss.
|
|
312
|
+
- Made JSON persistence atomic through same-directory temporary-file renames.
|
|
313
|
+
- Made list deletion restart-safe with routed tombstones.
|
|
314
|
+
- Fixed installed npm CLI execution through symlink-aware entry detection.
|
|
315
|
+
- Added packed-install, restart, traversal, corruption, and atomic-write checks.
|
|
316
|
+
- Documented writable `MOCKS_DIR`, process isolation, and single-writer limits.
|
|
317
|
+
|
|
272
318
|
### v1.3.0
|
|
273
319
|
- Added database-routing parity through deterministic JSON filename prefixes.
|
|
274
320
|
- Added separate entity, event, and Catalox/metadata prefix resolution.
|
package/dist/config.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export declare const DATA_DIR: string;
|
|
|
5
5
|
export declare const PORT: number;
|
|
6
6
|
export declare const HOST: string;
|
|
7
7
|
export declare const DISK_FLUSH_DEBOUNCE_MS: number;
|
|
8
|
+
export declare function validateRouteId(value: string, variableName?: string): string;
|
|
8
9
|
export declare const MEMORIX_ORG_ID: string;
|
|
9
10
|
export declare const MEMORIX_AGENT_ID: string;
|
|
10
11
|
export declare const MEMORIX_DEPLOYMENT_PROFILE: string;
|
package/dist/config.js
CHANGED
|
@@ -20,13 +20,23 @@ function resolveOrgId() {
|
|
|
20
20
|
if ((process.env.MEMORIX_DEPLOYMENT_PROFILE || "").toLowerCase() === "ebook") {
|
|
21
21
|
return "ebooks";
|
|
22
22
|
}
|
|
23
|
-
return process.env.MEMORIX_ORG_ID || "memorix";
|
|
23
|
+
return validateRouteId(process.env.MEMORIX_ORG_ID || "memorix", "MEMORIX_ORG_ID");
|
|
24
24
|
}
|
|
25
25
|
function resolveAgentId() {
|
|
26
26
|
if ((process.env.MEMORIX_DEPLOYMENT_PROFILE || "").toLowerCase() === "ebook") {
|
|
27
27
|
return "ebooks";
|
|
28
28
|
}
|
|
29
|
-
return process.env.MEMORIX_AGENT_ID || "default-agent";
|
|
29
|
+
return validateRouteId(process.env.MEMORIX_AGENT_ID || "default-agent", "MEMORIX_AGENT_ID");
|
|
30
|
+
}
|
|
31
|
+
export function validateRouteId(value, variableName = "route ID") {
|
|
32
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value) ||
|
|
33
|
+
value.includes("--") ||
|
|
34
|
+
value === "." ||
|
|
35
|
+
value === "..") {
|
|
36
|
+
throw Object.assign(new Error(`${variableName} must contain only letters, numbers, dot, underscore, or hyphen; ` +
|
|
37
|
+
`it must not contain path separators or the reserved "--" delimiter`), { statusCode: 400 });
|
|
38
|
+
}
|
|
39
|
+
return value;
|
|
30
40
|
}
|
|
31
41
|
export const MEMORIX_ORG_ID = resolveOrgId();
|
|
32
42
|
export const MEMORIX_AGENT_ID = resolveAgentId();
|
|
@@ -62,6 +72,12 @@ export function buildMetadataFilePrefix() {
|
|
|
62
72
|
return buildCataloxDbString();
|
|
63
73
|
}
|
|
64
74
|
export function prefixedJsonFilename(prefix, basename) {
|
|
75
|
+
validateRouteId(prefix, "file routing prefix");
|
|
76
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(basename) || basename.includes("--")) {
|
|
77
|
+
throw Object.assign(new Error(`Unsafe JSON fixture basename: ${basename}`), {
|
|
78
|
+
statusCode: 400,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
65
81
|
return `${prefix}--${basename}.json`;
|
|
66
82
|
}
|
|
67
83
|
// ---------------------------------------------------------------------------
|
package/dist/routes/index.js
CHANGED
|
@@ -228,7 +228,8 @@ export async function registerRoutes(app) {
|
|
|
228
228
|
return { ok: false, error: "Metadata writes are disabled" };
|
|
229
229
|
}
|
|
230
230
|
const { listId } = request.params;
|
|
231
|
-
store.
|
|
231
|
+
store.deleteMeta(`lists/${listId}`);
|
|
232
|
+
await store.flushAll();
|
|
232
233
|
return { ok: true, deleted: listId };
|
|
233
234
|
});
|
|
234
235
|
// ---------- F. Narratives Engine ----------
|
package/dist/server.d.ts
CHANGED
|
@@ -5,7 +5,10 @@ export interface StartServerOptions {
|
|
|
5
5
|
logger?: boolean;
|
|
6
6
|
installSignalHandlers?: boolean;
|
|
7
7
|
}
|
|
8
|
+
export declare const CLI_HELP = "Memorix Explorer static server\n\nUsage:\n memorix-explorer [options]\n\nOptions:\n --port <number> Port to listen on (default: 5030)\n --host <address> Host to bind (default: 0.0.0.0)\n -h, --help Show this help\n -v, --version Show the installed version\n\nEnvironment:\n MOCKS_DIR Writable JSON fixture directory\n MEMORIX_ORG_ID Organization routing ID\n MEMORIX_AGENT_ID Agent/Catalox routing ID\n\nExample:\n memorix-explorer --port 5030\n";
|
|
9
|
+
export declare function getPackageVersion(): string;
|
|
8
10
|
export declare function parseCliArgs(args: string[]): StartServerOptions;
|
|
11
|
+
export declare function isCliEntrypoint(argvPath: string | undefined, moduleUrl: string): boolean;
|
|
9
12
|
/** Build a loaded Fastify instance without opening a network port. */
|
|
10
13
|
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
14
|
/** Build and listen. Options override PORT/HOST environment configuration. */
|
package/dist/server.js
CHANGED
|
@@ -1,10 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import Fastify from "fastify";
|
|
3
|
+
import fs from "node:fs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { store } from "./storage/InMemoryStore.js";
|
|
6
7
|
import { registerRoutes } from "./routes/index.js";
|
|
7
8
|
import { PORT, HOST, MOCKS_DIR } from "./config.js";
|
|
9
|
+
export const CLI_HELP = `Memorix Explorer static server
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
memorix-explorer [options]
|
|
13
|
+
|
|
14
|
+
Options:
|
|
15
|
+
--port <number> Port to listen on (default: 5030)
|
|
16
|
+
--host <address> Host to bind (default: 0.0.0.0)
|
|
17
|
+
-h, --help Show this help
|
|
18
|
+
-v, --version Show the installed version
|
|
19
|
+
|
|
20
|
+
Environment:
|
|
21
|
+
MOCKS_DIR Writable JSON fixture directory
|
|
22
|
+
MEMORIX_ORG_ID Organization routing ID
|
|
23
|
+
MEMORIX_AGENT_ID Agent/Catalox routing ID
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
memorix-explorer --port 5030
|
|
27
|
+
`;
|
|
28
|
+
export function getPackageVersion() {
|
|
29
|
+
const packageFile = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../package.json");
|
|
30
|
+
return JSON.parse(fs.readFileSync(packageFile, "utf8")).version;
|
|
31
|
+
}
|
|
8
32
|
export function parseCliArgs(args) {
|
|
9
33
|
const options = {};
|
|
10
34
|
for (let index = 0; index < args.length; index++) {
|
|
@@ -31,6 +55,16 @@ export function parseCliArgs(args) {
|
|
|
31
55
|
}
|
|
32
56
|
return options;
|
|
33
57
|
}
|
|
58
|
+
export function isCliEntrypoint(argvPath, moduleUrl) {
|
|
59
|
+
if (!argvPath)
|
|
60
|
+
return false;
|
|
61
|
+
try {
|
|
62
|
+
return fs.realpathSync(path.resolve(argvPath)) === fileURLToPath(moduleUrl);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
34
68
|
/** Build a loaded Fastify instance without opening a network port. */
|
|
35
69
|
export async function buildServer(options = {}) {
|
|
36
70
|
const app = Fastify({ logger: options.logger ?? false });
|
|
@@ -59,9 +93,13 @@ export async function startServer(options = {}) {
|
|
|
59
93
|
await app.listen({ port, host });
|
|
60
94
|
const address = app.server.address();
|
|
61
95
|
const boundPort = typeof address === "object" && address ? address.port : port;
|
|
96
|
+
const displayHost = host === "0.0.0.0" ? "localhost" : host;
|
|
62
97
|
// eslint-disable-next-line no-console
|
|
63
|
-
console.log(
|
|
64
|
-
console.log(`
|
|
98
|
+
console.log(`\nMemorix Explorer is ready`);
|
|
99
|
+
console.log(` Demo: http://${displayHost}:${boundPort}/demo`);
|
|
100
|
+
console.log(` API: http://${displayHost}:${boundPort}/api/explorer`);
|
|
101
|
+
console.log(` Health: http://${displayHost}:${boundPort}/health`);
|
|
102
|
+
console.log(` JSON: ${MOCKS_DIR}\n`);
|
|
65
103
|
if (options.installSignalHandlers ?? true) {
|
|
66
104
|
const shutdown = async () => {
|
|
67
105
|
console.log("\n[mock-memorix-explorer-api] flushing + shutting down");
|
|
@@ -74,13 +112,19 @@ export async function startServer(options = {}) {
|
|
|
74
112
|
}
|
|
75
113
|
return app;
|
|
76
114
|
}
|
|
77
|
-
const isCliEntry = process.argv[1]
|
|
78
|
-
? path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)
|
|
79
|
-
: false;
|
|
115
|
+
const isCliEntry = isCliEntrypoint(process.argv[1], import.meta.url);
|
|
80
116
|
if (isCliEntry) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
console.
|
|
84
|
-
|
|
85
|
-
|
|
117
|
+
const args = process.argv.slice(2);
|
|
118
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
119
|
+
console.log(CLI_HELP);
|
|
120
|
+
}
|
|
121
|
+
else if (args.includes("--version") || args.includes("-v")) {
|
|
122
|
+
console.log(getPackageVersion());
|
|
123
|
+
}
|
|
124
|
+
else
|
|
125
|
+
Promise.resolve().then(() => startServer(parseCliArgs(args))).catch((err) => {
|
|
126
|
+
// eslint-disable-next-line no-console
|
|
127
|
+
console.error(err);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
});
|
|
86
130
|
}
|
|
@@ -12,6 +12,8 @@ export declare class InMemoryStore {
|
|
|
12
12
|
private loadDataCollections;
|
|
13
13
|
getMeta<T = any>(key: string): T | undefined;
|
|
14
14
|
setMeta(key: string, data: any, persist?: boolean): void;
|
|
15
|
+
/** Persist a routed tombstone so an unprefixed seed does not reappear. */
|
|
16
|
+
deleteMeta(key: string): void;
|
|
15
17
|
getMetaKeys(): string[];
|
|
16
18
|
getCollection(objectType: string, ct: ContentType): any[];
|
|
17
19
|
setCollection(objectType: string, ct: ContentType, data: any[]): void;
|
|
@@ -1,11 +1,28 @@
|
|
|
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, buildDataFilePrefix, buildMetadataFilePrefix, prefixedJsonFilename, } from "../config.js";
|
|
4
|
+
import { MOCKS_DIR, METADATA_DIR, DATA_DIR, buildDataFilePrefix, buildMetadataFilePrefix, prefixedJsonFilename, validateRouteId, } 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";
|
|
8
8
|
const DIRTY = Symbol("dirty");
|
|
9
|
+
function routedMetadataPath(key) {
|
|
10
|
+
const parts = key.split("/");
|
|
11
|
+
if (parts.length === 1 && parts[0] === "agents") {
|
|
12
|
+
return path.join(METADATA_DIR, prefixedJsonFilename(buildMetadataFilePrefix(), "agents"));
|
|
13
|
+
}
|
|
14
|
+
if (parts.length !== 2) {
|
|
15
|
+
throw Object.assign(new Error(`Unsafe metadata key: ${key}`), { statusCode: 400 });
|
|
16
|
+
}
|
|
17
|
+
const [section, name] = parts;
|
|
18
|
+
if (!["object-types", "lists", "write-descriptors", "narratives"].includes(section)) {
|
|
19
|
+
throw Object.assign(new Error(`Unsupported metadata section: ${section}`), {
|
|
20
|
+
statusCode: 400,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
validateRouteId(name, "metadata fixture name");
|
|
24
|
+
return path.join(METADATA_DIR, section, prefixedJsonFilename(buildMetadataFilePrefix(), name));
|
|
25
|
+
}
|
|
9
26
|
export class InMemoryStore {
|
|
10
27
|
collections = new Map(); // key: `${objectType}/${contentType}`
|
|
11
28
|
metadata = new Map();
|
|
@@ -108,7 +125,7 @@ export class InMemoryStore {
|
|
|
108
125
|
}
|
|
109
126
|
else {
|
|
110
127
|
this.metadata.set(key, {
|
|
111
|
-
path:
|
|
128
|
+
path: routedMetadataPath(key),
|
|
112
129
|
data,
|
|
113
130
|
[DIRTY]: true,
|
|
114
131
|
});
|
|
@@ -116,11 +133,28 @@ export class InMemoryStore {
|
|
|
116
133
|
if (persist)
|
|
117
134
|
this.scheduleMetaFlush(key);
|
|
118
135
|
}
|
|
136
|
+
/** Persist a routed tombstone so an unprefixed seed does not reappear. */
|
|
137
|
+
deleteMeta(key) {
|
|
138
|
+
const existing = this.metadata.get(key);
|
|
139
|
+
if (existing) {
|
|
140
|
+
existing.data = null;
|
|
141
|
+
existing[DIRTY] = true;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
this.metadata.set(key, {
|
|
145
|
+
path: routedMetadataPath(key),
|
|
146
|
+
data: null,
|
|
147
|
+
[DIRTY]: true,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
this.scheduleMetaFlush(key);
|
|
151
|
+
}
|
|
119
152
|
getMetaKeys() {
|
|
120
153
|
return [...this.metadata.keys()];
|
|
121
154
|
}
|
|
122
155
|
// ---- Collection access ----
|
|
123
156
|
getCollection(objectType, ct) {
|
|
157
|
+
validateRouteId(objectType, "object type");
|
|
124
158
|
const key = `${objectType}/${ct}`;
|
|
125
159
|
const c = this.collections.get(key);
|
|
126
160
|
if (!c) {
|
|
@@ -134,6 +168,7 @@ export class InMemoryStore {
|
|
|
134
168
|
return c.data;
|
|
135
169
|
}
|
|
136
170
|
setCollection(objectType, ct, data) {
|
|
171
|
+
validateRouteId(objectType, "object type");
|
|
137
172
|
const key = `${objectType}/${ct}`;
|
|
138
173
|
const c = this.collections.get(key);
|
|
139
174
|
if (c) {
|
package/dist/storage/fs.js
CHANGED
|
@@ -15,16 +15,31 @@ export function readJson(p, fallback) {
|
|
|
15
15
|
try {
|
|
16
16
|
const raw = fs.readFileSync(p, "utf8");
|
|
17
17
|
if (!raw.trim())
|
|
18
|
-
|
|
18
|
+
throw new Error("file is empty");
|
|
19
19
|
return JSON.parse(raw);
|
|
20
20
|
}
|
|
21
|
-
catch {
|
|
22
|
-
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (error.code === "ENOENT")
|
|
23
|
+
return fallback;
|
|
24
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
25
|
+
throw new Error(`Failed to read JSON fixture ${p}: ${message}`);
|
|
23
26
|
}
|
|
24
27
|
}
|
|
25
28
|
export function writeJson(p, data) {
|
|
26
29
|
ensureDir(path.dirname(p));
|
|
27
|
-
|
|
30
|
+
const serialized = JSON.stringify(data, null, 2);
|
|
31
|
+
if (serialized === undefined) {
|
|
32
|
+
throw new Error(`Cannot serialize undefined JSON fixture: ${p}`);
|
|
33
|
+
}
|
|
34
|
+
const temp = path.join(path.dirname(p), `.${path.basename(p)}.${process.pid}.tmp`);
|
|
35
|
+
try {
|
|
36
|
+
fs.writeFileSync(temp, serialized + "\n", "utf8");
|
|
37
|
+
fs.renameSync(temp, p);
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
if (fs.existsSync(temp))
|
|
41
|
+
fs.unlinkSync(temp);
|
|
42
|
+
}
|
|
28
43
|
}
|
|
29
44
|
export function listJsonFiles(dir) {
|
|
30
45
|
if (!isDir(dir))
|
package/guides/demo-app.md
CHANGED
|
@@ -324,7 +324,9 @@ with fields for title, area, status, priority, and assignee. On submit:
|
|
|
324
324
|
descriptor for the chosen area (`product-task-write`, etc.).
|
|
325
325
|
3. The server validates the payload against the descriptor's JSON schema.
|
|
326
326
|
4. On success, the record is appended to the in-memory collection and the
|
|
327
|
-
|
|
327
|
+
flush writes it to
|
|
328
|
+
`mocks/data/<area>/<org>-memorix-entities--snapshots.json`. If that routed
|
|
329
|
+
file does not exist yet, the unprefixed `snapshots.json` is used as seed data.
|
|
328
330
|
5. The UI unshifts the returned record into `state.tasks`, selects it, and
|
|
329
331
|
re-renders.
|
|
330
332
|
|
|
@@ -385,8 +387,9 @@ scale. For higher volume you'd model comments as `events` records keyed by
|
|
|
385
387
|
## Why each area is a separate collection
|
|
386
388
|
|
|
387
389
|
We could have put all tasks in one `tasks` collection with an `area` field.
|
|
388
|
-
The demo deliberately splits
|
|
389
|
-
`marketing/snapshots.json`, and `admin/snapshots.json
|
|
390
|
+
The demo deliberately splits the seed fixtures across `product/snapshots.json`,
|
|
391
|
+
`marketing/snapshots.json`, and `admin/snapshots.json`; runtime mutations go to
|
|
392
|
+
their routed, prefixed counterparts. This demonstrates:
|
|
390
393
|
|
|
391
394
|
1. **Multi-entity applications** — typical real apps have more than one object
|
|
392
395
|
type. Showing 3 object types is more representative than 1.
|
|
@@ -29,6 +29,10 @@ the routing matrix is:
|
|
|
29
29
|
4. Unprefixed seed files are never overwritten while routed mode is active.
|
|
30
30
|
5. Files belonging to a different prefix are ignored by the active process.
|
|
31
31
|
|
|
32
|
+
Deleting a metadata document writes JSON `null` as a routed tombstone. This is
|
|
33
|
+
intentional: removing only the routed file would cause the unprefixed seed to
|
|
34
|
+
reappear on the next restart.
|
|
35
|
+
|
|
32
36
|
This makes onboarding migration-safe: existing files such as
|
|
33
37
|
`product/snapshots.json` continue to seed a new tenant, while the first write
|
|
34
38
|
creates `product/neo-memorix-entities--snapshots.json`. Starting with another
|
|
@@ -37,6 +41,20 @@ organization ID selects a different file and therefore different state.
|
|
|
37
41
|
The delimiter `--` is reserved. Do not use it inside ordinary, unprefixed
|
|
38
42
|
fixture basenames.
|
|
39
43
|
|
|
44
|
+
Routing IDs are validated before any files are opened. Organization IDs,
|
|
45
|
+
agent IDs, object types, and metadata names may contain letters, numbers,
|
|
46
|
+
dot, underscore, and hyphen, but cannot contain path separators, `..`, or the
|
|
47
|
+
reserved `--` delimiter.
|
|
48
|
+
|
|
49
|
+
Malformed or empty JSON stops startup with the affected path in the error;
|
|
50
|
+
it is never silently converted to an empty collection. Writes use a temporary
|
|
51
|
+
file and same-directory rename so readers never observe a partially written
|
|
52
|
+
JSON document.
|
|
53
|
+
|
|
54
|
+
Routing is process-scoped. Run one server process per organization/agent route,
|
|
55
|
+
and use only one writer process for a given prefix. Two processes writing the
|
|
56
|
+
same routed file concurrently use last-writer-wins semantics.
|
|
57
|
+
|
|
40
58
|
### Inspect the active route
|
|
41
59
|
|
|
42
60
|
```bash
|
|
@@ -12,9 +12,9 @@ from in-memory state backed by JSON files on disk.
|
|
|
12
12
|
npm install -g @x12i/static-memorix-explorer-api
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
This makes the `
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
This makes the short `memorix-explorer` command available globally.
|
|
16
|
+
`static-memorix-explorer-api` and `mock-memorix-explorer-api` remain available
|
|
17
|
+
as compatibility aliases.
|
|
18
18
|
|
|
19
19
|
### From source (git clone)
|
|
20
20
|
|
|
@@ -45,11 +45,19 @@ iteration, but not suitable for distribution.
|
|
|
45
45
|
### As a global CLI
|
|
46
46
|
|
|
47
47
|
```bash
|
|
48
|
-
|
|
48
|
+
memorix-explorer
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
After `npm install -g`, the CLI starts the server with default settings. The
|
|
52
|
-
mocks directory defaults to
|
|
52
|
+
mocks directory defaults to the bundled fixtures, and the CLI prints the URLs
|
|
53
|
+
to open:
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
Memorix Explorer is ready
|
|
57
|
+
Demo: http://localhost:5030/demo
|
|
58
|
+
API: http://localhost:5030/api/explorer
|
|
59
|
+
Health: http://localhost:5030/health
|
|
60
|
+
```
|
|
53
61
|
|
|
54
62
|
### With `npx` (no global install)
|
|
55
63
|
|
|
@@ -61,8 +69,15 @@ Choose a port with a CLI option or environment variable. CLI options take
|
|
|
61
69
|
precedence:
|
|
62
70
|
|
|
63
71
|
```bash
|
|
64
|
-
|
|
65
|
-
PORT=5030 HOST=127.0.0.1
|
|
72
|
+
memorix-explorer --port 5030 --host 127.0.0.1
|
|
73
|
+
PORT=5030 HOST=127.0.0.1 memorix-explorer
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Built-in help and version output do not start the server:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
memorix-explorer --help
|
|
80
|
+
memorix-explorer --version
|
|
66
81
|
```
|
|
67
82
|
|
|
68
83
|
### From application code
|
|
@@ -104,7 +119,19 @@ All configuration is via environment variables. Defaults work out of the box.
|
|
|
104
119
|
| --------- | ----------- | ------------------------------ |
|
|
105
120
|
| `PORT` | `5030` | HTTP listen port |
|
|
106
121
|
| `HOST` | `0.0.0.0` | HTTP listen host |
|
|
107
|
-
| `MOCKS_DIR` | `<
|
|
122
|
+
| `MOCKS_DIR` | bundled `<package>/mocks` | Root directory for JSON fixtures |
|
|
123
|
+
|
|
124
|
+
`MOCKS_DIR` must be writable if you use mutation endpoints. For installed or
|
|
125
|
+
containerized deployments, point it at a persistent application-owned volume
|
|
126
|
+
rather than relying on the package's bundled seed directory:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
MOCKS_DIR=/var/lib/my-app/memorix-mocks memorix-explorer --port 5030
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Configuration is resolved when the package is imported. In programmatic usage,
|
|
133
|
+
set `process.env.MOCKS_DIR`, `MEMORIX_ORG_ID`, and `MEMORIX_AGENT_ID` before the
|
|
134
|
+
first dynamic import, and run separate processes for separate routing contexts.
|
|
108
135
|
|
|
109
136
|
### DB Router Simulation
|
|
110
137
|
|
|
@@ -150,7 +177,7 @@ default.
|
|
|
150
177
|
Set to `true` or `1` to enable:
|
|
151
178
|
|
|
152
179
|
```bash
|
|
153
|
-
MEMORIX_EXPLORER_ENABLE_METADATA_WRITES=true
|
|
180
|
+
MEMORIX_EXPLORER_ENABLE_METADATA_WRITES=true memorix-explorer
|
|
154
181
|
```
|
|
155
182
|
|
|
156
183
|
### Disk flush
|
|
@@ -168,7 +195,7 @@ MOCKS_DIR=/path/to/my/fixtures \
|
|
|
168
195
|
MEMORIX_ORG_ID=neo \
|
|
169
196
|
MEMORIX_AGENT_ID=neo-agent \
|
|
170
197
|
MEMORIX_EXPLORER_ENABLE_METADATA_WRITES=true \
|
|
171
|
-
|
|
198
|
+
memorix-explorer
|
|
172
199
|
```
|
|
173
200
|
|
|
174
201
|
## Health checks
|
package/guides/using-the-api.md
CHANGED
|
@@ -261,5 +261,5 @@ when the corresponding feature flag is not enabled. Set the environment
|
|
|
261
261
|
variable to `true` or `1` to allow writes:
|
|
262
262
|
|
|
263
263
|
```bash
|
|
264
|
-
MEMORIX_EXPLORER_ENABLE_METADATA_WRITES=true
|
|
264
|
+
MEMORIX_EXPLORER_ENABLE_METADATA_WRITES=true memorix-explorer
|
|
265
265
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x12i/static-memorix-explorer-api",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.2",
|
|
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",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"bin": {
|
|
18
|
+
"memorix-explorer": "dist/server.js",
|
|
18
19
|
"static-memorix-explorer-api": "dist/server.js",
|
|
19
20
|
"mock-memorix-explorer-api": "dist/server.js"
|
|
20
21
|
},
|