From a304c8430406e80ca4673329ebe3ef9ea8209e62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Sans=C3=B3n?= Date: Thu, 18 Jul 2024 10:00:01 +0200 Subject: [PATCH] Added mergedAt attribute to commit --- apps/web/src/app/(private)/layout.tsx | 10 +- .../commits/[commitUuid]/layout.tsx | 28 + .../(private)/projects/[projectId]/layout.tsx | 15 + .../commits/[commitUuid]/documents/route.ts | 10 +- .../components/Sidebar/DocumentTree/index.tsx | 27 +- apps/web/src/components/Sidebar/index.tsx | 17 +- apps/web/src/stores/documentVersions.ts | 18 +- .../core/drizzle/0004_cold_ben_parker.sql | 14 + packages/core/drizzle/meta/0004_snapshot.json | 801 ++++++++++++++++++ packages/core/drizzle/meta/_journal.json | 7 + packages/core/src/data-access/commits.ts | 71 +- .../src/data-access/documentVersions.test.ts | 152 ++++ .../core/src/data-access/documentVersions.ts | 136 ++- packages/core/src/schema/models/commits.ts | 16 +- packages/core/src/schema/models/projects.ts | 2 +- packages/core/src/services/commits/merge.ts | 46 + .../src/services/documentVersions/create.ts | 40 +- .../documentVersions/materializeAtCommit.ts | 20 +- .../core/src/tests/factories/documents.ts | 49 ++ packages/core/src/tests/factories/index.ts | 1 + packages/core/src/tests/tmp.test.ts | 8 - packages/web-ui/src/index.ts | 1 + .../web-ui/src/providers/CommitProvider.tsx | 36 + .../web-ui/src/providers/ProjectProvider.tsx | 34 + packages/web-ui/src/providers/index.ts | 2 + 25 files changed, 1437 insertions(+), 124 deletions(-) create mode 100644 apps/web/src/app/(private)/projects/[projectId]/commits/[commitUuid]/layout.tsx create mode 100644 apps/web/src/app/(private)/projects/[projectId]/layout.tsx rename apps/web/src/app/api/{ => projects/[projectId]}/commits/[commitUuid]/documents/route.ts (60%) create mode 100644 packages/core/drizzle/0004_cold_ben_parker.sql create mode 100644 packages/core/drizzle/meta/0004_snapshot.json create mode 100644 packages/core/src/data-access/documentVersions.test.ts create mode 100644 packages/core/src/services/commits/merge.ts create mode 100644 packages/core/src/tests/factories/documents.ts delete mode 100644 packages/core/src/tests/tmp.test.ts create mode 100644 packages/web-ui/src/providers/CommitProvider.tsx create mode 100644 packages/web-ui/src/providers/ProjectProvider.tsx create mode 100644 packages/web-ui/src/providers/index.ts diff --git a/apps/web/src/app/(private)/layout.tsx b/apps/web/src/app/(private)/layout.tsx index 709e988ba..63e0f847d 100644 --- a/apps/web/src/app/(private)/layout.tsx +++ b/apps/web/src/app/(private)/layout.tsx @@ -1,6 +1,5 @@ import { ReactNode } from 'react' -import Sidebar from '$/components/Sidebar' import { getSession } from '$/services/auth/getSession' import { ROUTES } from '$/services/routes' import { redirect } from 'next/navigation' @@ -14,12 +13,5 @@ export default async function PrivateLayout({ if (!data.session) { return redirect(ROUTES.auth.login) } - return ( -
-
- -
-
{children}
-
- ) + return children } diff --git a/apps/web/src/app/(private)/projects/[projectId]/commits/[commitUuid]/layout.tsx b/apps/web/src/app/(private)/projects/[projectId]/commits/[commitUuid]/layout.tsx new file mode 100644 index 000000000..17ee3b1d0 --- /dev/null +++ b/apps/web/src/app/(private)/projects/[projectId]/commits/[commitUuid]/layout.tsx @@ -0,0 +1,28 @@ +import { ReactNode } from 'react' + +import { CommitProvider } from '@latitude-data/web-ui' +import Sidebar from '$/components/Sidebar' +import { getCommitMergedAt } from '$core/data-access' + +export default async function PrivateLayout({ + children, + params, +}: { + children: ReactNode + params: { commitUuid: string; projectId: number } +}) { + const { commitUuid, projectId } = params + const commitMergeTime = await getCommitMergedAt({ projectId, commitUuid }) + const isDraft = commitMergeTime.unwrap() === null + + return ( + +
+
+ +
+
{children}
+
+
+ ) +} diff --git a/apps/web/src/app/(private)/projects/[projectId]/layout.tsx b/apps/web/src/app/(private)/projects/[projectId]/layout.tsx new file mode 100644 index 000000000..b0b841869 --- /dev/null +++ b/apps/web/src/app/(private)/projects/[projectId]/layout.tsx @@ -0,0 +1,15 @@ +import { ReactNode } from 'react' + +import { ProjectProvider } from '@latitude-data/web-ui' + +export default async function PrivateLayout({ + children, + params, +}: { + children: ReactNode + params: { projectId: number } +}) { + const { projectId } = params + + return {children} +} diff --git a/apps/web/src/app/api/commits/[commitUuid]/documents/route.ts b/apps/web/src/app/api/projects/[projectId]/commits/[commitUuid]/documents/route.ts similarity index 60% rename from apps/web/src/app/api/commits/[commitUuid]/documents/route.ts rename to apps/web/src/app/api/projects/[projectId]/commits/[commitUuid]/documents/route.ts index 41a125cf1..9f152dd40 100644 --- a/apps/web/src/app/api/commits/[commitUuid]/documents/route.ts +++ b/apps/web/src/app/api/projects/[projectId]/commits/[commitUuid]/documents/route.ts @@ -2,12 +2,14 @@ import { materializeDocumentsAtCommit } from '@latitude-data/core' import { NextRequest, NextResponse } from 'next/server' export async function GET( - req: NextRequest, - { commitUuid }: { commitUuid: string }, + _: NextRequest, + { commitUuid, projectId }: { commitUuid: string; projectId: number }, ) { try { - const staged = Boolean(req.nextUrl.searchParams.get('staged') || false) - const documents = await materializeDocumentsAtCommit({ commitUuid, staged }) + const documents = await materializeDocumentsAtCommit({ + commitUuid, + projectId, + }) return NextResponse.json(documents) } catch (err: unknown) { diff --git a/apps/web/src/components/Sidebar/DocumentTree/index.tsx b/apps/web/src/components/Sidebar/DocumentTree/index.tsx index 3f2f1d2cf..69f1bf477 100644 --- a/apps/web/src/components/Sidebar/DocumentTree/index.tsx +++ b/apps/web/src/components/Sidebar/DocumentTree/index.tsx @@ -2,6 +2,7 @@ import { faker } from '@faker-js/faker' import type { DocumentType, DocumentVersion } from '@latitude-data/core' +import { useCurrentCommit, useCurrentProject } from '@latitude-data/web-ui' import useDocumentVersions from '$/stores/documentVersions' import { Node, useTree } from '../toTree' @@ -20,16 +21,20 @@ export function CreateNode({ parentId }: { parentId?: number }) { } function CreateFolder({ parentId }: { parentId?: number }) { - const { create } = useDocumentVersions({ staged: true }) + const { commitUuid, isDraft } = useCurrentCommit() + const { projectId } = useCurrentProject() + const { create } = useDocumentVersions({ projectId, commitUuid }) return ( @@ -37,9 +42,17 @@ function CreateFolder({ parentId }: { parentId?: number }) { } function CreateDocument({ parentId }: { parentId?: number }) { - const { create } = useDocumentVersions({ staged: true }) + const { commitUuid, isDraft } = useCurrentCommit() + const { projectId } = useCurrentProject() + const { create } = useDocumentVersions({ projectId, commitUuid }) return ( - ) @@ -74,8 +87,10 @@ export default function DocumentTree({ }: { documents: DocumentVersion[] }) { + const { commitUuid } = useCurrentCommit() + const { projectId } = useCurrentProject() const { documents } = useDocumentVersions( - { staged: true }, + { commitUuid, projectId }, { fallbackData: serverDocuments }, ) const rootNode = useTree({ documents }) diff --git a/apps/web/src/components/Sidebar/index.tsx b/apps/web/src/components/Sidebar/index.tsx index c03362159..82a93b76a 100644 --- a/apps/web/src/components/Sidebar/index.tsx +++ b/apps/web/src/components/Sidebar/index.tsx @@ -1,12 +1,19 @@ -import { HEAD_COMMIT, materializeDocumentsAtCommit } from '@latitude-data/core' +import { materializeDocumentsAtCommit } from '@latitude-data/core' import DocumentTree, { CreateNode } from './DocumentTree' -export default async function Sidebar() { - const documents = await materializeDocumentsAtCommit({ - commitUuid: HEAD_COMMIT, - staged: true, +export default async function Sidebar({ + commitUuid, + projectId, +}: { + commitUuid: string + projectId: number +}) { + const documentsResult = await materializeDocumentsAtCommit({ + projectId, + commitUuid, }) + const documents = documentsResult.unwrap() return (
diff --git a/apps/web/src/stores/documentVersions.ts b/apps/web/src/stores/documentVersions.ts index 6afd8e2a7..8b8cda338 100644 --- a/apps/web/src/stores/documentVersions.ts +++ b/apps/web/src/stores/documentVersions.ts @@ -8,22 +8,17 @@ import { createDocumentVersionAction } from '$/actions/documents/create' import useSWR, { SWRConfiguration } from 'swr' import { useServerAction } from 'zsa-react' -const FIXME_HARDCODED_PROJECT_ID = 1 export default function useDocumentVersions( { - commitUuid = HEAD_COMMIT, - staged = false, + commitUuid, + projectId, }: { commitUuid?: string - staged?: boolean + projectId: number }, opts?: SWRConfiguration, ) { - const key = - `/api/commits/${commitUuid}/documents?` + - new URLSearchParams({ - staged: String(staged), - }).toString() + const key = `/api/projects/${projectId}/commits/${commitUuid ?? HEAD_COMMIT}/documents` const { mutate, data, ...rest } = useSWR( key, @@ -34,16 +29,15 @@ export default function useDocumentVersions( const { execute } = useServerAction(createDocumentVersionAction) const create = useCallback( async (payload: { - commitUuid?: string name: string documentType?: DocumentType parentId?: number }) => { const [document] = await execute({ ...payload, - projectId: FIXME_HARDCODED_PROJECT_ID, + projectId, name: payload.name!, - commitUuid: payload.commitUuid || HEAD_COMMIT, + commitUuid: commitUuid || HEAD_COMMIT, }) const prev = documents ?? [] diff --git a/packages/core/drizzle/0004_cold_ben_parker.sql b/packages/core/drizzle/0004_cold_ben_parker.sql new file mode 100644 index 000000000..4095b7668 --- /dev/null +++ b/packages/core/drizzle/0004_cold_ben_parker.sql @@ -0,0 +1,14 @@ +ALTER TABLE "latitude"."commits" DROP CONSTRAINT "commits_next_commit_id_commits_id_fk"; +--> statement-breakpoint +ALTER TABLE "latitude"."commits" DROP CONSTRAINT "commits_project_id_workspaces_id_fk"; +--> statement-breakpoint +DROP INDEX IF EXISTS "commit_next_commit_idx";--> statement-breakpoint +ALTER TABLE "latitude"."commits" ADD COLUMN "merged_at" timestamp;--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "latitude"."commits" ADD CONSTRAINT "commits_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "latitude"."projects"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "project_commit_order_idx" ON "latitude"."commits" USING btree ("merged_at","project_id");--> statement-breakpoint +ALTER TABLE "latitude"."commits" DROP COLUMN IF EXISTS "next_commit_id"; \ No newline at end of file diff --git a/packages/core/drizzle/meta/0004_snapshot.json b/packages/core/drizzle/meta/0004_snapshot.json new file mode 100644 index 000000000..d12a8c198 --- /dev/null +++ b/packages/core/drizzle/meta/0004_snapshot.json @@ -0,0 +1,801 @@ +{ + "id": "d9cf6cd2-f6a9-41e1-8acc-984d00e7afcd", + "prevId": "eedd6d42-c764-44b7-9e9e-2428f529d3dc", + "version": "7", + "dialect": "postgresql", + "tables": { + "latitude.document_hierarchies": { + "name": "document_hierarchies", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "depth": { + "name": "depth", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "child_id": { + "name": "child_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "latitude.users": { + "name": "users", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_password": { + "name": "encrypted_password", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + } + }, + "latitude.sessions": { + "name": "sessions", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "schemaTo": "latitude", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "latitude.workspaces": { + "name": "workspaces", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspaces_creator_id_users_id_fk": { + "name": "workspaces_creator_id_users_id_fk", + "tableFrom": "workspaces", + "tableTo": "users", + "schemaTo": "latitude", + "columnsFrom": [ + "creator_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "latitude.memberships": { + "name": "memberships", + "schema": "latitude", + "columns": { + "workspace_id": { + "name": "workspace_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "memberships_workspace_id_workspaces_id_fk": { + "name": "memberships_workspace_id_workspaces_id_fk", + "tableFrom": "memberships", + "tableTo": "workspaces", + "schemaTo": "latitude", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "memberships_user_id_users_id_fk": { + "name": "memberships_user_id_users_id_fk", + "tableFrom": "memberships", + "tableTo": "users", + "schemaTo": "latitude", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "memberships_workspace_id_user_id_pk": { + "name": "memberships_workspace_id_user_id_pk", + "columns": [ + "workspace_id", + "user_id" + ] + } + }, + "uniqueConstraints": {} + }, + "latitude.api_keys": { + "name": "api_keys", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workspace_id_idx": { + "name": "workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_keys_workspace_id_workspaces_id_fk": { + "name": "api_keys_workspace_id_workspaces_id_fk", + "tableFrom": "api_keys", + "tableTo": "workspaces", + "schemaTo": "latitude", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_keys_uuid_unique": { + "name": "api_keys_uuid_unique", + "nullsNotDistinct": false, + "columns": [ + "uuid" + ] + } + } + }, + "latitude.projects": { + "name": "projects", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_idx": { + "name": "workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_workspace_id_workspaces_id_fk": { + "name": "projects_workspace_id_workspaces_id_fk", + "tableFrom": "projects", + "tableTo": "workspaces", + "schemaTo": "latitude", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "latitude.commits": { + "name": "commits", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "merged_at": { + "name": "merged_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_commit_order_idx": { + "name": "project_commit_order_idx", + "columns": [ + { + "expression": "merged_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "commits_project_id_projects_id_fk": { + "name": "commits_project_id_projects_id_fk", + "tableFrom": "commits", + "tableTo": "projects", + "schemaTo": "latitude", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "commits_uuid_unique": { + "name": "commits_uuid_unique", + "nullsNotDistinct": false, + "columns": [ + "uuid" + ] + } + } + }, + "latitude.document_snapshots": { + "name": "document_snapshots", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "commit_id": { + "name": "commit_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "document_version_id": { + "name": "document_version_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "prompt_commit_idx": { + "name": "prompt_commit_idx", + "columns": [ + { + "expression": "commit_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_snapshot_document_version_idx": { + "name": "document_snapshot_document_version_idx", + "columns": [ + { + "expression": "document_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_snapshots_commit_id_commits_id_fk": { + "name": "document_snapshots_commit_id_commits_id_fk", + "tableFrom": "document_snapshots", + "tableTo": "commits", + "schemaTo": "latitude", + "columnsFrom": [ + "commit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "document_snapshots_document_version_id_document_versions_id_fk": { + "name": "document_snapshots_document_version_id_document_versions_id_fk", + "tableFrom": "document_snapshots", + "tableTo": "document_versions", + "schemaTo": "latitude", + "columnsFrom": [ + "document_version_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "latitude.document_versions": { + "name": "document_versions", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "document_type": { + "name": "document_type", + "type": "document_type", + "typeSchema": "latitude", + "primaryKey": false, + "notNull": true, + "default": "'document'" + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "hash": { + "name": "hash", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "document_uuid": { + "name": "document_uuid", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "parent_id": { + "name": "parent_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "commit_id": { + "name": "commit_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "document_versions_parent_id_document_versions_id_fk": { + "name": "document_versions_parent_id_document_versions_id_fk", + "tableFrom": "document_versions", + "tableTo": "document_versions", + "schemaTo": "latitude", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_versions_commit_id_commits_id_fk": { + "name": "document_versions_commit_id_commits_id_fk", + "tableFrom": "document_versions", + "tableTo": "commits", + "schemaTo": "latitude", + "columnsFrom": [ + "commit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": { + "latitude.document_type": { + "name": "document_type", + "schema": "latitude", + "values": [ + "document", + "folder" + ] + } + }, + "schemas": { + "latitude": "latitude" + }, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/core/drizzle/meta/_journal.json b/packages/core/drizzle/meta/_journal.json index ba3a397a1..5696de412 100644 --- a/packages/core/drizzle/meta/_journal.json +++ b/packages/core/drizzle/meta/_journal.json @@ -29,6 +29,13 @@ "when": 1721124762014, "tag": "0003_cold_spirit", "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1721290010434, + "tag": "0004_cold_ben_parker", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/core/src/data-access/commits.ts b/packages/core/src/data-access/commits.ts index e605596ad..d14d18c70 100644 --- a/packages/core/src/data-access/commits.ts +++ b/packages/core/src/data-access/commits.ts @@ -1,26 +1,73 @@ import { database } from '$core/client' import { HEAD_COMMIT } from '$core/constants' -import { commits } from '$core/schema' -import { desc, eq, isNull } from 'drizzle-orm' +import { Result, TypedResult } from '$core/lib' +import { LatitudeError, NotFoundError } from '$core/lib/errors' +import { Commit, commits } from '$core/schema' +import { and, desc, eq, isNotNull } from 'drizzle-orm' -const selectCondition = (uuid?: Exclude) => { - if (!uuid) return isNull(commits.nextCommitId) +export async function findHeadCommit( + { projectId }: { projectId: number }, + tx = database, +): Promise> { + const result = await tx + .select() + .from(commits) + .where(and(isNotNull(commits.mergedAt), eq(commits.projectId, projectId))) + .orderBy(desc(commits.mergedAt)) + .limit(1) - return eq(commits.uuid, uuid) + if (result.length < 1) { + return Result.error(new NotFoundError('No head commit found')) + } + + const headCommit = result[0]! + return Result.ok(headCommit) } -export async function findCommit({ uuid }: { uuid?: string }, tx = database) { - if (uuid === HEAD_COMMIT) { - return tx.query.commits.findFirst({ orderBy: desc(commits.id) }) - } +export async function findCommit( + { projectId, commitUuid }: { projectId: number; commitUuid: string }, + tx = database, +): Promise> { + if (commitUuid === HEAD_COMMIT) return findHeadCommit({ projectId }, tx) - return tx.query.commits.findFirst({ where: selectCondition(uuid) }) + const commit = await tx.query.commits.findFirst({ + where: eq(commits.uuid, commitUuid), + }) + + if (!commit) return Result.error(new NotFoundError('Commit not found')) + return Result.ok(commit) } export async function listCommits() { return database.select().from(commits) } -export async function listStagedCommits() { - return database.select().from(commits).where(isNull(commits.nextCommitId)) +export async function getCommitMergedAt( + { projectId, commitUuid }: { projectId: number; commitUuid: string }, + tx = database, +): Promise> { + if (commitUuid === HEAD_COMMIT) { + const result = await tx + .select({ mergedAt: commits.mergedAt }) + .from(commits) + .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 tx + .select({ mergedAt: commits.mergedAt }) + .from(commits) + .where(eq(commits.uuid, commitUuid)) + .limit(1) + + if (!result.length) return Result.error(new NotFoundError('Commit not found')) + + const commit = result[0]! + return Result.ok(commit.mergedAt) } diff --git a/packages/core/src/data-access/documentVersions.test.ts b/packages/core/src/data-access/documentVersions.test.ts new file mode 100644 index 000000000..60748ff5c --- /dev/null +++ b/packages/core/src/data-access/documentVersions.test.ts @@ -0,0 +1,152 @@ +import { HEAD_COMMIT } from '$core/constants' +import mergeCommit from '$core/services/commits/merge' +import useTestDatabase from '$core/tests/useTestDatabase' +import { describe, expect, it } from 'vitest' + +import { getDocumentsAtCommit } from './documentVersions' + +useTestDatabase() + +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 { documentVersion: doc } = await ctx.factories.createDocumentVersion({ + commit, + }) + await mergeCommit({ commitId: commit.id }) + + const result = await getDocumentsAtCommit({ + commitUuid: commit.uuid, + projectId: project.id, + }) + const documents = result.unwrap() + + expect(documents.length).toBe(1) + expect(documents[0]!.id).toBe(doc.id) + }) + + it('returns the right document version for each commit', async (ctx) => { + const { project } = await ctx.factories.createProject() + + const { commit: commit1 } = await ctx.factories.createDraft({ project }) + const { documentVersion: doc } = await ctx.factories.createDocumentVersion({ + commit: commit1, + content: 'VERSION 1', + }) + + const { commit: commit2 } = await ctx.factories.createDraft({ project }) + await ctx.factories.createDocumentVersion({ + commit: commit2, + documentUuid: doc.documentUuid, + content: 'VERSION 2', + }) + + const { commit: commit3 } = await ctx.factories.createDraft({ project }) + await ctx.factories.createDocumentVersion({ + commit: commit3, + documentUuid: doc.documentUuid, + content: 'VERSION 3', + }) + + // Commit 1 is merged AFTER commit 2 + // Commit 3 is not merged + await mergeCommit({ commitId: commit2.id }) + await mergeCommit({ commitId: commit1.id }) + + const commit1Result = await getDocumentsAtCommit({ + commitUuid: commit1.uuid, + projectId: project.id, + }) + const commit1Docs = commit1Result.unwrap() + expect(commit1Docs.length).toBe(1) + expect(commit1Docs[0]!.content).toBe('VERSION 1') + + const commit2Result = await getDocumentsAtCommit({ + commitUuid: commit2.uuid, + projectId: project.id, + }) + const commit2Docs = commit2Result.unwrap() + expect(commit2Docs.length).toBe(1) + expect(commit2Docs[0]!.content).toBe('VERSION 2') + + const commit3Result = await getDocumentsAtCommit({ + commitUuid: commit3.uuid, + projectId: project.id, + }) + const commit3Docs = commit3Result.unwrap() + expect(commit3Docs.length).toBe(1) + expect(commit3Docs[0]!.content).toBe('VERSION 3') + + const headResult = await getDocumentsAtCommit({ + commitUuid: HEAD_COMMIT, + projectId: project.id, + }) + const headDocs = headResult.unwrap() + expect(headDocs.length).toBe(1) + expect(headDocs[0]!.content).toBe('VERSION 1') + }) + + it('returns documents that were last modified in a previous commit', async (ctx) => { + const { project } = await ctx.factories.createProject() + + const { commit: commit1 } = await ctx.factories.createDraft({ project }) + await ctx.factories.createDocumentVersion({ + commit: commit1, + content: 'Doc 1 commit 1', + }) + await mergeCommit({ commitId: commit1.id }) + + 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 }) + + const { commit: commit3 } = await ctx.factories.createDraft({ project }) + await ctx.factories.createDocumentVersion({ + commit: commit3, + documentUuid: doc2.documentUuid, + content: 'Doc 2 commit 3 (draft)', + }) + + const commit1Result = await getDocumentsAtCommit({ + commitUuid: commit1.uuid, + projectId: project.id, + }) + const commit1Docs = commit1Result.unwrap() + expect(commit1Docs.length).toBe(1) + const commit1DocContents = commit1Docs.map((d) => d.content) + expect(commit1DocContents).toContain('Doc 1 commit 1') + + const commit2Result = await getDocumentsAtCommit({ + commitUuid: commit2.uuid, + projectId: project.id, + }) + const commit2Docs = commit2Result.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 commit3Result = await getDocumentsAtCommit({ + commitUuid: commit3.uuid, + projectId: project.id, + }) + const commit3Docs = commit3Result.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 headResult = await getDocumentsAtCommit({ + commitUuid: HEAD_COMMIT, + projectId: project.id, + }) + const headDocs = headResult.unwrap() + expect(headDocs.length).toBe(2) + const headDocContents = headDocs.map((d) => d.content) + expect(headDocContents).toContain('Doc 1 commit 1') + expect(headDocContents).toContain('Doc 2 commit 2') + }) +}) diff --git a/packages/core/src/data-access/documentVersions.ts b/packages/core/src/data-access/documentVersions.ts index e1ad4f3e3..13a14522a 100644 --- a/packages/core/src/data-access/documentVersions.ts +++ b/packages/core/src/data-access/documentVersions.ts @@ -1,48 +1,124 @@ -import { commits, database, documentVersions } from '@latitude-data/core' -import { desc, eq, inArray, lte } from 'drizzle-orm' +import { + commits, + database, + DocumentVersion, + documentVersions, + findCommit, + getCommitMergedAt, + Result, + TypedResult, +} from '@latitude-data/core' +import { LatitudeError, NotFoundError } from '$core/lib/errors' +import { and, eq, isNotNull, lte, max } from 'drizzle-orm' -import { listStagedCommits } from './commits' +export async function getDocumentsAtCommit( + { commitUuid, projectId }: { commitUuid: string; projectId: number }, + tx = database, +): Promise> { + const maxMergedAtResult = await getCommitMergedAt({ commitUuid, projectId }) + if (maxMergedAtResult.error) return maxMergedAtResult + const maxMergedAt = maxMergedAtResult.unwrap() -export async function listStagedDocuments() { - const commits = await listStagedCommits() - if (!commits.length) return [] + const whereStatement = () => { + const mergedAtNotNull = isNotNull(commits.mergedAt) + if (!maxMergedAt) { + return mergedAtNotNull + } + return and(mergedAtNotNull, lte(commits.mergedAt, maxMergedAt)) + } - return database + 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(whereStatement()) + .groupBy(documentVersions.documentUuid), + ) + + const documentsAtPreviousMergedCommitsResult = await tx + .with(lastVersionOfEachDocument) .select() .from(documentVersions) - .where( - inArray( - documentVersions.commitId, - commits.map((c) => c.id), + .innerJoin( + commits, + and( + eq(commits.id, documentVersions.commitId), + isNotNull(commits.mergedAt), ), ) -} + .innerJoin( + lastVersionOfEachDocument, + and( + eq( + documentVersions.documentUuid, + lastVersionOfEachDocument.documentUuid, + ), + eq(commits.mergedAt, lastVersionOfEachDocument.mergedAt), + ), + ) + + const documentsAtPreviousMergedCommits = + documentsAtPreviousMergedCommitsResult.map((d) => d.document_versions) -export async function getDocumentsAtCommit(commitUuid: string) { - const referenceCommitId = await database - .select({ id: commits.id }) - .from(commits) - .where(eq(commits.uuid, commitUuid)) + if (maxMergedAt) { + // Referenced commit is merged. No additional documents to return. + return Result.ok(documentsAtPreviousMergedCommits) + } - if (referenceCommitId.length === 0) return [] + const commitResult = await findCommit({ projectId, commitUuid }) + if (commitResult.error) return commitResult - const commitIdsBeforeReferenceCommit = await database - .select({ id: commits.id }) - .from(commits) - .where(lte(commits.id, referenceCommitId[0]!.id)) + const commit = commitResult.unwrap() - const docsInCommits = await database - .selectDistinct() + const documentsAtDraftResult = await tx + .select() .from(documentVersions) .innerJoin(commits, eq(commits.id, documentVersions.commitId)) + .where(eq(commits.id, commit.id)) + + const documentsAtDraft = documentsAtDraftResult.map( + (d) => d.document_versions, + ) + const totalDocuments = documentsAtPreviousMergedCommits + .filter((d) => + documentsAtDraft.find((d2) => d2.documentUuid !== d.documentUuid), + ) + .concat(documentsAtDraft) + + return Result.ok(totalDocuments) +} + +export async function getDocument({ + projectId, + commitUuid, + documentId, +}: { + projectId: number + commitUuid: string + documentId: number +}): Promise> { + const commitResult = await findCommit({ commitUuid, projectId }) + if (commitResult.error) return commitResult + const commit = commitResult.unwrap() + + const result = await database + .select({ content: documentVersions.content }) + .from(documentVersions) .where( - inArray( - commits.id, - commitIdsBeforeReferenceCommit.map((d) => d.id), + and( + eq(documentVersions.id, documentId), + eq(documentVersions.commitId, commit.id), ), ) - .groupBy(documentVersions.documentUuid) - .orderBy(desc(documentVersions.commitId)) - return docsInCommits.map((doc) => doc.document_versions) + if (result.length === 0) { + return Result.error(new NotFoundError('Document not found')) + } + + const documentVersion = result[0]! + return Result.ok({ content: documentVersion.content ?? '' }) } diff --git a/packages/core/src/schema/models/commits.ts b/packages/core/src/schema/models/commits.ts index a40dd5418..3d8bf1dcb 100644 --- a/packages/core/src/schema/models/commits.ts +++ b/packages/core/src/schema/models/commits.ts @@ -1,15 +1,15 @@ import { InferSelectModel, relations, sql } from 'drizzle-orm' import { - AnyPgColumn, bigint, bigserial, index, text, + timestamp, uuid, varchar, } from 'drizzle-orm/pg-core' -import { documentSnapshots, latitudeSchema, projects, workspaces } from '..' +import { documentSnapshots, latitudeSchema, projects } from '..' import { timestamps } from '../schemaHelpers' export const commits = latitudeSchema.table( @@ -20,19 +20,19 @@ export const commits = latitudeSchema.table( .notNull() .unique() .default(sql`gen_random_uuid()`), - nextCommitId: bigint('next_commit_id', { mode: 'number' }).references( - (): AnyPgColumn => commits.id, - { onDelete: 'restrict' }, - ), title: varchar('title', { length: 256 }), description: text('description'), projectId: bigint('project_id', { mode: 'number' }) .notNull() - .references(() => workspaces.id, { onDelete: 'cascade' }), + .references(() => projects.id, { onDelete: 'cascade' }), + mergedAt: timestamp('merged_at'), ...timestamps(), }, (table) => ({ - nextCommitIdx: index('commit_next_commit_idx').on(table.nextCommitId), + projectCommitOrderIdx: index('project_commit_order_idx').on( + table.mergedAt, + table.projectId, + ), }), ) diff --git a/packages/core/src/schema/models/projects.ts b/packages/core/src/schema/models/projects.ts index 16395fd5d..ade049721 100644 --- a/packages/core/src/schema/models/projects.ts +++ b/packages/core/src/schema/models/projects.ts @@ -15,7 +15,7 @@ export const projects = latitudeSchema.table( ...timestamps(), }, (table) => ({ - nextCommitIdx: index('workspace_idx').on(table.workspaceId), + projectWorkspaceIdx: index('workspace_idx').on(table.workspaceId), }), ) diff --git a/packages/core/src/services/commits/merge.ts b/packages/core/src/services/commits/merge.ts new file mode 100644 index 000000000..771c4e087 --- /dev/null +++ b/packages/core/src/services/commits/merge.ts @@ -0,0 +1,46 @@ +import { + Commit, + commits, + database, + Result, + Transaction, +} from '@latitude-data/core' +import { LatitudeError, NotFoundError } from '$core/lib/errors' +import { and, eq } from 'drizzle-orm' + +export default async function mergeCommit( + { commitId }: { commitId: number }, + 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), + ), + }) + + if (otherCommits.length > 0) + return Result.error( + new LatitudeError('Commit merge time conflict, try again'), + ) + + const result = await tx + .update(commits) + .set({ mergedAt }) + .where(eq(commits.id, commitId)) + .returning() + const updatedCommit = result[0]! + + return Result.ok(updatedCommit) + }, db) +} diff --git a/packages/core/src/services/documentVersions/create.ts b/packages/core/src/services/documentVersions/create.ts index 0635c86d0..51b6671e4 100644 --- a/packages/core/src/services/documentVersions/create.ts +++ b/packages/core/src/services/documentVersions/create.ts @@ -6,19 +6,22 @@ import { Transaction, type DocumentType, } from '@latitude-data/core' - -import createCommit from '../commits/create' +import { ForbiddenError } from '$core/lib/errors' function createDocument({ name, commitId, parentId, documentType, + documentUuid, + content, }: { name: string commitId: number parentId?: number documentType?: DocumentType + documentUuid?: string + content?: string }) { return Transaction.call(async (tx) => { const result = await tx @@ -28,6 +31,8 @@ function createDocument({ commitId, parentId, documentType, + documentUuid, + content, }) .returning() const documentVersion = result[0] @@ -36,32 +41,37 @@ function createDocument({ } export async function createDocumentVersion({ + documentUuid, projectId, name, commitUuid, documentType, parentId, + content, }: { + documentUuid?: string projectId: number name: string commitUuid: string documentType?: DocumentType parentId?: number + content?: string }) { - let commit = await findCommit({ uuid: commitUuid }) - return Transaction.call(async (tx) => { - if (!commit) { - const resultCommit = await createCommit({ projectId, db: tx }) - if (resultCommit.error) return resultCommit + const commitResult = await findCommit({ commitUuid, projectId }) + const commit = commitResult.unwrap() - commit = resultCommit.value - } + if (commit.mergedAt !== null) { + return Result.error( + new ForbiddenError('Cannot create a document version in a merged commit'), + ) + } - return createDocument({ - name, - commitId: commit.id, - parentId, - documentType, - }) + return createDocument({ + documentUuid, + name, + commitId: commit.id, + parentId, + documentType, + content, }) } diff --git a/packages/core/src/services/documentVersions/materializeAtCommit.ts b/packages/core/src/services/documentVersions/materializeAtCommit.ts index a7490097e..e0c9f91bc 100644 --- a/packages/core/src/services/documentVersions/materializeAtCommit.ts +++ b/packages/core/src/services/documentVersions/materializeAtCommit.ts @@ -1,28 +1,20 @@ -import { uniqBy } from 'lodash-es' - import { HEAD_COMMIT } from '$core/constants' -import { - getDocumentsAtCommit, - listdocumentSnapshots, - listStagedDocuments, -} from '$core/data-access' +import { getDocumentsAtCommit, listdocumentSnapshots } from '$core/data-access' +import { Result } from '$core/lib' export async function materializeDocumentsAtCommit({ commitUuid = HEAD_COMMIT, - staged = true, + projectId, }: { commitUuid: string - staged: boolean + projectId: number }) { if (commitUuid === HEAD_COMMIT) { const snapshots = (await listdocumentSnapshots()).map( (snap) => snap.document_versions, ) - if (!staged) return snapshots - - const versions = await listStagedDocuments() - return uniqBy([...versions, ...snapshots], (doc) => doc.documentUuid) + return Result.ok(snapshots) } else { - return await getDocumentsAtCommit(commitUuid) + return await getDocumentsAtCommit({ commitUuid, projectId }) } } diff --git a/packages/core/src/tests/factories/documents.ts b/packages/core/src/tests/factories/documents.ts new file mode 100644 index 000000000..fcd79539e --- /dev/null +++ b/packages/core/src/tests/factories/documents.ts @@ -0,0 +1,49 @@ +import { faker } from '@faker-js/faker' +import { DocumentType } from '$core/constants' +import type { Commit, DocumentVersion } from '$core/schema' +import { createDocumentVersion as createDocumentVersionFn } from '$core/services/documentVersions/create' + +export type IDocumentVersionData = { + commit: Commit + documentUuid?: string + name?: string + content?: string + parentFolder?: DocumentVersion + type?: DocumentType +} + +function makeRandomDocumentVersionData() { + return { + name: faker.commerce.department(), + content: faker.lorem.paragraphs(), + } +} + +export async function createDocumentVersion( + documentData: IDocumentVersionData, +) { + const randomData = makeRandomDocumentVersionData() + + const data = { + ...randomData, + type: DocumentType.Document, + content: + documentData.type === DocumentType.Folder + ? undefined + : randomData.content, + ...documentData, + } + + const result = await createDocumentVersionFn({ + projectId: data.commit.projectId, + name: data.name, + commitUuid: data.commit.uuid, + documentType: data.type, + parentId: data.parentFolder?.id, + documentUuid: data.documentUuid, + content: data.content, + }) + + const documentVersion = result.unwrap() + return { documentVersion } +} diff --git a/packages/core/src/tests/factories/index.ts b/packages/core/src/tests/factories/index.ts index 29da19130..a7b0ae42e 100644 --- a/packages/core/src/tests/factories/index.ts +++ b/packages/core/src/tests/factories/index.ts @@ -2,3 +2,4 @@ export * from './users' export * from './workspaces' export * from './projects' export * from './commits' +export * from './documents' diff --git a/packages/core/src/tests/tmp.test.ts b/packages/core/src/tests/tmp.test.ts deleted file mode 100644 index 2382870c5..000000000 --- a/packages/core/src/tests/tmp.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { describe, expect, it } from 'vitest' - -// TODO: Remove when adding an actual test suite -describe('placeholder', () => { - it('works', () => { - expect(true).toBe(true) - }) -}) diff --git a/packages/web-ui/src/index.ts b/packages/web-ui/src/index.ts index 3b623a7cc..ff7a6152a 100644 --- a/packages/web-ui/src/index.ts +++ b/packages/web-ui/src/index.ts @@ -3,3 +3,4 @@ export * from './ds/atoms' export * from './ds/molecules' export * from './layouts' export * from './sections' +export * from './providers' diff --git a/packages/web-ui/src/providers/CommitProvider.tsx b/packages/web-ui/src/providers/CommitProvider.tsx new file mode 100644 index 000000000..47ffca55a --- /dev/null +++ b/packages/web-ui/src/providers/CommitProvider.tsx @@ -0,0 +1,36 @@ +'use client' + +import { createContext, ReactNode, useContext } from 'react' + +import { HEAD_COMMIT } from '@latitude-data/core/browser' + +interface CommitContextType { + commitUuid: string | typeof HEAD_COMMIT + isDraft: boolean +} + +const CommitContext = createContext({ + commitUuid: HEAD_COMMIT, + isDraft: false, +}) + +const CommitProvider = ({ + children, + ...context +}: { + children: ReactNode +} & CommitContextType) => { + return ( + {children} + ) +} + +const useCurrentCommit = () => { + const context = useContext(CommitContext) + if (!context) { + throw new Error('useCurrentCommit must be used within a CommitProvider') + } + return context +} + +export { CommitProvider, useCurrentCommit } diff --git a/packages/web-ui/src/providers/ProjectProvider.tsx b/packages/web-ui/src/providers/ProjectProvider.tsx new file mode 100644 index 000000000..8417e0b44 --- /dev/null +++ b/packages/web-ui/src/providers/ProjectProvider.tsx @@ -0,0 +1,34 @@ +'use client' + +import { createContext, ReactNode, useContext } from 'react' + +interface ProjectContextType { + projectId: number +} + +const ProjectContext = createContext({ + projectId: 0, +}) + +const ProjectProvider = ({ + children, + ...context +}: { + children: ReactNode +} & ProjectContextType) => { + return ( + + {children} + + ) +} + +const useCurrentProject = () => { + const context = useContext(ProjectContext) + if (!context) { + throw new Error('useCurrentProject must be used within a ProjectProvider') + } + return context +} + +export { ProjectProvider, useCurrentProject } diff --git a/packages/web-ui/src/providers/index.ts b/packages/web-ui/src/providers/index.ts new file mode 100644 index 000000000..e49618d50 --- /dev/null +++ b/packages/web-ui/src/providers/index.ts @@ -0,0 +1,2 @@ +export * from './CommitProvider' +export * from './ProjectProvider'