1
0
Fork 0

feat(wiki): add wiki collections and database migration

Add WikiPages, WikiRevisions, and WikiTemplates collections (admin group
Wiki), register them in the Payload config, and add migration batch 28
creating the wiki_pages/_texts, wiki_revisions, and wiki_templates tables
plus the three wiki values on the game_event_logs target_collection enum.
Includes the regenerated payload-types for the new collections.
This commit is contained in:
Jason Fraley 2026-09-05 00:01:58 -04:00
parent 72715c42a4
commit 01b1a73158
8 changed files with 21930 additions and 0 deletions

View file

@ -0,0 +1,86 @@
import { CollectionConfig } from "payload";
import { hasIntelligenceQualification } from "@/utils/access-control/hasIntelligenceQualification";
export const WikiPages: CollectionConfig = {
slug: "wiki-pages",
admin: {
group: "Wiki",
useAsTitle: "title",
defaultColumns: ["title", "slug", "category", "updatedAt"],
listSearchableFields: ["title", "slug"],
pagination: { defaultLimit: 50 },
},
access: {
read: ({ req }) => !!req.user,
create: ({ req }) => !!req.user,
update: ({ req }) => !!req.user,
delete: async ({ req }) => {
return await hasIntelligenceQualification(req.payload, req.user);
},
},
fields: [
{
name: "title",
type: "text",
required: true,
},
{
name: "slug",
type: "text",
required: true,
unique: true,
index: true,
admin: {
description: "URL slug. Generated by the service layer from the title.",
},
},
{
name: "category",
type: "select",
required: true,
options: [
{ label: "Campaign", value: "Campaign" },
{ label: "World", value: "World" },
{ label: "Lore", value: "Lore" },
{ label: "Characters", value: "Characters" },
{ label: "Plot", value: "Plot" },
{ label: "Media", value: "Media" },
{ label: "Guides", value: "Guides" },
{ label: "Meta", value: "Meta" },
],
index: true,
},
{
name: "tags",
type: "text",
hasMany: true,
},
{
name: "body",
type: "textarea",
required: true,
admin: {
description: "Markdown source. Rendered by the frontend.",
},
},
{
name: "lockdown",
type: "checkbox",
defaultValue: false,
},
{
name: "lockedBy",
type: "relationship",
relationTo: "users",
},
{
name: "lockedAt",
type: "date",
},
{
name: "lastEditor",
type: "relationship",
relationTo: "users",
},
],
};

View file

@ -0,0 +1,64 @@
import type { CollectionConfig } from "payload";
export const WikiRevisions: CollectionConfig = {
slug: "wiki-revisions",
admin: {
group: "Wiki",
defaultColumns: ["page", "revisionNumber", "type", "editor", "createdAt"],
pagination: { defaultLimit: 50 },
},
access: {
read: ({ req }) => Boolean(req.user),
create: () => false,
update: () => false,
delete: () => false,
},
fields: [
{
name: "page",
type: "relationship",
relationTo: "wiki-pages",
required: true,
index: true,
},
{
name: "revisionNumber",
type: "number",
required: true,
min: 1,
},
{
name: "titleSnapshot",
type: "text",
required: true,
},
{
name: "contentSnapshot",
type: "textarea",
required: true,
},
{
name: "editor",
type: "relationship",
relationTo: "users",
},
{
name: "summary",
type: "text",
},
{
name: "type",
type: "select",
required: true,
options: [
{ label: "Create", value: "create" },
{ label: "Edit", value: "edit" },
{ label: "Restore", value: "restore" },
],
},
{
name: "restoredFromRevision",
type: "number",
},
],
};

View file

