diff --git a/apps/web/src/actions/documents/create.ts b/apps/web/src/actions/documents/create.ts index 30579a416..e5d7dc3f8 100644 --- a/apps/web/src/actions/documents/create.ts +++ b/apps/web/src/actions/documents/create.ts @@ -1,7 +1,6 @@ 'use server' -import { createNewDocument } from '@latitude-data/core' -import { findCommit } from '$/app/(private)/_data-access' +import { CommitsRepository, createNewDocument } from '@latitude-data/core' import { z } from 'zod' import { withProject } from '../procedures' @@ -15,14 +14,15 @@ export const createDocumentVersionAction = withProject }), { type: 'json' }, ) - .handler(async ({ input }) => { - const commit = await findCommit({ - projectId: input.projectId, - uuid: input.commitUuid, - }) + .handler(async ({ input, ctx }) => { + const commit = await new CommitsRepository(ctx.project.workspaceId) + .getCommitByUuid({ uuid: input.commitUuid, projectId: ctx.project.id }) + .then((r) => r.unwrap()) + const result = await createNewDocument({ - commitId: commit.id, + commit, path: input.path, }) + return result.unwrap() }) diff --git a/apps/web/src/actions/documents/getContentByPath.ts b/apps/web/src/actions/documents/getContentByPath.ts index 49d1be1b3..f3ca95c6c 100644 --- a/apps/web/src/actions/documents/getContentByPath.ts +++ b/apps/web/src/actions/documents/getContentByPath.ts @@ -1,5 +1,6 @@ 'use server' +import { CommitsRepository } from '@latitude-data/core' import { getDocumentByPath } from '$/app/(private)/_data-access' import { z } from 'zod' @@ -14,9 +15,13 @@ export const getDocumentContentByPathAction = withProject }), { type: 'json' }, ) - .handler(async ({ input }) => { + .handler(async ({ input, ctx }) => { + const commitsScope = new CommitsRepository(ctx.project.workspaceId) + const commit = await commitsScope + .getCommitById(input.commitId) + .then((r) => r.unwrap()) const document = await getDocumentByPath({ - commitId: input.commitId, + commit, path: input.path, }) return document.content diff --git a/apps/web/src/actions/documents/updateContent.ts b/apps/web/src/actions/documents/updateContent.ts index eaf97f8fc..127f0c644 100644 --- a/apps/web/src/actions/documents/updateContent.ts +++ b/apps/web/src/actions/documents/updateContent.ts @@ -1,6 +1,10 @@ 'use server' -import { updateDocument } from '@latitude-data/core' +import { + CommitsRepository, + DocumentVersionsRepository, + updateDocument, +} from '@latitude-data/core' import { z } from 'zod' import { withProject } from '../procedures' @@ -15,11 +19,24 @@ export const updateDocumentContentAction = withProject }), { type: 'json' }, ) - .handler(async ({ input }) => { + .handler(async ({ input, ctx }) => { + const commitsScope = new CommitsRepository(ctx.project.workspaceId) + const commit = await commitsScope + .getCommitById(input.commitId) + .then((r) => r.unwrap()) + const docsScope = new DocumentVersionsRepository(ctx.project.workspaceId) + const document = await docsScope + .getDocumentByUuid({ + commit, + documentUuid: input.documentUuid, + }) + .then((r) => r.unwrap()) + const result = await updateDocument({ - commitId: input.commitId, - documentUuid: input.documentUuid, + commit, + document, content: input.content, }) + return result.unwrap() }) diff --git a/apps/web/src/actions/procedures/index.ts b/apps/web/src/actions/procedures/index.ts index 70c25bc8e..4a61fff93 100644 --- a/apps/web/src/actions/procedures/index.ts +++ b/apps/web/src/actions/procedures/index.ts @@ -1,4 +1,4 @@ -import { findProject } from '@latitude-data/core' +import { ProjectsRepository } from '@latitude-data/core' import { getCurrentUser } from '$/services/auth/getCurrentUser' import { z } from 'zod' import { createServerActionProcedure } from 'zsa' @@ -16,11 +16,11 @@ export const authProcedure = createServerActionProcedure().handler(async () => { export const withProject = createServerActionProcedure(authProcedure) .input(z.object({ projectId: z.number() })) .handler(async ({ input, ctx }) => { + const { workspace } = ctx + const projectScope = new ProjectsRepository(workspace.id) const project = ( - await findProject({ - projectId: input.projectId, - workspaceId: ctx.workspace.id, - }) + await projectScope.getProjectById(input.projectId) ).unwrap() + return { ...ctx, project } }) diff --git a/apps/web/src/actions/user/setupAction.ts b/apps/web/src/actions/user/setupAction.ts index 48588a4d5..1d8570d22 100644 --- a/apps/web/src/actions/user/setupAction.ts +++ b/apps/web/src/actions/user/setupAction.ts @@ -24,7 +24,6 @@ export const setupAction = createServerAction() ) .handler(async ({ input }) => { const itWasAlreadySetup = await isWorkspaceCreated() - if (itWasAlreadySetup) { throw new Error('Workspace already created') } diff --git a/apps/web/src/app/(private)/_data-access/index.ts b/apps/web/src/app/(private)/_data-access/index.ts index 8523bb865..ac7bb2855 100644 --- a/apps/web/src/app/(private)/_data-access/index.ts +++ b/apps/web/src/app/(private)/_data-access/index.ts @@ -1,20 +1,19 @@ import { cache } from 'react' import { - getDocumentAtCommit, + Commit, + CommitsRepository, + DocumentVersionsRepository, + findWorkspaceFromCommit, NotFoundError, - findCommitByUuid as originalfindCommit, - findProject as originalFindProject, - getDocumentsAtCommit as originalGetDocumentsAtCommit, - getFirstProject as originalGetFirstProject, - type FindCommitByUuidProps, - type FindProjectProps, - type GetDocumentAtCommitProps, + Project, + ProjectsRepository, } from '@latitude-data/core' export const getFirstProject = cache( async ({ workspaceId }: { workspaceId: number }) => { - const result = await originalGetFirstProject({ workspaceId }) + const projectsScope = new ProjectsRepository(workspaceId) + const result = await projectsScope.getFirstProject() const project = result.unwrap() return project @@ -22,8 +21,15 @@ export const getFirstProject = cache( ) export const findProject = cache( - async ({ projectId, workspaceId }: FindProjectProps) => { - const result = await originalFindProject({ projectId, workspaceId }) + async ({ + projectId, + workspaceId, + }: { + projectId: number + workspaceId: number + }) => { + const projectsScope = new ProjectsRepository(workspaceId) + const result = await projectsScope.getProjectById(projectId) const project = result.unwrap() return project @@ -31,8 +37,12 @@ export const findProject = cache( ) export const findCommit = cache( - async ({ uuid, projectId }: FindCommitByUuidProps) => { - const result = await originalfindCommit({ uuid, projectId }) + async ({ uuid, project }: { uuid: string; project: Project }) => { + const commitsScope = new CommitsRepository(project.workspaceId) + const result = await commitsScope.getCommitByUuid({ + projectId: project.id, + uuid, + }) const commit = result.unwrap() return commit @@ -40,23 +50,32 @@ export const findCommit = cache( ) export const getDocumentByUuid = cache( - async ({ documentUuid, commitId }: GetDocumentAtCommitProps) => { - const result = await getDocumentAtCommit({ documentUuid, commitId }) - const document = result.unwrap() + async ({ + documentUuid, + commit, + }: { + documentUuid: string + commit: Commit + }) => { + const workspace = await findWorkspaceFromCommit(commit) + const scope = new DocumentVersionsRepository(workspace!.id) + const result = await scope.getDocumentAtCommit({ documentUuid, commit }) - return document + return result.unwrap() }, ) export const getDocumentByPath = cache( - async ({ commitId, path }: { commitId: number; path: string }) => { - const documents = ( - await originalGetDocumentsAtCommit({ commitId }) - ).unwrap() + async ({ commit, path }: { commit: Commit; path: string }) => { + const workspace = await findWorkspaceFromCommit(commit) + const docsScope = new DocumentVersionsRepository(workspace!.id) + const documents = await docsScope + .getDocumentsAtCommit(commit) + .then((r) => r.unwrap()) + const document = documents.find((d) => d.path === path) - if (!document) { - throw new NotFoundError('Document not found') - } + if (!document) throw new NotFoundError('Document not found') + return document }, ) diff --git a/apps/web/src/app/(private)/api/v1/projects/[projectId]/commits/[commitUuid]/[...documentPath]/route.test.ts b/apps/web/src/app/(private)/api/v1/projects/[projectId]/commits/[commitUuid]/[...documentPath]/route.test.ts index f09e866cf..d98a4534f 100644 --- a/apps/web/src/app/(private)/api/v1/projects/[projectId]/commits/[commitUuid]/[...documentPath]/route.test.ts +++ b/apps/web/src/app/(private)/api/v1/projects/[projectId]/commits/[commitUuid]/[...documentPath]/route.test.ts @@ -1,6 +1,6 @@ import { HEAD_COMMIT, mergeCommit } from '@latitude-data/core' +import { LatitudeRequest } from '$/middleware' import useTestDatabase from '$core/tests/useTestDatabase' -import { NextRequest } from 'next/server' import { describe, expect, test } from 'vitest' import { GET } from './route' @@ -10,26 +10,26 @@ useTestDatabase() describe('GET documentVersion', () => { test('returns the document by path', async (ctx) => { const { project } = await ctx.factories.createProject() - const { commit } = await ctx.factories.createDraft({ project }) + let { commit } = await ctx.factories.createDraft({ project }) const { documentVersion: doc } = await ctx.factories.createDocumentVersion({ commit, }) - await mergeCommit({ commitId: commit.id }) - - const response = await GET( - new NextRequest( - 'http://localhost/api/projects/projectId/commits/commitUuid/path/to/doc', - ), - { - params: { - projectId: project.id, - commitUuid: commit.uuid, - documentPath: doc.path.split('/'), - }, - }, + commit = await mergeCommit(commit).then((r) => r.unwrap()) + const req = new LatitudeRequest( + 'http://localhost/api/projects/projectId/commits/commitUuid/path/to/doc', ) + req.workspaceId = project.workspaceId + + const response = await GET(req, { + params: { + projectId: project.id, + commitUuid: commit.uuid, + documentPath: doc.path.split('/'), + }, + }) + expect(response.status).toBe(200) const responseDoc = await response.json() expect(responseDoc.documentUuid).toEqual(doc.documentUuid) @@ -38,25 +38,24 @@ describe('GET documentVersion', () => { test('returns the document in main branch if commitUuid is HEAD', async (ctx) => { const { project } = await ctx.factories.createProject() - const { commit } = await ctx.factories.createDraft({ project }) + let { commit } = await ctx.factories.createDraft({ project }) const { documentVersion: doc } = await ctx.factories.createDocumentVersion({ commit, }) - await mergeCommit({ commitId: commit.id }) - - const response = await GET( - new NextRequest( - 'http://localhost/api/projects/projectId/commits/HEAD/path/to/doc', - ), - { - params: { - projectId: project.id, - commitUuid: HEAD_COMMIT, - documentPath: doc.path.split('/'), - }, - }, + commit = await mergeCommit(commit).then((r) => r.unwrap()) + const req = new LatitudeRequest( + 'http://localhost/api/projects/projectId/commits/commitUuid/path/to/doc', ) + req.workspaceId = project.workspaceId + + const response = await GET(req, { + params: { + projectId: project.id, + commitUuid: HEAD_COMMIT, + documentPath: doc.path.split('/'), + }, + }) expect(response.status).toBe(200) const responseDoc = await response.json() @@ -66,22 +65,21 @@ describe('GET documentVersion', () => { test('returns 404 if document is not found', async (ctx) => { const { project } = await ctx.factories.createProject() - const { commit } = await ctx.factories.createDraft({ project }) + let { commit } = await ctx.factories.createDraft({ project }) - await mergeCommit({ commitId: commit.id }) - - const response = await GET( - new NextRequest( - 'http://localhost/api/projects/projectId/commits/commitUuid/path/to/doc', - ), - { - params: { - projectId: project.id, - commitUuid: commit.uuid, - documentPath: ['path', 'to', 'doc'], - }, - }, + commit = await mergeCommit(commit).then((r) => r.unwrap()) + const req = new LatitudeRequest( + 'http://localhost/api/projects/projectId/commits/commitUuid/path/to/doc', ) + req.workspaceId = project.workspaceId + + const response = await GET(req, { + params: { + projectId: project.id, + commitUuid: commit.uuid, + documentPath: ['path', 'to', 'doc'], + }, + }) expect(response.status).toBe(404) }) @@ -93,18 +91,18 @@ describe('GET documentVersion', () => { commit, }) - const response = await GET( - new NextRequest( - 'http://localhost/api/projects/projectId/commits/commitUuid/path/to/doc', - ), - { - params: { - projectId: project.id, - commitUuid: commit.uuid, - documentPath: doc.path.split('/'), - }, - }, + const req = new LatitudeRequest( + 'http://localhost/api/projects/projectId/commits/commitUuid/path/to/doc', ) + req.workspaceId = project.workspaceId + + const response = await GET(req, { + params: { + projectId: project.id, + commitUuid: commit.uuid, + documentPath: doc.path.split('/'), + }, + }) expect(response.status).toBe(200) const responseDoc = await response.json() diff --git a/apps/web/src/app/(private)/api/v1/projects/[projectId]/commits/[commitUuid]/[...documentPath]/route.ts b/apps/web/src/app/(private)/api/v1/projects/[projectId]/commits/[commitUuid]/[...documentPath]/route.ts index bcbd259c7..acdfa3a24 100644 --- a/apps/web/src/app/(private)/api/v1/projects/[projectId]/commits/[commitUuid]/[...documentPath]/route.ts +++ b/apps/web/src/app/(private)/api/v1/projects/[projectId]/commits/[commitUuid]/[...documentPath]/route.ts @@ -1,9 +1,12 @@ -import { getDocumentByPath } from '@latitude-data/core' +import { + CommitsRepository, + DocumentVersionsRepository, +} from '@latitude-data/core' import apiRoute from '$/helpers/api/route' -import { NextRequest } from 'next/server' +import { LatitudeRequest } from '$/middleware' export async function GET( - _: NextRequest, + req: LatitudeRequest, { params, }: { @@ -15,11 +18,15 @@ export async function GET( }, ) { return apiRoute(async () => { - const { projectId, commitUuid, documentPath } = params - - const result = await getDocumentByPath({ - projectId: Number(projectId), - commitUuid, + const workspaceId = req.workspaceId! + const { commitUuid, projectId, documentPath } = params + const commitsScope = new CommitsRepository(workspaceId) + const commit = await commitsScope + .getCommitByUuid({ uuid: commitUuid, projectId }) + .then((r) => r.unwrap()) + const documentVersionsScope = new DocumentVersionsRepository(workspaceId) + const result = await documentVersionsScope.getDocumentByPath({ + commit, path: documentPath.join('/'), }) const document = result.unwrap() diff --git a/apps/web/src/app/(private)/page.tsx b/apps/web/src/app/(private)/page.tsx index 5f57c6971..66b548e30 100644 --- a/apps/web/src/app/(private)/page.tsx +++ b/apps/web/src/app/(private)/page.tsx @@ -15,7 +15,9 @@ export default async function AppRoot() { try { session = await getCurrentUser() project = await getFirstProject({ workspaceId: session.workspace.id }) - await findCommit({ uuid: HEAD_COMMIT, projectId: project.id }) + + await findCommit({ uuid: HEAD_COMMIT, project }) + url = PROJECT_ROUTE({ id: project.id }).commits.latest } catch (error) { if (error instanceof NotFoundError) { diff --git a/apps/web/src/app/(private)/projects/[projectId]/page.tsx b/apps/web/src/app/(private)/projects/[projectId]/page.tsx index 428460402..0cc279af0 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/page.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/page.tsx @@ -18,10 +18,10 @@ export default async function ProjectPage({ params }: ProjectPageParams) { try { session = await getCurrentUser() project = await findProject({ - projectId: params.projectId, + projectId: Number(params.projectId), workspaceId: session.workspace.id, }) - await findCommit({ uuid: HEAD_COMMIT, projectId: project.id }) + await findCommit({ uuid: HEAD_COMMIT, project }) url = PROJECT_ROUTE({ id: +project.id }).commits.latest } catch (error) { if (error instanceof NotFoundError) { diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/_components/Sidebar/index.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/_components/Sidebar/index.tsx index be0c5273b..321e171d6 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/_components/Sidebar/index.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/_components/Sidebar/index.tsx @@ -1,6 +1,10 @@ import { Suspense } from 'react' -import { Commit, getDocumentsAtCommit } from '@latitude-data/core' +import { + Commit, + DocumentVersionsRepository, + findWorkspaceFromCommit, +} from '@latitude-data/core' import { DocumentSidebar } from '@latitude-data/web-ui' import ClientFilesTree from './ClientFilesTree' @@ -14,7 +18,9 @@ export default async function Sidebar({ documentPath?: string documentUuid?: string }) { - const documents = await getDocumentsAtCommit({ commitId: commit.id }) + const workspace = await findWorkspaceFromCommit(commit) + const docsScope = new DocumentVersionsRepository(workspace!.id) + const documents = await docsScope.getDocumentsAtCommit(commit) return ( Loading...}> diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/layout.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/layout.tsx index 0bfbc02c3..9886ee479 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/layout.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/layout.tsx @@ -1,7 +1,11 @@ import React from 'react' import { DocumentDetailWrapper } from '@latitude-data/web-ui' -import { findCommit, getDocumentByUuid } from '$/app/(private)/_data-access' +import { + findCommit, + findProject, + getDocumentByUuid, +} from '$/app/(private)/_data-access' import Sidebar from '../../_components/Sidebar' @@ -12,13 +16,18 @@ export default async function DocumentLayout({ children: React.ReactNode params: { projectId: string; commitUuid: string; documentUuid: string } }) { - const commit = await findCommit({ + const workspaceId = Number(sessionStorage.getItem('workspaceId')!) + const project = await findProject({ projectId: Number(params.projectId), + workspaceId, + }) + const commit = await findCommit({ + project, uuid: params.commitUuid, }) const document = await getDocumentByUuid({ documentUuid: params.documentUuid, - commitId: commit.id, + commit, }) return ( diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/page.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/page.tsx index 8203cfadf..21dc6e207 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/page.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/documents/[documentUuid]/page.tsx @@ -1,4 +1,8 @@ -import { findCommit, getDocumentByUuid } from '$/app/(private)/_data-access' +import { + findCommit, + findProject, + getDocumentByUuid, +} from '$/app/(private)/_data-access' import ClientDocumentEditor from './_components/DocumentEditor' @@ -7,13 +11,18 @@ export default async function DocumentPage({ }: { params: { projectId: string; commitUuid: string; documentUuid: string } }) { - const commit = await findCommit({ + const workspaceId = Number(sessionStorage.getItem('workspaceId')!) + const project = await findProject({ projectId: Number(params.projectId), + workspaceId, + }) + const commit = await findCommit({ + project, uuid: params.commitUuid, }) const document = await getDocumentByUuid({ documentUuid: params.documentUuid, - commitId: commit.id, + commit, }) return } diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/layout.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/layout.tsx index 6125c3fd9..4ab0344a7 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/layout.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/layout.tsx @@ -2,13 +2,14 @@ import { ReactNode } from 'react' import { Commit, + CommitsRepository, HEAD_COMMIT, NotFoundError, Project, + ProjectsRepository, } from '@latitude-data/core' import { CommitProvider, ProjectProvider } from '@latitude-data/web-ui' import { AppLayout, BreadcrumpBadge } from '@latitude-data/web-ui/browser' -import { findCommit, findProject } from '$/app/(private)/_data-access' import { NAV_LINKS } from '$/app/(private)/_lib/constants' import { ProjectPageParams } from '$/app/(private)/projects/[projectId]/page' import { getCurrentUser, SessionData } from '$/services/auth/getCurrentUser' @@ -29,18 +30,20 @@ export default async function CommitLayout({ let commit: Commit try { session = await getCurrentUser() - project = await findProject({ - projectId: params.projectId, - workspaceId: session.workspace.id, - }) - commit = await findCommit({ - uuid: params.commitUuid, - projectId: project.id, - }) + const projectsRepo = new ProjectsRepository(session.workspace.id) + const commitsRepo = new CommitsRepository(session.workspace.id) + project = ( + await projectsRepo.getProjectById(Number(params.projectId)) + ).unwrap() + commit = ( + await commitsRepo.getCommitByUuid({ + uuid: params.commitUuid, + projectId: project.id, + }) + ).unwrap() } catch (error) { - if (error instanceof NotFoundError) { - return notFound() - } + if (error instanceof NotFoundError) return notFound() + throw error } diff --git a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/page.tsx b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/page.tsx index b0f604ee6..bdaf81994 100644 --- a/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/page.tsx +++ b/apps/web/src/app/(private)/projects/[projectId]/versions/[commitUuid]/page.tsx @@ -1,5 +1,5 @@ import { DocumentDetailWrapper } from '@latitude-data/web-ui' -import { findCommit } from '$/app/(private)/_data-access' +import { findCommit, findProject } from '$/app/(private)/_data-access' import Sidebar from './_components/Sidebar' @@ -10,8 +10,13 @@ export default async function CommitRoot({ }: { params: { projectId: string; commitUuid: string } }) { - const commit = await findCommit({ + const workspaceId = Number(sessionStorage.getItem('workspaceId')!) + const project = await findProject({ projectId: Number(params.projectId), + workspaceId, + }) + const commit = await findCommit({ + project, uuid: params.commitUuid, }) return ( diff --git a/apps/web/src/app/api/projects/[projectId]/commits/[commitUuid]/documents/route.ts b/apps/web/src/app/api/projects/[projectId]/commits/[commitUuid]/documents/route.ts new file mode 100644 index 000000000..5c7984845 --- /dev/null +++ b/apps/web/src/app/api/projects/[projectId]/commits/[commitUuid]/documents/route.ts @@ -0,0 +1,28 @@ +import { + CommitsRepository, + DocumentVersionsRepository, +} from '@latitude-data/core' +import { LatitudeRequest } from '$/middleware' +import { NextResponse } from 'next/server' + +export async function GET( + req: LatitudeRequest, + { + params: { commitUuid, projectId }, + }: { params: { commitUuid: string; projectId: number } }, +) { + try { + const workspaceId = req.workspaceId! + const scope = new DocumentVersionsRepository(workspaceId) + const commitsScope = new CommitsRepository(workspaceId) + const commit = await commitsScope + .getCommitByUuid({ uuid: commitUuid, projectId }) + .then((r) => r.unwrap()) + const documents = await scope.getDocumentsAtCommit(commit) + + return NextResponse.json(documents.unwrap()) + } catch (err: unknown) { + const error = err as Error + return NextResponse.json({ error: error.message }, { status: 500 }) + } +} diff --git a/apps/web/src/components/Sidebar/index.tsx b/apps/web/src/components/Sidebar/index.tsx index 2116b4275..aeee736b6 100644 --- a/apps/web/src/components/Sidebar/index.tsx +++ b/apps/web/src/components/Sidebar/index.tsx @@ -1,20 +1,31 @@ -import { getDocumentsAtCommit } from '@latitude-data/core' -import { findCommit } from '$/app/(private)/_data-access' +import { + CommitsRepository, + DocumentVersionsRepository, + Project, +} from '@latitude-data/core' import DocumentTree, { CreateNode } from './DocumentTree' export default async function Sidebar({ commitUuid, - projectId, + project, }: { commitUuid: string - projectId: number + project: Project }) { - const commit = await findCommit({ projectId, uuid: commitUuid }) - const documentsResult = await getDocumentsAtCommit({ - commitId: commit.id, - }) - const documents = documentsResult.unwrap() + const commitsScope = new CommitsRepository(project.workspaceId) + const commit = await commitsScope + .getCommitByUuid({ + uuid: commitUuid, + projectId: project.id, + }) + .then((r) => r.unwrap()) + const documentVersionsScope = new DocumentVersionsRepository( + project.workspaceId, + ) + const documents = await documentVersionsScope + .getDocumentsAtCommit(commit) + .then((r) => r.unwrap()) return (
diff --git a/apps/web/src/data-access/users.ts b/apps/web/src/data-access/users.ts index a5f474478..0cccc55d1 100644 --- a/apps/web/src/data-access/users.ts +++ b/apps/web/src/data-access/users.ts @@ -1,8 +1,8 @@ import { database, - getUser, NotFoundError, Result, + unsafelyGetUser, users, verifyPassword, type PromisedResult, @@ -31,15 +31,12 @@ export async function getUserFromCredentials({ }, where: eq(users.email, email), }) - if (!user) return notFound() const validPassword = await verifyPassword(password, user.encryptedPassword) - if (!validPassword) notFound() const wpResult = await getWorkspace({ userId: user.id }) - if (wpResult.error) { return Result.error(wpResult.error) } @@ -60,7 +57,7 @@ export async function getCurrentUserFromDB({ }: { userId: string | undefined }): PromisedResult { - const user = await getUser(userId) + const user = await unsafelyGetUser(userId) if (!user) return notFound() const wpResult = await getWorkspace({ userId: user.id }) diff --git a/apps/web/src/env.ts b/apps/web/src/env.ts index d8c36bbf4..3dd7f25cc 100644 --- a/apps/web/src/env.ts +++ b/apps/web/src/env.ts @@ -4,7 +4,8 @@ import { createEnv } from '@t3-oss/env-nextjs' import { z } from 'zod' export default createEnv({ - skipValidation: process.env.BUILDING_CONTAINER == 'true', + skipValidation: + process.env.BUILDING_CONTAINER == 'true' || process.env.NODE_ENV === 'test', server: { DATABASE_URL: z.string(), REDIS_HOST: z.string(), diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts index d1a713e89..8bf0ca80f 100644 --- a/apps/web/src/middleware.ts +++ b/apps/web/src/middleware.ts @@ -1,4 +1,4 @@ -import { getApiKey } from '@latitude-data/core' +import { unsafelyGetApiKey } from '@latitude-data/core' import { NextRequest, NextResponse } from 'next/server' import env from './env' @@ -15,7 +15,7 @@ export async function middleware(request: LatitudeRequest) { return apiUnauthorized() } - const result = await getApiKey({ uuid: token }) + const result = await unsafelyGetApiKey({ uuid: token }) if (result.error) return apiUnauthorized() request.workspaceId = result.value.workspaceId diff --git a/package.json b/package.json index 397a652b9..6231186a4 100644 --- a/package.json +++ b/package.json @@ -34,12 +34,9 @@ "pnpm": { "peerDependencyRules": { "allowedVersions": { - "react": "18.x", - "react-dom": "18.x", - "react": "19.x", - "react-dom": "19.x", - "eslint": "8.x", - "eslint": "9.x" + "react": ">=18.x", + "react-dom": ">=18.x", + "eslint": ">=8.x" } } } diff --git a/packages/core/src/data-access/apiKeys.ts b/packages/core/src/data-access/apiKeys.ts index 54445e9d8..1c4984123 100644 --- a/packages/core/src/data-access/apiKeys.ts +++ b/packages/core/src/data-access/apiKeys.ts @@ -3,8 +3,13 @@ import { NotFoundError, Result } from '$core/lib' import { apiKeys } from '$core/schema' import { eq } from 'drizzle-orm' -export async function getApiKey({ uuid }: { uuid: string }, db = database) { - const apiKey = await db.query.apiKeys.findFirst({ where: eq(apiKeys, uuid) }) +export async function unsafelyGetApiKey( + { uuid }: { uuid: string }, + db = database, +) { + const apiKey = await db.query.apiKeys.findFirst({ + where: eq(apiKeys.uuid, uuid), + }) if (!apiKey) return Result.error(new NotFoundError('API key not found')) return Result.ok(apiKey) diff --git a/packages/core/src/data-access/documentVersions/getDocumentById.ts b/packages/core/src/data-access/documentVersions/getDocumentById.ts deleted file mode 100644 index 003bb0735..000000000 --- a/packages/core/src/data-access/documentVersions/getDocumentById.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { database } from '$core/client' -import { LatitudeError, NotFoundError, Result, TypedResult } from '$core/lib' -import { documentVersions } from '$core/schema' -import { eq } from 'drizzle-orm' - -export async function getDocumentById( - { - documentId, - }: { - documentId: number - }, - db = database, -): Promise> { - const document = await db.query.documentVersions.findFirst({ - where: eq(documentVersions.id, documentId), - }) - if (!document) return Result.error(new NotFoundError('Document not found')) - - return Result.ok(document) -} diff --git a/packages/core/src/data-access/documentVersions/getDocumentByPath.ts b/packages/core/src/data-access/documentVersions/getDocumentByPath.ts deleted file mode 100644 index 92204da02..000000000 --- a/packages/core/src/data-access/documentVersions/getDocumentByPath.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { database } from '$core/client' -import { NotFoundError, Result } from '$core/lib' - -import { findCommitByUuid } from '../commits' -import { getDocumentsAtCommit } from './getDocumentsAtCommit' - -export async function getDocumentByPath( - { - projectId, - commitUuid, - path, - }: { - projectId: number - commitUuid: string - path: string - }, - db = database, -) { - try { - const commit = await findCommitByUuid({ projectId, uuid: commitUuid }, db) - if (commit.error) return commit - - const result = await getDocumentsAtCommit({ commitId: commit.value.id }, db) - const documents = result.unwrap() - const document = documents.find((doc) => doc.path === path) - if (!document) { - return Result.error( - new NotFoundError( - `No document with path ${path} at commit ${commitUuid}`, - ), - ) - } - - return Result.ok(document) - } catch (err) { - return Result.error(err as Error) - } -} diff --git a/packages/core/src/data-access/documentVersions/getDocumentsAtCommit.ts b/packages/core/src/data-access/documentVersions/getDocumentsAtCommit.ts deleted file mode 100644 index 0f3c68fc1..000000000 --- a/packages/core/src/data-access/documentVersions/getDocumentsAtCommit.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { - commits, - database, - DocumentVersion, - documentVersions, - findCommitById, - Result, - TypedResult, -} from '@latitude-data/core' -import { LatitudeError, NotFoundError } from '$core/lib/errors' -import { and, eq, getTableColumns, isNotNull, lte, max } from 'drizzle-orm' - -async function fetchDocumentsFromMergedCommits( - { - projectId, - maxMergedAt, - }: { - projectId: number - maxMergedAt: Date | null - }, - tx = database, -): Promise { - const filterByMaxMergedAt = () => { - const mergedAtNotNull = isNotNull(commits.mergedAt) - if (maxMergedAt === null) return mergedAtNotNull - return and(mergedAtNotNull, lte(commits.mergedAt, maxMergedAt)) - } - - const lastVersionOfEachDocument = tx.$with('lastVersionOfDocuments').as( - tx - .select({ - documentUuid: documentVersions.documentUuid, - mergedAt: max(commits.mergedAt).as('maxMergedAt'), - }) - .from(documentVersions) - .innerJoin(commits, eq(commits.id, documentVersions.commitId)) - .where(and(filterByMaxMergedAt(), eq(commits.projectId, projectId))) - .groupBy(documentVersions.documentUuid), - ) - - const documentsFromMergedCommits = await tx - .with(lastVersionOfEachDocument) - .select(getTableColumns(documentVersions)) - .from(documentVersions) - .innerJoin( - commits, - and( - eq(commits.id, documentVersions.commitId), - isNotNull(commits.mergedAt), - ), - ) - .innerJoin( - lastVersionOfEachDocument, - and( - eq( - documentVersions.documentUuid, - lastVersionOfEachDocument.documentUuid, - ), - eq(commits.mergedAt, lastVersionOfEachDocument.mergedAt), - ), - ) - - return documentsFromMergedCommits -} - -function mergeDocuments( - ...documentsArr: DocumentVersion[][] -): DocumentVersion[] { - return documentsArr.reduce((acc, documents) => { - return acc - .filter((d) => { - return !documents.find((d2) => d2.documentUuid === d.documentUuid) - }) - .concat(documents) - }, []) -} - -export async function getDocumentsAtCommit( - { commitId }: { commitId: number }, - tx = database, -): Promise> { - const commitResult = await findCommitById({ id: commitId }, tx) - if (commitResult.error) return commitResult - const commit = commitResult.value! - - const documentsFromMergedCommits = await fetchDocumentsFromMergedCommits( - { - projectId: commit.projectId, - maxMergedAt: commit.mergedAt, - }, - tx, - ) - - if (commit.mergedAt !== null) { - // Referenced commit is merged. No additional documents to return. - return Result.ok(documentsFromMergedCommits) - } - - const documentsFromDraft = await tx - .select(getTableColumns(documentVersions)) - .from(documentVersions) - .innerJoin(commits, eq(commits.id, documentVersions.commitId)) - .where(eq(commits.id, commitId)) - - const totalDocuments = mergeDocuments( - documentsFromMergedCommits, - documentsFromDraft, - ) - - return Result.ok(totalDocuments) -} - -export type GetDocumentAtCommitProps = { - commitId: number - documentUuid: string -} -export async function getDocumentAtCommit( - { commitId, documentUuid }: GetDocumentAtCommitProps, - tx = database, -): Promise> { - const documentInCommit = await tx.query.documentVersions.findFirst({ - where: and( - eq(documentVersions.commitId, commitId), - eq(documentVersions.documentUuid, documentUuid), - ), - }) - if (documentInCommit !== undefined) return Result.ok(documentInCommit) - - const documentsAtCommit = await getDocumentsAtCommit({ commitId }, tx) - if (documentsAtCommit.error) return Result.error(documentsAtCommit.error) - - const document = documentsAtCommit.value.find( - (d) => d.documentUuid === documentUuid, - ) - - if (!document) return Result.error(new NotFoundError('Document not found')) - - return Result.ok(document) -} - -export async function listCommitChanges( - { commitId }: { commitId: number }, - tx = database, -) { - const changedDocuments = await tx.query.documentVersions.findMany({ - where: eq(documentVersions.commitId, commitId), - }) - - return Result.ok(changedDocuments) -} diff --git a/packages/core/src/data-access/documentVersions/index.ts b/packages/core/src/data-access/documentVersions/index.ts deleted file mode 100644 index 84dbd18ac..000000000 --- a/packages/core/src/data-access/documentVersions/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './getDocumentsAtCommit' -export * from './getDocumentByPath' -export * from './getDocumentById' diff --git a/packages/core/src/data-access/index.ts b/packages/core/src/data-access/index.ts index 952fcc44a..45f6b2153 100644 --- a/packages/core/src/data-access/index.ts +++ b/packages/core/src/data-access/index.ts @@ -1,5 +1,3 @@ -export * from './users' -export * from './projects' -export * from './commits' -export * from './documentVersions' export * from './apiKeys' +export * from './users' +export * from './workspaces' diff --git a/packages/core/src/data-access/projects.ts b/packages/core/src/data-access/projects.ts deleted file mode 100644 index 8dc9bd692..000000000 --- a/packages/core/src/data-access/projects.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { database } from '$core/client' -import { Result } from '$core/lib' -import { NotFoundError } from '$core/lib/errors' -import { projects } from '$core/schema' -import { and, eq } from 'drizzle-orm' - -const NOT_FOUND_MSG = 'Project not found' - -export type FindProjectProps = { - projectId: number | string - workspaceId: number -} -export async function findProject({ - projectId, - workspaceId, -}: FindProjectProps) { - const id = Number(projectId) - if (isNaN(id)) { - return Result.error(new NotFoundError(NOT_FOUND_MSG)) - } - - const project = await database.query.projects.findFirst({ - where: and(eq(projects.workspaceId, workspaceId), eq(projects.id, id)), - }) - - if (!project) { - return Result.error(new NotFoundError(NOT_FOUND_MSG)) - } - - return Result.ok(project!) -} - -export async function getFirstProject({ - workspaceId, -}: { - workspaceId: number -}) { - const project = await database.query.projects.findFirst({ - where: eq(projects.workspaceId, workspaceId), - }) - - if (!project) { - return Result.error(new NotFoundError(NOT_FOUND_MSG)) - } - - return Result.ok(project!) -} diff --git a/packages/core/src/data-access/users.ts b/packages/core/src/data-access/users.ts index 1b97787a9..da674ca71 100644 --- a/packages/core/src/data-access/users.ts +++ b/packages/core/src/data-access/users.ts @@ -6,7 +6,7 @@ export type SessionData = { workspace: { id: number; name: string } } -export function getUser(id?: string) { +export function unsafelyGetUser(id?: string) { return database.query.users.findFirst({ where: eq(users.id, id ?? ''), }) diff --git a/packages/core/src/data-access/workspaces.ts b/packages/core/src/data-access/workspaces.ts new file mode 100644 index 000000000..fec6cc38a --- /dev/null +++ b/packages/core/src/data-access/workspaces.ts @@ -0,0 +1,15 @@ +import { database } from '$core/client' +import { Commit, commits, projects, workspaces } from '$core/schema' +import { eq, getTableColumns } from 'drizzle-orm' + +export async function findWorkspaceFromCommit(commit: Commit, db = database) { + const results = await db + .select(getTableColumns(workspaces)) + .from(workspaces) + .innerJoin(projects, eq(projects.workspaceId, workspaces.id)) + .innerJoin(commits, eq(commits.projectId, projects.id)) + .where(eq(commits.id, commit.id)) + .limit(1) + + return results[0] +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 830fa9395..9e285fb53 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -4,3 +4,4 @@ export * from './data-access' export * from './lib' export * from './schema' export * from './services' +export * from './repositories' diff --git a/packages/core/src/repositories/commitsRepository.ts b/packages/core/src/repositories/commitsRepository.ts new file mode 100644 index 000000000..24a3ef089 --- /dev/null +++ b/packages/core/src/repositories/commitsRepository.ts @@ -0,0 +1,116 @@ +import { HEAD_COMMIT } from '$core/constants' +import { NotFoundError, Result } from '$core/lib' +import { commits, projects } from '$core/schema' +import { and, desc, eq, getTableColumns, isNotNull } from 'drizzle-orm' + +import Repository from './repository' + +export class CommitsRepository extends Repository { + get scope() { + return this.db + .select(getTableColumns(commits)) + .from(commits) + .innerJoin(projects, eq(projects.workspaceId, this.workspaceId)) + .where(eq(commits.projectId, projects.id)) + .as('commitsScope') + } + + async getHeadCommit({ projectId }: { projectId: number }) { + const result = await this.db + .select() + .from(this.scope) + .where( + and( + isNotNull(this.scope.mergedAt), + eq(this.scope.projectId, projectId), + ), + ) + .orderBy(desc(this.scope.mergedAt)) + .limit(1) + + if (result.length < 1) { + return Result.error(new NotFoundError('No head commit found')) + } + + return Result.ok(result[0]!) + } + + async getCommitByUuid({ + uuid, + projectId, + }: { + projectId?: number + uuid: string + }) { + if (uuid === HEAD_COMMIT) { + if (!projectId) { + return Result.error(new NotFoundError('Project ID is required')) + } + + return this.getHeadCommit({ projectId }) + } + + const result = await this.db + .select() + .from(this.scope) + .where(eq(this.scope.uuid, uuid)) + .limit(1) + const commit = result[0] + if (!commit) return Result.error(new NotFoundError('Commit not found')) + + return Result.ok(commit) + } + + async getCommitById(id: number) { + const result = await this.db + .select() + .from(this.scope) + .where(eq(this.scope.id, id)) + .limit(1) + const commit = result[0] + if (!commit) return Result.error(new NotFoundError('Commit not found')) + + return Result.ok(commit) + } + + async getCommits() { + return this.db.select().from(this.scope) + } + + async getCommitMergedAt({ + projectId, + uuid, + }: { + projectId: number + uuid: string + }) { + if (uuid === HEAD_COMMIT) { + const result = await this.db + .select({ mergedAt: this.scope.mergedAt }) + .from(this.scope) + .where( + and( + eq(this.scope.projectId, projectId), + isNotNull(this.scope.mergedAt), + ), + ) + .orderBy(desc(this.scope.mergedAt)) + .limit(1) + + if (!result.length) { + return Result.error(new NotFoundError('No head commit found')) + } + const headCommit = result[0]! + return Result.ok(headCommit.mergedAt!) + } + + const result = await this.db + .select() + .from(this.scope) + .where(eq(this.scope.uuid, uuid)) + const commit = result[0] + if (!commit) return Result.error(new NotFoundError('Commit not found')) + + return Result.ok(commit.mergedAt) + } +} diff --git a/packages/core/src/repositories/documentVersionsRepository.ts b/packages/core/src/repositories/documentVersionsRepository.ts new file mode 100644 index 000000000..c6a94ec52 --- /dev/null +++ b/packages/core/src/repositories/documentVersionsRepository.ts @@ -0,0 +1,211 @@ +import { NotFoundError, Result } from '$core/lib' +import { + Commit, + commits, + DocumentVersion, + documentVersions, + projects, +} from '$core/schema' +import { and, eq, getTableColumns, isNotNull, lte, max } from 'drizzle-orm' + +import Repository from './repository' + +export type GetDocumentAtCommitProps = { + commit: Commit + documentUuid: string +} + +export class DocumentVersionsRepository extends Repository { + get scope() { + return this.db + .select(getTableColumns(documentVersions)) + .from(documentVersions) + .innerJoin(projects, eq(projects.workspaceId, this.workspaceId)) + .innerJoin(commits, eq(commits.projectId, projects.id)) + .where(eq(documentVersions.commitId, commits.id)) + .as('documentVersionsScope') + } + + async getDocumentById(documentId: number) { + const res = await this.db + .select() + .from(this.scope) + .where(eq(this.scope.id, documentId)) + + // NOTE: I hate this + const document = res[0] + if (!document) return Result.error(new NotFoundError('Document not found')) + + return Result.ok(document) + } + + async getDocumentByUuid({ + commit, + documentUuid, + }: { + commit: Commit + documentUuid: string + }) { + const document = await this.db + .select() + .from(this.scope) + .where( + and( + eq(documentVersions.commitId, commit.id), + eq(documentVersions.documentUuid, documentUuid), + ), + ) + .limit(1) + .then((docs) => docs[0]) + + if (!document) return Result.error(new NotFoundError('Document not found')) + + return Result.ok(document) + } + + async getDocumentByPath({ commit, path }: { commit: Commit; path: string }) { + try { + const result = await this.getDocumentsAtCommit(commit) + const documents = result.unwrap() + const document = documents.find((doc) => doc.path === path) + if (!document) { + return Result.error( + new NotFoundError( + `No document with path ${path} at commit ${commit.uuid}`, + ), + ) + } + + return Result.ok(document) + } catch (err) { + return Result.error(err as Error) + } + } + + async getDocumentsAtCommit(commit: Commit) { + const documentsFromMergedCommits = + await this.fetchDocumentsFromMergedCommits({ + projectId: commit.projectId, + maxMergedAt: commit.mergedAt, + }) + + if (commit.mergedAt !== null) { + // Referenced commit is merged. No additional documents to return. + return Result.ok(documentsFromMergedCommits) + } + + const documentsFromDraft = await this.db + .select(getTableColumns(documentVersions)) + .from(documentVersions) + .innerJoin(commits, eq(commits.id, documentVersions.commitId)) + .where(eq(commits.id, commit.id)) + + const totalDocuments = mergeDocuments( + documentsFromMergedCommits, + documentsFromDraft, + ) + + return Result.ok(totalDocuments) + } + + async getDocumentAtCommit({ + commit, + documentUuid, + }: GetDocumentAtCommitProps) { + const documentInCommit = await this.db + .select() + .from(this.scope) + .where( + and( + eq(documentVersions.commitId, commit.id), + eq(documentVersions.documentUuid, documentUuid), + ), + ) + .limit(1) + .then((docs) => docs[0]) + if (documentInCommit !== undefined) return Result.ok(documentInCommit) + + const documentsAtCommit = await this.getDocumentsAtCommit(commit) + if (documentsAtCommit.error) return Result.error(documentsAtCommit.error) + + const document = documentsAtCommit.value.find( + (d) => d.documentUuid === documentUuid, + ) + if (!document) return Result.error(new NotFoundError('Document not found')) + + return Result.ok(document) + } + + async listCommitChanges(commit: Commit) { + const changedDocuments = await this.db + .select() + .from(this.scope) + .where(eq(this.scope.commitId, commit.id)) + + return Result.ok(changedDocuments) + } + + private async fetchDocumentsFromMergedCommits({ + projectId, + maxMergedAt, + }: { + projectId: number + maxMergedAt: Date | null + }): Promise { + const filterByMaxMergedAt = () => { + const mergedAtNotNull = isNotNull(commits.mergedAt) + if (maxMergedAt === null) return mergedAtNotNull + return and(mergedAtNotNull, lte(commits.mergedAt, maxMergedAt)) + } + + const lastVersionOfEachDocument = this.db + .$with('lastVersionOfDocuments') + .as( + this.db + .select({ + documentUuid: documentVersions.documentUuid, + mergedAt: max(commits.mergedAt).as('maxMergedAt'), + }) + .from(documentVersions) + .innerJoin(commits, eq(commits.id, documentVersions.commitId)) + .where(and(filterByMaxMergedAt(), eq(commits.projectId, projectId))) + .groupBy(documentVersions.documentUuid), + ) + + const documentsFromMergedCommits = await this.db + .with(lastVersionOfEachDocument) + .select(getTableColumns(documentVersions)) + .from(documentVersions) + .innerJoin( + commits, + and( + eq(commits.id, documentVersions.commitId), + isNotNull(commits.mergedAt), + ), + ) + .innerJoin( + lastVersionOfEachDocument, + and( + eq( + documentVersions.documentUuid, + lastVersionOfEachDocument.documentUuid, + ), + eq(commits.mergedAt, lastVersionOfEachDocument.mergedAt), + ), + ) + + return documentsFromMergedCommits + } +} + +function mergeDocuments( + ...documentsArr: DocumentVersion[][] +): DocumentVersion[] { + return documentsArr.reduce((acc, documents) => { + return acc + .filter((d) => { + return !documents.find((d2) => d2.documentUuid === d.documentUuid) + }) + .concat(documents) + }, []) +} diff --git a/packages/core/src/data-access/documentVersions/getDocumentsAtCommit.test.ts b/packages/core/src/repositories/getDocumentsAtCommit.test.ts similarity index 54% rename from packages/core/src/data-access/documentVersions/getDocumentsAtCommit.test.ts rename to packages/core/src/repositories/getDocumentsAtCommit.test.ts index 1e7a03837..01a3bdf32 100644 --- a/packages/core/src/data-access/documentVersions/getDocumentsAtCommit.test.ts +++ b/packages/core/src/repositories/getDocumentsAtCommit.test.ts @@ -3,21 +3,22 @@ import { updateDocument } from '$core/services' import { mergeCommit } from '$core/services/commits/merge' import { describe, expect, it } from 'vitest' -import { findCommitByUuid } from '../commits' -import { getDocumentsAtCommit } from './getDocumentsAtCommit' +import { CommitsRepository } from './commitsRepository' +import { DocumentVersionsRepository } from './documentVersionsRepository' describe('getDocumentsAtCommit', () => { it('returns the document of the only commit', async (ctx) => { const { project } = await ctx.factories.createProject() - const { commit } = await ctx.factories.createDraft({ project }) + const workspaceId = project.workspaceId + let { commit } = await ctx.factories.createDraft({ project }) const { documentVersion: doc } = await ctx.factories.createDocumentVersion({ commit, }) - await mergeCommit({ commitId: commit.id }) - const result = await getDocumentsAtCommit({ - commitId: commit.id, - }) + commit = await mergeCommit(commit).then((r) => r.unwrap()) + + const scope = new DocumentVersionsRepository(workspaceId) + const result = await scope.getDocumentsAtCommit(commit) const documents = result.unwrap() expect(documents.length).toBe(1) @@ -26,113 +27,124 @@ describe('getDocumentsAtCommit', () => { it('returns the right document version for each commit', async (ctx) => { const { project } = await ctx.factories.createProject() + const workspaceId = project.workspaceId + const scope = new DocumentVersionsRepository(workspaceId) - const { commit: commit1 } = await ctx.factories.createDraft({ project }) + let { commit: commit1 } = await ctx.factories.createDraft({ project }) const { documentVersion: doc } = await ctx.factories.createDocumentVersion({ commit: commit1, content: 'VERSION 1', }) - await mergeCommit({ commitId: commit1.id }).then((r) => r.unwrap()) + commit1 = await mergeCommit(commit1).then((r) => r.unwrap()) const { commit: commit2 } = await ctx.factories.createDraft({ project }) await updateDocument({ - commitId: commit2.id, - documentUuid: doc.documentUuid, + commit: commit2, + document: doc, content: 'VERSION 2', }).then((r) => r.unwrap()) const { commit: commit3 } = await ctx.factories.createDraft({ project }) await updateDocument({ - commitId: commit3.id, - documentUuid: doc.documentUuid, + commit: commit3, + document: doc, content: 'VERSION 3 (draft)', }).then((r) => r.unwrap()) - await mergeCommit({ commitId: commit2.id }).then((r) => r.unwrap()) + await mergeCommit(commit2).then((r) => r.unwrap()) - const commit1Docs = await getDocumentsAtCommit({ - commitId: commit1.id, - }).then((r) => r.unwrap()) + const commit1Docs = await scope + .getDocumentsAtCommit(commit1) + .then((r) => r.unwrap()) expect(commit1Docs.length).toBe(1) expect(commit1Docs[0]!.content).toBe('VERSION 1') - const commit2Docs = await getDocumentsAtCommit({ - commitId: commit2.id, - }).then((r) => r.unwrap()) + const commit2Docs = await scope + .getDocumentsAtCommit(commit2) + .then((r) => r.unwrap()) expect(commit2Docs.length).toBe(1) expect(commit2Docs[0]!.content).toBe('VERSION 2') - const commit3Docs = await getDocumentsAtCommit({ - commitId: commit3.id, - }).then((r) => r.unwrap()) + const commit3Docs = await scope + .getDocumentsAtCommit(commit3) + .then((r) => r.unwrap()) expect(commit3Docs.length).toBe(1) expect(commit3Docs[0]!.content).toBe('VERSION 3 (draft)') - const headCommit = await findCommitByUuid({ - projectId: project.id, - uuid: HEAD_COMMIT, - }).then((r) => r.unwrap()) - const headDocs = await getDocumentsAtCommit({ - commitId: headCommit.id, - }).then((r) => r.unwrap()) + const commitsScope = new CommitsRepository(workspaceId) + const headCommit = await commitsScope + .getCommitByUuid({ + projectId: project.id, + uuid: HEAD_COMMIT, + }) + .then((r) => r.unwrap()) + const headDocs = await scope + .getDocumentsAtCommit(headCommit) + .then((r) => r.unwrap()) expect(headDocs.length).toBe(1) expect(headDocs[0]!.content).toBe('VERSION 2') }) it('returns documents that were last modified in a previous commit', async (ctx) => { const { project } = await ctx.factories.createProject() + const workspaceId = project.workspaceId + const scope = new DocumentVersionsRepository(workspaceId) - const { commit: commit1 } = await ctx.factories.createDraft({ project }) + let { commit: commit1 } = await ctx.factories.createDraft({ project }) await ctx.factories.createDocumentVersion({ commit: commit1, content: 'Doc 1 commit 1', }) - await mergeCommit({ commitId: commit1.id }).then((r) => r.unwrap()) - const { commit: commit2 } = await ctx.factories.createDraft({ project }) + commit1 = await mergeCommit(commit1).then((r) => r.unwrap()) + + let { commit: commit2 } = await ctx.factories.createDraft({ project }) const { documentVersion: doc2 } = await ctx.factories.createDocumentVersion( { commit: commit2, content: 'Doc 2 commit 2' }, ) - await mergeCommit({ commitId: commit2.id }).then((r) => r.unwrap()) + commit2 = await mergeCommit(commit2).then((r) => r.unwrap()) const { commit: commit3 } = await ctx.factories.createDraft({ project }) await updateDocument({ - commitId: commit3.id, - documentUuid: doc2.documentUuid, + commit: commit3, + document: doc2, content: 'Doc 2 commit 3 (draft)', }).then((r) => r.unwrap()) - const commit1Docs = await getDocumentsAtCommit({ - commitId: commit1.id, - }).then((r) => r.unwrap()) + const commit1Docs = await scope + .getDocumentsAtCommit(commit1) + .then((r) => r.unwrap()) expect(commit1Docs.length).toBe(1) const commit1DocContents = commit1Docs.map((d) => d.content) expect(commit1DocContents).toContain('Doc 1 commit 1') - const commit2Docs = await getDocumentsAtCommit({ - commitId: commit2.id, - }).then((r) => r.unwrap()) + const commit2Docs = await scope + .getDocumentsAtCommit(commit2) + .then((r) => r.unwrap()) expect(commit2Docs.length).toBe(2) const commit2DocContents = commit2Docs.map((d) => d.content) expect(commit2DocContents).toContain('Doc 1 commit 1') expect(commit2DocContents).toContain('Doc 2 commit 2') - const commit3Docs = await getDocumentsAtCommit({ - commitId: commit3.id, - }).then((r) => r.unwrap()) + const commit3Docs = await scope + .getDocumentsAtCommit(commit3) + .then((r) => r.unwrap()) expect(commit3Docs.length).toBe(2) const commit3DocContents = commit3Docs.map((d) => d.content) expect(commit3DocContents).toContain('Doc 1 commit 1') expect(commit3DocContents).toContain('Doc 2 commit 3 (draft)') - const headCommit = await findCommitByUuid({ - projectId: project.id, - uuid: HEAD_COMMIT, - }) - const headDocs = await getDocumentsAtCommit({ - commitId: headCommit.unwrap().id, - }).then((r) => r.unwrap()) + const commitsScope = new CommitsRepository(workspaceId) + const headCommit = await commitsScope + .getCommitByUuid({ + projectId: project.id, + uuid: HEAD_COMMIT, + }) + .then((r) => r.unwrap()) + const headDocs = await scope + .getDocumentsAtCommit(headCommit) + .then((r) => r.unwrap()) expect(headDocs.length).toBe(2) const headDocContents = headDocs.map((d) => d.content) expect(headDocContents).toContain('Doc 1 commit 1') diff --git a/packages/core/src/repositories/index.ts b/packages/core/src/repositories/index.ts new file mode 100644 index 000000000..eaa8ae3dc --- /dev/null +++ b/packages/core/src/repositories/index.ts @@ -0,0 +1,3 @@ +export * from './commitsRepository' +export * from './projectsRepository' +export * from './documentVersionsRepository' diff --git a/packages/core/src/repositories/projectsRepository.ts b/packages/core/src/repositories/projectsRepository.ts new file mode 100644 index 000000000..52196feb9 --- /dev/null +++ b/packages/core/src/repositories/projectsRepository.ts @@ -0,0 +1,39 @@ +import { NotFoundError, Result } from '$core/lib' +import { projects } from '$core/schema' +import { eq } from 'drizzle-orm' + +import Repository from './repository' + +const NOT_FOUND_MSG = 'Project not found' + +export class ProjectsRepository extends Repository { + get scope() { + return this.db + .select() + .from(projects) + .where(eq(projects.workspaceId, this.workspaceId)) + .as('projectsScope') + } + + async getProjectById(id: number) { + const result = await this.db + .select() + .from(this.scope) + .where(eq(projects.id, id)) + const project = result[0] + + if (!project) { + return Result.error(new NotFoundError(NOT_FOUND_MSG)) + } + + return Result.ok(project) + } + + async getFirstProject() { + const result = await this.db.select().from(this.scope).limit(1) + const project = result[0] + if (!project) return Result.error(new NotFoundError(NOT_FOUND_MSG)) + + return Result.ok(project) + } +} diff --git a/packages/core/src/repositories/repository.ts b/packages/core/src/repositories/repository.ts new file mode 100644 index 000000000..6c222a5d8 --- /dev/null +++ b/packages/core/src/repositories/repository.ts @@ -0,0 +1,14 @@ +import { database } from '$core/client' +import { Subquery } from 'drizzle-orm' + +export default abstract class Repository { + protected workspaceId: number + protected db = database + + constructor(workspaceId: number, db = database) { + this.workspaceId = workspaceId + this.db = db + } + + abstract get scope(): Subquery +} diff --git a/packages/core/src/services/commits/create.ts b/packages/core/src/services/commits/create.ts index 77d8be510..a141f062a 100644 --- a/packages/core/src/services/commits/create.ts +++ b/packages/core/src/services/commits/create.ts @@ -17,7 +17,11 @@ export async function createCommit({ return Transaction.call(async (tx) => { const result = await tx .insert(commits) - .values({ projectId: commit.projectId!, title: commit.title }) + .values({ + projectId: commit.projectId!, + title: commit.title, + mergedAt: commit.mergedAt, + }) .returning() const createdCommit = result[0] diff --git a/packages/core/src/services/commits/merge.ts b/packages/core/src/services/commits/merge.ts index 6af48ecac..4c7dd3757 100644 --- a/packages/core/src/services/commits/merge.ts +++ b/packages/core/src/services/commits/merge.ts @@ -6,37 +6,28 @@ import { Result, Transaction, } from '@latitude-data/core' -import { LatitudeError, NotFoundError } from '$core/lib/errors' +import { LatitudeError } from '$core/lib/errors' import { and, eq } from 'drizzle-orm' -export async function mergeCommit( - { commitId }: { commitId: number }, - db = database, -) { +export async function mergeCommit(commit: Commit, db = database) { return Transaction.call(async (tx) => { const mergedAt = new Date() - - const commit = await tx.query.commits.findFirst({ - where: eq(commits.id, commitId), - }) - - if (!commit) return Result.error(new NotFoundError('Commit not found')) - - // Check that there is no other commit with same mergeAt in the same project - const otherCommits = await tx.query.commits.findMany({ - where: and( - eq(commits.projectId, commit.projectId), - eq(commits.mergedAt, mergedAt), - ), - }) - + const otherCommits = await tx + .select() + .from(commits) + .where( + and( + eq(commits.projectId, commit.projectId), + eq(commits.mergedAt, mergedAt), + ), + ) if (otherCommits.length > 0) { return Result.error( new LatitudeError('Commit merge time conflict, try again'), ) } - const recomputedResults = await recomputeChanges({ commitId }, tx) + const recomputedResults = await recomputeChanges(commit, tx) if (recomputedResults.error) return recomputedResults if (Object.keys(recomputedResults.value.errors).length > 0) { return Result.error( @@ -49,7 +40,7 @@ export async function mergeCommit( const result = await tx .update(commits) .set({ mergedAt }) - .where(eq(commits.id, commitId)) + .where(eq(commits.id, commit.id)) .returning() const updatedCommit = result[0]! diff --git a/packages/core/src/services/documents/create.test.ts b/packages/core/src/services/documents/create.test.ts index 128cf5eaf..802a362e2 100644 --- a/packages/core/src/services/documents/create.test.ts +++ b/packages/core/src/services/documents/create.test.ts @@ -1,4 +1,4 @@ -import { listCommitChanges } from '$core/data-access' +import { DocumentVersionsRepository } from '$core/repositories' import { describe, expect, it } from 'vitest' import { mergeCommit } from '../commits/merge' @@ -10,14 +10,15 @@ describe('createNewDocument', () => { const { commit } = await ctx.factories.createDraft({ project }) const documentResult = await createNewDocument({ - commitId: commit.id, + commit, path: 'foo', }) const document = documentResult.unwrap() expect(document.path).toBe('foo') - const commitChanges = await listCommitChanges({ commitId: commit.id }) + const scope = new DocumentVersionsRepository(project.workspaceId) + const commitChanges = await scope.listCommitChanges(commit) expect(commitChanges.value.length).toBe(1) expect(commitChanges.value[0]!.documentUuid).toBe(document.documentUuid) expect(commitChanges.value[0]!.path).toBe(document.path) @@ -28,12 +29,12 @@ describe('createNewDocument', () => { const { commit } = await ctx.factories.createDraft({ project }) await createNewDocument({ - commitId: commit.id, + commit, path: 'foo', }) const result = await createNewDocument({ - commitId: commit.id, + commit, path: 'foo', }) @@ -45,11 +46,11 @@ describe('createNewDocument', () => { it('fails when trying to create a document in a merged commit', async (ctx) => { const { project } = await ctx.factories.createProject() - const { commit } = await ctx.factories.createDraft({ project }) - await mergeCommit({ commitId: commit.id }) + let { commit } = await ctx.factories.createDraft({ project }) + commit = await mergeCommit(commit).then((r) => r.unwrap()) const result = await createNewDocument({ - commitId: commit.id, + commit, path: 'foo', }) @@ -59,11 +60,11 @@ describe('createNewDocument', () => { it('fails when trying to create a document in a merged commit', async (ctx) => { const { project } = await ctx.factories.createProject() - const { commit } = await ctx.factories.createDraft({ project }) - await mergeCommit({ commitId: commit.id }) + let { commit } = await ctx.factories.createDraft({ project }) + commit = await mergeCommit(commit).then((r) => r.unwrap()) const result = await createNewDocument({ - commitId: commit.id, + commit, path: 'foo', }) diff --git a/packages/core/src/services/documents/create.ts b/packages/core/src/services/documents/create.ts index 69bfebc70..a0e6941cc 100644 --- a/packages/core/src/services/documents/create.ts +++ b/packages/core/src/services/documents/create.ts @@ -1,25 +1,30 @@ -import { findCommitById, getDocumentsAtCommit } from '$core/data-access' +import { findWorkspaceFromCommit } from '$core/data-access' import { Result, Transaction, TypedResult } from '$core/lib' import { BadRequestError } from '$core/lib/errors' -import { DocumentVersion, documentVersions } from '$core/schema' +import { DocumentVersionsRepository } from '$core/repositories' +import { Commit, DocumentVersion, documentVersions } from '$core/schema' import { eq } from 'drizzle-orm' export async function createNewDocument({ - commitId, + commit, path, content, }: { - commitId: number + commit: Commit path: string content?: string }): Promise> { return await Transaction.call(async (tx) => { - const commit = (await findCommitById({ id: commitId }, tx)).unwrap() if (commit.mergedAt !== null) { return Result.error(new BadRequestError('Cannot modify a merged commit')) } - const currentDocs = (await getDocumentsAtCommit({ commitId }, tx)).unwrap() + const workspace = await findWorkspaceFromCommit(commit, tx) + const docsScope = new DocumentVersionsRepository(workspace!.id, tx) + + const currentDocs = await docsScope + .getDocumentsAtCommit(commit) + .then((r) => r.unwrap()) if (currentDocs.find((d) => d.path === path)) { return Result.error( new BadRequestError('A document with the same path already exists'), @@ -29,7 +34,7 @@ export async function createNewDocument({ const newDoc = await tx .insert(documentVersions) .values({ - commitId, + commitId: commit.id, path, content: content ?? '', }) @@ -39,7 +44,7 @@ export async function createNewDocument({ await tx .update(documentVersions) .set({ resolvedContent: null }) - .where(eq(documentVersions.commitId, commitId)) + .where(eq(documentVersions.commitId, commit.id)) return Result.ok(newDoc[0]!) }) diff --git a/packages/core/src/services/documents/recomputeChanges.ts b/packages/core/src/services/documents/recomputeChanges.ts index e935db2a9..f7c1a0656 100644 --- a/packages/core/src/services/documents/recomputeChanges.ts +++ b/packages/core/src/services/documents/recomputeChanges.ts @@ -1,9 +1,8 @@ import type { CompileError } from '@latitude-data/compiler' import { database } from '$core/client' -import { findCommitById } from '$core/data-access' import { Result, TypedResult } from '$core/lib' import { BadRequestError } from '$core/lib/errors' -import { DocumentVersion } from '$core/schema' +import { Commit, DocumentVersion } from '$core/schema' import { getMergedAndDraftDocuments, @@ -17,15 +16,10 @@ type RecomputedChanges = { } export async function recomputeChanges( - { - commitId, - }: { - commitId: number - }, + draft: Commit, tx = database, ): Promise> { try { - const draft = (await findCommitById({ id: commitId }, tx)).unwrap() if (draft.mergedAt !== null) { return Result.error( new BadRequestError('Cannot recompute changes in a merged commit'), @@ -50,7 +44,7 @@ export async function recomputeChanges( const newDraftDocuments = ( await replaceCommitChanges( { - commitId, + commitId: draft.id, documentChanges: documentsToUpdate, }, tx, diff --git a/packages/core/src/services/documents/update.test.ts b/packages/core/src/services/documents/update.test.ts index b504aad46..611ccf0f5 100644 --- a/packages/core/src/services/documents/update.test.ts +++ b/packages/core/src/services/documents/update.test.ts @@ -1,8 +1,7 @@ import { - findHeadCommit, - getDocumentsAtCommit, - listCommitChanges, -} from '$core/data-access' + CommitsRepository, + DocumentVersionsRepository, +} from '$core/repositories' import { describe, expect, it } from 'vitest' import { recomputeChanges } from './recomputeChanges' @@ -16,19 +15,20 @@ describe('updateDocument', () => { }, }) + const docsScope = new DocumentVersionsRepository(project.workspaceId) const { commit } = await ctx.factories.createDraft({ project }) await updateDocument({ - commitId: commit.id, - documentUuid: documents[0]!.documentUuid, + commit, + document: documents[0]!, content: 'Doc 1 commit 2', }).then((r) => r.unwrap()) - await recomputeChanges({ commitId: commit.id }) + await recomputeChanges(commit) - const changedDocuments = await listCommitChanges({ - commitId: commit.id, - }).then((r) => r.unwrap()) + const changedDocuments = await docsScope + .listCommitChanges(commit) + .then((r) => r.unwrap()) expect(changedDocuments.length).toBe(1) expect(changedDocuments[0]!.path).toBe('doc1') @@ -37,6 +37,7 @@ describe('updateDocument', () => { it('modifies a document that was created in the same commit', async (ctx) => { const { project } = await ctx.factories.createProject() + const docsScope = new DocumentVersionsRepository(project.workspaceId) const { commit } = await ctx.factories.createDraft({ project }) const { documentVersion: doc } = await ctx.factories.createDocumentVersion({ commit: commit, @@ -45,16 +46,16 @@ describe('updateDocument', () => { }) await updateDocument({ - commitId: commit.id, - documentUuid: doc.documentUuid, + commit, + document: doc, content: 'Doc 1 v2', }).then((r) => r.unwrap()) - await recomputeChanges({ commitId: commit.id }) + await recomputeChanges(commit) - const changedDocuments = await listCommitChanges({ - commitId: commit.id, - }).then((r) => r.unwrap()) + const changedDocuments = await docsScope + .listCommitChanges(commit) + .then((r) => r.unwrap()) expect(changedDocuments.length).toBe(1) expect(changedDocuments[0]!.path).toBe('doc1') @@ -71,20 +72,21 @@ describe('updateDocument', () => { }, }) + const docsScope = new DocumentVersionsRepository(project.workspaceId) const referencedDoc = documents.find((d) => d.path === 'referenced/doc')! const { commit } = await ctx.factories.createDraft({ project }) await updateDocument({ - commitId: commit.id, - documentUuid: referencedDoc.documentUuid, + commit, + document: referencedDoc, content: 'The document that is being referenced v2', }).then((r) => r.unwrap()) - await recomputeChanges({ commitId: commit.id }) + await recomputeChanges(commit) - const changedDocuments = await listCommitChanges({ - commitId: commit.id, - }).then((r) => r.unwrap()) + const changedDocuments = await docsScope + .listCommitChanges(commit) + .then((r) => r.unwrap()) expect(changedDocuments.length).toBe(2) expect( @@ -102,21 +104,22 @@ describe('updateDocument', () => { main: '', }, }) + const docsScope = new DocumentVersionsRepository(project.workspaceId) const refDoc = documents.find((d) => d.path === 'referenced/doc')! const { commit } = await ctx.factories.createDraft({ project }) await updateDocument({ - commitId: commit.id, - documentUuid: refDoc.documentUuid, + commit, + document: refDoc, path: 'referenced/doc2', }).then((r) => r.unwrap()) - await recomputeChanges({ commitId: commit.id }) + await recomputeChanges(commit) - const changedDocuments = await listCommitChanges({ - commitId: commit.id, - }).then((r) => r.unwrap()) + const changedDocuments = await docsScope + .listCommitChanges(commit) + .then((r) => r.unwrap()) expect(changedDocuments.length).toBe(2) expect( @@ -134,21 +137,22 @@ describe('updateDocument', () => { unmodified: '', }, }) + const docsScope = new DocumentVersionsRepository(project.workspaceId) const referencedDoc = documents.find((d) => d.path === 'referenced/doc')! const { commit } = await ctx.factories.createDraft({ project }) await updateDocument({ - commitId: commit.id, - documentUuid: referencedDoc.documentUuid, + commit, + document: referencedDoc, content: 'The document that is being referenced v2', }).then((r) => r.unwrap()) - await recomputeChanges({ commitId: commit.id }) + await recomputeChanges(commit) - const changedDocuments = await listCommitChanges({ - commitId: commit.id, - }).then((r) => r.unwrap()) + const changedDocuments = await docsScope + .listCommitChanges(commit) + .then((r) => r.unwrap()) expect(changedDocuments.length).toBe(2) expect( @@ -157,16 +161,16 @@ describe('updateDocument', () => { expect(changedDocuments.find((d) => d.path === 'unmodified')).toBeDefined() await updateDocument({ - commitId: commit.id, - documentUuid: referencedDoc.documentUuid, + commit, + document: referencedDoc, content: referencedDoc.content, // Undo the change }).then((r) => r.unwrap()) - await recomputeChanges({ commitId: commit.id }) + await recomputeChanges(commit) - const changedDocuments2 = await listCommitChanges({ - commitId: commit.id, - }).then((r) => r.unwrap()) + const changedDocuments2 = await docsScope + .listCommitChanges(commit) + .then((r) => r.unwrap()) expect(changedDocuments2.length).toBe(0) }) @@ -183,8 +187,8 @@ describe('updateDocument', () => { const doc1 = documents.find((d) => d.path === 'doc1')! const updateResult = await updateDocument({ - commitId: commit.id, - documentUuid: doc1.documentUuid, + commit, + document: doc1, path: 'doc2', }) @@ -200,15 +204,16 @@ describe('updateDocument', () => { foo: 'foo', }, }) + const commitsScope = new CommitsRepository(project.workspaceId) - const commit = await findHeadCommit({ projectId: project.id }).then((r) => - r.unwrap(), - ) + const commit = await commitsScope + .getHeadCommit({ projectId: project.id }) + .then((r) => r.unwrap()) const fooDoc = documents.find((d) => d.path === 'foo')! const result = await updateDocument({ - commitId: commit.id, - documentUuid: fooDoc.documentUuid, + commit, + document: fooDoc, content: 'bar', }) @@ -223,28 +228,29 @@ describe('updateDocument', () => { doc2: 'Doc 2', }, }) + const docsScope = new DocumentVersionsRepository(project.workspaceId) const { commit } = await ctx.factories.createDraft({ project }) const doc1 = documents.find((d) => d.path === 'doc1')! const doc2 = documents.find((d) => d.path === 'doc2')! await updateDocument({ - commitId: commit.id, - documentUuid: doc1.documentUuid, + commit, + document: doc1, content: 'Doc 1 v2', }).then((r) => r.unwrap()) - await recomputeChanges({ commitId: commit.id }) + await recomputeChanges(commit) await updateDocument({ - commitId: commit.id, - documentUuid: doc2.documentUuid, + commit, + document: doc2, content: 'Doc 2 v2', }) - const commitDocs = await getDocumentsAtCommit({ commitId: commit.id }).then( - (r) => r.unwrap(), - ) + const commitDocs = await docsScope + .getDocumentsAtCommit(commit) + .then((r) => r.unwrap()) expect(commitDocs.find((d) => d.path === 'doc1')!.resolvedContent).toBe( null, diff --git a/packages/core/src/services/documents/update.ts b/packages/core/src/services/documents/update.ts index 92508fdda..04bddb2ae 100644 --- a/packages/core/src/services/documents/update.ts +++ b/packages/core/src/services/documents/update.ts @@ -1,38 +1,43 @@ import { omit } from 'lodash-es' -import { findCommitById, getDocumentsAtCommit } from '$core/data-access' +import { findWorkspaceFromCommit } from '$core/data-access' import { Result, Transaction, TypedResult } from '$core/lib' import { BadRequestError, NotFoundError } from '$core/lib/errors' -import { DocumentVersion, documentVersions } from '$core/schema' +import { DocumentVersionsRepository } from '$core/repositories' +import { Commit, DocumentVersion, documentVersions } from '$core/schema' import { eq } from 'drizzle-orm' +// TODO: refactor, can be simplified export async function updateDocument({ - commitId, - documentUuid, + commit, + document, path, content, deletedAt, }: { - commitId: number - documentUuid: string + commit: Commit + document: DocumentVersion path?: string content?: string | null deletedAt?: Date | null }): Promise> { - const updatedDocData = Object.fromEntries( - Object.entries({ path, content, deletedAt }).filter( - ([_, v]) => v !== undefined, - ), - ) - return await Transaction.call(async (tx) => { - const commit = (await findCommitById({ id: commitId }, tx)).unwrap() + const updatedDocData = Object.fromEntries( + Object.entries({ path, content, deletedAt }).filter( + ([_, v]) => v !== undefined, + ), + ) + if (commit.mergedAt !== null) { return Result.error(new BadRequestError('Cannot modify a merged commit')) } - const currentDocs = (await getDocumentsAtCommit({ commitId }, tx)).unwrap() - const currentDoc = currentDocs.find((d) => d.documentUuid === documentUuid) + const workspace = await findWorkspaceFromCommit(commit, tx) + const docsScope = new DocumentVersionsRepository(workspace!.id, tx) + const currentDocs = (await docsScope.getDocumentsAtCommit(commit)).unwrap() + const currentDoc = currentDocs.find( + (d) => d.documentUuid === document.documentUuid, + ) if (!currentDoc) { return Result.error(new NotFoundError('Document does not exist')) } @@ -40,7 +45,7 @@ export async function updateDocument({ if (path !== undefined) { if ( currentDocs.find( - (d) => d.path === path && d.documentUuid !== documentUuid, + (d) => d.path === path && d.documentUuid !== document.documentUuid, ) ) { return Result.error( @@ -50,11 +55,10 @@ export async function updateDocument({ } const oldVersion = omit(currentDoc, ['id', 'commitId', 'updatedAt']) - const newVersion = { ...oldVersion, ...updatedDocData, - commitId, + commitId: commit.id, } const updatedDocs = await tx @@ -65,7 +69,6 @@ export async function updateDocument({ set: newVersion, }) .returning() - if (updatedDocs.length === 0) { return Result.error(new NotFoundError('Document does not exist')) } @@ -74,7 +77,7 @@ export async function updateDocument({ await tx .update(documentVersions) .set({ resolvedContent: null }) - .where(eq(documentVersions.commitId, commitId)) + .where(eq(documentVersions.commitId, commit.id)) return Result.ok(updatedDocs[0]!) }) diff --git a/packages/core/src/services/documents/utils.ts b/packages/core/src/services/documents/utils.ts index 55411dcdb..90ec09563 100644 --- a/packages/core/src/services/documents/utils.ts +++ b/packages/core/src/services/documents/utils.ts @@ -2,31 +2,15 @@ import { omit } from 'lodash-es' import { readMetadata, type CompileError } from '@latitude-data/compiler' import { database } from '$core/client' -import { - findCommitById, - findHeadCommit, - getDocumentsAtCommit, - listCommitChanges, -} from '$core/data-access' +import { findWorkspaceFromCommit } from '$core/data-access' import { Result, Transaction, TypedResult } from '$core/lib' -import { ForbiddenError, LatitudeError } from '$core/lib/errors' +import { + CommitsRepository, + DocumentVersionsRepository, +} from '$core/repositories' import { Commit, DocumentVersion, documentVersions } from '$core/schema' import { eq } from 'drizzle-orm' -export async function getDraft( - commitId: number, -): Promise> { - 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, @@ -37,27 +21,33 @@ export async function getMergedAndDraftDocuments( ): Promise> { const mergedDocuments: DocumentVersion[] = [] - const headCommit = await findHeadCommit({ projectId: draft.projectId }, tx) - if (headCommit.ok) { - // "Head commit" may not exist if the project is empty - const headDocuments = await getDocumentsAtCommit( - { - commitId: headCommit.value!.id, - }, - tx, - ) - if (headDocuments.error) return headDocuments - mergedDocuments.push(...headDocuments.value) - } + const workspace = await findWorkspaceFromCommit(draft, tx) + const commitsScope = new CommitsRepository(workspace!.id, tx) + const docsScope = new DocumentVersionsRepository(workspace!.id, tx) + const headCommitResult = await commitsScope.getHeadCommit({ + projectId: draft.projectId, + }) + + if (headCommitResult.error) return headCommitResult + + const headDocumentsResult = await docsScope.getDocumentsAtCommit( + headCommitResult.value, + ) + if (headDocumentsResult.error) return Result.error(headDocumentsResult.error) + + mergedDocuments.push(...headDocumentsResult.value) - const draftChanges = await listCommitChanges({ commitId: draft.id }, tx) - if (draftChanges.error) return Result.error(draftChanges.error) + const draftChangesResult = await docsScope.listCommitChanges(draft) + if (draftChangesResult.error) return Result.error(draftChangesResult.error) const draftDocuments = mergedDocuments .filter( - (d) => !draftChanges.value.find((c) => c.documentUuid === d.documentUuid), + (d) => + !draftChangesResult.value.find( + (c) => c.documentUuid === d.documentUuid, + ), ) - .concat(draftChanges.value) + .concat(draftChangesResult.value) return Result.ok([mergedDocuments, structuredClone(draftDocuments)]) } @@ -122,6 +112,7 @@ export async function resolveDocumentChanges({ return { documents: changedDocuments, errors } } +// TODO: replace commitId param with commit object export async function replaceCommitChanges( { commitId, diff --git a/packages/core/src/services/projects/create.ts b/packages/core/src/services/projects/create.ts index b4dd17f5a..4562a976a 100644 --- a/packages/core/src/services/projects/create.ts +++ b/packages/core/src/services/projects/create.ts @@ -7,6 +7,7 @@ import { } from '@latitude-data/core' import { createCommit } from '$core/services/commits/create' +// TODO: pass a workspace instead of workspaceId export async function createProject( { workspaceId, @@ -21,7 +22,8 @@ export async function createProject( const project = ( await tx.insert(projects).values({ workspaceId, name }).returning() )[0]! - const commit = await createCommit({ + + const result = await createCommit({ commit: { projectId: project.id, title: 'Initial version', @@ -29,8 +31,7 @@ export async function createProject( }, db: tx, }) - - if (commit.error) return commit + if (result.error) return result return Result.ok(project) }, db) diff --git a/packages/core/src/services/users/createUser.ts b/packages/core/src/services/users/createUser.ts index ba55fab2e..395d6d54d 100644 --- a/packages/core/src/services/users/createUser.ts +++ b/packages/core/src/services/users/createUser.ts @@ -33,6 +33,7 @@ export async function createUser( email: users.email, name: users.name, }) + const user = inserts[0]! return Result.ok(user) }, db) diff --git a/packages/core/src/tests/factories/documents.ts b/packages/core/src/tests/factories/documents.ts index e957e2808..8af4765e3 100644 --- a/packages/core/src/tests/factories/documents.ts +++ b/packages/core/src/tests/factories/documents.ts @@ -20,7 +20,6 @@ export async function createDocumentVersion( documentData: IDocumentVersionData, ) { const randomData = makeRandomDocumentVersionData() - const data = { ...randomData, content: randomData.content, @@ -28,18 +27,19 @@ export async function createDocumentVersion( } let result = await createNewDocument({ - commitId: data.commit.id, + commit: data.commit, path: data.path, }) if (data.content) { result = await updateDocument({ - commitId: data.commit.id, - documentUuid: result.unwrap().documentUuid, + commit: data.commit, + document: result.unwrap(), content: data.content, }) } const documentVersion = result.unwrap() + return { documentVersion } } diff --git a/packages/core/src/tests/factories/projects.ts b/packages/core/src/tests/factories/projects.ts index 03b71c28d..cb7a77a7d 100644 --- a/packages/core/src/tests/factories/projects.ts +++ b/packages/core/src/tests/factories/projects.ts @@ -1,5 +1,5 @@ import { faker } from '@faker-js/faker' -import { getUser } from '$core/data-access' +import { unsafelyGetUser } from '$core/data-access' import { DocumentVersion, Workspace, type SafeUser } from '$core/schema' import { createNewDocument, mergeCommit, updateDocument } from '$core/services' import { createProject as createProjectFn } from '$core/services/projects' @@ -45,7 +45,7 @@ export async function createProject(projectData: Partial = {}) { let workspace: Workspace if ('id' in workspaceData) { - user = (await getUser(workspaceData.creatorId!)) as SafeUser + user = (await unsafelyGetUser(workspaceData.creatorId!)) as SafeUser workspace = workspaceData as Workspace } else { const newWorkspace = await createWorkspace(workspaceData) @@ -70,17 +70,17 @@ export async function createProject(projectData: Partial = {}) { }) const { commit: draft } = await createDraft({ project }) for await (const { path, content } of documentsToCreate) { - const newDoc = await createNewDocument({ commitId: draft.id, path }).then( + const newDoc = await createNewDocument({ commit: draft, path }).then( (r) => r.unwrap(), ) const updatedDoc = await updateDocument({ - commitId: draft.id, - documentUuid: newDoc.documentUuid, + commit: draft, + document: newDoc, content, }) documents.push(updatedDoc.unwrap()) } - await mergeCommit({ commitId: draft.id }).then((r) => r.unwrap()) + await mergeCommit(draft).then((r) => r.unwrap()) } return { project, user, workspace, documents }