@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.
@@ -19,11 +19,29 @@ export const typeDefs = /* GraphQL */ `
19
19
  createdAt: String!
20
20
  }
21
21
 
22
+ # A node returned from a subtree traversal, annotated with how it was reached:
23
+ # the parent it descends from (parentId), how many edges down the root it sits
24
+ # (depth, root's direct children are depth 1), and the relation of the edge
25
+ # that links it to its parent.
26
+ type SubtreeNode {
27
+ id: String!
28
+ type: String!
29
+ title: String!
30
+ description: String
31
+ status: String!
32
+ metadata: String
33
+ createdAt: String!
34
+ updatedAt: String!
35
+ parentId: String!
36
+ depth: Int!
37
+ relation: String!
38
+ }
39
+
22
40
  type Query {
23
41
  node(id: String!): Node
24
42
  nodes(type: String): [Node!]!
25
43
  descendants(nodeId: String!, relation: String, maxDepth: Int): [Node!]!
26
- subtree(nodeId: String!, maxDepth: Int): [Node!]!
44
+ subtree(nodeId: String!, relation: String, maxDepth: Int): [SubtreeNode!]!
27
45
  edges(nodeId: String!, relation: String!, direction: String): [Node!]!
28
46
  readyTasks(projectId: String): [Node!]!
29
47
  search(query: String!, type: String, status: String, limit: Int): [Node!]!
@@ -1,18 +1,14 @@
1
1
  import { Command } from 'commander'
2
2
  import { graphql } from '../util/client.ts'
3
3
  import { output, outputError } from '../util/format.ts'
4
+ import { APPROVE_NODE } from '../util/operations.ts'
4
5
 
5
6
  export const approveCommand = new Command('approve')
6
7
  .description('Approve a node (must be in pending_review)')
7
8
  .argument('<id>', 'Node ID')
8
9
  .action(async (id: string) => {
9
10
  try {
10
- const data = await graphql<{ approveNode: unknown }>(
11
- `mutation ApproveNode($id: String!) {
12
- approveNode(id: $id) { id type title status updatedAt }
13
- }`,
14
- { id },
15
- )
11
+ const data = await graphql<{ approveNode: unknown }>(APPROVE_NODE, { id })
16
12
  output(data.approveNode)
17
13
  } catch (error) {
18
14
  outputError(error)
@@ -3,11 +3,13 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
3
3
  let mockGraphql: ReturnType<typeof vi.fn>
4
4
  let mockOutput: ReturnType<typeof vi.fn>
5
5
  let mockOutputError: ReturnType<typeof vi.fn>
6
+ let mockMode: 'local' | 'remote'
6
7
 
7
8
  beforeEach(() => {
8
9
  mockGraphql = vi.fn()
9
10
  mockOutput = vi.fn()
10
11
  mockOutputError = vi.fn()
12
+ mockMode = 'remote'
11
13
 
12
14
  vi.doMock('../util/client.ts', () => ({
13
15
  graphql: mockGraphql,
@@ -17,6 +19,18 @@ beforeEach(() => {
17
19
  output: mockOutput,
18
20
  outputError: mockOutputError,
19
21
  }))
22
+
23
+ vi.doMock('../util/config.ts', () => ({
24
+ requireRemoteMode: (commandName: string) => {
25
+ if (mockMode === 'local') {
26
+ const err = new Error(
27
+ `"flowy ${commandName}" is only available in remote mode. The active mode is local mode.`,
28
+ ) as Error & { code?: string }
29
+ err.code = 'LOCAL_MODE'
30
+ throw err
31
+ }
32
+ },
33
+ }))
20
34
  })
21
35
 
22
36
  afterEach(() => {
@@ -94,4 +108,22 @@ describe('billing command', () => {
94
108
 
95
109
  expect(mockOutputError).toHaveBeenCalledWith(error)
96
110
  })
111
+
112
+ test('checkout errors cleanly in local mode without hitting the server', async () => {
113
+ mockMode = 'local'
114
+
115
+ const { billingCommand } = await import('./billing.ts')
116
+ await billingCommand.parseAsync(['checkout', '--tier', 'pro'], {
117
+ from: 'user',
118
+ })
119
+
120
+ expect(mockGraphql).not.toHaveBeenCalled()
121
+ expect(mockOutput).not.toHaveBeenCalled()
122
+ expect(mockOutputError).toHaveBeenCalledWith(
123
+ expect.objectContaining({
124
+ message: expect.stringMatching(/local mode/i),
125
+ code: 'LOCAL_MODE',
126
+ }),
127
+ )
128
+ })
97
129
  })
@@ -1,6 +1,8 @@
1
1
  import { Command, Option } from 'commander'
2
2
  import { graphql } from '../util/client.ts'
3
+ import { requireRemoteMode } from '../util/config.ts'
3
4
  import { output, outputError } from '../util/format.ts'
5
+ import { CREATE_CHECKOUT } from '../util/operations.ts'
4
6
 
5
7
  const checkoutCommand = new Command('checkout')
6
8
  .description('Create a checkout session for a subscription tier')
@@ -11,12 +13,9 @@ const checkoutCommand = new Command('checkout')
11
13
  )
12
14
  .action(async (opts: { tier: string }) => {
13
15
  try {
16
+ requireRemoteMode('billing checkout')
14
17
  const data = await graphql<{ createCheckout: { url: string } }>(
15
- `mutation CreateCheckout($tier: String!) {
16
- createCheckout(tier: $tier) {
17
- url
18
- }
19
- }`,
18
+ CREATE_CHECKOUT,
20
19
  { tier: opts.tier },
21
20
  )
22
21
  output(data.createCheckout)
@@ -12,6 +12,11 @@ import {
12
12
  serializeManifest,
13
13
  stripClientKey,
14
14
  } from '../util/manifest.ts'
15
+ import {
16
+ EXPORT_DESCENDANTS,
17
+ EXPORT_EDGES,
18
+ EXPORT_PROJECT,
19
+ } from '../util/operations.ts'
15
20
 
16
21
  /** Relations export captures from the real edge model. */
17
22
  const RELATIONS = ['part_of', 'blocks'] as const
@@ -25,22 +30,6 @@ interface ServerNode {
25
30
  metadata: string | null
26
31
  }
27
32
 
28
- const PROJECT_QUERY = `query ExportProject($id: String!) {
29
- node(id: $id) { id type title description status metadata }
30
- }`
31
-
32
- const DESCENDANTS_QUERY = `query ExportDescendants($nodeId: String!, $relation: String, $maxDepth: Int) {
33
- descendants(nodeId: $nodeId, relation: $relation, maxDepth: $maxDepth) {
34
- id type title description status metadata
35
- }
36
- }`
37
-
38
- const EDGES_QUERY = `query ExportEdges($nodeId: String!, $relation: String!) {
39
- edges(nodeId: $nodeId, relation: $relation, direction: "outgoing") {
40
- id metadata
41
- }
42
- }`
43
-
44
33
  export const exportCommand = new Command('export')
45
34
  .description(
46
35
  'Dump the active project (nodes + edges, with client-keys) as a manifest',
@@ -49,14 +38,14 @@ export const exportCommand = new Command('export')
49
38
  .action(async (outputPath: string | undefined) => {
50
39
  try {
51
40
  const project = requireProject()
52
- const root = await graphql<{ node: ServerNode | null }>(PROJECT_QUERY, {
41
+ const root = await graphql<{ node: ServerNode | null }>(EXPORT_PROJECT, {
53
42
  id: project.id,
54
43
  })
55
44
  if (!root.node) {
56
45
  throw new Error(`Active project ${project.id} not found.`)
57
46
  }
58
47
  const descendants = await graphql<{ descendants: ServerNode[] }>(
59
- DESCENDANTS_QUERY,
48
+ EXPORT_DESCENDANTS,
60
49
  { nodeId: project.id, relation: 'part_of', maxDepth: 100 },
61
50
  )
62
51
 
@@ -94,7 +83,7 @@ export const exportCommand = new Command('export')
94
83
  for (const relation of RELATIONS) {
95
84
  const data = await graphql<{
96
85
  edges: Array<{ id: string; metadata: string | null }>
97
- }>(EDGES_QUERY, { nodeId: sn.id, relation })
86
+ }>(EXPORT_EDGES, { nodeId: sn.id, relation })
98
87
  for (const target of data.edges) {
99
88
  const targetKey =
100
89
  keyById.get(target.id) ?? keyOf(target.id, target.metadata)
@@ -7,6 +7,15 @@ import {
7
7
  } from '../util/config.ts'
8
8
  import { resolveDescription } from '../util/description.ts'
9
9
  import { output, outputError } from '../util/format.ts'
10
+ import {
11
+ CREATE_EDGE,
12
+ CREATE_NODE,
13
+ DELETE_NODE,
14
+ DESCENDANTS,
15
+ DESCENDANTS_BRIEF,
16
+ GET_NODE,
17
+ UPDATE_NODE,
18
+ } from '../util/operations.ts'
10
19
 
11
20
  export const featureCommand = new Command('feature').description(
12
21
  'Manage features in the active project',
@@ -32,22 +41,15 @@ featureCommand
32
41
  descriptionFile: opts.descriptionFile,
33
42
  })
34
43
  const nodeData = await graphql<{ createNode: { id: string } }>(
35
- `mutation CreateNode($type: String!, $title: String!, $description: String) {
36
- createNode(type: $type, title: $title, description: $description) {
37
- id type title description status createdAt updatedAt
38
- }
39
- }`,
44
+ CREATE_NODE,
40
45
  { type: 'feature', title: opts.title, description },
41
46
  )
42
47
  const featureId = nodeData.createNode.id
43
- await graphql(
44
- `mutation CreateEdge($sourceId: String!, $targetId: String!, $relation: String!) {
45
- createEdge(sourceId: $sourceId, targetId: $targetId, relation: $relation) {
46
- sourceId targetId relation createdAt
47
- }
48
- }`,
49
- { sourceId: featureId, targetId: project.id, relation: 'part_of' },
50
- )
48
+ await graphql(CREATE_EDGE, {
49
+ sourceId: featureId,
50
+ targetId: project.id,
51
+ relation: 'part_of',
52
+ })
51
53
  output(nodeData.createNode)
52
54
  } catch (error) {
53
55
  outputError(error)
@@ -63,14 +65,11 @@ featureCommand
63
65
  const project = requireProject()
64
66
  const data = await graphql<{
65
67
  descendants: Array<{ id: string; type: string; title: string }>
66
- }>(
67
- `query Descendants($nodeId: String!, $relation: String, $maxDepth: Int) {
68
- descendants(nodeId: $nodeId, relation: $relation, maxDepth: $maxDepth) {
69
- id type title status
70
- }
71
- }`,
72
- { nodeId: project.id, relation: 'part_of', maxDepth: 1 },
73
- )
68
+ }>(DESCENDANTS_BRIEF, {
69
+ nodeId: project.id,
70
+ relation: 'part_of',
71
+ maxDepth: 1,
72
+ })
74
73
  const features = data.descendants.filter((n) => n.type === 'feature')
75
74
  const match = features.find(
76
75
  (f) => f.id === nameOrId || f.title === nameOrId,
@@ -111,14 +110,7 @@ featureCommand
111
110
  const project = requireProject()
112
111
  const data = await graphql<{
113
112
  descendants: Array<{ id: string; type: string }>
114
- }>(
115
- `query Descendants($nodeId: String!, $relation: String, $maxDepth: Int) {
116
- descendants(nodeId: $nodeId, relation: $relation, maxDepth: $maxDepth) {
117
- id type title description status createdAt updatedAt
118
- }
119
- }`,
120
- { nodeId: project.id, relation: 'part_of', maxDepth: 1 },
121
- )
113
+ }>(DESCENDANTS, { nodeId: project.id, relation: 'part_of', maxDepth: 1 })
122
114
  const features = data.descendants.filter((n) => n.type === 'feature')
123
115
  output(features)
124
116
  } catch (error) {
@@ -158,11 +150,7 @@ featureCommand
158
150
  }
159
151
  if (opts.metadata != null) variables.metadata = opts.metadata
160
152
  const data = await graphql<{ updateNode: unknown }>(
161
- `mutation UpdateNode($id: String!, $title: String, $description: String, $metadata: String) {
162
- updateNode(id: $id, title: $title, description: $description, metadata: $metadata) {
163
- id type title description status metadata createdAt updatedAt
164
- }
165
- }`,
153
+ UPDATE_NODE,
166
154
  variables,
167
155
  )
168
156
  output(data.updateNode)
@@ -183,12 +171,9 @@ featureCommand
183
171
  'No feature specified. Pass an ID or set an active feature.',
184
172
  )
185
173
  }
186
- const data = await graphql<{ deleteNode: boolean }>(
187
- `mutation DeleteNode($id: String!) {
188
- deleteNode(id: $id)
189
- }`,
190
- { id: featureId },
191
- )
174
+ const data = await graphql<{ deleteNode: boolean }>(DELETE_NODE, {
175
+ id: featureId,
176
+ })
192
177
  output({ deleted: data.deleteNode })
193
178
  } catch (error) {
194
179
  outputError(error)
@@ -207,14 +192,9 @@ featureCommand
207
192
  'No feature specified. Pass an ID or set an active feature.',
208
193
  )
209
194
  }
210
- const data = await graphql<{ node: unknown }>(
211
- `query GetNode($id: String!) {
212
- node(id: $id) {
213
- id type title description status metadata createdAt updatedAt
214
- }
215
- }`,
216
- { id: featureId },
217
- )
195
+ const data = await graphql<{ node: unknown }>(GET_NODE, {
196
+ id: featureId,
197
+ })
218
198
  output(data.node)
219
199
  } catch (error) {
220
200
  outputError(error)
@@ -9,6 +9,13 @@ import {
9
9
  parseManifest,
10
10
  readClientKey,
11
11
  } from '../util/manifest.ts'
12
+ import {
13
+ IMPORT_CREATE,
14
+ IMPORT_EDGE,
15
+ IMPORT_EDGES,
16
+ IMPORT_EXISTING,
17
+ IMPORT_UPDATE,
18
+ } from '../util/operations.ts'
12
19
 
13
20
  const NODE_TYPES = ['project', 'feature', 'task'] as const
14
21
 
@@ -53,12 +60,9 @@ function desiredEdges(manifest: Manifest): DesiredEdge[] {
53
60
  async function loadExisting(): Promise<Map<string, string>> {
54
61
  const idByKey = new Map<string, string>()
55
62
  for (const type of NODE_TYPES) {
56
- const data = await graphql<{ nodes: ServerNode[] }>(
57
- `query ImportExisting($type: String) {
58
- nodes(type: $type) { id type title metadata }
59
- }`,
60
- { type },
61
- )
63
+ const data = await graphql<{ nodes: ServerNode[] }>(IMPORT_EXISTING, {
64
+ type,
65
+ })
62
66
  for (const node of data.nodes) {
63
67
  const key = readClientKey(node.metadata)
64
68
  if (key) idByKey.set(key, node.id)
@@ -78,9 +82,7 @@ async function loadExistingEdges(nodeIds: string[]): Promise<Set<string>> {
78
82
  for (const nodeId of nodeIds) {
79
83
  for (const relation of RELATIONS) {
80
84
  const data = await graphql<{ edges: Array<{ id: string }> }>(
81
- `query ImportEdges($nodeId: String!, $relation: String!) {
82
- edges(nodeId: $nodeId, relation: $relation, direction: "outgoing") { id }
83
- }`,
85
+ IMPORT_EDGES,
84
86
  { nodeId, relation },
85
87
  )
86
88
  for (const target of data.edges) {
@@ -91,25 +93,13 @@ async function loadExistingEdges(nodeIds: string[]): Promise<Set<string>> {
91
93
  return existing
92
94
  }
93
95
 
94
- const CREATE_NODE = `mutation ImportCreate($type: String!, $title: String!, $description: String, $status: String, $metadata: String) {
95
- createNode(type: $type, title: $title, description: $description, status: $status, metadata: $metadata) { id }
96
- }`
97
-
98
- const UPDATE_NODE = `mutation ImportUpdate($id: String!, $title: String, $description: String, $status: String, $metadata: String) {
99
- updateNode(id: $id, title: $title, description: $description, status: $status, metadata: $metadata) { id }
100
- }`
101
-
102
- const CREATE_EDGE = `mutation ImportEdge($sourceId: String!, $targetId: String!, $relation: String!) {
103
- createEdge(sourceId: $sourceId, targetId: $targetId, relation: $relation) { sourceId targetId relation }
104
- }`
105
-
106
96
  async function upsertNode(
107
97
  node: ManifestNode,
108
98
  existingId: string | undefined,
109
99
  ): Promise<string> {
110
100
  const metadata = buildNodeMetadata(node.key, node.metadata)
111
101
  if (existingId) {
112
- await graphql<{ updateNode: { id: string } }>(UPDATE_NODE, {
102
+ await graphql<{ updateNode: { id: string } }>(IMPORT_UPDATE, {
113
103
  id: existingId,
114
104
  title: node.title,
115
105
  description: node.description ?? null,
@@ -118,7 +108,7 @@ async function upsertNode(
118
108
  })
119
109
  return existingId
120
110
  }
121
- const data = await graphql<{ createNode: { id: string } }>(CREATE_NODE, {
111
+ const data = await graphql<{ createNode: { id: string } }>(IMPORT_CREATE, {
122
112
  type: node.type,
123
113
  title: node.title,
124
114
  description: node.description ?? null,
@@ -162,7 +152,7 @@ export const importCommand = new Command('import')
162
152
  const targetId = idByKey.get(edge.targetKey)
163
153
  if (!sourceId || !targetId) continue
164
154
  if (present.has(edgeKey(sourceId, targetId, edge.relation))) continue
165
- await graphql(CREATE_EDGE, {
155
+ await graphql(IMPORT_EDGE, {
166
156
  sourceId,
167
157
  targetId,
168
158
  relation: edge.relation,
@@ -4,6 +4,7 @@ import { Command } from 'commander'
4
4
  import { graphql } from '../util/client.ts'
5
5
  import { loadConfig, saveConfig } from '../util/config.ts'
6
6
  import { output, outputError } from '../util/format.ts'
7
+ import { CREATE_PROJECT } from '../util/operations.ts'
7
8
 
8
9
  export const initCommand = new Command('init')
9
10
  .description('Initialize Flowy for the current git repository')
@@ -30,11 +31,7 @@ export const initCommand = new Command('init')
30
31
  }
31
32
 
32
33
  const data = await graphql<{ createNode: { id: string; title: string } }>(
33
- `mutation CreateProject($type: String!, $title: String!) {
34
- createNode(type: $type, title: $title) {
35
- id type title description status metadata createdAt updatedAt
36
- }
37
- }`,
34
+ CREATE_PROJECT,
38
35
  { type: 'project', title: repoName },
39
36
  )
40
37
 
@@ -7,7 +7,7 @@ let mockOutputError: ReturnType<typeof vi.fn>
7
7
 
8
8
  beforeEach(() => {
9
9
  mockLoadConfig = vi.fn(() => ({
10
- mode: 'saas',
10
+ mode: 'remote',
11
11
  apiUrl: 'https://flowy-ai.fly.dev/graphql',
12
12
  apiKey: 'old-key',
13
13
  client: { name: '' },
@@ -26,6 +26,16 @@ beforeEach(() => {
26
26
  loadConfig: mockLoadConfig,
27
27
  saveConfig: mockSaveConfig,
28
28
  fingerprintKey: actual.fingerprintKey,
29
+ requireRemoteMode: (commandName: string) => {
30
+ const cfg = (mockLoadConfig as unknown as () => { mode: string })()
31
+ if (cfg.mode === 'local') {
32
+ const err = new Error(
33
+ `"flowy ${commandName}" is only available in remote mode. The active mode is local mode.`,
34
+ ) as Error & { code?: string }
35
+ err.code = 'LOCAL_MODE'
36
+ throw err
37
+ }
38
+ },
29
39
  }
30
40
  })
31
41
 
@@ -160,4 +170,29 @@ describe('key command', () => {
160
170
  expect(mockSaveConfig).not.toHaveBeenCalled()
161
171
  expect(mockOutput).not.toHaveBeenCalled()
162
172
  })
173
+
174
+ test('rotate errors cleanly in local mode without hitting the server', async () => {
175
+ mockLoadConfig.mockReturnValue({
176
+ mode: 'local',
177
+ apiUrl: 'http://localhost:4000/graphql',
178
+ apiKey: 'old-key',
179
+ client: { name: '' },
180
+ projects: {},
181
+ })
182
+ const mockGraphql = vi.fn()
183
+ vi.doMock('../util/client.ts', () => ({ graphql: mockGraphql }))
184
+
185
+ const { keyCommand } = await import('./key.ts')
186
+ await keyCommand.parseAsync(['rotate'], { from: 'user' })
187
+
188
+ expect(mockGraphql).not.toHaveBeenCalled()
189
+ expect(mockSaveConfig).not.toHaveBeenCalled()
190
+ expect(mockOutput).not.toHaveBeenCalled()
191
+ expect(mockOutputError).toHaveBeenCalledWith(
192
+ expect.objectContaining({
193
+ message: expect.stringMatching(/local mode/i),
194
+ code: 'LOCAL_MODE',
195
+ }),
196
+ )
197
+ })
163
198
  })
@@ -1,7 +1,13 @@
1
1
  import { Command } from 'commander'
2
2
  import { graphql } from '../util/client.ts'
3
- import { fingerprintKey, loadConfig, saveConfig } from '../util/config.ts'
3
+ import {
4
+ fingerprintKey,
5
+ loadConfig,
6
+ requireRemoteMode,
7
+ saveConfig,
8
+ } from '../util/config.ts'
4
9
  import { output, outputError } from '../util/format.ts'
10
+ import { ROTATE_API_KEY } from '../util/operations.ts'
5
11
 
6
12
  export const keyCommand = new Command('key').description('API key management')
7
13
 
@@ -14,6 +20,7 @@ keyCommand
14
20
  )
15
21
  .action(async (opts) => {
16
22
  try {
23
+ requireRemoteMode('key rotate')
17
24
  const data = await graphql<{
18
25
  rotateApiKey: {
19
26
  user: {
@@ -25,14 +32,7 @@ keyCommand
25
32
  }
26
33
  apiKey: string
27
34
  }
28
- }>(
29
- `mutation RotateApiKey {
30
- rotateApiKey {
31
- user { id email tier createdAt graceEndsAt }
32
- apiKey
33
- }
34
- }`,
35
- )
35
+ }>(ROTATE_API_KEY)
36
36
 
37
37
  const { user, apiKey } = data.rotateApiKey
38
38
  const config = loadConfig()
@@ -3,6 +3,14 @@ import { graphql } from '../util/client.ts'
3
3
  import { loadConfig, requireProject, saveConfig } from '../util/config.ts'
4
4
  import { resolveDescription } from '../util/description.ts'
5
5
  import { output, outputError } from '../util/format.ts'
6
+ import {
7
+ CREATE_PROJECT,
8
+ DELETE_NODE,
9
+ GET_PROJECT,
10
+ LIST_PROJECTS,
11
+ LIST_PROJECTS_FOR_SET,
12
+ UPDATE_NODE,
13
+ } from '../util/operations.ts'
6
14
 
7
15
  export const projectCommand = new Command('project').description(
8
16
  'Manage projects',
@@ -14,14 +22,10 @@ projectCommand
14
22
  .argument('<name>', 'Project name')
15
23
  .action(async (name: string) => {
16
24
  try {
17
- const data = await graphql<{ createNode: unknown }>(
18
- `mutation CreateProject($type: String!, $title: String!) {
19
- createNode(type: $type, title: $title) {
20
- id type title description status metadata createdAt updatedAt
21
- }
22
- }`,
23
- { type: 'project', title: name },
24
- )
25
+ const data = await graphql<{ createNode: unknown }>(CREATE_PROJECT, {
26
+ type: 'project',
27
+ title: name,
28
+ })
25
29
  output(data.createNode)
26
30
  } catch (error) {
27
31
  outputError(error)
@@ -44,14 +48,7 @@ projectCommand
44
48
  try {
45
49
  const data = await graphql<{
46
50
  nodes: Array<{ id: string; title: string }>
47
- }>(
48
- `query ListProjects($type: String) {
49
- nodes(type: $type) {
50
- id title
51
- }
52
- }`,
53
- { type: 'project' },
54
- )
51
+ }>(LIST_PROJECTS_FOR_SET, { type: 'project' })
55
52
  const project = data.nodes.find((n) => n.title === name)
56
53
  if (!project) {
57
54
  throw new Error(`Project "${name}" not found.`)
@@ -67,14 +64,9 @@ projectCommand
67
64
  .description('List all projects')
68
65
  .action(async () => {
69
66
  try {
70
- const data = await graphql<{ nodes: unknown[] }>(
71
- `query ListProjects($type: String) {
72
- nodes(type: $type) {
73
- id type title description status createdAt updatedAt
74
- }
75
- }`,
76
- { type: 'project' },
77
- )
67
+ const data = await graphql<{ nodes: unknown[] }>(LIST_PROJECTS, {
68
+ type: 'project',
69
+ })
78
70
  output(data.nodes)
79
71
  } catch (error) {
80
72
  outputError(error)
@@ -84,14 +76,9 @@ projectCommand
84
76
  export async function showProject(id?: string): Promise<void> {
85
77
  try {
86
78
  const projectId = id ?? requireProject().id
87
- const data = await graphql<{ node: unknown }>(
88
- `query GetProject($id: String!) {
89
- node(id: $id) {
90
- id type title description status metadata createdAt updatedAt
91
- }
92
- }`,
93
- { id: projectId },
94
- )
79
+ const data = await graphql<{ node: unknown }>(GET_PROJECT, {
80
+ id: projectId,
81
+ })
95
82
  output(data.node)
96
83
  } catch (error) {
97
84
  outputError(error)
@@ -131,11 +118,7 @@ projectCommand
131
118
  }
132
119
  if (opts.metadata != null) variables.metadata = opts.metadata
133
120
  const data = await graphql<{ updateNode: unknown }>(
134
- `mutation UpdateNode($id: String!, $title: String, $description: String, $metadata: String) {
135
- updateNode(id: $id, title: $title, description: $description, metadata: $metadata) {
136
- id type title description status metadata createdAt updatedAt
137
- }
138
- }`,
121
+ UPDATE_NODE,
139
122
  variables,
140
123
  )
141
124
  output(data.updateNode)
@@ -151,12 +134,9 @@ projectCommand
151
134
  .action(async (id?: string) => {
152
135
  try {
153
136
  const projectId = id ?? requireProject().id
154
- const data = await graphql<{ deleteNode: boolean }>(
155
- `mutation DeleteNode($id: String!) {
156
- deleteNode(id: $id)
157
- }`,
158
- { id: projectId },
159
- )
137
+ const data = await graphql<{ deleteNode: boolean }>(DELETE_NODE, {
138
+ id: projectId,
139
+ })
160
140
  output({ deleted: data.deleteNode })
161
141
  } catch (error) {
162
142
  outputError(error)
@@ -1,6 +1,7 @@
1
1
  import { Command } from 'commander'
2
2
  import { graphql } from '../util/client.ts'
3
3
  import { output, outputError } from '../util/format.ts'
4
+ import { SEARCH } from '../util/operations.ts'
4
5
 
5
6
  export const searchCommand = new Command('search')
6
7
  .description('Search nodes by text')
@@ -10,19 +11,12 @@ export const searchCommand = new Command('search')
10
11
  .option('--limit <n>', 'Limit results', '50')
11
12
  .action(async (query: string, opts) => {
12
13
  try {
13
- const data = await graphql<{ search: unknown[] }>(
14
- `query Search($query: String!, $type: String, $status: String, $limit: Int) {
15
- search(query: $query, type: $type, status: $status, limit: $limit) {
16
- id type title description status
17
- }
18
- }`,
19
- {
20
- query,
21
- type: opts.type,
22
- status: opts.status,
23
- limit: opts.limit ? Number.parseInt(opts.limit, 10) : undefined,
24
- },
25
- )
14
+ const data = await graphql<{ search: unknown[] }>(SEARCH, {
15
+ query,
16
+ type: opts.type,
17
+ status: opts.status,
18
+ limit: opts.limit ? Number.parseInt(opts.limit, 10) : undefined,
19
+ })
26
20
  output(data.search)
27
21
  } catch (error) {
28
22
  outputError(error)