Skip to content

Commit

Permalink
Include locale in draft keys
Browse files Browse the repository at this point in the history
  • Loading branch information
benmerckx committed Dec 4, 2024
1 parent b27cdf9 commit 0698761
Show file tree
Hide file tree
Showing 13 changed files with 82 additions and 40 deletions.
5 changes: 3 additions & 2 deletions src/backend/Backend.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {Request, Response} from '@alinea/iso'
import {Connection} from 'alinea/core/Connection'
import {Draft} from 'alinea/core/Draft'
import {Draft, DraftKey} from 'alinea/core/Draft'
import {EntryRecord} from 'alinea/core/EntryRecord'
import {Mutation} from 'alinea/core/Mutation'
import {User} from 'alinea/core/User'
Expand Down Expand Up @@ -38,13 +38,14 @@ export interface Media {

export interface DraftTransport {
entryId: string
locale: string | null
commitHash: string
fileHash: string
draft: string
}

export interface Drafts {
get(ctx: RequestContext, entryId: string): Promise<Draft | undefined>
get(ctx: RequestContext, draftKey: DraftKey): Promise<Draft | undefined>
store(ctx: AuthedContext, draft: Draft): Promise<void>
}

Expand Down
6 changes: 5 additions & 1 deletion src/backend/Database.ts
Original file line number Diff line number Diff line change
Expand Up @@ -890,7 +890,11 @@ export class Database implements Syncable {
// Temporarily add preview entry
await tx
.delete(EntryRow)
.where(eq(EntryRow.id, entry.id), eq(EntryRow.active, true))
.where(
eq(EntryRow.id, entry.id),
is(EntryRow.locale, entry.locale),
eq(EntryRow.active, true)
)
await tx.insert(EntryRow).values(entry)
await Database.index(tx)
const result = await query(tx)
Expand Down
18 changes: 11 additions & 7 deletions src/backend/Handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {JWTPreviews} from 'alinea/backend/util/JWTPreviews'
import {cloudBackend} from 'alinea/cloud/CloudBackend'
import {CMS} from 'alinea/core/CMS'
import {Connection} from 'alinea/core/Connection'
import {Draft} from 'alinea/core/Draft'
import {Draft, DraftKey, formatDraftKey} from 'alinea/core/Draft'
import {AnyQueryResult, Graph, GraphQuery} from 'alinea/core/Graph'
import {EditMutation, Mutation, MutationType} from 'alinea/core/Mutation'
import {PreviewUpdate} from 'alinea/core/Preview'
Expand Down Expand Up @@ -143,18 +143,22 @@ export function createHandler(
async function persistEdit(ctx: AuthedContext, mutation: EditMutation) {
if (!mutation.update) return
const update = new Uint8Array(await decode(mutation.update))
const currentDraft = await backend.drafts.get(ctx, mutation.entryId)
const currentDraft = await backend.drafts.get(
ctx,
formatDraftKey(mutation.entry)
)
const updatedDraft = currentDraft
? mergeUpdatesV2([currentDraft.draft, update])
: update
const draft = {
entryId: mutation.entryId,
locale: mutation.locale,
fileHash: mutation.entry.fileHash,
draft: updatedDraft
}
await backend.drafts.store(ctx, draft)
const {contentHash} = await db.meta()
previews.setDraft(mutation.entryId, {contentHash, draft})
previews.setDraft(formatDraftKey(mutation.entry), {contentHash, draft})
}
})

Expand Down Expand Up @@ -200,8 +204,8 @@ export function createHandler(
revisionId
)
},
async getDraft(entryId: string) {
return backend.drafts.get(context as AuthedContext, entryId)
async getDraft(key) {
return backend.drafts.get(context as AuthedContext, key)
},
async storeDraft(draft: Draft) {
return backend.drafts.store(context as AuthedContext, draft)
Expand Down Expand Up @@ -349,8 +353,8 @@ export function createHandler(
if (action === HandleAction.Draft && request.method === 'GET') {
const ctx = await internal
expectJson()
const entryId = string(url.searchParams.get('entryId'))
const draft = await backend.drafts.get(ctx, entryId)
const key = string(url.searchParams.get('key')) as DraftKey
const draft = await backend.drafts.get(ctx, key)
return Response.json(
draft ? {...draft, draft: base64.stringify(draft.draft)} : null
)
Expand Down
28 changes: 20 additions & 8 deletions src/backend/api/DatabaseApi.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,35 @@
import {Drafts, Media, Pending, Target} from 'alinea/backend/Backend'
import {parseDraftKey} from 'alinea/core/Draft'
import {createId} from 'alinea/core/Id'
import {Mutation} from 'alinea/core/Mutation'
import {basename, extname} from 'alinea/core/util/Paths'
import {slugify} from 'alinea/core/util/Slugs'
import PLazy from 'p-lazy'
import {asc, Database, eq, gt, table} from 'rado'
import {asc, Database, eq, gt, primaryKey, table} from 'rado'
import {IsMysql, IsPostgres, IsSqlite} from 'rado/core/MetaData.js'
import * as column from 'rado/universal/columns'
import {HandleAction} from '../HandleAction.js'
import {is} from '../util/ORM.js'

export interface DatabaseOptions {
db: Database
target: Target
}

const Draft = table('alinea_draft', {
entryId: column.text().primaryKey(),
fileHash: column.text().notNull(),
draft: column.blob().notNull()
})
const Draft = table(
'alinea_draft',
{
entryId: column.text().notNull(),
locale: column.text(),
fileHash: column.text().notNull(),
draft: column.blob().notNull()
},
Draft => {
return {
primary: primaryKey(Draft.entryId, Draft.locale)
}
}
)

const Mutation = table('alinea_mutation', {
id: column.id(),
Expand All @@ -38,12 +49,13 @@ export function databaseApi(options: DatabaseOptions) {
return options.db
})
const drafts: Drafts = {
async get(ctx, entryId) {
async get(ctx, key) {
const {entryId, locale} = parseDraftKey(key)
const db = await setup
const found = await db
.select()
.from(Draft)
.where(eq(Draft.entryId, entryId))
.where(eq(Draft.entryId, entryId), is(Draft.locale, locale))
.get()
return found ?? undefined
},
Expand Down
18 changes: 10 additions & 8 deletions src/backend/resolver/ParsePreview.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
import {Entry} from 'alinea/core'
import {parseYDoc} from 'alinea/core/Doc'
import {Draft} from 'alinea/core/Draft'
import {Draft, DraftKey, formatDraftKey} from 'alinea/core/Draft'
import {PreviewRequest} from 'alinea/core/Preview'
import {decodePreviewPayload} from 'alinea/preview/PreviewPayload'
import * as Y from 'yjs'
import {Database} from '../Database.js'

export function createPreviewParser(db: Database) {
const drafts = new Map<
string,
DraftKey,
Promise<{contentHash: string; draft?: Draft}>
>()
return {
async parse(
preview: PreviewRequest,
sync: () => Promise<unknown>,
getDraft: (entryId: string) => Promise<Draft | undefined>
getDraft: (draftKey: DraftKey) => Promise<Draft | undefined>
): Promise<PreviewRequest | undefined> {
if (!(preview && 'payload' in preview)) return preview
const update = await decodePreviewPayload(preview.payload)
Expand All @@ -28,18 +28,20 @@ export function createPreviewParser(db: Database) {
first: true,
select: Entry,
id: update.entryId,
locale: update.locale,
status: 'preferDraft'
})
if (!entry) return
const cachedDraft = await drafts.get(update.entryId)
const key = formatDraftKey(entry)
const cachedDraft = await drafts.get(key)
let currentDraft: Draft | undefined
if (cachedDraft?.contentHash === meta.contentHash) {
currentDraft = cachedDraft.draft
} else {
try {
const pending = getDraft(update.entryId)
const pending = getDraft(key)
drafts.set(
update.entryId,
key,
pending.then(draft => ({contentHash: meta.contentHash, draft}))
)
currentDraft = await pending
Expand All @@ -57,8 +59,8 @@ export function createPreviewParser(db: Database) {
const entryData = parseYDoc(type, doc)
return {entry: {...entry, ...entryData, path: entry.path}}
},
setDraft(entryId: string, input: {contentHash: string; draft?: Draft}) {
drafts.set(entryId, Promise.resolve(input))
setDraft(key: DraftKey, input: {contentHash: string; draft?: Draft}) {
drafts.set(key, Promise.resolve(input))
}
}
}
10 changes: 7 additions & 3 deletions src/cloud/CloudBackend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
import {ChangeSet} from 'alinea/backend/data/ChangeSet'
import {router} from 'alinea/backend/router/Router'
import {Config} from 'alinea/core/Config'
import {formatDraftKey, parseDraftKey} from 'alinea/core/Draft'
import {HttpError} from 'alinea/core/HttpError'
import {outcome, Outcome, OutcomeJSON} from 'alinea/core/Outcome'
import {User} from 'alinea/core/User'
Expand Down Expand Up @@ -245,25 +246,28 @@ export function cloudBackend(config: Config): Backend {
}
}
const drafts: Drafts = {
async get(ctx, entryId) {
async get(ctx, key) {
if (!validApiKey(ctx.apiKey)) return
const {entryId, locale} = parseDraftKey(key)
type CloudDraft = {fileHash: string; update: string; commitHash: string}
const data = await parseOutcome<CloudDraft | null>(
fetch(cloudConfig.drafts + '/' + entryId, json({headers: bearer(ctx)}))
fetch(cloudConfig.drafts + '/' + key, json({headers: bearer(ctx)}))
)
return data?.update
? {
entryId,
locale,
commitHash: data.commitHash,
fileHash: data.fileHash,
draft: base64.parse(data.update)
}
: undefined
},
store(ctx, draft) {
const key = formatDraftKey({id: draft.entryId, locale: draft.locale})
return parseOutcome(
fetch(
cloudConfig.drafts + '/' + draft.entryId,
cloudConfig.drafts + '/' + key,
json({
method: 'PUT',
headers: bearer(ctx),
Expand Down
6 changes: 3 additions & 3 deletions src/core/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {HandleAction} from 'alinea/backend/HandleAction'
import {PreviewInfo} from 'alinea/backend/Previews'
import {Config} from './Config.js'
import {Connection, SyncResponse} from './Connection.js'
import {Draft} from './Draft.js'
import {Draft, DraftKey} from './Draft.js'
import {EntryRecord} from './EntryRecord.js'
import {AnyQueryResult, GraphQuery} from './Graph.js'
import {HttpError} from './HttpError.js'
Expand Down Expand Up @@ -120,8 +120,8 @@ export class Client implements Connection {

// Drafts

getDraft(entryId: string): Promise<Draft | undefined> {
return this.#requestJson({action: HandleAction.Draft, entryId})
getDraft(key: DraftKey): Promise<Draft | undefined> {
return this.#requestJson({action: HandleAction.Draft, key})
.then<DraftTransport | null>(this.#failOnHttpError)
.then(draft =>
draft ? {...draft, draft: base64.parse(draft.draft)} : undefined
Expand Down
4 changes: 2 additions & 2 deletions src/core/Connection.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Revision} from 'alinea/backend/Backend'
import {PreviewInfo} from 'alinea/backend/Previews'
import {ChangeSet} from 'alinea/backend/data/ChangeSet'
import {Draft} from './Draft.js'
import {Draft, DraftKey} from './Draft.js'
import {EntryRecord} from './EntryRecord.js'
import {EntryRow} from './EntryRow.js'
import {AnyQueryResult, GraphQuery} from './Graph.js'
Expand Down Expand Up @@ -31,7 +31,7 @@ export interface Connection extends Syncable {
file: string,
revisionId: string
): Promise<EntryRecord | undefined>
getDraft(entryId: string): Promise<Draft | undefined>
getDraft(draftKey: DraftKey): Promise<Draft | undefined>
storeDraft(draft: Draft): Promise<void>
}

Expand Down
15 changes: 15 additions & 0 deletions src/core/Draft.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
export interface Draft {
entryId: string
locale: string | null
fileHash: string
draft: Uint8Array
}

export type DraftKey = string & {__brand: 'DraftKey'}

export function formatDraftKey(entry: {
id: string
locale: string | null
}): DraftKey {
return `${entry.id}.${entry.locale ?? ''}` as DraftKey
}

export function parseDraftKey(key: DraftKey) {
const [entryId, locale] = key.split('.')
return {entryId, locale: locale || null}
}
5 changes: 3 additions & 2 deletions src/dashboard/atoms/EntryEditorAtoms.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {Config} from 'alinea/core/Config'
import {Connection} from 'alinea/core/Connection'
import {createYDoc, DOC_KEY, parseYDoc} from 'alinea/core/Doc'
import {formatDraftKey} from 'alinea/core/Draft'
import {Entry} from 'alinea/core/Entry'
import {EntryRow, EntryStatus} from 'alinea/core/EntryRow'
import {Field} from 'alinea/core/Field'
Expand Down Expand Up @@ -106,9 +107,9 @@ export const entryEditorAtoms = atomFamily(
locale: entry.locale
})
)

const key = formatDraftKey(entry)
const loadDraft = client
.getDraft(entryId)
.getDraft(key)
.then(draft => {
if (draft) {
edits.applyRemoteUpdate(draft.draft)
Expand Down
2 changes: 1 addition & 1 deletion src/field/code/CodeField.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const fields = type('Field', {
code: code('Code'),
disabled: code('Code (read-only)', {
readOnly: true,
initialValue: `console.log('Hello world!')`
initialValue: `console.info('Hello world!')`
})
}
})
Expand Down
3 changes: 1 addition & 2 deletions src/preview/ChunkCookieValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ export function chunkCookieValue(
batch = 0
while (position < cookieValue.length) {
const name = `${cookieName}-${batch}`
const prefix = `${name}=`
const amount = maxLength - prefix.length
const amount = maxLength - name.length
if (amount <= 0) throw new Error('Max length is not sufficient')
const value = cookieValue.substring(position, position + amount)
position += amount
Expand Down
2 changes: 1 addition & 1 deletion src/preview/PreviewCookies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export async function setPreviewCookies(
try {
const expiry = new Date(Date.now() + expiresIn)
for (const {name, value} of chunks)
document.cookie = `${name}=${value};expires=${expiry.toUTCString()}`
document.cookie = `${name}=${value};path=/;expires=${expiry.toUTCString()}`
return true
} catch {
return false
Expand Down

0 comments on commit 0698761

Please sign in to comment.