🎉 init: 小龙的工作空间
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
|
||||
node_modules/
|
||||
dist/
|
||||
data/
|
||||
.env
|
||||
*.db
|
||||
*.db-journal
|
||||
*.db-wal
|
||||
@@ -0,0 +1,139 @@
|
||||
# MealPrep — Backend API
|
||||
|
||||
> 一款支持商品浏览、购物车、在线支付和物流追踪的电商应用。
|
||||
|
||||
## 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
|
||||
|
||||
#### foods
|
||||
- `GET /api/foods` — List all
|
||||
- `GET /api/foods/:id` — Get by ID
|
||||
- `POST /api/foods` — Create
|
||||
- `PUT /api/foods/:id` — Update
|
||||
- `DELETE /api/foods/:id` — Delete
|
||||
|
||||
#### customers
|
||||
- `GET /api/customers` — List all
|
||||
- `GET /api/customers/:id` — Get by ID
|
||||
- `POST /api/customers` — Create
|
||||
- `PUT /api/customers/:id` — Update
|
||||
- `DELETE /api/customers/:id` — Delete
|
||||
|
||||
#### meal_plans
|
||||
- `GET /api/meal_plans` — List all
|
||||
- `GET /api/meal_plans/:id` — Get by ID
|
||||
- `POST /api/meal_plans` — Create
|
||||
- `PUT /api/meal_plans/:id` — Update
|
||||
- `DELETE /api/meal_plans/:id` — Delete
|
||||
|
||||
#### nutrition
|
||||
- `GET /api/nutrition` — List all
|
||||
- `GET /api/nutrition/:id` — Get by ID
|
||||
- `POST /api/nutrition` — Create
|
||||
- `PUT /api/nutrition/:id` — Update
|
||||
- `DELETE /api/nutrition/:id` — Delete
|
||||
|
||||
#### shopping_lists
|
||||
- `GET /api/shopping_lists` — List all
|
||||
- `GET /api/shopping_lists/:id` — Get by ID
|
||||
- `POST /api/shopping_lists` — Create
|
||||
- `PUT /api/shopping_lists/:id` — Update
|
||||
- `DELETE /api/shopping_lists/:id` — Delete
|
||||
|
||||
#### templates
|
||||
- `GET /api/templates` — List all
|
||||
- `GET /api/templates/:id` — Get by ID
|
||||
- `POST /api/templates` — Create
|
||||
- `PUT /api/templates/:id` — Update
|
||||
- `DELETE /api/templates/:id` — Delete
|
||||
|
||||
#### products
|
||||
- `GET /api/products` — List all
|
||||
- `GET /api/products/:id` — Get by ID
|
||||
- `POST /api/products` — Create
|
||||
- `PUT /api/products/:id` — Update
|
||||
- `DELETE /api/products/:id` — Delete
|
||||
|
||||
#### orders
|
||||
- `GET /api/orders` — List all
|
||||
- `GET /api/orders/:id` — Get by ID
|
||||
- `POST /api/orders` — Create
|
||||
- `PUT /api/orders/:id` — Update
|
||||
- `DELETE /api/orders/:id` — Delete
|
||||
|
||||
#### order_items
|
||||
- `GET /api/order_items` — List all
|
||||
- `GET /api/order_items/:id` — Get by ID
|
||||
- `POST /api/order_items` — Create
|
||||
- `PUT /api/order_items/:id` — Update
|
||||
- `DELETE /api/order_items/:id` — Delete
|
||||
|
||||
#### logistics
|
||||
- `GET /api/logistics` — List all
|
||||
- `GET /api/logistics/:id` — Get by ID
|
||||
- `POST /api/logistics` — Create
|
||||
- `PUT /api/logistics/:id` — Update
|
||||
- `DELETE /api/logistics/:id` — Delete
|
||||
|
||||
### System
|
||||
- `GET /api/health` — Health check
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "mealprep",
|
||||
"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,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 @@
|
||||
// Customer 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/customers", () => {
|
||||
it("returns empty list", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/customers",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
const body = res.json();
|
||||
assert.ok(Array.isArray(body.data));
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/customers", () => {
|
||||
it("creates a customer", async () => {
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/customers",
|
||||
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/customers/:id", () => {
|
||||
it("returns the created customer", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/customers/${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/customers/nonexistent`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /api/customers/:id", () => {
|
||||
it("updates the customer", async () => {
|
||||
const res = await app.inject({
|
||||
method: "PUT",
|
||||
url: `/api/customers/${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/customers/:id", () => {
|
||||
it("deletes the customer", async () => {
|
||||
const res = await app.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/customers/${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/customers/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
// Food 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/foods", () => {
|
||||
it("returns empty list", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/foods",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
const body = res.json();
|
||||
assert.ok(Array.isArray(body.data));
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/foods", () => {
|
||||
it("creates a food", async () => {
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/foods",
|
||||
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/foods/:id", () => {
|
||||
it("returns the created food", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/foods/${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/foods/nonexistent`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /api/foods/:id", () => {
|
||||
it("updates the food", async () => {
|
||||
const res = await app.inject({
|
||||
method: "PUT",
|
||||
url: `/api/foods/${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/foods/:id", () => {
|
||||
it("deletes the food", async () => {
|
||||
const res = await app.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/foods/${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/foods/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
// Logistic 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/logistics", () => {
|
||||
it("returns empty list", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/logistics",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
const body = res.json();
|
||||
assert.ok(Array.isArray(body.data));
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/logistics", () => {
|
||||
it("creates a logistic", async () => {
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/logistics",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: {
|
||||
"orderId": "00000000-0000-0000-0000-000000000001",
|
||||
"carrier": "sample-carrier",
|
||||
"trackingNumber": "sample-trackingnumber",
|
||||
"status": "sample-status",
|
||||
"events": "sample-events"
|
||||
},
|
||||
});
|
||||
assert.equal(res.statusCode, 201);
|
||||
const body = res.json();
|
||||
assert.ok(body.data.id);
|
||||
createdId = body.data.id;
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/logistics/:id", () => {
|
||||
it("returns the created logistic", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/logistics/${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/logistics/nonexistent`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /api/logistics/:id", () => {
|
||||
it("updates the logistic", async () => {
|
||||
const res = await app.inject({
|
||||
method: "PUT",
|
||||
url: `/api/logistics/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: {
|
||||
"orderId": "00000000-0000-0000-0000-000000000001",
|
||||
"carrier": "sample-carrier",
|
||||
"trackingNumber": "sample-trackingnumber",
|
||||
"status": "sample-status",
|
||||
"events": "sample-events"
|
||||
},
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /api/logistics/:id", () => {
|
||||
it("deletes the logistic", async () => {
|
||||
const res = await app.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/logistics/${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/logistics/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
// MealPlan 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/meal_plans", () => {
|
||||
it("returns empty list", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/meal_plans",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
const body = res.json();
|
||||
assert.ok(Array.isArray(body.data));
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/meal_plans", () => {
|
||||
it("creates a meal_plan", async () => {
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/meal_plans",
|
||||
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/meal_plans/:id", () => {
|
||||
it("returns the created meal_plan", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/meal_plans/${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/meal_plans/nonexistent`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /api/meal_plans/:id", () => {
|
||||
it("updates the meal_plan", async () => {
|
||||
const res = await app.inject({
|
||||
method: "PUT",
|
||||
url: `/api/meal_plans/${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/meal_plans/:id", () => {
|
||||
it("deletes the meal_plan", async () => {
|
||||
const res = await app.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/meal_plans/${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/meal_plans/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
// Nutrition 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/nutrition", () => {
|
||||
it("returns empty list", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/nutrition",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
const body = res.json();
|
||||
assert.ok(Array.isArray(body.data));
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/nutrition", () => {
|
||||
it("creates a nutrition", async () => {
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/nutrition",
|
||||
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/nutrition/:id", () => {
|
||||
it("returns the created nutrition", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/nutrition/${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/nutrition/nonexistent`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /api/nutrition/:id", () => {
|
||||
it("updates the nutrition", async () => {
|
||||
const res = await app.inject({
|
||||
method: "PUT",
|
||||
url: `/api/nutrition/${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/nutrition/:id", () => {
|
||||
it("deletes the nutrition", async () => {
|
||||
const res = await app.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/nutrition/${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/nutrition/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
// OrderItem 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/order_items", () => {
|
||||
it("returns empty list", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/order_items",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
const body = res.json();
|
||||
assert.ok(Array.isArray(body.data));
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/order_items", () => {
|
||||
it("creates a order_item", async () => {
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/order_items",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: {
|
||||
"orderId": "00000000-0000-0000-0000-000000000001",
|
||||
"productId": "00000000-0000-0000-0000-000000000001",
|
||||
"quantity": 1,
|
||||
"unitPrice": 1
|
||||
},
|
||||
});
|
||||
assert.equal(res.statusCode, 201);
|
||||
const body = res.json();
|
||||
assert.ok(body.data.id);
|
||||
createdId = body.data.id;
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/order_items/:id", () => {
|
||||
it("returns the created order_item", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/order_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/order_items/nonexistent`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /api/order_items/:id", () => {
|
||||
it("updates the order_item", async () => {
|
||||
const res = await app.inject({
|
||||
method: "PUT",
|
||||
url: `/api/order_items/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: {
|
||||
"orderId": "00000000-0000-0000-0000-000000000001",
|
||||
"productId": "00000000-0000-0000-0000-000000000001",
|
||||
"quantity": 1,
|
||||
"unitPrice": 1
|
||||
},
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /api/order_items/:id", () => {
|
||||
it("deletes the order_item", async () => {
|
||||
const res = await app.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/order_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/order_items/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
// Order 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/orders", () => {
|
||||
it("returns empty list", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/orders",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
const body = res.json();
|
||||
assert.ok(Array.isArray(body.data));
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/orders", () => {
|
||||
it("creates a order", async () => {
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/orders",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: {
|
||||
"userId": "00000000-0000-0000-0000-000000000001",
|
||||
"status": "sample-status",
|
||||
"totalAmount": 1,
|
||||
"addressId": "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/orders/:id", () => {
|
||||
it("returns the created order", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/orders/${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/orders/nonexistent`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /api/orders/:id", () => {
|
||||
it("updates the order", async () => {
|
||||
const res = await app.inject({
|
||||
method: "PUT",
|
||||
url: `/api/orders/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: {
|
||||
"userId": "00000000-0000-0000-0000-000000000001",
|
||||
"status": "sample-status",
|
||||
"totalAmount": 1,
|
||||
"addressId": "00000000-0000-0000-0000-000000000001"
|
||||
},
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /api/orders/:id", () => {
|
||||
it("deletes the order", async () => {
|
||||
const res = await app.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/orders/${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/orders/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
// Product 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/products", () => {
|
||||
it("returns empty list", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/products",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
const body = res.json();
|
||||
assert.ok(Array.isArray(body.data));
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/products", () => {
|
||||
it("creates a product", async () => {
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/products",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: {
|
||||
"title": "sample-title",
|
||||
"price": 1,
|
||||
"stock": 1,
|
||||
"images": "sample-images",
|
||||
"categoryId": "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/products/:id", () => {
|
||||
it("returns the created product", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/products/${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/products/nonexistent`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /api/products/:id", () => {
|
||||
it("updates the product", async () => {
|
||||
const res = await app.inject({
|
||||
method: "PUT",
|
||||
url: `/api/products/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload: {
|
||||
"title": "sample-title",
|
||||
"price": 1,
|
||||
"stock": 1,
|
||||
"images": "sample-images",
|
||||
"categoryId": "00000000-0000-0000-0000-000000000001"
|
||||
},
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /api/products/:id", () => {
|
||||
it("deletes the product", async () => {
|
||||
const res = await app.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/products/${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/products/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
// ShoppingList 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/shopping_lists", () => {
|
||||
it("returns empty list", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/shopping_lists",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
const body = res.json();
|
||||
assert.ok(Array.isArray(body.data));
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/shopping_lists", () => {
|
||||
it("creates a shopping_list", async () => {
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/shopping_lists",
|
||||
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/shopping_lists/:id", () => {
|
||||
it("returns the created shopping_list", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/shopping_lists/${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/shopping_lists/nonexistent`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /api/shopping_lists/:id", () => {
|
||||
it("updates the shopping_list", async () => {
|
||||
const res = await app.inject({
|
||||
method: "PUT",
|
||||
url: `/api/shopping_lists/${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/shopping_lists/:id", () => {
|
||||
it("deletes the shopping_list", async () => {
|
||||
const res = await app.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/shopping_lists/${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/shopping_lists/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
// Template 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/templates", () => {
|
||||
it("returns empty list", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/templates",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
const body = res.json();
|
||||
assert.ok(Array.isArray(body.data));
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/templates", () => {
|
||||
it("creates a template", async () => {
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/templates",
|
||||
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/templates/:id", () => {
|
||||
it("returns the created template", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/templates/${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/templates/nonexistent`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /api/templates/:id", () => {
|
||||
it("updates the template", async () => {
|
||||
const res = await app.inject({
|
||||
method: "PUT",
|
||||
url: `/api/templates/${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/templates/:id", () => {
|
||||
it("deletes the template", async () => {
|
||||
const res = await app.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/templates/${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/templates/${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,30 @@
|
||||
// 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 foods (\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_foods_user ON foods(user_id)",
|
||||
"CREATE TABLE IF NOT EXISTS customers (\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_customers_user ON customers(user_id)",
|
||||
"CREATE TABLE IF NOT EXISTS meal_plans (\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_meal_plans_user ON meal_plans(user_id)",
|
||||
"CREATE TABLE IF NOT EXISTS nutrition (\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_nutrition_user ON nutrition(user_id)",
|
||||
"CREATE TABLE IF NOT EXISTS shopping_lists (\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_shopping_lists_user ON shopping_lists(user_id)",
|
||||
"CREATE TABLE IF NOT EXISTS templates (\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_templates_user ON templates(user_id)",
|
||||
"CREATE TABLE IF NOT EXISTS products (\n id TEXT PRIMARY KEY,\n title TEXT NOT NULL,\n price REAL NOT NULL,\n stock INTEGER DEFAULT 0,\n images TEXT,\n category_id TEXT REFERENCES categories(id),\n created_at TEXT\n);",
|
||||
"idx_products_category ON products(category_id)",
|
||||
"CREATE TABLE IF NOT EXISTS orders (\n id TEXT PRIMARY KEY,\n user_id TEXT REFERENCES users(id),\n status TEXT DEFAULT 'pending',\n total_amount REAL,\n address_id TEXT REFERENCES addresses(id),\n created_at TEXT\n);",
|
||||
"CREATE TABLE IF NOT EXISTS order_items (\n id TEXT PRIMARY KEY,\n order_id TEXT REFERENCES orders(id),\n product_id TEXT REFERENCES products(id),\n quantity INTEGER NOT NULL,\n unit_price REAL\n);",
|
||||
"CREATE TABLE IF NOT EXISTS logistics (\n id TEXT PRIMARY KEY,\n order_id TEXT UNIQUE REFERENCES orders(id),\n carrier TEXT,\n tracking_number TEXT,\n status TEXT,\n events TEXT DEFAULT '[]',\n updated_at TEXT\n);"
|
||||
];
|
||||
for (const sql of statements) {
|
||||
const trimmed = sql.trim();
|
||||
if (trimmed) db.run(trimmed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// MealPrep — 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 { foodsRoutes } from "./routes/foods.js";
|
||||
import { customersRoutes } from "./routes/customers.js";
|
||||
import { meal_plansRoutes } from "./routes/meal_plans.js";
|
||||
import { nutritionRoutes } from "./routes/nutrition.js";
|
||||
import { shopping_listsRoutes } from "./routes/shopping_lists.js";
|
||||
import { templatesRoutes } from "./routes/templates.js";
|
||||
import { productsRoutes } from "./routes/products.js";
|
||||
import { ordersRoutes } from "./routes/orders.js";
|
||||
import { order_itemsRoutes } from "./routes/order_items.js";
|
||||
import { logisticsRoutes } from "./routes/logistics.js";
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-455b54e9";
|
||||
|
||||
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(foodsRoutes, { prefix: "/api/foods" });
|
||||
await app.register(customersRoutes, { prefix: "/api/customers" });
|
||||
await app.register(meal_plansRoutes, { prefix: "/api/meal_plans" });
|
||||
await app.register(nutritionRoutes, { prefix: "/api/nutrition" });
|
||||
await app.register(shopping_listsRoutes, { prefix: "/api/shopping_lists" });
|
||||
await app.register(templatesRoutes, { prefix: "/api/templates" });
|
||||
await app.register(productsRoutes, { prefix: "/api/products" });
|
||||
await app.register(ordersRoutes, { prefix: "/api/orders" });
|
||||
await app.register(order_itemsRoutes, { prefix: "/api/order_items" });
|
||||
await app.register(logisticsRoutes, { prefix: "/api/logistics" });
|
||||
|
||||
// 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,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 Customer routes
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { authenticate } from "../middleware/auth.js";
|
||||
import { CustomerService } from "../services/customer.js";
|
||||
import type { CreateCustomerInput, UpdateCustomerInput } from "../types/index.js";
|
||||
|
||||
const service = new CustomerService();
|
||||
|
||||
export async function customersRoutes(app: FastifyInstance): Promise<void> {
|
||||
// All routes require authentication
|
||||
app.addHook("onRequest", authenticate);
|
||||
|
||||
// GET /api/customers — list
|
||||
app.get("/", async (request, reply) => {
|
||||
const items = service.list();
|
||||
return { data: items };
|
||||
});
|
||||
|
||||
// GET /api/customers/: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: "Customer not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// POST /api/customers — create
|
||||
app.post<{ Body: CreateCustomerInput }>("/", async (request, reply) => {
|
||||
const item = service.create(request.body);
|
||||
return reply.status(201).send({ data: item });
|
||||
});
|
||||
|
||||
// PUT /api/customers/:id — update
|
||||
app.put<{ Params: { id: string }; Body: UpdateCustomerInput }>("/:id", async (request, reply) => {
|
||||
const item = service.update(request.params.id, request.body);
|
||||
if (!item) {
|
||||
return reply.status(404).send({ error: "Not Found", message: "Customer not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// DELETE /api/customers/: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: "Customer not found", statusCode: 404 });
|
||||
}
|
||||
return reply.status(204).send();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Auto-generated Food routes
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { authenticate } from "../middleware/auth.js";
|
||||
import { FoodService } from "../services/food.js";
|
||||
import type { CreateFoodInput, UpdateFoodInput } from "../types/index.js";
|
||||
|
||||
const service = new FoodService();
|
||||
|
||||
export async function foodsRoutes(app: FastifyInstance): Promise<void> {
|
||||
// All routes require authentication
|
||||
app.addHook("onRequest", authenticate);
|
||||
|
||||
// GET /api/foods — list
|
||||
app.get("/", async (request, reply) => {
|
||||
const items = service.list();
|
||||
return { data: items };
|
||||
});
|
||||
|
||||
// GET /api/foods/: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: "Food not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// POST /api/foods — create
|
||||
app.post<{ Body: CreateFoodInput }>("/", async (request, reply) => {
|
||||
const item = service.create(request.body);
|
||||
return reply.status(201).send({ data: item });
|
||||
});
|
||||
|
||||
// PUT /api/foods/:id — update
|
||||
app.put<{ Params: { id: string }; Body: UpdateFoodInput }>("/:id", async (request, reply) => {
|
||||
const item = service.update(request.params.id, request.body);
|
||||
if (!item) {
|
||||
return reply.status(404).send({ error: "Not Found", message: "Food not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// DELETE /api/foods/: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: "Food not found", statusCode: 404 });
|
||||
}
|
||||
return reply.status(204).send();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Auto-generated Logistic routes
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { authenticate } from "../middleware/auth.js";
|
||||
import { LogisticService } from "../services/logistic.js";
|
||||
import type { CreateLogisticInput, UpdateLogisticInput } from "../types/index.js";
|
||||
|
||||
const service = new LogisticService();
|
||||
|
||||
export async function logisticsRoutes(app: FastifyInstance): Promise<void> {
|
||||
// All routes require authentication
|
||||
app.addHook("onRequest", authenticate);
|
||||
|
||||
// GET /api/logistics — list
|
||||
app.get("/", async (request, reply) => {
|
||||
const items = service.list();
|
||||
return { data: items };
|
||||
});
|
||||
|
||||
// GET /api/logistics/: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: "Logistic not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// POST /api/logistics — create
|
||||
app.post<{ Body: CreateLogisticInput }>("/", async (request, reply) => {
|
||||
const item = service.create(request.body);
|
||||
return reply.status(201).send({ data: item });
|
||||
});
|
||||
|
||||
// PUT /api/logistics/:id — update
|
||||
app.put<{ Params: { id: string }; Body: UpdateLogisticInput }>("/:id", async (request, reply) => {
|
||||
const item = service.update(request.params.id, request.body);
|
||||
if (!item) {
|
||||
return reply.status(404).send({ error: "Not Found", message: "Logistic not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// DELETE /api/logistics/: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: "Logistic not found", statusCode: 404 });
|
||||
}
|
||||
return reply.status(204).send();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Auto-generated MealPlan routes
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { authenticate } from "../middleware/auth.js";
|
||||
import { MealPlanService } from "../services/meal_plan.js";
|
||||
import type { CreateMealPlanInput, UpdateMealPlanInput } from "../types/index.js";
|
||||
|
||||
const service = new MealPlanService();
|
||||
|
||||
export async function meal_plansRoutes(app: FastifyInstance): Promise<void> {
|
||||
// All routes require authentication
|
||||
app.addHook("onRequest", authenticate);
|
||||
|
||||
// GET /api/meal_plans — list
|
||||
app.get("/", async (request, reply) => {
|
||||
const items = service.list();
|
||||
return { data: items };
|
||||
});
|
||||
|
||||
// GET /api/meal_plans/: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: "MealPlan not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// POST /api/meal_plans — create
|
||||
app.post<{ Body: CreateMealPlanInput }>("/", async (request, reply) => {
|
||||
const item = service.create(request.body);
|
||||
return reply.status(201).send({ data: item });
|
||||
});
|
||||
|
||||
// PUT /api/meal_plans/:id — update
|
||||
app.put<{ Params: { id: string }; Body: UpdateMealPlanInput }>("/:id", async (request, reply) => {
|
||||
const item = service.update(request.params.id, request.body);
|
||||
if (!item) {
|
||||
return reply.status(404).send({ error: "Not Found", message: "MealPlan not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// DELETE /api/meal_plans/: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: "MealPlan not found", statusCode: 404 });
|
||||
}
|
||||
return reply.status(204).send();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Auto-generated Nutrition routes
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { authenticate } from "../middleware/auth.js";
|
||||
import { NutritionService } from "../services/nutrition.js";
|
||||
import type { CreateNutritionInput, UpdateNutritionInput } from "../types/index.js";
|
||||
|
||||
const service = new NutritionService();
|
||||
|
||||
export async function nutritionRoutes(app: FastifyInstance): Promise<void> {
|
||||
// All routes require authentication
|
||||
app.addHook("onRequest", authenticate);
|
||||
|
||||
// GET /api/nutrition — list
|
||||
app.get("/", async (request, reply) => {
|
||||
const items = service.list();
|
||||
return { data: items };
|
||||
});
|
||||
|
||||
// GET /api/nutrition/: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: "Nutrition not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// POST /api/nutrition — create
|
||||
app.post<{ Body: CreateNutritionInput }>("/", async (request, reply) => {
|
||||
const item = service.create(request.body);
|
||||
return reply.status(201).send({ data: item });
|
||||
});
|
||||
|
||||
// PUT /api/nutrition/:id — update
|
||||
app.put<{ Params: { id: string }; Body: UpdateNutritionInput }>("/:id", async (request, reply) => {
|
||||
const item = service.update(request.params.id, request.body);
|
||||
if (!item) {
|
||||
return reply.status(404).send({ error: "Not Found", message: "Nutrition not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// DELETE /api/nutrition/: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: "Nutrition not found", statusCode: 404 });
|
||||
}
|
||||
return reply.status(204).send();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Auto-generated OrderItem routes
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { authenticate } from "../middleware/auth.js";
|
||||
import { OrderItemService } from "../services/order_item.js";
|
||||
import type { CreateOrderItemInput, UpdateOrderItemInput } from "../types/index.js";
|
||||
|
||||
const service = new OrderItemService();
|
||||
|
||||
export async function order_itemsRoutes(app: FastifyInstance): Promise<void> {
|
||||
// All routes require authentication
|
||||
app.addHook("onRequest", authenticate);
|
||||
|
||||
// GET /api/order_items — list
|
||||
app.get("/", async (request, reply) => {
|
||||
const items = service.list();
|
||||
return { data: items };
|
||||
});
|
||||
|
||||
// GET /api/order_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: "OrderItem not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// POST /api/order_items — create
|
||||
app.post<{ Body: CreateOrderItemInput }>("/", async (request, reply) => {
|
||||
const item = service.create(request.body);
|
||||
return reply.status(201).send({ data: item });
|
||||
});
|
||||
|
||||
// PUT /api/order_items/:id — update
|
||||
app.put<{ Params: { id: string }; Body: UpdateOrderItemInput }>("/:id", async (request, reply) => {
|
||||
const item = service.update(request.params.id, request.body);
|
||||
if (!item) {
|
||||
return reply.status(404).send({ error: "Not Found", message: "OrderItem not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// DELETE /api/order_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: "OrderItem not found", statusCode: 404 });
|
||||
}
|
||||
return reply.status(204).send();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Auto-generated Order routes
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { authenticate } from "../middleware/auth.js";
|
||||
import { OrderService } from "../services/order.js";
|
||||
import type { CreateOrderInput, UpdateOrderInput } from "../types/index.js";
|
||||
|
||||
const service = new OrderService();
|
||||
|
||||
export async function ordersRoutes(app: FastifyInstance): Promise<void> {
|
||||
// All routes require authentication
|
||||
app.addHook("onRequest", authenticate);
|
||||
|
||||
// GET /api/orders — list
|
||||
app.get("/", async (request, reply) => {
|
||||
const items = service.list();
|
||||
return { data: items };
|
||||
});
|
||||
|
||||
// GET /api/orders/: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: "Order not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// POST /api/orders — create
|
||||
app.post<{ Body: CreateOrderInput }>("/", async (request, reply) => {
|
||||
const item = service.create(request.body);
|
||||
return reply.status(201).send({ data: item });
|
||||
});
|
||||
|
||||
// PUT /api/orders/:id — update
|
||||
app.put<{ Params: { id: string }; Body: UpdateOrderInput }>("/:id", async (request, reply) => {
|
||||
const item = service.update(request.params.id, request.body);
|
||||
if (!item) {
|
||||
return reply.status(404).send({ error: "Not Found", message: "Order not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// DELETE /api/orders/: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: "Order not found", statusCode: 404 });
|
||||
}
|
||||
return reply.status(204).send();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Auto-generated Product routes
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { authenticate } from "../middleware/auth.js";
|
||||
import { ProductService } from "../services/product.js";
|
||||
import type { CreateProductInput, UpdateProductInput } from "../types/index.js";
|
||||
|
||||
const service = new ProductService();
|
||||
|
||||
export async function productsRoutes(app: FastifyInstance): Promise<void> {
|
||||
// All routes require authentication
|
||||
app.addHook("onRequest", authenticate);
|
||||
|
||||
// GET /api/products — list
|
||||
app.get("/", async (request, reply) => {
|
||||
const items = service.list();
|
||||
return { data: items };
|
||||
});
|
||||
|
||||
// GET /api/products/: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: "Product not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// POST /api/products — create
|
||||
app.post<{ Body: CreateProductInput }>("/", async (request, reply) => {
|
||||
const item = service.create(request.body);
|
||||
return reply.status(201).send({ data: item });
|
||||
});
|
||||
|
||||
// PUT /api/products/:id — update
|
||||
app.put<{ Params: { id: string }; Body: UpdateProductInput }>("/:id", async (request, reply) => {
|
||||
const item = service.update(request.params.id, request.body);
|
||||
if (!item) {
|
||||
return reply.status(404).send({ error: "Not Found", message: "Product not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// DELETE /api/products/: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: "Product not found", statusCode: 404 });
|
||||
}
|
||||
return reply.status(204).send();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Auto-generated ShoppingList routes
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { authenticate } from "../middleware/auth.js";
|
||||
import { ShoppingListService } from "../services/shopping_list.js";
|
||||
import type { CreateShoppingListInput, UpdateShoppingListInput } from "../types/index.js";
|
||||
|
||||
const service = new ShoppingListService();
|
||||
|
||||
export async function shopping_listsRoutes(app: FastifyInstance): Promise<void> {
|
||||
// All routes require authentication
|
||||
app.addHook("onRequest", authenticate);
|
||||
|
||||
// GET /api/shopping_lists — list
|
||||
app.get("/", async (request, reply) => {
|
||||
const items = service.list();
|
||||
return { data: items };
|
||||
});
|
||||
|
||||
// GET /api/shopping_lists/: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: "ShoppingList not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// POST /api/shopping_lists — create
|
||||
app.post<{ Body: CreateShoppingListInput }>("/", async (request, reply) => {
|
||||
const item = service.create(request.body);
|
||||
return reply.status(201).send({ data: item });
|
||||
});
|
||||
|
||||
// PUT /api/shopping_lists/:id — update
|
||||
app.put<{ Params: { id: string }; Body: UpdateShoppingListInput }>("/:id", async (request, reply) => {
|
||||
const item = service.update(request.params.id, request.body);
|
||||
if (!item) {
|
||||
return reply.status(404).send({ error: "Not Found", message: "ShoppingList not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// DELETE /api/shopping_lists/: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: "ShoppingList not found", statusCode: 404 });
|
||||
}
|
||||
return reply.status(204).send();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Auto-generated Template routes
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { authenticate } from "../middleware/auth.js";
|
||||
import { TemplateService } from "../services/template.js";
|
||||
import type { CreateTemplateInput, UpdateTemplateInput } from "../types/index.js";
|
||||
|
||||
const service = new TemplateService();
|
||||
|
||||
export async function templatesRoutes(app: FastifyInstance): Promise<void> {
|
||||
// All routes require authentication
|
||||
app.addHook("onRequest", authenticate);
|
||||
|
||||
// GET /api/templates — list
|
||||
app.get("/", async (request, reply) => {
|
||||
const items = service.list();
|
||||
return { data: items };
|
||||
});
|
||||
|
||||
// GET /api/templates/: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: "Template not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// POST /api/templates — create
|
||||
app.post<{ Body: CreateTemplateInput }>("/", async (request, reply) => {
|
||||
const item = service.create(request.body);
|
||||
return reply.status(201).send({ data: item });
|
||||
});
|
||||
|
||||
// PUT /api/templates/:id — update
|
||||
app.put<{ Params: { id: string }; Body: UpdateTemplateInput }>("/:id", async (request, reply) => {
|
||||
const item = service.update(request.params.id, request.body);
|
||||
if (!item) {
|
||||
return reply.status(404).send({ error: "Not Found", message: "Template not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// DELETE /api/templates/: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: "Template 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 Customer service
|
||||
import { queryAll, queryOne, execute } from "../db/client.js";
|
||||
import type { Customer, CreateCustomerInput, UpdateCustomerInput } from "../types/index.js";
|
||||
|
||||
export class CustomerService {
|
||||
/** List all customers */
|
||||
list(): Customer[] {
|
||||
return queryAll<Customer>("SELECT * FROM customers ORDER BY created_at DESC");
|
||||
}
|
||||
|
||||
/** Get by ID */
|
||||
getById(id: string): Customer | undefined {
|
||||
return queryOne<Customer>("SELECT * FROM customers WHERE id = ?", [id]);
|
||||
}
|
||||
|
||||
/** Create */
|
||||
create(input: CreateCustomerInput): Customer {
|
||||
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 customers (${cols.join(", ")}) VALUES (${placeholders})`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Update */
|
||||
update(id: string, input: UpdateCustomerInput): Customer | 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 customers SET ${sets.join(", ")} WHERE id = ?`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Delete */
|
||||
delete(id: string): boolean {
|
||||
const result = execute("DELETE FROM customers WHERE id = ?", [id]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Auto-generated Food service
|
||||
import { queryAll, queryOne, execute } from "../db/client.js";
|
||||
import type { Food, CreateFoodInput, UpdateFoodInput } from "../types/index.js";
|
||||
|
||||
export class FoodService {
|
||||
/** List all foods */
|
||||
list(): Food[] {
|
||||
return queryAll<Food>("SELECT * FROM foods ORDER BY created_at DESC");
|
||||
}
|
||||
|
||||
/** Get by ID */
|
||||
getById(id: string): Food | undefined {
|
||||
return queryOne<Food>("SELECT * FROM foods WHERE id = ?", [id]);
|
||||
}
|
||||
|
||||
/** Create */
|
||||
create(input: CreateFoodInput): Food {
|
||||
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 foods (${cols.join(", ")}) VALUES (${placeholders})`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Update */
|
||||
update(id: string, input: UpdateFoodInput): Food | 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 foods SET ${sets.join(", ")} WHERE id = ?`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Delete */
|
||||
delete(id: string): boolean {
|
||||
const result = execute("DELETE FROM foods WHERE id = ?", [id]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Auto-generated Logistic service
|
||||
import { queryAll, queryOne, execute } from "../db/client.js";
|
||||
import type { Logistic, CreateLogisticInput, UpdateLogisticInput } from "../types/index.js";
|
||||
|
||||
export class LogisticService {
|
||||
/** List all logistics */
|
||||
list(): Logistic[] {
|
||||
return queryAll<Logistic>("SELECT * FROM logistics ORDER BY created_at DESC");
|
||||
}
|
||||
|
||||
/** Get by ID */
|
||||
getById(id: string): Logistic | undefined {
|
||||
return queryOne<Logistic>("SELECT * FROM logistics WHERE id = ?", [id]);
|
||||
}
|
||||
|
||||
/** Create */
|
||||
create(input: CreateLogisticInput): Logistic {
|
||||
const id = crypto.randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
const hasCreatedAt = false;
|
||||
const hasUpdatedAt = true;
|
||||
const cols = ["id", "order_id", "carrier", "tracking_number", "status", "events", "updated_at"];
|
||||
const placeholders = cols.map(() => "?").join(", ");
|
||||
const values = [id, input.orderId ?? null, input.carrier ?? null, input.trackingNumber ?? null, input.status ?? null, input.events ?? null, now];
|
||||
|
||||
execute(`INSERT INTO logistics (${cols.join(", ")}) VALUES (${placeholders})`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Update */
|
||||
update(id: string, input: UpdateLogisticInput): Logistic | undefined {
|
||||
const existing = this.getById(id);
|
||||
if (!existing) return undefined;
|
||||
|
||||
const sets: string[] = [];
|
||||
const values: unknown[] = [];
|
||||
if (input.orderId !== undefined) { sets.push("order_id = ?"); values.push(input.orderId); }
|
||||
if (input.carrier !== undefined) { sets.push("carrier = ?"); values.push(input.carrier); }
|
||||
if (input.trackingNumber !== undefined) { sets.push("tracking_number = ?"); values.push(input.trackingNumber); }
|
||||
if (input.status !== undefined) { sets.push("status = ?"); values.push(input.status); }
|
||||
if (input.events !== undefined) { sets.push("events = ?"); values.push(input.events); }
|
||||
|
||||
if (sets.length === 0) return existing;
|
||||
|
||||
sets.push("updated_at = ?");
|
||||
values.push(new Date().toISOString());
|
||||
|
||||
values.push(id);
|
||||
execute(`UPDATE logistics SET ${sets.join(", ")} WHERE id = ?`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Delete */
|
||||
delete(id: string): boolean {
|
||||
const result = execute("DELETE FROM logistics WHERE id = ?", [id]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Auto-generated MealPlan service
|
||||
import { queryAll, queryOne, execute } from "../db/client.js";
|
||||
import type { MealPlan, CreateMealPlanInput, UpdateMealPlanInput } from "../types/index.js";
|
||||
|
||||
export class MealPlanService {
|
||||
/** List all meal_plans */
|
||||
list(): MealPlan[] {
|
||||
return queryAll<MealPlan>("SELECT * FROM meal_plans ORDER BY created_at DESC");
|
||||
}
|
||||
|
||||
/** Get by ID */
|
||||
getById(id: string): MealPlan | undefined {
|
||||
return queryOne<MealPlan>("SELECT * FROM meal_plans WHERE id = ?", [id]);
|
||||
}
|
||||
|
||||
/** Create */
|
||||
create(input: CreateMealPlanInput): MealPlan {
|
||||
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 meal_plans (${cols.join(", ")}) VALUES (${placeholders})`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Update */
|
||||
update(id: string, input: UpdateMealPlanInput): MealPlan | 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 meal_plans SET ${sets.join(", ")} WHERE id = ?`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Delete */
|
||||
delete(id: string): boolean {
|
||||
const result = execute("DELETE FROM meal_plans WHERE id = ?", [id]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Auto-generated Nutrition service
|
||||
import { queryAll, queryOne, execute } from "../db/client.js";
|
||||
import type { Nutrition, CreateNutritionInput, UpdateNutritionInput } from "../types/index.js";
|
||||
|
||||
export class NutritionService {
|
||||
/** List all nutrition */
|
||||
list(): Nutrition[] {
|
||||
return queryAll<Nutrition>("SELECT * FROM nutrition ORDER BY created_at DESC");
|
||||
}
|
||||
|
||||
/** Get by ID */
|
||||
getById(id: string): Nutrition | undefined {
|
||||
return queryOne<Nutrition>("SELECT * FROM nutrition WHERE id = ?", [id]);
|
||||
}
|
||||
|
||||
/** Create */
|
||||
create(input: CreateNutritionInput): Nutrition {
|
||||
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 nutrition (${cols.join(", ")}) VALUES (${placeholders})`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Update */
|
||||
update(id: string, input: UpdateNutritionInput): Nutrition | 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 nutrition SET ${sets.join(", ")} WHERE id = ?`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Delete */
|
||||
delete(id: string): boolean {
|
||||
const result = execute("DELETE FROM nutrition WHERE id = ?", [id]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Auto-generated Order service
|
||||
import { queryAll, queryOne, execute } from "../db/client.js";
|
||||
import type { Order, CreateOrderInput, UpdateOrderInput } from "../types/index.js";
|
||||
|
||||
export class OrderService {
|
||||
/** List all orders */
|
||||
list(): Order[] {
|
||||
return queryAll<Order>("SELECT * FROM orders ORDER BY created_at DESC");
|
||||
}
|
||||
|
||||
/** Get by ID */
|
||||
getById(id: string): Order | undefined {
|
||||
return queryOne<Order>("SELECT * FROM orders WHERE id = ?", [id]);
|
||||
}
|
||||
|
||||
/** Create */
|
||||
create(input: CreateOrderInput): Order {
|
||||
const id = crypto.randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
const hasCreatedAt = true;
|
||||
const hasUpdatedAt = false;
|
||||
const cols = ["id", "user_id", "status", "total_amount", "address_id", "created_at"];
|
||||
const placeholders = cols.map(() => "?").join(", ");
|
||||
const values = [id, input.userId ?? null, input.status ?? null, input.totalAmount ?? null, input.addressId ?? null, now];
|
||||
|
||||
execute(`INSERT INTO orders (${cols.join(", ")}) VALUES (${placeholders})`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Update */
|
||||
update(id: string, input: UpdateOrderInput): Order | 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.status !== undefined) { sets.push("status = ?"); values.push(input.status); }
|
||||
if (input.totalAmount !== undefined) { sets.push("total_amount = ?"); values.push(input.totalAmount); }
|
||||
if (input.addressId !== undefined) { sets.push("address_id = ?"); values.push(input.addressId); }
|
||||
|
||||
if (sets.length === 0) return existing;
|
||||
|
||||
|
||||
|
||||
values.push(id);
|
||||
execute(`UPDATE orders SET ${sets.join(", ")} WHERE id = ?`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Delete */
|
||||
delete(id: string): boolean {
|
||||
const result = execute("DELETE FROM orders WHERE id = ?", [id]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Auto-generated OrderItem service
|
||||
import { queryAll, queryOne, execute } from "../db/client.js";
|
||||
import type { OrderItem, CreateOrderItemInput, UpdateOrderItemInput } from "../types/index.js";
|
||||
|
||||
export class OrderItemService {
|
||||
/** List all order_items */
|
||||
list(): OrderItem[] {
|
||||
return queryAll<OrderItem>("SELECT * FROM order_items ORDER BY created_at DESC");
|
||||
}
|
||||
|
||||
/** Get by ID */
|
||||
getById(id: string): OrderItem | undefined {
|
||||
return queryOne<OrderItem>("SELECT * FROM order_items WHERE id = ?", [id]);
|
||||
}
|
||||
|
||||
/** Create */
|
||||
create(input: CreateOrderItemInput): OrderItem {
|
||||
const id = crypto.randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
const hasCreatedAt = false;
|
||||
const hasUpdatedAt = false;
|
||||
const cols = ["id", "order_id", "product_id", "quantity", "unit_price"];
|
||||
const placeholders = cols.map(() => "?").join(", ");
|
||||
const values = [id, input.orderId ?? null, input.productId ?? null, input.quantity ?? null, input.unitPrice ?? null];
|
||||
|
||||
execute(`INSERT INTO order_items (${cols.join(", ")}) VALUES (${placeholders})`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Update */
|
||||
update(id: string, input: UpdateOrderItemInput): OrderItem | undefined {
|
||||
const existing = this.getById(id);
|
||||
if (!existing) return undefined;
|
||||
|
||||
const sets: string[] = [];
|
||||
const values: unknown[] = [];
|
||||
if (input.orderId !== undefined) { sets.push("order_id = ?"); values.push(input.orderId); }
|
||||
if (input.productId !== undefined) { sets.push("product_id = ?"); values.push(input.productId); }
|
||||
if (input.quantity !== undefined) { sets.push("quantity = ?"); values.push(input.quantity); }
|
||||
if (input.unitPrice !== undefined) { sets.push("unit_price = ?"); values.push(input.unitPrice); }
|
||||
|
||||
if (sets.length === 0) return existing;
|
||||
|
||||
|
||||
|
||||
values.push(id);
|
||||
execute(`UPDATE order_items SET ${sets.join(", ")} WHERE id = ?`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Delete */
|
||||
delete(id: string): boolean {
|
||||
const result = execute("DELETE FROM order_items WHERE id = ?", [id]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Auto-generated Product service
|
||||
import { queryAll, queryOne, execute } from "../db/client.js";
|
||||
import type { Product, CreateProductInput, UpdateProductInput } from "../types/index.js";
|
||||
|
||||
export class ProductService {
|
||||
/** List all products */
|
||||
list(): Product[] {
|
||||
return queryAll<Product>("SELECT * FROM products ORDER BY created_at DESC");
|
||||
}
|
||||
|
||||
/** Get by ID */
|
||||
getById(id: string): Product | undefined {
|
||||
return queryOne<Product>("SELECT * FROM products WHERE id = ?", [id]);
|
||||
}
|
||||
|
||||
/** Create */
|
||||
create(input: CreateProductInput): Product {
|
||||
const id = crypto.randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
const hasCreatedAt = true;
|
||||
const hasUpdatedAt = false;
|
||||
const cols = ["id", "title", "price", "stock", "images", "category_id", "created_at"];
|
||||
const placeholders = cols.map(() => "?").join(", ");
|
||||
const values = [id, input.title ?? null, input.price ?? null, input.stock ?? null, input.images ?? null, input.categoryId ?? null, now];
|
||||
|
||||
execute(`INSERT INTO products (${cols.join(", ")}) VALUES (${placeholders})`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Update */
|
||||
update(id: string, input: UpdateProductInput): Product | undefined {
|
||||
const existing = this.getById(id);
|
||||
if (!existing) return undefined;
|
||||
|
||||
const sets: string[] = [];
|
||||
const values: unknown[] = [];
|
||||
if (input.title !== undefined) { sets.push("title = ?"); values.push(input.title); }
|
||||
if (input.price !== undefined) { sets.push("price = ?"); values.push(input.price); }
|
||||
if (input.stock !== undefined) { sets.push("stock = ?"); values.push(input.stock); }
|
||||
if (input.images !== undefined) { sets.push("images = ?"); values.push(input.images); }
|
||||
if (input.categoryId !== undefined) { sets.push("category_id = ?"); values.push(input.categoryId); }
|
||||
|
||||
if (sets.length === 0) return existing;
|
||||
|
||||
|
||||
|
||||
values.push(id);
|
||||
execute(`UPDATE products SET ${sets.join(", ")} WHERE id = ?`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Delete */
|
||||
delete(id: string): boolean {
|
||||
const result = execute("DELETE FROM products WHERE id = ?", [id]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Auto-generated ShoppingList service
|
||||
import { queryAll, queryOne, execute } from "../db/client.js";
|
||||
import type { ShoppingList, CreateShoppingListInput, UpdateShoppingListInput } from "../types/index.js";
|
||||
|
||||
export class ShoppingListService {
|
||||
/** List all shopping_lists */
|
||||
list(): ShoppingList[] {
|
||||
return queryAll<ShoppingList>("SELECT * FROM shopping_lists ORDER BY created_at DESC");
|
||||
}
|
||||
|
||||
/** Get by ID */
|
||||
getById(id: string): ShoppingList | undefined {
|
||||
return queryOne<ShoppingList>("SELECT * FROM shopping_lists WHERE id = ?", [id]);
|
||||
}
|
||||
|
||||
/** Create */
|
||||
create(input: CreateShoppingListInput): ShoppingList {
|
||||
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 shopping_lists (${cols.join(", ")}) VALUES (${placeholders})`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Update */
|
||||
update(id: string, input: UpdateShoppingListInput): ShoppingList | 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 shopping_lists SET ${sets.join(", ")} WHERE id = ?`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Delete */
|
||||
delete(id: string): boolean {
|
||||
const result = execute("DELETE FROM shopping_lists WHERE id = ?", [id]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Auto-generated Template service
|
||||
import { queryAll, queryOne, execute } from "../db/client.js";
|
||||
import type { Template, CreateTemplateInput, UpdateTemplateInput } from "../types/index.js";
|
||||
|
||||
export class TemplateService {
|
||||
/** List all templates */
|
||||
list(): Template[] {
|
||||
return queryAll<Template>("SELECT * FROM templates ORDER BY created_at DESC");
|
||||
}
|
||||
|
||||
/** Get by ID */
|
||||
getById(id: string): Template | undefined {
|
||||
return queryOne<Template>("SELECT * FROM templates WHERE id = ?", [id]);
|
||||
}
|
||||
|
||||
/** Create */
|
||||
create(input: CreateTemplateInput): Template {
|
||||
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 templates (${cols.join(", ")}) VALUES (${placeholders})`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Update */
|
||||
update(id: string, input: UpdateTemplateInput): Template | 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 templates SET ${sets.join(", ")} WHERE id = ?`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Delete */
|
||||
delete(id: string): boolean {
|
||||
const result = execute("DELETE FROM templates 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;
|
||||
}
|
||||
}
|
||||
@@ -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,327 @@
|
||||
// Auto-generated types
|
||||
|
||||
// ─── Base ───────────────────────────────────────────
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
nickname?: string;
|
||||
role: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface Food {
|
||||
id: string;
|
||||
userId?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
data?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface Customer {
|
||||
id: string;
|
||||
userId?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
data?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface MealPlan {
|
||||
id: string;
|
||||
userId?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
data?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface Nutrition {
|
||||
id: string;
|
||||
userId?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
data?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface ShoppingList {
|
||||
id: string;
|
||||
userId?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
data?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface Template {
|
||||
id: string;
|
||||
userId?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
data?: string;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface Product {
|
||||
id: string;
|
||||
title: string;
|
||||
price: number;
|
||||
stock?: number;
|
||||
images?: string;
|
||||
categoryId?: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface Order {
|
||||
id: string;
|
||||
userId?: string;
|
||||
status?: string;
|
||||
totalAmount?: number;
|
||||
addressId?: string;
|
||||
createdAt?: string;
|
||||
}
|
||||
|
||||
export interface OrderItem {
|
||||
id: string;
|
||||
orderId?: string;
|
||||
productId?: string;
|
||||
quantity: number;
|
||||
unitPrice?: number;
|
||||
}
|
||||
|
||||
export interface Logistic {
|
||||
id: string;
|
||||
orderId?: string;
|
||||
carrier?: string;
|
||||
trackingNumber?: string;
|
||||
status?: string;
|
||||
events?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
|
||||
export interface CreateUserInput {
|
||||
phone?: string;
|
||||
nickname?: string;
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
export interface CreateFoodInput {
|
||||
userId?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
data?: string;
|
||||
}
|
||||
|
||||
export interface CreateCustomerInput {
|
||||
userId?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
data?: string;
|
||||
}
|
||||
|
||||
export interface CreateMealPlanInput {
|
||||
userId?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
data?: string;
|
||||
}
|
||||
|
||||
export interface CreateNutritionInput {
|
||||
userId?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
data?: string;
|
||||
}
|
||||
|
||||
export interface CreateShoppingListInput {
|
||||
userId?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
data?: string;
|
||||
}
|
||||
|
||||
export interface CreateTemplateInput {
|
||||
userId?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
data?: string;
|
||||
}
|
||||
|
||||
export interface CreateProductInput {
|
||||
title: string;
|
||||
price: number;
|
||||
stock?: number;
|
||||
images?: string;
|
||||
categoryId?: string;
|
||||
}
|
||||
|
||||
export interface CreateOrderInput {
|
||||
userId?: string;
|
||||
status?: string;
|
||||
totalAmount?: number;
|
||||
addressId?: string;
|
||||
}
|
||||
|
||||
export interface CreateOrderItemInput {
|
||||
orderId?: string;
|
||||
productId?: string;
|
||||
quantity: number;
|
||||
unitPrice?: number;
|
||||
}
|
||||
|
||||
export interface CreateLogisticInput {
|
||||
orderId?: string;
|
||||
carrier?: string;
|
||||
trackingNumber?: string;
|
||||
status?: string;
|
||||
events?: string;
|
||||
}
|
||||
|
||||
export interface UpdateUserInput {
|
||||
phone?: string;
|
||||
nickname?: string;
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
export interface UpdateFoodInput {
|
||||
userId?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
data?: string;
|
||||
}
|
||||
|
||||
export interface UpdateCustomerInput {
|
||||
userId?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
data?: string;
|
||||
}
|
||||
|
||||
export interface UpdateMealPlanInput {
|
||||
userId?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
data?: string;
|
||||
}
|
||||
|
||||
export interface UpdateNutritionInput {
|
||||
userId?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
data?: string;
|
||||
}
|
||||
|
||||
export interface UpdateShoppingListInput {
|
||||
userId?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
data?: string;
|
||||
}
|
||||
|
||||
export interface UpdateTemplateInput {
|
||||
userId?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
status?: string;
|
||||
data?: string;
|
||||
}
|
||||
|
||||
export interface UpdateProductInput {
|
||||
title?: string;
|
||||
price?: number;
|
||||
stock?: number;
|
||||
images?: string;
|
||||
categoryId?: string;
|
||||
}
|
||||
|
||||
export interface UpdateOrderInput {
|
||||
userId?: string;
|
||||
status?: string;
|
||||
totalAmount?: number;
|
||||
addressId?: string;
|
||||
}
|
||||
|
||||
export interface UpdateOrderItemInput {
|
||||
orderId?: string;
|
||||
productId?: string;
|
||||
quantity?: number;
|
||||
unitPrice?: number;
|
||||
}
|
||||
|
||||
export interface UpdateLogisticInput {
|
||||
orderId?: string;
|
||||
carrier?: string;
|
||||
trackingNumber?: string;
|
||||
status?: string;
|
||||
events?: 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
@@ -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>;
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user