@x12i/static-memorix 2.0.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/LICENSE +21 -0
- package/README.md +431 -0
- package/dist/cli.d.ts +17 -0
- package/dist/cli.js +278 -0
- package/dist/config.d.ts +39 -0
- package/dist/config.js +109 -0
- package/dist/engine/aliases.d.ts +32 -0
- package/dist/engine/aliases.js +74 -0
- package/dist/engine/associations.d.ts +26 -0
- package/dist/engine/associations.js +60 -0
- package/dist/engine/identity.d.ts +22 -0
- package/dist/engine/identity.js +50 -0
- package/dist/engine/inventory.d.ts +29 -0
- package/dist/engine/inventory.js +133 -0
- package/dist/engine/lists.d.ts +18 -0
- package/dist/engine/lists.js +62 -0
- package/dist/engine/narratives.d.ts +16 -0
- package/dist/engine/narratives.js +67 -0
- package/dist/engine/objectTypes.d.ts +11 -0
- package/dist/engine/objectTypes.js +71 -0
- package/dist/engine/query.d.ts +38 -0
- package/dist/engine/query.js +191 -0
- package/dist/engine/records.d.ts +23 -0
- package/dist/engine/records.js +152 -0
- package/dist/engine/snapshots.d.ts +17 -0
- package/dist/engine/snapshots.js +110 -0
- package/dist/engine/write.d.ts +30 -0
- package/dist/engine/write.js +163 -0
- package/dist/routes/helpers.d.ts +4 -0
- package/dist/routes/helpers.js +29 -0
- package/dist/routes/index.d.ts +2 -0
- package/dist/routes/index.js +402 -0
- package/dist/server.d.ts +15 -0
- package/dist/server.js +130 -0
- package/dist/storage/InMemoryStore.d.ts +31 -0
- package/dist/storage/InMemoryStore.js +242 -0
- package/dist/storage/fs.d.ts +6 -0
- package/dist/storage/fs.js +54 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.js +37 -0
- package/guides/demo-app.md +527 -0
- package/guides/managing-json-files.md +384 -0
- package/guides/running-the-service.md +269 -0
- package/guides/using-the-api.md +265 -0
- package/mocks/data/admin/snapshots.json +52 -0
- package/mocks/data/assets/analysis.json +14 -0
- package/mocks/data/assets/decisions.json +14 -0
- package/mocks/data/assets/events.json +4 -0
- package/mocks/data/assets/snapshots.json +49 -0
- package/mocks/data/findings/analysis.json +4 -0
- package/mocks/data/findings/decisions.json +4 -0
- package/mocks/data/findings/events.json +3 -0
- package/mocks/data/findings/snapshots.json +21 -0
- package/mocks/data/marketing/snapshots.json +75 -0
- package/mocks/data/memory/memory.json +31 -0
- package/mocks/data/product/snapshots.json +84 -0
- package/mocks/data/system/inventory.json +8 -0
- package/mocks/data/users/snapshots.json +5 -0
- package/mocks/demo.html +828 -0
- package/mocks/metadata/agents.json +7 -0
- package/mocks/metadata/lists/assets-default.json +22 -0
- package/mocks/metadata/lists/backlog.json +10 -0
- package/mocks/metadata/lists/findings-default.json +9 -0
- package/mocks/metadata/lists/my-plate.json +10 -0
- package/mocks/metadata/lists/this-week.json +10 -0
- package/mocks/metadata/narratives/admin.json +7 -0
- package/mocks/metadata/narratives/assets.json +17 -0
- package/mocks/metadata/narratives/findings.json +7 -0
- package/mocks/metadata/narratives/marketing.json +17 -0
- package/mocks/metadata/narratives/product.json +12 -0
- package/mocks/metadata/object-types/admin.json +6 -0
- package/mocks/metadata/object-types/assets.json +9 -0
- package/mocks/metadata/object-types/findings.json +8 -0
- package/mocks/metadata/object-types/marketing.json +6 -0
- package/mocks/metadata/object-types/memory.json +6 -0
- package/mocks/metadata/object-types/product.json +6 -0
- package/mocks/metadata/object-types/users.json +13 -0
- package/mocks/metadata/write-descriptors/admin-task-write.json +20 -0
- package/mocks/metadata/write-descriptors/assets-analysis-write.json +14 -0
- package/mocks/metadata/write-descriptors/marketing-task-write.json +20 -0
- package/mocks/metadata/write-descriptors/memory-write.json +14 -0
- package/mocks/metadata/write-descriptors/product-task-write.json +20 -0
- package/package.json +67 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
export const CLI_HELP = `Memorix Explorer static server
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
static-memorix create-demo [--dir <path>]
|
|
12
|
+
static-memorix stop [--port <number>]
|
|
13
|
+
static-memorix [options]
|
|
14
|
+
|
|
15
|
+
Commands:
|
|
16
|
+
create-demo Create a writable demo folder (default: ./memorix-demo)
|
|
17
|
+
stop Stop a managed server (default port: 5030)
|
|
18
|
+
|
|
19
|
+
Server options:
|
|
20
|
+
--port <number> Port to listen on (default: 5030)
|
|
21
|
+
--host <address> Host to bind (default: 0.0.0.0)
|
|
22
|
+
--mocks-dir <path> JSON fixture directory
|
|
23
|
+
-h, --help Show this help
|
|
24
|
+
-v, --version Show the installed version
|
|
25
|
+
|
|
26
|
+
Quick start:
|
|
27
|
+
static-memorix create-demo
|
|
28
|
+
static-memorix --mocks-dir ./memorix-demo
|
|
29
|
+
|
|
30
|
+
("memorix-explorer" remains available as a short alias for this command.)
|
|
31
|
+
`;
|
|
32
|
+
function packageRoot() {
|
|
33
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
34
|
+
}
|
|
35
|
+
export function getPackageVersion() {
|
|
36
|
+
return JSON.parse(fs.readFileSync(path.join(packageRoot(), "package.json"), "utf8")).version;
|
|
37
|
+
}
|
|
38
|
+
export function parseServerArgs(args) {
|
|
39
|
+
const options = {};
|
|
40
|
+
for (let index = 0; index < args.length; index++) {
|
|
41
|
+
const arg = args[index];
|
|
42
|
+
if (arg === "--port") {
|
|
43
|
+
const raw = args[++index];
|
|
44
|
+
const port = Number(raw);
|
|
45
|
+
if (!raw || !Number.isInteger(port) || port < 0 || port > 65535) {
|
|
46
|
+
throw new Error("--port must be an integer from 0 to 65535");
|
|
47
|
+
}
|
|
48
|
+
options.port = port;
|
|
49
|
+
}
|
|
50
|
+
else if (arg === "--host") {
|
|
51
|
+
const host = args[++index];
|
|
52
|
+
if (!host)
|
|
53
|
+
throw new Error("--host requires a value");
|
|
54
|
+
options.host = host;
|
|
55
|
+
}
|
|
56
|
+
else if (arg === "--mocks-dir") {
|
|
57
|
+
const mocksDir = args[++index];
|
|
58
|
+
if (!mocksDir)
|
|
59
|
+
throw new Error("--mocks-dir requires a path");
|
|
60
|
+
options.mocksDir = path.resolve(mocksDir);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
throw new Error(`Unknown CLI argument: ${arg}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return options;
|
|
67
|
+
}
|
|
68
|
+
export function parseCreateDemoArgs(args) {
|
|
69
|
+
let targetDir = path.resolve("memorix-demo");
|
|
70
|
+
for (let index = 0; index < args.length; index++) {
|
|
71
|
+
const arg = args[index];
|
|
72
|
+
if (arg === "--dir") {
|
|
73
|
+
const value = args[++index];
|
|
74
|
+
if (!value)
|
|
75
|
+
throw new Error("--dir requires a path");
|
|
76
|
+
targetDir = path.resolve(value);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
throw new Error(`Unknown create-demo argument: ${arg}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { targetDir };
|
|
83
|
+
}
|
|
84
|
+
export function createDemo(targetDir) {
|
|
85
|
+
const resolved = path.resolve(targetDir);
|
|
86
|
+
if (fs.existsSync(resolved)) {
|
|
87
|
+
throw new Error(`Refusing to overwrite existing path: ${resolved}`);
|
|
88
|
+
}
|
|
89
|
+
const bundledMocks = path.join(packageRoot(), "mocks");
|
|
90
|
+
if (!fs.existsSync(bundledMocks)) {
|
|
91
|
+
throw new Error(`Bundled demo fixtures not found: ${bundledMocks}`);
|
|
92
|
+
}
|
|
93
|
+
const temporary = `${resolved}.creating-${randomUUID()}`;
|
|
94
|
+
try {
|
|
95
|
+
fs.cpSync(bundledMocks, temporary, {
|
|
96
|
+
recursive: true,
|
|
97
|
+
errorOnExist: true,
|
|
98
|
+
force: false,
|
|
99
|
+
// A globally installed package may already have routed runtime files.
|
|
100
|
+
// Copy only pristine bundled seeds, never another user's generated state.
|
|
101
|
+
filter: (source) => !path.basename(source).includes("--"),
|
|
102
|
+
});
|
|
103
|
+
fs.writeFileSync(path.join(temporary, "README.md"), `# Memorix Explorer demo\n\n` +
|
|
104
|
+
`Start this demo from this directory:\n\n` +
|
|
105
|
+
"```bash\nmemorix-explorer --mocks-dir . --port 5030\n```\n\n" +
|
|
106
|
+
`Then open http://localhost:5030/demo.\n\n` +
|
|
107
|
+
`Create, edit, and delete operations update the routed JSON files here.\n`, "utf8");
|
|
108
|
+
fs.renameSync(temporary, resolved);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
fs.rmSync(temporary, { recursive: true, force: true });
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
return resolved;
|
|
115
|
+
}
|
|
116
|
+
function quote(value) {
|
|
117
|
+
return `"${value.replaceAll('"', '\\"')}"`;
|
|
118
|
+
}
|
|
119
|
+
function displayPath(value) {
|
|
120
|
+
const relative = path.relative(process.cwd(), value);
|
|
121
|
+
if (relative && !relative.startsWith("..") && !path.isAbsolute(relative)) {
|
|
122
|
+
return `./${relative}`;
|
|
123
|
+
}
|
|
124
|
+
return value;
|
|
125
|
+
}
|
|
126
|
+
function parsePortOption(args) {
|
|
127
|
+
if (args.length === 0)
|
|
128
|
+
return 5030;
|
|
129
|
+
if (args.length !== 2 || args[0] !== "--port") {
|
|
130
|
+
throw new Error("Usage: memorix-explorer stop [--port <number>]");
|
|
131
|
+
}
|
|
132
|
+
const port = Number(args[1]);
|
|
133
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
134
|
+
throw new Error("--port must be an integer from 1 to 65535");
|
|
135
|
+
}
|
|
136
|
+
return port;
|
|
137
|
+
}
|
|
138
|
+
function runtimeDirectory() {
|
|
139
|
+
const user = typeof process.getuid === "function" ? String(process.getuid()) : os.userInfo().username;
|
|
140
|
+
return path.join(os.tmpdir(), `memorix-explorer-${user}`);
|
|
141
|
+
}
|
|
142
|
+
export function pidFileForPort(port) {
|
|
143
|
+
return path.join(runtimeDirectory(), `${port}.pid.json`);
|
|
144
|
+
}
|
|
145
|
+
function processCommand(pid) {
|
|
146
|
+
if (process.platform === "win32")
|
|
147
|
+
return "memorix-explorer";
|
|
148
|
+
try {
|
|
149
|
+
return execFileSync("ps", ["-p", String(pid), "-o", "command="], {
|
|
150
|
+
encoding: "utf8",
|
|
151
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
152
|
+
}).trim();
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function isRunning(pid) {
|
|
159
|
+
try {
|
|
160
|
+
process.kill(pid, 0);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
export function registerManagedServer(port) {
|
|
168
|
+
const file = pidFileForPort(port);
|
|
169
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
170
|
+
fs.writeFileSync(file, JSON.stringify({ pid: process.pid, port, startedAt: new Date().toISOString() }) + "\n");
|
|
171
|
+
process.once("exit", () => {
|
|
172
|
+
try {
|
|
173
|
+
const current = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
174
|
+
if (current.pid === process.pid)
|
|
175
|
+
fs.unlinkSync(file);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Missing or replaced PID files need no cleanup.
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
export async function stopManagedServer(port) {
|
|
183
|
+
const file = pidFileForPort(port);
|
|
184
|
+
if (!fs.existsSync(file))
|
|
185
|
+
return false;
|
|
186
|
+
let pid;
|
|
187
|
+
try {
|
|
188
|
+
pid = Number(JSON.parse(fs.readFileSync(file, "utf8")).pid);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
fs.unlinkSync(file);
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
if (!Number.isInteger(pid) || pid <= 0 || !isRunning(pid)) {
|
|
195
|
+
fs.unlinkSync(file);
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
const command = processCommand(pid);
|
|
199
|
+
if (process.platform !== "win32" && !command) {
|
|
200
|
+
throw new Error(`Cannot verify Memorix Explorer PID ${pid}; refusing to stop it`);
|
|
201
|
+
}
|
|
202
|
+
if (command && !/memorix-explorer|static-memorix|dist\/cli\.js/.test(command)) {
|
|
203
|
+
throw new Error(`Refusing to stop PID ${pid}; it is not a Memorix Explorer process`);
|
|
204
|
+
}
|
|
205
|
+
process.kill(pid, "SIGTERM");
|
|
206
|
+
for (let attempt = 0; attempt < 50 && isRunning(pid); attempt++) {
|
|
207
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
208
|
+
}
|
|
209
|
+
if (isRunning(pid))
|
|
210
|
+
throw new Error(`Timed out while stopping Memorix Explorer PID ${pid}`);
|
|
211
|
+
if (fs.existsSync(file))
|
|
212
|
+
fs.unlinkSync(file);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
export async function runCli(args) {
|
|
216
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
217
|
+
console.log(CLI_HELP);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
221
|
+
console.log(getPackageVersion());
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (args[0] === "create-demo") {
|
|
225
|
+
const { targetDir } = parseCreateDemoArgs(args.slice(1));
|
|
226
|
+
const created = createDemo(targetDir);
|
|
227
|
+
const shown = displayPath(created);
|
|
228
|
+
console.log(`\nDemo created:\n ${shown}\n`);
|
|
229
|
+
console.log("Start the server with:");
|
|
230
|
+
console.log(` memorix-explorer --mocks-dir ${quote(shown)}\n`);
|
|
231
|
+
console.log("Then open:");
|
|
232
|
+
console.log(" http://localhost:5030/demo\n");
|
|
233
|
+
console.log("Create, edit, and delete operations will update JSON files in that folder.");
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (args[0] === "stop") {
|
|
237
|
+
const port = parsePortOption(args.slice(1));
|
|
238
|
+
const stopped = await stopManagedServer(port);
|
|
239
|
+
console.log(stopped
|
|
240
|
+
? `Memorix Explorer on port ${port} stopped.`
|
|
241
|
+
: `No managed Memorix Explorer server found on port ${port}.`);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const options = parseServerArgs(args);
|
|
245
|
+
if (options.mocksDir)
|
|
246
|
+
process.env.MOCKS_DIR = options.mocksDir;
|
|
247
|
+
const { startServer } = await import("./server.js");
|
|
248
|
+
const port = options.port ?? 5030;
|
|
249
|
+
try {
|
|
250
|
+
const app = await startServer({ port, host: options.host });
|
|
251
|
+
const address = app.server.address();
|
|
252
|
+
const boundPort = typeof address === "object" && address ? address.port : port;
|
|
253
|
+
registerManagedServer(boundPort);
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
if (error.code === "EADDRINUSE") {
|
|
257
|
+
throw new Error(`Port ${port} is already in use. Run "memorix-explorer stop --port ${port}" ` +
|
|
258
|
+
`or choose another port with "--port ${port + 1}".`);
|
|
259
|
+
}
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function isCliEntrypoint() {
|
|
264
|
+
if (!process.argv[1])
|
|
265
|
+
return false;
|
|
266
|
+
try {
|
|
267
|
+
return fs.realpathSync(path.resolve(process.argv[1])) === fileURLToPath(import.meta.url);
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (isCliEntrypoint()) {
|
|
274
|
+
runCli(process.argv.slice(2)).catch((error) => {
|
|
275
|
+
console.error(error instanceof Error ? error.message : error);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
});
|
|
278
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
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 function validateRouteId(value: string, variableName?: string): string;
|
|
9
|
+
export declare const MEMORIX_ORG_ID: string;
|
|
10
|
+
export declare const MEMORIX_AGENT_ID: string;
|
|
11
|
+
export declare const MEMORIX_DEPLOYMENT_PROFILE: string;
|
|
12
|
+
/**
|
|
13
|
+
* Construct the router string the frontend expects.
|
|
14
|
+
* e.g. "neo-memorix-entities + neo-memorix-events" for MEMORIX_ORG_ID=neo
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildMemorixDbString(): string;
|
|
17
|
+
/**
|
|
18
|
+
* Construct the agent-scoped Catalox DB name.
|
|
19
|
+
* e.g. "ebooks-memorix-catalox"
|
|
20
|
+
*/
|
|
21
|
+
export declare function buildCataloxDbString(): string;
|
|
22
|
+
/**
|
|
23
|
+
* Files emulate routed databases by using the resolved database name as a
|
|
24
|
+
* prefix. The `--` separator is reserved for routing and must not be used in
|
|
25
|
+
* an unprefixed fixture basename.
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildDataFilePrefix(contentType: string): string;
|
|
28
|
+
export declare function buildMetadataFilePrefix(): string;
|
|
29
|
+
export declare function prefixedJsonFilename(prefix: string, basename: string): string;
|
|
30
|
+
export declare const FLAGS: {
|
|
31
|
+
METADATA_WRITES: boolean;
|
|
32
|
+
PIPELINE_WRITES: boolean;
|
|
33
|
+
REGISTRY_WRITES: boolean;
|
|
34
|
+
};
|
|
35
|
+
export declare function metadataWritesEnabled(): boolean;
|
|
36
|
+
export declare function pipelineWritesEnabled(): boolean;
|
|
37
|
+
export declare function registryWritesEnabled(): boolean;
|
|
38
|
+
export declare function metadataPath(...segments: string[]): string;
|
|
39
|
+
export declare function dataPath(...segments: string[]): string;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
4
|
+
const __dirname = path.dirname(__filename);
|
|
5
|
+
export const ROOT_DIR = path.resolve(__dirname);
|
|
6
|
+
export const MOCKS_DIR = process.env.MOCKS_DIR
|
|
7
|
+
? path.resolve(process.env.MOCKS_DIR)
|
|
8
|
+
: path.resolve(ROOT_DIR, "..", "mocks");
|
|
9
|
+
export const METADATA_DIR = path.join(MOCKS_DIR, "metadata");
|
|
10
|
+
export const DATA_DIR = path.join(MOCKS_DIR, "data");
|
|
11
|
+
export const PORT = Number(process.env.PORT || 5030);
|
|
12
|
+
export const HOST = process.env.HOST || "0.0.0.0";
|
|
13
|
+
export const DISK_FLUSH_DEBOUNCE_MS = Number(process.env.DISK_FLUSH_DEBOUNCE_MS || 300);
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// DB Router Simulation (@x12i/memorix-db-router)
|
|
16
|
+
// The mock does not connect to MongoDB, but it must convince the frontend that
|
|
17
|
+
// it is talking to the routed tenant databases.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
function resolveOrgId() {
|
|
20
|
+
if ((process.env.MEMORIX_DEPLOYMENT_PROFILE || "").toLowerCase() === "ebook") {
|
|
21
|
+
return "ebooks";
|
|
22
|
+
}
|
|
23
|
+
return validateRouteId(process.env.MEMORIX_ORG_ID || "memorix", "MEMORIX_ORG_ID");
|
|
24
|
+
}
|
|
25
|
+
function resolveAgentId() {
|
|
26
|
+
if ((process.env.MEMORIX_DEPLOYMENT_PROFILE || "").toLowerCase() === "ebook") {
|
|
27
|
+
return "ebooks";
|
|
28
|
+
}
|
|
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;
|
|
40
|
+
}
|
|
41
|
+
export const MEMORIX_ORG_ID = resolveOrgId();
|
|
42
|
+
export const MEMORIX_AGENT_ID = resolveAgentId();
|
|
43
|
+
export const MEMORIX_DEPLOYMENT_PROFILE = process.env.MEMORIX_DEPLOYMENT_PROFILE || "default";
|
|
44
|
+
/**
|
|
45
|
+
* Construct the router string the frontend expects.
|
|
46
|
+
* e.g. "neo-memorix-entities + neo-memorix-events" for MEMORIX_ORG_ID=neo
|
|
47
|
+
*/
|
|
48
|
+
export function buildMemorixDbString() {
|
|
49
|
+
const org = MEMORIX_ORG_ID;
|
|
50
|
+
return `${org}-memorix-entities + ${org}-memorix-events`;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Construct the agent-scoped Catalox DB name.
|
|
54
|
+
* e.g. "ebooks-memorix-catalox"
|
|
55
|
+
*/
|
|
56
|
+
export function buildCataloxDbString() {
|
|
57
|
+
return `${MEMORIX_AGENT_ID}-memorix-catalox`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Files emulate routed databases by using the resolved database name as a
|
|
61
|
+
* prefix. The `--` separator is reserved for routing and must not be used in
|
|
62
|
+
* an unprefixed fixture basename.
|
|
63
|
+
*/
|
|
64
|
+
export function buildDataFilePrefix(contentType) {
|
|
65
|
+
if (contentType === "events")
|
|
66
|
+
return `${MEMORIX_ORG_ID}-memorix-events`;
|
|
67
|
+
if (contentType === "memory")
|
|
68
|
+
return buildCataloxDbString();
|
|
69
|
+
return `${MEMORIX_ORG_ID}-memorix-entities`;
|
|
70
|
+
}
|
|
71
|
+
export function buildMetadataFilePrefix() {
|
|
72
|
+
return buildCataloxDbString();
|
|
73
|
+
}
|
|
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
|
+
}
|
|
81
|
+
return `${prefix}--${basename}.json`;
|
|
82
|
+
}
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Feature Flag Enforcement
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
function flagEnabled(name) {
|
|
87
|
+
const v = process.env[name];
|
|
88
|
+
return v === "1" || v === "true";
|
|
89
|
+
}
|
|
90
|
+
export const FLAGS = {
|
|
91
|
+
METADATA_WRITES: flagEnabled("MEMORIX_EXPLORER_ENABLE_METADATA_WRITES"),
|
|
92
|
+
PIPELINE_WRITES: flagEnabled("MEMORIX_EXPLORER_ENABLE_PIPELINE_WRITES"),
|
|
93
|
+
REGISTRY_WRITES: flagEnabled("MEMORIX_EXPLORER_ENABLE_REGISTRY_WRITES"),
|
|
94
|
+
};
|
|
95
|
+
export function metadataWritesEnabled() {
|
|
96
|
+
return FLAGS.METADATA_WRITES;
|
|
97
|
+
}
|
|
98
|
+
export function pipelineWritesEnabled() {
|
|
99
|
+
return FLAGS.PIPELINE_WRITES;
|
|
100
|
+
}
|
|
101
|
+
export function registryWritesEnabled() {
|
|
102
|
+
return FLAGS.REGISTRY_WRITES;
|
|
103
|
+
}
|
|
104
|
+
export function metadataPath(...segments) {
|
|
105
|
+
return path.join(METADATA_DIR, ...segments);
|
|
106
|
+
}
|
|
107
|
+
export function dataPath(...segments) {
|
|
108
|
+
return path.join(DATA_DIR, ...segments);
|
|
109
|
+
}
|
|
@@ -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,74 @@
|
|
|
1
|
+
export const STORAGE_ALIASES = {
|
|
2
|
+
data: {
|
|
3
|
+
clientKey: "data",
|
|
4
|
+
storageProp: "doc.associatedData",
|
|
5
|
+
fallbacks: ["associatedData"],
|
|
6
|
+
},
|
|
7
|
+
discovery: {
|
|
8
|
+
clientKey: "discovery",
|
|
9
|
+
storageProp: "doc.associatedInferred or doc.associatedDiscovery",
|
|
10
|
+
fallbacks: ["associatedInferred", "associatedDiscovery"],
|
|
11
|
+
},
|
|
12
|
+
analysis: {
|
|
13
|
+
clientKey: "analysis",
|
|
14
|
+
storageProp: "doc.associatedAnalysis",
|
|
15
|
+
fallbacks: ["associatedAnalysis"],
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Resolve a normalized associated bucket from a raw document.
|
|
20
|
+
* Strips raw `associated*` fields and reconstructs the `associated` bucket
|
|
21
|
+
* using the alias map. Custom associated properties follow the pattern
|
|
22
|
+
* associated{Custom}.
|
|
23
|
+
*/
|
|
24
|
+
export function buildAssociatedBucket(rawDoc) {
|
|
25
|
+
const associated = {};
|
|
26
|
+
const properties = [];
|
|
27
|
+
const strippedDoc = {};
|
|
28
|
+
for (const [k, v] of Object.entries(rawDoc)) {
|
|
29
|
+
if (k.startsWith("associated")) {
|
|
30
|
+
// Map known aliases: associatedData -> data, associatedInferred -> discovery, etc.
|
|
31
|
+
const clientKey = aliasStorageToClient(k);
|
|
32
|
+
associated[clientKey] = v;
|
|
33
|
+
properties.push({
|
|
34
|
+
propertyName: clientKey,
|
|
35
|
+
source: k,
|
|
36
|
+
count: Array.isArray(v) ? v.length : v != null ? 1 : 0,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
strippedDoc[k] = v;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { associated, strippedDoc, properties };
|
|
44
|
+
}
|
|
45
|
+
export function aliasStorageToClient(storageKey) {
|
|
46
|
+
switch (storageKey) {
|
|
47
|
+
case "associatedData":
|
|
48
|
+
return "data";
|
|
49
|
+
case "associatedInferred":
|
|
50
|
+
case "associatedDiscovery":
|
|
51
|
+
return "discovery";
|
|
52
|
+
case "associatedAnalysis":
|
|
53
|
+
return "analysis";
|
|
54
|
+
default:
|
|
55
|
+
// associated{Custom} -> {custom}
|
|
56
|
+
return storageKey.replace(/^associated/, "").replace(/^./, (c) => c.toLowerCase());
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function aliasClientToStorage(clientKey) {
|
|
60
|
+
const entry = STORAGE_ALIASES[clientKey];
|
|
61
|
+
if (entry) {
|
|
62
|
+
return { primary: entry.fallbacks[0], fallbacks: entry.fallbacks };
|
|
63
|
+
}
|
|
64
|
+
// custom -> associated{Custom}
|
|
65
|
+
const cap = clientKey.replace(/^./, (c) => c.toUpperCase());
|
|
66
|
+
const storage = `associated${cap}`;
|
|
67
|
+
return { primary: storage, fallbacks: [storage] };
|
|
68
|
+
}
|
|
69
|
+
export function resolveInclude(include) {
|
|
70
|
+
if (!include)
|
|
71
|
+
return null;
|
|
72
|
+
const list = Array.isArray(include) ? include : include.split(",");
|
|
73
|
+
return new Set(list.map((s) => s.trim()).filter(Boolean));
|
|
74
|
+
}
|
|
@@ -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,60 @@
|
|
|
1
|
+
import { store } from "../storage/InMemoryStore.js";
|
|
2
|
+
import { CONTENT_TYPES } from "../types.js";
|
|
3
|
+
import { computeAssociationFingerprint } from "../engine/snapshots.js";
|
|
4
|
+
import { listDataObjectTypes } from "../engine/inventory.js";
|
|
5
|
+
export function planAssociations(objectType) {
|
|
6
|
+
const types = objectType ? [objectType] : listDataObjectTypes();
|
|
7
|
+
const plans = [];
|
|
8
|
+
for (const ot of types) {
|
|
9
|
+
const fingerprint = computeAssociationFingerprint(ot);
|
|
10
|
+
const mutations = [];
|
|
11
|
+
for (const ct of CONTENT_TYPES) {
|
|
12
|
+
const records = store.getCollection(ot, ct);
|
|
13
|
+
mutations.push({
|
|
14
|
+
recordId: `${ot}/${ct}`,
|
|
15
|
+
targetArray: `${ot}/${ct}`,
|
|
16
|
+
op: "noop",
|
|
17
|
+
count: records.length,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
plans.push({
|
|
21
|
+
objectType: ot,
|
|
22
|
+
expectedPlanFingerprint: fingerprint,
|
|
23
|
+
proposedMutations: mutations,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return plans;
|
|
27
|
+
}
|
|
28
|
+
export function applyAssociations(expectedPlanFingerprint, objectType) {
|
|
29
|
+
const types = objectType ? [objectType] : listDataObjectTypes();
|
|
30
|
+
const results = [];
|
|
31
|
+
for (const ot of types) {
|
|
32
|
+
const actual = computeAssociationFingerprint(ot);
|
|
33
|
+
const mismatch = actual !== expectedPlanFingerprint;
|
|
34
|
+
results.push({
|
|
35
|
+
objectType: ot,
|
|
36
|
+
applied: !mismatch,
|
|
37
|
+
expectedPlanFingerprint,
|
|
38
|
+
actualFingerprint: actual,
|
|
39
|
+
mismatch,
|
|
40
|
+
count: store.getCollection(ot, "snapshots").length,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
// Trigger flush
|
|
44
|
+
for (const ot of types) {
|
|
45
|
+
store.flushCollectionNow(`${ot}/snapshots`);
|
|
46
|
+
}
|
|
47
|
+
return results;
|
|
48
|
+
}
|
|
49
|
+
export function verifyAssociations(expectedPlanFingerprint, objectType) {
|
|
50
|
+
const types = objectType ? [objectType] : listDataObjectTypes();
|
|
51
|
+
return types.map((ot) => {
|
|
52
|
+
const actual = computeAssociationFingerprint(ot);
|
|
53
|
+
return {
|
|
54
|
+
objectType: ot,
|
|
55
|
+
expectedPlanFingerprint,
|
|
56
|
+
actualFingerprint: actual,
|
|
57
|
+
verified: actual === expectedPlanFingerprint,
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -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;
|