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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,23 @@ Auth challenge, verification, and refresh endpoints use Fastify rate limiting ba

Google auth routes are not registered as a default login path. With `GOOGLE_AUTH_ENABLED=false`, `/auth/google/*` returns a not-enabled response. Enabling Google currently exposes the guarded boundary only; Google OAuth account linking and automatic Hive provisioning remain deferred until secure non-custodial account creation and key delivery are specified.

Implemented world endpoints:

- `POST /worlds`: requires authentication, creates a mutable off-chain World Seed and initial `WorldBibleVersion` in one transaction, and assigns the authenticated user an active `FOUNDER` membership. The route never accepts a client-supplied founder or role and does not publish to Hive.
- `GET /worlds`: browses discoverable worlds with pagination and basic title/description, genre, and tone filters.
- `GET /worlds/:worldId`: returns one world with its founder, seed, and current latest World Bible version.
- `GET /worlds/:worldId/hub`: returns the World Hub aggregate for the frontend, including world seed, current bible preview, simple canon/proposal stats, and latest canon lore entry summaries.
- `PATCH /worlds/:worldId`: requires `EDIT_INITIAL_CANON` world permission and updates mutable off-chain world title/description, seed, and latest initial bible content only. It does not create proposals, canonize content, or publish to Hive.

Implemented contribution endpoints:

- `POST /worlds/:worldId/contributions`: requires authentication and `CREATE_LORE_DRAFT` world permission, creates an author-owned structured contribution draft for lore or story content, and optionally links it to a lore entry in the same world.
- `GET /worlds/:worldId/contributions`: requires authentication and lists only the authenticated writer's contributions in the world with page/pageSize pagination and optional status/kind filters.
- `GET /worlds/:worldId/contributions/:contributionId`: returns an authenticated writer's own contribution draft or submitted contribution without exposing other writers' private drafts.
- `PATCH /worlds/:worldId/contributions/:contributionId`: requires `EDIT_OWN_DRAFT`, updates only mutable draft fields, and rejects submitted contributions.
- `DELETE /worlds/:worldId/contributions/:contributionId`: requires `EDIT_OWN_DRAFT`, deletes only draft contributions, and never deletes submitted proposals.
- `POST /worlds/:worldId/contributions/:contributionId/submit`: requires `SUBMIT_PROPOSAL`, atomically locks the draft, creates exactly one submitted proposal with an immutable `proposedContent` snapshot, and does not publish to Hive.

---

# Reputation & Rewards
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
CREATE TABLE "WorldSeed" (
"id" TEXT NOT NULL,
"worldId" TEXT NOT NULL,
"premise" TEXT NOT NULL,
"genre" TEXT NOT NULL,
"tone" TEXT NOT NULL,
"mainConflict" TEXT NOT NULL,
"startingLocation" TEXT,
"firstCharacters" JSONB,
"firstFactions" JSONB,
"firstHistoricalEvent" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "WorldSeed_pkey" PRIMARY KEY ("id")
);

CREATE UNIQUE INDEX "WorldSeed_worldId_key" ON "WorldSeed"("worldId");
CREATE INDEX "WorldSeed_genre_idx" ON "WorldSeed"("genre");
CREATE INDEX "WorldSeed_tone_idx" ON "WorldSeed"("tone");

ALTER TABLE "WorldSeed" ADD CONSTRAINT "WorldSeed_worldId_fkey" FOREIGN KEY ("worldId") REFERENCES "World"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
-- CreateEnum
CREATE TYPE "ContributionKind" AS ENUM ('LORE', 'STORY');

-- CreateEnum
CREATE TYPE "ContributionStatus" AS ENUM ('DRAFT', 'SUBMITTED');

-- AlterEnum
ALTER TYPE "ProposalType" ADD VALUE 'ADD_STORY';

-- AlterEnum
ALTER TYPE "WorldAuditAction" ADD VALUE 'CONTRIBUTION_CREATED';
ALTER TYPE "WorldAuditAction" ADD VALUE 'CONTRIBUTION_UPDATED';
ALTER TYPE "WorldAuditAction" ADD VALUE 'CONTRIBUTION_DELETED';
ALTER TYPE "WorldAuditAction" ADD VALUE 'CONTRIBUTION_SUBMITTED';
ALTER TYPE "WorldAuditAction" ADD VALUE 'PROPOSAL_CREATED';

-- AlterTable
ALTER TABLE "Proposal" ADD COLUMN "contributionKind" "ContributionKind";

