@x12i/static-memorix-explorer-api 1.0.0 → 1.1.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.
Files changed (48) hide show
  1. package/README.md +103 -12
  2. package/dist/engine/identity.js +11 -9
  3. package/dist/engine/write.js +48 -4
  4. package/dist/routes/index.js +57 -7
  5. package/dist/server.js +7 -2
  6. package/guides/demo-app.md +524 -0
  7. package/guides/managing-json-files.md +310 -0
  8. package/guides/running-the-service.md +136 -0
  9. package/guides/using-the-api.md +265 -0
  10. package/mocks/data/admin/snapshots.json +52 -0
  11. package/mocks/data/assets/analysis.json +14 -0
  12. package/mocks/data/assets/decisions.json +14 -0
  13. package/mocks/data/assets/events.json +4 -0
  14. package/mocks/data/assets/snapshots.json +49 -0
  15. package/mocks/data/findings/analysis.json +4 -0
  16. package/mocks/data/findings/decisions.json +4 -0
  17. package/mocks/data/findings/events.json +3 -0
  18. package/mocks/data/findings/snapshots.json +21 -0
  19. package/mocks/data/marketing/snapshots.json +75 -0
  20. package/mocks/data/memory/memory.json +31 -0
  21. package/mocks/data/product/snapshots.json +84 -0
  22. package/mocks/data/system/inventory.json +8 -0
  23. package/mocks/data/users/snapshots.json +5 -0
  24. package/mocks/demo.html +828 -0
  25. package/mocks/metadata/agents.json +7 -0
  26. package/mocks/metadata/lists/assets-default.json +22 -0
  27. package/mocks/metadata/lists/backlog.json +10 -0
  28. package/mocks/metadata/lists/findings-default.json +9 -0
  29. package/mocks/metadata/lists/my-plate.json +10 -0
  30. package/mocks/metadata/lists/this-week.json +10 -0
  31. package/mocks/metadata/narratives/admin.json +7 -0
  32. package/mocks/metadata/narratives/assets.json +17 -0
  33. package/mocks/metadata/narratives/findings.json +7 -0
  34. package/mocks/metadata/narratives/marketing.json +17 -0
  35. package/mocks/metadata/narratives/product.json +12 -0
  36. package/mocks/metadata/object-types/admin.json +6 -0
  37. package/mocks/metadata/object-types/assets.json +9 -0
  38. package/mocks/metadata/object-types/findings.json +8 -0
  39. package/mocks/metadata/object-types/marketing.json +6 -0
  40. package/mocks/metadata/object-types/memory.json +6 -0
  41. package/mocks/metadata/object-types/product.json +6 -0
  42. package/mocks/metadata/object-types/users.json +13 -0
  43. package/mocks/metadata/write-descriptors/admin-task-write.json +20 -0
  44. package/mocks/metadata/write-descriptors/assets-analysis-write.json +14 -0
  45. package/mocks/metadata/write-descriptors/marketing-task-write.json +20 -0
  46. package/mocks/metadata/write-descriptors/memory-write.json +14 -0
  47. package/mocks/metadata/write-descriptors/product-task-write.json +20 -0
  48. package/package.json +5 -2
package/README.md CHANGED
@@ -1,9 +1,13 @@
1
- # @x12i/mock-memorix-explorer-api
1
+ # @x12i/static-memorix-explorer-api
2
2
 
3
- In-memory mock server that provides **full API parity** for the Memorix Explorer
3
+ Static mock server that provides **full API parity** for the Memorix Explorer
4
4
  Fastify API. No MongoDB, Catalox, or Redis required — state lives in
5
5
  `./mocks/**/*.json` and is flushed to disk (debounced) on mutation.
6
6
 
7
+ Ships with a **complete reference demo app** (FlowState — a small task
8
+ manager) at `GET /demo` that exercises every read/write path in the API.
9
+ See [guides/demo-app.md](./guides/demo-app.md) for the full walkthrough.
10
+
7
11
  ## Quick start
8
12
 
9
13
  ```bash
@@ -11,6 +15,9 @@ npm install
11
15
  npm run build
12
16
  npm start # listens on :4300 (PORT / HOST / MOCKS_DIR env-overridable)
13
17
  # dev (no build): npm run dev
18
+
19
+ # open the demo UI in your browser
20
+ open http://localhost:4300/demo
14
21
  ```
