Skip to content

Commit

Permalink
Recompute all draft changes when updating any document
Browse files Browse the repository at this point in the history
  • Loading branch information
csansoon committed Jul 22, 2024
1 parent 504ae0b commit 2408379
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 194 deletions.
22 changes: 22 additions & 0 deletions packages/core/src/services/documents/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,26 @@ describe('createNewDocument', () => {
'Cannot create a document version in a merged commit',
)
})

it('modifies other documents if it is referenced by another document', async (ctx) => {
const { project } = await ctx.factories.createProject({
documents: {
main: '<ref prompt="referenced/doc" />',
},
})

const { commit } = await ctx.factories.createDraft({ project })
await createNewDocument({
commitId: commit.id,
path: 'referenced/doc',
})

const changes = await listCommitChanges({ commitId: commit.id }).then((r) =>
r.unwrap(),
)
expect(changes.length).toBe(2)
const changedDocsPahts = changes.map((c) => c.path)
expect(changedDocsPahts).toContain('main')
expect(changedDocsPahts).toContain('referenced/doc')
})
})
68 changes: 35 additions & 33 deletions packages/core/src/services/documents/create.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import {
DocumentVersion,
documentVersions,
getDocumentsAtCommit,
Result,
Transaction,
} from '@latitude-data/core'
import { DocumentVersion, Result } from '@latitude-data/core'
import { BadRequestError } from '$core/lib/errors'

import {
assertCommitIsEditable,
existsAnotherDocumentWithSamePath,
} from './utils'
getDraft,
getMergedAndDraftDocuments,
replaceCommitChanges,
resolveDocumentChanges,
} from './shared'

