🎉 init: 小龙的工作空间
This commit is contained in:
@@ -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,119 @@
|
||||
// Items 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;
|
||||
let payload: Record<string, unknown>;
|
||||
|
||||
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;
|
||||
|
||||
// Resolve user FK references with the registered user's real ID
|
||||
payload = {
|
||||
"userId": regRes.json().user.id,
|
||||
"title": "sample-title",
|
||||
"description": "sample-description",
|
||||
"data": "sample-data"
|
||||
};
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe("GET /api/items", () => {
|
||||
it("returns empty list", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/items",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
const body = res.json();
|
||||
assert.ok(Array.isArray(body.data));
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/items", () => {
|
||||
it("creates a item", async () => {
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/items",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload,
|
||||
});
|
||||
assert.equal(res.statusCode, 201);
|
||||
const body = res.json();
|
||||
assert.ok(body.data.id);
|
||||
createdId = body.data.id;
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/items/:id", () => {
|
||||
it("returns the created item", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/items/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
const body = res.json();
|
||||
assert.equal(body.data.id, createdId);
|
||||
});
|
||||
|
||||
it("returns 404 for nonexistent id", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/items/nonexistent`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /api/items/:id", () => {
|
||||
it("updates the item", async () => {
|
||||
const res = await app.inject({
|
||||
method: "PUT",
|
||||
url: `/api/items/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload,
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /api/items/:id", () => {
|
||||
it("deletes the item", async () => {
|
||||
const res = await app.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/items/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 204);
|
||||
});
|
||||
|
||||
it("returns 404 after delete", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/items/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,119 @@
|
||||
// StockAlerts 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;
|
||||
let payload: Record<string, unknown>;
|
||||
|
||||
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;
|
||||
|
||||
// Resolve user FK references with the registered user's real ID
|
||||
payload = {
|
||||
"userId": regRes.json().user.id,
|
||||
"productName": "sample-productname",
|
||||
"currentQty": 1,
|
||||
"threshold": 1
|
||||
};
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe("GET /api/stock_alerts", () => {
|
||||
it("returns empty list", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/stock_alerts",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
const body = res.json();
|
||||
assert.ok(Array.isArray(body.data));
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/stock_alerts", () => {
|
||||
it("creates a stock_alert", async () => {
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/stock_alerts",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload,
|
||||
});
|
||||
assert.equal(res.statusCode, 201);
|
||||
const body = res.json();
|
||||
assert.ok(body.data.id);
|
||||
createdId = body.data.id;
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/stock_alerts/:id", () => {
|
||||
it("returns the created stock_alert", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/stock_alerts/${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/stock_alerts/nonexistent`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /api/stock_alerts/:id", () => {
|
||||
it("updates the stock_alert", async () => {
|
||||
const res = await app.inject({
|
||||
method: "PUT",
|
||||
url: `/api/stock_alerts/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload,
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /api/stock_alerts/:id", () => {
|
||||
it("deletes the stock_alert", async () => {
|
||||
const res = await app.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/stock_alerts/${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/stock_alerts/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
// StockIn 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;
|
||||
let payload: Record<string, unknown>;
|
||||
|
||||
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;
|
||||
|
||||
// Resolve user FK references with the registered user's real ID
|
||||
payload = {
|
||||
"userId": regRes.json().user.id,
|
||||
"productName": "sample-productname",
|
||||
"quantity": 1,
|
||||
"supplier": "sample-supplier",
|
||||
"receivedBy": "sample-receivedby"
|
||||
};
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe("GET /api/stock_in", () => {
|
||||
it("returns empty list", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/stock_in",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
const body = res.json();
|
||||
assert.ok(Array.isArray(body.data));
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/stock_in", () => {
|
||||
it("creates a stock_in", async () => {
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/stock_in",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload,
|
||||
});
|
||||
assert.equal(res.statusCode, 201);
|
||||
const body = res.json();
|
||||
assert.ok(body.data.id);
|
||||
createdId = body.data.id;
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/stock_in/:id", () => {
|
||||
it("returns the created stock_in", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/stock_in/${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/stock_in/nonexistent`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /api/stock_in/:id", () => {
|
||||
it("updates the stock_in", async () => {
|
||||
const res = await app.inject({
|
||||
method: "PUT",
|
||||
url: `/api/stock_in/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload,
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /api/stock_in/:id", () => {
|
||||
it("deletes the stock_in", async () => {
|
||||
const res = await app.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/stock_in/${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/stock_in/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
// StockOut 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;
|
||||
let payload: Record<string, unknown>;
|
||||
|
||||
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;
|
||||
|
||||
// Resolve user FK references with the registered user's real ID
|
||||
payload = {
|
||||
"userId": regRes.json().user.id,
|
||||
"productName": "sample-productname",
|
||||
"quantity": 1,
|
||||
"recipient": "sample-recipient",
|
||||
"approvedBy": "sample-approvedby",
|
||||
"shippedAt": "sample-shippedat"
|
||||
};
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe("GET /api/stock_out", () => {
|
||||
it("returns empty list", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/stock_out",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
const body = res.json();
|
||||
assert.ok(Array.isArray(body.data));
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/stock_out", () => {
|
||||
it("creates a stock_out", async () => {
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/stock_out",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload,
|
||||
});
|
||||
assert.equal(res.statusCode, 201);
|
||||
const body = res.json();
|
||||
assert.ok(body.data.id);
|
||||
createdId = body.data.id;
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/stock_out/:id", () => {
|
||||
it("returns the created stock_out", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/stock_out/${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/stock_out/nonexistent`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /api/stock_out/:id", () => {
|
||||
it("updates the stock_out", async () => {
|
||||
const res = await app.inject({
|
||||
method: "PUT",
|
||||
url: `/api/stock_out/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload,
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /api/stock_out/:id", () => {
|
||||
it("deletes the stock_out", async () => {
|
||||
const res = await app.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/stock_out/${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/stock_out/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,121 @@
|
||||
// Stocktaking 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;
|
||||
let payload: Record<string, unknown>;
|
||||
|
||||
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;
|
||||
|
||||
// Resolve user FK references with the registered user's real ID
|
||||
payload = {
|
||||
"userId": regRes.json().user.id,
|
||||
"warehouse": "sample-warehouse",
|
||||
"productName": "sample-productname",
|
||||
"expectedQty": 1,
|
||||
"actualQty": 1,
|
||||
"diff": 1
|
||||
};
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe("GET /api/stocktaking", () => {
|
||||
it("returns empty list", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/stocktaking",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
const body = res.json();
|
||||
assert.ok(Array.isArray(body.data));
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/stocktaking", () => {
|
||||
it("creates a stocktaking", async () => {
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/stocktaking",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload,
|
||||
});
|
||||
assert.equal(res.statusCode, 201);
|
||||
const body = res.json();
|
||||
assert.ok(body.data.id);
|
||||
createdId = body.data.id;
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/stocktaking/:id", () => {
|
||||
it("returns the created stocktaking", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/stocktaking/${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/stocktaking/nonexistent`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /api/stocktaking/:id", () => {
|
||||
it("updates the stocktaking", async () => {
|
||||
const res = await app.inject({
|
||||
method: "PUT",
|
||||
url: `/api/stocktaking/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload,
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /api/stocktaking/:id", () => {
|
||||
it("deletes the stocktaking", async () => {
|
||||
const res = await app.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/stocktaking/${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/stocktaking/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,120 @@
|
||||
// Warehouse 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;
|
||||
let payload: Record<string, unknown>;
|
||||
|
||||
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;
|
||||
|
||||
// Resolve user FK references with the registered user's real ID
|
||||
payload = {
|
||||
"userId": regRes.json().user.id,
|
||||
"productName": "sample-productname",
|
||||
"quantity": 1,
|
||||
"supplier": "sample-supplier",
|
||||
"receivedBy": "sample-receivedby"
|
||||
};
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe("GET /api/warehouse", () => {
|
||||
it("returns empty list", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: "/api/warehouse",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
const body = res.json();
|
||||
assert.ok(Array.isArray(body.data));
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/warehouse", () => {
|
||||
it("creates a warehouse", async () => {
|
||||
const res = await app.inject({
|
||||
method: "POST",
|
||||
url: "/api/warehouse",
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload,
|
||||
});
|
||||
assert.equal(res.statusCode, 201);
|
||||
const body = res.json();
|
||||
assert.ok(body.data.id);
|
||||
createdId = body.data.id;
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /api/warehouse/:id", () => {
|
||||
it("returns the created warehouse", async () => {
|
||||
const res = await app.inject({
|
||||
method: "GET",
|
||||
url: `/api/warehouse/${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/warehouse/nonexistent`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
});
|
||||
assert.equal(res.statusCode, 404);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PUT /api/warehouse/:id", () => {
|
||||
it("updates the warehouse", async () => {
|
||||
const res = await app.inject({
|
||||
method: "PUT",
|
||||
url: `/api/warehouse/${createdId}`,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
payload,
|
||||
});
|
||||
assert.equal(res.statusCode, 200);
|
||||
});
|
||||
});
|
||||
|
||||
describe("DELETE /api/warehouse/:id", () => {
|
||||
it("deletes the warehouse", async () => {
|
||||
const res = await app.inject({
|
||||
method: "DELETE",
|
||||
url: `/api/warehouse/${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/warehouse/${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,18 @@
|
||||
// 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 username TEXT NOT NULL UNIQUE,\n password_hash TEXT NOT NULL,\n nickname TEXT,\n role TEXT DEFAULT 'user',\n phone TEXT,\n avatar_url TEXT,\n created_at TEXT,\n updated_at TEXT\n);",
|
||||
"CREATE TABLE IF NOT EXISTS items (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n title TEXT NOT NULL,\n description TEXT,\n status TEXT,\n data TEXT,\n created_at TEXT,\n updated_at TEXT\n);",
|
||||
"CREATE TABLE IF NOT EXISTS stock_in (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n product_name TEXT NOT NULL,\n quantity INTEGER NOT NULL,\n supplier TEXT,\n received_by TEXT,\n received_at TEXT,\n created_at TEXT,\n updated_at TEXT\n);",
|
||||
"CREATE TABLE IF NOT EXISTS stock_out (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n product_name TEXT NOT NULL,\n quantity INTEGER NOT NULL,\n recipient TEXT,\n approved_by TEXT,\n shipped_at TEXT,\n created_at TEXT,\n updated_at TEXT\n);",
|
||||
"CREATE TABLE IF NOT EXISTS stocktaking (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n warehouse TEXT,\n product_name TEXT,\n expected_qty INTEGER,\n actual_qty INTEGER,\n diff INTEGER,\n stocktaken_at TEXT,\n created_at TEXT,\n updated_at TEXT\n);",
|
||||
"CREATE TABLE IF NOT EXISTS stock_alerts (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n product_name TEXT NOT NULL,\n current_qty INTEGER,\n threshold INTEGER,\n alerted_at TEXT,\n created_at TEXT,\n updated_at TEXT\n);",
|
||||
"CREATE TABLE IF NOT EXISTS warehouse (\n id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n user_id TEXT REFERENCES users(id),\n product_name TEXT NOT NULL,\n quantity INTEGER NOT NULL,\n supplier TEXT,\n received_by TEXT,\n received_at TEXT,\n created_at TEXT,\n updated_at TEXT\n);"
|
||||
];
|
||||
for (const sql of statements) {
|
||||
const trimmed = sql.trim();
|
||||
if (trimmed) db.run(trimmed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// NoteApp — 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 { itemsRoutes } from "./routes/items.js";
|
||||
import { stock_inRoutes } from "./routes/stock_in.js";
|
||||
import { stock_outRoutes } from "./routes/stock_out.js";
|
||||
import { stocktakingRoutes } from "./routes/stocktaking.js";
|
||||
import { stock_alertsRoutes } from "./routes/stock_alerts.js";
|
||||
import { warehouseRoutes } from "./routes/warehouse.js";
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || "change-me-in-production-d9c63547";
|
||||
|
||||
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(itemsRoutes, { prefix: "/api/items" });
|
||||
await app.register(stock_inRoutes, { prefix: "/api/stock_in" });
|
||||
await app.register(stock_outRoutes, { prefix: "/api/stock_out" });
|
||||
await app.register(stocktakingRoutes, { prefix: "/api/stocktaking" });
|
||||
await app.register(stock_alertsRoutes, { prefix: "/api/stock_alerts" });
|
||||
await app.register(warehouseRoutes, { prefix: "/api/warehouse" });
|
||||
|
||||
// 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 (not when imported by tests)
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Guard: only run when executed directly, not when imported
|
||||
const isMain = process.argv[1] && (import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith(process.argv[1]) || process.argv[1].endsWith("/src/index.ts") || process.argv[1].endsWith("/src/index.js"));
|
||||
if (isMain) {
|
||||
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 Items routes
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { authenticate } from "../middleware/auth.js";
|
||||
import { ItemsService } from "../services/item.js";
|
||||
import type { CreateItemsInput, UpdateItemsInput } from "../types/index.js";
|
||||
|
||||
const service = new ItemsService();
|
||||
|
||||
export async function itemsRoutes(app: FastifyInstance): Promise<void> {
|
||||
// All routes require authentication
|
||||
app.addHook("onRequest", authenticate);
|
||||
|
||||
// GET /api/items — list
|
||||
app.get("/", async (request, reply) => {
|
||||
const items = service.list();
|
||||
return { data: items };
|
||||
});
|
||||
|
||||
// GET /api/items/:id — get by id
|
||||
app.get<{ Params: { id: string } }>("/:id", async (request, reply) => {
|
||||
const item = service.getById(request.params.id);
|
||||
if (!item) {
|
||||
return reply.status(404).send({ error: "Not Found", message: "Items not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// POST /api/items — create
|
||||
app.post<{ Body: CreateItemsInput }>("/", async (request, reply) => {
|
||||
const item = service.create(request.body);
|
||||
return reply.status(201).send({ data: item });
|
||||
});
|
||||
|
||||
// PUT /api/items/:id — update
|
||||
app.put<{ Params: { id: string }; Body: UpdateItemsInput }>("/:id", async (request, reply) => {
|
||||
const item = service.update(request.params.id, request.body);
|
||||
if (!item) {
|
||||
return reply.status(404).send({ error: "Not Found", message: "Items not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// DELETE /api/items/:id — delete
|
||||
app.delete<{ Params: { id: string } }>("/:id", async (request, reply) => {
|
||||
const deleted = service.delete(request.params.id);
|
||||
if (!deleted) {
|
||||
return reply.status(404).send({ error: "Not Found", message: "Items not found", statusCode: 404 });
|
||||
}
|
||||
return reply.status(204).send();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Auto-generated StockAlerts routes
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { authenticate } from "../middleware/auth.js";
|
||||
import { StockAlertsService } from "../services/stock_alert.js";
|
||||
import type { CreateStockAlertsInput, UpdateStockAlertsInput } from "../types/index.js";
|
||||
|
||||
const service = new StockAlertsService();
|
||||
|
||||
export async function stock_alertsRoutes(app: FastifyInstance): Promise<void> {
|
||||
// All routes require authentication
|
||||
app.addHook("onRequest", authenticate);
|
||||
|
||||
// GET /api/stock_alerts — list
|
||||
app.get("/", async (request, reply) => {
|
||||
const items = service.list();
|
||||
return { data: items };
|
||||
});
|
||||
|
||||
// GET /api/stock_alerts/: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: "StockAlerts not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// POST /api/stock_alerts — create
|
||||
app.post<{ Body: CreateStockAlertsInput }>("/", async (request, reply) => {
|
||||
const item = service.create(request.body);
|
||||
return reply.status(201).send({ data: item });
|
||||
});
|
||||
|
||||
// PUT /api/stock_alerts/:id — update
|
||||
app.put<{ Params: { id: string }; Body: UpdateStockAlertsInput }>("/:id", async (request, reply) => {
|
||||
const item = service.update(request.params.id, request.body);
|
||||
if (!item) {
|
||||
return reply.status(404).send({ error: "Not Found", message: "StockAlerts not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// DELETE /api/stock_alerts/: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: "StockAlerts not found", statusCode: 404 });
|
||||
}
|
||||
return reply.status(204).send();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Auto-generated StockIn routes
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { authenticate } from "../middleware/auth.js";
|
||||
import { StockInService } from "../services/stock_in.js";
|
||||
import type { CreateStockInInput, UpdateStockInInput } from "../types/index.js";
|
||||
|
||||
const service = new StockInService();
|
||||
|
||||
export async function stock_inRoutes(app: FastifyInstance): Promise<void> {
|
||||
// All routes require authentication
|
||||
app.addHook("onRequest", authenticate);
|
||||
|
||||
// GET /api/stock_in — list
|
||||
app.get("/", async (request, reply) => {
|
||||
const items = service.list();
|
||||
return { data: items };
|
||||
});
|
||||
|
||||
// GET /api/stock_in/: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: "StockIn not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// POST /api/stock_in — create
|
||||
app.post<{ Body: CreateStockInInput }>("/", async (request, reply) => {
|
||||
const item = service.create(request.body);
|
||||
return reply.status(201).send({ data: item });
|
||||
});
|
||||
|
||||
// PUT /api/stock_in/:id — update
|
||||
app.put<{ Params: { id: string }; Body: UpdateStockInInput }>("/:id", async (request, reply) => {
|
||||
const item = service.update(request.params.id, request.body);
|
||||
if (!item) {
|
||||
return reply.status(404).send({ error: "Not Found", message: "StockIn not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// DELETE /api/stock_in/: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: "StockIn not found", statusCode: 404 });
|
||||
}
|
||||
return reply.status(204).send();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Auto-generated StockOut routes
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { authenticate } from "../middleware/auth.js";
|
||||
import { StockOutService } from "../services/stock_out.js";
|
||||
import type { CreateStockOutInput, UpdateStockOutInput } from "../types/index.js";
|
||||
|
||||
const service = new StockOutService();
|
||||
|
||||
export async function stock_outRoutes(app: FastifyInstance): Promise<void> {
|
||||
// All routes require authentication
|
||||
app.addHook("onRequest", authenticate);
|
||||
|
||||
// GET /api/stock_out — list
|
||||
app.get("/", async (request, reply) => {
|
||||
const items = service.list();
|
||||
return { data: items };
|
||||
});
|
||||
|
||||
// GET /api/stock_out/: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: "StockOut not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// POST /api/stock_out — create
|
||||
app.post<{ Body: CreateStockOutInput }>("/", async (request, reply) => {
|
||||
const item = service.create(request.body);
|
||||
return reply.status(201).send({ data: item });
|
||||
});
|
||||
|
||||
// PUT /api/stock_out/:id — update
|
||||
app.put<{ Params: { id: string }; Body: UpdateStockOutInput }>("/:id", async (request, reply) => {
|
||||
const item = service.update(request.params.id, request.body);
|
||||
if (!item) {
|
||||
return reply.status(404).send({ error: "Not Found", message: "StockOut not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// DELETE /api/stock_out/: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: "StockOut not found", statusCode: 404 });
|
||||
}
|
||||
return reply.status(204).send();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Auto-generated Stocktaking routes
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { authenticate } from "../middleware/auth.js";
|
||||
import { StocktakingService } from "../services/stocktaking.js";
|
||||
import type { CreateStocktakingInput, UpdateStocktakingInput } from "../types/index.js";
|
||||
|
||||
const service = new StocktakingService();
|
||||
|
||||
export async function stocktakingRoutes(app: FastifyInstance): Promise<void> {
|
||||
// All routes require authentication
|
||||
app.addHook("onRequest", authenticate);
|
||||
|
||||
// GET /api/stocktaking — list
|
||||
app.get("/", async (request, reply) => {
|
||||
const items = service.list();
|
||||
return { data: items };
|
||||
});
|
||||
|
||||
// GET /api/stocktaking/: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: "Stocktaking not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// POST /api/stocktaking — create
|
||||
app.post<{ Body: CreateStocktakingInput }>("/", async (request, reply) => {
|
||||
const item = service.create(request.body);
|
||||
return reply.status(201).send({ data: item });
|
||||
});
|
||||
|
||||
// PUT /api/stocktaking/:id — update
|
||||
app.put<{ Params: { id: string }; Body: UpdateStocktakingInput }>("/:id", async (request, reply) => {
|
||||
const item = service.update(request.params.id, request.body);
|
||||
if (!item) {
|
||||
return reply.status(404).send({ error: "Not Found", message: "Stocktaking not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// DELETE /api/stocktaking/: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: "Stocktaking not found", statusCode: 404 });
|
||||
}
|
||||
return reply.status(204).send();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Auto-generated Warehouse routes
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import { authenticate } from "../middleware/auth.js";
|
||||
import { WarehouseService } from "../services/warehouse.js";
|
||||
import type { CreateWarehouseInput, UpdateWarehouseInput } from "../types/index.js";
|
||||
|
||||
const service = new WarehouseService();
|
||||
|
||||
export async function warehouseRoutes(app: FastifyInstance): Promise<void> {
|
||||
// All routes require authentication
|
||||
app.addHook("onRequest", authenticate);
|
||||
|
||||
// GET /api/warehouse — list
|
||||
app.get("/", async (request, reply) => {
|
||||
const items = service.list();
|
||||
return { data: items };
|
||||
});
|
||||
|
||||
// GET /api/warehouse/: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: "Warehouse not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// POST /api/warehouse — create
|
||||
app.post<{ Body: CreateWarehouseInput }>("/", async (request, reply) => {
|
||||
const item = service.create(request.body);
|
||||
return reply.status(201).send({ data: item });
|
||||
});
|
||||
|
||||
// PUT /api/warehouse/:id — update
|
||||
app.put<{ Params: { id: string }; Body: UpdateWarehouseInput }>("/:id", async (request, reply) => {
|
||||
const item = service.update(request.params.id, request.body);
|
||||
if (!item) {
|
||||
return reply.status(404).send({ error: "Not Found", message: "Warehouse not found", statusCode: 404 });
|
||||
}
|
||||
return { data: item };
|
||||
});
|
||||
|
||||
// DELETE /api/warehouse/: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: "Warehouse not found", statusCode: 404 });
|
||||
}
|
||||
return reply.status(204).send();
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Auto-generated Items service
|
||||
import { queryAll, queryOne, execute } from "../db/client.js";
|
||||
import type { Items, CreateItemsInput, UpdateItemsInput } from "../types/index.js";
|
||||
|
||||
export class ItemsService {
|
||||
/** List all items */
|
||||
list(): Items[] {
|
||||
return queryAll<Items>("SELECT * FROM items ORDER BY created_at DESC");
|
||||
}
|
||||
|
||||
/** Get by ID */
|
||||
getById(id: string): Items | undefined {
|
||||
return queryOne<Items>("SELECT * FROM items WHERE id = ?", [id]);
|
||||
}
|
||||
|
||||
/** Create */
|
||||
create(input: CreateItemsInput): Items {
|
||||
const id = crypto.randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
const cols = ["id", "user_id", "title", "description", "data", "created_at", "updated_at"];
|
||||
const values = [id, input.userId ?? null, input.title ?? null, input.description ?? null, input.data ?? null, now, now];
|
||||
|
||||
execute(`INSERT INTO items (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?)`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Update */
|
||||
update(id: string, input: UpdateItemsInput): Items | 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.data !== undefined) { sets.push("data = ?"); values.push(input.data); }
|
||||
|
||||
if (sets.length === 0) return existing;
|
||||
|
||||
sets.push("updated_at = ?");
|
||||
values.push(new Date().toISOString());
|
||||
|
||||
values.push(id);
|
||||
execute(`UPDATE items SET ${sets.join(", ")} WHERE id = ?`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Delete */
|
||||
delete(id: string): boolean {
|
||||
const result = execute("DELETE FROM items WHERE id = ?", [id]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Auto-generated StockAlerts service
|
||||
import { queryAll, queryOne, execute } from "../db/client.js";
|
||||
import type { StockAlerts, CreateStockAlertsInput, UpdateStockAlertsInput } from "../types/index.js";
|
||||
|
||||
export class StockAlertsService {
|
||||
/** List all stock_alerts */
|
||||
list(): StockAlerts[] {
|
||||
return queryAll<StockAlerts>("SELECT * FROM stock_alerts ORDER BY created_at DESC");
|
||||
}
|
||||
|
||||
/** Get by ID */
|
||||
getById(id: string): StockAlerts | undefined {
|
||||
return queryOne<StockAlerts>("SELECT * FROM stock_alerts WHERE id = ?", [id]);
|
||||
}
|
||||
|
||||
/** Create */
|
||||
create(input: CreateStockAlertsInput): StockAlerts {
|
||||
const id = crypto.randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
const cols = ["id", "user_id", "product_name", "current_qty", "threshold", "created_at", "updated_at"];
|
||||
const values = [id, input.userId ?? null, input.productName ?? null, input.currentQty ?? null, input.threshold ?? null, now, now];
|
||||
|
||||
execute(`INSERT INTO stock_alerts (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?)`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Update */
|
||||
update(id: string, input: UpdateStockAlertsInput): StockAlerts | 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.productName !== undefined) { sets.push("product_name = ?"); values.push(input.productName); }
|
||||
if (input.currentQty !== undefined) { sets.push("current_qty = ?"); values.push(input.currentQty); }
|
||||
if (input.threshold !== undefined) { sets.push("threshold = ?"); values.push(input.threshold); }
|
||||
|
||||
if (sets.length === 0) return existing;
|
||||
|
||||
sets.push("updated_at = ?");
|
||||
values.push(new Date().toISOString());
|
||||
|
||||
values.push(id);
|
||||
execute(`UPDATE stock_alerts SET ${sets.join(", ")} WHERE id = ?`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Delete */
|
||||
delete(id: string): boolean {
|
||||
const result = execute("DELETE FROM stock_alerts WHERE id = ?", [id]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Auto-generated StockIn service
|
||||
import { queryAll, queryOne, execute } from "../db/client.js";
|
||||
import type { StockIn, CreateStockInInput, UpdateStockInInput } from "../types/index.js";
|
||||
|
||||
export class StockInService {
|
||||
/** List all stock_in */
|
||||
list(): StockIn[] {
|
||||
return queryAll<StockIn>("SELECT * FROM stock_in ORDER BY created_at DESC");
|
||||
}
|
||||
|
||||
/** Get by ID */
|
||||
getById(id: string): StockIn | undefined {
|
||||
return queryOne<StockIn>("SELECT * FROM stock_in WHERE id = ?", [id]);
|
||||
}
|
||||
|
||||
/** Create */
|
||||
create(input: CreateStockInInput): StockIn {
|
||||
const id = crypto.randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
const cols = ["id", "user_id", "product_name", "quantity", "supplier", "received_by", "created_at", "updated_at"];
|
||||
const values = [id, input.userId ?? null, input.productName ?? null, input.quantity ?? null, input.supplier ?? null, input.receivedBy ?? null, now, now];
|
||||
|
||||
execute(`INSERT INTO stock_in (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Update */
|
||||
update(id: string, input: UpdateStockInInput): StockIn | 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.productName !== undefined) { sets.push("product_name = ?"); values.push(input.productName); }
|
||||
if (input.quantity !== undefined) { sets.push("quantity = ?"); values.push(input.quantity); }
|
||||
if (input.supplier !== undefined) { sets.push("supplier = ?"); values.push(input.supplier); }
|
||||
if (input.receivedBy !== undefined) { sets.push("received_by = ?"); values.push(input.receivedBy); }
|
||||
|
||||
if (sets.length === 0) return existing;
|
||||
|
||||
sets.push("updated_at = ?");
|
||||
values.push(new Date().toISOString());
|
||||
|
||||
values.push(id);
|
||||
execute(`UPDATE stock_in SET ${sets.join(", ")} WHERE id = ?`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Delete */
|
||||
delete(id: string): boolean {
|
||||
const result = execute("DELETE FROM stock_in WHERE id = ?", [id]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Auto-generated StockOut service
|
||||
import { queryAll, queryOne, execute } from "../db/client.js";
|
||||
import type { StockOut, CreateStockOutInput, UpdateStockOutInput } from "../types/index.js";
|
||||
|
||||
export class StockOutService {
|
||||
/** List all stock_out */
|
||||
list(): StockOut[] {
|
||||
return queryAll<StockOut>("SELECT * FROM stock_out ORDER BY created_at DESC");
|
||||
}
|
||||
|
||||
/** Get by ID */
|
||||
getById(id: string): StockOut | undefined {
|
||||
return queryOne<StockOut>("SELECT * FROM stock_out WHERE id = ?", [id]);
|
||||
}
|
||||
|
||||
/** Create */
|
||||
create(input: CreateStockOutInput): StockOut {
|
||||
const id = crypto.randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
const cols = ["id", "user_id", "product_name", "quantity", "recipient", "approved_by", "shipped_at", "created_at", "updated_at"];
|
||||
const values = [id, input.userId ?? null, input.productName ?? null, input.quantity ?? null, input.recipient ?? null, input.approvedBy ?? null, input.shippedAt ?? null, now, now];
|
||||
|
||||
execute(`INSERT INTO stock_out (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Update */
|
||||
update(id: string, input: UpdateStockOutInput): StockOut | 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.productName !== undefined) { sets.push("product_name = ?"); values.push(input.productName); }
|
||||
if (input.quantity !== undefined) { sets.push("quantity = ?"); values.push(input.quantity); }
|
||||
if (input.recipient !== undefined) { sets.push("recipient = ?"); values.push(input.recipient); }
|
||||
if (input.approvedBy !== undefined) { sets.push("approved_by = ?"); values.push(input.approvedBy); }
|
||||
if (input.shippedAt !== undefined) { sets.push("shipped_at = ?"); values.push(input.shippedAt); }
|
||||
|
||||
if (sets.length === 0) return existing;
|
||||
|
||||
sets.push("updated_at = ?");
|
||||
values.push(new Date().toISOString());
|
||||
|
||||
values.push(id);
|
||||
execute(`UPDATE stock_out SET ${sets.join(", ")} WHERE id = ?`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Delete */
|
||||
delete(id: string): boolean {
|
||||
const result = execute("DELETE FROM stock_out WHERE id = ?", [id]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Auto-generated Stocktaking service
|
||||
import { queryAll, queryOne, execute } from "../db/client.js";
|
||||
import type { Stocktaking, CreateStocktakingInput, UpdateStocktakingInput } from "../types/index.js";
|
||||
|
||||
export class StocktakingService {
|
||||
/** List all stocktaking */
|
||||
list(): Stocktaking[] {
|
||||
return queryAll<Stocktaking>("SELECT * FROM stocktaking ORDER BY created_at DESC");
|
||||
}
|
||||
|
||||
/** Get by ID */
|
||||
getById(id: string): Stocktaking | undefined {
|
||||
return queryOne<Stocktaking>("SELECT * FROM stocktaking WHERE id = ?", [id]);
|
||||
}
|
||||
|
||||
/** Create */
|
||||
create(input: CreateStocktakingInput): Stocktaking {
|
||||
const id = crypto.randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
const cols = ["id", "user_id", "warehouse", "product_name", "expected_qty", "actual_qty", "diff", "created_at", "updated_at"];
|
||||
const values = [id, input.userId ?? null, input.warehouse ?? null, input.productName ?? null, input.expectedQty ?? null, input.actualQty ?? null, input.diff ?? null, now, now];
|
||||
|
||||
execute(`INSERT INTO stocktaking (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Update */
|
||||
update(id: string, input: UpdateStocktakingInput): Stocktaking | 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.warehouse !== undefined) { sets.push("warehouse = ?"); values.push(input.warehouse); }
|
||||
if (input.productName !== undefined) { sets.push("product_name = ?"); values.push(input.productName); }
|
||||
if (input.expectedQty !== undefined) { sets.push("expected_qty = ?"); values.push(input.expectedQty); }
|
||||
if (input.actualQty !== undefined) { sets.push("actual_qty = ?"); values.push(input.actualQty); }
|
||||
if (input.diff !== undefined) { sets.push("diff = ?"); values.push(input.diff); }
|
||||
|
||||
if (sets.length === 0) return existing;
|
||||
|
||||
sets.push("updated_at = ?");
|
||||
values.push(new Date().toISOString());
|
||||
|
||||
values.push(id);
|
||||
execute(`UPDATE stocktaking SET ${sets.join(", ")} WHERE id = ?`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Delete */
|
||||
delete(id: string): boolean {
|
||||
const result = execute("DELETE FROM stocktaking WHERE id = ?", [id]);
|
||||
return result.changes > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Auto-generated Warehouse service
|
||||
import { queryAll, queryOne, execute } from "../db/client.js";
|
||||
import type { Warehouse, CreateWarehouseInput, UpdateWarehouseInput } from "../types/index.js";
|
||||
|
||||
export class WarehouseService {
|
||||
/** List all warehouse */
|
||||
list(): Warehouse[] {
|
||||
return queryAll<Warehouse>("SELECT * FROM warehouse ORDER BY created_at DESC");
|
||||
}
|
||||
|
||||
/** Get by ID */
|
||||
getById(id: string): Warehouse | undefined {
|
||||
return queryOne<Warehouse>("SELECT * FROM warehouse WHERE id = ?", [id]);
|
||||
}
|
||||
|
||||
/** Create */
|
||||
create(input: CreateWarehouseInput): Warehouse {
|
||||
const id = crypto.randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
const cols = ["id", "user_id", "product_name", "quantity", "supplier", "received_by", "created_at", "updated_at"];
|
||||
const values = [id, input.userId ?? null, input.productName ?? null, input.quantity ?? null, input.supplier ?? null, input.receivedBy ?? null, now, now];
|
||||
|
||||
execute(`INSERT INTO warehouse (${cols.join(", ")}) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Update */
|
||||
update(id: string, input: UpdateWarehouseInput): Warehouse | 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.productName !== undefined) { sets.push("product_name = ?"); values.push(input.productName); }
|
||||
if (input.quantity !== undefined) { sets.push("quantity = ?"); values.push(input.quantity); }
|
||||
if (input.supplier !== undefined) { sets.push("supplier = ?"); values.push(input.supplier); }
|
||||
if (input.receivedBy !== undefined) { sets.push("received_by = ?"); values.push(input.receivedBy); }
|
||||
|
||||
if (sets.length === 0) return existing;
|
||||
|
||||
sets.push("updated_at = ?");
|
||||
values.push(new Date().toISOString());
|
||||
|
||||
values.push(id);
|
||||
execute(`UPDATE warehouse SET ${sets.join(", ")} WHERE id = ?`, values);
|
||||
return this.getById(id)!;
|
||||
}
|
||||
|
||||
/** Delete */
|
||||
delete(id: string): boolean {
|
||||
const result = execute("DELETE FROM warehouse 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,169 @@
|
||||
// Import and re-export shared entity types
|
||||
import type {
|
||||
User,
|
||||
Items,
|
||||
StockIn,
|
||||
StockOut,
|
||||
Stocktaking,
|
||||
StockAlerts,
|
||||
Warehouse,
|
||||
ApiResponse,
|
||||
PaginatedResponse,
|
||||
ErrorResponse,
|
||||
} from "@shared/types";
|
||||
|
||||
export type {
|
||||
User,
|
||||
Items,
|
||||
StockIn,
|
||||
StockOut,
|
||||
Stocktaking,
|
||||
StockAlerts,
|
||||
Warehouse,
|
||||
ApiResponse,
|
||||
PaginatedResponse,
|
||||
ErrorResponse,
|
||||
};
|
||||
|
||||
// Auto-generated types (from Model Contract)
|
||||
|
||||
export interface CreateUserInput {
|
||||
username: string;
|
||||
passwordHash: string;
|
||||
nickname?: string;
|
||||
role?: string;
|
||||
phone?: string;
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
export interface CreateItemsInput {
|
||||
userId?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface CreateStockInInput {
|
||||
userId?: string;
|
||||
productName: string;
|
||||
quantity: number;
|
||||
supplier?: string;
|
||||
receivedBy?: string;
|
||||
}
|
||||
|
||||
export interface CreateStockOutInput {
|
||||
userId?: string;
|
||||
productName: string;
|
||||
quantity: number;
|
||||
recipient?: string;
|
||||
approvedBy?: string;
|
||||
shippedAt?: string;
|
||||
}
|
||||
|
||||
export interface CreateStocktakingInput {
|
||||
userId?: string;
|
||||
warehouse?: string;
|
||||
productName?: string;
|
||||
expectedQty?: number;
|
||||
actualQty?: number;
|
||||
diff?: number;
|
||||
}
|
||||
|
||||
export interface CreateStockAlertsInput {
|
||||
userId?: string;
|
||||
productName: string;
|
||||
currentQty?: number;
|
||||
threshold?: number;
|
||||
}
|
||||
|
||||
export interface CreateWarehouseInput {
|
||||
userId?: string;
|
||||
productName: string;
|
||||
quantity: number;
|
||||
supplier?: string;
|
||||
receivedBy?: string;
|
||||
}
|
||||
|
||||
export interface UpdateUserInput {
|
||||
username?: string;
|
||||
passwordHash?: string;
|
||||
nickname?: string;
|
||||
role?: string;
|
||||
phone?: string;
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
export interface UpdateItemsInput {
|
||||
userId?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
data?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface UpdateStockInInput {
|
||||
userId?: string;
|
||||
productName?: string;
|
||||
quantity?: number;
|
||||
supplier?: string;
|
||||
receivedBy?: string;
|
||||
}
|
||||
|
||||
export interface UpdateStockOutInput {
|
||||
userId?: string;
|
||||
productName?: string;
|
||||
quantity?: number;
|
||||
recipient?: string;
|
||||
approvedBy?: string;
|
||||
shippedAt?: string;
|
||||
}
|
||||
|
||||
export interface UpdateStocktakingInput {
|
||||
userId?: string;
|
||||
warehouse?: string;
|
||||
productName?: string;
|
||||
expectedQty?: number;
|
||||
actualQty?: number;
|
||||
diff?: number;
|
||||
}
|
||||
|
||||
export interface UpdateStockAlertsInput {
|
||||
userId?: string;
|
||||
productName?: string;
|
||||
currentQty?: number;
|
||||
threshold?: number;
|
||||
}
|
||||
|
||||
export interface UpdateWarehouseInput {
|
||||
userId?: string;
|
||||
productName?: string;
|
||||
quantity?: number;
|
||||
supplier?: string;
|
||||
receivedBy?: 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 ────────────────────────────────────────────
|
||||
// ─── JWT ────────────────────────────────────────────
|
||||
export interface JwtPayload {
|
||||
userId: string;
|
||||
username: string;
|
||||
role: string;
|
||||
iat?: number;
|
||||
exp?: number;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
Reference in New Issue
Block a user