15
22
 
16
23
  Health:
@@ -23,15 +30,15 @@ curl "localhost:4300/api/explorer/health?includeInventory=1"
23
30
  ## Architecture
24
31
 
25
32
  ```
26
- Fastify Router (/api/explorer/* + /health + Authix/M2M shim)
33
+ Fastify Router (/api/explorer/* + /health + Authix/M2M shim + /demo)
27
34
 
28
35
  Query & Interceptor Engine (mingo + narrative virtual filters)
29
- ┌────────────┬──────────────────┬──────────────┐
36
+ ┌────────────┬──────────────────┬──────────────┐
30
37
  Inventory/Lenses Snapshots/Aliases Records Writer
31
- └────────────┴──────────────────┴──────────────┘
32
- InMemoryStore (state matrix)
33
- │ debounced disk flush
34
- ./mocks/metadata & ./mocks/data
38
+ └────────────┴──────────────────┴──────────────┘
39
+ InMemoryStore (state matrix)
40
+ │ debounced disk flush
41
+ ./mocks/metadata & ./mocks/data
35
42
  ```
36
43
 
37
44
  ## Routes
@@ -41,13 +48,14 @@ Inventory/Lenses Snapshots/Aliases Records Writer
41
48
  | Health | `GET /health`, `GET /api/explorer/health?includeInventory=1` |
42
49
  | Inventory | `/inventory/collections`, `/summary`, `/issues`, `/graph` (query `sourceLens=db-first\|catalox-first`) |
43
50
  | Snapshots | `/snapshots/:objectType/:recordId[.../associated[/:propertyName]]`, `/snapshots/:objectType/associated-properties`, `/associations/plan\|apply\|verify` |
44
- | Records | `/records/collection`, `/full`, `/item`, `/content`, `/raw-collection`, `/raw-item`, `/workspace` |
51
+ | Records | `/records/collection`, `/full`, `/item`, `/content`, `/raw-collection`, `/raw-item`, `/workspace`, `POST /records/write`, `DELETE /records/item` |
45
52
  | Lists | `/lists`, `/lists/:listId`, `/lists/:listId/records`, `/lists/suggest`, mutations `POST/PATCH/PUT/DELETE` |
46
53
  | Narratives | `/narratives`, `/:entity`, `/:entity/raw`, `/:entity/:key`, `/:entity/:key/records`, mutations `POST/PATCH/DELETE` |
47
54
  | Object Types | `/object-types`, `/:name`, `/:name/root-property-catalog`, `POST .../compute` |
48
- | Write | `POST /records/write` (validate / dryRun / upsert / patch / replace) |
55
+ | Write | `POST /records/write` (validate / dryRun / add / upsert / patch / replace / **delete**) |
49
56
  | Agents | `GET /agents` |
50
57
  | Auth shim | `POST /api/explorer/auth/token` |
58
+ | Demo | `GET /demo` — serves `mocks/demo.html` |
51
59
 
52
60
  ## Query conventions
53
61
 
