Skip to content

Commit

Permalink
Add hooks to handler (#405)
Browse files Browse the repository at this point in the history
* Add hooks to handler

* Make sure the entry properties are not mutated
  • Loading branch information
benmerckx authored Jan 20, 2025
1 parent 472b04e commit 6ac3775
Show file tree
Hide file tree
Showing 11 changed files with 164 additions and 68 deletions.
46 changes: 31 additions & 15 deletions src/adapter/next/handler.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import {
AvailableDrivers,
BackendOptions,
createBackend
} from 'alinea/backend/api/CreateBackend'
import {BackendOptions, createBackend} from 'alinea/backend/api/CreateBackend'
import {Backend} from 'alinea/backend/Backend'
import {createHandler as createCoreHandler} from 'alinea/backend/Handler'
import {
createHandler as createCoreHandler,
HandlerHooks
} from 'alinea/backend/Handler'
import {JWTPreviews} from 'alinea/backend/util/JWTPreviews'
import {cloudBackend} from 'alinea/cloud/CloudBackend'
import {Entry} from 'alinea/core/Entry'
Expand All @@ -15,16 +14,33 @@ import {devUrl, requestContext} from './context.js'
type Handler = (request: Request) => Promise<Response>
const handlers = new WeakMap<NextCMS, Handler>()

export function createHandler<Driver extends AvailableDrivers>(
export interface NextHandlerOptions extends HandlerHooks {
cms: NextCMS
backend?: BackendOptions | Backend
}

export function createHandler(cms: NextCMS): Handler
export function createHandler(options: NextHandlerOptions): Handler
/** @deprecated */
export function createHandler(
cms: NextCMS,
backend: BackendOptions<Driver> | Backend = cloudBackend(cms.config)
) {
if (handlers.has(cms)) return handlers.get(cms)!
const api = 'database' in backend ? createBackend(backend) : backend
const handleBackend = createCoreHandler(cms, api)
backend: BackendOptions | Backend
): Handler
export function createHandler(
input: NextCMS | NextHandlerOptions,
backend?: BackendOptions | Backend
): Handler {
const options = input instanceof NextCMS ? {cms: input, backend} : input
const providedBackend = options.backend ?? cloudBackend(options.cms)
if (handlers.has(options.cms)) return handlers.get(options.cms)!
const api =
'database' in providedBackend
? createBackend(providedBackend)
: providedBackend
const handleBackend = createCoreHandler({...options, backend: api})
const handle: Handler = async request => {
try {
const context = await requestContext(cms.config)
const context = await requestContext(options.cms.config)
const previews = new JWTPreviews(context.apiKey)
const {searchParams} = new URL(request.url)
const previewToken = searchParams.get('preview')
Expand All @@ -36,7 +52,7 @@ export function createHandler<Driver extends AvailableDrivers>(
const info = await previews.verify(previewToken)
const cookie = await cookies()
const connection = devUrl()
? await cms.connect()
? await options.cms.connect()
: handleBackend.connect(context)
const payload = getPreviewPayloadFromCookies(cookie.getAll())
const url = await connection.resolve({
Expand Down Expand Up @@ -65,6 +81,6 @@ export function createHandler<Driver extends AvailableDrivers>(
return new Response('Internal server error', {status: 500})
}
}
handlers.set(cms, handle)
handlers.set(options.cms, handle)
return handle
}
2 changes: 1 addition & 1 deletion src/adapter/test/TestCMS.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function createCMS<Definition extends Config>(
})
)
const handle = PLazy.from(async () => {
return createHandler(cms, await backend, db)
return createHandler({cms, backend: await backend, database: db})
})
const cms: CMS<Definition> = new CMS(config, async () => {
const {connect} = await handle
Expand Down
6 changes: 3 additions & 3 deletions src/backend/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,9 +346,9 @@ export class Database implements Syncable {
).then(r => rows.concat(r))
})
}
case MutationType.FileRemove:
case MutationType.RemoveFile:
if (mutation.replace) return
case MutationType.Remove: {
case MutationType.RemoveEntry: {
const statuses = await tx
.select()
.from(EntryRow)
Expand Down Expand Up @@ -376,7 +376,7 @@ export class Database implements Syncable {
)
return async () => [statuses[0].id]
}
case MutationType.Discard: {
case MutationType.RemoveDraft: {
const existing = await tx
.select()
.from(EntryRow)
Expand Down
87 changes: 75 additions & 12 deletions src/backend/Handler.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {Database} from 'alinea/backend/Database'
import {JWTPreviews} from 'alinea/backend/util/JWTPreviews'
import {cloudBackend} from 'alinea/cloud/CloudBackend'
import {Entry} from 'alinea/core'
import {CMS} from 'alinea/core/CMS'
import {Connection} from 'alinea/core/Connection'
import {Draft, DraftKey, formatDraftKey} from 'alinea/core/Draft'
import {AnyQueryResult, Graph, GraphQuery} from 'alinea/core/Graph'
import {ErrorCode, HttpError} from 'alinea/core/HttpError'
import {EditMutation, Mutation, MutationType} from 'alinea/core/Mutation'
import {Mutation, MutationType, UpdateMutation} from 'alinea/core/Mutation'
import {PreviewUpdate} from 'alinea/core/Preview'
import {getScope} from 'alinea/core/Scope'
import {decode} from 'alinea/core/util/BufferToBase64'
Expand Down Expand Up @@ -49,11 +50,31 @@ export interface HandlerWithConnect {
connect(context: RequestContext | AuthedContext): Connection
}

export function createHandler(
cms: CMS,
backend: Backend = cloudBackend(cms.config),
database = generatedStore.then(store => new Database(cms.config, store))
): HandlerWithConnect {
export type HookResponse<T = void> = void | T | Promise<T> | Promise<void>

export interface HandlerHooks {
beforeCreate?(entry: Entry): HookResponse<Entry>
afterCreate?(entry: Entry): HookResponse
beforeUpdate?(entry: Entry): HookResponse<Entry>
afterUpdate?(entry: Entry): HookResponse
beforeArchive?(entryId: string): HookResponse
afterArchive?(entryId: string): HookResponse
beforeRemove?(entryId: string): HookResponse
afterRemove?(entryId: string): HookResponse
}

export interface HandlerOptions extends HandlerHooks {
cms: CMS
backend?: Backend
database?: Promise<Database>
}

export function createHandler({
cms,
backend = cloudBackend(cms.config),
database = generatedStore.then(store => new Database(cms.config, store)),
...hooks
}: HandlerOptions): HandlerWithConnect {
const init = PLazy.from(async () => {
const db = await database
const previews = createPreviewParser(db)
Expand Down Expand Up @@ -115,23 +136,65 @@ export function createHandler(
mutations: Array<Mutation>,
retry = false
): Promise<{commitHash: string}> {
const changeSet = await changes.create(mutations)
let fromCommitHash: string = await db.meta().then(meta => meta.commitHash)
try {
for (const mutation of mutations) {
switch (mutation.type) {
case MutationType.Create: {
if (!hooks.beforeCreate) continue
const maybeEntry = await hooks.beforeCreate({...mutation.entry})
if (maybeEntry) mutation.entry.data = maybeEntry.data
continue
}
case MutationType.Edit: {
if (!hooks.beforeUpdate) continue
const maybeEntry = await hooks.beforeUpdate({...mutation.entry})
if (maybeEntry) mutation.entry.data = maybeEntry.data
continue
}
case MutationType.Archive: {
if (!hooks.beforeArchive) continue
await hooks.beforeArchive(mutation.entryId)
continue
}
case MutationType.RemoveEntry: {
if (!hooks.beforeRemove) continue
await hooks.beforeRemove(mutation.entryId)
continue
}
}
}
const changeSet = await changes.create(mutations)
const result = await backend.target.mutate(ctx, {
commitHash: fromCommitHash,
mutations: changeSet
})
await db.applyMutations(mutations, result.commitHash)
const tasks = []
for (const mutation of mutations) {
switch (mutation.type) {
case MutationType.Edit:
tasks.push(persistEdit(ctx, mutation))
case MutationType.Create: {
if (!hooks.afterCreate) continue
await hooks.afterCreate(mutation.entry)
continue
}
case MutationType.Edit: {
await persistEdit(ctx, mutation)
if (!hooks.afterUpdate) continue
await hooks.afterUpdate(mutation.entry)
continue
}
case MutationType.Archive: {
if (!hooks.afterArchive) continue
await hooks.afterArchive(mutation.entryId)
continue
}
case MutationType.RemoveEntry: {
if (!hooks.afterRemove) continue
await hooks.afterRemove(mutation.entryId)
continue
}
}
}
await Promise.all(tasks)
return {commitHash: result.commitHash}
} catch (error: any) {
if (retry) throw error
Expand All @@ -143,7 +206,7 @@ export function createHandler(
}
}

async function persistEdit(ctx: AuthedContext, mutation: EditMutation) {
async function persistEdit(ctx: AuthedContext, mutation: UpdateMutation) {
if (!mutation.update) return
const update = new Uint8Array(await decode(mutation.update))
const currentDraft = await backend.drafts.get(
Expand Down
29 changes: 21 additions & 8 deletions src/backend/api/CreateBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,31 @@ export type AvailableDrivers =
| 'sql.js'
| '@libsql/client'

export interface BackendOptions<Driver extends AvailableDrivers> {
type DatabaseClient<Driver extends AvailableDrivers> = Parameters<
(typeof driver)[Driver]
>[0]
type DatabaseOption<Driver extends AvailableDrivers> = {
driver: Driver
client: DatabaseClient<Driver>
}

export type DatabaseDeclaration =
| DatabaseOption<'d1'>
| DatabaseOption<'mysql2'>
| DatabaseOption<'@neondatabase/serverless'>
| DatabaseOption<'@vercel/postgres'>
| DatabaseOption<'pg'>
| DatabaseOption<'@electric-sql/pglite'>
| DatabaseOption<'sql.js'>
| DatabaseOption<'@libsql/client'>

export interface BackendOptions {
auth(username: string, password: string): boolean | Promise<boolean>
database: {
driver: Driver
client: Parameters<(typeof driver)[Driver]>[0]
}
database: DatabaseDeclaration
github: GithubOptions
}

export function createBackend<Driver extends AvailableDrivers>(
options: BackendOptions<Driver>
): Backend {
export function createBackend(options: BackendOptions): Backend {
const ghApi = githubApi(options.github)
const db = driver[options.database.driver](options.database.client)
const dbApi = databaseApi({...options, db, target: ghApi.target})
Expand Down
18 changes: 9 additions & 9 deletions src/backend/data/ChangeSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ import {Graph} from 'alinea/core/Graph'
import {
ArchiveMutation,
CreateMutation,
DiscardDraftMutation,
EditMutation,
FileRemoveMutation,
MoveMutation,
Mutation,
MutationType,
OrderMutation,
PatchMutation,
PublishMutation,
RemoveDraftMutation,
RemoveEntryMutation,
RemoveFileMutation,
UpdateMutation,
UploadMutation
} from 'alinea/core/Mutation'
import {Type} from 'alinea/core/Type'
Expand Down Expand Up @@ -73,7 +73,7 @@ const loader = JsonLoader
export class ChangeSetCreator {
constructor(protected config: Config, protected graph: Graph) {}

editChanges({previousFile, file, entry}: EditMutation): Array<Change> {
editChanges({previousFile, file, entry}: UpdateMutation): Array<Change> {
const type = this.config.schema[entry.type]
if (!type)
throw new Error(`Cannot publish entry of unknown type: ${entry.type}`)
Expand Down Expand Up @@ -190,7 +190,7 @@ export class ChangeSetCreator {
]
}

discardChanges({file}: DiscardDraftMutation): Array<Change> {
discardChanges({file}: RemoveDraftMutation): Array<Change> {
const fileEnd = `.${EntryStatus.Draft}.json`
if (!file.endsWith(fileEnd))
throw new Error(`Cannot discard non-draft file: ${file}`)
Expand Down Expand Up @@ -240,7 +240,7 @@ export class ChangeSetCreator {
return [{type: ChangeType.Upload, file: mutation.file, url: mutation.url}]
}

fileRemoveChanges(mutation: FileRemoveMutation): Array<Change> {
fileRemoveChanges(mutation: RemoveFileMutation): Array<Change> {
const mediaDir =
Workspace.data(this.config.workspaces[mutation.workspace])?.mediaDir ?? ''
const binaryLocation = join(mediaDir, mutation.location)
Expand All @@ -261,17 +261,17 @@ export class ChangeSetCreator {
return this.publishChanges(mutation)
case MutationType.Archive:
return this.archiveChanges(mutation)
case MutationType.Remove:
case MutationType.RemoveEntry:
return this.removeChanges(mutation)
case MutationType.Discard:
case MutationType.RemoveDraft:
return this.discardChanges(mutation)
case MutationType.Order:
return this.orderChanges(mutation)
case MutationType.Move:
return this.moveChanges(mutation)
case MutationType.Upload:
return this.fileUploadChanges(mutation)
case MutationType.FileRemove:
case MutationType.RemoveFile:
return this.fileRemoveChanges(mutation)
}
}
Expand Down
6 changes: 5 additions & 1 deletion src/cli/Serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,11 @@ export async function serve(options: ServeOptions): Promise<void> {
} else {
const history = new GitHistory(cms.config, rootDir)
const backend = createBackend()
const handleApi = createHandler(cms, backend, Promise.resolve(db))
const handleApi = createHandler({
cms,
backend,
database: Promise.resolve(db)
})
if (localServer) localServer.close()
localServer = createLocalServer(context, cms, handleApi, user)
currentCMS = cms
Expand Down
Loading

0 comments on commit 6ac3775

Please sign in to comment.