Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,29 @@ jobs:
needs: guard
if: needs.guard.outputs.run == 'true'
runs-on: ubuntu-latest

# A **fresh** database on every run. The point is not convenience: a suite
# that runs against a database somebody already migrated proves the queries
# and nothing about the schema, so the migrations are never really tested.
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_PASSWORD: wikifake
POSTGRES_DB: wikifake
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 10

env:
# `@wikifake/db` refuses to skip its integration tests when CI is set, so
# this variable missing turns the suite red rather than quiet.
DATABASE_URL: postgres://postgres:wikifake@localhost:5432/wikifake

steps:
- uses: actions/checkout@v4

Expand All @@ -110,6 +133,12 @@ jobs:
restore-keys: pnpm-

- run: pnpm install --frozen-lockfile

# The CLI, on a database that has never been migrated: the exit gate of
# phase 2 asks for exactly that, and a programmatic migrator passing is
# not the same proof.
- run: pnpm --filter @wikifake/db migrate

- run: pnpm typecheck
- run: pnpm lint
- run: pnpm test
Expand Down
5 changes: 5 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ dist/
# Documentation has its own formatting rules (plans/method/).
plans/
*.md

# Drizzle owns these: it writes the SQL and reads its own snapshots back to
# compute the next diff. Reformatting a snapshot is how a migration chain starts
# disagreeing with the database it describes.
packages/db/migrations
1 change: 1 addition & 0 deletions packages/config/src/workspace-graph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const EXPECTED: Readonly<Record<string, readonly string[]>> = {
env: ['zod'],
protocol: ['zod'],
domain: ['@wikifake/protocol'],
db: ['@wikifake/env', 'drizzle-orm', 'postgres'],
};

describe('workspace dependency graph', () => {
Expand Down
17 changes: 13 additions & 4 deletions packages/config/vitest.base.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { defineConfig } from 'vitest/config';

// Shared baseline: every package re-exports it. Coverage is measured but has
// no blocking threshold — a threshold on a repository that is just starting
// measures nothing.
export const baseConfig = defineConfig({
//
// Deliberately a plain object rather than `defineConfig(...)`. Wrapping it puts
// a *type* from Vitest on an export that crosses package boundaries, and pnpm is
// free to give two packages two instances of Vitest — which it did the moment
// `drizzle-kit` pulled in `tsx`, since `tsx` is a peer of Vite. The two
// instances then disagree about `UserConfigExport` and every package that
// re-exports this stops typechecking, for a reason that has nothing to do with
// its own code.
//
// Vitest reads a plain object exactly the same way, and a typo here fails
// loudly: no test is found.
export const baseConfig = {
test: {
include: ['src/**/*.test.ts'],
coverage: {
Expand All @@ -14,4 +23,4 @@ export const baseConfig = defineConfig({
exclude: ['src/**/*.test.ts'],
},
},
});
};
21 changes: 21 additions & 0 deletions packages/db/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { defineConfig } from 'drizzle-kit';

// `DATABASE_URL` is read straight from the environment here rather than through
// `@wikifake/env`: `drizzle-kit` is a CLI run by hand and by CI, and it should
// say which variable is missing rather than fail a schema validation that also
// wants a Redis URL and a model key.
const url = process.env['DATABASE_URL'];
if (url === undefined || url === '') {
throw new Error('DATABASE_URL is not set — drizzle-kit needs a database to talk to');
}

export default defineConfig({
schema: './src/schema/index.ts',
out: './migrations',
dialect: 'postgresql',
dbCredentials: { url },
// Every rule the repository has about diffs applies to migrations too: a
// reviewer has to be able to read what changes.
verbose: true,
strict: true,
});
1 change: 1 addition & 0 deletions packages/db/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@wikifake/config/eslint';
65 changes: 65 additions & 0 deletions packages/db/migrations/0000_chubby_risque.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
CREATE TABLE "account" (
"id" text PRIMARY KEY NOT NULL,
"user_id" text NOT NULL,
"issuer" text NOT NULL,
"account_id" text NOT NULL,
"provider_id" text NOT NULL,
"access_token" text,
"refresh_token" text,
"access_token_expires_at" timestamp with time zone,
"refresh_token_expires_at" timestamp with time zone,
"scope" text,
"id_token" text,
"password" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "account_issuer_account_id_key" UNIQUE("issuer","account_id")
);
--> statement-breakpoint
CREATE TABLE "profile" (
"user_id" text PRIMARY KEY NOT NULL,
"display_name" text NOT NULL,
"accent" text DEFAULT 'teal' NOT NULL,
"preferences" jsonb DEFAULT '{}'::jsonb NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "session" (
"id" text PRIMARY KEY NOT NULL,
"user_id" text NOT NULL,
"token" text NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"ip_address" text,
"user_agent" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "session_token_unique" UNIQUE("token")
);
--> statement-breakpoint
CREATE TABLE "user" (
"id" text PRIMARY KEY NOT NULL,
"name" text NOT NULL,
"email" text NOT NULL,
"email_verified" boolean DEFAULT false NOT NULL,
"image" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "user_email_unique" UNIQUE("email")
);
--> statement-breakpoint
CREATE TABLE "verification" (
"id" text PRIMARY KEY NOT NULL,
"identifier" text NOT NULL,
"value" text NOT NULL,
"expires_at" timestamp with time zone NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "profile" ADD CONSTRAINT "profile_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "account_user_id_idx" ON "account" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "session_user_id_idx" ON "session" USING btree ("user_id");--> statement-breakpoint
CREATE INDEX "verification_identifier_idx" ON "verification" USING btree ("identifier");
Loading
Loading