@x12i/static-memorix-explorer-api 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -16
- package/dist/config.js +1 -1
- package/dist/engine/identity.js +11 -9
- package/dist/engine/write.js +48 -4
- package/dist/routes/index.js +57 -7
- package/dist/server.js +42 -20
- package/guides/demo-app.md +524 -0
- package/guides/managing-json-files.md +310 -0
- package/guides/running-the-service.md +171 -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 +5 -2
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
# Demo App — FlowState
|
|
2
|
+
|
|
3
|
+
This repository ships with a complete reference demo application — **FlowState**,
|
|
4
|
+
a small task manager — that exercises the Memorix Explorer API end-to-end. The
|
|
5
|
+
demo is served at `GET /demo` and runs entirely in the browser with no build
|
|
6
|
+
step.
|
|
7
|
+
|
|
8
|
+
This document explains how every piece of the demo is implemented: the
|
|
9
|
+
metadata that defines the schema, the seed data the demo reads, and the way the
|
|
10
|
+
UI maps each user action (create / edit / delete / comment) onto Explorer API
|
|
11
|
+
calls.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## TL;DR
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm run build
|
|
19
|
+
npm start
|
|
20
|
+
# open http://localhost:5030/demo
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Every mutation you make in the UI is persisted back to `mocks/data/**/*.json`
|
|
24
|
+
through the Explorer API. Restart the server and your edits are still there.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## What the demo proves
|
|
29
|
+
|
|
30
|
+
The demo is intentionally small, but it touches every part of the Explorer API
|
|
31
|
+
that matters for a real frontend:
|
|
32
|
+
|
|
33
|
+
| Capability | Endpoint used |
|
|
34
|
+
| ----------------------- | -------------------------------------------------------- |
|
|
35
|
+
| List users | `GET /api/explorer/records/collection?entityName=users` |
|
|
36
|
+
| List tasks per area | `GET /api/explorer/records/collection?entityName=<area>`|
|
|
37
|
+
| Full-text search | `?searchText=` / `?q=` |
|
|
38
|
+
| Equality filters | `?filter=status:eq:Doing&filter=priority:eq:Urgent` |
|
|
39
|
+
| Create a task | `POST /api/explorer/records/write` (`operation=add`) |
|
|
40
|
+
| Update a task | `POST /api/explorer/records/write` (`operation=upsert`) |
|
|
41
|
+
| Delete a task | `DELETE /api/explorer/records/item` |
|
|
42
|
+
| Move a task between areas | `DELETE` on old collection + `POST …/write op=add` on new |
|
|
43
|
+
| Object-type discovery | `GET /api/explorer/object-types` (used for inventory) |
|
|
44
|
+
|
|
45
|
+
No private endpoints, no shortcuts: the demo is a normal API client.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Architecture overview
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
┌─────────────────────────── Browser (/demo) ────────────────────────────┐
|
|
53
|
+
│ │
|
|
54
|
+
│ FlowState UI ──► fetch() ──► /api/explorer/* (Explorer API) │
|
|
55
|
+
│ │
|
|
56
|
+
└────────────────────────────────────────────────────────────────────────┘
|
|
57
|
+
│
|
|
58
|
+
▼
|
|
59
|
+
┌──────────────────────── Fastify mock server ───────────────────────────┐
|
|
60
|
+
│ │
|
|
61
|
+
│ routes/index.ts │
|
|
62
|
+
│ ├── /demo serves mocks/demo.html │
|
|
63
|
+
│ ├── /records/collection reads ← store.getCollection │
|
|
64
|
+
│ ├── /records/write writes → store.setCollection │
|
|
65
|
+
│ └── /records/item (DELETE) writes → store.setCollection │
|
|
66
|
+
│ │
|
|
67
|
+
│ engine/write.ts validates + executes add/upsert/patch/delete │
|
|
68
|
+
│ engine/records.ts query pipeline (filter / sort / search / page) │
|
|
69
|
+
│ │
|
|
70
|
+
│ storage/InMemoryStore.ts in-memory + debounced disk flush │
|
|
71
|
+
│ │
|
|
72
|
+
└────────────────────────────────────────────────────────────────────────┘
|
|
73
|
+
│
|
|
74
|
+
▼
|
|
75
|
+
mocks/metadata/**/*.json
|
|
76
|
+
mocks/data/**/*.json
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The UI never talks to the filesystem directly. Every mutation goes through
|
|
80
|
+
`POST /api/explorer/records/write` (or `DELETE /api/explorer/records/item`),
|
|
81
|
+
which mutates the in-memory store, which debounces a flush back to the JSON
|
|
82
|
+
file on disk.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Metadata model
|
|
87
|
+
|
|
88
|
+
Metadata files (under `mocks/metadata/`) describe the **shape** of the system.
|
|
89
|
+
The demo contributes the following metadata.
|
|
90
|
+
|
|
91
|
+
### Object types — `mocks/metadata/object-types/*.json`
|
|
92
|
+
|
|
93
|
+
Each area in the demo is its own object type:
|
|
94
|
+
|
|
95
|
+
```json
|
|
96
|
+
// mocks/metadata/object-types/product.json
|
|
97
|
+
{
|
|
98
|
+
"name": "product",
|
|
99
|
+
"summary": "Building the thing.",
|
|
100
|
+
"catalogRelations": [],
|
|
101
|
+
"rootPropertyCatalog": []
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The same shape is repeated for `marketing.json`, `admin.json`. Plus a `users`
|
|
106
|
+
object type so people are first-class queryable records:
|
|
107
|
+
|
|
108
|
+
```json
|
|
109
|
+
// mocks/metadata/object-types/users.json
|
|
110
|
+
{
|
|
111
|
+
"name": "users",
|
|
112
|
+
"summary": "People who can be assigned to tasks.",
|
|
113
|
+
"catalogRelations": [
|
|
114
|
+
{
|
|
115
|
+
"sourceProperty": "assigneeId",
|
|
116
|
+
"targetObject": "product",
|
|
117
|
+
"targetProperty": "recordId",
|
|
118
|
+
"relationType": "assigned-to"
|
|
119
|
+
}
|
|
120
|
+
],
|
|
121
|
+
"rootPropertyCatalog": []
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`catalogRelations` is what powers `GET /api/explorer/inventory/graph`.
|
|
126
|
+
|
|
127
|
+
### Narratives — `mocks/metadata/narratives/*.json`
|
|
128
|
+
|
|
129
|
+
Narratives are authored tags that get merged into the snapshot view. The demo
|
|
130
|
+
ships with a few per area:
|
|
131
|
+
|
|
132
|
+
```json
|
|
133
|
+
// mocks/metadata/narratives/product.json
|
|
134
|
+
{
|
|
135
|
+
"website-refresh": {
|
|
136
|
+
"key": "website-refresh",
|
|
137
|
+
"label": "Website Refresh",
|
|
138
|
+
"description": "Redesigning the public-facing site."
|
|
139
|
+
},
|
|
140
|
+
"launch-prep": {
|
|
141
|
+
"key": "launch-prep",
|
|
142
|
+
"label": "Launch Prep",
|
|
143
|
+
"description": "Getting ready for public launch."
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Records reference narratives via `doc.narratives["website-refresh"] = true`.
|
|
149
|
+
You can filter on them with the virtual filter syntax:
|
|
150
|
+
`?filter=narrativeId:eq:website-refresh`.
|
|
151
|
+
|
|
152
|
+
### Lists — `mocks/metadata/lists/*.json`
|
|
153
|
+
|
|
154
|
+
Saved queries. The demo ships with three:
|
|
155
|
+
|
|
156
|
+
- `my-plate.json` — sort by priority desc
|
|
157
|
+
- `this-week.json` — sort by dueDate asc
|
|
158
|
+
- `backlog.json` — default ordering
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
// mocks/metadata/lists/this-week.json
|
|
162
|
+
{
|
|
163
|
+
"id": "this-week",
|
|
164
|
+
"entity": "product",
|
|
165
|
+
"entityName": "product",
|
|
166
|
+
"baseContentType": "snapshots",
|
|
167
|
+
"fields": ["recordId", "title", "status", "priority", "owner", "dueDate"],
|
|
168
|
+
"sort": { "dueDate": 1 },
|
|
169
|
+
"query": {},
|
|
170
|
+
"listId": "this-week"
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
### Write descriptors — `mocks/metadata/write-descriptors/*.json`
|
|
175
|
+
|
|
176
|
+
This is the contract between the UI's mutation layer and the server's write
|
|
177
|
+
engine. The demo ships with one descriptor per area:
|
|
178
|
+
|
|
179
|
+
```json
|
|
180
|
+
// mocks/metadata/write-descriptors/product-task-write.json
|
|
181
|
+
{
|
|
182
|
+
"writeDescriptorId": "product-task-write",
|
|
183
|
+
"targetCollection": "product/snapshots",
|
|
184
|
+
"schema": {
|
|
185
|
+
"type": "object",
|
|
186
|
+
"required": ["recordId", "title"],
|
|
187
|
+
"properties": {
|
|
188
|
+
"recordId": { "type": "string" },
|
|
189
|
+
"title": { "type": "string" },
|
|
190
|
+
"status": { "type": "string" },
|
|
191
|
+
"priority": { "type": "string" },
|
|
192
|
+
"owner": { "type": "string" },
|
|
193
|
+
"area": { "type": "string" },
|
|
194
|
+
"assigneeId": { "type": "string" },
|
|
195
|
+
"dueDate": { "type": "string" },
|
|
196
|
+
"notes": { "type": "string" },
|
|
197
|
+
"chat": { "type": "array" }
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The same shape is mirrored for `marketing-task-write.json` and
|
|
204
|
+
`admin-task-write.json`. The `targetCollection` field tells the write engine
|
|
205
|
+
which `objectType/contentType` pair to mutate.
|
|
206
|
+
|
|
207
|
+
**Required-field semantics:** for `operation: "add"` every field in `required`
|
|
208
|
+
must be present. For `upsert`, `patch`, and `delete`, only the identity field
|
|
209
|
+
(`recordId` for tasks, `memoryId` for the memory tier) is mandatory — the rest
|
|
210
|
+
of the payload is treated as a partial update.
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Data model
|
|
215
|
+
|
|
216
|
+
Data files (under `mocks/data/`) hold the actual records.
|
|
217
|
+
|
|
218
|
+
### Users — `mocks/data/users/snapshots.json`
|
|
219
|
+
|
|
220
|
+
```json
|
|
221
|
+
[
|
|
222
|
+
{ "recordId": "usr-1", "name": "Ami", "role": "owner" },
|
|
223
|
+
{ "recordId": "usr-2", "name": "Devran", "role": "engineer" },
|
|
224
|
+
{ "recordId": "usr-3", "name": "Ilan", "role": "growth" }
|
|
225
|
+
]
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The demo loads these with:
|
|
229
|
+
|
|
230
|
+
```
|
|
231
|
+
GET /api/explorer/records/collection?entityName=users&limit=500
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
…and uses `recordId` as the foreign key on each task's `assigneeId`.
|
|
235
|
+
|
|
236
|
+
### Tasks — `mocks/data/{product,marketing,admin}/snapshots.json`
|
|
237
|
+
|
|
238
|
+
Each area is its own collection. A typical task:
|
|
239
|
+
|
|
240
|
+
```json
|
|
241
|
+
{
|
|
242
|
+
"recordId": "t1",
|
|
243
|
+
"title": "Review new frontend styling for the landing page",
|
|
244
|
+
"status": "doing",
|
|
245
|
+
"priority": "urgent",
|
|
246
|
+
"owner": "Sam",
|
|
247
|
+
"area": "product",
|
|
248
|
+
"assigneeId": "usr-2",
|
|
249
|
+
"dueDate": "2026-07-21",
|
|
250
|
+
"notes": "Ensure the dark-mode contrast ratios meet the updated accessibility guidelines.",
|
|
251
|
+
"chat": [
|
|
252
|
+
{
|
|
253
|
+
"id": "cmt-1",
|
|
254
|
+
"authorId": "usr-1",
|
|
255
|
+
"timestamp": "2026-07-19T14:22:00Z",
|
|
256
|
+
"text": "Can you add tests for the refresh token flow as well?"
|
|
257
|
+
}
|
|
258
|
+
],
|
|
259
|
+
"narratives": { "website-refresh": true }
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Notes and chat live **on the snapshot itself**. This keeps the data model
|
|
264
|
+
simple for the demo. In a richer deployment you could split chat into the
|
|
265
|
+
`events` content type and join via the list `extensions` mechanism.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## UI → API call map
|
|
270
|
+
|
|
271
|
+
Every interactive element in `mocks/demo.html` is wired to a specific API
|
|
272
|
+
call. The table below is the source of truth.
|
|
273
|
+
|
|
274
|
+
### Reads
|
|
275
|
+
|
|
276
|
+
| UI element | API call |
|
|
277
|
+
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
278
|
+
| Initial load | `GET /records/collection?entityName=users&limit=500` + `GET /records/collection?entityName=<area>&limit=500` (×3 areas, in parallel) |
|
|
279
|
+
| Search box (`/` to focus) | `GET /records/collection?entityName=<area>&searchText=<q>` |
|
|
280
|
+
| "To Do" / "Doing" / "Done" filter pills | `GET /records/collection?entityName=<area>&filter=status:eq:<Value>` |
|
|
281
|
+
| "Urgent" filter pill | `GET /records/collection?entityName=<area>&filter=priority:eq:Urgent` |
|
|
282
|
+
| Sidebar area / quick view selections | client-side filter on the already-loaded rows (no extra round trip) |
|
|
283
|
+
|
|
284
|
+
Because tasks span 3 collections, the demo loads all of them up front and
|
|
285
|
+
filters client-side. For larger datasets you would issue one `records/collection`
|
|
286
|
+
per navigation and rely on `?filter=` and `?limit=` server-side.
|
|
287
|
+
|
|
288
|
+
### Mutations
|
|
289
|
+
|
|
290
|
+
Every mutation is a single API call. The UI uses optimistic updates with
|
|
291
|
+
rollback on failure (the previous state is restored and a toast is shown).
|
|
292
|
+
|
|
293
|
+
| UI action | API call |
|
|
294
|
+
| ------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
295
|
+
| **+ New Task** (modal) | `POST /records/write` body: `{writeDescriptorId:"<area>-task-write", operation:"add", input:{…}}` |
|
|
296
|
+
| **Status pill** (Todo/Doing/Done/Someday) | `POST /records/write` body: `{operation:"upsert", input:{recordId, status}}` |
|
|
297
|
+
| **Priority pill** (Urgent/Normal/Low) | `POST /records/write` body: `{operation:"upsert", input:{recordId, priority}}` |
|
|
298
|
+
| **Area dropdown** (move task) | Descriptor-verified `DELETE /records/item` then `POST /records/write operation:add` with the new area |
|
|
299
|
+
| **Assignee dropdown** | `POST /records/write` body: `{operation:"upsert", input:{recordId, assigneeId}}` |
|
|
300
|
+
| **Due date input** | `POST /records/write` body: `{operation:"upsert", input:{recordId, dueDate}}` |
|
|
301
|
+
| **Notes textarea** (debounced) | `POST /records/write` body: `{operation:"upsert", input:{recordId, notes}}` |
|
|
302
|
+
| **Send comment** | `POST /records/write` body: `{operation:"upsert", input:{recordId, chat:[…]}}` (full chat array) |
|
|
303
|
+
| **Delete** button | `DELETE /records/item?entityName=<area>&recordId=<id>&writeDescriptorId=<area>-task-write` |
|
|
304
|
+
|
|
305
|
+
Two equivalent ways to delete exist:
|
|
306
|
+
|
|
307
|
+
1. **REST-style:** `DELETE /api/explorer/records/item?entityName=…&recordId=…&writeDescriptorId=…`
|
|
308
|
+
2. **Operation-style:** `POST /api/explorer/records/write` body:
|
|
309
|
+
`{writeDescriptorId:"…", operation:"delete", input:{recordId:"…"}}`
|
|
310
|
+
|
|
311
|
+
The demo uses #1 (the REST form) because it doesn't require knowing the
|
|
312
|
+
writeDescriptorId for the source collection.
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## Create flow (modal)
|
|
317
|
+
|
|
318
|
+
When the user clicks **+ New Task** (`Cmd/Ctrl-N` also works), a modal opens
|
|
319
|
+
with fields for title, area, status, priority, and assignee. On submit:
|
|
320
|
+
|
|
321
|
+
1. The UI generates a `recordId` of the form `t-<Date.now()>` to avoid
|
|
322
|
+
collisions with the seed data (`t1`…`t12`).
|
|
323
|
+
2. A `POST /api/explorer/records/write` is sent with `operation:"add"` and the
|
|
324
|
+
descriptor for the chosen area (`product-task-write`, etc.).
|
|
325
|
+
3. The server validates the payload against the descriptor's JSON schema.
|
|
326
|
+
4. On success, the record is appended to the in-memory collection and the
|
|
327
|
+
debounced flush writes it to `mocks/data/<area>/snapshots.json` within 300 ms.
|
|
328
|
+
5. The UI unshifts the returned record into `state.tasks`, selects it, and
|
|
329
|
+
re-renders.
|
|
330
|
+
|
|
331
|
+
On validation failure, the server returns
|
|
332
|
+
`{ok:false, validated:false, errors:[…]}` and the UI shows a toast.
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## Edit flow
|
|
337
|
+
|
|
338
|
+
Edit affordances are inline in the **Overview** tab of the detail panel:
|
|
339
|
+
|
|
340
|
+
- **Status** — four pill buttons (Todo / Doing / Done / Someday). Clicking one
|
|
341
|
+
immediately fires an `upsert` with `{recordId, status}`.
|
|
342
|
+
- **Priority** — three pill buttons (Urgent / Normal / Low). Same pattern.
|
|
343
|
+
- **Area** — dropdown. **Moving areas is a delete + add** (because each area
|
|
344
|
+
is a separate collection on disk). The UI sequences:
|
|
345
|
+
1. `DELETE /records/item?entityName=<old>&recordId=<id>&writeDescriptorId=<old>-task-write`
|
|
346
|
+
2. `POST /records/write` with `operation:"add"` on the new area's descriptor.
|
|
347
|
+
- **Assignee** — dropdown. `upsert` with `{recordId, assigneeId}`.
|
|
348
|
+
- **Due date** — date input. `upsert` with `{recordId, dueDate}`.
|
|
349
|
+
- **Notes** — textarea. Debounced 400 ms before sending `upsert`.
|
|
350
|
+
|
|
351
|
+
All edits are **optimistic**: the local state is updated immediately, the UI
|
|
352
|
+
re-renders, and the network call fires in the background. If the call fails,
|
|
353
|
+
the previous state is restored and a red toast is shown.
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Delete flow
|
|
358
|
+
|
|
359
|
+
The detail panel header has a **Delete** button. Clicking it shows a
|
|
360
|
+
confirmation dialog explaining which endpoint will be called. On confirm:
|
|
361
|
+
|
|
362
|
+
1. `DELETE /api/explorer/records/item?entityName=<area>&recordId=<id>&writeDescriptorId=<area>-task-write`
|
|
363
|
+
2. The server removes the record from the in-memory collection, returns the
|
|
364
|
+
removed document, and schedules a disk flush.
|
|
365
|
+
3. The UI removes the record from `state.tasks`, clears the selection, and
|
|
366
|
+
re-renders.
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## Comment flow
|
|
371
|
+
|
|
372
|
+
Comments live on the task's `chat` array. Adding a comment:
|
|
373
|
+
|
|
374
|
+
1. The UI appends `{id:"cmt-<ts>", authorId, timestamp, text}` to a **copy** of
|
|
375
|
+
the task's existing `chat` array.
|
|
376
|
+
2. Sends `POST /records/write` with `operation:"upsert"` and
|
|
377
|
+
`input:{recordId, chat:[…full array…]}`.
|
|
378
|
+
|
|
379
|
+
This rewrites the whole `chat` array per comment, which is fine for the demo's
|
|
380
|
+
scale. For higher volume you'd model comments as `events` records keyed by
|
|
381
|
+
`eventId` and join them in via a list descriptor's `extensions` array.
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## Why each area is a separate collection
|
|
386
|
+
|
|
387
|
+
We could have put all tasks in one `tasks` collection with an `area` field.
|
|
388
|
+
The demo deliberately splits them across `product/snapshots.json`,
|
|
389
|
+
`marketing/snapshots.json`, and `admin/snapshots.json` to demonstrate:
|
|
390
|
+
|
|
391
|
+
1. **Multi-entity applications** — typical real apps have more than one object
|
|
392
|
+
type. Showing 3 object types is more representative than 1.
|
|
393
|
+
2. **Cross-collection navigation** — the demo's "All Tasks" view merges 3
|
|
394
|
+
parallel `records/collection` calls, the same pattern a real dashboard uses.
|
|
395
|
+
3. **Cross-collection moves** — moving a task between areas exercises
|
|
396
|
+
`DELETE` + `add`, which is what you'd do when an entity changes type.
|
|
397
|
+
4. **Per-object-type metadata** — each area can carry its own narratives,
|
|
398
|
+
catalog relations, and write descriptors. The demo ships per-area narratives
|
|
399
|
+
(`product.json`, `marketing.json`, `admin.json`) to show this.
|
|
400
|
+
|
|
401
|
+
If you want a single-collection layout, just put all tasks in one
|
|
402
|
+
`tasks/snapshots.json`, drop the area field, and update the demo's
|
|
403
|
+
`loadAllTasks()` to fetch from one entity.
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## Object identity
|
|
408
|
+
|
|
409
|
+
Every record carries exactly one identity key. For the demo, that's always
|
|
410
|
+
`recordId`. The mock server's `MemorixRecordArraySchema` validates this at
|
|
411
|
+
startup — see `engine/identity.ts`. If a record is missing its identity key,
|
|
412
|
+
the server logs a warning at boot but continues running.
|
|
413
|
+
|
|
414
|
+
The other supported identity keys are `entityId`, `eventId`, `knowledgeId`,
|
|
415
|
+
and `memoryId`. The memory tier (`mocks/data/memory/memory.json`) uses
|
|
416
|
+
`memoryId` and is the only place in the demo where `recordId` is not the key.
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## Persistence guarantees
|
|
421
|
+
|
|
422
|
+
- **In-memory state** is updated synchronously on every write.
|
|
423
|
+
- **Disk flush** is debounced by `DISK_FLUSH_DEBOUNCE_MS` (default 300 ms).
|
|
424
|
+
- **On `SIGINT` / `SIGTERM`**, the server flushes everything before exiting —
|
|
425
|
+
see `server.ts`.
|
|
426
|
+
- **Crash** (SIGKILL, panic) loses any writes that hadn't been flushed yet
|
|
427
|
+
(at most 300 ms of writes).
|
|
428
|
+
|
|
429
|
+
This matches the production semantics of a write-behind cache. For tests that
|
|
430
|
+
need to assert against on-disk JSON, wait > `DISK_FLUSH_DEBOUNCE_MS` after the
|
|
431
|
+
write call.
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## Feature flags
|
|
436
|
+
|
|
437
|
+
`POST /records/write` and `DELETE /records/item` are **not** gated behind a
|
|
438
|
+
feature flag — they're always on. This matches the production behavior where
|
|
439
|
+
record writes are gated by the write-descriptor contract, not by an
|
|
440
|
+
environment toggle.
|
|
441
|
+
|
|
442
|
+
Mutations on **lists** and **narratives** (`POST/PATCH/PUT/DELETE` under
|
|
443
|
+
`/lists` and `/narratives`) are gated behind `MEMORIX_EXPLORER_ENABLE_METADATA_WRITES=true`.
|
|
444
|
+
The demo does not exercise those endpoints.
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## Verifying it works
|
|
449
|
+
|
|
450
|
+
A 30-second smoke test, runnable from the repo root:
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
# 1. Start the server
|
|
454
|
+
npm run build && npm start &
|
|
455
|
+
|
|
456
|
+
# 2. Read
|
|
457
|
+
curl -s "localhost:5030/api/explorer/records/collection?entityName=users&limit=10"
|
|
458
|
+
|
|
459
|
+
# 3. Create
|
|
460
|
+
curl -s -X POST localhost:5030/api/explorer/records/write \
|
|
461
|
+
-H "Content-Type: application/json" \
|
|
462
|
+
-d '{"writeDescriptorId":"product-task-write","operation":"add","input":{"recordId":"t-smoke-1","title":"Smoke","area":"product","status":"todo","priority":"normal"}}'
|
|
463
|
+
|
|
464
|
+
# 4. Update (partial)
|
|
465
|
+
curl -s -X POST localhost:5030/api/explorer/records/write \
|
|
466
|
+
-H "Content-Type: application/json" \
|
|
467
|
+
-d '{"writeDescriptorId":"product-task-write","operation":"upsert","input":{"recordId":"t-smoke-1","status":"doing"}}'
|
|
468
|
+
|
|
469
|
+
# 5. Delete
|
|
470
|
+
curl -s -X DELETE "localhost:5030/api/explorer/records/item?entityName=product&recordId=t-smoke-1&writeDescriptorId=product-task-write"
|
|
471
|
+
|
|
472
|
+
# 6. Open the UI
|
|
473
|
+
open http://localhost:5030/demo
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## Extending the demo
|
|
479
|
+
|
|
480
|
+
Common modifications:
|
|
481
|
+
|
|
482
|
+
- **Add a new area** — create `mocks/metadata/object-types/<area>.json`,
|
|
483
|
+
`mocks/metadata/write-descriptors/<area>-task-write.json`,
|
|
484
|
+
`mocks/data/<area>/snapshots.json`, then add the area key to the `AREAS` map
|
|
485
|
+
at the top of `mocks/demo.html`.
|
|
486
|
+
- **Add a new task field** — extend the write descriptor's `schema.properties`,
|
|
487
|
+
then render a new input in `renderDetail()` and send it via `updateTask()`.
|
|
488
|
+
- **Switch to per-view server-side pagination** — replace the demo's
|
|
489
|
+
`loadAllTasks()` (3 parallel fetches) with a single fetch per navigation that
|
|
490
|
+
uses `?filter=`, `?limit=`, and `?offset=`.
|
|
491
|
+
- **Add a list descriptor** — create a JSON file under
|
|
492
|
+
`mocks/metadata/lists/`, then call
|
|
493
|
+
`GET /api/explorer/lists/<listId>/records` from the UI.
|
|
494
|
+
|
|
495
|
+
---
|
|
496
|
+
|
|
497
|
+
## File map
|
|
498
|
+
|
|
499
|
+
```
|
|
500
|
+
mocks/
|
|
501
|
+
├── demo.html ← the UI
|
|
502
|
+
├── metadata/
|
|
503
|
+
│ ├── object-types/
|
|
504
|
+
│ │ ├── product.json marketing.json admin.json
|
|
505
|
+
│ │ └── users.json ← demo users (assignable)
|
|
506
|
+
│ ├── narratives/
|
|
507
|
+
│ │ ├── product.json marketing.json admin.json
|
|
508
|
+
│ ├── lists/
|
|
509
|
+
│ │ ├── my-plate.json this-week.json backlog.json
|
|
510
|
+
│ ├── write-descriptors/
|
|
511
|
+
│ │ ├── product-task-write.json
|
|
512
|
+
│ │ ├── marketing-task-write.json
|
|
513
|
+
│ │ └── admin-task-write.json
|
|
514
|
+
│ └── agents.json ← still here for the assets demo
|
|
515
|
+
└── data/
|
|
516
|
+
├── users/snapshots.json ← 3 seed users
|
|
517
|
+
├── product/snapshots.json ← 5 seed tasks
|
|
518
|
+
├── marketing/snapshots.json ← 4 seed tasks
|
|
519
|
+
└── admin/snapshots.json ← 3 seed tasks
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
The `assets/`, `findings/`, and `memory/` collections are not used by the
|
|
523
|
+
demo UI; they exist to keep the Explorer API parity complete and to support
|
|
524
|
+
the smoke tests in the guides.
|