feat(api): first version of the implementation, done with opencode and multiple iterations
This commit is contained in:
parent
aec3f2ff98
commit
7d72932a16
12
.env.example
Normal file
12
.env.example
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# PostgreSQL
|
||||||
|
POSTGRES_HOST=localhost
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
POSTGRES_USER=postgres
|
||||||
|
POSTGRES_PASSWORD=postgres
|
||||||
|
POSTGRES_DB=work_hub
|
||||||
|
|
||||||
|
# API
|
||||||
|
API_PORT=3001
|
||||||
|
|
||||||
|
# Web
|
||||||
|
WEB_PORT=3000
|
||||||
|
|
@ -10,5 +10,6 @@
|
||||||
"mode": "semantic",
|
"mode": "semantic",
|
||||||
"ignore": ["**/lib/**", "**/legacy/**", "**/__generated__/**", "**/generated/**"]
|
"ignore": ["**/lib/**", "**/legacy/**", "**/__generated__/**", "**/generated/**"]
|
||||||
},
|
},
|
||||||
"rules": {}
|
"rules": {},
|
||||||
|
"ignoreDependencies": ["@nestjs/schematics", "eslint-plugin-prettier"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,6 @@ All commands run from the repository root.
|
||||||
- Key relaxed rules:
|
- Key relaxed rules:
|
||||||
- `@typescript-eslint/no-explicit-any: off`
|
- `@typescript-eslint/no-explicit-any: off`
|
||||||
- `@typescript-eslint/no-floating-promises: warn`
|
- `@typescript-eslint/no-floating-promises: warn`
|
||||||
- `@typescript-eslint/no-unsafe-argument: warn`
|
|
||||||
- Web uses `@nuxt/eslint` with stylistic config (`commaDangle: never`, `braceStyle: 1tbs`).
|
- Web uses `@nuxt/eslint` with stylistic config (`commaDangle: never`, `braceStyle: 1tbs`).
|
||||||
|
|
||||||
### TypeScript
|
### TypeScript
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,78 @@
|
||||||
import { createNestjsConfig } from "@workspace/tooling/eslint/nestjs";
|
import { base, baseRules } from "@workspace/tooling/eslint/base";
|
||||||
|
import { globals } from "@workspace/tooling/eslint/nestjs";
|
||||||
|
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
|
||||||
|
import { defineConfig } from "eslint/config";
|
||||||
|
|
||||||
export default createNestjsConfig(import.meta.dirname);
|
export default defineConfig(
|
||||||
|
{ ignores: ["eslint.config.mjs"] },
|
||||||
|
...base,
|
||||||
|
eslintPluginPrettierRecommended,
|
||||||
|
{
|
||||||
|
files: ["src/**/*.ts"],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.jest,
|
||||||
|
},
|
||||||
|
sourceType: "commonjs",
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["src/**/*.ts"],
|
||||||
|
rules: {
|
||||||
|
"class-methods-use-this": "off",
|
||||||
|
"@typescript-eslint/no-floating-promises": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-argument": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-assignment": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-call": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-member-access": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-return": "warn",
|
||||||
|
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["src/**/*.spec.ts"],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.jest,
|
||||||
|
},
|
||||||
|
sourceType: "commonjs",
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/unbound-method": "off",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["test/**/*.ts"],
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.jest,
|
||||||
|
},
|
||||||
|
sourceType: "commonjs",
|
||||||
|
parserOptions: {
|
||||||
|
project: "./tsconfig.eslint.json",
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
"class-methods-use-this": "off",
|
||||||
|
"@typescript-eslint/no-floating-promises": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-argument": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-assignment": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-call": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-member-access": "warn",
|
||||||
|
"@typescript-eslint/no-unsafe-return": "warn",
|
||||||
|
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -26,27 +26,31 @@
|
||||||
"@nestjs/core": "^11.1.26",
|
"@nestjs/core": "^11.1.26",
|
||||||
"@nestjs/platform-express": "^11.1.26",
|
"@nestjs/platform-express": "^11.1.26",
|
||||||
"@workspace/schema": "workspace:*",
|
"@workspace/schema": "workspace:*",
|
||||||
|
"drizzle-orm": "^0.45.2",
|
||||||
|
"pg": "^8.21.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.2"
|
"rxjs": "^7.8.2",
|
||||||
|
"valibot": "^1.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@jest/globals": "^30.4.1",
|
||||||
"@nestjs/cli": "^11.0.23",
|
"@nestjs/cli": "^11.0.23",
|
||||||
"@nestjs/schematics": "^11.1.0",
|
"@nestjs/schematics": "^11.1.0",
|
||||||
"@nestjs/testing": "^11.1.26",
|
"@nestjs/testing": "^11.1.26",
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^25.9.3",
|
"@types/node": "^25.9.3",
|
||||||
|
"@types/pg": "^8.20.0",
|
||||||
"@types/supertest": "^7.2.0",
|
"@types/supertest": "^7.2.0",
|
||||||
"@workspace/tooling": "workspace:*",
|
"@workspace/tooling": "workspace:*",
|
||||||
"eslint": "^10.4.1",
|
"eslint": "^10.4.1",
|
||||||
|
"eslint-plugin-prettier": "^5.5.6",
|
||||||
"jest": "^30.4.2",
|
"jest": "^30.4.2",
|
||||||
"prettier": "^3.8.4",
|
"prettier": "^3.8.4",
|
||||||
"source-map-support": "^0.5.21",
|
|
||||||
"supertest": "^7.2.2",
|
"supertest": "^7.2.2",
|
||||||
"ts-jest": "^29.4.11",
|
"ts-jest": "^29.4.11",
|
||||||
"ts-loader": "^9.6.0",
|
"ts-loader": "^9.6.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsconfig-paths": "^4.2.0",
|
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
|
|
@ -64,6 +68,10 @@
|
||||||
"**/*.(t|j)s"
|
"**/*.(t|j)s"
|
||||||
],
|
],
|
||||||
"coverageDirectory": "../coverage",
|
"coverageDirectory": "../coverage",
|
||||||
"testEnvironment": "node"
|
"testEnvironment": "node",
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^@workspace/schema$": "<rootDir>/../test/__mocks__/schema.ts",
|
||||||
|
"^valibot$": "<rootDir>/../test/__mocks__/valibot.ts"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
import { AppController } from "./app.controller";
|
import { AppController } from "./app.controller";
|
||||||
import { AppService } from "./app.service";
|
import { AppService } from "./app.service";
|
||||||
|
import { ProjectModule } from "./project/project.module";
|
||||||
|
import { DatabaseModule } from "./database/database.module";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [ProjectModule, DatabaseModule],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
})
|
})
|
||||||
|
|
|
||||||
28
apps/api/src/common/pipes/valibot-validation.pipe.ts
Normal file
28
apps/api/src/common/pipes/valibot-validation.pipe.ts
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { PipeTransform, Injectable, BadRequestException } from "@nestjs/common";
|
||||||
|
import { safeParse, type InferOutput, type BaseSchema, type BaseIssue } from "valibot";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ValibotValidationPipe implements PipeTransform {
|
||||||
|
constructor(private readonly schema: BaseSchema<unknown, unknown, BaseIssue<unknown>>) {}
|
||||||
|
|
||||||
|
transform(value: unknown): InferOutput<BaseSchema<unknown, unknown, BaseIssue<unknown>>> {
|
||||||
|
const result = safeParse(this.schema, value);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
const messages = result.issues.map((issue) => {
|
||||||
|
const path = issue.path?.map((p) => String(p.key)).join(".") ?? "root";
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
message: issue.message,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
throw new BadRequestException({
|
||||||
|
statusCode: 400,
|
||||||
|
message: "Validation failed",
|
||||||
|
errors: messages,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.output;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
apps/api/src/database/database.module.ts
Normal file
9
apps/api/src/database/database.module.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { Global, Module } from "@nestjs/common";
|
||||||
|
import { DrizzleService } from "./drizzle.service";
|
||||||
|
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [DrizzleService],
|
||||||
|
exports: [DrizzleService],
|
||||||
|
})
|
||||||
|
export class DatabaseModule {}
|
||||||
43
apps/api/src/database/drizzle.service.ts
Normal file
43
apps/api/src/database/drizzle.service.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { Injectable, OnModuleInit, OnModuleDestroy } from "@nestjs/common";
|
||||||
|
import { Pool } from "pg";
|
||||||
|
import {
|
||||||
|
drizzle,
|
||||||
|
NodePgDatabase,
|
||||||
|
migrate,
|
||||||
|
projects,
|
||||||
|
tasks,
|
||||||
|
projectsRelations,
|
||||||
|
tasksRelations,
|
||||||
|
migrationsDir,
|
||||||
|
} from "@workspace/schema";
|
||||||
|
|
||||||
|
const DEFAULT_PG_PORT = 5432;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class DrizzleService implements OnModuleInit, OnModuleDestroy {
|
||||||
|
private pool: Pool;
|
||||||
|
public db: NodePgDatabase<Record<string, unknown>>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.pool = new Pool({
|
||||||
|
host: String(process.env.POSTGRES_HOST ?? "localhost"),
|
||||||
|
port: Number(process.env.POSTGRES_PORT ?? DEFAULT_PG_PORT),
|
||||||
|
user: String(process.env.POSTGRES_USER ?? "postgres"),
|
||||||
|
password: String(process.env.POSTGRES_PASSWORD ?? "postgres"),
|
||||||
|
database: String(process.env.POSTGRES_DB ?? "work_hub"),
|
||||||
|
});
|
||||||
|
this.db = drizzle(this.pool, {
|
||||||
|
schema: { projects, tasks, projectsRelations, tasksRelations },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleInit() {
|
||||||
|
await migrate(this.db, {
|
||||||
|
migrationsFolder: migrationsDir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async onModuleDestroy() {
|
||||||
|
await this.pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
74
apps/api/src/project/project.controller.spec.ts
Normal file
74
apps/api/src/project/project.controller.spec.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
|
import { ProjectController } from "./project.controller";
|
||||||
|
import { ProjectService } from "./project.service";
|
||||||
|
import { ProjectRepository } from "./project.repository";
|
||||||
|
|
||||||
|
describe("ProjectController", () => {
|
||||||
|
let controller: ProjectController;
|
||||||
|
|
||||||
|
const mockRepository = {
|
||||||
|
create: jest.fn(),
|
||||||
|
findAll: jest.fn(),
|
||||||
|
findOne: jest.fn(),
|
||||||
|
update: jest.fn(),
|
||||||
|
remove: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [ProjectController],
|
||||||
|
providers: [ProjectService, { provide: ProjectRepository, useValue: mockRepository }],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<ProjectController>(ProjectController);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be defined", () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("findAll", () => {
|
||||||
|
it("should return an array", async () => {
|
||||||
|
mockRepository.findAll.mockResolvedValue([{ id: "1", name: "Test" }]);
|
||||||
|
const result = await controller.findAll();
|
||||||
|
expect(Array.isArray(result)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("findOne", () => {
|
||||||
|
it("should return an object with the given id", async () => {
|
||||||
|
const expected = { id: "1", name: "Test" };
|
||||||
|
mockRepository.findOne.mockResolvedValue(expected);
|
||||||
|
const result = await controller.findOne("1");
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("create", () => {
|
||||||
|
it("should return the created body", async () => {
|
||||||
|
const body = { name: "Test Project" };
|
||||||
|
mockRepository.create.mockResolvedValue(body);
|
||||||
|
const result = await controller.create(body);
|
||||||
|
expect(result).toEqual(body);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("update", () => {
|
||||||
|
it("should merge id with the update body", async () => {
|
||||||
|
const body = { name: "Updated" };
|
||||||
|
mockRepository.findOne.mockResolvedValue({ id: "1" });
|
||||||
|
mockRepository.update.mockResolvedValue({ id: "1", ...body });
|
||||||
|
const result = await controller.update("1", body);
|
||||||
|
expect(result).toEqual({ id: "1", ...body });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("remove", () => {
|
||||||
|
it("should return the removed id", async () => {
|
||||||
|
mockRepository.findOne.mockResolvedValue({ id: "1" });
|
||||||
|
mockRepository.remove.mockResolvedValue({ id: "1" });
|
||||||
|
const result = await controller.remove("1");
|
||||||
|
expect(result).toEqual({ id: "1" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
37
apps/api/src/project/project.controller.ts
Normal file
37
apps/api/src/project/project.controller.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { Controller, Get, Post, Body, Patch, Param, Delete, ParseUUIDPipe } from "@nestjs/common";
|
||||||
|
import { ProjectService } from "./project.service";
|
||||||
|
import { projectInsert, projectUpdate, type ProjectInsertDTO, type ProjectUpdateDTO } from "@workspace/schema";
|
||||||
|
import { ValibotValidationPipe } from "../common/pipes/valibot-validation.pipe";
|
||||||
|
|
||||||
|
@Controller("projects")
|
||||||
|
export class ProjectController {
|
||||||
|
constructor(private readonly projectService: ProjectService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
create(@Body(new ValibotValidationPipe(projectInsert)) body: ProjectInsertDTO) {
|
||||||
|
return this.projectService.create(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
findAll() {
|
||||||
|
return this.projectService.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(":id")
|
||||||
|
findOne(@Param("id", ParseUUIDPipe) id: string) {
|
||||||
|
return this.projectService.findOne(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch(":id")
|
||||||
|
update(
|
||||||
|
@Param("id", ParseUUIDPipe) id: string,
|
||||||
|
@Body(new ValibotValidationPipe(projectUpdate)) body: ProjectUpdateDTO,
|
||||||
|
) {
|
||||||
|
return this.projectService.update(id, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(":id")
|
||||||
|
remove(@Param("id", ParseUUIDPipe) id: string) {
|
||||||
|
return this.projectService.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
apps/api/src/project/project.module.ts
Normal file
10
apps/api/src/project/project.module.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { ProjectService } from "./project.service";
|
||||||
|
import { ProjectController } from "./project.controller";
|
||||||
|
import { ProjectRepository } from "./project.repository";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [ProjectController],
|
||||||
|
providers: [ProjectService, ProjectRepository],
|
||||||
|
})
|
||||||
|
export class ProjectModule {}
|
||||||
36
apps/api/src/project/project.repository.ts
Normal file
36
apps/api/src/project/project.repository.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { projects, eq, ProjectInsertDTO, ProjectUpdateDTO } from "@workspace/schema";
|
||||||
|
import { DrizzleService } from "../database/drizzle.service";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ProjectRepository {
|
||||||
|
constructor(private readonly drizzle: DrizzleService) {}
|
||||||
|
|
||||||
|
private get db() {
|
||||||
|
return this.drizzle.db;
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(body: ProjectInsertDTO) {
|
||||||
|
const [project] = await this.db.insert(projects).values(body).returning();
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
findAll() {
|
||||||
|
return this.db.select().from(projects);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string) {
|
||||||
|
const [project] = await this.db.select().from(projects).where(eq(projects.id, id));
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, body: ProjectUpdateDTO) {
|
||||||
|
const [project] = await this.db.update(projects).set(body).where(eq(projects.id, id)).returning();
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string) {
|
||||||
|
const [project] = await this.db.delete(projects).where(eq(projects.id, id)).returning();
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
}
|
||||||
112
apps/api/src/project/project.service.spec.ts
Normal file
112
apps/api/src/project/project.service.spec.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
import { Test, TestingModule } from "@nestjs/testing";
|
||||||
|
import { NotFoundException } from "@nestjs/common";
|
||||||
|
import { ProjectService } from "./project.service";
|
||||||
|
import { ProjectRepository } from "./project.repository";
|
||||||
|
|
||||||
|
type MockProject = { id: string; name: string };
|
||||||
|
|
||||||
|
describe("ProjectService", () => {
|
||||||
|
let service: ProjectService;
|
||||||
|
|
||||||
|
const mockFindAll = jest.fn<Promise<MockProject[]>, []>();
|
||||||
|
const mockFindOne = jest.fn<Promise<MockProject | undefined>, [string]>();
|
||||||
|
const mockCreate = jest.fn<Promise<MockProject>, [Record<string, unknown>]>();
|
||||||
|
const mockUpdate = jest.fn<Promise<MockProject>, [string, Record<string, unknown>]>();
|
||||||
|
const mockRemove = jest.fn<Promise<MockProject>, [string]>();
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mockFindAll.mockReset();
|
||||||
|
mockFindOne.mockReset();
|
||||||
|
mockCreate.mockReset();
|
||||||
|
mockUpdate.mockReset();
|
||||||
|
mockRemove.mockReset();
|
||||||
|
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
ProjectService,
|
||||||
|
{
|
||||||
|
provide: ProjectRepository,
|
||||||
|
useValue: {
|
||||||
|
findAll: mockFindAll,
|
||||||
|
findOne: mockFindOne,
|
||||||
|
create: mockCreate,
|
||||||
|
update: mockUpdate,
|
||||||
|
remove: mockRemove,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<ProjectService>(ProjectService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be defined", () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("findAll", () => {
|
||||||
|
it("should return all projects", async () => {
|
||||||
|
const expected = [{ id: "1", name: "Test" }];
|
||||||
|
mockFindAll.mockResolvedValue(expected);
|
||||||
|
|
||||||
|
const result = await service.findAll();
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
expect(mockFindAll).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("findOne", () => {
|
||||||
|
it("should return a project by id", async () => {
|
||||||
|
const expected = { id: "1", name: "Test" };
|
||||||
|
mockFindOne.mockResolvedValue(expected);
|
||||||
|
|
||||||
|
const result = await service.findOne("1");
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
expect(mockFindOne).toHaveBeenCalledWith("1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw NotFoundException when project not found", async () => {
|
||||||
|
mockFindOne.mockResolvedValue(undefined);
|
||||||
|
|
||||||
|
await expect(service.findOne("nonexistent")).rejects.toThrow(NotFoundException);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("create", () => {
|
||||||
|
it("should create and return a project", async () => {
|
||||||
|
const body = { name: "New Project" };
|
||||||
|
const expected = { id: "1", name: "New Project" };
|
||||||
|
mockCreate.mockResolvedValue(expected);
|
||||||
|
|
||||||
|
const result = await service.create(body);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
expect(mockCreate).toHaveBeenCalledWith(body);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("update", () => {
|
||||||
|
it("should update and return a project", async () => {
|
||||||
|
const body = { name: "Updated" };
|
||||||
|
const expected = { id: "1", name: "Updated" };
|
||||||
|
mockFindOne.mockResolvedValue({ id: "1" } as MockProject);
|
||||||
|
mockUpdate.mockResolvedValue(expected);
|
||||||
|
|
||||||
|
const result = await service.update("1", body);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
expect(mockFindOne).toHaveBeenCalledWith("1");
|
||||||
|
expect(mockUpdate).toHaveBeenCalledWith("1", body);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("remove", () => {
|
||||||
|
it("should delete and return a project", async () => {
|
||||||
|
mockFindOne.mockResolvedValue({ id: "1" } as MockProject);
|
||||||
|
mockRemove.mockResolvedValue({ id: "1" } as MockProject);
|
||||||
|
|
||||||
|
const result = await service.remove("1");
|
||||||
|
expect(result).toEqual({ id: "1" });
|
||||||
|
expect(mockFindOne).toHaveBeenCalledWith("1");
|
||||||
|
expect(mockRemove).toHaveBeenCalledWith("1");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
32
apps/api/src/project/project.service.ts
Normal file
32
apps/api/src/project/project.service.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||||
|
import type { ProjectInsertDTO, ProjectUpdateDTO } from "@workspace/schema";
|
||||||
|
import { ProjectRepository } from "./project.repository";
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ProjectService {
|
||||||
|
constructor(private readonly projectRepository: ProjectRepository) {}
|
||||||
|
|
||||||
|
create(body: ProjectInsertDTO) {
|
||||||
|
return this.projectRepository.create(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
findAll() {
|
||||||
|
return this.projectRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string) {
|
||||||
|
const project = await this.projectRepository.findOne(id);
|
||||||
|
if (!project) throw new NotFoundException();
|
||||||
|
return project;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, body: ProjectUpdateDTO) {
|
||||||
|
await this.findOne(id);
|
||||||
|
return this.projectRepository.update(id, body);
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string) {
|
||||||
|
await this.findOne(id);
|
||||||
|
return this.projectRepository.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
apps/api/test/__mocks__/schema.ts
Normal file
21
apps/api/test/__mocks__/schema.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { jest } from "@jest/globals";
|
||||||
|
|
||||||
|
const noopSchema = {
|
||||||
|
type: "object",
|
||||||
|
entries: {},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const mockTable = {};
|
||||||
|
|
||||||
|
class MockNodePgDatabase {}
|
||||||
|
|
||||||
|
export const projectInsert = noopSchema;
|
||||||
|
export const projectUpdate = noopSchema;
|
||||||
|
export const projects = mockTable;
|
||||||
|
export const tasks = mockTable;
|
||||||
|
export const projectsRelations = {};
|
||||||
|
export const tasksRelations = {};
|
||||||
|
export const eq = jest.fn(() => ({}));
|
||||||
|
export const drizzle = jest.fn(() => new MockNodePgDatabase());
|
||||||
|
export const NodePgDatabase = MockNodePgDatabase;
|
||||||
|
export const migrate = jest.fn(() => Promise.resolve());
|
||||||
6
apps/api/test/__mocks__/valibot.ts
Normal file
6
apps/api/test/__mocks__/valibot.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { jest } from "@jest/globals";
|
||||||
|
|
||||||
|
export const safeParse = jest.fn((_schema: unknown, input: unknown) => ({
|
||||||
|
success: true as const,
|
||||||
|
output: input,
|
||||||
|
}));
|
||||||
|
|
@ -3,20 +3,81 @@ import { INestApplication } from "@nestjs/common";
|
||||||
import request from "supertest";
|
import request from "supertest";
|
||||||
import { App } from "supertest/types";
|
import { App } from "supertest/types";
|
||||||
import { AppModule } from "./../src/app.module";
|
import { AppModule } from "./../src/app.module";
|
||||||
|
import { ProjectRepository } from "./../src/project/project.repository";
|
||||||
|
|
||||||
describe("AppController (e2e)", () => {
|
describe("AppController (e2e)", () => {
|
||||||
let app: INestApplication<App>;
|
let app: INestApplication<App>;
|
||||||
|
|
||||||
|
const TEST_UUID = "550e8400-e29b-41d4-a716-446655440000";
|
||||||
|
|
||||||
|
const mockRepository = {
|
||||||
|
create: jest.fn(),
|
||||||
|
findAll: jest.fn(),
|
||||||
|
findOne: jest.fn(),
|
||||||
|
update: jest.fn(),
|
||||||
|
remove: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
mockRepository.findAll.mockResolvedValue([{ id: TEST_UUID, name: "Test" }]);
|
||||||
|
mockRepository.findOne.mockResolvedValue({ id: TEST_UUID, name: "Test" });
|
||||||
|
mockRepository.create.mockResolvedValue({ id: TEST_UUID, name: "E2E Project" });
|
||||||
|
mockRepository.update.mockResolvedValue({ id: TEST_UUID, name: "Updated E2E" });
|
||||||
|
mockRepository.remove.mockResolvedValue({ id: TEST_UUID });
|
||||||
|
|
||||||
const moduleFixture: TestingModule = await Test.createTestingModule({
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
imports: [AppModule],
|
imports: [AppModule],
|
||||||
}).compile();
|
})
|
||||||
|
.overrideProvider(ProjectRepository)
|
||||||
|
.useValue(mockRepository)
|
||||||
|
.compile();
|
||||||
|
|
||||||
app = moduleFixture.createNestApplication();
|
app = moduleFixture.createNestApplication();
|
||||||
await app.init();
|
await app.init();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await app.close();
|
||||||
|
});
|
||||||
|
|
||||||
it("/ (GET)", () => {
|
it("/ (GET)", () => {
|
||||||
return request(app.getHttpServer()).get("/").expect(200).expect("Hello World!");
|
return request(app.getHttpServer()).get("/").expect(200).expect("Hello World!");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("/projects", () => {
|
||||||
|
it("GET /projects returns an array", () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get("/projects")
|
||||||
|
.expect(200)
|
||||||
|
.expect([{ id: TEST_UUID, name: "Test" }]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("POST /projects creates a project", () => {
|
||||||
|
const body = { name: "E2E Project", status: "active" };
|
||||||
|
return request(app.getHttpServer()).post("/projects").send(body).expect(201);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("GET /projects/:id returns a project by id", () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get(`/projects/${TEST_UUID}`)
|
||||||
|
.expect(200)
|
||||||
|
.expect({ id: TEST_UUID, name: "Test" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("PATCH /projects/:id updates a project", () => {
|
||||||
|
const body = { name: "Updated E2E" };
|
||||||
|
mockRepository.findOne.mockResolvedValue({ id: TEST_UUID, name: "Updated E2E" });
|
||||||
|
mockRepository.update.mockResolvedValue({ id: TEST_UUID, name: "Updated E2E" });
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.patch(`/projects/${TEST_UUID}`)
|
||||||
|
.send(body)
|
||||||
|
.expect(200)
|
||||||
|
.expect({ id: TEST_UUID, name: "Updated E2E" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("DELETE /projects/:id deletes a project", () => {
|
||||||
|
mockRepository.findOne.mockResolvedValue({ id: TEST_UUID });
|
||||||
|
return request(app.getHttpServer()).delete(`/projects/${TEST_UUID}`).expect(200).expect({ id: TEST_UUID });
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,12 @@
|
||||||
"testRegex": ".e2e-spec.ts$",
|
"testRegex": ".e2e-spec.ts$",
|
||||||
"transform": {
|
"transform": {
|
||||||
"^.+\\.(t|j)s$": "ts-jest"
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
}
|
},
|
||||||
|
"moduleNameMapper": {
|
||||||
|
"^@workspace/schema$": "<rootDir>/__mocks__/schema.ts",
|
||||||
|
"^valibot$": "<rootDir>/__mocks__/valibot.ts"
|
||||||
|
},
|
||||||
|
"transformIgnorePatterns": [
|
||||||
|
"node_modules/(?!@workspace)"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"incremental": false
|
||||||
|
},
|
||||||
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
apps/api/tsconfig.eslint.json
Normal file
5
apps/api/tsconfig.eslint.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"include": ["src/**/*", "test/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,10 @@
|
||||||
"extends": "@workspace/tooling/tsconfig/nestjs.json",
|
"extends": "@workspace/tooling/tsconfig/nestjs.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"types": ["jest"]
|
"types": ["jest", "node"]
|
||||||
}
|
},
|
||||||
}
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist", "test"]
|
||||||
|
}
|
||||||
106
apps/web/app/composables/useProjectApi.ts
Normal file
106
apps/web/app/composables/useProjectApi.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import type { Project } from "@workspace/schema";
|
||||||
|
|
||||||
|
interface ApiResponse<T> {
|
||||||
|
data: T | null;
|
||||||
|
error: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBaseUrl(): string {
|
||||||
|
const config = useRuntimeConfig();
|
||||||
|
return config.public.apiBaseUrl as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function request<T>(path: string, options?: RequestInit): Promise<ApiResponse<T>> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${getBaseUrl()}${path}`, {
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const body = await res.json().catch(() => null);
|
||||||
|
return { data: null, error: body?.message ?? `HTTP ${res.status}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = (await res.json()) as T;
|
||||||
|
return { data, error: null };
|
||||||
|
} catch (err) {
|
||||||
|
return { data: null, error: err instanceof Error ? err.message : "Unknown error" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useProjectApi() {
|
||||||
|
const NOT_FOUND = -1;
|
||||||
|
|
||||||
|
const projects = ref<Project[]>([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
const error = ref<string | null>(null);
|
||||||
|
|
||||||
|
async function fetchProjects(): Promise<void> {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
const { data, error: err } = await request<Project[]>("/projects");
|
||||||
|
if (data) projects.value = data;
|
||||||
|
else error.value = err;
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createProject(body: Record<string, unknown>): Promise<Project | null> {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
const { data, error: err } = await request<Project>("/projects", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
projects.value.push(data);
|
||||||
|
} else {
|
||||||
|
error.value = err;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateProject(id: string, body: Record<string, unknown>): Promise<Project | null> {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
const { data, error: err } = await request<Project>(`/projects/${id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
const idx = projects.value.findIndex(p => p.id === id);
|
||||||
|
if (idx !== NOT_FOUND) projects.value[idx] = data;
|
||||||
|
} else {
|
||||||
|
error.value = err;
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteProject(id: string): Promise<boolean> {
|
||||||
|
loading.value = true;
|
||||||
|
error.value = null;
|
||||||
|
|
||||||
|
const { error: err } = await request(`/projects/${id}`, { method: "DELETE" });
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
error.value = err;
|
||||||
|
} else {
|
||||||
|
projects.value = projects.value.filter(p => p.id !== id);
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
return !err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { projects, loading, error, fetchProjects, createProject, updateProject, deleteProject };
|
||||||
|
}
|
||||||
|
|
@ -5,5 +5,6 @@ import { baseRules, } from "@workspace/tooling/eslint/base"
|
||||||
export default withNuxt({
|
export default withNuxt({
|
||||||
rules: {
|
rules: {
|
||||||
...baseRules,
|
...baseRules,
|
||||||
|
"@stylistic/member-delimiter-style": "off",
|
||||||
},
|
},
|
||||||
},)
|
},)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,12 @@ export default defineNuxtConfig({
|
||||||
|
|
||||||
css: ["~/assets/css/main.css"],
|
css: ["~/assets/css/main.css"],
|
||||||
|
|
||||||
|
runtimeConfig: {
|
||||||
|
public: {
|
||||||
|
apiBaseUrl: process.env.NUXT_PUBLIC_API_BASE_URL ?? "http://localhost:3001",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
routeRules: {
|
routeRules: {
|
||||||
"/": { prerender: true },
|
"/": { prerender: true },
|
||||||
},
|
},
|
||||||
|
|
|
||||||
22
docker-compose.yml
Normal file
22
docker-compose.yml
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: work-hub-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${POSTGRES_PORT:-5432}:5432"
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-work_hub}
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-postgres} -d ${POSTGRES_DB:-work_hub}"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
driver: local
|
||||||
|
|
@ -1,19 +1,19 @@
|
||||||
## 1. Database & Infrastructure Setup (Mandatory First Step)
|
## 1. Database & Infrastructure Setup (Mandatory First Step)
|
||||||
|
|
||||||
- [ ] 1.1 **Dockerization**: Create a root `docker-compose.yml` (`work-hub-server/docker-compose.yml`) that declares a PostgreSQL service and named volumes for persistence. Do NOT include API or Web app containers — they run natively during development.
|
- [x] 1.1 **Dockerization**: Create a root `docker-compose.yml` (`work-hub-server/docker-compose.yml`) that declares a PostgreSQL service and named volumes for persistence. Do NOT include API or Web app containers — they run natively during development.
|
||||||
- [ ] 1.2 **Persistence**: Define and map persistent volumes in the root `docker-compose.yml` to ensure project data survives container restarts.
|
- [x] 1.2 **Persistence**: Define and map persistent volumes in the root `docker-compose.yml` to ensure project data survives container restarts.
|
||||||
- [ ] 1.3 **CLI Utility**: Implement a dedicated command/utility (e.g., `db:setup`) that orchestrates the execution of migrations and seeding, ensuring idempotency.
|
- [x] 1.3 **CLI Utility**: Implement a dedicated command/utility (e.g., `db:setup`) that orchestrates the execution of migrations and seeding, ensuring idempotency.
|
||||||
|
|
||||||
## 2. API Layer Implementation (API)
|
## 2. API Layer Implementation (API)
|
||||||
|
|
||||||
- [ ] 2.1 **Scaffolding**: Use the Nest CLI (`nest generate resource`) to create the basic resource structure for Project (Controller, Service, Module). This ensures adherence to module structure, dependency injection patterns, and common boilerplate across all APIs in the monorepo.
|
- [x] 2.1 **Scaffolding**: Use the Nest CLI (`nest generate resource`) to create the basic resource structure for Project (Controller, Service, Module). This ensures adherence to module structure, dependency injection patterns, and common boilerplate across all APIs in the monorepo.
|
||||||
- [ ] 2.2 Implement necessary request validation using **Schema-Driven Pipes** that import their structure from `@workspace/schema`, ensuring consistency between API and Web layers.
|
- [x] 2.2 Implement necessary request validation using **Schema-Driven Pipes** that import their structure from `@workspace/schema`, ensuring consistency between API and Web layers.
|
||||||
|
|
||||||
## 3. Web Layer Implementation (Web)
|
## 3. Web Layer Implementation (Web)
|
||||||
|
|
||||||
- [ ] 3.1 Update `apps/web` store/composables to call the new API endpoints and manage state correctly following successful write or delete operations.
|
- [x] 3.1 Update `apps/web` store/composables to call the new API endpoints and manage state correctly following successful write or delete operations.
|
||||||
|
|
||||||
## 4. Testing & Tooling
|
## 4. Testing & Tooling
|
||||||
|
|
||||||
- [ ] 4.1 Write comprehensive unit tests covering the Controller, the Service, and the Repository layers.
|
- [x] 4.1 Write comprehensive unit tests covering the Controller, the Service, and the Repository layers.
|
||||||
- [ ] 4.2 Update E2E test suites to verify complete CRUD workflows via HTTP calls and UI interactions, ensuring the service layer is correctly tested against a containerized DB.
|
- [x] 4.2 Update E2E test suites to verify complete CRUD workflows via HTTP calls and UI interactions, ensuring the service layer is correctly tested against a containerized DB.
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@
|
||||||
"lint": "turbo run lint",
|
"lint": "turbo run lint",
|
||||||
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
|
||||||
"format:check": "prettier --check \"**/*.{ts,tsx,md}\"",
|
"format:check": "prettier --check \"**/*.{ts,tsx,md}\"",
|
||||||
"check-types": "turbo run check-types"
|
"check-types": "turbo run check-types",
|
||||||
|
"db:setup": "pnpm --filter=@workspace/schema db:generate && pnpm --filter=@workspace/schema db:migrate && pnpm --filter=@workspace/schema db:seed"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@workspace/tooling": "workspace:*",
|
"@workspace/tooling": "workspace:*",
|
||||||
|
|
|
||||||
14
packages/schema/drizzle.config.ts
Normal file
14
packages/schema/drizzle.config.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { defineConfig } from "drizzle-kit";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
schema: "./src/drizzle/index.ts",
|
||||||
|
out: "./drizzle",
|
||||||
|
dialect: "postgresql",
|
||||||
|
dbCredentials: {
|
||||||
|
host: process.env.POSTGRES_HOST ?? "localhost",
|
||||||
|
port: Number(process.env.POSTGRES_PORT ?? 5432),
|
||||||
|
user: process.env.POSTGRES_USER ?? "postgres",
|
||||||
|
password: process.env.POSTGRES_PASSWORD ?? "postgres",
|
||||||
|
database: process.env.POSTGRES_DB ?? "work_hub",
|
||||||
|
},
|
||||||
|
});
|
||||||
26
packages/schema/drizzle/0000_yielding_photon.sql
Normal file
26
packages/schema/drizzle/0000_yielding_photon.sql
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
CREATE TABLE "projects" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"status" text DEFAULT 'active' NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "project_status_check" CHECK ("projects"."status" IN ('active', 'archived', 'completed'))
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "tasks" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"project_id" uuid NOT NULL,
|
||||||
|
"title" text NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"status" text DEFAULT 'todo' NOT NULL,
|
||||||
|
"priority" text DEFAULT 'medium' NOT NULL,
|
||||||
|
"sort_order" integer DEFAULT 0 NOT NULL,
|
||||||
|
"due_date" timestamp,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
CONSTRAINT "task_status_check" CHECK ("tasks"."status" IN ('todo', 'in_progress', 'in_review', 'done')),
|
||||||
|
CONSTRAINT "task_priority_check" CHECK ("tasks"."priority" IN ('low', 'medium', 'high', 'urgent'))
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "tasks" ADD CONSTRAINT "tasks_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
179
packages/schema/drizzle/meta/0000_snapshot.json
Normal file
179
packages/schema/drizzle/meta/0000_snapshot.json
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
{
|
||||||
|
"id": "9fa00e9c-266b-43c1-8f18-2a454e3fbf99",
|
||||||
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.projects": {
|
||||||
|
"name": "projects",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'active'"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"project_status_check": {
|
||||||
|
"name": "project_status_check",
|
||||||
|
"value": "\"projects\".\"status\" IN ('active', 'archived', 'completed')"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.tasks": {
|
||||||
|
"name": "tasks",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"project_id": {
|
||||||
|
"name": "project_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"name": "description",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"name": "status",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'todo'"
|
||||||
|
},
|
||||||
|
"priority": {
|
||||||
|
"name": "priority",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'medium'"
|
||||||
|
},
|
||||||
|
"sort_order": {
|
||||||
|
"name": "sort_order",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"due_date": {
|
||||||
|
"name": "due_date",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"tasks_project_id_projects_id_fk": {
|
||||||
|
"name": "tasks_project_id_projects_id_fk",
|
||||||
|
"tableFrom": "tasks",
|
||||||
|
"tableTo": "projects",
|
||||||
|
"columnsFrom": [
|
||||||
|
"project_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {
|
||||||
|
"task_status_check": {
|
||||||
|
"name": "task_status_check",
|
||||||
|
"value": "\"tasks\".\"status\" IN ('todo', 'in_progress', 'in_review', 'done')"
|
||||||
|
},
|
||||||
|
"task_priority_check": {
|
||||||
|
"name": "task_priority_check",
|
||||||
|
"value": "\"tasks\".\"priority\" IN ('low', 'medium', 'high', 'urgent')"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
packages/schema/drizzle/meta/_journal.json
Normal file
13
packages/schema/drizzle/meta/_journal.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"entries": [
|
||||||
|
{
|
||||||
|
"idx": 0,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1781334448956,
|
||||||
|
"tag": "0000_yielding_photon",
|
||||||
|
"breakpoints": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ export default defineConfig(
|
||||||
{ ignores: ["eslint.config.mjs", "dist"] },
|
{ ignores: ["eslint.config.mjs", "dist"] },
|
||||||
...base,
|
...base,
|
||||||
{
|
{
|
||||||
|
files: ["src/**/*.ts"],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
projectService: true,
|
projectService: true,
|
||||||
|
|
@ -14,6 +15,15 @@ export default defineConfig(
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
files: ["seed.ts"],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
projectService: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["src/**/*.ts"],
|
||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/no-floating-promises": "warn",
|
"@typescript-eslint/no-floating-promises": "warn",
|
||||||
"@typescript-eslint/no-unsafe-argument": "warn",
|
"@typescript-eslint/no-unsafe-argument": "warn",
|
||||||
|
|
|
||||||
|
|
@ -13,20 +13,29 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsup src/index.ts --format esm --dts --clean --external drizzle-orm --external drizzle-valibot --external valibot",
|
||||||
|
"dev": "tsup src/index.ts --format esm --dts --clean --external drizzle-orm --external drizzle-valibot --external valibot --watch",
|
||||||
"clean": "rm -rf dist",
|
"clean": "rm -rf dist",
|
||||||
"check-types": "tsc --noEmit",
|
"check-types": "tsc --noEmit",
|
||||||
"lint": "eslint \"src/**/*.ts\" --fix"
|
"lint": "eslint \"src/**/*.ts\" --fix",
|
||||||
|
"db:generate": "drizzle-kit generate",
|
||||||
|
"db:migrate": "drizzle-kit migrate",
|
||||||
|
"db:seed": "tsx seed.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"drizzle-orm": "^0.45.2",
|
"drizzle-orm": "^0.45.2",
|
||||||
"drizzle-valibot": "^0.4.2",
|
"drizzle-valibot": "^0.4.2",
|
||||||
|
"pg": "^8.21.0",
|
||||||
"valibot": "^1.4.1"
|
"valibot": "^1.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^25.9.3",
|
"@types/node": "^25.9.3",
|
||||||
|
"@types/pg": "^8.20.0",
|
||||||
"@workspace/tooling": "workspace:*",
|
"@workspace/tooling": "workspace:*",
|
||||||
|
"drizzle-kit": "^0.31.10",
|
||||||
"eslint": "^10.4.1",
|
"eslint": "^10.4.1",
|
||||||
|
"tsup": "^8.5.1",
|
||||||
|
"tsx": "^4.22.4",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
31
packages/schema/seed.ts
Normal file
31
packages/schema/seed.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { drizzle } from "drizzle-orm/node-postgres";
|
||||||
|
import pkg from "pg";
|
||||||
|
const { Pool } = pkg;
|
||||||
|
import { projects } from "./src/drizzle/projects";
|
||||||
|
|
||||||
|
async function seed() {
|
||||||
|
const pool = new Pool({
|
||||||
|
host: process.env.POSTGRES_HOST ?? "localhost",
|
||||||
|
port: Number(process.env.POSTGRES_PORT ?? 5432),
|
||||||
|
user: process.env.POSTGRES_USER ?? "postgres",
|
||||||
|
password: process.env.POSTGRES_PASSWORD ?? "postgres",
|
||||||
|
database: process.env.POSTGRES_DB ?? "work_hub",
|
||||||
|
});
|
||||||
|
|
||||||
|
const db = drizzle(pool);
|
||||||
|
|
||||||
|
console.log("Seeding database...");
|
||||||
|
|
||||||
|
await db.insert(projects).values([
|
||||||
|
{ name: "Sample Project", description: "A sample project for development", status: "active" },
|
||||||
|
{ name: "Archived Project", description: "An archived test project", status: "archived" },
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log("Seed complete.");
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
seed().catch((err) => {
|
||||||
|
console.error("Seed failed:", err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
export const PROJECT_STATUSES = ["active", "archived", "completed"] as const;
|
|
||||||
|
|
||||||
export const TASK_STATUSES = ["todo", "in_progress", "in_review", "done"] as const;
|
|
||||||
|
|
||||||
export const PRIORITIES = ["low", "medium", "high", "urgent"] as const;
|
|
||||||
|
|
||||||
export type ProjectStatus = (typeof PROJECT_STATUSES)[number];
|
|
||||||
export type TaskStatus = (typeof TASK_STATUSES)[number];
|
|
||||||
export type Priority = (typeof PRIORITIES)[number];
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
export { PROJECT_STATUSES, TASK_STATUSES, PRIORITIES } from "./enums";
|
|
||||||
export { projects } from "./projects";
|
|
||||||
export { tasks } from "./tasks";
|
|
||||||
export { projectsRelations, tasksRelations } from "./relations";
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
import { pgTable, uuid, text, timestamp, check } from "drizzle-orm/pg-core";
|
import { pgTable, uuid, text, timestamp, check } from "drizzle-orm/pg-core";
|
||||||
import { sql } from "drizzle-orm";
|
import { sql } from "drizzle-orm";
|
||||||
import { PROJECT_STATUSES } from "./enums";
|
import { createInsertSchema, createSelectSchema, createUpdateSchema } from "drizzle-valibot";
|
||||||
|
import { InferOutput, picklist, pipe } from "valibot";
|
||||||
|
|
||||||
|
export const PROJECT_STATUSES = ["active", "archived", "completed"] as const;
|
||||||
|
export type ProjectStatus = (typeof PROJECT_STATUSES)[number];
|
||||||
|
|
||||||
const projectStatusValues = PROJECT_STATUSES.map((s) => `'${s}'`).join(", ");
|
const projectStatusValues = PROJECT_STATUSES.map((s) => `'${s}'`).join(", ");
|
||||||
|
const createProjectStatusSchema = (schema: any) => pipe(schema, picklist([...PROJECT_STATUSES])) as any;
|
||||||
|
|
||||||
export const projects = pgTable(
|
export const projects = pgTable(
|
||||||
"projects",
|
"projects",
|
||||||
|
|
@ -16,3 +21,18 @@ export const projects = pgTable(
|
||||||
},
|
},
|
||||||
(table) => [check("project_status_check", sql`${table.status} IN (${sql.raw(projectStatusValues)})`)],
|
(table) => [check("project_status_check", sql`${table.status} IN (${sql.raw(projectStatusValues)})`)],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const projectSelect = createSelectSchema(projects, {
|
||||||
|
status: createProjectStatusSchema,
|
||||||
|
});
|
||||||
|
export type Project = InferOutput<typeof projectSelect>;
|
||||||
|
|
||||||
|
export const projectInsert = createInsertSchema(projects, {
|
||||||
|
status: createProjectStatusSchema,
|
||||||
|
});
|
||||||
|
export type ProjectInsertDTO = InferOutput<typeof projectInsert>;
|
||||||
|
|
||||||
|
export const projectUpdate = createUpdateSchema(projects, {
|
||||||
|
status: createProjectStatusSchema,
|
||||||
|
});
|
||||||
|
export type ProjectUpdateDTO = InferOutput<typeof projectUpdate>;
|
||||||
|
|
|
||||||
4
packages/schema/src/drizzle/refs.ts
Normal file
4
packages/schema/src/drizzle/refs.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
export const refs: Record<string, any> = {
|
||||||
|
projects: undefined,
|
||||||
|
tasks: undefined,
|
||||||
|
};
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
|
import { refs } from "./refs";
|
||||||
import { projects } from "./projects";
|
import { projects } from "./projects";
|
||||||
import { tasks } from "./tasks";
|
import { tasks } from "./tasks";
|
||||||
|
|
||||||
|
refs.projects = projects;
|
||||||
|
refs.tasks = tasks;
|
||||||
|
|
||||||
export const projectsRelations = relations(projects, ({ many }) => ({
|
export const projectsRelations = relations(projects, ({ many }) => ({
|
||||||
tasks: many(tasks),
|
tasks: many(tasks),
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
import { pgTable, uuid, text, integer, timestamp, check } from "drizzle-orm/pg-core";
|
import { pgTable, uuid, text, integer, timestamp, check } from "drizzle-orm/pg-core";
|
||||||
import { sql } from "drizzle-orm";
|
import { sql } from "drizzle-orm";
|
||||||
import { TASK_STATUSES, PRIORITIES } from "./enums";
|
import { createInsertSchema, createSelectSchema, createUpdateSchema } from "drizzle-valibot";
|
||||||
import { projects } from "./projects";
|
import { InferOutput, picklist, pipe } from "valibot";
|
||||||
|
import { refs } from "./refs";
|
||||||
|
|
||||||
|
export const TASK_STATUSES = ["todo", "in_progress", "in_review", "done"] as const;
|
||||||
|
export type TaskStatus = (typeof TASK_STATUSES)[number];
|
||||||
|
|
||||||
|
export const PRIORITIES = ["low", "medium", "high", "urgent"] as const;
|
||||||
|
export type Priority = (typeof PRIORITIES)[number];
|
||||||
|
|
||||||
const taskStatusValues = TASK_STATUSES.map((s) => `'${s}'`).join(", ");
|
const taskStatusValues = TASK_STATUSES.map((s) => `'${s}'`).join(", ");
|
||||||
const priorityValues = PRIORITIES.map((p) => `'${p}'`).join(", ");
|
const priorityValues = PRIORITIES.map((p) => `'${p}'`).join(", ");
|
||||||
|
|
@ -12,7 +19,8 @@ export const tasks = pgTable(
|
||||||
id: uuid("id").primaryKey().defaultRandom(),
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
projectId: uuid("project_id")
|
projectId: uuid("project_id")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => projects.id, { onDelete: "cascade" }),
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
|
.references(() => refs.projects.id, { onDelete: "cascade" }),
|
||||||
title: text("title").notNull(),
|
title: text("title").notNull(),
|
||||||
description: text("description"),
|
description: text("description"),
|
||||||
status: text("status").notNull().default("todo"),
|
status: text("status").notNull().default("todo"),
|
||||||
|
|
@ -27,3 +35,22 @@ export const tasks = pgTable(
|
||||||
check("task_priority_check", sql`${table.priority} IN (${sql.raw(priorityValues)})`),
|
check("task_priority_check", sql`${table.priority} IN (${sql.raw(priorityValues)})`),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const createTaskStatusSchema = (schema: any) => pipe(schema, picklist([...TASK_STATUSES])) as any;
|
||||||
|
const createPrioritySchema = (schema: any) => pipe(schema, picklist([...PRIORITIES])) as any;
|
||||||
|
|
||||||
|
export const taskSelect = createSelectSchema(tasks, {
|
||||||
|
status: createTaskStatusSchema,
|
||||||
|
priority: createPrioritySchema,
|
||||||
|
});
|
||||||
|
export type Task = InferOutput<typeof taskSelect>;
|
||||||
|
|
||||||
|
export const taskInsert = createInsertSchema(tasks, {
|
||||||
|
status: createTaskStatusSchema,
|
||||||
|
priority: createPrioritySchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const taskUpdate = createUpdateSchema(tasks, {
|
||||||
|
status: createTaskStatusSchema,
|
||||||
|
priority: createPrioritySchema,
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,37 @@
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { dirname, resolve } from "path";
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
export const migrationsDir = resolve(__dirname, "../drizzle");
|
||||||
|
|
||||||
|
export { eq } from "drizzle-orm";
|
||||||
|
export { drizzle, NodePgDatabase } from "drizzle-orm/node-postgres";
|
||||||
|
export { migrate } from "drizzle-orm/node-postgres/migrator";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
PROJECT_STATUSES,
|
PROJECT_STATUSES,
|
||||||
TASK_STATUSES,
|
|
||||||
PRIORITIES,
|
|
||||||
projects,
|
projects,
|
||||||
tasks,
|
projectSelect,
|
||||||
|
projectInsert,
|
||||||
|
projectUpdate,
|
||||||
|
type Project,
|
||||||
|
type ProjectStatus,
|
||||||
|
type ProjectInsertDTO,
|
||||||
|
type ProjectUpdateDTO,
|
||||||
|
} from "./drizzle/projects";
|
||||||
|
export {
|
||||||
projectsRelations,
|
projectsRelations,
|
||||||
tasksRelations,
|
tasksRelations,
|
||||||
} from "./drizzle/index";
|
} from "./drizzle/relations";
|
||||||
|
export {
|
||||||
export { projectSelect, projectInsert, projectUpdate, taskSelect, taskInsert, taskUpdate } from "./valibot";
|
TASK_STATUSES,
|
||||||
|
PRIORITIES,
|
||||||
import type { InferOutput } from "valibot";
|
tasks,
|
||||||
import { projectSelect, taskSelect } from "./valibot";
|
taskSelect,
|
||||||
|
taskInsert,
|
||||||
export type Project = InferOutput<typeof projectSelect>;
|
taskUpdate,
|
||||||
export type Task = InferOutput<typeof taskSelect>;
|
type Task,
|
||||||
|
type TaskStatus,
|
||||||
|
type Priority,
|
||||||
|
} from "./drizzle/tasks";
|
||||||
|
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
import { createInsertSchema, createSelectSchema, createUpdateSchema } from "drizzle-valibot";
|
|
||||||
import { pipe, picklist } from "valibot";
|
|
||||||
import { projects, tasks, PROJECT_STATUSES, TASK_STATUSES, PRIORITIES } from "./drizzle/index";
|
|
||||||
|
|
||||||
const createProjectStatusSchema = (schema: any) => pipe(schema, picklist([...PROJECT_STATUSES])) as any;
|
|
||||||
const createTaskStatusSchema = (schema: any) => pipe(schema, picklist([...TASK_STATUSES])) as any;
|
|
||||||
const createPrioritySchema = (schema: any) => pipe(schema, picklist([...PRIORITIES])) as any;
|
|
||||||
|
|
||||||
export const projectSelect = createSelectSchema(projects, {
|
|
||||||
status: createProjectStatusSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const taskSelect = createSelectSchema(tasks, {
|
|
||||||
status: createTaskStatusSchema,
|
|
||||||
priority: createPrioritySchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const projectInsert = createInsertSchema(projects, {
|
|
||||||
status: createProjectStatusSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const projectUpdate = createUpdateSchema(projects, {
|
|
||||||
status: createProjectStatusSchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const taskInsert = createInsertSchema(tasks, {
|
|
||||||
status: createTaskStatusSchema,
|
|
||||||
priority: createPrioritySchema,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const taskUpdate = createUpdateSchema(tasks, {
|
|
||||||
status: createTaskStatusSchema,
|
|
||||||
priority: createPrioritySchema,
|
|
||||||
});
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
"extends": "@workspace/tooling/tsconfig/library.json",
|
"extends": "@workspace/tooling/tsconfig/library.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src"
|
"rootDir": "./src",
|
||||||
|
"ignoreDeprecations": "6.0"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ export function createNestjsConfig(tsconfigRootDir, overrides = {}) {
|
||||||
rules: {
|
rules: {
|
||||||
"class-methods-use-this": "off",
|
"class-methods-use-this": "off",
|
||||||
"@typescript-eslint/no-floating-promises": "warn",
|
"@typescript-eslint/no-floating-promises": "warn",
|
||||||
"@typescript-eslint/no-unsafe-argument": "warn",
|
// "@typescript-eslint/no-unsafe-argument": "warn",
|
||||||
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
||||||
...overrides,
|
...overrides,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@stylistic/eslint-plugin": "^5.10.0",
|
"@stylistic/eslint-plugin": "^5.10.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
|
||||||
"eslint-plugin-prettier": "^5.5.6",
|
"eslint-plugin-prettier": "^5.5.6",
|
||||||
"globals": "^17.6.0",
|
"globals": "^17.6.0",
|
||||||
"typescript-eslint": "^8.61.0"
|
"typescript-eslint": "^8.61.0"
|
||||||
|
|
|
||||||
843
pnpm-lock.yaml
843
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -15,7 +15,8 @@
|
||||||
},
|
},
|
||||||
"dev": {
|
"dev": {
|
||||||
"cache": false,
|
"cache": false,
|
||||||
"persistent": true
|
"persistent": true,
|
||||||
|
"dependsOn": ["^build"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue