@sqaoss/flowy 1.7.0 → 1.9.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/README.md +14 -0
- package/package.json +2 -1
- package/server/src/contract.test.ts +440 -0
- package/server/src/resolvers.test.ts +121 -1
- package/server/src/resolvers.ts +43 -8
- package/server/src/schema.ts +19 -1
- package/src/commands/approve.ts +2 -6
- package/src/commands/billing.test.ts +32 -0
- package/src/commands/billing.ts +4 -5
- package/src/commands/export.ts +8 -19
- package/src/commands/feature.ts +28 -48
- package/src/commands/import.ts +14 -24
- package/src/commands/init.ts +2 -5
- package/src/commands/key.test.ts +36 -1
- package/src/commands/key.ts +9 -9
- package/src/commands/project.ts +23 -43
- package/src/commands/search.ts +7 -13
- package/src/commands/setup.test.ts +48 -1
- package/src/commands/setup.ts +2 -10
- package/src/commands/status.ts +5 -8
- package/src/commands/task.ts +48 -87
- package/src/commands/tree.test.ts +89 -1
- package/src/commands/tree.ts +11 -8
- package/src/commands/whoami.test.ts +34 -1
- package/src/commands/whoami.ts +8 -8
- package/src/util/config.test.ts +168 -1
- package/src/util/config.ts +188 -18
- package/src/util/operations.test.ts +114 -0
- package/src/util/operations.ts +332 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical GraphQL operations the Flowy CLI sends.
|
|
3
|
+
*
|
|
4
|
+
* Every operation the CLI relies on lives here, copied verbatim from the
|
|
5
|
+
* command that issues it. Commands import these constants instead of inlining
|
|
6
|
+
* the query/mutation text, so there is a single source of truth for the
|
|
7
|
+
* contract the CLI expects a backend to satisfy.
|
|
8
|
+
*
|
|
9
|
+
* Two consumers share this module:
|
|
10
|
+
* 1. `src/commands/*.ts` — the runtime CLI commands.
|
|
11
|
+
* 2. `server/src/contract.test.ts` — the contract guard that executes each
|
|
12
|
+
* operation against the bundled local server and asserts it is satisfied.
|
|
13
|
+
*
|
|
14
|
+
* If the bundled local server (or the SaaS server) renames an operation, a
|
|
15
|
+
* field, or an argument the CLI uses, the contract test fails — catching drift
|
|
16
|
+
* before it ships. See `server/src/contract.test.ts` for the documented list
|
|
17
|
+
* of intentional local/SaaS divergences (SaaS-only `whoami`, `register`,
|
|
18
|
+
* `rotateApiKey`, `createCheckout`, `auditLog`, `ancestors`).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
// --- Nodes: read --------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/** project.ts `show`, feature.ts `show` — fetch a single node by id. */
|
|
24
|
+
export const GET_NODE = `query GetNode($id: String!) {
|
|
25
|
+
node(id: $id) {
|
|
26
|
+
id type title description status metadata createdAt updatedAt
|
|
27
|
+
}
|
|
28
|
+
}`
|
|
29
|
+
|
|
30
|
+
/** project.ts `show` — same shape as GET_NODE, distinct operation name. */
|
|
31
|
+
export const GET_PROJECT = `query GetProject($id: String!) {
|
|
32
|
+
node(id: $id) {
|
|
33
|
+
id type title description status metadata createdAt updatedAt
|
|
34
|
+
}
|
|
35
|
+
}`
|
|
36
|
+
|
|
37
|
+
/** project.ts `set` — list nodes of a type, minimal fields for name matching. */
|
|
38
|
+
export const LIST_PROJECTS_FOR_SET = `query ListProjects($type: String) {
|
|
39
|
+
nodes(type: $type) {
|
|
40
|
+
id title
|
|
41
|
+
}
|
|
42
|
+
}`
|
|
43
|
+
|
|
44
|
+
/** project.ts `list` — list nodes of a type with display fields. */
|
|
45
|
+
export const LIST_PROJECTS = `query ListProjects($type: String) {
|
|
46
|
+
nodes(type: $type) {
|
|
47
|
+
id type title description status createdAt updatedAt
|
|
48
|
+
}
|
|
49
|
+
}`
|
|
50
|
+
|
|
51
|
+
/** task.ts `list --all` — every node of a type. */
|
|
52
|
+
export const ALL_TASKS = `query AllTasks($type: String!) {
|
|
53
|
+
nodes(type: $type) {
|
|
54
|
+
id type title status createdAt
|
|
55
|
+
}
|
|
56
|
+
}`
|
|
57
|
+
|
|
58
|
+
/** feature.ts `list` — direct children via relation, full display fields. */
|
|
59
|
+
export const DESCENDANTS = `query Descendants($nodeId: String!, $relation: String, $maxDepth: Int) {
|
|
60
|
+
descendants(nodeId: $nodeId, relation: $relation, maxDepth: $maxDepth) {
|
|
61
|
+
id type title description status createdAt updatedAt
|
|
62
|
+
}
|
|
63
|
+
}`
|
|
64
|
+
|
|
65
|
+
/** feature.ts `set` — direct children via relation, brief fields for matching. */
|
|
66
|
+
export const DESCENDANTS_BRIEF = `query Descendants($nodeId: String!, $relation: String, $maxDepth: Int) {
|
|
67
|
+
descendants(nodeId: $nodeId, relation: $relation, maxDepth: $maxDepth) {
|
|
68
|
+
id type title status
|
|
69
|
+
}
|
|
70
|
+
}`
|
|
71
|
+
|
|
72
|
+
/** task.ts `list` (active feature) — children with status only. */
|
|
73
|
+
export const LIST_TASKS = `query ListTasks($nodeId: String!, $relation: String!, $maxDepth: Int) {
|
|
74
|
+
descendants(nodeId: $nodeId, relation: $relation, maxDepth: $maxDepth) {
|
|
75
|
+
id type title status createdAt
|
|
76
|
+
}
|
|
77
|
+
}`
|
|
78
|
+
|
|
79
|
+
/** tree.ts — subtree from any root, filtered to one relation (default part_of),
|
|
80
|
+
* each node annotated with parentId/depth/relation. */
|
|
81
|
+
export const SUBTREE = `query Subtree($nodeId: String!, $relation: String, $maxDepth: Int) {
|
|
82
|
+
subtree(nodeId: $nodeId, relation: $relation, maxDepth: $maxDepth) {
|
|
83
|
+
id type title status parentId depth relation
|
|
84
|
+
}
|
|
85
|
+
}`
|
|
86
|
+
|
|
87
|
+
/** task.ts `list --ready` — actionable tasks (server-side dependency logic). */
|
|
88
|
+
export const READY_TASKS = `query ReadyTasks($projectId: String) {
|
|
89
|
+
readyTasks(projectId: $projectId) {
|
|
90
|
+
id type title status createdAt
|
|
91
|
+
}
|
|
92
|
+
}`
|
|
93
|
+
|
|
94
|
+
/** task.ts `show` — node plus its incoming/outgoing `blocks` edges. */
|
|
95
|
+
export const SHOW_TASK = `query ShowTask($id: String!) {
|
|
96
|
+
node(id: $id) {
|
|
97
|
+
id type title description status metadata createdAt updatedAt
|
|
98
|
+
}
|
|
99
|
+
blockedBy: edges(nodeId: $id, relation: "blocks", direction: "incoming") {
|
|
100
|
+
id type title status
|
|
101
|
+
}
|
|
102
|
+
blocks: edges(nodeId: $id, relation: "blocks", direction: "outgoing") {
|
|
103
|
+
id type title status
|
|
104
|
+
}
|
|
105
|
+
}`
|
|
106
|
+
|
|
107
|
+
/** task.ts `deps` — incoming/outgoing `blocks` edges only. */
|
|
108
|
+
export const TASK_DEPS = `query TaskDeps($id: String!) {
|
|
109
|
+
blockedBy: edges(nodeId: $id, relation: "blocks", direction: "incoming") {
|
|
110
|
+
id type title status
|
|
111
|
+
}
|
|
112
|
+
blocks: edges(nodeId: $id, relation: "blocks", direction: "outgoing") {
|
|
113
|
+
id type title status
|
|
114
|
+
}
|
|
115
|
+
}`
|
|
116
|
+
|
|
117
|
+
/** search.ts — full-text search with optional type/status/limit filters. */
|
|
118
|
+
export const SEARCH = `query Search($query: String!, $type: String, $status: String, $limit: Int) {
|
|
119
|
+
search(query: $query, type: $type, status: $status, limit: $limit) {
|
|
120
|
+
id type title description status
|
|
121
|
+
}
|
|
122
|
+
}`
|
|
123
|
+
|
|
124
|
+
// --- Nodes: write -------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
/** project.ts `create`, init.ts — create a node by type/title. */
|
|
127
|
+
export const CREATE_PROJECT = `mutation CreateProject($type: String!, $title: String!) {
|
|
128
|
+
createNode(type: $type, title: $title) {
|
|
129
|
+
id type title description status metadata createdAt updatedAt
|
|
130
|
+
}
|
|
131
|
+
}`
|
|
132
|
+
|
|
133
|
+
/** feature.ts `create` — create a node with a description. */
|
|
134
|
+
export const CREATE_NODE = `mutation CreateNode($type: String!, $title: String!, $description: String) {
|
|
135
|
+
createNode(type: $type, title: $title, description: $description) {
|
|
136
|
+
id type title description status createdAt updatedAt
|
|
137
|
+
}
|
|
138
|
+
}`
|
|
139
|
+
|
|
140
|
+
/** task.ts `create` — create a task node. */
|
|
141
|
+
export const CREATE_TASK = `mutation CreateTask($type: String!, $title: String!, $description: String) {
|
|
142
|
+
createNode(type: $type, title: $title, description: $description) {
|
|
143
|
+
id type title description status createdAt
|
|
144
|
+
}
|
|
145
|
+
}`
|
|
146
|
+
|
|
147
|
+
/** project/feature/task `update` — title/description/metadata. */
|
|
148
|
+
export const UPDATE_NODE = `mutation UpdateNode($id: String!, $title: String, $description: String, $metadata: String) {
|
|
149
|
+
updateNode(id: $id, title: $title, description: $description, metadata: $metadata) {
|
|
150
|
+
id type title description status metadata createdAt updatedAt
|
|
151
|
+
}
|
|
152
|
+
}`
|
|
153
|
+
|
|
154
|
+
/** status.ts — status-only shorthand update. */
|
|
155
|
+
export const UPDATE_STATUS = `mutation UpdateStatus($id: String!, $status: String) {
|
|
156
|
+
updateNode(id: $id, status: $status) {
|
|
157
|
+
id type title status updatedAt
|
|
158
|
+
}
|
|
159
|
+
}`
|
|
160
|
+
|
|
161
|
+
/** approve.ts — promote a pending_review node to approved. */
|
|
162
|
+
export const APPROVE_NODE = `mutation ApproveNode($id: String!) {
|
|
163
|
+
approveNode(id: $id) { id type title status updatedAt }
|
|
164
|
+
}`
|
|
165
|
+
|
|
166
|
+
/** project/feature/task `delete` — remove a node (and its edges). */
|
|
167
|
+
export const DELETE_NODE = `mutation DeleteNode($id: String!) {
|
|
168
|
+
deleteNode(id: $id)
|
|
169
|
+
}`
|
|
170
|
+
|
|
171
|
+
// --- Edges --------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
/** feature.ts `create` — link feature under project. */
|
|
174
|
+
export const CREATE_EDGE = `mutation CreateEdge($sourceId: String!, $targetId: String!, $relation: String!) {
|
|
175
|
+
createEdge(sourceId: $sourceId, targetId: $targetId, relation: $relation) {
|
|
176
|
+
sourceId targetId relation createdAt
|
|
177
|
+
}
|
|
178
|
+
}`
|
|
179
|
+
|
|
180
|
+
/** task.ts `create` — link task under feature (no createdAt selected). */
|
|
181
|
+
export const LINK_TASK = `mutation LinkTask($sourceId: String!, $targetId: String!, $relation: String!) {
|
|
182
|
+
createEdge(sourceId: $sourceId, targetId: $targetId, relation: $relation) {
|
|
183
|
+
sourceId targetId relation
|
|
184
|
+
}
|
|
185
|
+
}`
|
|
186
|
+
|
|
187
|
+
/** task.ts `block` — create a `blocks` edge between two tasks. */
|
|
188
|
+
export const BLOCK_TASK = `mutation BlockTask($sourceId: String!, $targetId: String!, $relation: String!) {
|
|
189
|
+
createEdge(sourceId: $sourceId, targetId: $targetId, relation: $relation) {
|
|
190
|
+
sourceId targetId relation createdAt
|
|
191
|
+
}
|
|
192
|
+
}`
|
|
193
|
+
|
|
194
|
+
/** task.ts `unblock` — remove a `blocks` edge. */
|
|
195
|
+
export const UNBLOCK_TASK = `mutation UnblockTask($sourceId: String!, $targetId: String!, $relation: String!) {
|
|
196
|
+
removeEdge(sourceId: $sourceId, targetId: $targetId, relation: $relation)
|
|
197
|
+
}`
|
|
198
|
+
|
|
199
|
+
// --- Import / export ----------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
/** import.ts — read existing nodes of a type to dedup by client-key. */
|
|
202
|
+
export const IMPORT_EXISTING = `query ImportExisting($type: String) {
|
|
203
|
+
nodes(type: $type) { id type title metadata }
|
|
204
|
+
}`
|
|
205
|
+
|
|
206
|
+
/** import.ts — outgoing edges of a node, used to dedup edge creation. */
|
|
207
|
+
export const IMPORT_EDGES = `query ImportEdges($nodeId: String!, $relation: String!) {
|
|
208
|
+
edges(nodeId: $nodeId, relation: $relation, direction: "outgoing") { id }
|
|
209
|
+
}`
|
|
210
|
+
|
|
211
|
+
/** import.ts — create a node (full arg surface incl. status/metadata). */
|
|
212
|
+
export const IMPORT_CREATE = `mutation ImportCreate($type: String!, $title: String!, $description: String, $status: String, $metadata: String) {
|
|
213
|
+
createNode(type: $type, title: $title, description: $description, status: $status, metadata: $metadata) { id }
|
|
214
|
+
}`
|
|
215
|
+
|
|
216
|
+
/** import.ts — update a node (full arg surface incl. status/metadata). */
|
|
217
|
+
export const IMPORT_UPDATE = `mutation ImportUpdate($id: String!, $title: String, $description: String, $status: String, $metadata: String) {
|
|
218
|
+
updateNode(id: $id, title: $title, description: $description, status: $status, metadata: $metadata) { id }
|
|
219
|
+
}`
|
|
220
|
+
|
|
221
|
+
/** import.ts — create an edge during materialization. */
|
|
222
|
+
export const IMPORT_EDGE = `mutation ImportEdge($sourceId: String!, $targetId: String!, $relation: String!) {
|
|
223
|
+
createEdge(sourceId: $sourceId, targetId: $targetId, relation: $relation) { sourceId targetId relation }
|
|
224
|
+
}`
|
|
225
|
+
|
|
226
|
+
/** export.ts — fetch the root project node. */
|
|
227
|
+
export const EXPORT_PROJECT = `query ExportProject($id: String!) {
|
|
228
|
+
node(id: $id) { id type title description status metadata }
|
|
229
|
+
}`
|
|
230
|
+
|
|
231
|
+
/** export.ts — all descendants of the project for the dump. */
|
|
232
|
+
export const EXPORT_DESCENDANTS = `query ExportDescendants($nodeId: String!, $relation: String, $maxDepth: Int) {
|
|
233
|
+
descendants(nodeId: $nodeId, relation: $relation, maxDepth: $maxDepth) {
|
|
234
|
+
id type title description status metadata
|
|
235
|
+
}
|
|
236
|
+
}`
|
|
237
|
+
|
|
238
|
+
/** export.ts — outgoing edges of a node, read back through the edge model. */
|
|
239
|
+
export const EXPORT_EDGES = `query ExportEdges($nodeId: String!, $relation: String!) {
|
|
240
|
+
edges(nodeId: $nodeId, relation: $relation, direction: "outgoing") {
|
|
241
|
+
id metadata
|
|
242
|
+
}
|
|
243
|
+
}`
|
|
244
|
+
|
|
245
|
+
// --- SaaS-only operations (NOT served by the bundled local server) ------------
|
|
246
|
+
//
|
|
247
|
+
// These belong to the hosted `flowy-saas` backend (auth, billing, audit). The
|
|
248
|
+
// bundled local server intentionally does not implement them — see the
|
|
249
|
+
// divergence list in `server/src/contract.test.ts`. They are exported here so
|
|
250
|
+
// the CLI commands share the same single source of truth and the SaaS contract
|
|
251
|
+
// test (flowy-saas `test/helpers/cli-queries.ts`) can mirror them.
|
|
252
|
+
|
|
253
|
+
/** setup.ts remote — register a hosted account. */
|
|
254
|
+
export const REGISTER = `mutation Register($email: String!, $tier: String) {
|
|
255
|
+
register(email: $email, tier: $tier) {
|
|
256
|
+
user { id email tier createdAt graceEndsAt }
|
|
257
|
+
apiKey
|
|
258
|
+
checkoutUrl
|
|
259
|
+
}
|
|
260
|
+
}`
|
|
261
|
+
|
|
262
|
+
/** whoami.ts — current hosted user. */
|
|
263
|
+
export const WHOAMI = `query Whoami {
|
|
264
|
+
whoami {
|
|
265
|
+
id email tier createdAt graceEndsAt
|
|
266
|
+
}
|
|
267
|
+
}`
|
|
268
|
+
|
|
269
|
+
/** key.ts — rotate the hosted API key. */
|
|
270
|
+
export const ROTATE_API_KEY = `mutation RotateApiKey {
|
|
271
|
+
rotateApiKey {
|
|
272
|
+
user { id email tier createdAt graceEndsAt }
|
|
273
|
+
apiKey
|
|
274
|
+
}
|
|
275
|
+
}`
|
|
276
|
+
|
|
277
|
+
/** billing.ts — create a checkout session for a tier. */
|
|
278
|
+
export const CREATE_CHECKOUT = `mutation CreateCheckout($tier: String!) {
|
|
279
|
+
createCheckout(tier: $tier) {
|
|
280
|
+
url
|
|
281
|
+
}
|
|
282
|
+
}`
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Operations the bundled local server is contractually required to satisfy.
|
|
286
|
+
* The contract test executes each of these against a live local server.
|
|
287
|
+
*/
|
|
288
|
+
export const LOCAL_CONTRACT_OPERATIONS = {
|
|
289
|
+
GET_NODE,
|
|
290
|
+
GET_PROJECT,
|
|
291
|
+
LIST_PROJECTS_FOR_SET,
|
|
292
|
+
LIST_PROJECTS,
|
|
293
|
+
ALL_TASKS,
|
|
294
|
+
DESCENDANTS,
|
|
295
|
+
DESCENDANTS_BRIEF,
|
|
296
|
+
LIST_TASKS,
|
|
297
|
+
SUBTREE,
|
|
298
|
+
READY_TASKS,
|
|
299
|
+
SHOW_TASK,
|
|
300
|
+
TASK_DEPS,
|
|
301
|
+
SEARCH,
|
|
302
|
+
CREATE_PROJECT,
|
|
303
|
+
CREATE_NODE,
|
|
304
|
+
CREATE_TASK,
|
|
305
|
+
UPDATE_NODE,
|
|
306
|
+
UPDATE_STATUS,
|
|
307
|
+
APPROVE_NODE,
|
|
308
|
+
DELETE_NODE,
|
|
309
|
+
CREATE_EDGE,
|
|
310
|
+
LINK_TASK,
|
|
311
|
+
BLOCK_TASK,
|
|
312
|
+
UNBLOCK_TASK,
|
|
313
|
+
IMPORT_EXISTING,
|
|
314
|
+
IMPORT_EDGES,
|
|
315
|
+
IMPORT_CREATE,
|
|
316
|
+
IMPORT_UPDATE,
|
|
317
|
+
IMPORT_EDGE,
|
|
318
|
+
EXPORT_PROJECT,
|
|
319
|
+
EXPORT_DESCENDANTS,
|
|
320
|
+
EXPORT_EDGES,
|
|
321
|
+
} as const
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* SaaS-only operations the bundled local server intentionally does NOT serve.
|
|
325
|
+
* Documented here so the divergence is explicit and discoverable.
|
|
326
|
+
*/
|
|
327
|
+
export const SAAS_ONLY_OPERATIONS = {
|
|
328
|
+
REGISTER,
|
|
329
|
+
WHOAMI,
|
|
330
|
+
ROTATE_API_KEY,
|
|
331
|
+
CREATE_CHECKOUT,
|
|
332
|
+
} as const
|