@sqaoss/flowy 0.1.1 → 1.1.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/LICENSE +201 -661
- package/README.md +90 -56
- package/docker-compose.yml +14 -0
- package/package.json +24 -7
- 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 +128 -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/init.test.ts +174 -0
- package/src/commands/init.ts +50 -0
- package/src/commands/project.test.ts +83 -0
- package/src/commands/project.ts +104 -0
- package/src/commands/setup.test.ts +135 -0
- package/src/commands/setup.ts +109 -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 +14 -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,81 +1,64 @@
|
|
|
1
1
|
# Flowy
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Agentic persistent planning
|
|
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
|
+
Jira, Linear, Trello are built for humans clicking boards. AI agents don't click boards. When your agent needs to plan work, track progress, and close tickets, those tools add friction, load context, and get in the way.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Flowy is where agents store plans and flow through execution. Features are master plans. Tasks are execution steps. Everything persists in a database, not as files cluttering your git history. Your agent flows through work without friction.
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
You get full observability on what every agent planned, built, and shipped.
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## Get Started
|
|
16
|
+
|
|
17
|
+
### Install (once)
|
|
16
18
|
|
|
17
19
|
```bash
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
npm i -g @sqaoss/flowy
|
|
21
|
+
flowy setup remote --email you@example.com
|
|
22
|
+
```
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
flowy register --email you@example.com
|
|
23
|
-
export FLOWY_API_KEY=flowy_xxx_yyy
|
|
24
|
+
### Initialize a project
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
```bash
|
|
27
|
+
cd my-project
|
|
28
|
+
flowy init # auto-detects repo, creates project
|
|
29
|
+
```
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
flowy node create --type task --title "Implement OAuth"
|
|
30
|
-
flowy node create --type task --title "Write auth tests"
|
|
31
|
+
### Start planning
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
flowy
|
|
33
|
+
```bash
|
|
34
|
+
flowy feature create --title "User Auth" --description auth-spec.md
|
|
35
|
+
flowy feature set "User Auth"
|
|
36
|
+
|
|
37
|
+
flowy task create --title "Implement OAuth" --description oauth.md
|
|
38
|
+
flowy task create --title "Write tests" --description "Unit + integration"
|
|
34
39
|
|
|
35
|
-
# Track status
|
|
36
40
|
flowy status <task-id> in_progress
|
|
37
41
|
flowy status <task-id> done
|
|
38
|
-
|
|
39
|
-
# Search and explore
|
|
40
|
-
flowy search "OAuth" --type task
|
|
41
|
-
flowy tree subtree <project-id> --depth 3
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
Every command outputs JSON. Your agent reads it, acts on it, moves to the next task.
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|---------|-------------|
|
|
48
|
-
| `register --email <email>` | Register and get API key |
|
|
49
|
-
| `whoami` | Show current user |
|
|
50
|
-
| `node create --type <type> --title <title> [--description] [--status] [--metadata]` | Create node |
|
|
51
|
-
| `node get --id <id>` | Get node |
|
|
52
|
-
| `node list [--type] [--status] [--limit] [--offset]` | List nodes |
|
|
53
|
-
| `node update --id <id> [--title] [--description] [--status] [--metadata]` | Update node |
|
|
54
|
-
| `node delete --id <id>` | Delete node |
|
|
55
|
-
| `status <id> <status>` | Update status (shorthand) |
|
|
56
|
-
| `approve <id>` | Approve node (must be pending_review) |
|
|
57
|
-
| `edge create --source <id> --target <id> --relation <rel>` | Create edge |
|
|
58
|
-
| `edge list [--node <id>] [--relation <rel>]` | List edges |
|
|
59
|
-
| `edge remove --source <id> --target <id> --relation <rel>` | Remove edge |
|
|
60
|
-
| `search <query> [--type] [--status] [--limit]` | Search nodes |
|
|
61
|
-
| `tree subtree <id> [--depth N]` | Show subtree |
|
|
62
|
-
| `tree ancestors <id> [--depth N] [--relation <rel>]` | Show ancestors |
|
|
63
|
-
| `tree descendants <id> [--depth N] [--relation <rel>]` | Show descendants |
|
|
46
|
+
## Agent Skill
|
|
64
47
|
|
|
65
|
-
|
|
48
|
+
Flowy installs an agent skill during setup. Your AI agent automatically knows every command. No manual configuration needed.
|
|
66
49
|
|
|
67
|
-
|
|
50
|
+
Or install the skill manually: `npx skills add sqaoss/flowy`
|
|
68
51
|
|
|
69
|
-
|
|
52
|
+
See [skills/using-flowy/SKILL.md](skills/using-flowy/SKILL.md) for the full skill reference.
|
|
70
53
|
|
|
71
|
-
|
|
54
|
+
## Data Model
|
|
72
55
|
|
|
73
|
-
|
|
56
|
+
```
|
|
57
|
+
project -> feature -> task
|
|
58
|
+
1:many 1:many
|
|
59
|
+
```
|
|
74
60
|
|
|
75
|
-
|
|
76
|
-
- `depends_on` -- must complete before starting
|
|
77
|
-
- `blocks` -- prevents progress on target
|
|
78
|
-
- `informs` -- provides context to target
|
|
61
|
+
Every task belongs to a feature. Every feature belongs to a project. No orphans.
|
|
79
62
|
|
|
80
63
|
### Status Flow
|
|
81
64
|
|
|
@@ -83,17 +66,68 @@ All commands output JSON.
|
|
|
83
66
|
draft -> pending_review -> approved -> in_progress -> done
|
|
84
67
|
```
|
|
85
68
|
|
|
86
|
-
Also: `blocked`, `cancelled`
|
|
69
|
+
Also: `blocked`, `cancelled`. Only `pending_review` entities can be approved.
|
|
70
|
+
|
|
71
|
+
## Self-Hosted
|
|
72
|
+
|
|
73
|
+
Run Flowy on your own machine with SQLite and Docker. Same CLI, same commands.
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
flowy setup local # starts a local server via Docker
|
|
77
|
+
flowy init # auto-detects repo
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Command Reference
|
|
81
|
+
|
|
82
|
+
| Command | Description |
|
|
83
|
+
|---------|-------------|
|
|
84
|
+
| `setup remote --email <email>` | Register and connect to the hosted server |
|
|
85
|
+
| `setup local` | Start a local Docker server and configure the CLI |
|
|
86
|
+
| `init` | Auto-detect repo and create/map project |
|
|
87
|
+
| `whoami` | Show current user |
|
|
88
|
+
| `client set name <name>` | Set client display name |
|
|
89
|
+
| `project create <name>` | Create project |
|
|
90
|
+
| `project set <name>` | Map current directory to project |
|
|
91
|
+
| `project list` | List all projects |
|
|
92
|
+
| `project show [<id>]` | Show project details |
|
|
93
|
+
| `feature create --title <t> --description <d>` | Create feature (requires active project) |
|
|
94
|
+
| `feature set <name-or-id>` | Set active feature |
|
|
95
|
+
| `feature unset` | Clear active feature |
|
|
96
|
+
| `feature list` | List features in active project |
|
|
97
|
+
| `feature show [<id>]` | Show feature details |
|
|
98
|
+
| `task create --title <t> --description <d>` | Create task (requires active feature) |
|
|
99
|
+
| `task list` | List tasks in active feature |
|
|
100
|
+
| `task show <id>` | Show task details |
|
|
101
|
+
| `task block <id1> <id2>` | Mark task as blocking another |
|
|
102
|
+
| `task unblock <id1> <id2>` | Remove block |
|
|
103
|
+
| `status <id> <status>` | Update status (shorthand) |
|
|
104
|
+
| `approve <id>` | Approve (must be pending_review) |
|
|
105
|
+
| `search <query> [--type] [--status] [--limit]` | Full-text search |
|
|
106
|
+
| `tree <id> [--depth N]` | Show subtree |
|
|
107
|
+
|
|
108
|
+
All commands output JSON to stdout.
|
|
87
109
|
|
|
88
110
|
## Configuration
|
|
89
111
|
|
|
112
|
+
Config is stored at `~/.config/flowy/config.json`.
|
|
113
|
+
|
|
90
114
|
| Variable | Description | Default |
|
|
91
115
|
|----------|-------------|---------|
|
|
92
116
|
| `FLOWY_API_URL` | GraphQL endpoint | `https://flowy-ai.fly.dev/graphql` |
|
|
93
|
-
| `FLOWY_API_KEY` | API key
|
|
117
|
+
| `FLOWY_API_KEY` | API key (remote mode) | -- |
|
|
118
|
+
| `FLOWY_PROJECT` | Override active project by name | -- |
|
|
119
|
+
| `FLOWY_FEATURE` | Override active feature by ID | -- |
|
|
94
120
|
|
|
95
|
-
##
|
|
121
|
+
## Development
|
|
96
122
|
|
|
97
|
-
|
|
123
|
+
```bash
|
|
124
|
+
bun run test # CLI tests
|
|
125
|
+
bun run check # Lint + format
|
|
126
|
+
bun run typecheck # TypeScript
|
|
127
|
+
|
|
128
|
+
cd server && bunx --bun vitest run # Server tests
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## License
|
|
98
132
|
|
|
99
|
-
|
|
133
|
+
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,20 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sqaoss/flowy",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Agentic persistent planning",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"flowy": "./src/index.ts"
|
|
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,16 +41,23 @@
|
|
|
33
41
|
"coding-agent",
|
|
34
42
|
"ai-agents",
|
|
35
43
|
"graphql",
|
|
36
|
-
"bun"
|
|
44
|
+
"bun",
|
|
45
|
+
"agentic",
|
|
46
|
+
"planning"
|
|
37
47
|
],
|
|
38
48
|
"scripts": {
|
|
39
49
|
"cli": "bun src/index.ts",
|
|
40
50
|
"check": "biome check --write .",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"test:watch": "vitest",
|
|
41
53
|
"typecheck": "tsc --noEmit",
|
|
42
54
|
"prepare": "husky"
|
|
43
55
|
},
|
|
44
56
|
"dependencies": {
|
|
45
|
-
"
|
|
57
|
+
"@semantic-release/changelog": "6.0.3",
|
|
58
|
+
"@semantic-release/git": "10.0.1",
|
|
59
|
+
"commander": "^14.0.3",
|
|
60
|
+
"semantic-release": "25.0.3"
|
|
46
61
|
},
|
|
47
62
|
"devDependencies": {
|
|
48
63
|
"@biomejs/biome": "2.4.4",
|
|
@@ -50,7 +65,9 @@
|
|
|
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
|
+
}
|