@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
package/server/src/schema.ts
CHANGED
|
@@ -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): [
|
|
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!]!
|
package/src/commands/approve.ts
CHANGED
|
@@ -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
|
})
|
package/src/commands/billing.ts
CHANGED
|
@@ -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
|
-
|
|
16
|
-
createCheckout(tier: $tier) {
|
|
17
|
-
url
|
|
18
|
-
}
|
|
19
|
-
}`,
|
|
18
|
+
CREATE_CHECKOUT,
|
|
20
19
|
{ tier: opts.tier },
|
|
21
20
|
)
|
|
22
21
|
output(data.createCheckout)
|
package/src/commands/export.ts
CHANGED
|
@@ -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 }>(
|
|
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
|
-
|
|
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
|
-
}>(
|
|
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)
|
package/src/commands/feature.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
212
|
-
|
|
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)
|
package/src/commands/import.ts
CHANGED
|
@@ -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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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 } }>(
|
|
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 } }>(
|
|
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(
|
|
155
|
+
await graphql(IMPORT_EDGE, {
|
|
166
156
|
sourceId,
|
|
167
157
|
targetId,
|
|
168
158
|
relation: edge.relation,
|
package/src/commands/init.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/src/commands/key.test.ts
CHANGED
|
@@ -7,7 +7,7 @@ let mockOutputError: ReturnType<typeof vi.fn>
|
|
|
7
7
|
|
|
8
8
|
beforeEach(() => {
|
|
9
9
|
mockLoadConfig = vi.fn(() => ({
|
|
10
|
-
mode: '
|
|
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
|
})
|
package/src/commands/key.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { Command } from 'commander'
|
|
2
2
|
import { graphql } from '../util/client.ts'
|
|
3
|
-
import {
|
|
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()
|
package/src/commands/project.ts
CHANGED
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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)
|
package/src/commands/search.ts
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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)
|