From 412708052d4306af47e5cee0f7d13ec7f7687743 Mon Sep 17 00:00:00 2001 From: Gerard Clos Date: Sat, 20 Jul 2024 19:26:02 +0200 Subject: [PATCH] feature: tenancy --- apps/web/src/actions/documents/create.ts | 16 +- apps/web/src/actions/procedures/index.ts | 10 +- apps/web/src/actions/user/setupAction.ts | 1 - .../src/app/(private)/_data-access/index.ts | 30 ++-- .../[...documentPath]/route.test.ts | 6 +- .../[commitUuid]/[...documentPath]/route.ts | 23 ++- apps/web/src/app/(private)/page.tsx | 4 +- .../(private)/projects/[projectId]/page.tsx | 4 +- .../versions/[commitUuid]/layout.tsx | 27 +-- .../commits/[commitUuid]/documents/route.ts | 28 ++++ apps/web/src/components/Sidebar/index.tsx | 29 +++- apps/web/src/data-access/users.ts | 7 +- apps/web/src/middleware.ts | 4 +- packages/core/src/data-access/apiKeys.ts | 9 +- .../documentVersions/getDocumentById.ts | 20 --- .../documentVersions/getDocumentByPath.ts | 38 ----- .../documentVersions/getDocumentsAtCommit.ts | 119 -------------- .../src/data-access/documentVersions/index.ts | 3 - packages/core/src/data-access/index.ts | 6 +- packages/core/src/data-access/projects.ts | 47 ------ packages/core/src/data-access/users.ts | 2 +- packages/core/src/data-access/workspaces.ts | 15 ++ packages/core/src/index.ts | 1 + .../src/repositories/commitsRepository.ts | 108 ++++++++++++ .../documentVersionsRepository.ts | 155 ++++++++++++++++++ .../getDocumentsAtCommit.test.ts | 107 ++++++------ packages/core/src/repositories/index.ts | 3 + .../src/repositories/projectsRepository.ts | 39 +++++ packages/core/src/repositories/repository.ts | 14 ++ packages/core/src/services/commits/merge.ts | 33 ++-- .../src/services/documents/create.test.ts | 15 +- .../core/src/services/documents/create.ts | 20 +-- .../src/services/documents/update.test.ts | 52 +++--- .../core/src/services/documents/update.ts | 48 +++--- packages/core/src/services/documents/utils.ts | 8 +- packages/core/src/services/projects/create.ts | 9 +- .../core/src/tests/factories/documents.ts | 6 +- packages/core/src/tests/factories/projects.ts | 4 +- 38 files changed, 625 insertions(+), 445 deletions(-) create mode 100644 apps/web/src/app/api/projects/[projectId]/commits/[commitUuid]/documents/route.ts delete mode 100644 packages/core/src/data-access/documentVersions/getDocumentById.ts delete mode 100644 packages/core/src/data-access/documentVersions/getDocumentByPath.ts delete mode 100644 packages/core/src/data-access/documentVersions/getDocumentsAtCommit.ts delete mode 100644 packages/core/src/data-access/documentVersions/index.ts delete mode 100644 packages/core/src/data-access/projects.ts create mode 100644 packages/core/src/data-access/workspaces.ts create mode 100644 packages/core/src/repositories/commitsRepository.ts create mode 100644 packages/core/src/repositories/documentVersionsRepository.ts rename packages/core/src/{data-access/documentVersions => repositories}/getDocumentsAtCommit.test.ts (60%) create mode 100644 packages/core/src/repositories/index.ts create mode 100644 packages/core/src/repositories/projectsRepository.ts create mode 100644 packages/core/src/repositories/repository.ts 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/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 a5e0976d2..198c000e9 100644 --- a/apps/web/src/app/(private)/_data-access/index.ts +++ b/apps/web/src/app/(private)/_data-access/index.ts @@ -1,16 +1,15 @@ import { cache } from 'react' import { - findCommitByUuid as originalfindCommit, - findProject as originalFindProject, - getFirstProject as originalGetFirstProject, - type FindCommitByUuidProps, - type FindProjectProps, + CommitsRepository, + 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 @@ -18,8 +17,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 @@ -27,8 +33,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 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 229e7da5d..0449d30a1 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 @@ -15,7 +15,7 @@ describe('GET documentVersion', () => { commit, }) - await mergeCommit({ commitId: commit.id }) + await mergeCommit(commit) const response = await GET( new NextRequest( @@ -41,7 +41,7 @@ describe('GET documentVersion', () => { commit, }) - await mergeCommit({ commitId: commit.id }) + await mergeCommit(commit) const response = await GET( new NextRequest( @@ -64,7 +64,7 @@ describe('GET documentVersion', () => { const { project } = await ctx.factories.createProject() const { commit } = await ctx.factories.createDraft({ project }) - await mergeCommit({ commitId: commit.id }) + await mergeCommit(commit) const response = await GET( new NextRequest( 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]/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/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..d19c5c039 100644 --- a/apps/web/src/data-access/users.ts +++ b/apps/web/src/data-access/users.ts @@ -1,6 +1,6 @@ import { database, - getUser, + unsafelyGetUser, NotFoundError, Result, users, @@ -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/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/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 1e8730edc..000000000 --- a/packages/core/src/data-access/documentVersions/getDocumentsAtCommit.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { - commits, - database, - DocumentVersion, - documentVersions, - findCommitById, - Result, - TypedResult, -} from '@latitude-data/core' -import { LatitudeError } 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 }) - if (commitResult.error) return commitResult - const commit = commitResult.value! - - const documentsFromMergedCommits = await 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 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 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..df68a755b --- /dev/null +++ b/packages/core/src/repositories/commitsRepository.ts @@ -0,0 +1,108 @@ +import { HEAD_COMMIT } from '$core/constants' +import { NotFoundError, Result } from '$core/lib' +import { commits, projects } from '$core/schema' +import { and, desc, eq, isNotNull } from 'drizzle-orm' + +import Repository from './repository' + +export class CommitsRepository extends Repository { + get scope() { + return this.db + .select() + .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(commits.mergedAt), eq(commits.projectId, projectId))) + .orderBy(desc(commits.mergedAt)) + .limit(1) + + if (result.length < 1) { + return Result.error(new NotFoundError('No head commit found')) + } + + return Result.ok(result[0]!.commits) + } + + 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(commits.uuid, uuid)) + .limit(1) + const commit = result[0]?.commits + 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(commits.id, id)) + .limit(1) + const commit = result[0]?.commits + 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: commits.mergedAt }) + .from(this.scope) + .where( + and(eq(commits.projectId, projectId), isNotNull(commits.mergedAt)), + ) + .orderBy(desc(commits.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(commits.uuid, uuid)) + const commit = result[0]?.commits + 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..d9e9537b1 --- /dev/null +++ b/packages/core/src/repositories/documentVersionsRepository.ts @@ -0,0 +1,155 @@ +import { database } from '$core/client' +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 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(documentVersions.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 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 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 listCommitChanges(commit: Commit) { + const changedDocuments = await this.db + .select() + .from(this.scope) + .where(eq(documentVersions.commitId, commit.id)) + + return Result.ok(changedDocuments) + } +} + +function mergeDocuments( + ...documentsArr: DocumentVersion[][] +): DocumentVersion[] { + return documentsArr.reduce((acc, documents) => { + return acc + .filter((d) => { + return !documents.find((d2) => d2.documentUuid === d.documentUuid) + }) + .concat(documents) + }, []) +} + +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 +} diff --git a/packages/core/src/data-access/documentVersions/getDocumentsAtCommit.test.ts b/packages/core/src/repositories/getDocumentsAtCommit.test.ts similarity index 60% rename from packages/core/src/data-access/documentVersions/getDocumentsAtCommit.test.ts rename to packages/core/src/repositories/getDocumentsAtCommit.test.ts index 2e7e3d91a..62fb93c4d 100644 --- a/packages/core/src/data-access/documentVersions/getDocumentsAtCommit.test.ts +++ b/packages/core/src/repositories/getDocumentsAtCommit.test.ts @@ -3,21 +3,21 @@ 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 workspaceId = project.workspaceId const { commit } = await ctx.factories.createDraft({ project }) const { documentVersion: doc } = await ctx.factories.createDocumentVersion({ commit, }) - await mergeCommit({ commitId: commit.id }) + await mergeCommit(commit) - const result = await getDocumentsAtCommit({ - commitId: commit.id, - }) + const scope = new DocumentVersionsRepository(workspaceId) + const result = await scope.getDocumentsAtCommit(commit) const documents = result.unwrap() expect(documents.length).toBe(1) @@ -26,113 +26,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 }) const { documentVersion: doc } = await ctx.factories.createDocumentVersion({ commit: commit1, content: 'VERSION 1', }) - await mergeCommit({ commitId: commit1.id }).then((r) => r.unwrap()) + 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 }) await ctx.factories.createDocumentVersion({ commit: commit1, content: 'Doc 1 commit 1', }) - await mergeCommit({ commitId: commit1.id }).then((r) => r.unwrap()) + + await mergeCommit(commit1).then((r) => r.unwrap()) const { 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()) + 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/merge.ts b/packages/core/src/services/commits/merge.ts index f0e049659..6d71cbd7b 100644 --- a/packages/core/src/services/commits/merge.ts +++ b/packages/core/src/services/commits/merge.ts @@ -5,30 +5,21 @@ 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'), @@ -38,7 +29,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 d781532ba..b88cdc822 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', }) @@ -46,10 +47,10 @@ 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 }) + await mergeCommit(commit) 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 6a7827591..8a53635ae 100644 --- a/packages/core/src/services/documents/create.ts +++ b/packages/core/src/services/documents/create.ts @@ -1,7 +1,9 @@ import { + Commit, DocumentVersion, documentVersions, - getDocumentsAtCommit, + DocumentVersionsRepository, + findWorkspaceFromCommit, Result, Transaction, } from '@latitude-data/core' @@ -13,20 +15,18 @@ import { } from './utils' export async function createNewDocument({ - commitId, + commit, path, }: { - commitId: number + commit: Commit path: string }) { - const commitResult = await assertCommitIsEditable(commitId) + const commitResult = await assertCommitIsEditable(commit) if (commitResult.error) return commitResult - const currentDocuments = await getDocumentsAtCommit({ - commitId, - }) - if (currentDocuments.error) return currentDocuments - + const workspace = await findWorkspaceFromCommit(commit) + const scope = new DocumentVersionsRepository(workspace!.id) + const currentDocuments = await scope.getDocumentsAtCommit(commit) if ( existsAnotherDocumentWithSamePath({ documents: currentDocuments.value, @@ -42,7 +42,7 @@ export async function createNewDocument({ const result = await tx .insert(documentVersions) .values({ - commitId, + commitId: commit.id, path, }) .returning() diff --git a/packages/core/src/services/documents/update.test.ts b/packages/core/src/services/documents/update.test.ts index fbc92bf46..b98b2e235 100644 --- a/packages/core/src/services/documents/update.test.ts +++ b/packages/core/src/services/documents/update.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' @@ -13,19 +13,20 @@ describe('updateDocument', () => { path: 'doc1', content: 'Doc 1 commit 1', }) - await mergeCommit({ commitId: commit1.id }) + await mergeCommit(commit1) const { commit: commit2 } = await ctx.factories.createDraft({ project }) await updateDocument({ - commitId: commit2.id, - documentUuid: doc.documentUuid, + commit: commit2, + document: doc, content: 'Doc 1 commit 2', }).then((r) => r.unwrap()) - const changedDocuments = await listCommitChanges({ - commitId: commit2.id, - }).then((r) => r.unwrap()) + const scope = new DocumentVersionsRepository(project.workspaceId) + const changedDocuments = await scope + .listCommitChanges(commit2) + .then((r) => r.unwrap()) expect(changedDocuments.length).toBe(1) expect(changedDocuments[0]!.path).toBe('doc1') @@ -42,14 +43,15 @@ describe('updateDocument', () => { }) await updateDocument({ - commitId: commit.id, - documentUuid: doc.documentUuid, + commit, + document: doc, content: 'Doc 1 v2', }).then((r) => r.unwrap()) - const changedDocuments = await listCommitChanges({ - commitId: commit.id, - }).then((r) => r.unwrap()) + const scope = new DocumentVersionsRepository(project.workspaceId) + const changedDocuments = await scope + .listCommitChanges(commit) + .then((r) => r.unwrap()) expect(changedDocuments.length).toBe(1) expect(changedDocuments[0]!.path).toBe('doc1') @@ -58,6 +60,7 @@ describe('updateDocument', () => { it('modifying a document creates a change to all other documents that reference it', async (ctx) => { const { project } = await ctx.factories.createProject() + const scope = new DocumentVersionsRepository(project.workspaceId) const { commit: commit1 } = await ctx.factories.createDraft({ project }) const { documentVersion: referencedDoc } = await ctx.factories.createDocumentVersion({ @@ -70,19 +73,19 @@ describe('updateDocument', () => { path: 'unmodified', content: '', }) - await mergeCommit({ commitId: commit1.id }) + await mergeCommit(commit1) const { commit: commit2 } = await ctx.factories.createDraft({ project }) await updateDocument({ - commitId: commit2.id, - documentUuid: referencedDoc.documentUuid, + commit: commit2, + document: referencedDoc, content: 'The document that is being referenced v2', }).then((r) => r.unwrap()) - const changedDocuments = await listCommitChanges({ - commitId: commit2.id, - }).then((r) => r.unwrap()) + const changedDocuments = await scope + .listCommitChanges(commit2) + .then((r) => r.unwrap()) expect(changedDocuments.length).toBe(2) expect( @@ -93,6 +96,7 @@ describe('updateDocument', () => { it('renaming a document creates a change to all other documents that reference it', async (ctx) => { const { project } = await ctx.factories.createProject() + const scope = new DocumentVersionsRepository(project.workspaceId) const { commit: commit1 } = await ctx.factories.createDraft({ project }) const { documentVersion: referencedDoc } = await ctx.factories.createDocumentVersion({ @@ -105,19 +109,19 @@ describe('updateDocument', () => { path: 'unmodified', content: '', }) - await mergeCommit({ commitId: commit1.id }) + await mergeCommit(commit1) const { commit: commit2 } = await ctx.factories.createDraft({ project }) await updateDocument({ - commitId: commit2.id, - documentUuid: referencedDoc.documentUuid, + commit: commit2, + document: referencedDoc, path: 'referenced/doc2', }).then((r) => r.unwrap()) - const changedDocuments = await listCommitChanges({ - commitId: commit2.id, - }).then((r) => r.unwrap()) + const changedDocuments = await scope + .listCommitChanges(commit2) + .then((r) => r.unwrap()) expect(changedDocuments.length).toBe(2) expect( diff --git a/packages/core/src/services/documents/update.ts b/packages/core/src/services/documents/update.ts index ddce70672..2eadf856f 100644 --- a/packages/core/src/services/documents/update.ts +++ b/packages/core/src/services/documents/update.ts @@ -1,10 +1,11 @@ import { omit } from 'lodash-es' import { readMetadata } from '@latitude-data/compiler' -import { getDocumentsAtCommit } from '$core/data-access' +import { findWorkspaceFromCommit } from '$core/data-access' import { Result, Transaction, TypedResult } from '$core/lib' import { BadRequestError, LatitudeError, 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' import { assertCommitIsEditable } from './utils' @@ -74,37 +75,42 @@ async function getUpdatedDocuments({ return Result.ok(documentsWithUpdatedHash) } +// TODO: refactor in smaller chunks 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 }) { - const commitResult = await assertCommitIsEditable(commitId) + const commitResult = await assertCommitIsEditable(commit) if (commitResult.error) return commitResult const updateData = Object.fromEntries( - Object.entries({ documentUuid, path, content, deletedAt }).filter( - ([_, v]) => v !== undefined, - ), + Object.entries({ + documentUuid: document.documentUuid, + path, + content, + deletedAt, + }).filter(([_, v]) => v !== undefined), ) - const currentDocuments = await getDocumentsAtCommit({ - commitId, - }) - if (currentDocuments.error) return currentDocuments + const workspace = await findWorkspaceFromCommit(commit) + const scope = new DocumentVersionsRepository(workspace!.id) + const documents = await scope + .getDocumentsAtCommit(commit) + .then((r) => r.unwrap()) if ( path && - currentDocuments.value.find( - (d) => d.documentUuid !== documentUuid && d.path === path, + documents.find( + (d) => d.documentUuid !== document.documentUuid && d.path === path, ) ) { return Result.error( @@ -113,7 +119,7 @@ export async function updateDocument({ } const documentsToUpdateResult = await getUpdatedDocuments({ - currentDocuments: currentDocuments.value, + currentDocuments: documents, updateData, }) if (documentsToUpdateResult.error) return documentsToUpdateResult @@ -122,11 +128,11 @@ export async function updateDocument({ return Transaction.call(async (tx) => { const results = await Promise.all( documentsToUpdate.map(async (documentData) => { - const isNewDocumentVersion = documentData.commitId !== commitId + const isNewDocumentVersion = documentData.commitId !== commit.id const newDocumentVersion = { ...omit(documentData, ['id', 'commitId', 'updatedAt', 'createdAt']), path: documentData.path, // <- This should not be necessary, but Typescript somehow is not sure that path is present. - commitId, + commitId: commit.id, } if (isNewDocumentVersion) { @@ -146,6 +152,8 @@ export async function updateDocument({ }), ) - return Result.ok(results.find((r) => r.documentUuid === documentUuid)!) + return Result.ok( + results.find((r) => r.documentUuid === document.documentUuid)!, + ) }) } diff --git a/packages/core/src/services/documents/utils.ts b/packages/core/src/services/documents/utils.ts index bf7c65339..6ae741da1 100644 --- a/packages/core/src/services/documents/utils.ts +++ b/packages/core/src/services/documents/utils.ts @@ -1,17 +1,15 @@ import { + Commit, DocumentVersion, - findCommitById, Result, TypedResult, } from '@latitude-data/core' import { ForbiddenError, LatitudeError } from '$core/lib/errors' export async function assertCommitIsEditable( - commitId: number, + commit: Commit, ): Promise> { - const commit = await findCommitById({ id: commitId }) - - if (commit.value?.mergedAt !== null) { + if (commit.mergedAt !== null) { return Result.error( new ForbiddenError('Cannot create a document version in a merged commit'), ) diff --git a/packages/core/src/services/projects/create.ts b/packages/core/src/services/projects/create.ts index 22dda57c0..284b86eab 100644 --- a/packages/core/src/services/projects/create.ts +++ b/packages/core/src/services/projects/create.ts @@ -8,6 +8,7 @@ import { import { createCommit } from '$core/services/commits/create' import { mergeCommit } from '$core/services/commits/merge' +// TODO: pass a workspace instead of workspaceId export async function createProject( { workspaceId, @@ -22,15 +23,13 @@ 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' }, db: tx, }) + if (result.error) return result - if (commit.error) return commit - - const resultMerge = await mergeCommit({ commitId: commit.value.id }, tx) - + const resultMerge = await mergeCommit(result.value) if (resultMerge.error) return resultMerge return Result.ok(project) diff --git a/packages/core/src/tests/factories/documents.ts b/packages/core/src/tests/factories/documents.ts index e957e2808..3eba3dbb7 100644 --- a/packages/core/src/tests/factories/documents.ts +++ b/packages/core/src/tests/factories/documents.ts @@ -28,14 +28,14 @@ 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, }) } diff --git a/packages/core/src/tests/factories/projects.ts b/packages/core/src/tests/factories/projects.ts index 8026229cf..7834b69e3 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 { Workspace, type SafeUser } from '$core/schema' import { createProject as createProjectFn } from '$core/services/projects' @@ -15,7 +15,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)