@sqaoss/flowy 0.1.0 → 1.0.2
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/LICENSE +201 -661
- package/README.md +89 -44
- package/docker-compose.yml +14 -0
- package/package.json +23 -6
- package/server/Dockerfile +14 -0
- package/server/package.json +25 -0
- package/server/src/db.test.ts +93 -0
- package/server/src/db.ts +47 -0
- package/server/src/index.test.ts +25 -0
- package/server/src/index.ts +45 -0
- package/server/src/resolvers.test.ts +855 -0
- package/server/src/resolvers.ts +308 -0
- package/server/src/schema.test.ts +93 -0
- package/server/src/schema.ts +45 -0
- package/skills/using-flowy/SKILL.md +153 -0
- package/src/commands/client.test.ts +40 -0
- package/src/commands/client.ts +34 -0
- package/src/commands/feature.test.ts +71 -0
- package/src/commands/feature.ts +143 -0
- package/src/commands/project.test.ts +83 -0
- package/src/commands/project.ts +104 -0
- package/src/commands/setup.test.ts +101 -0
- package/src/commands/setup.ts +83 -0
- package/src/commands/task.test.ts +83 -0
- package/src/commands/task.ts +127 -0
- package/src/commands/tree.test.ts +9 -0
- package/src/commands/tree.ts +2 -59
- package/src/index.ts +12 -8
- package/src/util/config.test.ts +151 -0
- package/src/util/config.ts +107 -2
- package/src/util/description.test.ts +29 -0
- package/src/util/description.ts +8 -0
- package/src/commands/edge.ts +0 -84
- package/src/commands/node.ts +0 -134
- package/src/commands/register.ts +0 -25
package/README.md
CHANGED
|
@@ -1,79 +1,95 @@
|
|
|
1
1
|
# Flowy
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Project management for AI coding agents.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@sqaoss/flowy)
|
|
6
|
-
[](LICENSE)
|
|
7
7
|
[](https://github.com/sqaoss/flowy/actions/workflows/ci.yml)
|
|
8
8
|
|
|
9
9
|
## What is Flowy
|
|
10
10
|
|
|
11
|
-
Flowy is a
|
|
11
|
+
Flowy is a backend + CLI that gives AI coding agents structured project management. It enforces a strict hierarchy -- **client -> project -> feature -> task** -- so agents always have clear context about what they're working on.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Run it locally with SQLite and Docker, or connect to the hosted SaaS for multi-agent team collaboration.
|
|
14
|
+
|
|
15
|
+
## Quick Start (Local Mode)
|
|
16
|
+
|
|
17
|
+
Local mode runs a Flowy server on your machine using Docker. No account needed.
|
|
14
18
|
|
|
15
19
|
```bash
|
|
16
20
|
# Install
|
|
17
21
|
bun add -g @sqaoss/flowy # or: npm i -g @sqaoss/flowy
|
|
18
22
|
|
|
19
|
-
#
|
|
20
|
-
flowy
|
|
21
|
-
|
|
23
|
+
# Start the local server (requires Docker)
|
|
24
|
+
flowy setup local
|
|
25
|
+
|
|
26
|
+
# Set your client name
|
|
27
|
+
flowy client set name "Acme Corp"
|
|
22
28
|
|
|
23
|
-
# Create a project
|
|
24
|
-
flowy
|
|
29
|
+
# Create a project and map it to the current directory
|
|
30
|
+
flowy project create "Auth System"
|
|
31
|
+
flowy project set "Auth System"
|
|
25
32
|
|
|
26
|
-
#
|
|
27
|
-
flowy
|
|
28
|
-
flowy
|
|
33
|
+
# Plan a feature
|
|
34
|
+
flowy feature create --title "SSO Support" --description sso-spec.md
|
|
35
|
+
flowy feature set "SSO Support"
|
|
29
36
|
|
|
30
|
-
#
|
|
31
|
-
flowy
|
|
37
|
+
# Create tasks
|
|
38
|
+
flowy task create --title "Implement OAuth" --description oauth.md
|
|
39
|
+
flowy task create --title "Write auth tests" --description "Unit + integration tests"
|
|
32
40
|
|
|
33
|
-
# Track
|
|
41
|
+
# Track progress
|
|
34
42
|
flowy status <task-id> in_progress
|
|
35
43
|
flowy status <task-id> done
|
|
36
44
|
|
|
37
45
|
# Search and explore
|
|
38
46
|
flowy search "OAuth" --type task
|
|
39
|
-
flowy tree
|
|
47
|
+
flowy tree <project-id> --depth 3
|
|
40
48
|
```
|
|
41
49
|
|
|
50
|
+
## Remote Mode (Coming Soon)
|
|
51
|
+
|
|
52
|
+
Remote mode connects to the hosted Flowy SaaS for multi-agent collaboration, shared project state across teams, and persistent history. Registration and API key setup will happen directly through the CLI -- no website needed. This is currently a work in progress.
|
|
53
|
+
|
|
42
54
|
## Command Reference
|
|
43
55
|
|
|
44
56
|
| Command | Description |
|
|
45
57
|
|---------|-------------|
|
|
46
|
-
| `
|
|
58
|
+
| `setup local` | Start a local Docker server and configure the CLI |
|
|
59
|
+
| `setup remote` | Connect to the hosted SaaS (coming soon) |
|
|
47
60
|
| `whoami` | Show current user |
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
51
|
-
| `
|
|
52
|
-
| `
|
|
61
|
+
| `client set name <name>` | Set client display name |
|
|
62
|
+
| `project create <name>` | Create project |
|
|
63
|
+
| `project set <name>` | Map current directory to project |
|
|
64
|
+
| `project list` | List all projects |
|
|
65
|
+
| `project show [<id>]` | Show project details |
|
|
66
|
+
| `feature create --title <t> --description <d>` | Create feature (requires active project) |
|
|
67
|
+
| `feature set <name-or-id>` | Set active feature |
|
|
68
|
+
| `feature unset` | Clear active feature |
|
|
69
|
+
| `feature list` | List features in active project |
|
|
70
|
+
| `feature show [<id>]` | Show feature details |
|
|
71
|
+
| `task create --title <t> --description <d>` | Create task (requires active feature) |
|
|
72
|
+
| `task list` | List tasks in active feature |
|
|
73
|
+
| `task show <id>` | Show task details |
|
|
74
|
+
| `task block <id1> <id2>` | Mark task as blocking another |
|
|
75
|
+
| `task unblock <id1> <id2>` | Remove block |
|
|
53
76
|
| `status <id> <status>` | Update status (shorthand) |
|
|
54
|
-
| `approve <id>` | Approve
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
57
|
-
| `edge remove --source <id> --target <id> --relation <rel>` | Remove edge |
|
|
58
|
-
| `search <query> [--type] [--status] [--limit]` | Search nodes |
|
|
59
|
-
| `tree subtree <id> [--depth N]` | Show subtree |
|
|
60
|
-
| `tree ancestors <id> [--depth N] [--relation <rel>]` | Show ancestors |
|
|
61
|
-
| `tree descendants <id> [--depth N] [--relation <rel>]` | Show descendants |
|
|
77
|
+
| `approve <id>` | Approve (must be pending_review) |
|
|
78
|
+
| `search <query> [--type] [--status] [--limit]` | Full-text search |
|
|
79
|
+
| `tree <id> [--depth N]` | Show subtree |
|
|
62
80
|
|
|
63
|
-
All commands output JSON.
|
|
81
|
+
All commands output JSON to stdout.
|
|
64
82
|
|
|
65
83
|
## Data Model
|
|
66
84
|
|
|
67
|
-
###
|
|
68
|
-
|
|
69
|
-
`client`, `project`, `feature`, `epic`, `task`
|
|
85
|
+
### Entity Hierarchy
|
|
70
86
|
|
|
71
|
-
|
|
87
|
+
```
|
|
88
|
+
client -> project -> feature -> task
|
|
89
|
+
1:many 1:many 1:many
|
|
90
|
+
```
|
|
72
91
|
|
|
73
|
-
|
|
74
|
-
- `depends_on` -- must complete before starting
|
|
75
|
-
- `blocks` -- prevents progress on target
|
|
76
|
-
- `informs` -- provides context to target
|
|
92
|
+
Every entity belongs to its parent. No orphans.
|
|
77
93
|
|
|
78
94
|
### Status Flow
|
|
79
95
|
|
|
@@ -83,15 +99,44 @@ draft -> pending_review -> approved -> in_progress -> done
|
|
|
83
99
|
|
|
84
100
|
Also: `blocked`, `cancelled`
|
|
85
101
|
|
|
102
|
+
### Description Field
|
|
103
|
+
|
|
104
|
+
`--description` accepts a file path or an inline string:
|
|
105
|
+
- `--description spec.md` -- reads file content
|
|
106
|
+
- `--description "Do the thing"` -- sends string as-is
|
|
107
|
+
|
|
86
108
|
## Configuration
|
|
87
109
|
|
|
110
|
+
Config is stored at `~/.config/flowy/config.json`.
|
|
111
|
+
|
|
88
112
|
| Variable | Description | Default |
|
|
89
113
|
|----------|-------------|---------|
|
|
90
|
-
| `FLOWY_API_URL` | GraphQL endpoint | `
|
|
91
|
-
| `FLOWY_API_KEY` | API key
|
|
114
|
+
| `FLOWY_API_URL` | GraphQL endpoint | `http://localhost:4000/graphql` |
|
|
115
|
+
| `FLOWY_API_KEY` | API key (remote mode only) | -- |
|
|
116
|
+
| `FLOWY_PROJECT` | Override active project by name | -- |
|
|
117
|
+
| `FLOWY_FEATURE` | Override active feature by ID | -- |
|
|
92
118
|
|
|
93
|
-
##
|
|
119
|
+
## For AI Agents
|
|
94
120
|
|
|
95
|
-
|
|
121
|
+
Flowy integrates with [TanStack Intent](https://github.com/TanStack/intent) for automatic tool discovery. Run:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
npx @tanstack/intent install
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
This auto-discovers the Flowy skill and makes it available to your agent. See [`skills/using-flowy/SKILL.md`](skills/using-flowy/SKILL.md) for the full skill reference.
|
|
128
|
+
|
|
129
|
+
## Development
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
bun run test # Run CLI tests (Vitest)
|
|
133
|
+
bun run check # Biome lint + format
|
|
134
|
+
bun run typecheck # TypeScript type checking
|
|
135
|
+
|
|
136
|
+
# Server tests
|
|
137
|
+
cd server && bunx --bun vitest run
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## License
|
|
96
141
|
|
|
97
|
-
|
|
142
|
+
Apache-2.0 -- Copyright 2026 SQA & Automation SRL
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
services:
|
|
2
|
+
server:
|
|
3
|
+
build: ./server
|
|
4
|
+
ports:
|
|
5
|
+
- "4000:4000"
|
|
6
|
+
environment:
|
|
7
|
+
- NODE_ENV=production
|
|
8
|
+
restart: unless-stopped
|
|
9
|
+
healthcheck:
|
|
10
|
+
test: ["CMD", "bun", "-e", "const r = await fetch('http://localhost:4000/health'); if (!r.ok) process.exit(1)"]
|
|
11
|
+
interval: 10s
|
|
12
|
+
timeout: 5s
|
|
13
|
+
retries: 3
|
|
14
|
+
start_period: 5s
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sqaoss/flowy",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "CLI for Flowy — project management for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -8,13 +8,21 @@
|
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"src/",
|
|
11
|
+
"server/src/",
|
|
12
|
+
"server/package.json",
|
|
13
|
+
"server/Dockerfile",
|
|
14
|
+
"skills/",
|
|
15
|
+
"!skills/_artifacts",
|
|
16
|
+
"docker-compose.yml",
|
|
11
17
|
"LICENSE",
|
|
12
18
|
"README.md"
|
|
13
19
|
],
|
|
14
20
|
"publishConfig": {
|
|
15
|
-
"access": "public"
|
|
21
|
+
"access": "public",
|
|
22
|
+
"provenance": true,
|
|
23
|
+
"registry": "https://registry.npmjs.org/"
|
|
16
24
|
},
|
|
17
|
-
"license": "
|
|
25
|
+
"license": "Apache-2.0",
|
|
18
26
|
"repository": {
|
|
19
27
|
"type": "git",
|
|
20
28
|
"url": "https://github.com/sqaoss/flowy.git"
|
|
@@ -33,24 +41,33 @@
|
|
|
33
41
|
"coding-agent",
|
|
34
42
|
"ai-agents",
|
|
35
43
|
"graphql",
|
|
36
|
-
"bun"
|
|
44
|
+
"bun",
|
|
45
|
+
"tanstack-intent"
|
|
37
46
|
],
|
|
38
47
|
"scripts": {
|
|
39
48
|
"cli": "bun src/index.ts",
|
|
40
49
|
"check": "biome check --write .",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"test:watch": "vitest",
|
|
41
52
|
"typecheck": "tsc --noEmit",
|
|
42
53
|
"prepare": "husky"
|
|
43
54
|
},
|
|
44
55
|
"dependencies": {
|
|
45
|
-
"
|
|
56
|
+
"@semantic-release/changelog": "6.0.3",
|
|
57
|
+
"@semantic-release/git": "10.0.1",
|
|
58
|
+
"commander": "^14.0.3",
|
|
59
|
+
"semantic-release": "25.0.3"
|
|
46
60
|
},
|
|
47
61
|
"devDependencies": {
|
|
48
62
|
"@biomejs/biome": "2.4.4",
|
|
63
|
+
"@tanstack/intent": "^0.0.23",
|
|
49
64
|
"@commitlint/cli": "^20.4.2",
|
|
50
65
|
"@commitlint/config-conventional": "^20.4.2",
|
|
51
66
|
"husky": "^9.1.7",
|
|
52
67
|
"lint-staged": "^16.3.0",
|
|
53
|
-
"
|
|
68
|
+
"tdd-guard-vitest": "^0.1.6",
|
|
69
|
+
"typescript": "^5",
|
|
70
|
+
"vitest": "^4.1.2"
|
|
54
71
|
},
|
|
55
72
|
"lint-staged": {
|
|
56
73
|
"*.{ts,tsx,js,jsx}": [
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
FROM oven/bun:1.3.11 AS base
|
|
2
|
+
WORKDIR /app
|
|
3
|
+
|
|
4
|
+
FROM base AS install
|
|
5
|
+
COPY package.json bun.lock ./
|
|
6
|
+
RUN bun install --frozen-lockfile --production
|
|
7
|
+
|
|
8
|
+
FROM base
|
|
9
|
+
COPY --from=install /app/node_modules node_modules
|
|
10
|
+
COPY src src
|
|
11
|
+
COPY tsconfig.json .
|
|
12
|
+
|
|
13
|
+
EXPOSE 4000
|
|
14
|
+
CMD ["bun", "src/index.ts"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sqaoss/flowy-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "bun --watch src/index.ts",
|
|
8
|
+
"start": "bun src/index.ts",
|
|
9
|
+
"test": "bunx --bun vitest run",
|
|
10
|
+
"test:watch": "bunx --bun vitest",
|
|
11
|
+
"check": "biome check --write .",
|
|
12
|
+
"typecheck": "tsc --noEmit"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"graphql": "16.13.2",
|
|
16
|
+
"graphql-yoga": "5.18.1",
|
|
17
|
+
"nanoid": "5.1.7"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@biomejs/biome": "2.4.4",
|
|
21
|
+
"@types/bun": "^1.3.11",
|
|
22
|
+
"typescript": "^5",
|
|
23
|
+
"vitest": "^4.1.2"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { createDb } from './db.ts'
|
|
3
|
+
|
|
4
|
+
describe('createDb', () => {
|
|
5
|
+
it('creates nodes and edges tables', () => {
|
|
6
|
+
const db = createDb(':memory:')
|
|
7
|
+
|
|
8
|
+
const tables = db.raw
|
|
9
|
+
.query<{ name: string }, []>(
|
|
10
|
+
"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name",
|
|
11
|
+
)
|
|
12
|
+
.all()
|
|
13
|
+
.map((r) => r.name)
|
|
14
|
+
|
|
15
|
+
expect(tables).toContain('nodes')
|
|
16
|
+
expect(tables).toContain('edges')
|
|
17
|
+
|
|
18
|
+
db.close()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('rejects invalid node type', () => {
|
|
22
|
+
const db = createDb(':memory:')
|
|
23
|
+
|
|
24
|
+
expect(() =>
|
|
25
|
+
db.raw.run(
|
|
26
|
+
"INSERT INTO nodes (id, type, title) VALUES ('n1', 'invalid_type', 'Test')",
|
|
27
|
+
),
|
|
28
|
+
).toThrow()
|
|
29
|
+
|
|
30
|
+
db.close()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('rejects invalid status', () => {
|
|
34
|
+
const db = createDb(':memory:')
|
|
35
|
+
|
|
36
|
+
expect(() =>
|
|
37
|
+
db.raw.run(
|
|
38
|
+
"INSERT INTO nodes (id, type, title, status) VALUES ('n1', 'project', 'Test', 'invalid_status')",
|
|
39
|
+
),
|
|
40
|
+
).toThrow()
|
|
41
|
+
|
|
42
|
+
db.close()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('rejects invalid edge relation', () => {
|
|
46
|
+
const db = createDb(':memory:')
|
|
47
|
+
|
|
48
|
+
db.raw.run(
|
|
49
|
+
"INSERT INTO nodes (id, type, title) VALUES ('n1', 'project', 'P1')",
|
|
50
|
+
)
|
|
51
|
+
db.raw.run(
|
|
52
|
+
"INSERT INTO nodes (id, type, title) VALUES ('n2', 'feature', 'F1')",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
expect(() =>
|
|
56
|
+
db.raw.run(
|
|
57
|
+
"INSERT INTO edges (source_id, target_id, relation) VALUES ('n2', 'n1', 'invalid_rel')",
|
|
58
|
+
),
|
|
59
|
+
).toThrow()
|
|
60
|
+
|
|
61
|
+
db.close()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('creates indexes on nodes and edges', () => {
|
|
65
|
+
const db = createDb(':memory:')
|
|
66
|
+
|
|
67
|
+
const indexes = db.raw
|
|
68
|
+
.query<{ name: string }, []>(
|
|
69
|
+
"SELECT name FROM sqlite_master WHERE type='index' AND name LIKE 'idx_%' ORDER BY name",
|
|
70
|
+
)
|
|
71
|
+
.all()
|
|
72
|
+
.map((r) => r.name)
|
|
73
|
+
|
|
74
|
+
expect(indexes).toContain('idx_nodes_type')
|
|
75
|
+
expect(indexes).toContain('idx_nodes_status')
|
|
76
|
+
expect(indexes).toContain('idx_edges_target')
|
|
77
|
+
expect(indexes).toContain('idx_edges_source')
|
|
78
|
+
|
|
79
|
+
db.close()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('enables foreign keys', () => {
|
|
83
|
+
const db = createDb(':memory:')
|
|
84
|
+
|
|
85
|
+
const result = db.raw
|
|
86
|
+
.query<{ foreign_keys: number }, []>('PRAGMA foreign_keys')
|
|
87
|
+
.get()
|
|
88
|
+
|
|
89
|
+
expect(result?.foreign_keys).toBe(1)
|
|
90
|
+
|
|
91
|
+
db.close()
|
|
92
|
+
})
|
|
93
|
+
})
|
package/server/src/db.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite'
|
|
2
|
+
|
|
3
|
+
export type FlowyDb = ReturnType<typeof createDb>
|
|
4
|
+
|
|
5
|
+
export function createDb(path: string) {
|
|
6
|
+
const db = new Database(path)
|
|
7
|
+
|
|
8
|
+
db.run('PRAGMA journal_mode = WAL')
|
|
9
|
+
db.run('PRAGMA foreign_keys = ON')
|
|
10
|
+
|
|
11
|
+
db.run(`
|
|
12
|
+
CREATE TABLE IF NOT EXISTS nodes (
|
|
13
|
+
id TEXT PRIMARY KEY,
|
|
14
|
+
type TEXT NOT NULL CHECK(type IN ('project', 'feature', 'task')),
|
|
15
|
+
title TEXT NOT NULL,
|
|
16
|
+
description TEXT,
|
|
17
|
+
status TEXT NOT NULL DEFAULT 'draft' CHECK(status IN ('draft', 'pending_review', 'approved', 'in_progress', 'done', 'blocked', 'cancelled')),
|
|
18
|
+
metadata TEXT,
|
|
19
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
20
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
21
|
+
)
|
|
22
|
+
`)
|
|
23
|
+
|
|
24
|
+
db.run(`
|
|
25
|
+
CREATE TABLE IF NOT EXISTS edges (
|
|
26
|
+
source_id TEXT NOT NULL REFERENCES nodes(id),
|
|
27
|
+
target_id TEXT NOT NULL REFERENCES nodes(id),
|
|
28
|
+
relation TEXT NOT NULL CHECK(relation IN ('part_of', 'blocks')),
|
|
29
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
30
|
+
PRIMARY KEY (source_id, target_id, relation)
|
|
31
|
+
)
|
|
32
|
+
`)
|
|
33
|
+
|
|
34
|
+
db.run('CREATE INDEX IF NOT EXISTS idx_nodes_type ON nodes(type)')
|
|
35
|
+
db.run('CREATE INDEX IF NOT EXISTS idx_nodes_status ON nodes(status)')
|
|
36
|
+
db.run(
|
|
37
|
+
'CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id, relation)',
|
|
38
|
+
)
|
|
39
|
+
db.run(
|
|
40
|
+
'CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id, relation)',
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
raw: db,
|
|
45
|
+
close: () => db.close(),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { createServer } from './index.ts'
|
|
3
|
+
|
|
4
|
+
describe('createServer', () => {
|
|
5
|
+
let instance: ReturnType<typeof createServer> | undefined
|
|
6
|
+
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
instance?.close()
|
|
9
|
+
instance = undefined
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('exports a createServer function', () => {
|
|
13
|
+
expect(typeof createServer).toBe('function')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('responds to /health with status ok', async () => {
|
|
17
|
+
instance = createServer({ dbPath: ':memory:', port: 0 })
|
|
18
|
+
|
|
19
|
+
const res = await fetch(`http://localhost:${instance.port}/health`)
|
|
20
|
+
const json = await res.json()
|
|
21
|
+
|
|
22
|
+
expect(res.status).toBe(200)
|
|
23
|
+
expect(json).toEqual({ status: 'ok' })
|
|
24
|
+
})
|
|
25
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createSchema, createYoga } from 'graphql-yoga'
|
|
2
|
+
import { createDb } from './db.ts'
|
|
3
|
+
import { createResolvers } from './resolvers.ts'
|
|
4
|
+
import { typeDefs } from './schema.ts'
|
|
5
|
+
|
|
6
|
+
export function createServer(opts?: { dbPath?: string; port?: number }) {
|
|
7
|
+
const dbPath = opts?.dbPath ?? process.env.FLOWY_DB_PATH ?? './flowy.sqlite'
|
|
8
|
+
const port = opts?.port ?? Number(process.env.PORT ?? 4000)
|
|
9
|
+
|
|
10
|
+
const db = createDb(dbPath)
|
|
11
|
+
const resolvers = createResolvers(db)
|
|
12
|
+
|
|
13
|
+
const yoga = createYoga({
|
|
14
|
+
schema: createSchema({ typeDefs, resolvers }),
|
|
15
|
+
graphqlEndpoint: '/graphql',
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
const server = Bun.serve({
|
|
19
|
+
port,
|
|
20
|
+
fetch(req) {
|
|
21
|
+
const url = new URL(req.url)
|
|
22
|
+
if (url.pathname === '/health' && req.method === 'GET') {
|
|
23
|
+
return Response.json({ status: 'ok' })
|
|
24
|
+
}
|
|
25
|
+
return yoga.fetch(req)
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
server,
|
|
31
|
+
port: server.port,
|
|
32
|
+
db,
|
|
33
|
+
close() {
|
|
34
|
+
server.stop()
|
|
35
|
+
db.close()
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (import.meta.main) {
|
|
41
|
+
const { port } = createServer()
|
|
42
|
+
console.log(`Flowy local server running on http://localhost:${port}`)
|
|
43
|
+
console.log(` GraphQL: http://localhost:${port}/graphql`)
|
|
44
|
+
console.log(` Health: http://localhost:${port}/health`)
|
|
45
|
+
}
|