From 331316122db0fc042f4b5a06918421416c6bcade Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Wed, 4 Mar 2026 10:29:41 -0500 Subject: [PATCH 01/26] feat: coar notify US4, announce ingest --- core/playwright/coar-notify.spec.ts | 132 +++++++++++++++++- .../fixtures/coar-notify-payloads.ts | 3 + core/prisma/seeds/coar-notify.ts | 62 +++++++- .../app/components/SendNotificationForm.tsx | 60 +++++++- mock-notify/src/lib/payloads.ts | 3 + 5 files changed, 246 insertions(+), 14 deletions(-) diff --git a/core/playwright/coar-notify.spec.ts b/core/playwright/coar-notify.spec.ts index fd412dc5c..89589da5c 100644 --- a/core/playwright/coar-notify.spec.ts +++ b/core/playwright/coar-notify.spec.ts @@ -82,6 +82,17 @@ const seed = createSeed({ config: { path: WEBHOOK_PATH }, }, ], + condition: { + type: AutomationConditionBlockType.AND, + items: [ + { + kind: "condition", + type: "jsonata", + expression: + "'Offer' in $.json.type or ('Announce' in $.json.type and 'coar-notify:IngestAction' in $.json.type)", + }, + ], + }, actions: [ { action: Action.createPub, @@ -97,7 +108,7 @@ const seed = createSeed({ }, ], }, - "Create Review for Notification": { + "Create Review for Offer": { triggers: [ { event: AutomationEvent.pubEnteredStage, @@ -112,6 +123,11 @@ const seed = createSeed({ type: "jsonata", expression: "$.pub.pubType.name = 'Notification'", }, + { + kind: "condition", + type: "jsonata", + expression: "'Offer' in $eval($.pub.values.payload).type", + }, ], }, actions: [ @@ -122,8 +138,6 @@ const seed = createSeed({ formSlug: "review-default-editor", pubValues: { title: "Review for: {{ $.pub.values.title }}", - // Copy sourceurl from Notification to Review for use in Announce - // TODO: Investigate why relationship traversal ($.pub.out.relatedpub.values.sourceurl) isn't working sourceurl: "{{ $.pub.values.sourceurl }}", }, relationConfig: { @@ -136,8 +150,89 @@ const seed = createSeed({ }, ], }, + "Accept Request": { + triggers: [{ event: AutomationEvent.manual, config: {} }], + condition: { + type: AutomationConditionBlockType.AND, + items: [ + { + kind: "condition", + type: "jsonata", + expression: "$.pub.pubType.name = 'Notification'", + }, + ], + }, + actions: [{ action: Action.move, config: { stage: STAGE_IDS.Accepted } }], + }, + "Reject Request": { + triggers: [{ event: AutomationEvent.manual, config: {} }], + condition: { + type: AutomationConditionBlockType.AND, + items: [ + { + kind: "condition", + type: "jsonata", + expression: "$.pub.pubType.name = 'Notification'", + }, + ], + }, + actions: [{ action: Action.move, config: { stage: STAGE_IDS.Rejected } }], + }, }, }, + Accepted: { + id: STAGE_IDS.Accepted, + automations: { + "Create Review for Ingest": { + triggers: [ + { + event: AutomationEvent.pubEnteredStage, + config: {}, + }, + ], + condition: { + type: AutomationConditionBlockType.AND, + items: [ + { + kind: "condition", + type: "jsonata", + expression: "$.pub.pubType.name = 'Notification'", + }, + { + kind: "condition", + type: "jsonata", + expression: + "'Announce' in $eval($.pub.values.payload).type and 'coar-notify:IngestAction' in $eval($.pub.values.payload).type", + }, + ], + }, + resolver: `$.pub.id = {{ $replace($replace($eval($.pub.values.payload).object["as:inReplyTo"], $.env.PUBPUB_URL & "/c/" & $.community.slug & "/pubs/", ""), $.env.PUBPUB_URL & "/c/" & $.community.slug & "/pub/", "") }}`, + actions: [ + { + action: Action.createPub, + config: { + stage: STAGE_IDS.ReviewInbox, + formSlug: "review-default-editor", + pubValues: { + title: + "Review from aggregator: {{ $eval($.pub.values.payload).object.id }}", + }, + relationConfig: { + fieldSlug: `${COMMUNITY_SLUG}:relatedpub`, + relatedPubId: "{{ $.pub.id }}", + value: "Submission", + direction: "source", + }, + }, + }, + ], + }, + }, + }, + Rejected: { + id: STAGE_IDS.Rejected, + automations: {}, + }, ReviewInbox: { id: STAGE_IDS.ReviewInbox, automations: { @@ -250,7 +345,10 @@ const seed = createSeed({ ], stageConnections: { Inbox: { - to: ["ReviewRequested", "Published"], + to: ["ReviewRequested", "Published", "Accepted", "Rejected"], + }, + Accepted: { + to: ["ReviewInbox"], }, ReviewInbox: { to: ["Reviewing"], @@ -444,12 +542,17 @@ test.describe("User Story 4: Review Group Aggregation Announcement to Repositori await loginPage.goto() await loginPage.loginAndWaitForNavigation(community.users.admin.email, "password") - const webhookUrl = `${process.env.PLAYWRIGHT_BASE_URL || "http://localhost:3000"}/api/v0/c/${community.community.slug}/site/webhook/${WEBHOOK_PATH}` + const baseUrl = process.env.PLAYWRIGHT_BASE_URL || "http://localhost:3000" + const webhookUrl = `${baseUrl}/api/v0/c/${community.community.slug}/site/webhook/${WEBHOOK_PATH}` + + const submissionPub = community.pubs[0] + const workUrl = `${baseUrl}/c/${community.community.slug}/pub/${submissionPub.id}` const ingestionAnnouncement = createAnnounceIngestPayload({ reviewId: "review-123", serviceUrl: "https://review-group.org", aggregatorUrl: mockPreprintRepo.url, + workUrl, }) await mockPreprintRepo.sendNotification(webhookUrl, ingestionAnnouncement) @@ -463,5 +566,24 @@ test.describe("User Story 4: Review Group Aggregation Announcement to Repositori ).toBeVisible({ timeout: 15000, }) + + // Accept the notification to trigger Create Review for Ingest + const stagesManagePage = new StagesManagePage(page, community.community.slug) + await stagesManagePage.goTo() + await stagesManagePage.openStagePanelTab("Inbox", "Pubs") + await page.getByRole("button", { name: "Inbox" }).first().click() + await page.getByText("Move to Accepted").click() + + // Verify Review was created and linked to the Submission (Pre-existing Pub) + await expect + .poll( + async () => { + await page.goto(`/c/${community.community.slug}/stages`) + const reviewList = page.getByText("Review from aggregator:", { exact: false }) + return (await reviewList.count()) > 0 + }, + { timeout: 15000 } + ) + .toBe(true) }) }) diff --git a/core/playwright/fixtures/coar-notify-payloads.ts b/core/playwright/fixtures/coar-notify-payloads.ts index 361848c55..5d7ccf695 100644 --- a/core/playwright/fixtures/coar-notify-payloads.ts +++ b/core/playwright/fixtures/coar-notify-payloads.ts @@ -231,10 +231,12 @@ export function createAnnounceIngestPayload({ reviewId, serviceUrl, aggregatorUrl, + workUrl, }: { reviewId: string serviceUrl: string aggregatorUrl: string + workUrl?: string }): CoarNotifyPayload { const reviewUrl = `${serviceUrl}/review/${reviewId}` return { @@ -249,6 +251,7 @@ export function createAnnounceIngestPayload({ object: { id: reviewUrl, type: ["Page", "sorg:Review"], + ...(workUrl && { "as:inReplyTo": workUrl }), }, target: { id: serviceUrl, diff --git a/core/prisma/seeds/coar-notify.ts b/core/prisma/seeds/coar-notify.ts index f4f520463..2d0e061aa 100644 --- a/core/prisma/seeds/coar-notify.ts +++ b/core/prisma/seeds/coar-notify.ts @@ -115,14 +115,15 @@ export async function seedCoarNotify(communityId?: CommunitiesId) { config: { path: WEBHOOK_PATH }, }, ], - // Only process incoming Offer notifications (review requests from external services) + // Process incoming Offer (review requests) or Announce Ingest (aggregator notifications) condition: { type: AutomationConditionBlockType.AND, items: [ { kind: "condition", type: "jsonata", - expression: "'Offer' in $.json.type", + expression: + "'Offer' in $.json.type or ('Announce' in $.json.type and 'coar-notify:IngestAction' in $.json.type)", }, ], }, @@ -597,8 +598,8 @@ pre { background: #f3f4f6; padding: 1rem; border-radius: 0.5rem; overflow-x: aut }, ], }, - // Create a Review pub after accepting the request - "Create Review for Notification": { + // Create a Review pub after accepting an Offer (links to Notification) + "Create Review for Offer": { icon: { name: "plus-circle", color: "#10b981", @@ -617,6 +618,11 @@ pre { background: #f3f4f6; padding: 1rem; border-radius: 0.5rem; overflow-x: aut type: "jsonata", expression: "$.pub.pubType.name = 'Notification'", }, + { + kind: "condition", + type: "jsonata", + expression: "'Offer' in $eval($.pub.values.Payload).type", + }, ], }, actions: [ @@ -638,6 +644,54 @@ pre { background: #f3f4f6; padding: 1rem; border-radius: 0.5rem; overflow-x: aut }, ], }, + // Create a Review pub after accepting an Announce Ingest (links to resolved local article) + "Create Review for Ingest": { + icon: { + name: "plus-circle", + color: "#10b981", + }, + triggers: [ + { + event: AutomationEvent.pubEnteredStage, + config: {}, + }, + ], + condition: { + type: AutomationConditionBlockType.AND, + items: [ + { + kind: "condition", + type: "jsonata", + expression: "$.pub.pubType.name = 'Notification'", + }, + { + kind: "condition", + type: "jsonata", + expression: + "'Announce' in $eval($.pub.values.Payload).type and 'coar-notify:IngestAction' in $eval($.pub.values.Payload).type", + }, + ], + }, + resolver: `$.pub.id = {{ $replace($replace($eval($.pub.values.Payload).object["as:inReplyTo"], $.env.PUBPUB_URL & "/c/" & $.community.slug & "/pubs/", ""), $.env.PUBPUB_URL & "/c/" & $.community.slug & "/pub/", "") }}`, + actions: [ + { + action: Action.createPub, + config: { + stage: STAGE_IDS.ReviewInbox, + formSlug: "review-default-editor", + pubValues: { + Title: "Review from aggregator: {{ $eval($.pub.values.Payload).object.id }}", + }, + relationConfig: { + fieldSlug: "coar-notify:relatedpub", + relatedPubId: "{{ $.pub.id }}", + value: "Submission", + direction: "source", + }, + }, + }, + ], + }, }, }, Rejected: { diff --git a/mock-notify/src/app/components/SendNotificationForm.tsx b/mock-notify/src/app/components/SendNotificationForm.tsx index 3435342f2..edec482dc 100644 --- a/mock-notify/src/app/components/SendNotificationForm.tsx +++ b/mock-notify/src/app/components/SendNotificationForm.tsx @@ -65,6 +65,9 @@ export function SendNotificationForm({ onSent, prefill }: SendNotificationFormPr const [serviceName, setServiceName] = useState("Mock Review Service") const [inReplyTo, setInReplyTo] = useState(prefill?.inReplyTo ?? "") const [inReplyToUrl, setInReplyToUrl] = useState(prefill?.inReplyToObjectUrl ?? "") + const [workUrl, setWorkUrl] = useState( + "http://localhost:3000/c/coar-notify/pub/{pubId}" + ) const generatePayload = (): CoarNotifyPayload => { switch (templateType) { @@ -95,6 +98,7 @@ export function SendNotificationForm({ onSent, prefill }: SendNotificationFormPr reviewUrl, originUrl, targetUrl: targetServiceUrl, + workUrl: workUrl || undefined, }) case "Accept": return createAcceptPayload({ @@ -297,7 +301,6 @@ export function SendNotificationForm({ onSent, prefill }: SendNotificationFormPr ) case "Offer Ingest": - case "Announce Ingest": return ( <>
@@ -307,18 +310,65 @@ export function SendNotificationForm({ onSent, prefill }: SendNotificationFormPr type="text" value={reviewUrl} onChange={(e) => setReviewUrl(e.target.value)} - placeholder="http://localhost:4000/review/..." + placeholder="http://localhost:4001/review/..." + className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" + /> + +
+
+ +
+
+ setReviewUrl(e.target.value)} - placeholder="http://localhost:4001/review/..." + value={targetServiceUrl} + onChange={(e) => setTargetServiceUrl(e.target.value)} className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" />
+ + ) + case "Announce Ingest": + return ( + <> +
+ +
+
+ +
From 5239af947d907213bdc7876c89ed19a7bd0b86f3 Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Thu, 5 Mar 2026 15:15:29 -0500 Subject: [PATCH 03/26] proper syntax for seeding action configs w/ jsonata --- core/prisma/seeds/coar-notify.ts | 42 ++++++++++++++++---------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/core/prisma/seeds/coar-notify.ts b/core/prisma/seeds/coar-notify.ts index 0c18d5f30..fc1c8d61b 100644 --- a/core/prisma/seeds/coar-notify.ts +++ b/core/prisma/seeds/coar-notify.ts @@ -144,33 +144,33 @@ export async function seedCoarUS1(communityId?: CommunitiesId) { config: { url: REMOTE_INBOX_URL, method: "POST", - body: { + body: `<<< { "@context": [ "https://www.w3.org/ns/activitystreams", - "https://coar-notify.net", + "https://coar-notify.net" ], - type: ["Offer", "coar-notify:ReviewAction"], - id: "urn:uuid:{{ $.pub.id }}", - actor: { - id: "{{ $.env.PUBPUB_URL }}/c/{{ $.community.slug }}", - type: "Service", - name: "{{ $.community.name }}", - }, - object: { - id: "{{ $.env.PUBPUB_URL }}/c/{{ $.community.slug }}/pub/{{ $.pub.id }}", - type: ["Page", "sorg:AboutPage"], + "type": ["Offer", "coar-notify:ReviewAction"], + "id": "urn:uuid:" & $.pub.id, + "actor": { + "id": $.env.PUBPUB_URL & "/c/" & $.community.slug, + "type": "Service", + "name": $.community.name }, - target: { - id: REMOTE_INBOX_URL.replace("/inbox", ""), - inbox: REMOTE_INBOX_URL, - type: "Service", + "object": { + "id": $.env.PUBPUB_URL & "/c/" & $.community.slug & "/pub/" & $.pub.id, + "type": ["Page", "sorg:AboutPage"] }, - origin: { - id: "{{ $.env.PUBPUB_URL }}/c/{{ $.community.slug }}", - inbox: `{{ $.env.PUBPUB_URL }}/api/v0/c/{{ $.community.slug }}/site/webhook/${WEBHOOK_PATH}`, - type: "Service", + "target": { + "id": "${REMOTE_INBOX_URL.replace("/inbox", "")}", + "inbox": "${REMOTE_INBOX_URL}", + "type": "Service" }, - }, + "origin": { + "id": $.env.PUBPUB_URL & "/c/" & $.community.slug, + "inbox": $.env.PUBPUB_URL & "/api/v0/c/" & $.community.slug & "/site/webhook/${WEBHOOK_PATH}", + "type": "Service" + } + } >>>`, }, }, ], From 9d3792511e4fbce4397d3390cf938aa121cab465 Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Wed, 11 Mar 2026 10:58:36 -0400 Subject: [PATCH 04/26] wip --- .gitignore | 3 + core/actions/buildSite/action.ts | 8 +- core/actions/buildSite/run.tsx | 182 ++++++- core/playwright/coar-notify.spec.ts | 4 +- core/prisma/seeds/coar-notify.ts | 444 +++++++++++++++++- .../src/app/components/NotificationCard.tsx | 87 +++- .../app/components/SendNotificationForm.tsx | 9 +- mock-notify/src/app/page.tsx | 4 +- .../reviews/sample-review/content/route.ts | 25 + .../app/reviews/sample-review/docmap/route.ts | 51 ++ .../src/app/reviews/sample-review/route.ts | 102 ++++ mock-notify/src/lib/payloads.ts | 16 + mock-notify/src/lib/store.ts | 45 +- .../contracts/src/resources/site-builder-2.ts | 3 +- site-builder-2/server/server.ts | 76 ++- 15 files changed, 973 insertions(+), 86 deletions(-) create mode 100644 mock-notify/src/app/reviews/sample-review/content/route.ts create mode 100644 mock-notify/src/app/reviews/sample-review/docmap/route.ts create mode 100644 mock-notify/src/app/reviews/sample-review/route.ts diff --git a/.gitignore b/.gitignore index ccc4f294c..38a05eafe 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,6 @@ storybook-static ./playwright .local_data + +# mock-notify store +.notifications.json diff --git a/core/actions/buildSite/action.ts b/core/actions/buildSite/action.ts index 93e18a49d..a3a7e371c 100644 --- a/core/actions/buildSite/action.ts +++ b/core/actions/buildSite/action.ts @@ -91,11 +91,17 @@ const schema = z.object({ transform: z .string() .describe("JSONata expression that outputs content for the page"), + headExtra: z + .string() + .optional() + .describe( + "JSONata expression for additional HTML to inject into (e.g. tags). Only applies to HTML pages." + ), extension: z .string() .default("html") .describe( - "File extension for the generated output (e.g., 'html', 'json', 'xml'). Only 'html' pages are wrapped in an HTML shell." + "File extension for the generated output (e.g., 'html', 'json', 'xml'). Only 'html' pages are wrapped in an HTML shell. If content starts with return interpolate(expression, data) } +/** + * Browser script injected into submission pages that dynamically fetches + * review content by following signposting links: + * review page → → DocMap JSON → web-content URL → content + */ +const SIGNPOSTING_FETCH_SCRIPT = ` diff --git a/site-builder/src/components/PubSidebar.astro b/site-builder/src/components/PubSidebar.astro deleted file mode 100644 index dd92604d1..000000000 --- a/site-builder/src/components/PubSidebar.astro +++ /dev/null @@ -1,56 +0,0 @@ ---- - - - - - - - - - - - - - - - -// Sidebar component with action buttons for the pub page ---- - -
-
- -
- -
- -
- -
- -
- -
- -
-
diff --git a/site-builder/src/components/Welcome.astro b/site-builder/src/components/Welcome.astro deleted file mode 100644 index f2d2e15fc..000000000 --- a/site-builder/src/components/Welcome.astro +++ /dev/null @@ -1,11 +0,0 @@ ---- - ---- - -
-
-
-

Welcome to the PubPub Site Builder

-
-
-
diff --git a/site-builder/src/layouts/Layout.astro b/site-builder/src/layouts/Layout.astro deleted file mode 100644 index 5a3e0a72b..000000000 --- a/site-builder/src/layouts/Layout.astro +++ /dev/null @@ -1,128 +0,0 @@ ---- - - - - - - - - - - - - - - - -import NavMenu from "../components/NavMenu" -import type { NonGenericProcessedPub } from "contracts" - -import { getFieldValue, getRelatedPubs } from "../lib/getValue" - -export interface Props { - journal: NonGenericProcessedPub - title?: string - header: NonGenericProcessedPub -} - -const journal = Astro.props.journal -const title = Astro.props.title -const header = Astro.props.header - -export type FileUpload = { - id: string - fileName: string - fileSource: string - fileType: string - fileSize: number | null - fileMeta: Record - fileUploadUrl: string - filePreview?: string -}[] - -const journalTitle = getFieldValue(journal, "Title") -const avatar = getFieldValue(journal, "Avatar") -const favicon = getFieldValue(journal, "Favicon") -const avatarUrl = avatar ? avatar[0]?.fileUploadUrl : "/favicon.svg" -const faviconUrl = favicon ? favicon[0]?.fileUploadUrl : "/favicon.svg" - -// Get navigation menus and links -const navigationTargets = getRelatedPubs(header, "Navigation Targets") - -// Filter navigation items by type - - -const accentColorDark = getFieldValue(journal, "Accent Color Dark") - -const getStringFieldValue = getFieldValue ---- - - - - - - - - - {title ? `${title} | ${journalTitle}` : journalTitle} - - -
-
- - - - -
- -
- Home - { - navigationTargets?.map((target) => { - if (!target) return null; - const pubType = target?.pubType?.name; - - if (pubType === "Navigation Link") { - const url = getStringFieldValue(target, "URL"); - if (!url) return null; - - return ( - - {target.title} - - ); - } else if (pubType === "Navigation Menu") { - return ; - } else { - const slug = getStringFieldValue(target, "Slug"); - if (!slug) return null; - - return ( - - {target.title} - - ); - } - }) - } -
-
-
-
- - - - - - diff --git a/site-builder/src/lib/client/client.ts b/site-builder/src/lib/client/client.ts deleted file mode 100644 index c7ac57597..000000000 --- a/site-builder/src/lib/client/client.ts +++ /dev/null @@ -1,20 +0,0 @@ -// import type { InitClientArgs } from "@ts-rest/core"; - -import { initClient } from "@ts-rest/core" - -import { siteApi } from "contracts" - -let client: ReturnType> - -export const getClient = () => { - if (!client) { - client = initClient(siteApi, { - baseUrl: `${import.meta.env.PUBPUB_URL}`, - baseHeaders: { - Authorization: `Bearer ${import.meta.env.AUTH_TOKEN}`, - }, - }) - } - - return client -} diff --git a/site-builder/src/lib/client/queries.ts b/site-builder/src/lib/client/queries.ts deleted file mode 100644 index 2803483a5..000000000 --- a/site-builder/src/lib/client/queries.ts +++ /dev/null @@ -1,124 +0,0 @@ -import { expect } from "utils/assert" - -import { SITE_ENV } from "../env/site" -import { getClient } from "./client" - -export const getPubType = async (name: string | string[]) => { - const nameArray = Array.isArray(name) ? name : [name] - - const pubTypeId = await getClient().pubTypes.getMany({ - params: { - communitySlug: SITE_ENV.COMMUNITY_SLUG, - }, - query: { name: nameArray }, - }) - - if (pubTypeId.status !== 200) { - throw new Error("Failed to fetch pub type") - } - - return expect(pubTypeId.body?.filter((pubType) => nameArray.includes(pubType.name))) -} - -export const getJournal = async (opts?: { - depth?: number - withRelatedPubs?: boolean - fieldSlugs?: string[] -}) => { - const journalPubType = await getPubType("Journal") - - const journal = await getClient().pubs.getMany({ - params: { - communitySlug: SITE_ENV.COMMUNITY_SLUG, - }, - query: { - pubTypeId: [expect(journalPubType[0].id)], - limit: 1, - depth: opts?.depth ?? 3, - withRelatedPubs: opts?.withRelatedPubs ?? true, - }, - }) - - if (journal.status !== 200) { - throw new Error("Failed to fetch journal") - } - - return expect(journal.body?.[0]) -} - -export const getPages = async ({ slugs }: { slugs?: string[] } = {}) => { - const pagePubType = await getPubType([ - "Page", - "Collection", - "Issue", - "Conference Proceedings", - "Book", - ]) - - const pages = await getClient().pubs.getMany({ - params: { - communitySlug: SITE_ENV.COMMUNITY_SLUG, - }, - query: { - pubTypeId: pagePubType.map((pubType) => pubType.id), - depth: 1, - limit: 200, - ...(slugs && { - filters: { - [`${SITE_ENV.COMMUNITY_SLUG}:slug`]: { - $in: slugs, - }, - }, - }), - }, - }) - - if (pages.status !== 200) { - throw new Error("Failed to fetch pages") - } - - return expect(pages.body) -} - -export const getHeader = async () => { - const headerPubType = await getPubType("Header") - - const header = await getClient().pubs.getMany({ - params: { - communitySlug: SITE_ENV.COMMUNITY_SLUG, - }, - query: { - pubTypeId: [expect(headerPubType[0].id)], - limit: 1, - depth: 3, - withRelatedPubs: true, - withPubType: true, - }, - }) - - if (header.status !== 200) { - throw new Error("Failed to fetch header") - } - - return expect(header.body?.[0]) -} - -export const getJournalArticles = async (opts?: { limit?: number }) => { - const journalArticlePubType = await getPubType("Journal Article") - - const journalArticles = await getClient().pubs.getMany({ - params: { - communitySlug: SITE_ENV.COMMUNITY_SLUG, - }, - query: { - pubTypeId: [expect(journalArticlePubType[0].id)], - limit: opts?.limit ?? 500, - }, - }) - - if (journalArticles.status !== 200) { - throw new Error("Failed to fetch journal articles") - } - - return expect(journalArticles.body) -} diff --git a/site-builder/src/lib/env/server.ts b/site-builder/src/lib/env/server.ts deleted file mode 100644 index bc3693f3f..000000000 --- a/site-builder/src/lib/env/server.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { createEnv } from "@t3-oss/env-core" -import { z } from "zod" - -export const SERVER_ENV = createEnv({ - server: { - PUBPUB_URL: z.string().url(), - S3_ENDPOINT: z.string().url().optional(), - S3_REGION: z.string(), - S3_ACCESS_KEY: z.string(), - S3_SECRET_KEY: z.string(), - S3_BUCKET_NAME: z.string(), - PORT: z.coerce.number().gte(1024).lte(65535), - }, - runtimeEnv: process.env, - emptyStringAsUndefined: true, -}) diff --git a/site-builder/src/lib/env/site.ts b/site-builder/src/lib/env/site.ts deleted file mode 100644 index cbcdb703c..000000000 --- a/site-builder/src/lib/env/site.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { createEnv } from "@t3-oss/env-core" -import { z } from "zod" - -export const SITE_ENV = createEnv({ - server: { - AUTH_TOKEN: z.string(), - PUBPUB_URL: z.string().url(), - COMMUNITY_SLUG: z.string(), - }, - runtimeEnv: import.meta.env, - emptyStringAsUndefined: true, -}) diff --git a/site-builder/src/lib/getValue.ts b/site-builder/src/lib/getValue.ts deleted file mode 100644 index 9de59a294..000000000 --- a/site-builder/src/lib/getValue.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { NonGenericProcessedPub } from "contracts" - -export const getFieldValue = (pub: NonGenericProcessedPub, fieldName: string) => { - if (!pub) { - throw new Error("Pub is undefined") - } - return pub.values?.find((value) => value.fieldName === fieldName)?.value as T -} - -export const getRelatedPubs = ( - pub: NonGenericProcessedPub | null | undefined, - fieldName: string -) => { - if (!pub) { - throw new Error("Pub is undefined") - } - return pub.values - ?.filter((value) => value.fieldName === fieldName) - .map((value) => value.relatedPub) -} diff --git a/site-builder/src/pages/[slug].astro b/site-builder/src/pages/[slug].astro deleted file mode 100644 index 900ce82c0..000000000 --- a/site-builder/src/pages/[slug].astro +++ /dev/null @@ -1,83 +0,0 @@ ---- - - - - - - - - - - - - - - - -import type { FileUpload } from "../layouts/Layout.astro" -import Layout from "../layouts/Layout.astro" - -import { getHeader, getJournal, getPages } from "../lib/client/queries" -import { getFieldValue } from "../lib/getValue" - -export async function getStaticPaths() { - const [pages, journal, header] = await Promise.all([ - getPages(), - getJournal({ - depth: 1, - withRelatedPubs: true, - }), - getHeader(), - ]) - - const paths = pages - .map((page) => { - const slug = getFieldValue(page, "Slug") - if (!slug || typeof slug !== "string") { - // console.log("Page has no slug", page); - return null - } - const layout = getFieldValue(page, "Layout") - if (!layout) { - // console.log("Page has no layout", page); - return null - } - - const isPublic = getFieldValue(page, "Is Public") - if (!isPublic) { - return null - } - - if (slug === "/") { - return null - } - - return { - params: { - slug, - }, - props: { page, journal, header }, - } - }) - .filter((page) => !!page && page.params.slug) - - return paths -} - -const { slug } = Astro.params - -const { page, journal, header } = Astro.props - -const title = getFieldValue(page, "Title") -const description = getFieldValue(page, "Description") -const layout = getFieldValue(page, "Layout") -const avatar = getFieldValue(page, "Avatar") - ---- - - -
-
-
- diff --git a/site-builder/src/pages/index.astro b/site-builder/src/pages/index.astro deleted file mode 100644 index 32a0c8b20..000000000 --- a/site-builder/src/pages/index.astro +++ /dev/null @@ -1,115 +0,0 @@ ---- - - - - - - - - - - - - - - - -import Layout from "../layouts/Layout.astro" -import { getHeader, getJournal, getPages , } from "../lib/client/queries" -import { getFieldValue, getRelatedPubs } from "../lib/getValue" - -const journal = await getJournal() -const header = await getHeader() - -// Welcome to Astro! Wondering what to do next? Check out the Astro documentation at https://docs.astro.build -// don't want to use any of this? delete everything in this file, the `assets`, `components`, and `layouts` directories, and start fresh. - -const heroText = getFieldValue(journal, "Hero Text") -const description = getFieldValue(journal, "Description") -const publishAs = getFieldValue(journal, "Publish As") -const publicationDate = getFieldValue(journal, "Publication Date") - -const page = await getPages({ - slugs: ["/"], -}) - -const layout = getFieldValue(page[0], "Layout") ---- - - -
-
-
-
-

- {journal.title} -

- {heroText &&

{heroText}

} - {description &&

{description}

} - {publishAs &&

Published by: {publishAs}

} - { - publicationDate && ( -

Publication Date: {publicationDate}

- ) - } - -
- Publication Date: { - publicationDate - ? new Date(publicationDate).toLocaleDateString("en-US", { - month: "long", - day: "numeric", - year: "numeric", - }) - : "Unknown" - } -
-
-
- -
-
-
- -
-

Journal Articles

- -
- { - getRelatedPubs(journal, "Journal Articles") - ?.slice(0, 20) - .map((article) => { - if (!article) { - return; - } - const slug = getFieldValue(article, "Slug"); - return ( - - ); - }) - } -
-
-
-
-
diff --git a/site-builder/src/pages/pub/[slug].astro b/site-builder/src/pages/pub/[slug].astro deleted file mode 100644 index 2d36ccabc..000000000 --- a/site-builder/src/pages/pub/[slug].astro +++ /dev/null @@ -1,121 +0,0 @@ ---- - - - - - - - - - - - - - - - -import type { FileUpload } from "../../layouts/Layout.astro" -import Layout from "../../layouts/Layout.astro" -import PubHeader from "../../components/PubHeader.astro" -import PubAbstract from "../../components/PubAbstract.astro" -import PubContent from "../../components/PubContent.astro" -import PubSidebar from "../../components/PubSidebar.astro" - -import { expect } from "utils/assert" - -import { getHeader, getJournal, getJournalArticles } from "../../lib/client/queries" -import { getFieldValue } from "../../lib/getValue" - -const { slug } = Astro.params - -export async function getStaticPaths() { - const [journalArticles, journal, header] = await Promise.all([ - getJournalArticles(), - getJournal({ - depth: 1, - withRelatedPubs: true, - }), - getHeader(), - ]) - - const paths = journalArticles.map((pub) => ({ - params: { - slug: expect(pub.values?.find((value) => value.fieldName === "Slug")?.value), - }, - props: { pub, journal, header }, - })) - - return paths -} - -const { pub, journal, header } = Astro.props - -const title = getFieldValue(pub, "Title") -const abstract = getFieldValue(pub, "Abstract") -const publicationDate = getFieldValue(pub, "Publication Date") -const lastEdited = getFieldValue(pub, "Last Edited") -const doi = getFieldValue(pub, "DOI") -const volume = getFieldValue(pub, "Issue Volume") -const issueNumber = getFieldValue(pub, "Issue Number") -const content = getFieldValue(pub, "PubContent") -const headerImage = getFieldValue(pub, "Header Background Image") -const headerTextStyle = getFieldValue(pub, "Header Text Style") -const headerTheme = getFieldValue(pub, "Header Theme") - -// Assuming contributors are in the values array -const contributors = pub.values?.filter((value) => value.fieldName === "Contributors") || [] - -// Calculate months since last update -const getMonthsAgo = (dateString: string) => { - if (!dateString) return "" - const lastEditDate = new Date(dateString) - const now = new Date() - const months = - (now.getFullYear() - lastEditDate.getFullYear()) * 12 + - now.getMonth() - - lastEditDate.getMonth() - return `${months} months ago` -} - -const formatDate = (dateString: string) => { - if (!dateString) return "" - return new Date(dateString).toLocaleDateString("en-US", { - month: "short", - day: "numeric", - year: "numeric", - }) -} ---- - - - - - - -
-
-
- -
- - -
- - -
- -
-
-
-
-
diff --git a/site-builder/src/styles/global.css b/site-builder/src/styles/global.css deleted file mode 100644 index f534382b5..000000000 --- a/site-builder/src/styles/global.css +++ /dev/null @@ -1,5 +0,0 @@ -@import "tailwindcss"; - -@import "@pubpub/tailwind/style.css"; - -@source "../../packages/ui/src/**/*.tsx"; diff --git a/site-builder/tsconfig.json b/site-builder/tsconfig.json deleted file mode 100644 index 3406d9cdf..000000000 --- a/site-builder/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": ["tsconfig/base.json", "astro/tsconfigs/strict"], - "include": [".astro/types.d.ts", "**/*"], - "exclude": ["dist"], - "compilerOptions": { - "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json", - "lib": ["ESNext"], - "types": ["node"], - "moduleResolution": "bundler" - } -} From 698fe39b6a8a97de2a5927a367bc2e8057cdb55d Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Mon, 13 Apr 2026 12:01:11 -0400 Subject: [PATCH 23/26] fix migration --- .github/workflows/ci.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00b61d8d7..dc1ffe7ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,9 +73,5 @@ jobs: if: matrix.task == 'test-run' run: pnpm test:setup - - name: Run migrations - if: matrix.task == 'test-run' - run: pnpm --filter core migrate-test - - name: Run task run: NODE_ENV=test pnpm ${{ matrix.task }} From b8708171973a8a0e5718ff97d574db743720b152 Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Mon, 13 Apr 2026 13:47:02 -0400 Subject: [PATCH 24/26] drop table if exists --- core/lib/__tests__/globalSetup.ts | 11 ++++++----- core/prisma/seed.ts | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/core/lib/__tests__/globalSetup.ts b/core/lib/__tests__/globalSetup.ts index 94cdb20c3..6790eda6f 100644 --- a/core/lib/__tests__/globalSetup.ts +++ b/core/lib/__tests__/globalSetup.ts @@ -21,15 +21,16 @@ export const setup = async () => { shell: true, stdio: "inherit", }) - const { stderr, error } = result - if (!error) { - logger.info("Database reset successful") - } else { + if (result.error) { logger.error( "Something went wrong while trying to reset the database before running tests." ) - throw error + throw result.error } + if (result.status !== 0) { + throw new Error(`Database reset failed with exit code ${result.status}`) + } + logger.info("Database reset successful") } export default setup diff --git a/core/prisma/seed.ts b/core/prisma/seed.ts index 1c27e0aff..32883f66b 100644 --- a/core/prisma/seed.ts +++ b/core/prisma/seed.ts @@ -35,7 +35,7 @@ async function main() { logger.info("drop existing jobs") await workerUtils.withPgClient(async (client) => { - await client.query(`DROP SCHEMA graphile_worker CASCADE`) + await client.query(`DROP SCHEMA IF EXISTS graphile_worker CASCADE`) }) await workerUtils.migrate() From a707cbd84911ee641ce50a7ebb5dc96f95ba1a9b Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Mon, 13 Apr 2026 14:07:01 -0400 Subject: [PATCH 25/26] now this --- core/lib/__tests__/globalSetup.ts | 11 +++++++++++ core/prisma/seed.ts | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/core/lib/__tests__/globalSetup.ts b/core/lib/__tests__/globalSetup.ts index 6790eda6f..4e699bd58 100644 --- a/core/lib/__tests__/globalSetup.ts +++ b/core/lib/__tests__/globalSetup.ts @@ -2,6 +2,7 @@ import { spawnSync } from "node:child_process" import { config } from "dotenv" +import { makeWorkerUtils } from "graphile-worker" import { logger } from "logger" export const setup = async () => { @@ -31,6 +32,16 @@ export const setup = async () => { throw new Error(`Database reset failed with exit code ${result.status}`) } logger.info("Database reset successful") + + // Ensure graphile_worker schema exists for tests that query worker tables directly. + // The seed should create this, but we ensure it here as a safety net. + logger.info("Ensuring graphile_worker schema...") + const workerUtils = await makeWorkerUtils({ + connectionString: process.env.DATABASE_URL!, + }) + await workerUtils.migrate() + await workerUtils.release() + logger.info("graphile_worker schema ready") } export default setup diff --git a/core/prisma/seed.ts b/core/prisma/seed.ts index 32883f66b..1c27e0aff 100644 --- a/core/prisma/seed.ts +++ b/core/prisma/seed.ts @@ -35,7 +35,7 @@ async function main() { logger.info("drop existing jobs") await workerUtils.withPgClient(async (client) => { - await client.query(`DROP SCHEMA IF EXISTS graphile_worker CASCADE`) + await client.query(`DROP SCHEMA graphile_worker CASCADE`) }) await workerUtils.migrate() From f832f5fe8f968d1d2d68e74609c36d713251f4ae Mon Sep 17 00:00:00 2001 From: Eric McDaniel Date: Mon, 13 Apr 2026 14:10:24 -0400 Subject: [PATCH 26/26] lint --- core/lib/__tests__/globalSetup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/lib/__tests__/globalSetup.ts b/core/lib/__tests__/globalSetup.ts index 4e699bd58..20e35886e 100644 --- a/core/lib/__tests__/globalSetup.ts +++ b/core/lib/__tests__/globalSetup.ts @@ -1,8 +1,8 @@ /* eslint-disable no-restricted-properties */ import { spawnSync } from "node:child_process" import { config } from "dotenv" - import { makeWorkerUtils } from "graphile-worker" + import { logger } from "logger" export const setup = async () => {