@x12i/static-memorix-explorer-api 1.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.
@@ -0,0 +1,123 @@
1
+ import path from "node:path";
2
+ import { store } from "../storage/InMemoryStore.js";
3
+ import { basenameNoExt, listJsonFiles } from "../storage/fs.js";
4
+ import { METADATA_DIR } from "../config.js";
5
+ export function loadWriteDescriptor(writeDescriptorId) {
6
+ const desc = store.getMeta(`write-descriptors/${writeDescriptorId}`);
7
+ if (!desc) {
8
+ throw Object.assign(new Error(`Write descriptor not found: ${writeDescriptorId}`), {
9
+ statusCode: 404,
10
+ });
11
+ }
12
+ return desc;
13
+ }
14
+ /**
15
+ * Validate input against the write descriptor schema (lightweight AJV-free check).
16
+ */
17
+ export function validateInputSchema(writeSpec, input) {
18
+ const errors = [];
19
+ const schema = (writeSpec.schema || {});
20
+ const payload = Array.isArray(input) ? input : input ? [input] : [];
21
+ if (schema.required && Array.isArray(schema.required)) {
22
+ for (const req of schema.required) {
23
+ for (const item of payload) {
24
+ if (item?.[req] === undefined) {
25
+ errors.push(`Missing required field: ${req}`);
26
+ }
27
+ }
28
+ }
29
+ }
30
+ if (schema.properties) {
31
+ for (const item of payload) {
32
+ for (const [field, def] of Object.entries(schema.properties)) {
33
+ if (item[field] === undefined)
34
+ continue;
35
+ if (def.type) {
36
+ const actualType = Array.isArray(item[field]) ? "array" : typeof item[field];
37
+ const expected = def.type;
38
+ if (expected === "array" && !Array.isArray(item[field])) {
39
+ errors.push(`Field ${field} must be array`);
40
+ }
41
+ else if (expected !== "array" && expected !== actualType) {
42
+ errors.push(`Field ${field} must be ${expected}, got ${actualType}`);
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }
48
+ return { ok: errors.length === 0, errors };
49
+ }
50
+ export function parseTargetCollection(target) {
51
+ const [objectType, ctRaw] = target.split("/");
52
+ // The `memory` target routes to the standalone memory tier.
53
+ if (objectType === "memory" || ctRaw === "memory") {
54
+ return { objectType: "memory", ct: "memory" };
55
+ }
56
+ const ct = (ctRaw || "snapshots");
57
+ return { objectType, ct };
58
+ }
59
+ export function executeOperation(params) {
60
+ const { objectType, ct } = parseTargetCollection(params.target);
61
+ const collection = store.getCollection(objectType, ct);
62
+ const payload = Array.isArray(params.data) ? params.data : [params.data];
63
+ // Resolve the identity field for this collection tier.
64
+ // Memory tier uses memoryId; everything else defaults to recordId.
65
+ const idField = ct === "memory" ? "memoryId" : "recordId";
66
+ switch (params.op) {
67
+ case "add": {
68
+ const nextId = getNextId(collection, idField);
69
+ for (const item of payload) {
70
+ collection.push({ ...item, [idField]: item[idField] ?? nextId() });
71
+ }
72
+ break;
73
+ }
74
+ case "upsert": {
75
+ for (const item of payload) {
76
+ const id = item[idField] ?? item.recordId ?? item.id;
77
+ const idx = collection.findIndex((d) => (d[idField] ?? d.recordId ?? d.id) === id);
78
+ if (idx >= 0) {
79
+ collection[idx] = { ...collection[idx], ...item };
80
+ }
81
+ else {
82
+ collection.push(item);
83
+ }
84
+ }
85
+ break;
86
+ }
87
+ case "patch": {
88
+ for (const item of payload) {
89
+ const id = item[idField] ?? item.recordId ?? item.id;
90
+ const idx = collection.findIndex((d) => (d[idField] ?? d.recordId ?? d.id) === id);
91
+ if (idx >= 0) {
92
+ collection[idx] = { ...collection[idx], ...item };
93
+ }
94
+ }
95
+ break;
96
+ }
97
+ case "replace": {
98
+ return payload;
99
+ }
100
+ }
101
+ store.setCollection(objectType, ct, collection);
102
+ return payload;
103
+ }
104
+ function getNextId(collection, idField = "recordId") {
105
+ let max = 0;
106
+ for (const d of collection) {
107
+ const id = Number(d[idField] ?? d.recordId ?? d.id);
108
+ if (!isNaN(id) && id > max)
109
+ max = id;
110
+ }
111
+ let counter = max + 1;
112
+ return () => String(counter++);
113
+ }
114
+ export function listWriteDescriptors() {
115
+ const dir = path.join(METADATA_DIR, "write-descriptors");
116
+ const out = [];
117
+ for (const file of listJsonFiles(dir)) {
118
+ const desc = store.getMeta(`write-descriptors/${basenameNoExt(file)}`);
119
+ if (desc)
120
+ out.push(desc);
121
+ }
122
+ return out;
123
+ }
@@ -0,0 +1,29 @@
1
+ export function qp(request) {
2
+ const q = (request.query || {});
3
+ return {
4
+ filter: q.filter,
5
+ sort: q.sort,
6
+ searchText: q.searchText,
7
+ q: q.q,
8
+ limit: q.limit,
9
+ offset: q.offset,
10
+ skip: q.skip,
11
+ includeTotal: q.includeTotal === "1" || q.includeTotal === "true",
12
+ include: q.include,
13
+ mode: q.mode,
14
+ listId: q.listId,
15
+ entityName: q.entityName,
16
+ sourceLens: q.sourceLens,
17
+ target: q.target,
18
+ includeExactCounts: q.includeExactCounts === "1" || q.includeExactCounts === "true",
19
+ includeInventory: q.includeInventory === "1" || q.includeInventory === "true",
20
+ memoryId: q.memoryId,
21
+ recordId: q.recordId,
22
+ entityId: q.entityId,
23
+ eventId: q.eventId,
24
+ knowledgeId: q.knowledgeId,
25
+ };
26
+ }
27
+ export function str(v, fallback = "") {
28
+ return v === undefined || v === null ? fallback : String(v);
29
+ }
@@ -0,0 +1,345 @@
1
+ import { store } from "../storage/InMemoryStore.js";
2
+ import { buildMemorixDbString, buildCataloxDbString, metadataWritesEnabled, } from "../config.js";
3
+ import { listObjectTypes } from "../engine/inventory.js";
4
+ import { computeInventory, computeInventorySummary, computeInventoryIssues, computeGraph, } from "../engine/inventory.js";
5
+ import { qp, str } from "./helpers.js";
6
+ import { getSnapshotNormalized, getSnapshotAssociated, getSnapshotAssociatedProperty, getAssociatedProperties, } from "../engine/snapshots.js";
7
+ import { getRecordsCollection, getRecordsFull, getRecordsItem, getRawCollection, getRawItem, getWorkspaceRecords, getRecordContent, } from "../engine/records.js";
8
+ import { listListDescriptors, getListDescriptor, executeListDescriptor, suggestListExtensions, } from "../engine/lists.js";
9
+ import { getMergedNarratives, getRawNarratives, queryNarrativeRecords, } from "../engine/narratives.js";
10
+ import { computeRootPropertyCatalog } from "../engine/objectTypes.js";
11
+ import { loadWriteDescriptor, validateInputSchema, executeOperation, } from "../engine/write.js";
12
+ import { planAssociations, applyAssociations, verifyAssociations, } from "../engine/associations.js";
13
+ export async function registerRoutes(app) {
14
+ // ---------- A. Health & System Diagnostics ----------
15
+ app.get("/health", async () => ({ ok: true }));
16
+ app.get("/api/explorer/health", async (request) => {
17
+ const includeInventory = qp(request).includeInventory;
18
+ const objectTypes = listObjectTypes();
19
+ const base = {
20
+ ok: true,
21
+ mongoUriConfigured: true,
22
+ discoverySample: objectTypes,
23
+ memorixDb: buildMemorixDbString(),
24
+ cataloxDb: buildCataloxDbString(),
25
+ };
26
+ if (includeInventory) {
27
+ const rows = computeInventory({ sourceLens: "catalox-first" });
28
+ const summary = computeInventorySummary(rows, { includeExactCounts: false });
29
+ return { ...base, inventorySummary: summary };
30
+ }
31
+ return base;
32
+ });
33
+ // ---------- B. Inventory & Graph Engine ----------
34
+ app.get("/api/explorer/inventory/collections", async (request) => {
35
+ const rows = computeInventory(qp(request));
36
+ return { rows, total: rows.length };
37
+ });
38
+ app.get("/api/explorer/inventory/summary", async (request) => {
39
+ const rows = computeInventory(qp(request));
40
+ const summary = computeInventorySummary(rows, qp(request));
41
+ return { summary, rows };
42
+ });
43
+ app.get("/api/explorer/inventory/issues", async (request) => {
44
+ const rows = computeInventory(qp(request));
45
+ const issues = computeInventoryIssues(rows);
46
+ return { issues, total: issues.length };
47
+ });
48
+ app.get("/api/explorer/inventory/graph", async (request) => {
49
+ const rows = computeInventory(qp(request));
50
+ return computeGraph(rows);
51
+ });
52
+ // ---------- C. Snapshots & Associated Data Pipeline ----------
53
+ app.get("/api/explorer/snapshots/:objectType/:recordId", async (request, reply) => {
54
+ const { objectType, recordId } = request.params;
55
+ const q = qp(request);
56
+ const result = getSnapshotNormalized(objectType, recordId, q.include);
57
+ if (!result)
58
+ return send404(reply, `Snapshot not found: ${objectType}/${recordId}`);
59
+ return result;
60
+ });
61
+ app.get("/api/explorer/snapshots/:objectType/:recordId/associated", async (request, reply) => {
62
+ const { objectType, recordId } = request.params;
63
+ const result = getSnapshotAssociated(objectType, recordId);
64
+ if (!result)
65
+ return send404(reply, `Snapshot not found: ${objectType}/${recordId}`);
66
+ return result;
67
+ });
68
+ app.get("/api/explorer/snapshots/:objectType/:recordId/associated/:propertyName", async (request, reply) => {
69
+ const { objectType, recordId, propertyName } = request.params;
70
+ const result = getSnapshotAssociatedProperty(objectType, recordId, propertyName);
71
+ if (result === undefined)
72
+ return send404(reply, `Associated property not found: ${propertyName}`);
73
+ return result;
74
+ });
75
+ app.get("/api/explorer/snapshots/:objectType/associated-properties", async (request) => {
76
+ const { objectType } = request.params;
77
+ return { objectType, properties: getAssociatedProperties(objectType) };
78
+ });
79
+ app.post("/api/explorer/associations/plan", async (request) => {
80
+ const { objectType } = (request.body || {});
81
+ return { plans: planAssociations(objectType) };
82
+ });
83
+ app.post("/api/explorer/associations/apply", async (request) => {
84
+ const { expectedPlanFingerprint, objectType } = (request.body || {});
85
+ return { results: applyAssociations(expectedPlanFingerprint, objectType) };
86
+ });
87
+ app.post("/api/explorer/associations/verify", async (request) => {
88
+ const { expectedPlanFingerprint, objectType } = (request.body || {});
89
+ return { results: verifyAssociations(expectedPlanFingerprint, objectType) };
90
+ });
91
+ // ---------- D. Records Engine ----------
92
+ app.get("/api/explorer/records/collection", async (request) => {
93
+ const q = qp(request);
94
+ return await getRecordsCollection(q);
95
+ });
96
+ app.get("/api/explorer/records/full", async (request) => {
97
+ const q = qp(request);
98
+ const entityName = str(q.entityName);
99
+ const recordId = str(request.query.recordId);
100
+ if (!entityName || !recordId)
101
+ return { error: "entityName and recordId are required" };
102
+ return getRecordsFull(entityName, recordId);
103
+ });
104
+ app.get("/api/explorer/records/item", async (request, reply) => {
105
+ const q = qp(request);
106
+ const entityName = str(q.entityName);
107
+ const recordId = str(request.query.recordId);
108
+ const itemDescriptor = request.query.itemDescriptor
109
+ ? JSON.parse(request.query.itemDescriptor)
110
+ : undefined;
111
+ const result = getRecordsItem(entityName, recordId, itemDescriptor);
112
+ if (!result)
113
+ return send404(reply, `Item not found: ${entityName}/${recordId}`);
114
+ return result;
115
+ });
116
+ app.get("/api/explorer/records/content", async (request, reply) => {
117
+ const q = qp(request);
118
+ const entityName = str(q.entityName) || (q.target === "memory" ? "memory" : "");
119
+ const contentType = request.query.contentType;
120
+ const identity = {};
121
+ if (q.recordId)
122
+ identity.recordId = q.recordId;
123
+ if (q.entityId)
124
+ identity.entityId = q.entityId;
125
+ if (q.eventId)
126
+ identity.eventId = q.eventId;
127
+ if (q.knowledgeId)
128
+ identity.knowledgeId = q.knowledgeId;
129
+ if (q.memoryId)
130
+ identity.memoryId = q.memoryId;
131
+ if (!contentType || Object.keys(identity).length === 0) {
132
+ return { ok: false, error: "contentType and at least one identity key required" };
133
+ }
134
+ const result = getRecordContent(entityName, contentType, identity);
135
+ if (!result)
136
+ return send404(reply, `Content not found: ${entityName}/${contentType}`);
137
+ return result;
138
+ });
139
+ app.get("/api/explorer/records/raw-collection", async (request) => {
140
+ const { entityName, collectionName } = request.query;
141
+ const name = collectionName || entityName;
142
+ return getRawCollection(str(entityName), str(name));
143
+ });
144
+ app.get("/api/explorer/records/raw-item", async (request, reply) => {
145
+ const { collectionName, recordId } = request.query;
146
+ const result = getRawItem(str(collectionName), str(recordId));
147
+ if (!result)
148
+ return send404(reply, `Raw item not found: ${collectionName}/${recordId}`);
149
+ return result;
150
+ });
151
+ app.get("/api/explorer/records/workspace", async (request) => {
152
+ return await getWorkspaceRecords(qp(request));
153
+ });
154
+ // ---------- E. Lists Subsystem ----------
155
+ app.get("/api/explorer/lists", async (request) => {
156
+ const { entityName } = request.query;
157
+ return { lists: listListDescriptors(entityName) };
158
+ });
159
+ app.get("/api/explorer/lists/:listId", async (request, reply) => {
160
+ const { listId } = request.params;
161
+ const desc = getListDescriptor(listId);
162
+ if (!desc)
163
+ return send404(reply, `List descriptor not found: ${listId}`);
164
+ return desc;
165
+ });
166
+ app.get("/api/explorer/lists/:listId/records", async (request, reply) => {
167
+ const { listId } = request.params;
168
+ const desc = getListDescriptor(listId);
169
+ if (!desc)
170
+ return send404(reply, `List descriptor not found: ${listId}`);
171
+ const result = await executeListDescriptor(desc, qp(request));
172
+ return result;
173
+ });
174
+ app.get("/api/explorer/lists/:listId/records-ws", async (request, reply) => {
175
+ const { listId } = request.params;
176
+ const desc = getListDescriptor(listId);
177
+ if (!desc)
178
+ return send404(reply, `List descriptor not found: ${listId}`);
179
+ const result = await executeListDescriptor(desc, qp(request));
180
+ return result;
181
+ });
182
+ app.get("/api/explorer/lists/suggest", async (request) => {
183
+ const { entityName } = request.query;
184
+ return suggestListExtensions(str(entityName));
185
+ });
186
+ app.post("/api/explorer/lists", async (request) => {
187
+ if (!metadataWritesEnabled()) {
188
+ return { ok: false, error: "Metadata writes are disabled" };
189
+ }
190
+ const body = (request.body || {});
191
+ const listId = body.listId || body.id;
192
+ if (!listId)
193
+ return { error: "listId required" };
194
+ store.setMeta(`lists/${listId}`, { ...body, listId });
195
+ return { ok: true, listId };
196
+ });
197
+ app.patch("/api/explorer/lists/:listId", async (request) => {
198
+ if (!metadataWritesEnabled()) {
199
+ return { ok: false, error: "Metadata writes are disabled" };
200
+ }
201
+ const { listId } = request.params;
202
+ const existing = getListDescriptor(listId) || {};
203
+ const updated = { ...existing, ...(request.body || {}), listId };
204
+ store.setMeta(`lists/${listId}`, updated);
205
+ return { ok: true, listId, descriptor: updated };
206
+ });
207
+ app.put("/api/explorer/lists/:listId/sort", async (request) => {
208
+ if (!metadataWritesEnabled()) {
209
+ return { ok: false, error: "Metadata writes are disabled" };
210
+ }
211
+ const { listId } = request.params;
212
+ const existing = getListDescriptor(listId) || {};
213
+ const updated = { ...existing, sort: (request.body || {}).sort, listId };
214
+ store.setMeta(`lists/${listId}`, updated);
215
+ return { ok: true, listId, descriptor: updated };
216
+ });
217
+ app.delete("/api/explorer/lists/:listId", async (request) => {
218
+ if (!metadataWritesEnabled()) {
219
+ return { ok: false, error: "Metadata writes are disabled" };
220
+ }
221
+ const { listId } = request.params;
222
+ store.setMeta(`lists/${listId}`, undefined, false);
223
+ return { ok: true, deleted: listId };
224
+ });
225
+ // ---------- F. Narratives Engine ----------
226
+ app.get("/api/explorer/narratives", async () => {
227
+ return { narratives: getMergedNarratives() };
228
+ });
229
+ app.get("/api/explorer/narratives/:entity", async (request) => {
230
+ const { entity } = request.params;
231
+ return { entity, narratives: getMergedNarratives(entity) };
232
+ });
233
+ app.get("/api/explorer/narratives/:entity/raw", async (request) => {
234
+ const { entity } = request.params;
235
+ return { entity, raw: getRawNarratives(entity) };
236
+ });
237
+ app.get("/api/explorer/narratives/:entity/:key", async (request, reply) => {
238
+ const { entity, key } = request.params;
239
+ const merged = getMergedNarratives(entity);
240
+ const found = merged.find((n) => n.key === key);
241
+ if (!found)
242
+ return send404(reply, `Narrative not found: ${entity}/${key}`);
243
+ return found;
244
+ });
245
+ app.get("/api/explorer/narratives/:entity/:key/records", async (request) => {
246
+ const { entity, key } = request.params;
247
+ return { entity, key, records: queryNarrativeRecords(entity, key) };
248
+ });
249
+ app.post("/api/explorer/narratives", async (request) => {
250
+ if (!metadataWritesEnabled()) {
251
+ return { ok: false, error: "Metadata writes are disabled" };
252
+ }
253
+ const { entity, key, ...rest } = (request.body || {});
254
+ if (!entity || !key)
255
+ return { error: "entity and key required" };
256
+ const authored = getRawNarratives(entity);
257
+ authored[key] = { key, ...rest };
258
+ store.setMeta(`narratives/${entity}`, authored);
259
+ return { ok: true, entity, key };
260
+ });
261
+ app.patch("/api/explorer/narratives/:entity/:key", async (request) => {
262
+ if (!metadataWritesEnabled()) {
263
+ return { ok: false, error: "Metadata writes are disabled" };
264
+ }
265
+ const { entity, key } = request.params;
266
+ const authored = getRawNarratives(entity);
267
+ authored[key] = { ...authored[key], ...(request.body || {}), key };
268
+ store.setMeta(`narratives/${entity}`, authored);
269
+ return { ok: true, entity, key, descriptor: authored[key] };
270
+ });
271
+ app.delete("/api/explorer/narratives/:entity/:key", async (request) => {
272
+ if (!metadataWritesEnabled()) {
273
+ return { ok: false, error: "Metadata writes are disabled" };
274
+ }
275
+ const { entity, key } = request.params;
276
+ const authored = getRawNarratives(entity);
277
+ delete authored[key];
278
+ store.setMeta(`narratives/${entity}`, authored);
279
+ return { ok: true, entity, key };
280
+ });
281
+ // ---------- G. Object Types & Root Property Catalog ----------
282
+ app.get("/api/explorer/object-types", async () => {
283
+ return { objectTypes: listObjectTypes().map((name) => store.getMeta(`object-types/${name}`)) };
284
+ });
285
+ app.get("/api/explorer/object-types/:name/root-property-catalog", async (request) => {
286
+ const { name } = request.params;
287
+ const meta = store.getMeta(`object-types/${name}`);
288
+ return {
289
+ objectType: name,
290
+ rootPropertyCatalog: meta?.rootPropertyCatalog || [],
291
+ };
292
+ });
293
+ app.post("/api/explorer/object-types/:name/root-property-catalog/compute", async (request) => {
294
+ const { name } = request.params;
295
+ const entries = computeRootPropertyCatalog(name);
296
+ return { objectType: name, computed: true, rootPropertyCatalog: entries };
297
+ });
298
+ // ---------- H. Record Write Engine ----------
299
+ app.post("/api/explorer/records/write", async (request) => {
300
+ const { writeDescriptorId, operation = "add", input, records, dryRun = false, validateOnly = false, } = (request.body || {});
301
+ if (!writeDescriptorId)
302
+ return { error: "writeDescriptorId required", ok: false };
303
+ const writeSpec = loadWriteDescriptor(writeDescriptorId);
304
+ const targetCollection = writeSpec.targetCollection;
305
+ if (!targetCollection)
306
+ return { error: "writeDescriptor missing targetCollection", ok: false };
307
+ const data = input || records;
308
+ const validation = validateInputSchema(writeSpec, data);
309
+ if (!validation.ok) {
310
+ return { ok: false, validated: false, errors: validation.errors };
311
+ }
312
+ if (validateOnly)
313
+ return { ok: true, validated: true };
314
+ const results = executeOperation({
315
+ target: targetCollection,
316
+ op: operation,
317
+ data,
318
+ });
319
+ if (!dryRun) {
320
+ await store.flushAll();
321
+ }
322
+ return { ok: true, count: results.length, dryRun };
323
+ });
324
+ app.get("/api/explorer/object-types/:name", async (request, reply) => {
325
+ const { name } = request.params;
326
+ const meta = store.getMeta(`object-types/${name}`);
327
+ if (!meta)
328
+ return send404(reply, `Object type not found: ${name}`);
329
+ return meta;
330
+ });
331
+ // ---------- I. Agents Engine ----------
332
+ app.get("/api/explorer/agents", async () => {
333
+ const agents = store.getMeta("agents");
334
+ return agents || { agents: [] };
335
+ });
336
+ // ---- Authix / M2M Auth Shim ----
337
+ app.post("/api/explorer/auth/token", async () => ({
338
+ access_token: "mock-m2m-token",
339
+ token_type: "Bearer",
340
+ expires_in: 3600,
341
+ }));
342
+ }
343
+ function send404(reply, message) {
344
+ return reply.code(404).send({ ok: false, error: message });
345
+ }
package/dist/server.js ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ import Fastify from "fastify";
3
+ import { store } from "./storage/InMemoryStore.js";
4
+ import { registerRoutes } from "./routes/index.js";
5
+ import { PORT, HOST, MOCKS_DIR } from "./config.js";
6
+ async function main() {
7
+ const app = Fastify({ logger: false });
8
+ // Global error handler matching predictable error shapes.
9
+ app.setErrorHandler((error, _request, reply) => {
10
+ const status = error.statusCode && error.statusCode >= 400 ? error.statusCode : 500;
11
+ reply.status(status).send({
12
+ ok: false,
13
+ error: error.message || "Internal Server Error",
14
+ });
15
+ });
16
+ await store.load();
17
+ await registerRoutes(app);
18
+ const port = Number(process.env.PORT || PORT);
19
+ await app.listen({ port, host: HOST });
20
+ // eslint-disable-next-line no-console
21
+ console.log(`[mock-memorix-explorer-api] listening on ${HOST}:${port}`);
22
+ console.log(`[mock-memorix-explorer-api] mocks dir: ${MOCKS_DIR}`);
23
+ const shutdown = async () => {
24
+ console.log("\n[mock-memorix-explorer-api] flushing + shutting down");
25
+ await store.flushAll();
26
+ await app.close();
27
+ process.exit(0);
28
+ };
29
+ process.on("SIGINT", shutdown);
30
+ process.on("SIGTERM", shutdown);
31
+ }
32
+ main().catch((err) => {
33
+ // eslint-disable-next-line no-console
34
+ console.error(err);
35
+ process.exit(1);
36
+ });