Compare commits

..

2 commits

Author SHA1 Message Date
Nicolas HOARAU 9314e72f3e feat(schema): add shared schema package with Drizzle ORM and Valibot validation
Create @workspace/schema package with Project and Task table definitions, Drizzle relations, Valibot validation schemas (select/insert/update), and wire as workspace dependency in API and Web apps.
2026-06-04 12:46:51 +04:00
Nicolas HOARAU d1dbf3fe77 docs(schema): Documentation of first change
Creation of specs for openspec and opencode to start working
2026-06-04 10:58:36 +04:00
31 changed files with 11990 additions and 58 deletions

View file

@ -24,6 +24,7 @@
"@nestjs/common": "^11.0.1",
"@nestjs/core": "^11.0.1",
"@nestjs/platform-express": "^11.0.1",
"@workspace/schema": "workspace:*",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
},

View file

@ -1,8 +1,8 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Test, TestingModule } from "@nestjs/testing";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
describe('AppController', () => {
describe("AppController", () => {
let appController: AppController;
beforeEach(async () => {
@ -14,9 +14,9 @@ describe('AppController', () => {
appController = app.get<AppController>(AppController);
});
describe('root', () => {
describe("root", () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!');
expect(appController.getHello()).toBe("Hello World!");
});
});
});

View file

@ -1,5 +1,5 @@
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
import { Controller, Get } from "@nestjs/common";
import { AppService } from "./app.service";
@Controller()
export class AppController {

View file

@ -1,6 +1,6 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
@Module({
imports: [],

View file

@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { Injectable } from "@nestjs/common";
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
return "Hello World!";
}
}

View file

@ -1,5 +1,5 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";
async function bootstrap() {
const app = await NestFactory.create(AppModule);

View file

@ -1,10 +1,10 @@
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import request from 'supertest';
import { App } from 'supertest/types';
import { AppModule } from './../src/app.module';
import { Test, TestingModule } from "@nestjs/testing";
import { INestApplication } from "@nestjs/common";
import request from "supertest";
import { App } from "supertest/types";
import { AppModule } from "./../src/app.module";
describe('AppController (e2e)', () => {
describe("AppController (e2e)", () => {
let app: INestApplication<App>;
beforeEach(async () => {
@ -16,10 +16,10 @@ describe('AppController (e2e)', () => {
await app.init();
});
it('/ (GET)', () => {
it("/ (GET)", () => {
return request(app.getHttpServer())
.get('/')
.get("/")
.expect(200)
.expect('Hello World!');
.expect("Hello World!");
});
});

View file

@ -14,6 +14,7 @@
"@iconify-json/lucide": "^1.2.108",
"@iconify-json/simple-icons": "^1.2.83",
"@nuxt/a11y": "1.0.0-alpha.1",
"@workspace/schema": "workspace:*",
"@nuxt/hints": "1.1.2",
"@nuxt/image": "2.0.0",
"@nuxt/test-utils": "4.0.3",

View file

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-03

View file

@ -0,0 +1,47 @@
## Context
The monorepo currently has three workspace roots: `apps/api` (NestJS), `apps/web` (Nuxt), and `packages/schema` (empty placeholder). The API and web apps share no type definitions, validation logic, or database schema — each would need to duplicate these. The `packages/schema` directory exists but has no `package.json`, no `tsconfig.json`, and no source files.
The chosen stack is:
- **Drizzle ORM** (PostgreSQL) for database table definitions, relations, and migrations
- **Superstruct** for runtime schema validation (request payloads, configs, etc.)
- **TypeScript** types derived from both Drizzle and Superstruct schemas
## Goals / Non-Goals
**Goals:**
- Create a working `@workspace/schema` package with proper `package.json`, `tsconfig.json`, and build pipeline
- Define Drizzle table schemas, relations, and enums targeting PostgreSQL
- Define Superstruct validators for runtime data validation
- Export derived TypeScript types that both `api` and `web` can consume
- Wire the package into the Turborepo build graph so it builds before its consumers
**Non-Goals:**
- No migrations or database connections — this package only provides schema definitions
- No API endpoints or web UI — this is purely a data layer package
- No initial table definitions — the package structure and conventions will be set up; actual tables come in follow-up changes
## Decisions
1. **Package name `@workspace/schema`** — follows the existing `packages/` directory pattern; scoped to avoid conflicts with npm; private: true in package.json.
2. **Entry points split by concern** — three source entry points (or one barrel export):
- `src/drizzle/` — Drizzle table definitions (`pgTable`, relations, enums)
- `src/validation/` — Superstruct schemas (`object()`, `string()`, etc.)
- `src/types/` — Re-exported TypeScript types (inferred from Drizzle and Superstruct)
Single `src/index.ts` barrel re-exports everything. The `exports` field in `package.json` may expose sub-paths if the package grows large.
3. **Drizzle schema is the source of truth for DB structure** — Superstruct validators mirror the same shapes but can be more permissive (e.g., optional fields for partial updates). TypeScript types are inferred from both.
4. **`tsconfig.json` extends the root** — no root tsconfig exists yet, so `packages/schema` gets its own with `module: nodenext`, `target: ES2023`, strict mode, and declaration output.
5. **Build via `tsc`** — simple `tsc` build; no bundler needed since it's consumed internally by the monorepo.
## Risks / Trade-offs
- **[Duplication risk]** Superstruct schemas and Drizzle definitions could drift apart. Mitigation: Superstruct validators should reference Drizzle-derived types where possible, and tests should assert consistency.
- **[Build order]** Turborepo's `^build` dependsOn already handles topological order, so no config changes needed — just ensure api/web list `@workspace/schema` in dependencies.

View file

@ -0,0 +1,31 @@
## Why
The monorepo needs a single source of truth for business schema (database tables, validation rules, and shared types) that both the API and web apps can import. Currently `packages/schema` is an empty placeholder with no package.json or source files, forcing each app to duplicate type definitions and validation logic.
## What Changes
- Populate `packages/schema` with a private npm package (`@workspace/schema`) including `package.json`, `tsconfig.json`, and source entry points
- Add Drizzle ORM schema definitions for PostgreSQL (tables, relations, enums)
- Add Superstruct-based runtime validators for request/response shapes
- Export shared TypeScript types derived from both Drizzle and Superstruct definitions
- Add `packages/schema` as a workspace dependency in both `apps/api` and `apps/web`
- Update Turborepo pipeline so `schema` build runs before dependent apps
## Capabilities
### New Capabilities
- `database-schema`: Drizzle ORM table definitions, relations, and enums targeting PostgreSQL
- `validation`: Superstruct schema definitions for runtime validation of data shapes
- `shared-types`: TypeScript type exports re-exported from Drizzle and Superstruct schemas
### Modified Capabilities
None — this is the first real content in `packages/schema`.
## Impact
- `packages/schema` will get a `package.json` and build output; its contents become importable by other workspace packages
- `apps/api` and `apps/web` gain a new workspace dependency
- `turbo.json` needs no changes — `^build` already handles topological build order
- No existing code is removed or modified; this is purely additive

View file

@ -0,0 +1,42 @@
## ADDED Requirements
### Requirement: Define PostgreSQL tables with Drizzle ORM
The package SHALL export Drizzle ORM table definitions using `pgTable` from `drizzle-orm/pg-core`, with proper column types, constraints, and defaults targeting PostgreSQL.
#### Scenario: Table exports are available
- **WHEN** another package imports from `@workspace/schema`
- **THEN** all Drizzle table definitions SHALL be accessible as named exports
#### Scenario: Table uses standard column types
- **WHEN** a column is defined in a Drizzle table
- **THEN** it SHALL use a `drizzle-orm/pg-core` column type (e.g., `text()`, `integer()`, `timestamp()`, `uuid()`, `boolean()`)
### Requirement: Define PostgreSQL enums
The package SHALL support PostgreSQL enum definitions using `pgEnum` from `drizzle-orm/pg-core` for constrained string fields.
#### Scenario: Enum is defined and exportable
- **WHEN** an enum is created with `pgEnum`
- **THEN** it SHALL be exportable and usable as a column type in table definitions
### Requirement: Define table relations
The package SHALL define Drizzle relations (`relations` from `drizzle-orm`) between tables, supporting one-to-many, many-to-one, and many-to-many relationships.
#### Scenario: Relation links two tables
- **WHEN** a relation is defined between two tables
- **THEN** the related fields SHALL be queryable via Drizzle's relational query API
### Requirement: All schemas are co-located
The package SHALL colocate Drizzle table definitions, enums, and relations within the same directory structure under `src/drizzle/`.
#### Scenario: Single import for database concerns
- **WHEN** a consumer needs database schema
- **THEN** it SHALL import from `@workspace/schema` without accessing internal paths

View file

@ -0,0 +1,33 @@
## ADDED Requirements
### Requirement: Export TypeScript types inferred from Drizzle schemas
The package SHALL export TypeScript types inferred from Drizzle ORM table definitions using `InferSelectModel` and `InferInsertModel` from `drizzle-orm`, providing both row and insert shapes.
#### Scenario: Select type is available
- **WHEN** a table is defined in Drizzle
- **THEN** an `InferSelectModel` type SHALL be exported for that table
#### Scenario: Insert type is available
- **WHEN** a table is defined in Drizzle
- **THEN** an `InferInsertModel` type SHALL be exported for that table
### Requirement: Export TypeScript types inferred from Superstruct schemas
The package SHALL export TypeScript types inferred from Superstruct schemas using `Infer` from `superstruct`, so that runtime validation and compile-time types stay in sync.
#### Scenario: Infer type is available
- **WHEN** a Superstruct schema is defined
- **THEN** an `Infer` type SHALL be exported for that schema
### Requirement: Type re-exports via barrel
All TypeScript types SHALL be re-exported from a single barrel entry point (`src/index.ts`) so consumers import from `@workspace/schema` without deep path references.
#### Scenario: Single import for all types
- **WHEN** a consumer imports from `@workspace/schema`
- **THEN** all type exports SHALL be available from that single entry point

View file

@ -0,0 +1,38 @@
## ADDED Requirements
### Requirement: Define Superstruct validators for data shapes
The package SHALL export Superstruct schema definitions for validating runtime data shapes (request payloads, configuration objects, etc.).
#### Scenario: Validator validates correct data
- **WHEN** data matching the schema is passed to the validator
- **THEN** the validator SHALL return the validated data without throwing
#### Scenario: Validator rejects invalid data
- **WHEN** data not matching the schema is passed to the validator
- **THEN** the validator SHALL throw a `StructError` with details about the validation failure
### Requirement: Validator composability
Superstruct schemas SHALL be composable — schemas can reference other schemas for nested object validation.
#### Scenario: Nested object validation
- **WHEN** a schema defines a nested object field using another Superstruct schema
- **THEN** the nested schema SHALL be applied during validation of the parent
### Requirement: Optional and default fields
Superstruct schemas SHALL support optional fields (`optional()`) and fields with default values (`defaulted()`) for partial updates or configuration defaults.
#### Scenario: Optional field omitted
- **WHEN** an optional field is omitted from input data
- **THEN** the validator SHALL accept the data and not require the field
#### Scenario: Default value applied
- **WHEN** a field with a default value is omitted from input data
- **THEN** the validator SHALL apply the default value in the returned data

View file

@ -0,0 +1,38 @@
## 1. Package scaffolding
- [x] 1.1 Create `packages/schema/package.json` with name `@workspace/schema`, private: true, and TypeScript build script
- [x] 1.2 Create `packages/schema/tsconfig.json` extending the API's config pattern (nodenext, ES2023, strictNullChecks, declaration)
- [x] 1.3 Add `drizzle-orm`, `superstruct`, and `@types/node` as dependencies in the new package
- [x] 1.4 Create `packages/schema/src/index.ts` barrel file re-exporting from sub-modules
- [x] 1.5 Add `@workspace/schema` as a workspace dependency in `apps/api/package.json`
- [x] 1.6 Add `@workspace/schema` as a workspace dependency in `apps/web/package.json`
- [x] 1.7 Verify `pnpm install` resolves the workspace dependency and `pnpm build` compiles the package
## 2. Drizzle schema definitions
- [x] 2.1 Create `packages/schema/src/drizzle/` directory structure
- [x] 2.2 Define `pgEnum` exports for any PostgreSQL enums needed
- [x] 2.3 Define initial Drizzle tables with `pgTable`, proper column types, constraints, and defaults
- [x] 2.4 Define `relations()` between tables for relational queries
- [x] 2.5 Export all table definitions, enums, and relations from the Drizzle sub-module barrel
## 3. Superstruct validation schemas
- [x] 3.1 Create `packages/schema/src/validation/` directory structure
- [x] 3.2 Define Superstruct schemas for data shapes, with proper type constraints
- [x] 3.3 Support nested/optional/defaulted fields via Superstruct combinators
- [x] 3.4 Export all validation schemas from the validation sub-module barrel
## 4. Shared TypeScript types
- [x] 4.1 Create `packages/schema/src/types/` directory structure
- [x] 4.2 Export `InferSelectModel` and `InferInsertModel` types derived from all Drizzle tables
- [x] 4.3 Export `Infer` types derived from all Superstruct schemas
- [x] 4.4 Re-export all types through the top-level `src/index.ts` barrel
## 5. Wire into monorepo
- [x] 5.1 Verify Turborepo topological build order — `turbo.json` `^build` should already handle `schema``api`/`web`
- [x] 5.2 Run `pnpm build` from root to confirm clean compilation
- [x] 5.3 Run `pnpm --filter=api test` to confirm no regressions
- [x] 5.4 Run `pnpm check-types` to confirm type-checking passes across all workspaces

View file

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-06-04

View file

@ -0,0 +1,37 @@
## Context
The schema package currently has two manually maintained modules (`validation/` and `types/`) that mirror Drizzle table definitions. Drizzle ORM has built-in Valibot support (`drizzle-orm/valibot`) that can derive insert, update, and select schemas directly from `pgTable` definitions — eliminating the need for hand-written validators and separate type files.
The constant arrays in `drizzle/enums.ts` (`PROJECT_STATUSES`, `TASK_STATUSES`, `PRIORITIES`) already define the allowed values. These can feed into Valibot `picklist()` refinements via the customization callback in `createInsertSchema`/`createUpdateSchema`.
## Goals / Non-Goals
**Goals:**
- Replace Superstruct with Valibot as the validation library
- Derive insert/update schemas from Drizzle tables using `drizzle-orm/valibot`
- Remove the `validation/` and `types/` source directories
- Remove `superstruct` dependency; add `valibot`
- Keep DB-level CHECK constraints — Valibot refinements layer on top
- Ensure `createUpdateSchema` produces all-optional shapes (replacing manual partial logic)
**Non-Goals:**
- No change to `drizzle/` module structure or table definitions
- No change to the public import paths consumers use
- No migration or database changes
## Decisions
1. **Use `drizzle-orm/valibot` not `drizzle-valibot` package** — the integration is built into `drizzle-orm` itself (since v1.0.0-beta.15). No separate package needed.
2. **One composer file for all Valibot schemas** — a single `src/valibot.ts` (or inline in `drizzle/`) containing `createInsertSchema` and `createUpdateSchema` calls for each table. No separate `validation/` directory.
3. **Customization via the second argument callback** — enum-constrained fields use `(schema) => pipe(schema, picklist([...ARRAY]))` to add the allowed-value constraint on top of Drizzle's auto-generated type info.
4. **Types are inferred, not declared**`v.InferOutput<typeof projectInsert>` replaces the manual type aliases in `types/`. Consumers import the Valibot schema and derive types as needed.
## Risks / Trade-offs
- **[New dependency]** Valibot replaces Superstruct. The API surface is similar but not identical — consumers may need to adjust error handling (Valibot uses `ValiError` not `StructError`).
- **[Picklist sync]** The `picklist()` values reference the constant arrays, so they stay in sync automatically. No new drift risk.

View file

@ -0,0 +1,32 @@
## Why
The schema package currently duplicates validation logic: Superstruct schemas mirror Drizzle table definitions by hand. This is error-prone (they can drift) and requires maintaining a separate `validation/` and `types/` module. Drizzle provides first-class Valibot integration (`drizzle-orm/valibot`) that derives insert/update/select schemas directly from table definitions, eliminating duplication entirely. Valibot is also tree-shakeable, which benefits the web app's bundle.
## What Changes
- **BREAKING**: Replace Superstruct with Valibot across `@workspace/schema`
- Remove `packages/schema/src/validation/` module (hand-written Superstruct schemas)
- Remove `packages/schema/src/types/` module (manual type re-exports no longer needed)
- Add Valibot-derived schemas via `createInsertSchema` and `createUpdateSchema` from `drizzle-orm/valibot`
- Add `valibot` dependency; remove `superstruct` dependency
- Update main barrel export in `src/index.ts`
- Validation schemas for enum fields (`status`, `priority`) will use `picklist()` refinements derived from the existing constant arrays in `drizzle/enums.ts`
## Capabilities
### New Capabilities
None — this is a modification of existing capabilities.
### Modified Capabilities
- `validation`: Hand-written Superstruct schemas → auto-derived Valibot schemas from Drizzle tables
- `shared-types`: Manually maintained Infer types → inferred automatically from Valibot schemas via `v.InferOutput`
## Impact
- `packages/schema/package.json` — replace `superstruct` with `valibot` in dependencies
- `packages/schema/src/validation/` — entire directory removed
- `packages/schema/src/types/` — entire directory removed
- `packages/schema/src/index.ts` — exports change to drizzle tables + valibot schemas only
- `apps/api` and `apps/web` import from `@workspace/schema` — no import path changes, but consumers may need to adapt to new API if they used Superstruct directly

View file

@ -0,0 +1,41 @@
## MODIFIED Requirements
### Requirement: Export TypeScript types inferred from Drizzle schemas
The package SHALL export TypeScript types inferred from Drizzle ORM table definitions. These SHALL be derived from Valibot schemas via `v.InferOutput<typeof schema>` rather than manual `InferSelectModel`/`InferInsertModel` aliases.
#### Scenario: Select type is available via Valibot
- **WHEN** a table is defined in Drizzle and a `createSelectSchema` is created
- **THEN** the row type SHALL be accessible as `v.InferOutput<typeof selectSchema>`
#### Scenario: Insert type is available via Valibot
- **WHEN** an insert schema is created via `createInsertSchema`
- **THEN** the insert type SHALL be accessible as `v.InferOutput<typeof insertSchema>`
### Requirement: Export TypeScript types inferred from Valibot schemas
The package SHALL export TypeScript types inferred from Valibot schemas using `v.InferOutput`, so that runtime validation and compile-time types stay in sync.
#### Scenario: Infer type is available
- **WHEN** a Valibot schema is defined
- **THEN** the corresponding `v.InferOutput` type SHALL be accessible
### Requirement: Type re-exports via barrel
All Valibot schemas SHALL be exported from the package's main entry point (`src/index.ts`) so consumers import from `@workspace/schema` without deep path references. Types are accessible via `v.InferOutput<typeof schema>` directly from the exported schema.
#### Scenario: Single import for all schemas
- **WHEN** a consumer imports from `@workspace/schema`
- **THEN** all Drizzle tables and Valibot schemas SHALL be available from that single entry point
- **THEN** TypeScript types SHALL NOT require a separate type-only import — consumers use `v.InferOutput<typeof importedSchema>`
## REMOVED Requirements
### Requirement: Export TypeScript types inferred from Superstruct schemas
**Reason**: Superstruct removed in favor of Valibot. Types are now inferred via `v.InferOutput` instead of Superstruct's `Infer`.
**Migration**: Replace `import type { ProjectCreate } from "@workspace/schema"` with `import { projectInsert } from "@workspace/schema"; type ProjectCreate = v.InferOutput<typeof projectInsert>`

View file

@ -0,0 +1,47 @@
## MODIFIED Requirements
### Requirement: Define Valibot validators derived from Drizzle tables
The package SHALL export Valibot schemas derived from Drizzle table definitions using `createInsertSchema` and `createUpdateSchema` from `drizzle-orm/valibot`, replacing the previous hand-written Superstruct schemas.
#### Scenario: Insert schema available for each table
- **WHEN** a table is defined in Drizzle
- **THEN** a `createInsertSchema` SHALL be exported for that table, with auto-generated defaults, nullability, and column type validation
#### Scenario: Update schema available for each table
- **WHEN** a table is defined in Drizzle
- **THEN** a `createUpdateSchema` SHALL be exported for that table, with all non-generated fields marked optional
#### Scenario: Enum fields validated via picklist
- **WHEN** a `text()` column has a CHECK constraint restricting allowed values
- **THEN** the corresponding Valibot schema SHALL use `picklist()` refinement derived from the matching constant array in `drizzle/enums.ts`
#### Scenario: Validator validates correct data
- **WHEN** data matching the schema is passed to `v.parse()`
- **THEN** the validator SHALL return the validated data without throwing
#### Scenario: Validator rejects invalid data
- **WHEN** data not matching the schema is passed to `v.parse()`
- **THEN** the validator SHALL throw a `ValiError` with details about the validation failure
## REMOVED Requirements
### Requirement: Define Superstruct validators for data shapes
**Reason**: Replaced by Valibot schemas auto-derived from Drizzle tables via `drizzle-orm/valibot`
**Migration**: Replace `import { ProjectCreate } from "@workspace/schema"` with `import { projectInsert } from "@workspace/schema"` or use the Valibot equivalent
### Requirement: Validator composability
**Reason**: Redundant — Valibot schemas derived from Drizzle already handle nested types through table relations
**Migration**: No migration needed; Valibot schemas are flat by design (one schema per table)
### Requirement: Optional and default fields
**Reason**: Handled natively by `createInsertSchema` (omits generated columns) and `createUpdateSchema` (makes all fields optional). Superstruct's `optional()` and `defaulted()` combinators no longer needed.
**Migration**: Use `createUpdateSchema` for partial-update shapes instead of manually wrapping with `optional()`

View file

@ -0,0 +1,25 @@
## 1. Dependency swap
- [x] 1.1 Remove `superstruct` from `packages/schema/package.json` dependencies
- [x] 1.2 Add `valibot` to `packages/schema/package.json` dependencies
## 2. Create Valibot schemas
- [x] 2.1 Create `packages/schema/src/valibot.ts` with `createInsertSchema` and `createUpdateSchema` for `projects` and `tasks`
- [x] 2.2 Add `picklist()` refinements for enum fields (`status`, `priority`) using the constant arrays from `drizzle/enums.ts`
- [x] 2.3 Export insert and update schemas as named exports
## 3. Remove old modules
- [x] 3.1 Delete `packages/schema/src/validation/` directory
- [x] 3.2 Delete `packages/schema/src/types/` directory
## 4. Update barrel export
- [x] 4.1 Update `packages/schema/src/index.ts` to export Drizzle tables and Valibot schemas (remove old validation/types exports)
## 5. Verify
- [x] 5.1 Run `pnpm --filter=@workspace/schema build` to confirm compilation
- [x] 5.2 Run `pnpm build` from root to confirm full monorepo integrity
- [x] 5.3 Run `pnpm --filter=api test` to confirm no regressions

View file

@ -0,0 +1,28 @@
{
"name": "@workspace/schema",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
},
"scripts": {
"build": "tsc",
"clean": "rm -rf dist"
},
"dependencies": {
"drizzle-orm": "^0.43.1",
"drizzle-valibot": "^0.4.2",
"valibot": "^1.0.0"
},
"devDependencies": {
"@types/node": "^22.14.1",
"typescript": "^5.7.3"
}
}

View file

@ -0,0 +1,14 @@
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];

View file

@ -0,0 +1,4 @@
export { PROJECT_STATUSES, TASK_STATUSES, PRIORITIES } from "./enums";
export { projects } from "./projects";
export { tasks } from "./tasks";
export { projectsRelations, tasksRelations } from "./relations";

View file

@ -0,0 +1,27 @@
import { pgTable, uuid, text, timestamp, check } from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
import { PROJECT_STATUSES } from "./enums";
const projectStatusValues = PROJECT_STATUSES.map((s) => `'${s}'`).join(", ");
export const projects = pgTable(
"projects",
{
id: uuid("id").primaryKey().defaultRandom(),
name: text("name").notNull(),
description: text("description"),
status: text("status").notNull().default("active"),
createdAt: timestamp("created_at", { mode: "string" })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { mode: "string" })
.notNull()
.defaultNow(),
},
(table) => [
check(
"project_status_check",
sql`${table.status} IN (${sql.raw(projectStatusValues)})`,
),
],
);

View file

@ -0,0 +1,14 @@
import { relations } from "drizzle-orm";
import { projects } from "./projects";
import { tasks } from "./tasks";
export const projectsRelations = relations(projects, ({ many }) => ({
tasks: many(tasks),
}));
export const tasksRelations = relations(tasks, ({ one }) => ({
project: one(projects, {
fields: [tasks.projectId],
references: [projects.id],
}),
}));

View file

@ -0,0 +1,46 @@
import {
pgTable,
uuid,
text,
integer,
timestamp,
check,
} from "drizzle-orm/pg-core";
import { sql } from "drizzle-orm";
import { TASK_STATUSES, PRIORITIES } from "./enums";
import { projects } from "./projects";
const taskStatusValues = TASK_STATUSES.map((s) => `'${s}'`).join(", ");
const priorityValues = PRIORITIES.map((p) => `'${p}'`).join(", ");
export const tasks = pgTable(
"tasks",
{
id: uuid("id").primaryKey().defaultRandom(),
projectId: uuid("project_id")
.notNull()
.references(() => projects.id, { onDelete: "cascade" }),
title: text("title").notNull(),
description: text("description"),
status: text("status").notNull().default("todo"),
priority: text("priority").notNull().default("medium"),
sortOrder: integer("sort_order").notNull().default(0),
dueDate: timestamp("due_date", { mode: "string" }),
createdAt: timestamp("created_at", { mode: "string" })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { mode: "string" })
.notNull()
.defaultNow(),
},
(table) => [
check(
"task_status_check",
sql`${table.status} IN (${sql.raw(taskStatusValues)})`,
),
check(
"task_priority_check",
sql`${table.priority} IN (${sql.raw(priorityValues)})`,
),
],
);

View file

@ -0,0 +1,24 @@
export {
PROJECT_STATUSES,
TASK_STATUSES,
PRIORITIES,
projects,
tasks,
projectsRelations,
tasksRelations,
} from "./drizzle/index";
export {
projectSelect,
projectInsert,
projectUpdate,
taskSelect,
taskInsert,
taskUpdate,
} from "./valibot";
import type { InferOutput } from "valibot";
import { projectSelect, taskSelect } from "./valibot";
export type Project = InferOutput<typeof projectSelect>;
export type Task = InferOutput<typeof taskSelect>;

View file

@ -0,0 +1,40 @@
import {
createInsertSchema,
createSelectSchema,
createUpdateSchema,
} from "drizzle-valibot";
import { pipe, picklist } from "valibot";
import {
projects,
tasks,
PROJECT_STATUSES,
TASK_STATUSES,
PRIORITIES,
} from "./drizzle/index";
export const projectSelect = createSelectSchema(projects, {
status: (schema: any) => pipe(schema, picklist([...PROJECT_STATUSES])) as any,
});
export const taskSelect = createSelectSchema(tasks, {
status: (schema: any) => pipe(schema, picklist([...TASK_STATUSES])) as any,
priority: (schema: any) => pipe(schema, picklist([...PRIORITIES])) as any,
});
export const projectInsert = createInsertSchema(projects, {
status: (schema: any) => pipe(schema, picklist([...PROJECT_STATUSES])) as any,
});
export const projectUpdate = createUpdateSchema(projects, {
status: (schema: any) => pipe(schema, picklist([...PROJECT_STATUSES])) as any,
});
export const taskInsert = createInsertSchema(tasks, {
status: (schema: any) => pipe(schema, picklist([...TASK_STATUSES])) as any,
priority: (schema: any) => pipe(schema, picklist([...PRIORITIES])) as any,
});
export const taskUpdate = createUpdateSchema(tasks, {
status: (schema: any) => pipe(schema, picklist([...TASK_STATUSES])) as any,
priority: (schema: any) => pipe(schema, picklist([...PRIORITIES])) as any,
});

View file

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"target": "ES2023",
"lib": ["ES2023"],
"strict": true,
"strictNullChecks": true,
"esModuleInterop": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

File diff suppressed because it is too large Load diff