@medicine-wheel/app 0.5.8 → 0.5.9
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/app/api/nodes/route.ts +76 -8
- package/package.json +24 -24
package/app/api/nodes/route.ts
CHANGED
|
@@ -16,25 +16,93 @@ const NodeCreateSchema = z.object({
|
|
|
16
16
|
metadata: z.record(z.unknown()).optional(),
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* The query params `GET /api/nodes` understands. Anything else is a 400.
|
|
21
|
+
*
|
|
22
|
+
* Node `type` is a closed enum of six and cannot grow, so every artifact kind
|
|
23
|
+
* the ecosystem has invented — `chronicle_episode`, `structured_plan`,
|
|
24
|
+
* `service`, `stc_chart`, `product_goal` — got in through `metadata.kind`
|
|
25
|
+
* instead, and containment got in through `metadata.parent_id` (31 nodes point
|
|
26
|
+
* at `chronicle:miadi-chronicle` that way). Both were reachable only by
|
|
27
|
+
* fetching the whole graph and filtering client-side; forgewright's
|
|
28
|
+
* `chronicle/client.ts` and this repo's own `HttpStore.searchNodes` each do
|
|
29
|
+
* exactly that, the latter under the comment "Server has no search endpoint
|
|
30
|
+
* yet". So the wheel needed no schema change — it needed a way to ask.
|
|
31
|
+
*
|
|
32
|
+
* Query params rather than a scoped `/api/nodes/:id/children` route because the
|
|
33
|
+
* question consumers actually have is "artifacts of kind X belonging to episode
|
|
34
|
+
* Y", which is `kind` AND `parent_id` together; a scoped route would still need
|
|
35
|
+
* `?kind=` bolted onto it, and `type`/`direction` already set the query-param
|
|
36
|
+
* precedent on this same route.
|
|
37
|
+
*/
|
|
38
|
+
const NODE_FILTER_PARAMS = ["type", "direction", "kind", "parent_id"] as const;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A silently-ignored filter is the failure this route exists to prevent: a
|
|
42
|
+
* consumer that asks `?kinds=service` and gets a filtered-looking payload it
|
|
43
|
+
* never filtered has been lied to. An unknown param is therefore a 400 naming
|
|
44
|
+
* what is accepted, not a shrug.
|
|
45
|
+
*/
|
|
46
|
+
function rejectUnknownParams(searchParams: URLSearchParams) {
|
|
47
|
+
const unknown = [...new Set(searchParams.keys())].filter(
|
|
48
|
+
(key) => !(NODE_FILTER_PARAMS as readonly string[]).includes(key),
|
|
49
|
+
);
|
|
50
|
+
if (unknown.length === 0) return null;
|
|
51
|
+
|
|
52
|
+
return NextResponse.json(
|
|
53
|
+
{
|
|
54
|
+
error: `Unknown query parameter${unknown.length > 1 ? "s" : ""}: ${unknown.join(", ")} — nothing was filtered.`,
|
|
55
|
+
accepted: [...NODE_FILTER_PARAMS],
|
|
56
|
+
},
|
|
57
|
+
{ status: 400 },
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
19
61
|
export async function GET(request: Request) {
|
|
20
62
|
try {
|
|
21
63
|
const { searchParams } = new URL(request.url);
|
|
22
|
-
|
|
23
|
-
const
|
|
64
|
+
|
|
65
|
+
const rejection = rejectUnknownParams(searchParams);
|
|
66
|
+
if (rejection) return rejection;
|
|
67
|
+
|
|
68
|
+
// An empty value means "no filter" — forms submit "" for an unchosen
|
|
69
|
+
// option, the same reason POST preprocesses "" on `direction`.
|
|
70
|
+
const filters = Object.fromEntries(
|
|
71
|
+
NODE_FILTER_PARAMS.map((key) => [key, searchParams.get(key) || undefined]),
|
|
72
|
+
) as Partial<Record<(typeof NODE_FILTER_PARAMS)[number], string>>;
|
|
73
|
+
const filtering = Object.values(filters).some(Boolean);
|
|
24
74
|
|
|
25
75
|
const store = await createProvider();
|
|
26
|
-
let nodes = await store.getAllNodes();
|
|
27
76
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
77
|
+
// Unfiltered reads keep the provider's own default page size, so this route
|
|
78
|
+
// answers a bare GET exactly as it always has. A *filtered* read must see
|
|
79
|
+
// the whole store or it answers from a truncated window — `getAllNodes()`
|
|
80
|
+
// defaults to 100 and the live wheel already holds 76, so filtering after
|
|
81
|
+
// the default slice would start returning quietly incomplete sets rather
|
|
82
|
+
// than failing loudly.
|
|
83
|
+
let nodes = filtering
|
|
84
|
+
? await store.getAllNodes(Number.MAX_SAFE_INTEGER)
|
|
85
|
+
: await store.getAllNodes();
|
|
86
|
+
|
|
87
|
+
// Every supplied filter narrows (AND). Previously `type` and `direction`
|
|
88
|
+
// were `else if`, so `?type=x&direction=y` silently dropped the direction —
|
|
89
|
+
// the same silent-ignore defect the 400 above closes. Single-param requests
|
|
90
|
+
// are unaffected.
|
|
91
|
+
if (filters.type) nodes = nodes.filter((n) => n.type === filters.type);
|
|
92
|
+
if (filters.direction) nodes = nodes.filter((n) => n.direction === filters.direction);
|
|
93
|
+
if (filters.kind) nodes = nodes.filter((n) => n.metadata?.kind === filters.kind);
|
|
94
|
+
if (filters.parent_id) {
|
|
95
|
+
nodes = nodes.filter((n) => n.metadata?.parent_id === filters.parent_id);
|
|
32
96
|
}
|
|
33
97
|
|
|
34
98
|
return NextResponse.json({
|
|
35
99
|
nodes,
|
|
36
100
|
provider: detectProvider(),
|
|
37
|
-
count: nodes.length
|
|
101
|
+
count: nodes.length,
|
|
102
|
+
// Echoed only when asked for, so an unfiltered response stays byte-identical
|
|
103
|
+
// to what every existing consumer already parses — and so a caller can tell
|
|
104
|
+
// an empty result from an unapplied filter.
|
|
105
|
+
...(filtering ? { filters } : {}),
|
|
38
106
|
});
|
|
39
107
|
} catch (error: unknown) {
|
|
40
108
|
const message = error instanceof Error ? error.message : String(error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@medicine-wheel/app",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"description": "Medicine Wheel — Interactive visual layer for Indigenous relational research with Four Directions, ceremonies, and narrative arcs",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mw": "dist/cli/mw.js",
|
|
@@ -85,29 +85,29 @@
|
|
|
85
85
|
"release:major": "npm run version:major && npm run publish:all && npm run release:commit"
|
|
86
86
|
},
|
|
87
87
|
"dependencies": {
|
|
88
|
-
"@medicine-wheel/ceremonial-diary": "^0.5.
|
|
89
|
-
"@medicine-wheel/ceremony-protocol": "^0.5.
|
|
90
|
-
"@medicine-wheel/community-review": "^0.5.
|
|
91
|
-
"@medicine-wheel/consent-lifecycle": "^0.5.
|
|
92
|
-
"@medicine-wheel/creative-orientation": "^0.5.
|
|
93
|
-
"@medicine-wheel/data-store": "^0.5.
|
|
94
|
-
"@medicine-wheel/data-store-postgres": "^0.5.
|
|
95
|
-
"@medicine-wheel/fire-keeper": "^0.5.
|
|
96
|
-
"@medicine-wheel/github-ceremony": "^0.5.
|
|
97
|
-
"@medicine-wheel/graph-viz": "^0.5.
|
|
98
|
-
"@medicine-wheel/importance-unit": "^0.5.
|
|
99
|
-
"@medicine-wheel/mcp": "^4.5.
|
|
100
|
-
"@medicine-wheel/narrative-cluster": "^0.5.
|
|
101
|
-
"@medicine-wheel/narrative-engine": "^0.5.
|
|
102
|
-
"@medicine-wheel/ontology-core": "^0.5.
|
|
103
|
-
"@medicine-wheel/perception-layer": "^0.5.
|
|
104
|
-
"@medicine-wheel/prompt-decomposition": "^0.5.
|
|
105
|
-
"@medicine-wheel/relational-index": "^0.5.
|
|
106
|
-
"@medicine-wheel/relational-query": "^0.5.
|
|
107
|
-
"@medicine-wheel/session-reader": "^0.5.
|
|
108
|
-
"@medicine-wheel/storage-provider": "^0.5.
|
|
109
|
-
"@medicine-wheel/transformation-tracker": "^0.5.
|
|
110
|
-
"@medicine-wheel/ui-components": "^0.5.
|
|
88
|
+
"@medicine-wheel/ceremonial-diary": "^0.5.9",
|
|
89
|
+
"@medicine-wheel/ceremony-protocol": "^0.5.9",
|
|
90
|
+
"@medicine-wheel/community-review": "^0.5.9",
|
|
91
|
+
"@medicine-wheel/consent-lifecycle": "^0.5.9",
|
|
92
|
+
"@medicine-wheel/creative-orientation": "^0.5.9",
|
|
93
|
+
"@medicine-wheel/data-store": "^0.5.9",
|
|
94
|
+
"@medicine-wheel/data-store-postgres": "^0.5.9",
|
|
95
|
+
"@medicine-wheel/fire-keeper": "^0.5.9",
|
|
96
|
+
"@medicine-wheel/github-ceremony": "^0.5.9",
|
|
97
|
+
"@medicine-wheel/graph-viz": "^0.5.9",
|
|
98
|
+
"@medicine-wheel/importance-unit": "^0.5.9",
|
|
99
|
+
"@medicine-wheel/mcp": "^4.5.9",
|
|
100
|
+
"@medicine-wheel/narrative-cluster": "^0.5.9",
|
|
101
|
+
"@medicine-wheel/narrative-engine": "^0.5.9",
|
|
102
|
+
"@medicine-wheel/ontology-core": "^0.5.9",
|
|
103
|
+
"@medicine-wheel/perception-layer": "^0.5.9",
|
|
104
|
+
"@medicine-wheel/prompt-decomposition": "^0.5.9",
|
|
105
|
+
"@medicine-wheel/relational-index": "^0.5.9",
|
|
106
|
+
"@medicine-wheel/relational-query": "^0.5.9",
|
|
107
|
+
"@medicine-wheel/session-reader": "^0.5.9",
|
|
108
|
+
"@medicine-wheel/storage-provider": "^0.5.9",
|
|
109
|
+
"@medicine-wheel/transformation-tracker": "^0.5.9",
|
|
110
|
+
"@medicine-wheel/ui-components": "^0.5.9",
|
|
111
111
|
"@neondatabase/serverless": "^0.10.0",
|
|
112
112
|
"@xyflow/react": "^12.3.0",
|
|
113
113
|
"clsx": "^2.1.1",
|