export async function createNewDocument({
commitId,
Expand All @@ -19,34 +15,40 @@ export async function createNewDocument({
commitId: number
path: string
}) {
const commitResult = await assertCommitIsEditable(commitId)
if (commitResult.error) return commitResult
try {
const draft = (await getDraft(commitId)).unwrap()

const [mergedDocuments, draftDocuments] = (
await getMergedAndDraftDocuments({
draft,
})
).unwrap()

const currentDocuments = await getDocumentsAtCommit({
commitId,
})
if (currentDocuments.error) return currentDocuments
if (path && draftDocuments.find((d) => d.path === path)) {
return Result.error(
new BadRequestError('A document with the same path already exists'),
)
}

if (
existsAnotherDocumentWithSamePath({
documents: currentDocuments.value,
draftDocuments.push({
path,
content: '',
} as DocumentVersion)

const documentsToUpdate = await resolveDocumentChanges({
originalDocuments: mergedDocuments,
newDocuments: draftDocuments,
})
) {
return Result.error(
new BadRequestError('A document with the same path already exists'),
)
}

return Transaction.call<DocumentVersion>(async (tx) => {
const result = await tx
.insert(documentVersions)
.values({
const newDraftDocuments = (
await replaceCommitChanges({
commitId,
path,
documentChanges: documentsToUpdate,
})
.returning()
const documentVersion = result[0]
return Result.ok(documentVersion!)
})
).unwrap()

return Result.ok(newDraftDocuments.find((d) => d.path === path)!)
} catch (error) {
return Result.error(error as Error)
}
}
130 changes: 130 additions & 0 deletions packages/core/src/services/documents/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { omit } from 'lodash-es'

import { readMetadata } from '@latitude-data/compiler'
import {
Commit,
DocumentVersion,
documentVersions,
findCommitById,
findHeadCommit,
getDocumentsAtCommit,
listCommitChanges,
Result,
Transaction,
TypedResult,
} from '@latitude-data/core'
import { ForbiddenError, LatitudeError } from '$core/lib/errors'
import { eq } from 'drizzle-orm'

export async function getDraft(
commitId: number,
): Promise<TypedResult<Commit, LatitudeError>> {
const commit = await findCommitById({ id: commitId })

if (commit.value?.mergedAt !== null) {
return Result.error(
new ForbiddenError('Cannot create a document version in a merged commit'),
)
}

return Result.ok(commit.value!)
}

export async function getMergedAndDraftDocuments({
draft,
}: {
draft: Commit
}): Promise<TypedResult<[DocumentVersion[], DocumentVersion[]], Error>> {
const headCommit = await findHeadCommit({ projectId: draft.projectId })
if (headCommit.error) return headCommit

const mergedDocuments = await getDocumentsAtCommit({
commitId: headCommit.value.id,
})
if (mergedDocuments.error) return mergedDocuments

const draftChanges = await listCommitChanges({ commitId: draft.id })
if (draftChanges.error) return Result.error(draftChanges.error)

const draftDocuments = mergedDocuments.value
.filter(
(d) => !draftChanges.value.find((c) => c.documentUuid === d.documentUuid),
)
.concat(draftChanges.value)

return Result.ok([mergedDocuments.value, structuredClone(draftDocuments)])
}

export function existsAnotherDocumentWithSamePath({
documents,
path,
}: {
documents: DocumentVersion[]
path: string
}) {
return documents.find((d) => d.path === path) !== undefined
}

export async function resolveDocumentChanges({
originalDocuments,
newDocuments,
}: {
originalDocuments: DocumentVersion[]
newDocuments: DocumentVersion[]
}): Promise<DocumentVersion[]> {
const getDocumentContent = async (path: string): Promise<string> => {
const document = newDocuments.find((d) => d.path === path)
if (!document) {
throw new Error(`Document not found`)
}
return document.content
}

const newDocumentsWithUpdatedHash = await Promise.all(
newDocuments.map(async (d) => ({
...d,
hash: await readMetadata({
prompt: d.content ?? '',
referenceFn: getDocumentContent,
}).then((m) => m.hash),
})),
)

return newDocumentsWithUpdatedHash.filter(
(newDoc) =>
!originalDocuments.find(
(oldDoc) =>
oldDoc.documentUuid === newDoc.documentUuid &&
oldDoc.hash === newDoc.hash &&
oldDoc.path === newDoc.path,
),
)
}

export async function replaceCommitChanges({
commitId,
documentChanges,
}: {
commitId: number
documentChanges: DocumentVersion[]
}): Promise<TypedResult<DocumentVersion[], Error>> {
return Transaction.call<DocumentVersion[]>(async (tx) => {
await tx
.delete(documentVersions)
.where(eq(documentVersions.commitId, commitId))

if (documentChanges.length === 0) return Result.ok([])

const insertedDocuments = await tx
.insert(documentVersions)
.values(
documentChanges.map((d) => ({
...omit(d, ['id', 'commitId', 'updatedAt']),
commitId,
})),
)
.returning()

return Result.ok(insertedDocuments)
})
}
51 changes: 48 additions & 3 deletions packages/core/src/services/documents/update.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { listCommitChanges } from '$core/data-access'
import { getDocumentsAtCommit, listCommitChanges } from '$core/data-access'
import { describe, expect, it } from 'vitest'

import { mergeCommit } from '../commits/merge'
Expand Down Expand Up @@ -92,6 +92,39 @@ describe('updateDocument', () => {
})

it('renaming a document creates a change to all other documents that reference it', async (ctx) => {
const { project } = await ctx.factories.createProject({
documents: {
referenced: {
doc: 'The document that is being referenced',
},
main: '<ref prompt="referenced/doc" />',
},
})

const { commit } = await ctx.factories.createDraft({ project })
const documents = await getDocumentsAtCommit({ commitId: commit.id }).then(
(r) => r.unwrap(),
)
const refDoc = documents.find((d) => d.path === 'referenced/doc')!

await updateDocument({
commitId: commit.id,
documentUuid: refDoc.documentUuid,
path: 'referenced/doc2',
}).then((r) => r.unwrap())

const changedDocuments = await listCommitChanges({
commitId: commit.id,
}).then((r) => r.unwrap())

expect(changedDocuments.length).toBe(2)
expect(
changedDocuments.find((d) => d.path === 'referenced/doc2'),
).toBeDefined()
expect(changedDocuments.find((d) => d.path === 'main')).toBeDefined()
})

it('undoing a change to a document removes it from the list of changed documents', async (ctx) => {
const { project } = await ctx.factories.createProject()
const { commit: commit1 } = await ctx.factories.createDraft({ project })
const { documentVersion: referencedDoc } =
Expand All @@ -112,7 +145,7 @@ describe('updateDocument', () => {
await updateDocument({
commitId: commit2.id,
documentUuid: referencedDoc.documentUuid,
path: 'referenced/doc2',
content: 'The document that is being referenced v2',
}).then((r) => r.unwrap())

const changedDocuments = await listCommitChanges({
Expand All @@ -121,8 +154,20 @@ describe('updateDocument', () => {

expect(changedDocuments.length).toBe(2)
expect(
changedDocuments.find((d) => d.path === 'referenced/doc2'),
changedDocuments.find((d) => d.path === 'referenced/doc'),
).toBeDefined()
expect(changedDocuments.find((d) => d.path === 'unmodified')).toBeDefined()

await updateDocument({
commitId: commit2.id,
documentUuid: referencedDoc.documentUuid,
content: referencedDoc.content, // Undo the change
}).then((r) => r.unwrap())

const changedDocuments2 = await listCommitChanges({
commitId: commit2.id,
}).then((r) => r.unwrap())

expect(changedDocuments2.length).toBe(0)
})
})
Loading

0 comments on commit 2408379

Please sign in to comment.