Compare commits
2 commits
0e46bf8f97
...
9314e72f3e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9314e72f3e | ||
|
|
d1dbf3fe77 |
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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!");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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: [],
|
||||
|
|
|
|||
|
|
@ -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!";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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!");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-06-03
|
||||
47
openspec/changes/add-shared-schema-package/design.md
Normal file
47
openspec/changes/add-shared-schema-package/design.md
Normal 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.
|
||||
31
openspec/changes/add-shared-schema-package/proposal.md
Normal file
31
openspec/changes/add-shared-schema-package/proposal.md
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
38
openspec/changes/add-shared-schema-package/tasks.md
Normal file
38
openspec/changes/add-shared-schema-package/tasks.md
Normal 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
|
||||
2
openspec/changes/swap-superstruct-valibot/.openspec.yaml
Normal file
2
openspec/changes/swap-superstruct-valibot/.openspec.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-06-04
|
||||
37
openspec/changes/swap-superstruct-valibot/design.md
Normal file
37
openspec/changes/swap-superstruct-valibot/design.md
Normal 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.
|
||||
32
openspec/changes/swap-superstruct-valibot/proposal.md
Normal file
32
openspec/changes/swap-superstruct-valibot/proposal.md
Normal 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
|
||||
|
|
@ -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>`
|
||||
|
|
@ -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()`
|
||||
25
openspec/changes/swap-superstruct-valibot/tasks.md
Normal file
25
openspec/changes/swap-superstruct-valibot/tasks.md
Normal 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
|
||||
28
packages/schema/package.json
Normal file
28
packages/schema/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
14
packages/schema/src/drizzle/enums.ts
Normal file
14
packages/schema/src/drizzle/enums.ts
Normal 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];
|
||||
4
packages/schema/src/drizzle/index.ts
Normal file
4
packages/schema/src/drizzle/index.ts
Normal 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";
|
||||
27
packages/schema/src/drizzle/projects.ts
Normal file
27
packages/schema/src/drizzle/projects.ts
Normal 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)})`,
|
||||
),
|
||||
],
|
||||
);
|
||||
14
packages/schema/src/drizzle/relations.ts
Normal file
14
packages/schema/src/drizzle/relations.ts
Normal 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],
|
||||
}),
|
||||
}));
|
||||
46
packages/schema/src/drizzle/tasks.ts
Normal file
46
packages/schema/src/drizzle/tasks.ts
Normal 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)})`,
|
||||
),
|
||||
],
|
||||
);
|
||||
24
packages/schema/src/index.ts
Normal file
24
packages/schema/src/index.ts
Normal 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>;
|
||||
40
packages/schema/src/valibot.ts
Normal file
40
packages/schema/src/valibot.ts
Normal 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,
|
||||
});
|
||||
21
packages/schema/tsconfig.json
Normal file
21
packages/schema/tsconfig.json
Normal 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"]
|
||||
}
|
||||
11365
pnpm-lock.yaml
11365
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue