@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
|
@@ -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,402 @@
|
|
|
1
|
+
import { store } from "../storage/InMemoryStore.js";
|
|
2
|
+
import { buildMemorixDbString, buildCataloxDbString, buildDataFilePrefix, buildMetadataFilePrefix, 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
|
+
import { MOCKS_DIR } from "../config.js";
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
export async function registerRoutes(app) {
|
|
17
|
+
// ---------- A. Health & System Diagnostics ----------
|
|
18
|
+
app.get("/health", async () => ({ ok: true }));
|
|
19
|
+
app.get("/api/explorer/health", async (request) => {
|
|
20
|
+
const includeInventory = qp(request).includeInventory;
|
|
21
|
+
const objectTypes = listObjectTypes();
|
|
22
|
+
const base = {
|
|
23
|
+
ok: true,
|
|
24
|
+
mongoUriConfigured: true,
|
|
25
|
+
discoverySample: objectTypes,
|
|
26
|
+
memorixDb: buildMemorixDbString(),
|
|
27
|
+
cataloxDb: buildCataloxDbString(),
|
|
28
|
+
fileRouting: {
|
|
29
|
+
entitiesPrefix: buildDataFilePrefix("snapshots"),
|
|
30
|
+
eventsPrefix: buildDataFilePrefix("events"),
|
|
31
|
+
memoryPrefix: buildDataFilePrefix("memory"),
|
|
32
|
+
metadataPrefix: buildMetadataFilePrefix(),
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
if (includeInventory) {
|
|
36
|
+
const rows = computeInventory({ sourceLens: "catalox-first" });
|
|
37
|
+
const summary = computeInventorySummary(rows, { includeExactCounts: false });
|
|
38
|
+
return { ...base, inventorySummary: summary };
|
|
39
|
+
}
|
|
40
|
+
return base;
|
|
41
|
+
});
|
|
42
|
+
// ---------- B. Inventory & Graph Engine ----------
|
|
43
|
+
app.get("/api/explorer/inventory/collections", async (request) => {
|
|
44
|
+
const rows = computeInventory(qp(request));
|
|
45
|
+
return { rows, total: rows.length };
|
|
46
|
+
});
|
|
47
|
+
app.get("/api/explorer/inventory/summary", async (request) => {
|
|
48
|
+
const rows = computeInventory(qp(request));
|
|
49
|
+
const summary = computeInventorySummary(rows, qp(request));
|
|
50
|
+
return { summary, rows };
|
|
51
|
+
});
|
|
52
|
+
app.get("/api/explorer/inventory/issues", async (request) => {
|
|
53
|
+
const rows = computeInventory(qp(request));
|
|
54
|
+
const issues = computeInventoryIssues(rows);
|
|
55
|
+
return { issues, total: issues.length };
|
|
56
|
+
});
|
|
57
|
+
app.get("/api/explorer/inventory/graph", async (request) => {
|
|
58
|
+
const rows = computeInventory(qp(request));
|
|
59
|
+
return computeGraph(rows);
|
|
60
|
+
});
|
|
61
|
+
// ---------- C. Snapshots & Associated Data Pipeline ----------
|
|
62
|
+
app.get("/api/explorer/snapshots/:objectType/:recordId", async (request, reply) => {
|
|
63
|
+
const { objectType, recordId } = request.params;
|
|
64
|
+
const q = qp(request);
|
|
65
|
+
const result = getSnapshotNormalized(objectType, recordId, q.include);
|
|
66
|
+
if (!result)
|
|
67
|
+
return send404(reply, `Snapshot not found: ${objectType}/${recordId}`);
|
|
68
|
+
return result;
|
|
69
|
+
});
|
|
70
|
+
app.get("/api/explorer/snapshots/:objectType/:recordId/associated", async (request, reply) => {
|
|
71
|
+
const { objectType, recordId } = request.params;
|
|
72
|
+
const result = getSnapshotAssociated(objectType, recordId);
|
|
73
|
+
if (!result)
|
|
74
|
+
return send404(reply, `Snapshot not found: ${objectType}/${recordId}`);
|
|
75
|
+
return result;
|
|
76
|
+
});
|
|
77
|
+
app.get("/api/explorer/snapshots/:objectType/:recordId/associated/:propertyName", async (request, reply) => {
|
|
78
|
+
const { objectType, recordId, propertyName } = request.params;
|
|
79
|
+
const result = getSnapshotAssociatedProperty(objectType, recordId, propertyName);
|
|
80
|
+
if (result === undefined)
|
|
81
|
+
return send404(reply, `Associated property not found: ${propertyName}`);
|
|
82
|
+
return result;
|
|
83
|
+
});
|
|
84
|
+
app.get("/api/explorer/snapshots/:objectType/associated-properties", async (request) => {
|
|
85
|
+
const { objectType } = request.params;
|
|
86
|
+
return { objectType, properties: getAssociatedProperties(objectType) };
|
|
87
|
+
});
|
|
88
|
+
app.post("/api/explorer/associations/plan", async (request) => {
|
|
89
|
+
const { objectType } = (request.body || {});
|
|
90
|
+
return { plans: planAssociations(objectType) };
|
|
91
|
+
});
|
|
92
|
+
app.post("/api/explorer/associations/apply", async (request) => {
|
|
93
|
+
const { expectedPlanFingerprint, objectType } = (request.body || {});
|
|
94
|
+
return { results: applyAssociations(expectedPlanFingerprint, objectType) };
|
|
95
|
+
});
|
|
96
|
+
app.post("/api/explorer/associations/verify", async (request) => {
|
|
97
|
+
const { expectedPlanFingerprint, objectType } = (request.body || {});
|
|
98
|
+
return { results: verifyAssociations(expectedPlanFingerprint, objectType) };
|
|
99
|
+
});
|
|
100
|
+
// ---------- D. Records Engine ----------
|
|
101
|
+
app.get("/api/explorer/records/collection", async (request) => {
|
|
102
|
+
const q = qp(request);
|
|
103
|
+
return await getRecordsCollection(q);
|
|
104
|
+
});
|
|
105
|
+
app.get("/api/explorer/records/full", async (request) => {
|
|
106
|
+
const q = qp(request);
|
|
107
|
+
const entityName = str(q.entityName);
|
|
108
|
+
const recordId = str(request.query.recordId);
|
|
109
|
+
if (!entityName || !recordId)
|
|
110
|
+
return { error: "entityName and recordId are required" };
|
|
111
|
+
return getRecordsFull(entityName, recordId);
|
|
112
|
+
});
|
|
113
|
+
app.get("/api/explorer/records/item", async (request, reply) => {
|
|
114
|
+
const q = qp(request);
|
|
115
|
+
const entityName = str(q.entityName);
|
|
116
|
+
const recordId = str(request.query.recordId);
|
|
117
|
+
const itemDescriptor = request.query.itemDescriptor
|
|
118
|
+
? JSON.parse(request.query.itemDescriptor)
|
|
119
|
+
: undefined;
|
|
120
|
+
const result = getRecordsItem(entityName, recordId, itemDescriptor);
|
|
121
|
+
if (!result)
|
|
122
|
+
return send404(reply, `Item not found: ${entityName}/${recordId}`);
|
|
123
|
+
return result;
|
|
124
|
+
});
|
|
125
|
+
app.get("/api/explorer/records/content", async (request, reply) => {
|
|
126
|
+
const q = qp(request);
|
|
127
|
+
const entityName = str(q.entityName) || (q.target === "memory" ? "memory" : "");
|
|
128
|
+
const contentType = request.query.contentType;
|
|
129
|
+
const identity = {};
|
|
130
|
+
if (q.recordId)
|
|
131
|
+
identity.recordId = q.recordId;
|
|
132
|
+
if (q.entityId)
|
|
133
|
+
identity.entityId = q.entityId;
|
|
134
|
+
if (q.eventId)
|
|
135
|
+
identity.eventId = q.eventId;
|
|
136
|
+
if (q.knowledgeId)
|
|
137
|
+
identity.knowledgeId = q.knowledgeId;
|
|
138
|
+
if (q.memoryId)
|
|
139
|
+
identity.memoryId = q.memoryId;
|
|
140
|
+
if (!contentType || Object.keys(identity).length === 0) {
|
|
141
|
+
return { ok: false, error: "contentType and at least one identity key required" };
|
|
142
|
+
}
|
|
143
|
+
const result = getRecordContent(entityName, contentType, identity);
|
|
144
|
+
if (!result)
|
|
145
|
+
return send404(reply, `Content not found: ${entityName}/${contentType}`);
|
|
146
|
+
return result;
|
|
147
|
+
});
|
|
148
|
+
app.get("/api/explorer/records/raw-collection", async (request) => {
|
|
149
|
+
const { entityName, collectionName } = request.query;
|
|
150
|
+
const name = collectionName || entityName;
|
|
151
|
+
return getRawCollection(str(entityName), str(name));
|
|
152
|
+
});
|
|
153
|
+
app.get("/api/explorer/records/raw-item", async (request, reply) => {
|
|
154
|
+
const { collectionName, recordId } = request.query;
|
|
155
|
+
const result = getRawItem(str(collectionName), str(recordId));
|
|
156
|
+
if (!result)
|
|
157
|
+
return send404(reply, `Raw item not found: ${collectionName}/${recordId}`);
|
|
158
|
+
return result;
|
|
159
|
+
});
|
|
160
|
+
app.get("/api/explorer/records/workspace", async (request) => {
|
|
161
|
+
return await getWorkspaceRecords(qp(request));
|
|
162
|
+
});
|
|
163
|
+
// ---------- E. Lists Subsystem ----------
|
|
164
|
+
app.get("/api/explorer/lists", async (request) => {
|
|
165
|
+
const { entityName } = request.query;
|
|
166
|
+
return { lists: listListDescriptors(entityName) };
|
|
167
|
+
});
|
|
168
|
+
app.get("/api/explorer/lists/:listId", async (request, reply) => {
|
|
169
|
+
const { listId } = request.params;
|
|
170
|
+
const desc = getListDescriptor(listId);
|
|
171
|
+
if (!desc)
|
|
172
|
+
return send404(reply, `List descriptor not found: ${listId}`);
|
|
173
|
+
return desc;
|
|
174
|
+
});
|
|
175
|
+
app.get("/api/explorer/lists/:listId/records", async (request, reply) => {
|
|
176
|
+
const { listId } = request.params;
|
|
177
|
+
const desc = getListDescriptor(listId);
|
|
178
|
+
if (!desc)
|
|
179
|
+
return send404(reply, `List descriptor not found: ${listId}`);
|
|
180
|
+
const result = await executeListDescriptor(desc, qp(request));
|
|
181
|
+
return result;
|
|
182
|
+
});
|
|
183
|
+
app.get("/api/explorer/lists/:listId/records-ws", async (request, reply) => {
|
|
184
|
+
const { listId } = request.params;
|
|
185
|
+
const desc = getListDescriptor(listId);
|
|
186
|
+
if (!desc)
|
|
187
|
+
return send404(reply, `List descriptor not found: ${listId}`);
|
|
188
|
+
const result = await executeListDescriptor(desc, qp(request));
|
|
189
|
+
return result;
|
|
190
|
+
});
|
|
191
|
+
app.get("/api/explorer/lists/suggest", async (request) => {
|
|
192
|
+
const { entityName } = request.query;
|
|
193
|
+
return suggestListExtensions(str(entityName));
|
|
194
|
+
});
|
|
195
|
+
app.post("/api/explorer/lists", async (request) => {
|
|
196
|
+
if (!metadataWritesEnabled()) {
|
|
197
|
+
return { ok: false, error: "Metadata writes are disabled" };
|
|
198
|
+
}
|
|
199
|
+
const body = (request.body || {});
|
|
200
|
+
const listId = body.listId || body.id;
|
|
201
|
+
if (!listId)
|
|
202
|
+
return { error: "listId required" };
|
|
203
|
+
store.setMeta(`lists/${listId}`, { ...body, listId });
|
|
204
|
+
return { ok: true, listId };
|
|
205
|
+
});
|
|
206
|
+
app.patch("/api/explorer/lists/:listId", async (request) => {
|
|
207
|
+
if (!metadataWritesEnabled()) {
|
|
208
|
+
return { ok: false, error: "Metadata writes are disabled" };
|
|
209
|
+
}
|
|
210
|
+
const { listId } = request.params;
|
|
211
|
+
const existing = getListDescriptor(listId) || {};
|
|
212
|
+
const updated = { ...existing, ...(request.body || {}), listId };
|
|
213
|
+
store.setMeta(`lists/${listId}`, updated);
|
|
214
|
+
return { ok: true, listId, descriptor: updated };
|
|
215
|
+
});
|
|
216
|
+
app.put("/api/explorer/lists/:listId/sort", async (request) => {
|
|
217
|
+
if (!metadataWritesEnabled()) {
|
|
218
|
+
return { ok: false, error: "Metadata writes are disabled" };
|
|
219
|
+
}
|
|
220
|
+
const { listId } = request.params;
|
|
221
|
+
const existing = getListDescriptor(listId) || {};
|
|
222
|
+
const updated = { ...existing, sort: (request.body || {}).sort, listId };
|
|
223
|
+
store.setMeta(`lists/${listId}`, updated);
|
|
224
|
+
return { ok: true, listId, descriptor: updated };
|
|
225
|
+
});
|
|
226
|
+
app.delete("/api/explorer/lists/:listId", async (request) => {
|
|
227
|
+
if (!metadataWritesEnabled()) {
|
|
228
|
+
return { ok: false, error: "Metadata writes are disabled" };
|
|
229
|
+
}
|
|
230
|
+
const { listId } = request.params;
|
|
231
|
+
store.deleteMeta(`lists/${listId}`);
|
|
232
|
+
await store.flushAll();
|
|
233
|
+
return { ok: true, deleted: listId };
|
|
234
|
+
});
|
|
235
|
+
// ---------- F. Narratives Engine ----------
|
|
236
|
+
app.get("/api/explorer/narratives", async () => {
|
|
237
|
+
return { narratives: getMergedNarratives() };
|
|
238
|
+
});
|
|
239
|
+
app.get("/api/explorer/narratives/:entity", async (request) => {
|
|
240
|
+
const { entity } = request.params;
|
|
241
|
+
return { entity, narratives: getMergedNarratives(entity) };
|
|
242
|
+
});
|
|
243
|
+
app.get("/api/explorer/narratives/:entity/raw", async (request) => {
|
|
244
|
+
const { entity } = request.params;
|
|
245
|
+
return { entity, raw: getRawNarratives(entity) };
|
|
246
|
+
});
|
|
247
|
+
app.get("/api/explorer/narratives/:entity/:key", async (request, reply) => {
|
|
248
|
+
const { entity, key } = request.params;
|
|
249
|
+
const merged = getMergedNarratives(entity);
|
|
250
|
+
const found = merged.find((n) => n.key === key);
|
|
251
|
+
if (!found)
|
|
252
|
+
return send404(reply, `Narrative not found: ${entity}/${key}`);
|
|
253
|
+
return found;
|
|
254
|
+
});
|
|
255
|
+
app.get("/api/explorer/narratives/:entity/:key/records", async (request) => {
|
|
256
|
+
const { entity, key } = request.params;
|
|
257
|
+
return { entity, key, records: queryNarrativeRecords(entity, key) };
|
|
258
|
+
});
|
|
259
|
+
app.post("/api/explorer/narratives", async (request) => {
|
|
260
|
+
if (!metadataWritesEnabled()) {
|
|
261
|
+
return { ok: false, error: "Metadata writes are disabled" };
|
|
262
|
+
}
|
|
263
|
+
const { entity, key, ...rest } = (request.body || {});
|
|
264
|
+
if (!entity || !key)
|
|
265
|
+
return { error: "entity and key required" };
|
|
266
|
+
const authored = getRawNarratives(entity);
|
|
267
|
+
authored[key] = { key, ...rest };
|
|
268
|
+
store.setMeta(`narratives/${entity}`, authored);
|
|
269
|
+
return { ok: true, entity, key };
|
|
270
|
+
});
|
|
271
|
+
app.patch("/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
|
+
authored[key] = { ...authored[key], ...(request.body || {}), key };
|
|
278
|
+
store.setMeta(`narratives/${entity}`, authored);
|
|
279
|
+
return { ok: true, entity, key, descriptor: authored[key] };
|
|
280
|
+
});
|
|
281
|
+
app.delete("/api/explorer/narratives/:entity/:key", async (request) => {
|
|
282
|
+
if (!metadataWritesEnabled()) {
|
|
283
|
+
return { ok: false, error: "Metadata writes are disabled" };
|
|
284
|
+
}
|
|
285
|
+
const { entity, key } = request.params;
|
|
286
|
+
const authored = getRawNarratives(entity);
|
|
287
|
+
delete authored[key];
|
|
288
|
+
store.setMeta(`narratives/${entity}`, authored);
|
|
289
|
+
return { ok: true, entity, key };
|
|
290
|
+
});
|
|
291
|
+
// ---------- G. Object Types & Root Property Catalog ----------
|
|
292
|
+
app.get("/api/explorer/object-types", async () => {
|
|
293
|
+
return { objectTypes: listObjectTypes().map((name) => store.getMeta(`object-types/${name}`)) };
|
|
294
|
+
});
|
|
295
|
+
app.get("/api/explorer/object-types/:name/root-property-catalog", async (request) => {
|
|
296
|
+
const { name } = request.params;
|
|
297
|
+
const meta = store.getMeta(`object-types/${name}`);
|
|
298
|
+
return {
|
|
299
|
+
objectType: name,
|
|
300
|
+
rootPropertyCatalog: meta?.rootPropertyCatalog || [],
|
|
301
|
+
};
|
|
302
|
+
});
|
|
303
|
+
app.post("/api/explorer/object-types/:name/root-property-catalog/compute", async (request) => {
|
|
304
|
+
const { name } = request.params;
|
|
305
|
+
const entries = computeRootPropertyCatalog(name);
|
|
306
|
+
return { objectType: name, computed: true, rootPropertyCatalog: entries };
|
|
307
|
+
});
|
|
308
|
+
// ---------- H. Record Write Engine ----------
|
|
309
|
+
app.post("/api/explorer/records/write", async (request, reply) => {
|
|
310
|
+
const { writeDescriptorId, operation = "add", input, records, dryRun = false, validateOnly = false, } = (request.body || {});
|
|
311
|
+
if (!writeDescriptorId)
|
|
312
|
+
return reply.code(400).send({ error: "writeDescriptorId required", ok: false });
|
|
313
|
+
if (!["add", "upsert", "patch", "replace", "delete"].includes(operation)) {
|
|
314
|
+
return reply.code(400).send({ ok: false, error: `Unsupported operation: ${operation}` });
|
|
315
|
+
}
|
|
316
|
+
const writeSpec = loadWriteDescriptor(writeDescriptorId);
|
|
317
|
+
const targetCollection = writeSpec.targetCollection;
|
|
318
|
+
if (!targetCollection)
|
|
319
|
+
return reply.code(400).send({ error: "writeDescriptor missing targetCollection", ok: false });
|
|
320
|
+
const data = input ?? records;
|
|
321
|
+
if (data === undefined || data === null) {
|
|
322
|
+
return reply.code(400).send({ ok: false, error: "input or records required" });
|
|
323
|
+
}
|
|
324
|
+
const validation = validateInputSchema(writeSpec, data, operation);
|
|
325
|
+
if (!validation.ok) {
|
|
326
|
+
return reply.code(400).send({ ok: false, validated: false, errors: validation.errors });
|
|
327
|
+
}
|
|
328
|
+
if (validateOnly)
|
|
329
|
+
return { ok: true, validated: true };
|
|
330
|
+
const results = executeOperation({
|
|
331
|
+
target: targetCollection,
|
|
332
|
+
op: operation,
|
|
333
|
+
data,
|
|
334
|
+
persist: !dryRun,
|
|
335
|
+
});
|
|
336
|
+
if (!dryRun) {
|
|
337
|
+
await store.flushAll();
|
|
338
|
+
}
|
|
339
|
+
return { ok: true, count: results.length, operation, dryRun, results };
|
|
340
|
+
});
|
|
341
|
+
app.delete("/api/explorer/records/item", async (request, reply) => {
|
|
342
|
+
const { entityName, recordId, writeDescriptorId } = (request.query || {});
|
|
343
|
+
if (!entityName || !recordId || !writeDescriptorId) {
|
|
344
|
+
return reply.code(400).send({
|
|
345
|
+
ok: false,
|
|
346
|
+
error: "entityName, recordId, and writeDescriptorId query params are required",
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
const writeSpec = loadWriteDescriptor(writeDescriptorId);
|
|
350
|
+
const expectedTarget = `${entityName}/snapshots`;
|
|
351
|
+
if (writeSpec.targetCollection !== expectedTarget) {
|
|
352
|
+
return reply.code(400).send({
|
|
353
|
+
ok: false,
|
|
354
|
+
error: `Write descriptor targets ${writeSpec.targetCollection}, not ${expectedTarget}`,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
const validation = validateInputSchema(writeSpec, { recordId }, "delete");
|
|
358
|
+
if (!validation.ok) {
|
|
359
|
+
return reply.code(400).send({ ok: false, validated: false, errors: validation.errors });
|
|
360
|
+
}
|
|
361
|
+
const removed = executeOperation({
|
|
362
|
+
target: writeSpec.targetCollection,
|
|
363
|
+
op: "delete",
|
|
364
|
+
data: { recordId },
|
|
365
|
+
});
|
|
366
|
+
await store.flushAll();
|
|
367
|
+
return { ok: true, count: removed.length, removed };
|
|
368
|
+
});
|
|
369
|
+
app.get("/api/explorer/object-types/:name", async (request, reply) => {
|
|
370
|
+
const { name } = request.params;
|
|
371
|
+
const meta = store.getMeta(`object-types/${name}`);
|
|
372
|
+
if (!meta)
|
|
373
|
+
return send404(reply, `Object type not found: ${name}`);
|
|
374
|
+
return meta;
|
|
375
|
+
});
|
|
376
|
+
// ---------- I. Agents Engine ----------
|
|
377
|
+
app.get("/api/explorer/agents", async () => {
|
|
378
|
+
const agents = store.getMeta("agents");
|
|
379
|
+
return agents || { agents: [] };
|
|
380
|
+
});
|
|
381
|
+
// ---- Authix / M2M Auth Shim ----
|
|
382
|
+
app.post("/api/explorer/auth/token", async () => ({
|
|
383
|
+
access_token: "mock-m2m-token",
|
|
384
|
+
token_type: "Bearer",
|
|
385
|
+
expires_in: 3600,
|
|
386
|
+
}));
|
|
387
|
+
// ---- Demo UI ----
|
|
388
|
+
// The demo app served at /demo uses the Explorer API exclusively for its
|
|
389
|
+
// CRUD (records/collection, records/write, DELETE records/item). No separate
|
|
390
|
+
// /api/state flow is needed.
|
|
391
|
+
app.get("/demo", async (_request, reply) => {
|
|
392
|
+
const demoPath = path.join(MOCKS_DIR, "demo.html");
|
|
393
|
+
if (!fs.existsSync(demoPath)) {
|
|
394
|
+
return reply.code(404).send({ ok: false, error: "demo.html not found" });
|
|
395
|
+
}
|
|
396
|
+
const html = fs.readFileSync(demoPath, "utf8");
|
|
397
|
+
return reply.type("text/html").send(html);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
function send404(reply, message) {
|
|
401
|
+
return reply.code(404).send({ ok: false, error: message });
|
|
402
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
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 const CLI_HELP = "Memorix Explorer static server\n\nUsage:\n static-memorix [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 static-memorix --port 5030\n";
|
|
9
|
+
export declare function getPackageVersion(): string;
|
|
10
|
+
export declare function parseCliArgs(args: string[]): StartServerOptions;
|
|
11
|
+
export declare function isCliEntrypoint(argvPath: string | undefined, moduleUrl: string): boolean;
|
|
12
|
+
/** Build a loaded Fastify instance without opening a network port. */
|
|
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>>;
|
|
14
|
+
/** Build and listen. Options override PORT/HOST environment configuration. */
|
|
15
|
+
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
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import Fastify from "fastify";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { store } from "./storage/InMemoryStore.js";
|
|
7
|
+
import { registerRoutes } from "./routes/index.js";
|
|
8
|
+
import { PORT, HOST, MOCKS_DIR } from "./config.js";
|
|
9
|
+
export const CLI_HELP = `Memorix Explorer static server
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
static-memorix [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
|
+
static-memorix --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
|
+
}
|
|
32
|
+
export function parseCliArgs(args) {
|
|
33
|
+
const options = {};
|
|
34
|
+
for (let index = 0; index < args.length; index++) {
|
|
35
|
+
const arg = args[index];
|
|
36
|
+
if (arg === "--port") {
|
|
37
|
+
const raw = args[++index];
|
|
38
|
+
const port = Number(raw);
|
|
39
|
+
if (!raw || !Number.isInteger(port) || port < 0 || port > 65535) {
|
|
40
|
+
throw Object.assign(new Error("--port must be an integer from 0 to 65535"), {
|
|
41
|
+
statusCode: 400,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
options.port = port;
|
|
45
|
+
}
|
|
46
|
+
else if (arg === "--host") {
|
|
47
|
+
const host = args[++index];
|
|
48
|
+
if (!host)
|
|
49
|
+
throw Object.assign(new Error("--host requires a value"), { statusCode: 400 });
|
|
50
|
+
options.host = host;
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
throw Object.assign(new Error(`Unknown CLI argument: ${arg}`), { statusCode: 400 });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return options;
|
|
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
|
+
}
|
|
68
|
+
/** Build a loaded Fastify instance without opening a network port. */
|
|
69
|
+
export async function buildServer(options = {}) {
|
|
70
|
+
const app = Fastify({ logger: options.logger ?? false });
|
|
71
|
+
// Global error handler matching predictable error shapes.
|
|
72
|
+
app.setErrorHandler((error, _request, reply) => {
|
|
73
|
+
const normalized = error instanceof Error
|
|
74
|
+
? error
|
|
75
|
+
: new Error("Internal Server Error");
|
|
76
|
+
const status = normalized.statusCode && normalized.statusCode >= 400
|
|
77
|
+
? normalized.statusCode
|
|
78
|
+
: 500;
|
|
79
|
+
reply.status(status).send({
|
|
80
|
+
ok: false,
|
|
81
|
+
error: normalized.message || "Internal Server Error",
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
await store.load();
|
|
85
|
+
await registerRoutes(app);
|
|
86
|
+
return app;
|
|
87
|
+
}
|
|
88
|
+
/** Build and listen. Options override PORT/HOST environment configuration. */
|
|
89
|
+
export async function startServer(options = {}) {
|
|
90
|
+
const app = await buildServer(options);
|
|
91
|
+
const port = options.port ?? PORT;
|
|
92
|
+
const host = options.host ?? HOST;
|
|
93
|
+
await app.listen({ port, host });
|
|
94
|
+
const address = app.server.address();
|
|
95
|
+
const boundPort = typeof address === "object" && address ? address.port : port;
|
|
96
|
+
const displayHost = host === "0.0.0.0" ? "localhost" : host;
|
|
97
|
+
// eslint-disable-next-line no-console
|
|
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`);
|
|
103
|
+
if (options.installSignalHandlers ?? true) {
|
|
104
|
+
const shutdown = async () => {
|
|
105
|
+
console.log("\n[static-memorix] flushing + shutting down");
|
|
106
|
+
await store.flushAll();
|
|
107
|
+
await app.close();
|
|
108
|
+
process.exit(0);
|
|
109
|
+
};
|
|
110
|
+
process.once("SIGINT", shutdown);
|
|
111
|
+
process.once("SIGTERM", shutdown);
|
|
112
|
+
}
|
|
113
|
+
return app;
|
|
114
|
+
}
|
|
115
|
+
const isCliEntry = isCliEntrypoint(process.argv[1], import.meta.url);
|
|
116
|
+
if (isCliEntry) {
|
|
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
|
+
});
|
|
130
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
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
|
+
/** Persist a routed tombstone so an unprefixed seed does not reappear. */
|
|
16
|
+
deleteMeta(key: string): void;
|
|
17
|
+
getMetaKeys(): string[];
|
|
18
|
+
getCollection(objectType: string, ct: ContentType): any[];
|
|
19
|
+
setCollection(objectType: string, ct: ContentType, data: any[]): void;
|
|
20
|
+
getCollectionKeys(): string[];
|
|
21
|
+
private scheduleCollectionFlush;
|
|
22
|
+
private scheduleMetaFlush;
|
|
23
|
+
flushCollectionNow(key: string): void;
|
|
24
|
+
flushMetaNow(key: string): void;
|
|
25
|
+
/**
|
|
26
|
+
* Flush every dirty file immediately and clear timers.
|
|
27
|
+
*/
|
|
28
|
+
flushAll(): Promise<void>;
|
|
29
|
+
get mocksDir(): string;
|
|
30
|
+
}
|
|
31
|
+
export declare const store: InMemoryStore;
|