1
0
Fork 0

feat(migrations): add new migration for projects, sprints, releases, and tickets

This commit is contained in:
Jason Fraley 2026-08-21 00:37:44 -04:00
parent 643e282fe7
commit 2a0668fc03
7 changed files with 17933 additions and 15 deletions

View file

@ -1,5 +0,0 @@
DATABASE_URI=postgres://ptfapp:polaristaskforcedb@127.0.0.1:5432/ptf-app-stg
PAYLOAD_SECRET=b5ac11acc77ea8427d10fcd0
# Added by Payload
LAST_UPDATED=

View file

@ -283,8 +283,8 @@ shadcn/ui components live in `src/components/ui/`. Use `bunx shadcn@latest add <
## Environment ## Environment
- `.env` — local dev (PostgreSQL connection string + PAYLOAD_SECRET) - `.env` — local dev (PostgreSQL connection string + PAYLOAD_SECRET)
- `.env.example` — template. `DATABASE_URI` line still shows MongoDB (outdated — trust `.env.test` for the real Postgres format), but `APP_URL` + `GAME_TICK_NOTIFY_SECRET` are current and required by the game tick. - `.env.example` — template. `DATABASE_URI` line still shows MongoDB (outdated — trust `.env.stg` for the real Postgres format), but `APP_URL` + `GAME_TICK_NOTIFY_SECRET` are current and required by the game tick.
- `.env.test` — test/staging database - `.env.stg` — test/staging database
- `test.env` — NODE_OPTIONS for playwright (loaded by playwright config) - `test.env` — NODE_OPTIONS for playwright (loaded by playwright config)
## Deploy ## Deploy
@ -294,7 +294,7 @@ shadcn/ui components live in `src/components/ui/`. Use `bunx shadcn@latest add <
## Gotchas ## Gotchas
- Next.js 16 framework traps (each one caused a real failed fix — see "Next.js 16 hard rules" above): `searchParams`/`params` are Promises and must be awaited; `cookies().set()` throws outside Server Actions/Route Handlers; `x-invoke-path` does not carry the real pathname in layouts. - Next.js 16 framework traps (each one caused a real failed fix — see "Next.js 16 hard rules" above): `searchParams`/`params` are Promises and must be awaited; `cookies().set()` throws outside Server Actions/Route Handlers; `x-invoke-path` does not carry the real pathname in layouts.
- `.env.example` shows MongoDB URI but the app uses PostgreSQL — trust `DATABASE_URI` format in `.env.test` as the real reference. - `.env.example` shows MongoDB URI but the app uses PostgreSQL — trust `DATABASE_URI` format in `.env.stg` as the real reference.
- `bun run build` passes `--max-old-space-size=8000` — the build is memory-intensive. - `bun run build` passes `--max-old-space-size=8000` — the build is memory-intensive.
- The `devturbo` script uses Turbopack; `dev` and `devsafe` use webpack. These are different bundlers with different behavior. - The `devturbo` script uses Turbopack; `dev` and `devsafe` use webpack. These are different bundlers with different behavior.
- Playwright tests auto-start the dev server — make sure port 3000 is free before running e2e. - Playwright tests auto-start the dev server — make sure port 3000 is free before running e2e.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,196 @@
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
export async function up({ db, payload, req }: MigrateUpArgs): Promise<void> {
await db.execute(sql`
CREATE TYPE "public"."enum_tickets_type" AS ENUM('task', 'bug', 'story', 'epic', 'improvement');
CREATE TYPE "public"."enum_tickets_resolution" AS ENUM('fixed', 'wontfix', 'duplicate', 'invalid');
CREATE TYPE "public"."enum_projects_status" AS ENUM('active', 'archived');
CREATE TYPE "public"."enum_sprints_status" AS ENUM('planning', 'active', 'completed');
CREATE TYPE "public"."enum_releases_status" AS ENUM('draft', 'in_progress', 'released');
ALTER TYPE "public"."enum_roles_permissions" ADD VALUE 'projects:create' BEFORE 'tickets:create';
ALTER TYPE "public"."enum_roles_permissions" ADD VALUE 'projects:read' BEFORE 'tickets:create';
ALTER TYPE "public"."enum_roles_permissions" ADD VALUE 'projects:update' BEFORE 'tickets:create';
ALTER TYPE "public"."enum_roles_permissions" ADD VALUE 'projects:delete' BEFORE 'tickets:create';
ALTER TYPE "public"."enum_roles_permissions" ADD VALUE 'labels:create' BEFORE 'tickets:create';
ALTER TYPE "public"."enum_roles_permissions" ADD VALUE 'labels:read' BEFORE 'tickets:create';
ALTER TYPE "public"."enum_roles_permissions" ADD VALUE 'labels:update' BEFORE 'tickets:create';
ALTER TYPE "public"."enum_roles_permissions" ADD VALUE 'labels:delete' BEFORE 'tickets:create';
ALTER TYPE "public"."enum_roles_permissions" ADD VALUE 'sprints:create' BEFORE 'tickets:create';
ALTER TYPE "public"."enum_roles_permissions" ADD VALUE 'sprints:read' BEFORE 'tickets:create';
ALTER TYPE "public"."enum_roles_permissions" ADD VALUE 'sprints:update' BEFORE 'tickets:create';
ALTER TYPE "public"."enum_roles_permissions" ADD VALUE 'sprints:delete' BEFORE 'tickets:create';
ALTER TYPE "public"."enum_roles_permissions" ADD VALUE 'releases:create' BEFORE 'tickets:create';
ALTER TYPE "public"."enum_roles_permissions" ADD VALUE 'releases:read' BEFORE 'tickets:create';
ALTER TYPE "public"."enum_roles_permissions" ADD VALUE 'releases:update' BEFORE 'tickets:create';
ALTER TYPE "public"."enum_roles_permissions" ADD VALUE 'releases:delete' BEFORE 'tickets:create';
CREATE TABLE "tickets_rels" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer,
"parent_id" integer NOT NULL,
"path" varchar NOT NULL,
"labels_id" integer
);
CREATE TABLE "projects" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar NOT NULL,
"key" varchar NOT NULL,
"description" jsonb,
"lead_id" integer,
"status" "enum_projects_status" DEFAULT 'active',
"default_assignee_id" integer,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
CREATE TABLE "sprints" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar NOT NULL,
"project_id" integer NOT NULL,
"goal" varchar,
"start_date" timestamp(3) with time zone,
"end_date" timestamp(3) with time zone,
"status" "enum_sprints_status" DEFAULT 'planning',
"capacity" numeric,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
CREATE TABLE "releases" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar NOT NULL,
"project_id" integer NOT NULL,
"description" jsonb,
"status" "enum_releases_status" DEFAULT 'draft',
"release_date" timestamp(3) with time zone,
"target_version" varchar,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
CREATE TABLE "labels" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar NOT NULL,
"color" varchar DEFAULT '#6366f1',
"project_id" integer,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
ALTER TABLE "tickets" ADD COLUMN "project_id" integer;
ALTER TABLE "tickets" ADD COLUMN "sprint_id" integer;
ALTER TABLE "tickets" ADD COLUMN "release_id" integer;
ALTER TABLE "tickets" ADD COLUMN "parent_ticket_id" integer;
ALTER TABLE "tickets" ADD COLUMN "type" "enum_tickets_type" DEFAULT 'task';
ALTER TABLE "tickets" ADD COLUMN "story_points" numeric;
ALTER TABLE "tickets" ADD COLUMN "estimate" numeric;
ALTER TABLE "tickets" ADD COLUMN "start_date" timestamp(3) with time zone;
ALTER TABLE "tickets" ADD COLUMN "due_date" timestamp(3) with time zone;
ALTER TABLE "tickets" ADD COLUMN "resolution" "enum_tickets_resolution";
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "projects_id" integer;
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "sprints_id" integer;
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "releases_id" integer;
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "labels_id" integer;
ALTER TABLE "tickets_rels" ADD CONSTRAINT "tickets_rels_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."tickets"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "tickets_rels" ADD CONSTRAINT "tickets_rels_labels_fk" FOREIGN KEY ("labels_id") REFERENCES "public"."labels"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "projects" ADD CONSTRAINT "projects_lead_id_users_id_fk" FOREIGN KEY ("lead_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "projects" ADD CONSTRAINT "projects_default_assignee_id_users_id_fk" FOREIGN KEY ("default_assignee_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "sprints" ADD CONSTRAINT "sprints_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "releases" ADD CONSTRAINT "releases_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "labels" ADD CONSTRAINT "labels_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE set null ON UPDATE no action;
CREATE INDEX "tickets_rels_order_idx" ON "tickets_rels" USING btree ("order");
CREATE INDEX "tickets_rels_parent_idx" ON "tickets_rels" USING btree ("parent_id");
CREATE INDEX "tickets_rels_path_idx" ON "tickets_rels" USING btree ("path");
CREATE INDEX "tickets_rels_labels_id_idx" ON "tickets_rels" USING btree ("labels_id");
CREATE UNIQUE INDEX "projects_key_idx" ON "projects" USING btree ("key");
CREATE INDEX "projects_lead_idx" ON "projects" USING btree ("lead_id");
CREATE INDEX "projects_default_assignee_idx" ON "projects" USING btree ("default_assignee_id");
CREATE INDEX "projects_updated_at_idx" ON "projects" USING btree ("updated_at");
CREATE INDEX "projects_created_at_idx" ON "projects" USING btree ("created_at");
CREATE INDEX "sprints_project_idx" ON "sprints" USING btree ("project_id");
CREATE INDEX "sprints_updated_at_idx" ON "sprints" USING btree ("updated_at");
CREATE INDEX "sprints_created_at_idx" ON "sprints" USING btree ("created_at");
CREATE INDEX "releases_project_idx" ON "releases" USING btree ("project_id");
CREATE INDEX "releases_updated_at_idx" ON "releases" USING btree ("updated_at");
CREATE INDEX "releases_created_at_idx" ON "releases" USING btree ("created_at");
CREATE INDEX "labels_project_idx" ON "labels" USING btree ("project_id");
CREATE INDEX "labels_updated_at_idx" ON "labels" USING btree ("updated_at");
CREATE INDEX "labels_created_at_idx" ON "labels" USING btree ("created_at");
ALTER TABLE "tickets" ADD CONSTRAINT "tickets_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "tickets" ADD CONSTRAINT "tickets_sprint_id_sprints_id_fk" FOREIGN KEY ("sprint_id") REFERENCES "public"."sprints"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "tickets" ADD CONSTRAINT "tickets_release_id_releases_id_fk" FOREIGN KEY ("release_id") REFERENCES "public"."releases"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "tickets" ADD CONSTRAINT "tickets_parent_ticket_id_tickets_id_fk" FOREIGN KEY ("parent_ticket_id") REFERENCES "public"."tickets"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_projects_fk" FOREIGN KEY ("projects_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_sprints_fk" FOREIGN KEY ("sprints_id") REFERENCES "public"."sprints"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_releases_fk" FOREIGN KEY ("releases_id") REFERENCES "public"."releases"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_labels_fk" FOREIGN KEY ("labels_id") REFERENCES "public"."labels"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX "tickets_project_idx" ON "tickets" USING btree ("project_id");
CREATE INDEX "tickets_sprint_idx" ON "tickets" USING btree ("sprint_id");
CREATE INDEX "tickets_release_idx" ON "tickets" USING btree ("release_id");
CREATE INDEX "tickets_parent_ticket_idx" ON "tickets" USING btree ("parent_ticket_id");
CREATE INDEX "payload_locked_documents_rels_projects_id_idx" ON "payload_locked_documents_rels" USING btree ("projects_id");
CREATE INDEX "payload_locked_documents_rels_sprints_id_idx" ON "payload_locked_documents_rels" USING btree ("sprints_id");
CREATE INDEX "payload_locked_documents_rels_releases_id_idx" ON "payload_locked_documents_rels" USING btree ("releases_id");
CREATE INDEX "payload_locked_documents_rels_labels_id_idx" ON "payload_locked_documents_rels" USING btree ("labels_id");`)
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "tickets_rels" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "projects" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "sprints" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "releases" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "labels" DISABLE ROW LEVEL SECURITY;
DROP TABLE "tickets_rels" CASCADE;
DROP TABLE "projects" CASCADE;
DROP TABLE "sprints" CASCADE;
DROP TABLE "releases" CASCADE;
DROP TABLE "labels" CASCADE;
ALTER TABLE "tickets" DROP CONSTRAINT "tickets_project_id_projects_id_fk";
ALTER TABLE "tickets" DROP CONSTRAINT "tickets_sprint_id_sprints_id_fk";
ALTER TABLE "tickets" DROP CONSTRAINT "tickets_release_id_releases_id_fk";
ALTER TABLE "tickets" DROP CONSTRAINT "tickets_parent_ticket_id_tickets_id_fk";
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_projects_fk";
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_sprints_fk";
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_releases_fk";
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_labels_fk";
ALTER TABLE "roles_permissions" ALTER COLUMN "value" SET DATA TYPE text;
DROP TYPE "public"."enum_roles_permissions";
CREATE TYPE "public"."enum_roles_permissions" AS ENUM('system:admin-access', 'users:create', 'users:read', 'users:update', 'users:delete', 'users:unlock', 'users:assign-roles', 'ranks:create', 'ranks:read', 'ranks:update', 'ranks:delete', 'profiles:create', 'profiles:read', 'profiles:update', 'profiles:delete', 'awards:create', 'awards:read', 'awards:update', 'awards:delete', 'qualifications:create', 'qualifications:read', 'qualifications:update', 'qualifications:delete', 'assignments:create', 'assignments:read', 'assignments:update', 'assignments:delete', 'experience:create', 'experience:read', 'experience:update', 'experience:delete', 'user-notifications:create', 'user-notifications:read', 'user-notifications:update', 'user-notifications:delete', 'missions:create', 'missions:read', 'missions:update', 'missions:delete', 'missions:read-sensitive', 'mission-attendances:create', 'mission-attendances:read', 'mission-attendances:update', 'mission-attendances:delete', 'campaigns:create', 'campaigns:read', 'campaigns:update', 'campaigns:delete', 'factions:create', 'factions:read', 'factions:update', 'factions:delete', 'technologies:create', 'technologies:read', 'technologies:update', 'technologies:delete', 'assets:create', 'assets:read', 'assets:update', 'assets:delete', 'resources:create', 'resources:read', 'resources:update', 'resources:delete', 'vehicles:create', 'vehicles:read', 'vehicles:update', 'vehicles:delete', 'structures:create', 'structures:read', 'structures:update', 'structures:delete', 'shipments:create', 'shipments:read', 'shipments:update', 'shipments:delete', 'bank-accounts:create', 'bank-accounts:read', 'bank-accounts:update', 'bank-accounts:delete', 'bank-transactions:create', 'bank-transactions:read', 'bank-transactions:update', 'bank-transactions:delete', 'ledger-entries:create', 'ledger-entries:read', 'ledger-entries:update', 'ledger-entries:delete', 'locker-storages:create', 'locker-storages:read', 'locker-storages:update', 'locker-storages:delete', 'loadouts:create', 'loadouts:read', 'loadouts:update', 'loadouts:delete', 'market-listings:create', 'market-listings:read', 'market-listings:update', 'market-listings:delete', 'market-negotiations:create', 'market-negotiations:read', 'market-negotiations:update', 'market-negotiations:delete', 'tickets:create', 'tickets:read', 'tickets:update', 'tickets:delete', 'game-structures:create', 'game-structures:read', 'game-structures:update', 'game-structures:delete', 'game-vehicles:create', 'game-vehicles:read', 'game-vehicles:update', 'game-vehicles:delete', 'game-npcs:create', 'game-npcs:read', 'game-npcs:update', 'game-npcs:delete', 'game-hard-resources:create', 'game-hard-resources:read', 'game-hard-resources:update', 'game-hard-resources:delete', 'game-event-logs:create', 'game-event-logs:read', 'game-event-logs:update', 'game-event-logs:delete', 'maps:create', 'maps:read', 'maps:update', 'maps:delete', 'narrative-events:create', 'narrative-events:read', 'narrative-events:update', 'narrative-events:delete', 'mission-files:create', 'mission-files:read', 'mission-files:update', 'mission-files:delete', 'mod-lists:create', 'mod-lists:read', 'mod-lists:update', 'mod-lists:delete', 'game-rules:read', 'game-rules:update', 'shims:read', 'shims:update', 'logistics:manage', 'intelligence:manage', 'profiles:intel_excerpt:update', 'banking:manage', 'tickets:staff', 'discord:staff', 'discord:announce');
ALTER TABLE "roles_permissions" ALTER COLUMN "value" SET DATA TYPE "public"."enum_roles_permissions" USING "value"::"public"."enum_roles_permissions";
DROP INDEX "tickets_project_idx";
DROP INDEX "tickets_sprint_idx";
DROP INDEX "tickets_release_idx";
DROP INDEX "tickets_parent_ticket_idx";
DROP INDEX "payload_locked_documents_rels_projects_id_idx";
DROP INDEX "payload_locked_documents_rels_sprints_id_idx";
DROP INDEX "payload_locked_documents_rels_releases_id_idx";
DROP INDEX "payload_locked_documents_rels_labels_id_idx";
ALTER TABLE "tickets" DROP COLUMN "project_id";
ALTER TABLE "tickets" DROP COLUMN "sprint_id";
ALTER TABLE "tickets" DROP COLUMN "release_id";
ALTER TABLE "tickets" DROP COLUMN "parent_ticket_id";
ALTER TABLE "tickets" DROP COLUMN "type";
ALTER TABLE "tickets" DROP COLUMN "story_points";
ALTER TABLE "tickets" DROP COLUMN "estimate";
ALTER TABLE "tickets" DROP COLUMN "start_date";
ALTER TABLE "tickets" DROP COLUMN "due_date";
ALTER TABLE "tickets" DROP COLUMN "resolution";
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "projects_id";
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "sprints_id";
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "releases_id";
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "labels_id";
DROP TYPE "public"."enum_tickets_type";
DROP TYPE "public"."enum_tickets_resolution";
DROP TYPE "public"."enum_projects_status";
DROP TYPE "public"."enum_sprints_status";
DROP TYPE "public"."enum_releases_status";`)
}

View file

@ -1,15 +1,16 @@
import * as migration_20260818_024510 from './20260818_024510'; import * as migration_20260818_024510 from './_archived/20260818_024510';
import * as migration_20260820_004157 from './20260820_004157'; import * as migration_20260820_004157 from './20260820_004157';
import * as migration_20260821_043007 from './20260821_043007';
export const migrations = [ export const migrations = [
{
up: migration_20260818_024510.up,
down: migration_20260818_024510.down,
name: '20260818_024510',
},
{ {
up: migration_20260820_004157.up, up: migration_20260820_004157.up,
down: migration_20260820_004157.down, down: migration_20260820_004157.down,
name: '20260820_004157' name: '20260820_004157',
},
{
up: migration_20260821_043007.up,
down: migration_20260821_043007.down,
name: '20260821_043007'
}, },
]; ];