@@ -79,6 +87,54 @@ Client `associated.data|discovery|analysis|{custom}` maps to storage
79
87
  associated{Custom}`. Raw `associated*` fields are stripped from the returned
80
88
  document and recomposed into a normalized `associated` bucket.
81
89
 
90
+ ## Write engine
91
+
92
+ `POST /api/explorer/records/write` accepts an `operation` and routes the
93
+ payload through the write descriptor's JSON schema.
94
+
95
+ ### Operations
96
+
97
+ | Operation | Behavior |
98
+ |-----------|----------|
99
+ | `add` | Append. Auto-generates an identity if missing. |
100
+ | `upsert` | Update existing by identity key, or append. |
101
+ | `patch` | Update existing by identity key. No-op if missing. |
102
+ | `replace` | Replace the entire collection (use with caution). |
103
+ | `delete` | Remove every record matching the supplied identity keys. Returns the removed records. |
104
+
105
+ ### Required-field semantics
106
+
107
+ The `schema.required` array is enforced **fully** for `add` and `replace`.
108
+ For `upsert`, `patch`, and `delete`, only the identity field (`recordId`,
109
+ or `memoryId` on the memory tier) is required. This allows partial updates
110
+ like `{recordId, status}` without resupplying every required field — the
111
+ pattern the demo UI uses for every inline edit.
112
+
113
+ ### REST-style delete
114
+
115
+ For ergonomics, you can also delete a single record with:
116
+
117
+ ```
118
+ DELETE /api/explorer/records/item?entityName=product&recordId=t1&writeDescriptorId=product-task-write
119
+ ```
120
+
121
+ Returns `{ ok: true, count: <n>, removed: [<record>…] }` and flushes immediately.
122
+
123
+ ## Demo app
124
+
125
+ The `mocks/demo.html` reference UI (served at `GET /demo`) is a complete task
126
+ manager that uses the Explorer API for every read and mutation. It demonstrates:
127
+
128
+ - Multi-collection reads (3 parallel `records/collection` calls + a `users` collection)
129
+ - Filter / search / sort via query params
130
+ - **Create** via modal → `POST /records/write op=add`
131
+ - **Edit** via inline pills/dropdowns → `POST /records/write op=upsert` (partial updates)
132
+ - **Delete** via button → `DELETE /records/item`
133
+ - Cross-collection **move** (delete on old area + add on new area)
134
+ - Optimistic UI with rollback on failure
135
+
136
+ Full implementation walkthrough: [guides/demo-app.md](./guides/demo-app.md).
137
+
82
138
  ## Environment
83
139
 
84
140
  | Var | Default | Notes |
@@ -99,7 +155,7 @@ document and recomposed into a normalized `associated` bucket.
99
155
  The mock server does not connect to MongoDB, but it **tricks the frontend** into
100
156
  thinking it is talking to the routed tenant databases.
101
157
 
102
- - The `GET /api/explorer/health` response now includes a dynamic `memorixDb`
158
+ - The `GET /api/explorer/health` response includes a dynamic `memorixDb`
103
159
  field constructed as `<orgId>-memorix-entities + <orgId>-memorix-events`.
104
160
  - The agent-scoped catalox DB is reflected in the `cataloxDb` field as
105
161
  `<agentId>-memorix-catalox`.
@@ -118,6 +174,9 @@ The mock server enforces the following feature-flag gates with error responses:
118
174
  - Registry writes (if implemented) require
119
175
  `MEMORIX_EXPLORER_ENABLE_REGISTRY_WRITES=true`.
120
176
 
177
+ Record writes (`POST /records/write`, `DELETE /records/item`) are **not**
178
+ feature-flagged — they're always on, gated only by the write descriptor contract.
179
+
121
180
  When a flag is disabled or unset, the mock returns `{ ok: false, error: "..." }`
122
181
  matching the production Fastify error shape (typically 403/404).
123
182
 
@@ -134,4 +193,36 @@ and `knowledgeId`.
134
193
  - Records engine accepts `target=memory` and supports `memoryId` for fetching.
135
194
  - Write engine validates and persists `memoryId` on memory-tier records.
136
195
  - Startup validation (`MemorixRecordArraySchema` mock) allows `memoryId` without
137
- throwing schema errors.
196
+ throwing schema errors.
197
+
198
+ ## Documentation
199
+
200
+ - [guides/running-the-service.md](./guides/running-the-service.md) — installation, configuration, env vars
201
+ - [guides/using-the-api.md](./guides/using-the-api.md) — endpoint reference, query language, write operations
202
+ - [guides/managing-json-files.md](./guides/managing-json-files.md) — fixture structure, schemas, how the store works
203
+ - [guides/demo-app.md](./guides/demo-app.md) — FlowState reference UI walkthrough
204
+
205
+ ## Release history
206
+
207
+ ### v1.1.0
208
+ - **Added**: `delete` operation to the write engine
209
+ (`POST /records/write` body `operation:"delete"`).
210
+ - **Added**: REST-style `DELETE /api/explorer/records/item` endpoint.
211
+ - **Added**: Operation-aware schema validation — `upsert`/`patch`/`delete`
212
+ only require the identity field, not every `required` field.
213
+ - **Added**: Complete demo application (`mocks/demo.html`) wired entirely
214
+ through the Explorer API — Create/Edit/Delete/Move with optimistic UI.
215
+ - **Added**: `users` object type + collection.
216
+ - **Added**: Per-area task write descriptors
217
+ (`product/marketing/admin-task-write`).
218
+ - **Added**: Per-area narratives and enriched task snapshots
219
+ (`notes`, `chat`, `assigneeId`).
220
+ - **Removed**: obsolete `/api/state` flow and `mocks/db.json`
221
+ (replaced by Explorer API writes).
222
+ - **Docs**: full demo walkthrough (`guides/demo-app.md`) and refreshed
223
+ endpoint tables.
224
+
225
+ ### v1.0.0
226
+ - Initial implementation of mock Memorix Explorer API.
227
+ - Health, inventory, snapshots, records, lists, narratives, object types,
228
+ write engine (add/upsert/patch/replace), agents, auth shim.
@@ -6,34 +6,36 @@
6
6
  export function resolveIdField(ct, sample) {
7
7
  if (ct === "memory")
8
8
  return "memoryId";
9
- if (ct === "events")
10
- return "eventId";
11
9
  if (sample) {
10
+ if (ct === "events" && "eventId" in sample)
11
+ return "eventId";
12
12
  if ("recordId" in sample)
13
13
  return "recordId";
14
14
  if ("entityId" in sample)
15
15
  return "entityId";
16
+ if ("eventId" in sample)
17
+ return "eventId";
16
18
  if ("id" in sample)
17
19
  return "id";
18
20
  }
21
+ if (ct === "events")
22
+ return "eventId";
19
23
  return "recordId";
20
24
  }
21
25
  /**
22
26
  * Mock of the production `MemorixRecordArraySchema`. Validates that each record
23
- * in a collection array carries exactly one recognized identity key. Previously
24
- * this threw at boot if a collection used `memoryId`; it now accepts it.
27
+ * in a collection array carries the collection's canonical identity key.
28
+ * Additional identity-shaped fields are allowed because they can represent
29
+ * relationships (for example a memory has `memoryId` and may reference an
30
+ * `entityId` or `eventId`).
25
31
  */
26
32
  export function validateRecordArray(objectType, ct, records) {
27
33
  const errors = [];
28
34
  const expected = resolveIdField(ct, records[0]);
29
35
  for (const rec of records) {
30
- const present = ["recordId", "entityId", "eventId", "knowledgeId", "memoryId"].filter((k) => rec?.[k] !== undefined);
31
- if (present.length === 0) {
36
+ if (rec?.[expected] === undefined) {
32
37
  errors.push(`${objectType}/${ct}: record missing identity key (expected ${expected})`);
33
38
  }
34
- else if (present.length > 1) {
35
- errors.push(`${objectType}/${ct}: record has multiple identity keys [${present.join(", ")}]`);
36
- }
37
39
  }
38
40
  return { ok: errors.length === 0, errors };
39
41
  }
@@ -13,15 +13,31 @@ export function loadWriteDescriptor(writeDescriptorId) {
13
13
  }
14
14
  /**
15
15
  * Validate input against the write descriptor schema (lightweight AJV-free check).
16
+ *
17
+ * `operation` controls how `required` is enforced:
18
+ * - `add` / `replace`: every field in `schema.required` must be present.
19
+ * - `upsert` / `patch` / `delete`: only the collection's identity field
20
+ * (e.g. `recordId`, `memoryId`) is required; other `required` entries are
21
+ * ignored because partial updates are allowed.
22
+ *
23
+ * Type checks on whatever fields ARE present always run.
16
24
  */
17
- export function validateInputSchema(writeSpec, input) {
25
+ export function validateInputSchema(writeSpec, input, operation = "add") {
18
26
  const errors = [];
19
27
  const schema = (writeSpec.schema || {});
20
28
  const payload = Array.isArray(input) ? input : input ? [input] : [];
29
+ const partial = operation === "upsert" || operation === "patch" || operation === "delete";
21
30
  if (schema.required && Array.isArray(schema.required)) {
31
+ // Determine the identity field for this descriptor based on targetCollection.
32
+ const targetCollection = writeSpec.targetCollection || "";
33
+ const [, ctRaw] = targetCollection.split("/");
34
+ const idField = ctRaw === "memory" ? "memoryId" : "recordId";
22
35
  for (const req of schema.required) {
23
36
  for (const item of payload) {
24
37
  if (item?.[req] === undefined) {
38
+ // In partial modes, only the identity field is mandatory.
39
+ if (partial && req !== idField)
40
+ continue;
25
41
  errors.push(`Missing required field: ${req}`);
26
42
  }
27
43
  }
@@ -30,7 +46,7 @@ export function validateInputSchema(writeSpec, input) {
30
46
  if (schema.properties) {
31
47
  for (const item of payload) {
32
48
  for (const [field, def] of Object.entries(schema.properties)) {
33
- if (item[field] === undefined)
49
+ if (item[field] === undefined || item[field] === null)
34
50
  continue;
35
51
  if (def.type) {
36
52
  const actualType = Array.isArray(item[field]) ? "array" : typeof item[field];
@@ -58,8 +74,11 @@ export function parseTargetCollection(target) {
58
74
  }
59
75
  export function executeOperation(params) {
60
76
  const { objectType, ct } = parseTargetCollection(params.target);
61
- const collection = store.getCollection(objectType, ct);
77
+ // Work on a copy so dry runs cannot leak into the live in-memory store and
78
+ // later get persisted by an unrelated write.
79
+ const collection = [...store.getCollection(objectType, ct)];
62
80
  const payload = Array.isArray(params.data) ? params.data : [params.data];
81
+ const persist = params.persist !== false;
63
82
  // Resolve the identity field for this collection tier.
64
83
  // Memory tier uses memoryId; everything else defaults to recordId.
65
84
  const idField = ct === "memory" ? "memoryId" : "recordId";
@@ -95,10 +114,35 @@ export function executeOperation(params) {
95
114
  break;
96
115
  }
97
116
  case "replace": {
117
+ if (persist)
118
+ store.setCollection(objectType, ct, payload);
98
119
  return payload;
99
120
  }
121
+ case "delete": {
122
+ const removed = [];
123
+ for (const item of payload) {
124
+ const id = item[idField] ?? item.recordId ?? item.id;
125
+ if (id === undefined)
126
+ continue;
127
+ let i = collection.length;
128
+ while (i--) {
129
+ const candidate = collection[i];
130
+ if ((candidate[idField] ?? candidate.recordId ?? candidate.id) === id) {
131
+ removed.push(collection.splice(i, 1)[0]);
132
+ }
133
+ }
134
+ }
135
+ if (persist)
136
+ store.setCollection(objectType, ct, collection);
137
+ return removed;
138
+ }
139
+ default:
140
+ throw Object.assign(new Error(`Unsupported write operation: ${params.op}`), {
141
+ statusCode: 400,
142
+ });
100
143
  }
101
- store.setCollection(objectType, ct, collection);
144
+ if (persist)
145
+ store.setCollection(objectType, ct, collection);
102
146
  return payload;
103
147
  }
104
148
  function getNextId(collection, idField = "recordId") {
@@ -10,6 +10,9 @@ import { getMergedNarratives, getRawNarratives, queryNarrativeRecords, } from ".
10
10
  import { computeRootPropertyCatalog } from "../engine/objectTypes.js";
11
11
  import { loadWriteDescriptor, validateInputSchema, executeOperation, } from "../engine/write.js";
12
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";
13
16
  export async function registerRoutes(app) {
14
17
  // ---------- A. Health & System Diagnostics ----------
15
18
  app.get("/health", async () => ({ ok: true }));
@@ -296,18 +299,24 @@ export async function registerRoutes(app) {
296
299
  return { objectType: name, computed: true, rootPropertyCatalog: entries };
297
300
  });
298
301
  // ---------- H. Record Write Engine ----------
299
- app.post("/api/explorer/records/write", async (request) => {
302
+ app.post("/api/explorer/records/write", async (request, reply) => {
300
303
  const { writeDescriptorId, operation = "add", input, records, dryRun = false, validateOnly = false, } = (request.body || {});
301
304
  if (!writeDescriptorId)
302
- return { error: "writeDescriptorId required", ok: false };
305
+ return reply.code(400).send({ error: "writeDescriptorId required", ok: false });
306
+ if (!["add", "upsert", "patch", "replace", "delete"].includes(operation)) {
307
+ return reply.code(400).send({ ok: false, error: `Unsupported operation: ${operation}` });
308
+ }
303
309
  const writeSpec = loadWriteDescriptor(writeDescriptorId);
304
310
  const targetCollection = writeSpec.targetCollection;
305
311
  if (!targetCollection)
306
- return { error: "writeDescriptor missing targetCollection", ok: false };
307
- const data = input || records;
308
- const validation = validateInputSchema(writeSpec, data);
312
+ return reply.code(400).send({ error: "writeDescriptor missing targetCollection", ok: false });
313
+ const data = input ?? records;
314
+ if (data === undefined || data === null) {
315
+ return reply.code(400).send({ ok: false, error: "input or records required" });
316
+ }
317
+ const validation = validateInputSchema(writeSpec, data, operation);
309
318
  if (!validation.ok) {
310
- return { ok: false, validated: false, errors: validation.errors };
319
+ return reply.code(400).send({ ok: false, validated: false, errors: validation.errors });
311
320
  }
312
321
  if (validateOnly)
313
322
  return { ok: true, validated: true };
@@ -315,11 +324,40 @@ export async function registerRoutes(app) {
315
324
  target: targetCollection,
316
325
  op: operation,
317
326
  data,
327
+ persist: !dryRun,
318
328
  });
319
329
  if (!dryRun) {
320
330
  await store.flushAll();
321
331
  }
322
- return { ok: true, count: results.length, dryRun };
332
+ return { ok: true, count: results.length, operation, dryRun, results };
333
+ });
334
+ app.delete("/api/explorer/records/item", async (request, reply) => {
335
+ const { entityName, recordId, writeDescriptorId } = (request.query || {});
336
+ if (!entityName || !recordId || !writeDescriptorId) {
337
+ return reply.code(400).send({
338
+ ok: false,
339
+ error: "entityName, recordId, and writeDescriptorId query params are required",
340
+ });
341
+ }
342
+ const writeSpec = loadWriteDescriptor(writeDescriptorId);
343
+ const expectedTarget = `${entityName}/snapshots`;
344
+ if (writeSpec.targetCollection !== expectedTarget) {
345
+ return reply.code(400).send({
346
+ ok: false,
347
+ error: `Write descriptor targets ${writeSpec.targetCollection}, not ${expectedTarget}`,
348
+ });
349
+ }
350
+ const validation = validateInputSchema(writeSpec, { recordId }, "delete");
351
+ if (!validation.ok) {
352
+ return reply.code(400).send({ ok: false, validated: false, errors: validation.errors });
353
+ }
354
+ const removed = executeOperation({
355
+ target: writeSpec.targetCollection,
356
+ op: "delete",
357
+ data: { recordId },
358
+ });
359
+ await store.flushAll();
360
+ return { ok: true, count: removed.length, removed };
323
361
  });
324
362
  app.get("/api/explorer/object-types/:name", async (request, reply) => {
325
363
  const { name } = request.params;
@@ -339,6 +377,18 @@ export async function registerRoutes(app) {
339
377
  token_type: "Bearer",
340
378
  expires_in: 3600,
341
379
  }));
380
+ // ---- Demo UI ----
381
+ // The demo app served at /demo uses the Explorer API exclusively for its
382
+ // CRUD (records/collection, records/write, DELETE records/item). No separate
383
+ // /api/state flow is needed.
384
+ app.get("/demo", async (_request, reply) => {
385
+ const demoPath = path.join(MOCKS_DIR, "demo.html");
386
+ if (!fs.existsSync(demoPath)) {
387
+ return reply.code(404).send({ ok: false, error: "demo.html not found" });
388
+ }
389
+ const html = fs.readFileSync(demoPath, "utf8");
390
+ return reply.type("text/html").send(html);
391
+ });
342
392
  }
343
393
  function send404(reply, message) {
344
394
  return reply.code(404).send({ ok: false, error: message });
package/dist/server.js CHANGED
@@ -7,10 +7,15 @@ async function main() {
7
7
  const app = Fastify({ logger: false });
8
8
  // Global error handler matching predictable error shapes.
9
9
  app.setErrorHandler((error, _request, reply) => {
10
- const status = error.statusCode && error.statusCode >= 400 ? error.statusCode : 500;
10
+ const normalized = error instanceof Error
11
+ ? error
12
+ : new Error("Internal Server Error");
13
+ const status = normalized.statusCode && normalized.statusCode >= 400
14
+ ? normalized.statusCode
15
+ : 500;
11
16
  reply.status(status).send({
12
17
  ok: false,
13
- error: error.message || "Internal Server Error",
18
+ error: normalized.message || "Internal Server Error",
14
19
  });
15
20
  });
16
21
  await store.load();