@ -0,0 +1,40 @@
import { CollectionConfig } from "payload";
import hasRoles from "@/utils/access-control/hasRoles";
export const WikiTemplates: CollectionConfig = {
slug: "wiki-templates",
admin: {
group: "Wiki",
useAsTitle: "name",
defaultColumns: ["name", "updatedAt"],
listSearchableFields: ["name"],
pagination: { defaultLimit: 50 },
},
access: {
read: ({ req }) => !!req.user,
create: ({ req }) => hasRoles(["admin", "developer"], req.user),
update: ({ req }) => hasRoles(["admin", "developer"], req.user),
delete: ({ req }) => hasRoles(["admin", "developer"], req.user),
},
fields: [
{
name: "name",
type: "text",
required: true,
unique: true,
index: true,
},
{
name: "body",
type: "textarea",
required: true,
admin: {
description: "Snippet text with {{param}}-style placeholders as plain markdown.",
},
},
{
name: "description",
type: "textarea",
},
],
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,113 @@
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_wiki_pages_category" AS ENUM('Campaign', 'World', 'Lore', 'Characters', 'Plot', 'Media', 'Guides', 'Meta');
CREATE TYPE "public"."enum_wiki_revisions_type" AS ENUM('create', 'edit', 'restore');
ALTER TYPE "public"."enum_game_event_logs_target_collection" ADD VALUE 'wiki-pages' BEFORE 'game-servers';
ALTER TYPE "public"."enum_game_event_logs_target_collection" ADD VALUE 'wiki-revisions' BEFORE 'game-servers';
ALTER TYPE "public"."enum_game_event_logs_target_collection" ADD VALUE 'wiki-templates' BEFORE 'game-servers';
CREATE TABLE "wiki_pages" (
"id" serial PRIMARY KEY NOT NULL,
"title" varchar NOT NULL,
"slug" varchar NOT NULL,
"category" "enum_wiki_pages_category" NOT NULL,
"body" varchar NOT NULL,
"lockdown" boolean DEFAULT false,
"locked_by_id" integer,
"locked_at" timestamp(3) with time zone,
"last_editor_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 "wiki_pages_texts" (
"id" serial PRIMARY KEY NOT NULL,
"order" integer NOT NULL,
"parent_id" integer NOT NULL,
"path" varchar NOT NULL,
"text" varchar
);
CREATE TABLE "wiki_revisions" (
"id" serial PRIMARY KEY NOT NULL,
"page_id" integer NOT NULL,
"revision_number" numeric NOT NULL,
"title_snapshot" varchar NOT NULL,
"content_snapshot" varchar NOT NULL,
"editor_id" integer,
"summary" varchar,
"type" "enum_wiki_revisions_type" NOT NULL,
"restored_from_revision" 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 "wiki_templates" (
"id" serial PRIMARY KEY NOT NULL,
"name" varchar NOT NULL,
"body" varchar NOT NULL,
"description" varchar,
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
);
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "wiki_pages_id" integer;
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "wiki_revisions_id" integer;
ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "wiki_templates_id" integer;
ALTER TABLE "wiki_pages" ADD CONSTRAINT "wiki_pages_locked_by_id_users_id_fk" FOREIGN KEY ("locked_by_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "wiki_pages" ADD CONSTRAINT "wiki_pages_last_editor_id_users_id_fk" FOREIGN KEY ("last_editor_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "wiki_pages_texts" ADD CONSTRAINT "wiki_pages_texts_parent_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."wiki_pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "wiki_revisions" ADD CONSTRAINT "wiki_revisions_page_id_wiki_pages_id_fk" FOREIGN KEY ("page_id") REFERENCES "public"."wiki_pages"("id") ON DELETE set null ON UPDATE no action;
ALTER TABLE "wiki_revisions" ADD CONSTRAINT "wiki_revisions_editor_id_users_id_fk" FOREIGN KEY ("editor_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;
CREATE UNIQUE INDEX "wiki_pages_slug_idx" ON "wiki_pages" USING btree ("slug");
CREATE INDEX "wiki_pages_category_idx" ON "wiki_pages" USING btree ("category");
CREATE INDEX "wiki_pages_locked_by_idx" ON "wiki_pages" USING btree ("locked_by_id");
CREATE INDEX "wiki_pages_last_editor_idx" ON "wiki_pages" USING btree ("last_editor_id");
CREATE INDEX "wiki_pages_updated_at_idx" ON "wiki_pages" USING btree ("updated_at");
CREATE INDEX "wiki_pages_created_at_idx" ON "wiki_pages" USING btree ("created_at");
CREATE INDEX "wiki_pages_texts_order_parent" ON "wiki_pages_texts" USING btree ("order","parent_id");
CREATE INDEX "wiki_revisions_page_idx" ON "wiki_revisions" USING btree ("page_id");
CREATE INDEX "wiki_revisions_editor_idx" ON "wiki_revisions" USING btree ("editor_id");
CREATE INDEX "wiki_revisions_updated_at_idx" ON "wiki_revisions" USING btree ("updated_at");
CREATE INDEX "wiki_revisions_created_at_idx" ON "wiki_revisions" USING btree ("created_at");
CREATE UNIQUE INDEX "wiki_templates_name_idx" ON "wiki_templates" USING btree ("name");
CREATE INDEX "wiki_templates_updated_at_idx" ON "wiki_templates" USING btree ("updated_at");
CREATE INDEX "wiki_templates_created_at_idx" ON "wiki_templates" USING btree ("created_at");
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_wiki_pages_fk" FOREIGN KEY ("wiki_pages_id") REFERENCES "public"."wiki_pages"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_wiki_revisions_fk" FOREIGN KEY ("wiki_revisions_id") REFERENCES "public"."wiki_revisions"("id") ON DELETE cascade ON UPDATE no action;
ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_wiki_templates_fk" FOREIGN KEY ("wiki_templates_id") REFERENCES "public"."wiki_templates"("id") ON DELETE cascade ON UPDATE no action;
CREATE INDEX "payload_locked_documents_rels_wiki_pages_id_idx" ON "payload_locked_documents_rels" USING btree ("wiki_pages_id");
CREATE INDEX "payload_locked_documents_rels_wiki_revisions_id_idx" ON "payload_locked_documents_rels" USING btree ("wiki_revisions_id");
CREATE INDEX "payload_locked_documents_rels_wiki_templates_id_idx" ON "payload_locked_documents_rels" USING btree ("wiki_templates_id");`)
}
export async function down({ db, payload, req }: MigrateDownArgs): Promise<void> {
await db.execute(sql`
ALTER TABLE "wiki_pages" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "wiki_pages_texts" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "wiki_revisions" DISABLE ROW LEVEL SECURITY;
ALTER TABLE "wiki_templates" DISABLE ROW LEVEL SECURITY;
DROP TABLE "wiki_pages" CASCADE;
DROP TABLE "wiki_pages_texts" CASCADE;
DROP TABLE "wiki_revisions" CASCADE;
DROP TABLE "wiki_templates" CASCADE;
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_wiki_pages_fk";
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_wiki_revisions_fk";
ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_wiki_templates_fk";
ALTER TABLE "game_event_logs" ALTER COLUMN "target_collection" SET DATA TYPE text;
DROP TYPE "public"."enum_game_event_logs_target_collection";
CREATE TYPE "public"."enum_game_event_logs_target_collection" AS ENUM('game-structures', 'game-vehicles', 'game-npcs', 'structures', 'resources', 'assets', 'vehicles', 'factions', 'missions', 'campaigns', 'users', 'technologies', 'maps', 'shipments', 'bank-accounts', 'bank-transactions', 'ledger-entries', 'locker-storages', 'loadouts', 'market-listings', 'market-negotiations', 'evaluations', 'tickets', 'game-servers', 'assignment-transfers');
ALTER TABLE "game_event_logs" ALTER COLUMN "target_collection" SET DATA TYPE "public"."enum_game_event_logs_target_collection" USING "target_collection"::"public"."enum_game_event_logs_target_collection";
DROP INDEX "payload_locked_documents_rels_wiki_pages_id_idx";
DROP INDEX "payload_locked_documents_rels_wiki_revisions_id_idx";
DROP INDEX "payload_locked_documents_rels_wiki_templates_id_idx";
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "wiki_pages_id";
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "wiki_revisions_id";
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "wiki_templates_id";
DROP TYPE "public"."enum_wiki_pages_category";
DROP TYPE "public"."enum_wiki_revisions_type";`)
}

View file

@ -21,6 +21,7 @@ import * as migration_20260903_053419 from './20260903_053419';
import * as migration_20260903_060000_add_radio_mode_stats from './20260903_060000_add_radio_mode_stats';
import * as migration_20260903_061000_add_radio_incorrect_counts from './20260903_061000_add_radio_incorrect_counts';
import * as migration_20260903_062000_add_aim_trainer_stats from './20260903_062000_add_aim_trainer_stats';
import * as migration_20260904_230838_add_wiki_collections from './20260904_230838_add_wiki_collections';
export const migrations = [
{
@ -138,4 +139,9 @@ export const migrations = [
down: migration_20260903_062000_add_aim_trainer_stats.down,
name: '20260903_062000_add_aim_trainer_stats',
},
{
up: migration_20260904_230838_add_wiki_collections.up,
down: migration_20260904_230838_add_wiki_collections.down,
name: '20260904_230838_add_wiki_collections'
},
];

View file

@ -104,6 +104,9 @@ export interface Config {
'market-negotiations': MarketNegotiation;
tickets: Ticket;
'ticket-votes': TicketVote;
'wiki-pages': WikiPage;
'wiki-revisions': WikiRevision;
'wiki-templates': WikiTemplate;
projects: Project;
sprints: Sprint;
releases: Release;
@ -167,6 +170,9 @@ export interface Config {
'market-negotiations': MarketNegotiationsSelect<false> | MarketNegotiationsSelect<true>;
tickets: TicketsSelect<false> | TicketsSelect<true>;
'ticket-votes': TicketVotesSelect<false> | TicketVotesSelect<true>;
'wiki-pages': WikiPagesSelect<false> | WikiPagesSelect<true>;
'wiki-revisions': WikiRevisionsSelect<false> | WikiRevisionsSelect<true>;
'wiki-templates': WikiTemplatesSelect<false> | WikiTemplatesSelect<true>;
projects: ProjectsSelect<false> | ProjectsSelect<true>;
sprints: SprintsSelect<false> | SprintsSelect<true>;
releases: ReleasesSelect<false> | ReleasesSelect<true>;
@ -1677,6 +1683,9 @@ export interface GameEventLog {
| 'market-negotiations'
| 'evaluations'
| 'tickets'
| 'wiki-pages'
| 'wiki-revisions'
| 'wiki-templates'
| 'game-servers'
| 'assignment-transfers'
)
@ -3081,6 +3090,62 @@ export interface TicketVote {
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "wiki-pages".
*/
export interface WikiPage {
id: number;
title: string;
/**
* URL slug. Generated by the service layer from the title.
*/
slug: string;
category: 'Campaign' | 'World' | 'Lore' | 'Characters' | 'Plot' | 'Media' | 'Guides' | 'Meta';
tags?: string[] | null;
/**
* Markdown source. Rendered by the frontend.
*/
body: string;
lockdown?: boolean | null;
lockedBy?: (number | null) | User;
lockedAt?: string | null;
lastEditor?: (number | null) | User;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "wiki-revisions".
*/
export interface WikiRevision {
id: number;
page: number | WikiPage;
revisionNumber: number;
titleSnapshot: string;
contentSnapshot: string;
editor?: (number | null) | User;
summary?: string | null;
type: 'create' | 'edit' | 'restore';
restoredFromRevision?: number | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "wiki-templates".
*/
export interface WikiTemplate {
id: number;
name: string;
/**
* Snippet text with {{param}}-style placeholders as plain markdown.
*/
body: string;
description?: string | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "narrative-events".
@ -3436,6 +3501,18 @@ export interface PayloadLockedDocument {
relationTo: 'ticket-votes';
value: number | TicketVote;
} | null)
| ({
relationTo: 'wiki-pages';
value: number | WikiPage;
} | null)
| ({
relationTo: 'wiki-revisions';
value: number | WikiRevision;
} | null)
| ({
relationTo: 'wiki-templates';
value: number | WikiTemplate;
} | null)
| ({
relationTo: 'projects';
value: number | Project;
@ -4727,6 +4804,50 @@ export interface TicketVotesSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "wiki-pages_select".
*/
export interface WikiPagesSelect<T extends boolean = true> {
title?: T;
slug?: T;
category?: T;
tags?: T;
body?: T;
lockdown?: T;
lockedBy?: T;
lockedAt?: T;
lastEditor?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "wiki-revisions_select".
*/
export interface WikiRevisionsSelect<T extends boolean = true> {
page?: T;
revisionNumber?: T;
titleSnapshot?: T;
contentSnapshot?: T;
editor?: T;
summary?: T;
type?: T;
restoredFromRevision?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "wiki-templates_select".
*/
export interface WikiTemplatesSelect<T extends boolean = true> {
name?: T;
body?: T;
description?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "projects_select".

View file

@ -50,6 +50,9 @@ import { UserNotifications } from "@/collections/users/UserNotifications";
import { Evaluations } from "@/collections/users/Evaluations";
import { Tickets } from "@/collections/tickets/Tickets";
import { TicketVotes } from "@/collections/tickets/TicketVotes";
import { WikiPages } from "@/collections/wiki/WikiPages";
import { WikiRevisions } from "@/collections/wiki/WikiRevisions";
import { WikiTemplates } from "@/collections/wiki/WikiTemplates";
import { Projects } from "@/collections/projects/Projects";
import { Sprints } from "@/collections/projects/Sprints";
import { Releases } from "@/collections/projects/Releases";
@ -194,6 +197,11 @@ const collections = [
Tickets,
TicketVotes,
// Wiki
WikiPages,
WikiRevisions,
WikiTemplates,
// Projects
Projects,
Sprints,
@ -365,6 +373,11 @@ export default buildConfig({
// Helpdesk
[Tickets.slug]: true,
// Wiki
[WikiPages.slug]: true,
[WikiRevisions.slug]: true,
[WikiTemplates.slug]: true,
// Projects
[Projects.slug]: true,
[Sprints.slug]: true,