-- CreateTable
CREATE TABLE "ContributionDraft" (
"id" TEXT NOT NULL,
"worldId" TEXT NOT NULL,
"authorId" TEXT NOT NULL,
"targetLoreEntryId" TEXT,
"kind" "ContributionKind" NOT NULL,
"title" TEXT NOT NULL,
"summary" TEXT,
"content" JSONB NOT NULL,
"status" "ContributionStatus" NOT NULL DEFAULT 'DRAFT',
"proposalId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"submittedAt" TIMESTAMP(3),

CONSTRAINT "ContributionDraft_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "ContributionDraft_proposalId_key" ON "ContributionDraft"("proposalId");

-- CreateIndex
CREATE INDEX "ContributionDraft_worldId_idx" ON "ContributionDraft"("worldId");

-- CreateIndex
CREATE INDEX "ContributionDraft_authorId_idx" ON "ContributionDraft"("authorId");

-- CreateIndex
CREATE INDEX "ContributionDraft_worldId_status_idx" ON "ContributionDraft"("worldId", "status");

-- CreateIndex
CREATE INDEX "ContributionDraft_worldId_authorId_status_idx" ON "ContributionDraft"("worldId", "authorId", "status");

-- CreateIndex
CREATE INDEX "ContributionDraft_targetLoreEntryId_idx" ON "ContributionDraft"("targetLoreEntryId");

-- CreateIndex
CREATE INDEX "Proposal_worldId_contributionKind_idx" ON "Proposal"("worldId", "contributionKind");

-- AddForeignKey
ALTER TABLE "ContributionDraft" ADD CONSTRAINT "ContributionDraft_worldId_fkey" FOREIGN KEY ("worldId") REFERENCES "World"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "ContributionDraft" ADD CONSTRAINT "ContributionDraft_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "ContributionDraft" ADD CONSTRAINT "ContributionDraft_targetLoreEntryId_fkey" FOREIGN KEY ("targetLoreEntryId") REFERENCES "LoreEntry"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "ContributionDraft" ADD CONSTRAINT "ContributionDraft_proposalId_fkey" FOREIGN KEY ("proposalId") REFERENCES "Proposal"("id") ON DELETE SET NULL ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
UPDATE "WorldBibleVersion"
SET "publishedAt" = COALESCE("publishedAt", "createdAt")
WHERE "versionNumber" = 1
AND "publishedAt" IS NULL;
140 changes: 105 additions & 35 deletions apps/api/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,21 @@ enum LoreStatus {

enum ProposalType {
ADD_LORE
ADD_STORY
UPDATE_LORE
UPDATE_WORLD_BIBLE
}

enum ContributionKind {
LORE
STORY
}

enum ContributionStatus {
DRAFT
SUBMITTED
}

enum ProposalStatus {
DRAFT
SUBMITTED
Expand Down Expand Up @@ -129,6 +140,11 @@ enum WorldAuditAction {
MODERATION_ACTION
CANON_STATUS_EXECUTED
AI_WARNING_RESOLVED
CONTRIBUTION_CREATED
CONTRIBUTION_UPDATED
CONTRIBUTION_DELETED
CONTRIBUTION_SUBMITTED
PROPOSAL_CREATED
}

enum AuthProvider {
Expand Down Expand Up @@ -168,6 +184,7 @@ model User {
bibleVersions WorldBibleVersion[] @relation("WorldBibleVersionCreator")
loreEntries LoreEntry[] @relation("LoreEntryAuthor")
proposals Proposal[] @relation("ProposalAuthor")
contributionDrafts ContributionDraft[] @relation("ContributionDraftAuthor")
votes AppVote[] @relation("AppVoteVoter")
moderationReports ModerationReport[] @relation("ModerationReporter")
moderationReviews ModerationReport[] @relation("ModerationReviewer")
Expand Down Expand Up @@ -296,27 +313,49 @@ model RcDelegation {
}

model World {
id String @id @default(cuid())
slug String @unique
title String
description String
founderId String
founder User @relation("WorldFounder", fields: [founderId], references: [id], onDelete: Restrict)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
bibleVersions WorldBibleVersion[]
loreEntries LoreEntry[]
loreRelationships LoreRelationship[]
proposals Proposal[]
moderationReports ModerationReport[]
searchIndexes SearchIndex[]
memberships WorldMembership[]
auditLogs WorldAuditLog[]
id String @id @default(cuid())
slug String @unique
title String
description String
founderId String
founder User @relation("WorldFounder", fields: [founderId], references: [id], onDelete: Restrict)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
seed WorldSeed?
bibleVersions WorldBibleVersion[]
loreEntries LoreEntry[]
loreRelationships LoreRelationship[]
proposals Proposal[]
contributionDrafts ContributionDraft[]
moderationReports ModerationReport[]
searchIndexes SearchIndex[]
memberships WorldMembership[]
auditLogs WorldAuditLog[]

@@index([founderId])
@@index([createdAt])
}

model WorldSeed {
id String @id @default(cuid())
worldId String @unique
world World @relation(fields: [worldId], references: [id], onDelete: Restrict)
premise String
genre String
tone String
mainConflict String
startingLocation String?
firstCharacters Json?
firstFactions Json?
firstHistoricalEvent String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
/// Mutable off-chain world seed. Hive publication requires a future approved publication flow.

@@index([genre])
@@index([tone])
}

model WorldMembership {
id String @id @default(cuid())
worldId String
Expand Down Expand Up @@ -380,26 +419,27 @@ model WorldBibleVersion {
}

model LoreEntry {
id String @id @default(cuid())
worldId String
world World @relation(fields: [worldId], references: [id], onDelete: Restrict)
authorId String
author User @relation("LoreEntryAuthor", fields: [authorId], references: [id], onDelete: Restrict)
title String
slug String
loreType LoreType
content Json
status LoreStatus @default(DRAFT)
publishedAt DateTime?
hiveReferenceId String? @unique
id String @id @default(cuid())
worldId String
world World @relation(fields: [worldId], references: [id], onDelete: Restrict)
authorId String
author User @relation("LoreEntryAuthor", fields: [authorId], references: [id], onDelete: Restrict)
title String
slug String
loreType LoreType
content Json
status LoreStatus @default(DRAFT)
publishedAt DateTime?
hiveReferenceId String? @unique
/// Published canon must be confirmed through an indexed HiveReference, not only this mutable row.
hiveReference HiveReference? @relation(fields: [hiveReferenceId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
outgoingRelations LoreRelationship[] @relation("LoreRelationshipSource")
incomingRelations LoreRelationship[] @relation("LoreRelationshipTarget")
targetProposals Proposal[] @relation("ProposalTargetLoreEntry")
resultingProposals Proposal[] @relation("ProposalResultingLoreEntry")
hiveReference HiveReference? @relation(fields: [hiveReferenceId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
outgoingRelations LoreRelationship[] @relation("LoreRelationshipSource")
incomingRelations LoreRelationship[] @relation("LoreRelationshipTarget")
targetProposals Proposal[] @relation("ProposalTargetLoreEntry")
targetContributionDrafts ContributionDraft[] @relation("ContributionDraftTargetLoreEntry")
resultingProposals Proposal[] @relation("ProposalResultingLoreEntry")

@@unique([worldId, slug])
@@index([worldId, status])
Expand Down Expand Up @@ -434,6 +474,7 @@ model Proposal {
authorId String
author User @relation("ProposalAuthor", fields: [authorId], references: [id], onDelete: Restrict)
proposalType ProposalType
contributionKind ContributionKind?
status ProposalStatus @default(DRAFT)
title String
summary String
Expand All @@ -451,12 +492,41 @@ model Proposal {
publishedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
contributionDraft ContributionDraft?
votes AppVote[]
aiReports AIReport[]
moderationReports ModerationReport[]

@@index([worldId, status])
@@index([authorId])
@@index([worldId, contributionKind])
@@index([targetLoreEntryId])
}

model ContributionDraft {
id String @id @default(cuid())
worldId String
world World @relation(fields: [worldId], references: [id], onDelete: Cascade)
authorId String
author User @relation("ContributionDraftAuthor", fields: [authorId], references: [id], onDelete: Cascade)
targetLoreEntryId String?
targetLoreEntry LoreEntry? @relation("ContributionDraftTargetLoreEntry", fields: [targetLoreEntryId], references: [id], onDelete: SetNull)
kind ContributionKind
title String
summary String?
content Json
status ContributionStatus @default(DRAFT)
proposalId String? @unique
/// Submitted drafts are locked; Proposal.proposedContent stores the immutable reviewer snapshot.
proposal Proposal? @relation(fields: [proposalId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
submittedAt DateTime?

@@index([worldId])
@@index([authorId])
@@index([worldId, status])
@@index([worldId, authorId, status])
@@index([targetLoreEntryId])
}

Expand Down
Loading
Loading