🎉 init: 小龙的工作空间

This commit is contained in:
大海
2026-06-06 10:40:48 +08:00
commit a188ee1426
3201 changed files with 231817 additions and 0 deletions
File diff suppressed because one or more lines are too long
+7
View File
@@ -0,0 +1,7 @@
node_modules/
dist/
data/
.env
*.db
*.db-journal
*.db-wal
+132
View File
@@ -0,0 +1,132 @@
# TeamFlow — Backend API
> 一款支持多端同步、Markdown 编辑和标签管理的笔记应用。
## Tech Stack
- **Runtime**: Node.js
- **Framework**: Fastify 5
- **Language**: TypeScript
- **Database**: SQLite (better-sqlite3)
- **Auth**: JWT + bcrypt
## Getting Started
```bash
# Install dependencies
npm install
# Development (hot reload)
npm run dev
# Build
npm run build
# Production start
npm run start
# Run tests
npm test
```
## Project Structure
```
src/
├── index.ts # Server entry point
├── db/
│ ├── schema.ts # SQLite schema
│ └── client.ts # Database client
├── routes/
│ ├── auth.ts # Auth routes (register/login/me)
│ └── *.ts # CRUD routes
├── services/
│ └── *.ts # Business logic
├── middleware/
│ └── auth.ts # JWT middleware
├── types/
│ └── index.ts # TypeScript types
└── __tests__/
└── *.test.ts # Tests
```
## API Endpoints
### Auth
- `POST /api/auth/register` — Register
- `POST /api/auth/login` — Login
- `GET /api/auth/me` — Current user (auth required)
### Resources
#### users
- `GET /api/users` — List all
- `GET /api/users/:id` — Get by ID
- `POST /api/users` — Create
- `PUT /api/users/:id` — Update
- `DELETE /api/users/:id` — Delete
#### boards
- `GET /api/boards` — List all
- `GET /api/boards/:id` — Get by ID
- `POST /api/boards` — Create
- `PUT /api/boards/:id` — Update
- `DELETE /api/boards/:id` — Delete
#### tasks
- `GET /api/tasks` — List all
- `GET /api/tasks/:id` — Get by ID
- `POST /api/tasks` — Create
- `PUT /api/tasks/:id` — Update
- `DELETE /api/tasks/:id` — Delete
#### members
- `GET /api/members` — List all
- `GET /api/members/:id` — Get by ID
- `POST /api/members` — Create
- `PUT /api/members/:id` — Update
- `DELETE /api/members/:id` — Delete
#### items
- `GET /api/items` — List all
- `GET /api/items/:id` — Get by ID
- `POST /api/items` — Create
- `PUT /api/items/:id` — Update
- `DELETE /api/items/:id` — Delete
#### activity_logs
- `GET /api/activity_logs` — List all
- `GET /api/activity_logs/:id` — Get by ID
- `POST /api/activity_logs` — Create
- `PUT /api/activity_logs/:id` — Update
- `DELETE /api/activity_logs/:id` — Delete
#### stats
- `GET /api/stats` — List all
- `GET /api/stats/:id` — Get by ID
- `POST /api/stats` — Create
- `PUT /api/stats/:id` — Update
- `DELETE /api/stats/:id` — Delete
#### notes
- `GET /api/notes` — List all
- `GET /api/notes/:id` — Get by ID
- `POST /api/notes` — Create
- `PUT /api/notes/:id` — Update
- `DELETE /api/notes/:id` — Delete
#### tags
- `GET /api/tags` — List all
- `GET /api/tags/:id` — Get by ID
- `POST /api/tags` — Create
- `PUT /api/tags/:id` — Update
- `DELETE /api/tags/:id` — Delete
#### note_tags
- `GET /api/note_tags` — List all
- `GET /api/note_tags/:id` — Get by ID
- `POST /api/note_tags` — Create
- `PUT /api/note_tags/:id` — Update
- `DELETE /api/note_tags/:id` — Delete
### System
- `GET /api/health` — Health check
+27
View File
@@ -0,0 +1,27 @@
{
"name": "teamflow",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js",
"test": "node --import tsx --test src/__tests__/*.test.ts",
"test:watch": "node --import tsx --test --watch src/__tests__/*.test.ts"
},
"dependencies": {
"fastify": "^5.0.0",
"@fastify/cors": "^10.0.0",
"@fastify/jwt": "^9.0.0",
"sql.js": "^1.12.0",
"bcrypt": "^5.1.0"
},
"devDependencies": {
"@types/node": "^22.0.0",
"@types/bcrypt": "^5.0.0",
"typescript": "^5.6.0",
"tsx": "^4.0.0",
"pino-pretty": "^11.0.0"
}
}
@@ -0,0 +1,122 @@
// ActivityLog CRUD test
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import { buildApp } from "../index.js";
import type { FastifyInstance } from "fastify";
let app: FastifyInstance;
let token: string;
let createdId: string;
before(async () => {
process.env.JWT_SECRET = "test-secret";
process.env.DATABASE_URL = ":memory:";
app = await buildApp();
await app.ready();
// Register and login to get a token
const regRes = await app.inject({
method: "POST",
url: "/api/auth/register",
payload: { username: `test_${Date.now()}`, password: "password123" },
});
token = regRes.json().token;
});
after(async () => {
await app.close();
});
describe("GET /api/activity_logs", () => {
it("returns empty list", async () => {
const res = await app.inject({
method: "GET",
url: "/api/activity_logs",
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 200);
const body = res.json();
assert.ok(Array.isArray(body.data));
});
});
describe("POST /api/activity_logs", () => {
it("creates a activity_log", async () => {
const res = await app.inject({
method: "POST",
url: "/api/activity_logs",
headers: { authorization: `Bearer ${token}` },
payload: {
"userId": "00000000-0000-0000-0000-000000000001",
"title": "sample-title",
"description": "sample-description",
"status": "sample-status",
"data": "sample-data"
},
});
assert.equal(res.statusCode, 201);
const body = res.json();
assert.ok(body.data.id);
createdId = body.data.id;
});
});
describe("GET /api/activity_logs/:id", () => {
it("returns the created activity_log", async () => {
const res = await app.inject({
method: "GET",
url: `/api/activity_logs/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 200);
const body = res.json();
assert.equal(body.data.id, createdId);
});
it("returns 404 for nonexistent id", async () => {
const res = await app.inject({
method: "GET",
url: `/api/activity_logs/nonexistent`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 404);
});
});
describe("PUT /api/activity_logs/:id", () => {
it("updates the activity_log", async () => {
const res = await app.inject({
method: "PUT",
url: `/api/activity_logs/${createdId}`,
headers: { authorization: `Bearer ${token}` },
payload: {
"userId": "00000000-0000-0000-0000-000000000001",
"title": "sample-title",
"description": "sample-description",
"status": "sample-status",
"data": "sample-data"
},
});
assert.equal(res.statusCode, 200);
});
});
describe("DELETE /api/activity_logs/:id", () => {
it("deletes the activity_log", async () => {
const res = await app.inject({
method: "DELETE",
url: `/api/activity_logs/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 204);
});
it("returns 404 after delete", async () => {
const res = await app.inject({
method: "GET",
url: `/api/activity_logs/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 404);
});
});
@@ -0,0 +1,96 @@
// Auth routes test
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import { buildApp } from "../index.js";
import type { FastifyInstance } from "fastify";
let app: FastifyInstance;
let token: string;
before(async () => {
process.env.JWT_SECRET = "test-secret";
process.env.DATABASE_URL = ":memory:";
app = await buildApp();
await app.ready();
});
after(async () => {
await app.close();
});
describe("POST /api/auth/register", () => {
it("registers a new user", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/register",
payload: { username: "testuser", password: "password123" },
});
assert.equal(res.statusCode, 201);
const body = res.json();
assert.ok(body.token);
assert.equal(body.user.username, "testuser");
token = body.token;
});
it("rejects duplicate username", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/register",
payload: { username: "testuser", password: "password123" },
});
assert.equal(res.statusCode, 409);
});
it("rejects short password", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/register",
payload: { username: "user2", password: "123" },
});
assert.equal(res.statusCode, 400);
});
});
describe("POST /api/auth/login", () => {
it("logs in with correct credentials", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/login",
payload: { username: "testuser", password: "password123" },
});
assert.equal(res.statusCode, 200);
const body = res.json();
assert.ok(body.token);
token = body.token;
});
it("rejects wrong password", async () => {
const res = await app.inject({
method: "POST",
url: "/api/auth/login",
payload: { username: "testuser", password: "wrongpassword" },
});
assert.equal(res.statusCode, 401);
});
});
describe("GET /api/auth/me", () => {
it("returns current user with valid token", async () => {
const res = await app.inject({
method: "GET",
url: "/api/auth/me",
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 200);
const body = res.json();
assert.equal(body.data.username, "testuser");
});
it("rejects without token", async () => {
const res = await app.inject({
method: "GET",
url: "/api/auth/me",
});
assert.equal(res.statusCode, 401);
});
});
@@ -0,0 +1,122 @@
// Board CRUD test
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import { buildApp } from "../index.js";
import type { FastifyInstance } from "fastify";
let app: FastifyInstance;
let token: string;
let createdId: string;
before(async () => {
process.env.JWT_SECRET = "test-secret";
process.env.DATABASE_URL = ":memory:";
app = await buildApp();
await app.ready();
// Register and login to get a token
const regRes = await app.inject({
method: "POST",
url: "/api/auth/register",
payload: { username: `test_${Date.now()}`, password: "password123" },
});
token = regRes.json().token;
});
after(async () => {
await app.close();
});
describe("GET /api/boards", () => {
it("returns empty list", async () => {
const res = await app.inject({
method: "GET",
url: "/api/boards",
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 200);
const body = res.json();
assert.ok(Array.isArray(body.data));
});
});
describe("POST /api/boards", () => {
it("creates a board", async () => {
const res = await app.inject({
method: "POST",
url: "/api/boards",
headers: { authorization: `Bearer ${token}` },
payload: {
"userId": "00000000-0000-0000-0000-000000000001",
"title": "sample-title",
"description": "sample-description",
"status": "sample-status",
"data": "sample-data"
},
});
assert.equal(res.statusCode, 201);
const body = res.json();
assert.ok(body.data.id);
createdId = body.data.id;
});
});
describe("GET /api/boards/:id", () => {
it("returns the created board", async () => {
const res = await app.inject({
method: "GET",
url: `/api/boards/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 200);
const body = res.json();
assert.equal(body.data.id, createdId);
});
it("returns 404 for nonexistent id", async () => {
const res = await app.inject({
method: "GET",
url: `/api/boards/nonexistent`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 404);
});
});
describe("PUT /api/boards/:id", () => {
it("updates the board", async () => {
const res = await app.inject({
method: "PUT",
url: `/api/boards/${createdId}`,
headers: { authorization: `Bearer ${token}` },
payload: {
"userId": "00000000-0000-0000-0000-000000000001",
"title": "sample-title",
"description": "sample-description",
"status": "sample-status",
"data": "sample-data"
},
});
assert.equal(res.statusCode, 200);
});
});
describe("DELETE /api/boards/:id", () => {
it("deletes the board", async () => {
const res = await app.inject({
method: "DELETE",
url: `/api/boards/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 204);
});
it("returns 404 after delete", async () => {
const res = await app.inject({
method: "GET",
url: `/api/boards/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 404);
});
});
@@ -0,0 +1,122 @@
// Item CRUD test
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import { buildApp } from "../index.js";
import type { FastifyInstance } from "fastify";
let app: FastifyInstance;
let token: string;
let createdId: string;
before(async () => {
process.env.JWT_SECRET = "test-secret";
process.env.DATABASE_URL = ":memory:";
app = await buildApp();
await app.ready();
// Register and login to get a token
const regRes = await app.inject({
method: "POST",
url: "/api/auth/register",
payload: { username: `test_${Date.now()}`, password: "password123" },
});
token = regRes.json().token;
});
after(async () => {
await app.close();
});
describe("GET /api/items", () => {
it("returns empty list", async () => {
const res = await app.inject({
method: "GET",
url: "/api/items",
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 200);
const body = res.json();
assert.ok(Array.isArray(body.data));
});
});
describe("POST /api/items", () => {
it("creates a item", async () => {
const res = await app.inject({
method: "POST",
url: "/api/items",
headers: { authorization: `Bearer ${token}` },
payload: {
"userId": "00000000-0000-0000-0000-000000000001",
"title": "sample-title",
"description": "sample-description",
"status": "sample-status",
"data": "sample-data"
},
});
assert.equal(res.statusCode, 201);
const body = res.json();
assert.ok(body.data.id);
createdId = body.data.id;
});
});
describe("GET /api/items/:id", () => {
it("returns the created item", async () => {
const res = await app.inject({
method: "GET",
url: `/api/items/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 200);
const body = res.json();
assert.equal(body.data.id, createdId);
});
it("returns 404 for nonexistent id", async () => {
const res = await app.inject({
method: "GET",
url: `/api/items/nonexistent`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 404);
});
});
describe("PUT /api/items/:id", () => {
it("updates the item", async () => {
const res = await app.inject({
method: "PUT",
url: `/api/items/${createdId}`,
headers: { authorization: `Bearer ${token}` },
payload: {
"userId": "00000000-0000-0000-0000-000000000001",
"title": "sample-title",
"description": "sample-description",
"status": "sample-status",
"data": "sample-data"
},
});
assert.equal(res.statusCode, 200);
});
});
describe("DELETE /api/items/:id", () => {
it("deletes the item", async () => {
const res = await app.inject({
method: "DELETE",
url: `/api/items/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 204);
});
it("returns 404 after delete", async () => {
const res = await app.inject({
method: "GET",
url: `/api/items/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 404);
});
});
@@ -0,0 +1,122 @@
// Member CRUD test
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import { buildApp } from "../index.js";
import type { FastifyInstance } from "fastify";
let app: FastifyInstance;
let token: string;
let createdId: string;
before(async () => {
process.env.JWT_SECRET = "test-secret";
process.env.DATABASE_URL = ":memory:";
app = await buildApp();
await app.ready();
// Register and login to get a token
const regRes = await app.inject({
method: "POST",
url: "/api/auth/register",
payload: { username: `test_${Date.now()}`, password: "password123" },
});
token = regRes.json().token;
});
after(async () => {
await app.close();
});
describe("GET /api/members", () => {
it("returns empty list", async () => {
const res = await app.inject({
method: "GET",
url: "/api/members",
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 200);
const body = res.json();
assert.ok(Array.isArray(body.data));
});
});
describe("POST /api/members", () => {
it("creates a member", async () => {
const res = await app.inject({
method: "POST",
url: "/api/members",
headers: { authorization: `Bearer ${token}` },
payload: {
"userId": "00000000-0000-0000-0000-000000000001",
"title": "sample-title",
"description": "sample-description",
"status": "sample-status",
"data": "sample-data"
},
});
assert.equal(res.statusCode, 201);
const body = res.json();
assert.ok(body.data.id);
createdId = body.data.id;
});
});
describe("GET /api/members/:id", () => {
it("returns the created member", async () => {
const res = await app.inject({
method: "GET",
url: `/api/members/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 200);
const body = res.json();
assert.equal(body.data.id, createdId);
});
it("returns 404 for nonexistent id", async () => {
const res = await app.inject({
method: "GET",
url: `/api/members/nonexistent`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 404);
});
});
describe("PUT /api/members/:id", () => {
it("updates the member", async () => {
const res = await app.inject({
method: "PUT",
url: `/api/members/${createdId}`,
headers: { authorization: `Bearer ${token}` },
payload: {
"userId": "00000000-0000-0000-0000-000000000001",
"title": "sample-title",
"description": "sample-description",
"status": "sample-status",
"data": "sample-data"
},
});
assert.equal(res.statusCode, 200);
});
});
describe("DELETE /api/members/:id", () => {
it("deletes the member", async () => {
const res = await app.inject({
method: "DELETE",
url: `/api/members/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 204);
});
it("returns 404 after delete", async () => {
const res = await app.inject({
method: "GET",
url: `/api/members/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 404);
});
});
@@ -0,0 +1,118 @@
// NoteTag CRUD test
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import { buildApp } from "../index.js";
import type { FastifyInstance } from "fastify";
let app: FastifyInstance;
let token: string;
let createdId: string;
before(async () => {
process.env.JWT_SECRET = "test-secret";
process.env.DATABASE_URL = ":memory:";
app = await buildApp();
await app.ready();
// Register and login to get a token
const regRes = await app.inject({
method: "POST",
url: "/api/auth/register",
payload: { username: `test_${Date.now()}`, password: "password123" },
});
token = regRes.json().token;
});
after(async () => {
await app.close();
});
describe("GET /api/note_tags", () => {
it("returns empty list", async () => {
const res = await app.inject({
method: "GET",
url: "/api/note_tags",
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 200);
const body = res.json();
assert.ok(Array.isArray(body.data));
});
});
describe("POST /api/note_tags", () => {
it("creates a note_tag", async () => {
const res = await app.inject({
method: "POST",
url: "/api/note_tags",
headers: { authorization: `Bearer ${token}` },
payload: {
"noteId": "00000000-0000-0000-0000-000000000001",
"tagId": "00000000-0000-0000-0000-000000000001",
"PRIMARY KEY (noteId, tagId)": "sample-primary key (noteid, tagid)"
},
});
assert.equal(res.statusCode, 201);
const body = res.json();
assert.ok(body.data.id);
createdId = body.data.id;
});
});
describe("GET /api/note_tags/:id", () => {
it("returns the created note_tag", async () => {
const res = await app.inject({
method: "GET",
url: `/api/note_tags/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 200);
const body = res.json();
assert.equal(body.data.id, createdId);
});
it("returns 404 for nonexistent id", async () => {
const res = await app.inject({
method: "GET",
url: `/api/note_tags/nonexistent`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 404);
});
});
describe("PUT /api/note_tags/:id", () => {
it("updates the note_tag", async () => {
const res = await app.inject({
method: "PUT",
url: `/api/note_tags/${createdId}`,
headers: { authorization: `Bearer ${token}` },
payload: {
"noteId": "00000000-0000-0000-0000-000000000001",
"tagId": "00000000-0000-0000-0000-000000000001",
"PRIMARY KEY (noteId, tagId)": "sample-primary key (noteid, tagid)"
},
});
assert.equal(res.statusCode, 200);
});
});
describe("DELETE /api/note_tags/:id", () => {
it("deletes the note_tag", async () => {
const res = await app.inject({
method: "DELETE",
url: `/api/note_tags/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 204);
});
it("returns 404 after delete", async () => {
const res = await app.inject({
method: "GET",
url: `/api/note_tags/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 404);
});
});
@@ -0,0 +1,122 @@
// Note CRUD test
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import { buildApp } from "../index.js";
import type { FastifyInstance } from "fastify";
let app: FastifyInstance;
let token: string;
let createdId: string;
before(async () => {
process.env.JWT_SECRET = "test-secret";
process.env.DATABASE_URL = ":memory:";
app = await buildApp();
await app.ready();
// Register and login to get a token
const regRes = await app.inject({
method: "POST",
url: "/api/auth/register",
payload: { username: `test_${Date.now()}`, password: "password123" },
});
token = regRes.json().token;
});
after(async () => {
await app.close();
});
describe("GET /api/notes", () => {
it("returns empty list", async () => {
const res = await app.inject({
method: "GET",
url: "/api/notes",
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 200);
const body = res.json();
assert.ok(Array.isArray(body.data));
});
});
describe("POST /api/notes", () => {
it("creates a note", async () => {
const res = await app.inject({
method: "POST",
url: "/api/notes",
headers: { authorization: `Bearer ${token}` },
payload: {
"userId": "00000000-0000-0000-0000-000000000001",
"title": "sample-title",
"content": "sample-content",
"isMarkdown": true,
"folderId": "00000000-0000-0000-0000-000000000001"
},
});
assert.equal(res.statusCode, 201);
const body = res.json();
assert.ok(body.data.id);
createdId = body.data.id;
});
});
describe("GET /api/notes/:id", () => {
it("returns the created note", async () => {
const res = await app.inject({
method: "GET",
url: `/api/notes/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 200);
const body = res.json();
assert.equal(body.data.id, createdId);
});
it("returns 404 for nonexistent id", async () => {
const res = await app.inject({
method: "GET",
url: `/api/notes/nonexistent`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 404);
});
});
describe("PUT /api/notes/:id", () => {
it("updates the note", async () => {
const res = await app.inject({
method: "PUT",
url: `/api/notes/${createdId}`,
headers: { authorization: `Bearer ${token}` },
payload: {
"userId": "00000000-0000-0000-0000-000000000001",
"title": "sample-title",
"content": "sample-content",
"isMarkdown": true,
"folderId": "00000000-0000-0000-0000-000000000001"
},
});
assert.equal(res.statusCode, 200);
});
});
describe("DELETE /api/notes/:id", () => {
it("deletes the note", async () => {
const res = await app.inject({
method: "DELETE",
url: `/api/notes/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 204);
});
it("returns 404 after delete", async () => {
const res = await app.inject({
method: "GET",
url: `/api/notes/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 404);
});
});
@@ -0,0 +1,122 @@
// Stat CRUD test
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import { buildApp } from "../index.js";
import type { FastifyInstance } from "fastify";
let app: FastifyInstance;
let token: string;
let createdId: string;
before(async () => {
process.env.JWT_SECRET = "test-secret";
process.env.DATABASE_URL = ":memory:";
app = await buildApp();
await app.ready();
// Register and login to get a token
const regRes = await app.inject({
method: "POST",
url: "/api/auth/register",
payload: { username: `test_${Date.now()}`, password: "password123" },
});
token = regRes.json().token;
});
after(async () => {
await app.close();
});
describe("GET /api/stats", () => {
it("returns empty list", async () => {
const res = await app.inject({
method: "GET",
url: "/api/stats",
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 200);
const body = res.json();
assert.ok(Array.isArray(body.data));
});
});
describe("POST /api/stats", () => {
it("creates a stat", async () => {
const res = await app.inject({
method: "POST",
url: "/api/stats",
headers: { authorization: `Bearer ${token}` },
payload: {
"userId": "00000000-0000-0000-0000-000000000001",
"title": "sample-title",
"description": "sample-description",
"status": "sample-status",
"data": "sample-data"
},
});
assert.equal(res.statusCode, 201);
const body = res.json();
assert.ok(body.data.id);
createdId = body.data.id;
});
});
describe("GET /api/stats/:id", () => {
it("returns the created stat", async () => {
const res = await app.inject({
method: "GET",
url: `/api/stats/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 200);
const body = res.json();
assert.equal(body.data.id, createdId);
});
it("returns 404 for nonexistent id", async () => {
const res = await app.inject({
method: "GET",
url: `/api/stats/nonexistent`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 404);
});
});
describe("PUT /api/stats/:id", () => {
it("updates the stat", async () => {
const res = await app.inject({
method: "PUT",
url: `/api/stats/${createdId}`,
headers: { authorization: `Bearer ${token}` },
payload: {
"userId": "00000000-0000-0000-0000-000000000001",
"title": "sample-title",
"description": "sample-description",
"status": "sample-status",
"data": "sample-data"
},
});
assert.equal(res.statusCode, 200);
});
});
describe("DELETE /api/stats/:id", () => {
it("deletes the stat", async () => {
const res = await app.inject({
method: "DELETE",
url: `/api/stats/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 204);
});
it("returns 404 after delete", async () => {
const res = await app.inject({
method: "GET",
url: `/api/stats/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 404);
});
});
@@ -0,0 +1,116 @@
// Tag CRUD test
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import { buildApp } from "../index.js";
import type { FastifyInstance } from "fastify";
let app: FastifyInstance;
let token: string;
let createdId: string;
before(async () => {
process.env.JWT_SECRET = "test-secret";
process.env.DATABASE_URL = ":memory:";
app = await buildApp();
await app.ready();
// Register and login to get a token
const regRes = await app.inject({
method: "POST",
url: "/api/auth/register",
payload: { username: `test_${Date.now()}`, password: "password123" },
});
token = regRes.json().token;
});
after(async () => {
await app.close();
});
describe("GET /api/tags", () => {
it("returns empty list", async () => {
const res = await app.inject({
method: "GET",
url: "/api/tags",
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 200);
const body = res.json();
assert.ok(Array.isArray(body.data));
});
});
describe("POST /api/tags", () => {
it("creates a tag", async () => {
const res = await app.inject({
method: "POST",
url: "/api/tags",
headers: { authorization: `Bearer ${token}` },
payload: {
"name": "sample-name",
"color": "sample-color"
},
});
assert.equal(res.statusCode, 201);
const body = res.json();
assert.ok(body.data.id);
createdId = body.data.id;
});
});
describe("GET /api/tags/:id", () => {
it("returns the created tag", async () => {
const res = await app.inject({
method: "GET",
url: `/api/tags/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 200);
const body = res.json();
assert.equal(body.data.id, createdId);
});
it("returns 404 for nonexistent id", async () => {
const res = await app.inject({
method: "GET",
url: `/api/tags/nonexistent`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 404);
});
});
describe("PUT /api/tags/:id", () => {
it("updates the tag", async () => {
const res = await app.inject({
method: "PUT",
url: `/api/tags/${createdId}`,
headers: { authorization: `Bearer ${token}` },
payload: {
"name": "sample-name",
"color": "sample-color"
},
});
assert.equal(res.statusCode, 200);
});
});
describe("DELETE /api/tags/:id", () => {
it("deletes the tag", async () => {
const res = await app.inject({
method: "DELETE",
url: `/api/tags/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 204);
});
it("returns 404 after delete", async () => {
const res = await app.inject({
method: "GET",
url: `/api/tags/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 404);
});
});
@@ -0,0 +1,122 @@
// Task CRUD test
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import { buildApp } from "../index.js";
import type { FastifyInstance } from "fastify";
let app: FastifyInstance;
let token: string;
let createdId: string;
before(async () => {
process.env.JWT_SECRET = "test-secret";
process.env.DATABASE_URL = ":memory:";
app = await buildApp();
await app.ready();
// Register and login to get a token
const regRes = await app.inject({
method: "POST",
url: "/api/auth/register",
payload: { username: `test_${Date.now()}`, password: "password123" },
});
token = regRes.json().token;
});
after(async () => {
await app.close();
});
describe("GET /api/tasks", () => {
it("returns empty list", async () => {
const res = await app.inject({
method: "GET",
url: "/api/tasks",
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 200);
const body = res.json();
assert.ok(Array.isArray(body.data));
});
});
describe("POST /api/tasks", () => {
it("creates a task", async () => {
const res = await app.inject({
method: "POST",
url: "/api/tasks",
headers: { authorization: `Bearer ${token}` },
payload: {
"userId": "00000000-0000-0000-0000-000000000001",
"title": "sample-title",
"description": "sample-description",
"status": "sample-status",
"data": "sample-data"
},
});
assert.equal(res.statusCode, 201);
const body = res.json();
assert.ok(body.data.id);
createdId = body.data.id;
});
});
describe("GET /api/tasks/:id", () => {
it("returns the created task", async () => {
const res = await app.inject({
method: "GET",
url: `/api/tasks/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 200);
const body = res.json();
assert.equal(body.data.id, createdId);
});
it("returns 404 for nonexistent id", async () => {
const res = await app.inject({
method: "GET",
url: `/api/tasks/nonexistent`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 404);
});
});
describe("PUT /api/tasks/:id", () => {
it("updates the task", async () => {
const res = await app.inject({
method: "PUT",
url: `/api/tasks/${createdId}`,
headers: { authorization: `Bearer ${token}` },
payload: {
"userId": "00000000-0000-0000-0000-000000000001",
"title": "sample-title",
"description": "sample-description",
"status": "sample-status",
"data": "sample-data"
},
});
assert.equal(res.statusCode, 200);
});
});
describe("DELETE /api/tasks/:id", () => {
it("deletes the task", async () => {
const res = await app.inject({
method: "DELETE",
url: `/api/tasks/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 204);
});
it("returns 404 after delete", async () => {
const res = await app.inject({
method: "GET",
url: `/api/tasks/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 404);
});
});
@@ -0,0 +1,118 @@
// User CRUD test
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import { buildApp } from "../index.js";
import type { FastifyInstance } from "fastify";
let app: FastifyInstance;
let token: string;
let createdId: string;
before(async () => {
process.env.JWT_SECRET = "test-secret";
process.env.DATABASE_URL = ":memory:";
app = await buildApp();
await app.ready();
// Register and login to get a token
const regRes = await app.inject({
method: "POST",
url: "/api/auth/register",
payload: { username: `test_${Date.now()}`, password: "password123" },
});
token = regRes.json().token;
});
after(async () => {
await app.close();
});
describe("GET /api/users", () => {
it("returns empty list", async () => {
const res = await app.inject({
method: "GET",
url: "/api/users",
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 200);
const body = res.json();
assert.ok(Array.isArray(body.data));
});
});
describe("POST /api/users", () => {
it("creates a user", async () => {
const res = await app.inject({
method: "POST",
url: "/api/users",
headers: { authorization: `Bearer ${token}` },
payload: {
"phone": "sample-phone",
"nickname": "sample-nickname",
"avatarUrl": "sample-avatarurl"
},
});
assert.equal(res.statusCode, 201);
const body = res.json();
assert.ok(body.data.id);
createdId = body.data.id;
});
});
describe("GET /api/users/:id", () => {
it("returns the created user", async () => {
const res = await app.inject({
method: "GET",
url: `/api/users/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 200);
const body = res.json();
assert.equal(body.data.id, createdId);
});
it("returns 404 for nonexistent id", async () => {
const res = await app.inject({
method: "GET",
url: `/api/users/nonexistent`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 404);
});
});
describe("PUT /api/users/:id", () => {
it("updates the user", async () => {
const res = await app.inject({
method: "PUT",
url: `/api/users/${createdId}`,
headers: { authorization: `Bearer ${token}` },
payload: {
"phone": "sample-phone",
"nickname": "sample-nickname",
"avatarUrl": "sample-avatarurl"
},
});
assert.equal(res.statusCode, 200);
});
});
describe("DELETE /api/users/:id", () => {
it("deletes the user", async () => {
const res = await app.inject({
method: "DELETE",
url: `/api/users/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 204);
});
it("returns 404 after delete", async () => {
const res = await app.inject({
method: "GET",
url: `/api/users/${createdId}`,
headers: { authorization: `Bearer ${token}` },
});
assert.equal(res.statusCode, 404);
});
});
@@ -0,0 +1,93 @@
// SQLite database client (sql.js — pure WASM, no native deps)
import initSqlJs, { type Database, type BindParams } from "sql.js";
import { createTables } from "./schema.js";
let db: Database | null = null;
let initPromise: Promise<Database> | null = null;
/** Initialize the database (call once at startup). */
export async function initDb(dbPath?: string): Promise<Database> {
if (db) return db;
if (initPromise) return initPromise;
initPromise = (async () => {
const SQL = await initSqlJs();
const path = dbPath || process.env.DATABASE_URL || ":memory:";
// Try to load existing database from file
let buffer: ArrayLike<number> | undefined;
if (path !== ":memory:") {
try {
const fs = await import("node:fs/promises");
const data = await fs.readFile(path);
buffer = new Uint8Array(data);
} catch {
// File doesn't exist yet — start fresh
}
}
db = new SQL.Database(buffer);
db.run("PRAGMA foreign_keys = ON");
createTables(db);
return db;
})();
return initPromise;
}
/** Get the initialized database (must call initDb first). */
export function getDb(): Database {
if (!db) throw new Error("Database not initialized. Call initDb() first.");
return db;
}
/** Save database to disk. */
export async function saveDb(dbPath?: string): Promise<void> {
if (!db) return;
const path = dbPath || process.env.DATABASE_URL || "./data/app.db";
if (path === ":memory:") return;
const fs = await import("node:fs/promises");
const { dirname } = await import("node:path");
await fs.mkdir(dirname(path), { recursive: true });
const data = db.export();
await fs.writeFile(path, Buffer.from(data));
}
export async function closeDb(): Promise<void> {
if (db) {
await saveDb();
db.close();
db = null;
initPromise = null;
}
}
// Helper: run a query and return all rows as objects
export function queryAll<T = Record<string, unknown>>(sql: string, params: BindParams = []): T[] {
const d = getDb();
const stmt = d.prepare(sql);
if (params) stmt.bind(params);
const results: T[] = [];
while (stmt.step()) {
const row = stmt.getAsObject();
results.push(row as unknown as T);
}
stmt.free();
return results;
}
// Helper: run a query and return the first row
export function queryOne<T = Record<string, unknown>>(sql: string, params: BindParams = []): T | undefined {
const rows = queryAll<T>(sql, params);
return rows[0];
}
// Helper: run a mutation and return { changes, lastInsertRowid }
export function execute(sql: string, params: BindParams = []): { changes: number; lastInsertRowid: number } {
const d = getDb();
d.run(sql, params);
return {
changes: d.getRowsModified(),
lastInsertRowid: 0,
};
}
@@ -0,0 +1,31 @@
// Auto-generated SQLite schema
import type { Database } from "sql.js";
export function createTables(db: Database): void {
const statements = [
"CREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n phone TEXT UNIQUE,\n nickname TEXT,\n avatar_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);",
"idx_users_phone ON users(phone)",
"CREATE TABLE IF NOT EXISTS boards (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n description TEXT,\n status TEXT DEFAULT 'active',\n data TEXT,\n created_at TEXT,\n updated_at TEXT\n);",
"idx_boards_user ON boards(user_id)",
"CREATE TABLE IF NOT EXISTS tasks (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n description TEXT,\n status TEXT DEFAULT 'active',\n data TEXT,\n created_at TEXT,\n updated_at TEXT\n);",
"idx_tasks_user ON tasks(user_id)",
"CREATE TABLE IF NOT EXISTS members (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n description TEXT,\n status TEXT DEFAULT 'active',\n data TEXT,\n created_at TEXT,\n updated_at TEXT\n);",
"idx_members_user ON members(user_id)",
"CREATE TABLE IF NOT EXISTS items (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n description TEXT,\n status TEXT DEFAULT 'active',\n data TEXT,\n created_at TEXT,\n updated_at TEXT\n);",
"idx_items_user ON items(user_id)",
"CREATE TABLE IF NOT EXISTS activity_logs (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n description TEXT,\n status TEXT DEFAULT 'active',\n data TEXT,\n created_at TEXT,\n updated_at TEXT\n);",
"idx_activity_logs_user ON activity_logs(user_id)",
"CREATE TABLE IF NOT EXISTS stats (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n description TEXT,\n status TEXT DEFAULT 'active',\n data TEXT,\n created_at TEXT,\n updated_at TEXT\n);",
"idx_stats_user ON stats(user_id)",
"CREATE TABLE IF NOT EXISTS notes (\n id TEXT PRIMARY KEY,\n user_id TEXT REFERENCES users(id),\n title TEXT,\n content TEXT,\n is_markdown INTEGER DEFAULT false,\n folder_id TEXT REFERENCES folders(id),\n created_at TEXT,\n updated_at TEXT\n);",
"idx_notes_user ON notes(user_id)",
"idx_notes_folder ON notes(folder_id)",
"idx_notes_fts ON notes USING GIN (to_tsvector('simple', title || ' ' || content))",
"CREATE TABLE IF NOT EXISTS tags (\n id TEXT PRIMARY KEY,\n name TEXT NOT NULL UNIQUE,\n color TEXT\n);",
"CREATE TABLE IF NOT EXISTS note_tags (\n note_id TEXT REFERENCES notes(id),\n tag_id TEXT REFERENCES tags(id)\n);"
];
for (const sql of statements) {
const trimmed = sql.trim();
if (trimmed) db.run(trimmed);
}
}
+79
View File
@@ -0,0 +1,79 @@
// TeamFlow — Fastify Backend Server
import Fastify from "fastify";
import cors from "@fastify/cors";
import fjwt from "@fastify/jwt";
import { initDb, closeDb } from "./db/client.js";
import { authRoutes } from "./routes/auth.js";
import { usersRoutes } from "./routes/users.js";
import { boardsRoutes } from "./routes/boards.js";
import { tasksRoutes } from "./routes/tasks.js";
import { membersRoutes } from "./routes/members.js";
import { itemsRoutes } from "./routes/items.js";
import { activity_logsRoutes } from "./routes/activity_logs.js";
import { statsRoutes } from "./routes/stats.js";
import { notesRoutes } from "./routes/notes.js";
import { tagsRoutes } from "./routes/tags.js";
import { note_tagsRoutes } from "./routes/note_tags.js";
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-355636d6";
export async function buildApp() {
const app = Fastify({
logger: {
level: process.env.LOG_LEVEL || "info",
transport: process.env.NODE_ENV !== "production"
? { target: "pino-pretty", options: { colorize: true } }
: undefined,
},
});
// Init database
await initDb();
// Plugins
await app.register(cors, {
origin: process.env.CORS_ORIGIN || "*",
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
});
await app.register(fjwt, { secret: JWT_SECRET });
// Routes
await app.register(authRoutes, { prefix: "/api/auth" });
await app.register(usersRoutes, { prefix: "/api/users" });
await app.register(boardsRoutes, { prefix: "/api/boards" });
await app.register(tasksRoutes, { prefix: "/api/tasks" });
await app.register(membersRoutes, { prefix: "/api/members" });
await app.register(itemsRoutes, { prefix: "/api/items" });
await app.register(activity_logsRoutes, { prefix: "/api/activity_logs" });
await app.register(statsRoutes, { prefix: "/api/stats" });
await app.register(notesRoutes, { prefix: "/api/notes" });
await app.register(tagsRoutes, { prefix: "/api/tags" });
await app.register(note_tagsRoutes, { prefix: "/api/note_tags" });
// Health check
app.get("/api/health", async () => ({ status: "ok", timestamp: new Date().toISOString() }));
// Graceful shutdown
app.addHook("onClose", async () => {
closeDb();
});
return app;
}
// Start server if called directly
const port = parseInt(process.env.PORT || "3001", 10);
const host = process.env.HOST || "0.0.0.0";
async function main() {
const app = await buildApp();
try {
await app.listen({ port, host });
} catch (err) {
app.log.error(err);
process.exit(1);
}
}
main();
@@ -0,0 +1,41 @@
// JWT Authentication Middleware
import type { FastifyRequest, FastifyReply } from "fastify";
import type { JwtPayload } from "../types/index.js";
/**
* Verify JWT token and attach user to request.
*/
export async function authenticate(request: FastifyRequest, reply: FastifyReply): Promise<void> {
try {
await request.jwtVerify();
} catch (err) {
reply.status(401).send({ error: "Unauthorized", message: "Invalid or expired token", statusCode: 401 });
}
}
/** Helper to get typed user from request (after authenticate). */
export function getUser(request: FastifyRequest): JwtPayload {
return request.user as unknown as JwtPayload;
}
/**
* Require admin role.
* Must be used after authenticate.
*/
export async function requireAdmin(request: FastifyRequest, reply: FastifyReply): Promise<void> {
const user = request.user as unknown as JwtPayload | undefined;
if (!user || user.role !== "admin") {
reply.status(403).send({ error: "Forbidden", message: "Admin access required", statusCode: 403 });
}
}
/**
* Optional auth: attach user if token present, but don't fail if missing.
*/
export async function optionalAuth(request: FastifyRequest): Promise<void> {
try {
await request.jwtVerify();
} catch {
// No token or invalid — continue without user
}
}
@@ -0,0 +1,51 @@
// Auto-generated ActivityLog routes
import type { FastifyInstance } from "fastify";
import { authenticate } from "../middleware/auth.js";
import { ActivityLogService } from "../services/activity_log.js";
import type { CreateActivityLogInput, UpdateActivityLogInput } from "../types/index.js";
const service = new ActivityLogService();
export async function activity_logsRoutes(app: FastifyInstance): Promise<void> {
// All routes require authentication
app.addHook("onRequest", authenticate);
// GET /api/activity_logs — list
app.get("/", async (request, reply) => {
const items = service.list();
return { data: items };
});
// GET /api/activity_logs/:id — get by id
app.get<{ Params: { id: string } }>("/:id", async (request, reply) => {
const item = service.getById(request.params.id);
if (!item) {
return reply.status(404).send({ error: "Not Found", message: "ActivityLog not found", statusCode: 404 });
}
return { data: item };
});
// POST /api/activity_logs — create
app.post<{ Body: CreateActivityLogInput }>("/", async (request, reply) => {
const item = service.create(request.body);
return reply.status(201).send({ data: item });
});
// PUT /api/activity_logs/:id — update
app.put<{ Params: { id: string }; Body: UpdateActivityLogInput }>("/:id", async (request, reply) => {
const item = service.update(request.params.id, request.body);
if (!item) {
return reply.status(404).send({ error: "Not Found", message: "ActivityLog not found", statusCode: 404 });
}
return { data: item };
});
// DELETE /api/activity_logs/:id — delete
app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => {
const deleted = service.delete(request.params.id);
if (!deleted) {
return reply.status(404).send({ error: "Not Found", message: "ActivityLog not found", statusCode: 404 });
}
return reply.status(204).send();
});
}
@@ -0,0 +1,121 @@
// Authentication routes
import type { FastifyInstance } from "fastify";
import bcrypt from "bcrypt";
import { queryOne, execute } from "../db/client.js";
import { authenticate } from "../middleware/auth.js";
import type { User, RegisterInput, LoginInput, AuthResponse } from "../types/index.js";
const SALT_ROUNDS = 10;
export async function authRoutes(app: FastifyInstance): Promise<void> {
// POST /api/auth/register
app.post<{ Body: RegisterInput }>("/register", async (request, reply) => {
const { username, password, nickname } = request.body;
if (!username || !password) {
return reply.status(400).send({
error: "Bad Request",
message: "Username and password are required",
statusCode: 400,
});
}
if (password.length < 6) {
return reply.status(400).send({
error: "Bad Request",
message: "Password must be at least 6 characters",
statusCode: 400,
});
}
const existing = queryOne("SELECT id FROM users WHERE username = ?", [username]);
if (existing) {
return reply.status(409).send({
error: "Conflict",
message: "Username already exists",
statusCode: 409,
});
}
const id = crypto.randomUUID();
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
const now = new Date().toISOString();
execute(
"INSERT INTO users (id, username, password_hash, nickname, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)",
[id, username, passwordHash, nickname || username, now, now]
);
const user = queryOne<User>(
"SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?",
[id]
);
if (!user) {
return reply.status(500).send({ error: "Internal Error", message: "Failed to create user", statusCode: 500 });
}
const token = app.jwt.sign({ userId: id, username, role: user.role });
return reply.status(201).send({ token, user } satisfies AuthResponse);
});
// POST /api/auth/login
app.post<{ Body: LoginInput }>("/login", async (request, reply) => {
const { username, password } = request.body;
if (!username || !password) {
return reply.status(400).send({
error: "Bad Request",
message: "Username and password are required",
statusCode: 400,
});
}
const user = queryOne<User & { password_hash: string }>(
"SELECT * FROM users WHERE username = ?",
[username]
);
if (!user) {
return reply.status(401).send({
error: "Unauthorized",
message: "Invalid username or password",
statusCode: 401,
});
}
const valid = await bcrypt.compare(password, user.password_hash);
if (!valid) {
return reply.status(401).send({
error: "Unauthorized",
message: "Invalid username or password",
statusCode: 401,
});
}
const token = app.jwt.sign({ userId: user.id, username: user.username, role: user.role });
const { password_hash, ...safeUser } = user;
return { token, user: safeUser } satisfies AuthResponse;
});
// GET /api/auth/me — current user info
app.get("/me", { onRequest: [authenticate] }, async (request, reply) => {
const jwtUser = request.user as unknown as { userId: string };
const user = queryOne<User>(
"SELECT id, username, nickname, role, created_at, updated_at FROM users WHERE id = ?",
[jwtUser.userId]
);
if (!user) {
return reply.status(404).send({
error: "Not Found",
message: "User not found",
statusCode: 404,
});
}
return { data: user };
});
}
@@ -0,0 +1,51 @@
// Auto-generated Board routes
import type { FastifyInstance } from "fastify";
import { authenticate } from "../middleware/auth.js";
import { BoardService } from "../services/board.js";
import type { CreateBoardInput, UpdateBoardInput } from "../types/index.js";
const service = new BoardService();
export async function boardsRoutes(app: FastifyInstance): Promise<void> {
// All routes require authentication
app.addHook("onRequest", authenticate);
// GET /api/boards — list
app.get("/", async (request, reply) => {
const items = service.list();
return { data: items };
});
// GET /api/boards/:id — get by id
app.get<{ Params: { id: string } }>("/:id", async (request, reply) => {
const item = service.getById(request.params.id);
if (!item) {
return reply.status(404).send({ error: "Not Found", message: "Board not found", statusCode: 404 });
}
return { data: item };
});
// POST /api/boards — create
app.post<{ Body: CreateBoardInput }>("/", async (request, reply) => {
const item = service.create(request.body);
return reply.status(201).send({ data: item });
});
// PUT /api/boards/:id — update
app.put<{ Params: { id: string }; Body: UpdateBoardInput }>("/:id", async (request, reply) => {
const item = service.update(request.params.id, request.body);
if (!item) {
return reply.status(404).send({ error: "Not Found", message: "Board not found", statusCode: 404 });
}
return { data: item };
});
// DELETE /api/boards/:id — delete
app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => {
const deleted = service.delete(request.params.id);
if (!deleted) {
return reply.status(404).send({ error: "Not Found", message: "Board not found", statusCode: 404 });
}
return reply.status(204).send();
});
}
@@ -0,0 +1,51 @@
// Auto-generated Item routes
import type { FastifyInstance } from "fastify";
import { authenticate } from "../middleware/auth.js";
import { ItemService } from "../services/item.js";
import type { CreateItemInput, UpdateItemInput } from "../types/index.js";
const service = new ItemService();
export async function itemsRoutes(app: FastifyInstance): Promise<void> {
// All routes require authentication
app.addHook("onRequest", authenticate);
// GET /api/items — list
app.get("/", async (request, reply) => {
const items = service.list();
return { data: items };
});
// GET /api/items/:id — get by id
app.get<{ Params: { id: string } }>("/:id", async (request, reply) => {
const item = service.getById(request.params.id);
if (!item) {
return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 });
}
return { data: item };
});
// POST /api/items — create
app.post<{ Body: CreateItemInput }>("/", async (request, reply) => {
const item = service.create(request.body);
return reply.status(201).send({ data: item });
});
// PUT /api/items/:id — update
app.put<{ Params: { id: string }; Body: UpdateItemInput }>("/:id", async (request, reply) => {
const item = service.update(request.params.id, request.body);
if (!item) {
return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 });
}
return { data: item };
});
// DELETE /api/items/:id — delete
app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => {
const deleted = service.delete(request.params.id);
if (!deleted) {
return reply.status(404).send({ error: "Not Found", message: "Item not found", statusCode: 404 });
}
return reply.status(204).send();
});
}
@@ -0,0 +1,51 @@
// Auto-generated Member routes
import type { FastifyInstance } from "fastify";
import { authenticate } from "../middleware/auth.js";
import { MemberService } from "../services/member.js";
import type { CreateMemberInput, UpdateMemberInput } from "../types/index.js";
const service = new MemberService();
export async function membersRoutes(app: FastifyInstance): Promise<void> {
// All routes require authentication
app.addHook("onRequest", authenticate);
// GET /api/members — list
app.get("/", async (request, reply) => {
const items = service.list();
return { data: items };
});
// GET /api/members/:id — get by id
app.get<{ Params: { id: string } }>("/:id", async (request, reply) => {
const item = service.getById(request.params.id);
if (!item) {
return reply.status(404).send({ error: "Not Found", message: "Member not found", statusCode: 404 });
}
return { data: item };
});
// POST /api/members — create
app.post<{ Body: CreateMemberInput }>("/", async (request, reply) => {
const item = service.create(request.body);
return reply.status(201).send({ data: item });
});
// PUT /api/members/:id — update
app.put<{ Params: { id: string }; Body: UpdateMemberInput }>("/:id", async (request, reply) => {
const item = service.update(request.params.id, request.body);
if (!item) {
return reply.status(404).send({ error: "Not Found", message: "Member not found", statusCode: 404 });
}
return { data: item };
});
// DELETE /api/members/:id — delete
app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => {
const deleted = service.delete(request.params.id);
if (!deleted) {
return reply.status(404).send({ error: "Not Found", message: "Member not found", statusCode: 404 });
}
return reply.status(204).send();
});
}
@@ -0,0 +1,51 @@
// Auto-generated NoteTag routes
import type { FastifyInstance } from "fastify";
import { authenticate } from "../middleware/auth.js";
import { NoteTagService } from "../services/note_tag.js";
import type { CreateNoteTagInput, UpdateNoteTagInput } from "../types/index.js";
const service = new NoteTagService();
export async function note_tagsRoutes(app: FastifyInstance): Promise<void> {
// All routes require authentication
app.addHook("onRequest", authenticate);
// GET /api/note_tags — list
app.get("/", async (request, reply) => {
const items = service.list();
return { data: items };
});
// GET /api/note_tags/:id — get by id
app.get<{ Params: { id: string } }>("/:id", async (request, reply) => {
const item = service.getById(request.params.id);
if (!item) {
return reply.status(404).send({ error: "Not Found", message: "NoteTag not found", statusCode: 404 });
}
return { data: item };
});
// POST /api/note_tags — create
app.post<{ Body: CreateNoteTagInput }>("/", async (request, reply) => {
const item = service.create(request.body);
return reply.status(201).send({ data: item });
});
// PUT /api/note_tags/:id — update
app.put<{ Params: { id: string }; Body: UpdateNoteTagInput }>("/:id", async (request, reply) => {
const item = service.update(request.params.id, request.body);
if (!item) {
return reply.status(404).send({ error: "Not Found", message: "NoteTag not found", statusCode: 404 });
}
return { data: item };
});
// DELETE /api/note_tags/:id — delete
app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => {
const deleted = service.delete(request.params.id);
if (!deleted) {
return reply.status(404).send({ error: "Not Found", message: "NoteTag not found", statusCode: 404 });
}
return reply.status(204).send();
});
}
@@ -0,0 +1,51 @@
// Auto-generated Note routes
import type { FastifyInstance } from "fastify";
import { authenticate } from "../middleware/auth.js";
import { NoteService } from "../services/note.js";
import type { CreateNoteInput, UpdateNoteInput } from "../types/index.js";
const service = new NoteService();
export async function notesRoutes(app: FastifyInstance): Promise<void> {
// All routes require authentication
app.addHook("onRequest", authenticate);
// GET /api/notes — list
app.get("/", async (request, reply) => {
const items = service.list();
return { data: items };
});
// GET /api/notes/:id — get by id
app.get<{ Params: { id: string } }>("/:id", async (request, reply) => {
const item = service.getById(request.params.id);
if (!item) {
return reply.status(404).send({ error: "Not Found", message: "Note not found", statusCode: 404 });
}
return { data: item };
});
// POST /api/notes — create
app.post<{ Body: CreateNoteInput }>("/", async (request, reply) => {
const item = service.create(request.body);
return reply.status(201).send({ data: item });
});
// PUT /api/notes/:id — update
app.put<{ Params: { id: string }; Body: UpdateNoteInput }>("/:id", async (request, reply) => {
const item = service.update(request.params.id, request.body);
if (!item) {
return reply.status(404).send({ error: "Not Found", message: "Note not found", statusCode: 404 });
}
return { data: item };
});
// DELETE /api/notes/:id — delete
app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => {
const deleted = service.delete(request.params.id);
if (!deleted) {
return reply.status(404).send({ error: "Not Found", message: "Note not found", statusCode: 404 });
}
return reply.status(204).send();
});
}
@@ -0,0 +1,51 @@
// Auto-generated Stat routes
import type { FastifyInstance } from "fastify";
import { authenticate } from "../middleware/auth.js";
import { StatService } from "../services/stat.js";
import type { CreateStatInput, UpdateStatInput } from "../types/index.js";
const service = new StatService();
export async function statsRoutes(app: FastifyInstance): Promise<void> {
// All routes require authentication
app.addHook("onRequest", authenticate);
// GET /api/stats — list
app.get("/", async (request, reply) => {
const items = service.list();
return { data: items };
});
// GET /api/stats/:id — get by id
app.get<{ Params: { id: string } }>("/:id", async (request, reply) => {
const item = service.getById(request.params.id);
if (!item) {
return reply.status(404).send({ error: "Not Found", message: "Stat not found", statusCode: 404 });
}
return { data: item };
});
// POST /api/stats — create
app.post<{ Body: CreateStatInput }>("/", async (request, reply) => {
const item = service.create(request.body);
return reply.status(201).send({ data: item });
});
// PUT /api/stats/:id — update
app.put<{ Params: { id: string }; Body: UpdateStatInput }>("/:id", async (request, reply) => {
const item = service.update(request.params.id, request.body);
if (!item) {
return reply.status(404).send({ error: "Not Found", message: "Stat not found", statusCode: 404 });
}
return { data: item };
});
// DELETE /api/stats/:id — delete
app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => {
const deleted = service.delete(request.params.id);
if (!deleted) {
return reply.status(404).send({ error: "Not Found", message: "Stat not found", statusCode: 404 });
}
return reply.status(204).send();
});
}
@@ -0,0 +1,51 @@
// Auto-generated Tag routes
import type { FastifyInstance } from "fastify";
import { authenticate } from "../middleware/auth.js";
import { TagService } from "../services/tag.js";
import type { CreateTagInput, UpdateTagInput } from "../types/index.js";
const service = new TagService();
export async function tagsRoutes(app: FastifyInstance): Promise<void> {
// All routes require authentication
app.addHook("onRequest", authenticate);
// GET /api/tags — list
app.get("/", async (request, reply) => {
const items = service.list();
return { data: items };
});
// GET /api/tags/:id — get by id
app.get<{ Params: { id: string } }>("/:id", async (request, reply) => {
const item = service.getById(request.params.id);
if (!item) {
return reply.status(404).send({ error: "Not Found", message: "Tag not found", statusCode: 404 });
}
return { data: item };
});
// POST /api/tags — create
app.post<{ Body: CreateTagInput }>("/", async (request, reply) => {
const item = service.create(request.body);
return reply.status(201).send({ data: item });
});
// PUT /api/tags/:id — update
app.put<{ Params: { id: string }; Body: UpdateTagInput }>("/:id", async (request, reply) => {
const item = service.update(request.params.id, request.body);
if (!item) {
return reply.status(404).send({ error: "Not Found", message: "Tag not found", statusCode: 404 });
}
return { data: item };
});
// DELETE /api/tags/:id — delete
app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => {
const deleted = service.delete(request.params.id);
if (!deleted) {
return reply.status(404).send({ error: "Not Found", message: "Tag not found", statusCode: 404 });
}
return reply.status(204).send();
});
}
@@ -0,0 +1,51 @@
// Auto-generated Task routes
import type { FastifyInstance } from "fastify";
import { authenticate } from "../middleware/auth.js";
import { TaskService } from "../services/task.js";
import type { CreateTaskInput, UpdateTaskInput } from "../types/index.js";
const service = new TaskService();
export async function tasksRoutes(app: FastifyInstance): Promise<void> {
// All routes require authentication
app.addHook("onRequest", authenticate);
// GET /api/tasks — list
app.get("/", async (request, reply) => {
const items = service.list();
return { data: items };
});
// GET /api/tasks/:id — get by id
app.get<{ Params: { id: string } }>("/:id", async (request, reply) => {
const item = service.getById(request.params.id);
if (!item) {
return reply.status(404).send({ error: "Not Found", message: "Task not found", statusCode: 404 });
}
return { data: item };
});
// POST /api/tasks — create
app.post<{ Body: CreateTaskInput }>("/", async (request, reply) => {
const item = service.create(request.body);
return reply.status(201).send({ data: item });
});
// PUT /api/tasks/:id — update
app.put<{ Params: { id: string }; Body: UpdateTaskInput }>("/:id", async (request, reply) => {
const item = service.update(request.params.id, request.body);
if (!item) {
return reply.status(404).send({ error: "Not Found", message: "Task not found", statusCode: 404 });
}
return { data: item };
});
// DELETE /api/tasks/:id — delete
app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => {
const deleted = service.delete(request.params.id);
if (!deleted) {
return reply.status(404).send({ error: "Not Found", message: "Task not found", statusCode: 404 });
}
return reply.status(204).send();
});
}
@@ -0,0 +1,51 @@
// Auto-generated User routes
import type { FastifyInstance } from "fastify";
import { authenticate } from "../middleware/auth.js";
import { UserService } from "../services/user.js";
import type { CreateUserInput, UpdateUserInput } from "../types/index.js";
const service = new UserService();
export async function usersRoutes(app: FastifyInstance): Promise<void> {
// All routes require authentication
app.addHook("onRequest", authenticate);
// GET /api/users — list
app.get("/", async (request, reply) => {
const items = service.list();
return { data: items };
});
// GET /api/users/:id — get by id
app.get<{ Params: { id: string } }>("/:id", async (request, reply) => {
const item = service.getById(request.params.id);
if (!item) {
return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 });
}
return { data: item };
});
// POST /api/users — create
app.post<{ Body: CreateUserInput }>("/", async (request, reply) => {
const item = service.create(request.body);
return reply.status(201).send({ data: item });
});
// PUT /api/users/:id — update
app.put<{ Params: { id: string }; Body: UpdateUserInput }>("/:id", async (request, reply) => {
const item = service.update(request.params.id, request.body);
if (!item) {
return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 });
}
return { data: item };
});
// DELETE /api/users/:id — delete
app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => {
const deleted = service.delete(request.params.id);
if (!deleted) {
return reply.status(404).send({ error: "Not Found", message: "User not found", statusCode: 404 });
}
return reply.status(204).send();
});
}
@@ -0,0 +1,58 @@
// Auto-generated ActivityLog service
import { queryAll, queryOne, execute } from "../db/client.js";
import type { ActivityLog, CreateActivityLogInput, UpdateActivityLogInput } from "../types/index.js";
export class ActivityLogService {
/** List all activity_logs */
list(): ActivityLog[] {
return queryAll<ActivityLog>("SELECT * FROM activity_logs ORDER BY created_at DESC");
}
/** Get by ID */
getById(id: string): ActivityLog | undefined {
return queryOne<ActivityLog>("SELECT * FROM activity_logs WHERE id = ?", [id]);
}
/** Create */
create(input: CreateActivityLogInput): ActivityLog {
const id = crypto.randomUUID();
const now = new Date().toISOString();
const hasCreatedAt = true;
const hasUpdatedAt = true;
const cols = ["id", "user_id", "title", "description", "status", "data", "created_at", "updated_at"];
const placeholders = cols.map(() => "?").join(", ");
const values = [id, input.userId ?? null, input.title ?? null, input.description ?? null, input.status ?? null, input.data ?? null, now, now];
execute(`INSERT INTO activity_logs (${cols.join(", ")}) VALUES (${placeholders})`, values);
return this.getById(id)!;
}
/** Update */
update(id: string, input: UpdateActivityLogInput): ActivityLog | undefined {
const existing = this.getById(id);
if (!existing) return undefined;
const sets: string[] = [];
const values: unknown[] = [];
if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); }
if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); }
if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); }
if (input.status !== undefined) { sets.push("status = ?"); values.push(input.status); }
if (input.data !== undefined) { sets.push("data = ?"); values.push(input.data); }
if (sets.length === 0) return existing;
sets.push("updated_at = ?");
values.push(new Date().toISOString());
values.push(id);
execute(`UPDATE activity_logs SET ${sets.join(", ")} WHERE id = ?`, values);
return this.getById(id)!;
}
/** Delete */
delete(id: string): boolean {
const result = execute("DELETE FROM activity_logs WHERE id = ?", [id]);
return result.changes > 0;
}
}
@@ -0,0 +1,58 @@
// Auto-generated Board service
import { queryAll, queryOne, execute } from "../db/client.js";
import type { Board, CreateBoardInput, UpdateBoardInput } from "../types/index.js";
export class BoardService {
/** List all boards */
list(): Board[] {
return queryAll<Board>("SELECT * FROM boards ORDER BY created_at DESC");
}
/** Get by ID */
getById(id: string): Board | undefined {
return queryOne<Board>("SELECT * FROM boards WHERE id = ?", [id]);
}
/** Create */
create(input: CreateBoardInput): Board {
const id = crypto.randomUUID();
const now = new Date().toISOString();
const hasCreatedAt = true;
const hasUpdatedAt = true;
const cols = ["id", "user_id", "title", "description", "status", "data", "created_at", "updated_at"];
const placeholders = cols.map(() => "?").join(", ");
const values = [id, input.userId ?? null, input.title ?? null, input.description ?? null, input.status ?? null, input.data ?? null, now, now];
execute(`INSERT INTO boards (${cols.join(", ")}) VALUES (${placeholders})`, values);
return this.getById(id)!;
}
/** Update */
update(id: string, input: UpdateBoardInput): Board | undefined {
const existing = this.getById(id);
if (!existing) return undefined;
const sets: string[] = [];
const values: unknown[] = [];
if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); }
if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); }
if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); }
if (input.status !== undefined) { sets.push("status = ?"); values.push(input.status); }
if (input.data !== undefined) { sets.push("data = ?"); values.push(input.data); }
if (sets.length === 0) return existing;
sets.push("updated_at = ?");
values.push(new Date().toISOString());
values.push(id);
execute(`UPDATE boards SET ${sets.join(", ")} WHERE id = ?`, values);
return this.getById(id)!;
}
/** Delete */
delete(id: string): boolean {
const result = execute("DELETE FROM boards WHERE id = ?", [id]);
return result.changes > 0;
}
}
@@ -0,0 +1,58 @@
// Auto-generated Item service
import { queryAll, queryOne, execute } from "../db/client.js";
import type { Item, CreateItemInput, UpdateItemInput } from "../types/index.js";
export class ItemService {
/** List all items */
list(): Item[] {
return queryAll<Item>("SELECT * FROM items ORDER BY created_at DESC");
}
/** Get by ID */
getById(id: string): Item | undefined {
return queryOne<Item>("SELECT * FROM items WHERE id = ?", [id]);
}
/** Create */
create(input: CreateItemInput): Item {
const id = crypto.randomUUID();
const now = new Date().toISOString();
const hasCreatedAt = true;
const hasUpdatedAt = true;
const cols = ["id", "user_id", "title", "description", "status", "data", "created_at", "updated_at"];
const placeholders = cols.map(() => "?").join(", ");
const values = [id, input.userId ?? null, input.title ?? null, input.description ?? null, input.status ?? null, input.data ?? null, now, now];
execute(`INSERT INTO items (${cols.join(", ")}) VALUES (${placeholders})`, values);
return this.getById(id)!;
}
/** Update */
update(id: string, input: UpdateItemInput): Item | undefined {
const existing = this.getById(id);
if (!existing) return undefined;
const sets: string[] = [];
const values: unknown[] = [];
if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); }
if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); }
if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); }
if (input.status !== undefined) { sets.push("status = ?"); values.push(input.status); }
if (input.data !== undefined) { sets.push("data = ?"); values.push(input.data); }
if (sets.length === 0) return existing;
sets.push("updated_at = ?");
values.push(new Date().toISOString());
values.push(id);
execute(`UPDATE items SET ${sets.join(", ")} WHERE id = ?`, values);
return this.getById(id)!;
}
/** Delete */
delete(id: string): boolean {
const result = execute("DELETE FROM items WHERE id = ?", [id]);
return result.changes > 0;
}
}
@@ -0,0 +1,58 @@
// Auto-generated Member service
import { queryAll, queryOne, execute } from "../db/client.js";
import type { Member, CreateMemberInput, UpdateMemberInput } from "../types/index.js";
export class MemberService {
/** List all members */
list(): Member[] {
return queryAll<Member>("SELECT * FROM members ORDER BY created_at DESC");
}
/** Get by ID */
getById(id: string): Member | undefined {
return queryOne<Member>("SELECT * FROM members WHERE id = ?", [id]);
}
/** Create */
create(input: CreateMemberInput): Member {
const id = crypto.randomUUID();
const now = new Date().toISOString();
const hasCreatedAt = true;
const hasUpdatedAt = true;
const cols = ["id", "user_id", "title", "description", "status", "data", "created_at", "updated_at"];
const placeholders = cols.map(() => "?").join(", ");
const values = [id, input.userId ?? null, input.title ?? null, input.description ?? null, input.status ?? null, input.data ?? null, now, now];
execute(`INSERT INTO members (${cols.join(", ")}) VALUES (${placeholders})`, values);
return this.getById(id)!;
}
/** Update */
update(id: string, input: UpdateMemberInput): Member | undefined {
const existing = this.getById(id);
if (!existing) return undefined;
const sets: string[] = [];
const values: unknown[] = [];
if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); }
if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); }
if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); }
if (input.status !== undefined) { sets.push("status = ?"); values.push(input.status); }
if (input.data !== undefined) { sets.push("data = ?"); values.push(input.data); }
if (sets.length === 0) return existing;
sets.push("updated_at = ?");
values.push(new Date().toISOString());
values.push(id);
execute(`UPDATE members SET ${sets.join(", ")} WHERE id = ?`, values);
return this.getById(id)!;
}
/** Delete */
delete(id: string): boolean {
const result = execute("DELETE FROM members WHERE id = ?", [id]);
return result.changes > 0;
}
}
@@ -0,0 +1,58 @@
// Auto-generated Note service
import { queryAll, queryOne, execute } from "../db/client.js";
import type { Note, CreateNoteInput, UpdateNoteInput } from "../types/index.js";
export class NoteService {
/** List all notes */
list(): Note[] {
return queryAll<Note>("SELECT * FROM notes ORDER BY created_at DESC");
}
/** Get by ID */
getById(id: string): Note | undefined {
return queryOne<Note>("SELECT * FROM notes WHERE id = ?", [id]);
}
/** Create */
create(input: CreateNoteInput): Note {
const id = crypto.randomUUID();
const now = new Date().toISOString();
const hasCreatedAt = true;
const hasUpdatedAt = true;
const cols = ["id", "user_id", "title", "content", "is_markdown", "folder_id", "created_at", "updated_at"];
const placeholders = cols.map(() => "?").join(", ");
const values = [id, input.userId ?? null, input.title ?? null, input.content ?? null, input.isMarkdown ?? null, input.folderId ?? null, now, now];
execute(`INSERT INTO notes (${cols.join(", ")}) VALUES (${placeholders})`, values);
return this.getById(id)!;
}
/** Update */
update(id: string, input: UpdateNoteInput): Note | undefined {
const existing = this.getById(id);
if (!existing) return undefined;
const sets: string[] = [];
const values: unknown[] = [];
if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); }
if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); }
if (input.content !== undefined) { sets.push("content = ?"); values.push(input.content); }
if (input.isMarkdown !== undefined) { sets.push("is_markdown = ?"); values.push(input.isMarkdown); }
if (input.folderId !== undefined) { sets.push("folder_id = ?"); values.push(input.folderId); }
if (sets.length === 0) return existing;
sets.push("updated_at = ?");
values.push(new Date().toISOString());
values.push(id);
execute(`UPDATE notes SET ${sets.join(", ")} WHERE id = ?`, values);
return this.getById(id)!;
}
/** Delete */
delete(id: string): boolean {
const result = execute("DELETE FROM notes WHERE id = ?", [id]);
return result.changes > 0;
}
}
@@ -0,0 +1,54 @@
// Auto-generated NoteTag service
import { queryAll, queryOne, execute } from "../db/client.js";
import type { NoteTag, CreateNoteTagInput, UpdateNoteTagInput } from "../types/index.js";
export class NoteTagService {
/** List all note_tags */
list(): NoteTag[] {
return queryAll<NoteTag>("SELECT * FROM note_tags ORDER BY created_at DESC");
}
/** Get by ID */
getById(id: string): NoteTag | undefined {
return queryOne<NoteTag>("SELECT * FROM note_tags WHERE id = ?", [id]);
}
/** Create */
create(input: CreateNoteTagInput): NoteTag {
const id = crypto.randomUUID();
const now = new Date().toISOString();
const hasCreatedAt = false;
const hasUpdatedAt = false;
const cols = ["id", "note_id", "tag_id"];
const placeholders = cols.map(() => "?").join(", ");
const values = [id, input.noteId ?? null, input.tagId ?? null];
execute(`INSERT INTO note_tags (${cols.join(", ")}) VALUES (${placeholders})`, values);
return this.getById(id)!;
}
/** Update */
update(id: string, input: UpdateNoteTagInput): NoteTag | undefined {
const existing = this.getById(id);
if (!existing) return undefined;
const sets: string[] = [];
const values: unknown[] = [];
if (input.noteId !== undefined) { sets.push("note_id = ?"); values.push(input.noteId); }
if (input.tagId !== undefined) { sets.push("tag_id = ?"); values.push(input.tagId); }
if (sets.length === 0) return existing;
values.push(id);
execute(`UPDATE note_tags SET ${sets.join(", ")} WHERE id = ?`, values);
return this.getById(id)!;
}
/** Delete */
delete(id: string): boolean {
const result = execute("DELETE FROM note_tags WHERE id = ?", [id]);
return result.changes > 0;
}
}
@@ -0,0 +1,58 @@
// Auto-generated Stat service
import { queryAll, queryOne, execute } from "../db/client.js";
import type { Stat, CreateStatInput, UpdateStatInput } from "../types/index.js";
export class StatService {
/** List all stats */
list(): Stat[] {
return queryAll<Stat>("SELECT * FROM stats ORDER BY created_at DESC");
}
/** Get by ID */
getById(id: string): Stat | undefined {
return queryOne<Stat>("SELECT * FROM stats WHERE id = ?", [id]);
}
/** Create */
create(input: CreateStatInput): Stat {
const id = crypto.randomUUID();
const now = new Date().toISOString();
const hasCreatedAt = true;
const hasUpdatedAt = true;
const cols = ["id", "user_id", "title", "description", "status", "data", "created_at", "updated_at"];
const placeholders = cols.map(() => "?").join(", ");
const values = [id, input.userId ?? null, input.title ?? null, input.description ?? null, input.status ?? null, input.data ?? null, now, now];
execute(`INSERT INTO stats (${cols.join(", ")}) VALUES (${placeholders})`, values);
return this.getById(id)!;
}
/** Update */
update(id: string, input: UpdateStatInput): Stat | undefined {
const existing = this.getById(id);
if (!existing) return undefined;
const sets: string[] = [];
const values: unknown[] = [];
if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); }
if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); }
if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); }
if (input.status !== undefined) { sets.push("status = ?"); values.push(input.status); }
if (input.data !== undefined) { sets.push("data = ?"); values.push(input.data); }
if (sets.length === 0) return existing;
sets.push("updated_at = ?");
values.push(new Date().toISOString());
values.push(id);
execute(`UPDATE stats SET ${sets.join(", ")} WHERE id = ?`, values);
return this.getById(id)!;
}
/** Delete */
delete(id: string): boolean {
const result = execute("DELETE FROM stats WHERE id = ?", [id]);
return result.changes > 0;
}
}
@@ -0,0 +1,54 @@
// Auto-generated Tag service
import { queryAll, queryOne, execute } from "../db/client.js";
import type { Tag, CreateTagInput, UpdateTagInput } from "../types/index.js";
export class TagService {
/** List all tags */
list(): Tag[] {
return queryAll<Tag>("SELECT * FROM tags ORDER BY created_at DESC");
}
/** Get by ID */
getById(id: string): Tag | undefined {
return queryOne<Tag>("SELECT * FROM tags WHERE id = ?", [id]);
}
/** Create */
create(input: CreateTagInput): Tag {
const id = crypto.randomUUID();
const now = new Date().toISOString();
const hasCreatedAt = false;
const hasUpdatedAt = false;
const cols = ["id", "name", "color"];
const placeholders = cols.map(() => "?").join(", ");
const values = [id, input.name ?? null, input.color ?? null];
execute(`INSERT INTO tags (${cols.join(", ")}) VALUES (${placeholders})`, values);
return this.getById(id)!;
}
/** Update */
update(id: string, input: UpdateTagInput): Tag | undefined {
const existing = this.getById(id);
if (!existing) return undefined;
const sets: string[] = [];
const values: unknown[] = [];
if (input.name !== undefined) { sets.push("name = ?"); values.push(input.name); }
if (input.color !== undefined) { sets.push("color = ?"); values.push(input.color); }
if (sets.length === 0) return existing;
values.push(id);
execute(`UPDATE tags SET ${sets.join(", ")} WHERE id = ?`, values);
return this.getById(id)!;
}
/** Delete */
delete(id: string): boolean {
const result = execute("DELETE FROM tags WHERE id = ?", [id]);
return result.changes > 0;
}
}
@@ -0,0 +1,58 @@
// Auto-generated Task service
import { queryAll, queryOne, execute } from "../db/client.js";
import type { Task, CreateTaskInput, UpdateTaskInput } from "../types/index.js";
export class TaskService {
/** List all tasks */
list(): Task[] {
return queryAll<Task>("SELECT * FROM tasks ORDER BY created_at DESC");
}
/** Get by ID */
getById(id: string): Task | undefined {
return queryOne<Task>("SELECT * FROM tasks WHERE id = ?", [id]);
}
/** Create */
create(input: CreateTaskInput): Task {
const id = crypto.randomUUID();
const now = new Date().toISOString();
const hasCreatedAt = true;
const hasUpdatedAt = true;
const cols = ["id", "user_id", "title", "description", "status", "data", "created_at", "updated_at"];
const placeholders = cols.map(() => "?").join(", ");
const values = [id, input.userId ?? null, input.title ?? null, input.description ?? null, input.status ?? null, input.data ?? null, now, now];
execute(`INSERT INTO tasks (${cols.join(", ")}) VALUES (${placeholders})`, values);
return this.getById(id)!;
}
/** Update */
update(id: string, input: UpdateTaskInput): Task | undefined {
const existing = this.getById(id);
if (!existing) return undefined;
const sets: string[] = [];
const values: unknown[] = [];
if (input.userId !== undefined) { sets.push("user_id = ?"); values.push(input.userId); }
if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); }
if (input.description !== undefined) { sets.push("description = ?"); values.push(input.description); }
if (input.status !== undefined) { sets.push("status = ?"); values.push(input.status); }
if (input.data !== undefined) { sets.push("data = ?"); values.push(input.data); }
if (sets.length === 0) return existing;
sets.push("updated_at = ?");
values.push(new Date().toISOString());
values.push(id);
execute(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, values);
return this.getById(id)!;
}
/** Delete */
delete(id: string): boolean {
const result = execute("DELETE FROM tasks WHERE id = ?", [id]);
return result.changes > 0;
}
}
@@ -0,0 +1,56 @@
// Auto-generated User service
import { queryAll, queryOne, execute } from "../db/client.js";
import type { User, CreateUserInput, UpdateUserInput } from "../types/index.js";
export class UserService {
/** List all users */
list(): User[] {
return queryAll<User>("SELECT * FROM users ORDER BY created_at DESC");
}
/** Get by ID */
getById(id: string): User | undefined {
return queryOne<User>("SELECT * FROM users WHERE id = ?", [id]);
}
/** Create */
create(input: CreateUserInput): User {
const id = crypto.randomUUID();
const now = new Date().toISOString();
const hasCreatedAt = true;
const hasUpdatedAt = true;
const cols = ["id", "phone", "nickname", "avatar_url", "created_at", "updated_at"];
const placeholders = cols.map(() => "?").join(", ");
const values = [id, input.phone ?? null, input.nickname ?? null, input.avatarUrl ?? null, now, now];
execute(`INSERT INTO users (${cols.join(", ")}) VALUES (${placeholders})`, values);
return this.getById(id)!;
}
/** Update */
update(id: string, input: UpdateUserInput): User | undefined {
const existing = this.getById(id);
if (!existing) return undefined;
const sets: string[] = [];
const values: unknown[] = [];
if (input.phone !== undefined) { sets.push("phone = ?"); values.push(input.phone); }
if (input.nickname !== undefined) { sets.push("nickname = ?"); values.push(input.nickname); }
if (input.avatarUrl !== undefined) { sets.push("avatar_url = ?"); values.push(input.avatarUrl); }
if (sets.length === 0) return existing;
sets.push("updated_at = ?");
values.push(new Date().toISOString());
values.push(id);
execute(`UPDATE users SET ${sets.join(", ")} WHERE id = ?`, values);
return this.getById(id)!;
}
/** Delete */
delete(id: string): boolean {
const result = execute("DELETE FROM users WHERE id = ?", [id]);
return result.changes > 0;
}
}
+8
View File
@@ -0,0 +1,8 @@
// Augment Fastify request with JWT user
import type { JwtPayload } from "./index.js";
declare module "fastify" {
interface FastifyRequest {
user?: JwtPayload;
}
}
@@ -0,0 +1,288 @@
// Auto-generated types
// ─── Base ───────────────────────────────────────────
export interface User {
id: string;
username: string;
nickname?: string;
role: string;
createdAt: string;
updatedAt: string;
}
export interface Board {
id: string;
userId?: string;
title: string;
description?: string;
status?: string;
data?: string;
createdAt?: string;
updatedAt?: string;
}
export interface Task {
id: string;
userId?: string;
title: string;
description?: string;
status?: string;
data?: string;
createdAt?: string;
updatedAt?: string;
}
export interface Member {
id: string;
userId?: string;
title: string;
description?: string;
status?: string;
data?: string;
createdAt?: string;
updatedAt?: string;
}
export interface Item {
id: string;
userId?: string;
title: string;
description?: string;
status?: string;
data?: string;
createdAt?: string;
updatedAt?: string;
}
export interface ActivityLog {
id: string;
userId?: string;
title: string;
description?: string;
status?: string;
data?: string;
createdAt?: string;
updatedAt?: string;
}
export interface Stat {
id: string;
userId?: string;
title: string;
description?: string;
status?: string;
data?: string;
createdAt?: string;
updatedAt?: string;
}
export interface Note {
id: string;
userId?: string;
title?: string;
content?: string;
isMarkdown?: boolean;
folderId?: string;
createdAt?: string;
updatedAt?: string;
}
export interface Tag {
id: string;
name: string;
color?: string;
}
export interface NoteTag {
noteId?: string;
tagId?: string;
}
export interface CreateUserInput {
phone?: string;
nickname?: string;
avatarUrl?: string;
}
export interface CreateBoardInput {
userId?: string;
title: string;
description?: string;
status?: string;
data?: string;
}
export interface CreateTaskInput {
userId?: string;
title: string;
description?: string;
status?: string;
data?: string;
}
export interface CreateMemberInput {
userId?: string;
title: string;
description?: string;
status?: string;
data?: string;
}
export interface CreateItemInput {
userId?: string;
title: string;
description?: string;
status?: string;
data?: string;
}
export interface CreateActivityLogInput {
userId?: string;
title: string;
description?: string;
status?: string;
data?: string;
}
export interface CreateStatInput {
userId?: string;
title: string;
description?: string;
status?: string;
data?: string;
}
export interface CreateNoteInput {
userId?: string;
title?: string;
content?: string;
isMarkdown?: boolean;
folderId?: string;
}
export interface CreateTagInput {
name: string;
color?: string;
}
export interface CreateNoteTagInput {
noteId?: string;
tagId?: string;
}
export interface UpdateUserInput {
phone?: string;
nickname?: string;
avatarUrl?: string;
}
export interface UpdateBoardInput {
userId?: string;
title?: string;
description?: string;
status?: string;
data?: string;
}
export interface UpdateTaskInput {
userId?: string;
title?: string;
description?: string;
status?: string;
data?: string;
}
export interface UpdateMemberInput {
userId?: string;
title?: string;
description?: string;
status?: string;
data?: string;
}
export interface UpdateItemInput {
userId?: string;
title?: string;
description?: string;
status?: string;
data?: string;
}
export interface UpdateActivityLogInput {
userId?: string;
title?: string;
description?: string;
status?: string;
data?: string;
}
export interface UpdateStatInput {
userId?: string;
title?: string;
description?: string;
status?: string;
data?: string;
}
export interface UpdateNoteInput {
userId?: string;
title?: string;
content?: string;
isMarkdown?: boolean;
folderId?: string;
}
export interface UpdateTagInput {
name?: string;
color?: string;
}
export interface UpdateNoteTagInput {
noteId?: string;
tagId?: string;
}
// ─── Auth ───────────────────────────────────────────
export interface LoginInput {
username: string;
password: string;
}
export interface RegisterInput {
username: string;
password: string;
nickname?: string;
}
export interface AuthResponse {
token: string;
user: User;
}
// ─── API ────────────────────────────────────────────
export interface ApiResponse<T> {
data: T;
message?: string;
}
export interface PaginatedResponse<T> {
data: T[];
total: number;
page: number;
pageSize: number;
}
export interface ErrorResponse {
error: string;
message: string;
statusCode: number;
}
// ─── JWT ────────────────────────────────────────────
export interface JwtPayload {
userId: string;
username: string;
role: string;
iat?: number;
exp?: number;
}
+27
View File
@@ -0,0 +1,27 @@
// Type declarations for sql.js (no native types package available)
declare module "sql.js" {
export interface Database {
run(sql: string, params?: BindParams): void;
exec(sql: string): void;
prepare(sql: string): Statement;
export(): Uint8Array;
close(): void;
getRowsModified(): number;
}
export interface Statement {
bind(params?: BindParams): boolean;
step(): boolean;
getAsObject<T = Record<string, unknown>>(): T;
getColumnNames(): string[];
free(): boolean;
}
export type BindParams = unknown[] | Record<string, unknown>;
export interface SqlJsStatic {
Database: new (data?: ArrayLike<number>) => Database;
}
export default function initSqlJs(config?: Record<string, unknown>): Promise<SqlJsStatic>;
}
+27
View File
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": [
"ES2022"
],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"sourceMap": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"src/__tests__"
]
}
File diff suppressed because one or more lines are too long