Skip to content

Commit

Permalink
add publish endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
harishv7 committed Jul 29, 2024
1 parent 724aeb1 commit 5478fe4
Show file tree
Hide file tree
Showing 8 changed files with 203 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Image from "next/image"
import NextLink from "next/link"
import { useParams } from "next/navigation"
import { Flex, HStack } from "@chakra-ui/react"
import {
AvatarMenu,
Expand All @@ -13,10 +14,13 @@ import { BiLinkExternal } from "react-icons/bi"
import { ADMIN_NAVBAR_HEIGHT } from "~/constants/layouts"
import { useMe } from "~/features/me/api"
import { DASHBOARD, SETTINGS_PROFILE } from "~/lib/routes"
import PublishButton from "./PublishButton"

export function AppNavbar(): JSX.Element {
const { me, logout } = useMe()

const pathParams = useParams()

return (
<Flex flex="0 0 auto" gridColumn="1/-1" height={ADMIN_NAVBAR_HEIGHT}>
<Flex
Expand Down Expand Up @@ -62,6 +66,10 @@ export function AppNavbar(): JSX.Element {
>
Report an issue
</Button>
<PublishButton
pageId={pathParams.pageId as string}
siteId={pathParams.siteId as string}
/>
<AvatarMenu
name={me.name}
variant="subtle"
Expand Down
53 changes: 53 additions & 0 deletions apps/studio/src/components/AppNavbar/PublishButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useToast } from "@chakra-ui/react"
import { Button } from "@opengovsg/design-system-react"

import { trpc } from "~/utils/trpc"

interface PublishButtonProps {
pageId?: string
siteId?: string
}

const PublishButton = ({ pageId, siteId }: PublishButtonProps): JSX.Element => {
const toast = useToast()
const { mutate, isLoading } = trpc.page.publishPage.useMutation({
onSuccess: (data) => {
if (data.versionId) {
toast({
status: "success",
title: "Page published successfully",
})
} else {
toast({
status: "error",
title: data.error,
})
}
},
onError: () => {
toast({
status: "error",
title: "Failed to publish page",
})
},
})

const handlePublish = () => {
const coercedSiteId = Number(siteId)
const coercedPageId = Number(pageId)
if (coercedSiteId && coercedPageId)
mutate({ pageId: coercedPageId, siteId: coercedSiteId })
}
return (
<Button
variant="solid"
size="sm"
onClick={handlePublish}
isLoading={isLoading}
>
Publish
</Button>
)
}

export default PublishButton
1 change: 1 addition & 0 deletions apps/studio/src/components/AppNavbar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./AppNavbar"
6 changes: 6 additions & 0 deletions apps/studio/src/schemas/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,9 @@ export const createPageSchema = z.object({
// NOTE: implies that top level pages are allowed
folderId: z.number().min(1).optional(),
})

// TODO: siteId should be taken from user's context (not input)
export const publishPageSchema = z.object({
pageId: z.number().min(1),
siteId: z.number().min(1),
})
18 changes: 16 additions & 2 deletions apps/studio/src/server/modules/page/page.router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { TRPCError } from "@trpc/server"
import Ajv from "ajv"
import { z } from "zod"

import { createPageSchema, getEditPageSchema } from "~/schemas/page"
import {
createPageSchema,
getEditPageSchema,
publishPageSchema,
} from "~/schemas/page"
import { protectedProcedure, router } from "~/server/trpc"
import { safeJsonParse } from "~/utils/safeJsonParse"
import { db, ResourceType } from "../database"
Expand All @@ -14,7 +18,7 @@ import {
getNavBar,
} from "../resource/resource.service"
import { getSiteConfig } from "../site/site.service"
import { createDefaultPage } from "./page.service"
import { addNewVersion, createDefaultPage } from "./page.service"

const ajv = new Ajv({ allErrors: true, strict: false, logger: false })
const schemaValidator = ajv.compile<IsomerSchema>(schema)
Expand Down Expand Up @@ -137,4 +141,14 @@ export const pageRouter = router({
return { pageId: resource.id }
},
),
publishPage: protectedProcedure
.input(publishPageSchema)
.mutation(async ({ input: { siteId, pageId } }) => {
/* Step 1: Update DB table to latest state */
// Create a new version
const addedVersionResult = await addNewVersion(siteId, pageId)
return addedVersionResult

/* TODO: Step 2: Use AWS SDK to start a CodeBuild */
}),
})
50 changes: 50 additions & 0 deletions apps/studio/src/server/modules/page/page.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { format } from "date-fns"

import { db } from "../database"
import { getPageById, updatePageById } from "../resource/resource.service"
import { createVersion, getVersionById } from "../version/version.service"

export const createDefaultPage = ({
title,
layout,
Expand Down Expand Up @@ -42,3 +46,49 @@ export const createDefaultPage = ({
}
}
}

export const addNewVersion = async (siteId: number, pageId: number) => {
return await db.transaction().execute(async (tx) => {
const page = await getPageById({ siteId, resourceId: pageId })

if (!page.draftBlobId) {
return { error: "No drafts to publish for this page" }
}

let newVersionNum = 1
if (page.versionId) {
const currentVersion = await getVersionById({
versionId: page.versionId,
})
newVersionNum = Number(currentVersion.versionNum) + 1
}

// Create the new version
// TODO: To pass in the tx object
const newVersion = await createVersion(
{
versionNum: newVersionNum,
resourceId: pageId,
blobId: Number(page.draftBlobId),
},
tx,
)

// Update resource with new versionId and draft to be null
await updatePageById({
props: {
page: {
...page,
id: page.id,
versionId: newVersion.versionId,
draftBlobId: null,
state: "Published",
},
siteId,
},
tx,
})

return { versionId: newVersion }
})
}
36 changes: 26 additions & 10 deletions apps/studio/src/server/modules/resource/resource.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { SelectExpression } from "kysely"
import type { SelectExpression, Transaction } from "kysely"
import { type DB } from "~prisma/generated/generatedTypes"

import { db } from "../database"
Expand Down Expand Up @@ -51,7 +51,7 @@ export const getFolders = () =>
.execute()

// NOTE: Base method for retrieving a resource - no distinction made on whether `blobId` exists
const getById = ({
export const getById = ({
resourceId,
siteId,
}: {
Expand Down Expand Up @@ -88,22 +88,38 @@ export const getFullPageById = async (args: {
.executeTakeFirst()
}

export const getPageById = (args: { resourceId: number; siteId: number }) => {
return getById(args)
.where("type", "is", "Page")
export const getPageById = (args: { resourceId: number; siteId: number }) =>
getById(args)
.where("type", "=", "Page")
.select(defaultResourceSelect)
.executeTakeFirstOrThrow()
}

export const updatePageById = (
page: Partial<Omit<Page, "id">> & { id: number },
) => {
export const updatePageById = ({
props: { page, siteId },
tx,
}: {
props: {
page: Partial<Omit<Page, "id">> & { id: string }
siteId: number
}
tx?: Transaction<DB>
}) => {
const { id, ...rest } = page
// TODO: cleanup logic by refactoring
if (tx) {
return tx
.updateTable("Resource")
.set(rest)
.where("id", "=", id)
.where("siteId", "=", siteId)
.executeTakeFirstOrThrow()
}
return db.transaction().execute((tx) => {
return tx
.updateTable("Resource")
.set(rest)
.where("id", "=", String(id))
.where("id", "=", id)
.where("siteId", "=", siteId)
.executeTakeFirstOrThrow()
})
}
Expand Down
43 changes: 43 additions & 0 deletions apps/studio/src/server/modules/version/version.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { SelectExpression, Transaction } from "kysely"
import { type DB } from "~prisma/generated/generatedTypes"

import { db } from "../database"

const defaultVersionSelect: SelectExpression<DB, "Version">[] = [
"Version.id",
"Version.versionNum",
"Version.resourceId",
"Version.blobId",
"Version.publishedAt",
]

export const getVersionById = ({ versionId }: { versionId: string }) =>
db
.selectFrom("Version")
.where("Version.id", "=", versionId)
.select(defaultVersionSelect)
.executeTakeFirstOrThrow()

export const createVersion = async (
props: {
versionNum: number
resourceId: number
blobId: number
},
tx?: Transaction<DB>,
) => {
const { versionNum, resourceId, blobId } = props
const instance = tx ? tx : db
const addedVersion = await instance
.insertInto("Version")
.values({
versionNum: String(versionNum),
resourceId: String(resourceId),
blobId: String(blobId),
publishedAt: new Date(),
})
.returning("Version.id")
.executeTakeFirstOrThrow()

return { versionId: addedVersion.id }
}

0 comments on commit 5478fe4

Please sign in to comment.