chore: establish workspace and initial asset data model

This commit is contained in:
陈煜 committed 2026-10-01 15:47:36 +08:00
commit 67220e38ed
20 files changed
+2853

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
DATABASE_URL="mysql://USER:PASSWORD@HOST:3306/worthpath"
PORT=3000
WEB_ORIGIN=http://localhost:5173
COOKIE_SECURE=false
+35
View File
@@ -0,0 +1,35 @@
{
"name": "@worthpath/api",
"private": true,
"scripts": {
"dev": "tsx watch src/main.ts",
"build": "tsc",
"typecheck": "tsc --noEmit",
"test": "tsx --test test/*.test.ts",
"db:generate": "prisma generate",
"db:migrate": "prisma migrate deploy"
},
"dependencies": {
"@nestjs/common": "^11.0.0",
"@nestjs/core": "^11.0.0",
"@nestjs/platform-express": "^11.0.0",
"@prisma/client": "6.19.0",
"bcryptjs": "^3.0.0",
"cookie-parser": "^1.4.7",
"decimal.js": "^10.6.0",
"dotenv": "^17.2.0",
"helmet": "^8.1.0",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.2",
"zod": "^4.1.0"
},
"devDependencies": {
"prisma": "6.19.0",
"tsx": "^4.20.0",
"typescript": "^5.9.0",
"@types/node": "^24.0.0",
"@types/express": "^5.0.0",
"@types/cookie-parser": "^1.4.9",
"mysql2": "^3.15.0"
}
}
@@ -0,0 +1,97 @@
-- CreateTable
CREATE TABLE `User` (
`id` CHAR(36) NOT NULL,
`username` VARCHAR(64) NOT NULL,
`passwordHash` VARCHAR(255) NOT NULL,
`baseCurrency` CHAR(3) NOT NULL DEFAULT 'CNY',
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
UNIQUE INDEX `User_username_key`(`username`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Session` (
`id` CHAR(64) NOT NULL,
`userId` CHAR(36) NOT NULL,
`expiresAt` DATETIME(3) NOT NULL,
INDEX `Session_userId_idx`(`userId`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Position` (
`id` CHAR(36) NOT NULL,
`userId` CHAR(36) NOT NULL,
`kind` VARCHAR(16) NOT NULL,
`side` VARCHAR(16) NOT NULL,
`name` VARCHAR(100) NOT NULL,
`category` VARCHAR(40) NOT NULL,
`currency` CHAR(3) NOT NULL,
`notes` TEXT NOT NULL,
`archived` BOOLEAN NOT NULL DEFAULT false,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
INDEX `Position_userId_kind_idx`(`userId`, `kind`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `Revision` (
`id` CHAR(36) NOT NULL,
`positionId` CHAR(36) NOT NULL,
`amount` DECIMAL(24, 8) NOT NULL,
`effectiveDate` DATE NOT NULL,
`notes` TEXT NOT NULL,
`reason` VARCHAR(20) NOT NULL,
`createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updatedAt` DATETIME(3) NOT NULL,
UNIQUE INDEX `Revision_positionId_effectiveDate_key`(`positionId`, `effectiveDate`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `PositionLink` (
`id` CHAR(36) NOT NULL,
`sourceId` CHAR(36) NOT NULL,
`targetId` CHAR(36) NOT NULL,
UNIQUE INDEX `PositionLink_sourceId_targetId_key`(`sourceId`, `targetId`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `ExchangeRate` (
`id` CHAR(36) NOT NULL,
`userId` CHAR(36) NOT NULL,
`currency` CHAR(3) NOT NULL,
`baseCurrency` CHAR(3) NOT NULL,
`date` DATE NOT NULL,
`rate` DECIMAL(24, 12) NOT NULL,
`source` VARCHAR(30) NOT NULL,
UNIQUE INDEX `ExchangeRate_userId_currency_baseCurrency_date_key`(`userId`, `currency`, `baseCurrency`, `date`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `Session` ADD CONSTRAINT `Session_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Position` ADD CONSTRAINT `Position_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `Revision` ADD CONSTRAINT `Revision_positionId_fkey` FOREIGN KEY (`positionId`) REFERENCES `Position`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `PositionLink` ADD CONSTRAINT `PositionLink_sourceId_fkey` FOREIGN KEY (`sourceId`) REFERENCES `Position`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `PositionLink` ADD CONSTRAINT `PositionLink_targetId_fkey` FOREIGN KEY (`targetId`) REFERENCES `Position`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `ExchangeRate` ADD CONSTRAINT `ExchangeRate_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `User`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1 @@
provider = "mysql"
+74
View File
@@ -0,0 +1,74 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(uuid()) @db.Char(36)
username String @unique @db.VarChar(64)
passwordHash String @db.VarChar(255)
baseCurrency String @default("CNY") @db.Char(3)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
positions Position[]
rates ExchangeRate[]
sessions Session[]
}
model Session {
id String @id @db.Char(64)
userId String @db.Char(36)
user User @relation(fields:[userId],references:[id],onDelete:Cascade)
expiresAt DateTime
@@index([userId])
}
model Position {
id String @id @default(uuid()) @db.Char(36)
userId String @db.Char(36)
user User @relation(fields:[userId],references:[id],onDelete:Cascade)
kind String @db.VarChar(16)
side String @db.VarChar(16)
name String @db.VarChar(100)
category String @db.VarChar(40)
currency String @db.Char(3)
notes String @db.Text
archived Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
revisions Revision[]
outgoing PositionLink[] @relation("Source")
incoming PositionLink[] @relation("Target")
@@index([userId,kind])
}
model Revision {
id String @id @default(uuid()) @db.Char(36)
positionId String @db.Char(36)
position Position @relation(fields:[positionId],references:[id],onDelete:Cascade)
amount Decimal @db.Decimal(24,8)
effectiveDate DateTime @db.Date
notes String @db.Text
reason String @db.VarChar(20)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([positionId,effectiveDate])
}
model PositionLink {
id String @id @default(uuid()) @db.Char(36)
sourceId String @db.Char(36)
targetId String @db.Char(36)
source Position @relation("Source",fields:[sourceId],references:[id],onDelete:Cascade)
target Position @relation("Target",fields:[targetId],references:[id],onDelete:Cascade)
@@unique([sourceId,targetId])
}
model ExchangeRate {
id String @id @default(uuid()) @db.Char(36)
userId String @db.Char(36)
user User @relation(fields:[userId],references:[id],onDelete:Cascade)
currency String @db.Char(3)
baseCurrency String @db.Char(3)
date DateTime @db.Date
rate Decimal @db.Decimal(24,12)
source String @db.VarChar(30)
@@unique([userId,currency,baseCurrency,date])
}
+3
View File
@@ -0,0 +1,3 @@
require('dotenv').config({quiet:true});
const mysql=require('mysql2/promise');
(async()=>{let db;try {const u=new URL(process.env.DATABASE_URL);const name=u.pathname.slice(1);if(!/^[a-zA-Z0-9_]+$/.test(name))throw Error();db=await mysql.createConnection({host:u.hostname,port:Number(u.port||3306),user:decodeURIComponent(u.username),password:decodeURIComponent(u.password)});const [existing]=await db.execute('SELECT SCHEMA_NAME FROM information_schema.SCHEMATA WHERE SCHEMA_NAME=?',[name]);if(existing.length){const [tables]=await db.execute('SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA=?',[name]);console.log('Database already exists; tables:',tables.map(t=>t.TABLE_NAME));}else {await db.query('CREATE DATABASE `'+name+'` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci');console.log('Created new empty project database.');}}catch(e){console.error('Database preflight failed:',e.code||'configuration error');process.exitCode=1;}finally{if(db)await db.end();}})();
+8
View File
@@ -0,0 +1,8 @@
import 'reflect-metadata';
import 'dotenv/config';
import {Module,Controller,Get} from '@nestjs/common';
import {NestFactory} from '@nestjs/core';
import {PrismaClient} from '@prisma/client';
@Controller('api') class HealthController {@Get('health') health(){return {status:'ok',app:'WorthPath'};}}
@Module({controllers:[HealthController]}) class AppModule {}
async function bootstrap(){ const db=new PrismaClient();await db.$connect(); const app=await NestFactory.create(AppModule);await app.listen(Number(process.env.PORT||3000),'0.0.0.0');}void bootstrap();
+16
View File
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"moduleResolution": "node",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"skipLibCheck": true
},
"include": [
"src/**/*.ts"
]
}