Skip to content
Merged
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
60 changes: 0 additions & 60 deletions docs/publishing-migration.md

This file was deleted.

6 changes: 0 additions & 6 deletions packages/backend/convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,7 @@ import type * as pageGuests from "../pageGuests.js";
import type * as pages from "../pages.js";
import type * as permissions from "../permissions.js";
import type * as publication from "../publication.js";
import type * as publicationCleanup from "../publicationCleanup.js";
import type * as publicationMigrations from "../publicationMigrations.js";
import type * as published from "../published.js";
import type * as releasePublication from "../releasePublication.js";
import type * as releases from "../releases.js";
import type * as schema_aiCredits from "../schema/aiCredits.js";
import type * as schema_billing from "../schema/billing.js";
Expand Down Expand Up @@ -158,10 +155,7 @@ declare const fullApi: ApiFromModules<{
pages: typeof pages;
permissions: typeof permissions;
publication: typeof publication;
publicationCleanup: typeof publicationCleanup;
publicationMigrations: typeof publicationMigrations;
published: typeof published;
releasePublication: typeof releasePublication;
releases: typeof releases;
"schema/aiCredits": typeof schema_aiCredits;
"schema/billing": typeof schema_billing;
Expand Down
4 changes: 0 additions & 4 deletions packages/backend/convex/draftRestores.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { components, internal } from "./_generated/api";
import type { Doc } from "./_generated/dataModel";
import { mutation, query } from "./_generated/server";
import type { QueryCtx } from "./_generated/server";
import { isReleaseAvailable } from "./model/releaseState";
import {
isOrganizationMember,
requireOrganizationPermission,
Expand All @@ -29,9 +28,6 @@ export const restore = mutation({
site.organizationId,
{ resource: "content", action: "edit" },
);
if (!isReleaseAvailable(release)) {
throw new ConvexError("Release is not available for restore");
}
if (site.activeDraftRestoreId) {
const active = await ctx.db.get(site.activeDraftRestoreId);
if (
Expand Down
4 changes: 1 addition & 3 deletions packages/backend/convex/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
} from "./search";
import { recordStorageUsageEvent } from "./model/storageTelemetry";
import { pendingSiteAssetLifecycle } from "./model/siteAssets";
import { isReleaseAvailable } from "./model/releaseState";

async function isFileReferencedByAccessiblePage(
ctx: Parameters<typeof getPageAccessOrNull>[0],
Expand Down Expand Up @@ -180,7 +179,7 @@ export const getPublic = query({
const site = await ctx.db.get(file.siteId);
if (!site?.liveReleaseId || !isPubliclyPublishedSite(site)) return null;
const release = await ctx.db.get(site.liveReleaseId);
if (!release || !isReleaseAvailable(release)) return null;
if (!release) return null;
const snapshot = await ctx.db
.query("releaseFiles")
.withIndex("by_release_file", (q) =>
Expand Down Expand Up @@ -212,7 +211,6 @@ export const getAuthorized = query({
: null;
const released =
liveRelease &&
isReleaseAvailable(liveRelease) &&
(await ctx.db
.query("releaseFiles")
.withIndex("by_release_file", (q) =>
Expand Down
3 changes: 1 addition & 2 deletions packages/backend/convex/libraries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import {
requireLibraryManagement,
} from "./model/libraryAccess";
import { assertDraftReadable, touchSiteDraft } from "./model/draft";
import { isReleaseAvailable } from "./model/releaseState";

const librarySummary = v.object({
_id: v.id("documentLibraries"),
Expand Down Expand Up @@ -251,7 +250,7 @@ export const getPublishedExplorer = query({
const access = await resolvePublishedSiteAccess(ctx, site);
if (!canRenderPublishedSite(access)) return null;
const release = await ctx.db.get(site.liveReleaseId);
if (!release || !isReleaseAvailable(release)) return null;
if (!release) return null;
const releasedLibrary = await ctx.db
.query("releaseLibraries")
.withIndex("by_release_library", (q) =>
Expand Down
28 changes: 4 additions & 24 deletions packages/backend/convex/model/contentObjects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,21 @@ import { describe, expect, test } from "bun:test";
import { readContentRevisionSearchText } from "./contentObjects";

describe("content revision search text", () => {
test("rebuilds missing search text for revisions written before the field existed", async () => {
test("reads the captured search text from a content revision", async () => {
const revision = {
_id: "revision-1",
payloadId: "payload-1",
searchText: undefined,
};
const payload = {
_id: "payload-1",
content: {
type: "doc",
version: 1,
content: [
{
type: "paragraph",
attrs: { "openeditor-id": "paragraph-1" },
content: [{ type: "text", text: "Legacy searchable content" }],
},
],
},
searchText: "Captured searchable content",
};

const result = await readContentRevisionSearchText(
{
db: {
get: async (id: string) =>
id === revision._id
? revision
: id === payload._id
? payload
: null,
get: async (id: string) => (id === revision._id ? revision : null),
},
} as never,
revision._id as never,
);

expect(result).toContain("Legacy searchable content");
expect(result).toBe("Captured searchable content");
});
});
17 changes: 2 additions & 15 deletions packages/backend/convex/model/contentObjects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,21 @@ import type { DataModel, Id } from "../_generated/dataModel";
import {
extractOpenEditorReferences,
extractOpenEditorText,
parseOpenEditorDocument,
type OpenEditorDocument,
} from "../pageContentFormat";
import { recordStorageUsageEvent } from "./storageTelemetry";

type MutationCtx = Pick<GenericMutationCtx<DataModel>, "db">;

/**
* Reads the denormalized text when present and rebuilds it for content
* revisions written before `searchText` was added to the schema.
*/
/** Reads the denormalized text captured on the content revision. */
export async function readContentRevisionSearchText(
ctx: MutationCtx,
revisionId: Id<"contentRevisions"> | undefined,
): Promise<string> {
if (!revisionId) return "";
const revision = await ctx.db.get(revisionId);
if (!revision) return "";
if (revision.searchText !== undefined) return revision.searchText;
const payload = await ctx.db.get(revision.payloadId);
if (!payload) return "";
try {
return extractOpenEditorText(parseOpenEditorDocument(payload.content));
} catch {
// Search is a derived projection. Omit malformed historical content here;
// publication validates legacy payloads before activation.
return "";
}
return revision.searchText;
}

export async function getOrCreateContentObject(
Expand Down
6 changes: 1 addition & 5 deletions packages/backend/convex/model/draftSummary.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import type { Doc } from "../_generated/dataModel";
import type { QueryCtx } from "../_generated/server";
import { isReleaseAvailable } from "./releaseState";

export async function buildDraftSummary(ctx: QueryCtx, site: Doc<"sites">) {
const liveReleaseCandidate = site.liveReleaseId
? await ctx.db.get(site.liveReleaseId)
: null;
const liveRelease =
liveReleaseCandidate?.siteId === site._id &&
isReleaseAvailable(liveReleaseCandidate)
? liveReleaseCandidate
: null;
liveReleaseCandidate?.siteId === site._id ? liveReleaseCandidate : null;
const hasDraftChanges =
(await ctx.db
.query("draftChanges")
Expand Down
3 changes: 1 addition & 2 deletions packages/backend/convex/model/publishedRelease.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { Doc, Id } from "../_generated/dataModel";
import type { QueryCtx } from "../_generated/server";
import { isReleaseAvailable } from "./releaseState";
import { canRenderPublishedSite, resolvePublishedSiteAccess } from "../sharing";

export type ResolvedReleasePage = {
Expand Down Expand Up @@ -101,7 +100,7 @@ export async function getReadableLiveRelease(
releaseId: Id<"siteReleases">,
) {
const release = await ctx.db.get(releaseId);
if (!release || !isReleaseAvailable(release)) return null;
if (!release) return null;
const site = await ctx.db.get(release.siteId);
if (!site || site.liveReleaseId !== release._id) return null;
const access = await resolvePublishedSiteAccess(ctx, site);
Expand Down
9 changes: 0 additions & 9 deletions packages/backend/convex/model/releaseState.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { describe, expect, test } from "bun:test";
import {
extractionBlocksPublication,
extractionRetryInvalidatesDraft,
isReleaseAvailable,
publicationActionForTarget,
} from "./releaseState";

Expand Down Expand Up @@ -34,12 +33,4 @@ describe("release publication guards", () => {
expect(extractionRetryInvalidatesDraft(undefined)).toBe(true);
expect(extractionRetryInvalidatesDraft("processing")).toBe(false);
});

test("keeps atomic and completed legacy releases readable", () => {
expect(isReleaseAvailable({})).toBe(true);
expect(isReleaseAvailable({ publicationStatus: "complete" })).toBe(true);
expect(isReleaseAvailable({ publicationStatus: "clearing" })).toBe(true);
expect(isReleaseAvailable({ publicationStatus: "building" })).toBe(false);
expect(isReleaseAvailable({ publicationStatus: "failed" })).toBe(false);
});
});
25 changes: 0 additions & 25 deletions packages/backend/convex/model/releaseState.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import type { Doc } from "../_generated/dataModel";

export function publicationActionForTarget(
currentNumber: number | undefined,
targetNumber: number,
Expand All @@ -9,15 +7,6 @@ export function publicationActionForTarget(
: "republish";
}

export type PublicationStatus = "building" | "clearing" | "complete" | "failed";

/** Compatibility state helpers for releases created by the retired workflow. */
export function isPublicationInFlight(
status: PublicationStatus | undefined,
): boolean {
return status === "building" || status === "clearing";
}

export function extractionBlocksPublication(
status: "queued" | "processing" | "ready" | "failed",
): boolean {
Expand Down Expand Up @@ -45,17 +34,3 @@ export function extractionIsPublishable(
(extraction.status === "ready" || extraction.status === "failed")
);
}

/**
* Old releases may still carry a publication workflow status. New releases
* activate atomically and have no status, so an absent status is readable.
*/
export function isReleaseAvailable(
release: Pick<Doc<"siteReleases">, "publicationStatus">,
): boolean {
return (
release.publicationStatus === undefined ||
release.publicationStatus === "complete" ||
release.publicationStatus === "clearing"
);
}
21 changes: 2 additions & 19 deletions packages/backend/convex/publication.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ describe("publish", () => {
expect(state.inserted).toHaveLength(0);
});

test("rejects malformed legacy page content before activation", async () => {
test("rejects a page with a missing captured revision", async () => {
const site: Row = {
_id: "site-1",
organizationId: "organization-1",
Expand All @@ -394,27 +394,10 @@ describe("publish", () => {
contentHash: "hash-1",
updatedAt: 1,
};
const revision: Row = {
_id: "revision-1",
siteId: site._id,
payloadId: "payload-1",
};
const state = makeDb({
sites: [site],
pages: [page],
pageDocuments: [document],
contentRevisions: [revision],
contentPayloads: [
{
_id: "payload-1",
siteId: site._id,
content: JSON.stringify({
type: "doc",
version: 1,
content: [{ type: "unknownHistoricalNode" }],
}),
},
],
draftChanges: [],
});

Expand Down Expand Up @@ -507,7 +490,7 @@ describe("live search projection", () => {
);
});

test("does not leak current extraction into a legacy release", async () => {
test("uses empty text when a file snapshot has no captured text", async () => {
const site: Row = {
_id: "site-1",
liveReleaseId: "release-1",
Expand Down
Loading