From c026196ee73bdacbf9e3b327e1aad95342bfd132 Mon Sep 17 00:00:00 2001 From: andresgutgon Date: Thu, 5 Sep 2024 14:35:05 +0200 Subject: [PATCH] Implement datasets data model --- apps/web/src/actions/datasets/create.ts | 21 +- apps/web/src/actions/datasets/destroy.ts | 23 + apps/web/src/actions/datasets/fetch.ts | 12 + .../src/app/(private)/dashboard/layout.tsx | 2 +- .../_components/DatasetsTable/index.tsx | 76 + .../_components/DeleteDatasetModal/index.tsx | 35 + .../web/src/app/(private)/datasets/layout.tsx | 33 +- .../src/app/(private)/datasets/new/page.tsx | 17 +- .../layouts/AppLayout/Header/index.tsx | 40 +- .../components/modals/DestroyModal/index.tsx | 31 +- apps/web/src/stores/datasets.ts | 72 + package.json | 1 + packages/core/drizzle/0044_needy_the_hood.sql | 27 + packages/core/drizzle/meta/0044_snapshot.json | 1988 +++++++++++++++++ packages/core/drizzle/meta/_journal.json | 7 + packages/core/package.json | 2 + packages/core/src/lib/disk.ts | 4 + packages/core/src/lib/readCsv.ts | 34 + .../src/repositories/datasetsRepository.ts | 33 + packages/core/src/repositories/index.ts | 1 + packages/core/src/schema/index.ts | 1 + packages/core/src/schema/models/datasets.ts | 43 + packages/core/src/schema/relations.ts | 18 +- packages/core/src/schema/types.ts | 5 + packages/core/src/services/datasets/create.ts | 50 +- .../core/src/services/datasets/destroy.ts | 31 + .../web-ui/src/ds/atoms/Dropzone/index.tsx | 35 +- .../src/ds/atoms/DropzoneInput/index.tsx | 6 +- packages/web-ui/src/ds/atoms/Table/index.tsx | 89 +- packages/web-ui/src/index.ts | 1 + packages/web-ui/src/lib/dateUtils.ts | 23 + .../web-ui/src/lib/hooks/useCombineRefs.ts | 34 + pnpm-lock.yaml | 11 + 33 files changed, 2681 insertions(+), 125 deletions(-) create mode 100644 apps/web/src/actions/datasets/destroy.ts create mode 100644 apps/web/src/actions/datasets/fetch.ts create mode 100644 apps/web/src/app/(private)/datasets/_components/DatasetsTable/index.tsx create mode 100644 apps/web/src/app/(private)/datasets/_components/DeleteDatasetModal/index.tsx create mode 100644 apps/web/src/stores/datasets.ts create mode 100644 packages/core/drizzle/0044_needy_the_hood.sql create mode 100644 packages/core/drizzle/meta/0044_snapshot.json create mode 100644 packages/core/src/lib/readCsv.ts create mode 100644 packages/core/src/repositories/datasetsRepository.ts create mode 100644 packages/core/src/schema/models/datasets.ts create mode 100644 packages/core/src/services/datasets/destroy.ts create mode 100644 packages/web-ui/src/lib/dateUtils.ts create mode 100644 packages/web-ui/src/lib/hooks/useCombineRefs.ts diff --git a/apps/web/src/actions/datasets/create.ts b/apps/web/src/actions/datasets/create.ts index 71d918204..f52aebe49 100644 --- a/apps/web/src/actions/datasets/create.ts +++ b/apps/web/src/actions/datasets/create.ts @@ -6,12 +6,6 @@ import { z } from 'zod' import { authProcedure } from '../procedures' -const ACCEPTED_FILE_TYPES = [ - 'text/csv', - 'application/vnd.ms-excel', - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'application/vnd.oasis.opendocument.spreadsheet', -] const MAX_SIZE = 3 const MAX_UPLOAD_SIZE_IN_MB = 3 * 1024 * 1024 @@ -25,22 +19,23 @@ export const createDatasetAction = authProcedure .refine((file) => { return !file || file.size <= MAX_UPLOAD_SIZE_IN_MB }, `Your dataset must be less than ${MAX_SIZE}MB in size`) - .refine((file) => { - return ACCEPTED_FILE_TYPES.includes(file.type) - }, 'Your dataset must be an Excel or CSV file'), + .refine( + (file) => file.type === 'text/csv', + 'Your dataset must be a CSV file', + ), }), { type: 'formData' }, ) .handler(async ({ input, ctx }) => { - const result = await createDataset({ + return createDataset({ workspace: ctx.workspace, author: ctx.user, disk: disk, data: { name: input.name, file: input.dataset_file, + // TODO: Make UI radio button to pick delimiter + csvDelimiter: ';', }, - }) - - return result.unwrap() + }).then((r) => r.unwrap()) }) diff --git a/apps/web/src/actions/datasets/destroy.ts b/apps/web/src/actions/datasets/destroy.ts new file mode 100644 index 000000000..039d190ba --- /dev/null +++ b/apps/web/src/actions/datasets/destroy.ts @@ -0,0 +1,23 @@ +'use server' + +import { DatasetsRepository } from '@latitude-data/core/repositories' +import { destroyDataset } from '@latitude-data/core/services/datasets/destroy' +import disk from '$/lib/disk' +import { z } from 'zod' + +import { authProcedure } from '../procedures' + +export const destroyDatasetAction = authProcedure + .createServerAction() + .input( + z.object({ + id: z.string(), + }), + ) + .handler(async ({ input, ctx }) => { + const { id } = input + const repo = new DatasetsRepository(ctx.workspace.id) + const dataset = await repo.find(id).then((r) => r.unwrap()) + + return await destroyDataset({ dataset, disk }).then((r) => r.unwrap()) + }) diff --git a/apps/web/src/actions/datasets/fetch.ts b/apps/web/src/actions/datasets/fetch.ts new file mode 100644 index 000000000..52b090397 --- /dev/null +++ b/apps/web/src/actions/datasets/fetch.ts @@ -0,0 +1,12 @@ +'use server' + +import { DatasetsRepository } from '@latitude-data/core/repositories' + +import { authProcedure } from '../procedures' + +export const getDatasetsAction = authProcedure + .createServerAction() + .handler(async ({ ctx }) => { + const scope = new DatasetsRepository(ctx.workspace.id) + return await scope.findAll().then((r) => r.unwrap()) + }) diff --git a/apps/web/src/app/(private)/dashboard/layout.tsx b/apps/web/src/app/(private)/dashboard/layout.tsx index 79a010ebd..f4338731e 100644 --- a/apps/web/src/app/(private)/dashboard/layout.tsx +++ b/apps/web/src/app/(private)/dashboard/layout.tsx @@ -40,7 +40,7 @@ export default async function DashboardLayout({ return ( diff --git a/apps/web/src/app/(private)/datasets/_components/DatasetsTable/index.tsx b/apps/web/src/app/(private)/datasets/_components/DatasetsTable/index.tsx new file mode 100644 index 000000000..67f5d4b7c --- /dev/null +++ b/apps/web/src/app/(private)/datasets/_components/DatasetsTable/index.tsx @@ -0,0 +1,76 @@ +'use client' + +import { useState } from 'react' + +import { Dataset } from '@latitude-data/core/browser' +import { + Button, + dateFormatter, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + Text, +} from '@latitude-data/web-ui' +import DeleteDatasetModal from '$/app/(private)/datasets/_components/DeleteDatasetModal' +import useDatasets from '$/stores/datasets' + +export function DatasetsTable({ + datasets: serverDatasets, +}: { + datasets: Dataset[] +}) { + const [deletable, setDeletable] = useState(null) + const { data: datasets } = useDatasets(undefined, { + fallbackData: serverDatasets, + }) + return ( + <> + + + + + Name + Rows + Columns + Author + Created at + + + + + {datasets.map((dataset) => ( + + + {dataset.name} + + + {dataset.fileMetadata.rowCount} + + + {dataset.fileMetadata.headers.length} + + + {dataset.author?.name} + + + + {dateFormatter.formatDate(dataset.createdAt)} + + + +
+ + ) +} diff --git a/apps/web/src/app/(private)/datasets/_components/DeleteDatasetModal/index.tsx b/apps/web/src/app/(private)/datasets/_components/DeleteDatasetModal/index.tsx new file mode 100644 index 000000000..53ef3c087 --- /dev/null +++ b/apps/web/src/app/(private)/datasets/_components/DeleteDatasetModal/index.tsx @@ -0,0 +1,35 @@ +import { Dataset } from '@latitude-data/core/browser' +import { ReactStateDispatch } from '@latitude-data/web-ui' +import { destroyDatasetAction } from '$/actions/datasets/destroy' +import DestroyModal from '$/components/modals/DestroyModal' +import useDatasets from '$/stores/datasets' +import { useRouter } from 'next/navigation' + +export default function DeleteDatasetModal({ + dataset, + setDataset, +}: { + dataset: Dataset | null + setDataset: ReactStateDispatch +}) { + const router = useRouter() + const { data, destroy } = useDatasets() + const isLast = data?.length === 1 + if (!dataset) return null + + return ( + + title={`Delete ${dataset.name}`} + description='Deleted datasets will no longer accessible to generate new evaluations.' + onOpenChange={(open: boolean) => !open && setDataset(null)} + action={destroy} + submitStr='Delete dataset' + model={dataset} + onSuccess={() => { + if (isLast) router.refresh() + + setDataset(null) + }} + /> + ) +} diff --git a/apps/web/src/app/(private)/datasets/layout.tsx b/apps/web/src/app/(private)/datasets/layout.tsx index 48e6577d5..418663374 100644 --- a/apps/web/src/app/(private)/datasets/layout.tsx +++ b/apps/web/src/app/(private)/datasets/layout.tsx @@ -1,11 +1,13 @@ import { ReactNode } from 'react' +import { DatasetsRepository } from '@latitude-data/core/repositories' import { Container, TableBlankSlate, TableWithHeader, Text, } from '@latitude-data/web-ui' +import { DatasetsTable } from '$/app/(private)/datasets/_components/DatasetsTable' import { AppLayout } from '$/components/layouts' import { getCurrentUser } from '$/services/auth/getCurrentUser' import { ROUTES } from '$/services/routes' @@ -19,11 +21,12 @@ export default async function DatasetsList({ children: ReactNode }>) { const { workspace, user } = await getCurrentUser() - + const scope = new DatasetsRepository(workspace.id) + const datasets = await scope.findAll().then((r) => r.unwrap()) return ( Upload dataset } - /> - - - Create your first dataset - - + table={ + <> + {datasets.length > 0 ? ( + + ) : ( + + + Create your first dataset + + + } + /> + )} + } /> diff --git a/apps/web/src/app/(private)/datasets/new/page.tsx b/apps/web/src/app/(private)/datasets/new/page.tsx index 29829ce07..9c383e651 100644 --- a/apps/web/src/app/(private)/datasets/new/page.tsx +++ b/apps/web/src/app/(private)/datasets/new/page.tsx @@ -7,18 +7,18 @@ import { FormWrapper, Input, Modal, - // useToast, } from '@latitude-data/web-ui' -import { createDatasetAction } from '$/actions/datasets/create' -import useLatitudeAction from '$/hooks/useLatitudeAction' import { useNavigate } from '$/hooks/useNavigate' import { ROUTES } from '$/services/routes' +import useDatasets from '$/stores/datasets' export default function NewDataset() { const data = { name: '' } const navigate = useNavigate() - const { error, executeFormAction } = useLatitudeAction(createDatasetAction) - const errors = error?.fieldErrors + const { createError, createFormAction } = useDatasets({ + onCreateSuccess: () => navigate.push(ROUTES.datasets.root), + }) + const errors = createError?.fieldErrors return ( diff --git a/apps/web/src/components/layouts/AppLayout/Header/index.tsx b/apps/web/src/components/layouts/AppLayout/Header/index.tsx index 34ae4f641..cb5edb4fe 100644 --- a/apps/web/src/components/layouts/AppLayout/Header/index.tsx +++ b/apps/web/src/components/layouts/AppLayout/Header/index.tsx @@ -106,8 +106,8 @@ export default function AppHeader({ const pathname = usePathname() const info = currentUser ? getUserInfoFromSession(currentUser) : null return ( -
-
+
+
{sectionLinks.length > 0 ? ( -
- - {sectionLinks.map((link, idx) => { - let { href, label, index } = link - href = href?.startsWith('/') ? href : `/${href}` + + {sectionLinks.map((link, idx) => { + let { href, label, index } = link + href = href?.startsWith('/') ? href : `/${href}` - const selected = href - ? index - ? pathname === href - : pathname?.startsWith(href) - : false + const selected = href + ? index + ? pathname === href + : pathname?.startsWith(href) + : false - if (!href) return null + if (!href) return null - return ( - - - - ) - })} - -
+ return ( + + + + ) + })} + ) : null}
) diff --git a/apps/web/src/components/modals/DestroyModal/index.tsx b/apps/web/src/components/modals/DestroyModal/index.tsx index 3b4006209..8c5541d58 100644 --- a/apps/web/src/components/modals/DestroyModal/index.tsx +++ b/apps/web/src/components/modals/DestroyModal/index.tsx @@ -7,28 +7,31 @@ import { TAnyZodSafeFunctionHandler, } from 'zsa' -export default function DestroyModal({ - action, - onSuccess, - onOpenChange, - title, - description, - submitStr, - model, -}: { +type Props = { action: ( - data: inferServerActionInput, + data: inferServerActionInput, ) => Promise< - | [inferServerActionReturnData, null] - | [null, inferServerActionError] + | [inferServerActionReturnData, null] + | [null, inferServerActionError] > - onSuccess?: (payload: any) => void + onSuccess?: (payload: inferServerActionReturnData) => void onOpenChange?: (open: boolean) => void title: string description: string submitStr: string model: { id: string | number } -}) { +} +export default function DestroyModal< + TServerAction extends TAnyZodSafeFunctionHandler, +>({ + action, + onSuccess, + onOpenChange, + title, + description, + submitStr, + model, +}: Props) { const { toast } = useToast() const { action: actionFn } = useFormAction(action, { onError: (error) => { diff --git a/apps/web/src/stores/datasets.ts b/apps/web/src/stores/datasets.ts new file mode 100644 index 000000000..c213af611 --- /dev/null +++ b/apps/web/src/stores/datasets.ts @@ -0,0 +1,72 @@ +import { useCallback } from 'react' + +import type { Dataset } from '@latitude-data/core/browser' +import { useToast } from '@latitude-data/web-ui' +import { createDatasetAction } from '$/actions/datasets/create' +import { destroyDatasetAction } from '$/actions/datasets/destroy' +import { getDatasetsAction } from '$/actions/datasets/fetch' +import useLatitudeAction from '$/hooks/useLatitudeAction' +import useCurrentWorkspace from '$/stores/currentWorkspace' +import useSWR, { SWRConfiguration } from 'swr' + +export default function useDatasets( + { onCreateSuccess }: { onCreateSuccess?: (dataset: Dataset) => void } = {}, + opts?: SWRConfiguration, +) { + const { data: workspace } = useCurrentWorkspace() + const { toast } = useToast() + const fetcher = useCallback(async () => { + const [data, error] = await getDatasetsAction() + if (error) { + toast({ + title: 'Error', + description: error.message, + variant: 'destructive', + }) + + return [] + } + + return data + }, [toast]) + const { + data = [], + mutate, + ...rest + } = useSWR(['workspace', workspace.id, 'datasets'], fetcher, opts) + const { error: createError, executeFormAction: createFormAction } = + useLatitudeAction(createDatasetAction, { + onSuccess: ({ data: dataset }) => { + toast({ + title: 'Success', + description: 'Dataset uploaded successfully! 🎉', + }) + + mutate([...data, dataset]) + onCreateSuccess?.(dataset) + }, + }) + + const { execute: destroy, isPending: isDestroying } = useLatitudeAction< + typeof destroyDatasetAction + >(destroyDatasetAction, { + onSuccess: ({ data: dataset }) => { + toast({ + title: 'Success', + description: 'Dataset removed successfully', + }) + + mutate(data.filter((ds) => ds.id === dataset.id)) + }, + }) + + return { + data, + mutate, + createFormAction, + createError, + destroy, + isDestroying, + ...rest, + } +} diff --git a/package.json b/package.json index 17fa3513a..c8239bf9d 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "bullmq": "^5.12.11", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "csv-parse": "^5.5.6", "dotenv": "^16.4.5", "drizzle-orm": "^0.33.0", "flydrive": "^1.1.0", diff --git a/packages/core/drizzle/0044_needy_the_hood.sql b/packages/core/drizzle/0044_needy_the_hood.sql new file mode 100644 index 000000000..bfe5f00e8 --- /dev/null +++ b/packages/core/drizzle/0044_needy_the_hood.sql @@ -0,0 +1,27 @@ +CREATE TABLE IF NOT EXISTS "latitude"."datasets" ( + "id" bigserial PRIMARY KEY NOT NULL, + "name" varchar(256) NOT NULL, + "csv_delimiter" varchar(256) NOT NULL, + "workspace_id" bigint NOT NULL, + "author_id" text, + "file_key" varchar(256) NOT NULL, + "file_metadata" jsonb NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "latitude"."datasets" ADD CONSTRAINT "datasets_workspace_id_workspaces_id_fk" FOREIGN KEY ("workspace_id") REFERENCES "latitude"."workspaces"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "latitude"."datasets" ADD CONSTRAINT "datasets_author_id_users_id_fk" FOREIGN KEY ("author_id") REFERENCES "latitude"."users"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "datasets_workspace_idx" ON "latitude"."datasets" USING btree ("workspace_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "datasets_author_idx" ON "latitude"."datasets" USING btree ("author_id");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "datasets_workspace_id_name_index" ON "latitude"."datasets" USING btree ("workspace_id","name"); \ No newline at end of file diff --git a/packages/core/drizzle/meta/0044_snapshot.json b/packages/core/drizzle/meta/0044_snapshot.json new file mode 100644 index 000000000..252cfa5a4 --- /dev/null +++ b/packages/core/drizzle/meta/0044_snapshot.json @@ -0,0 +1,1988 @@ +{ + "id": "48b26764-9982-4f43-936c-605d36c80b0e", + "prevId": "d4a086e1-d04b-4551-8f19-30825d9434e9", + "version": "7", + "dialect": "postgresql", + "tables": { + "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 + }, + "confirmed_at": { + "name": "confirmed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "encrypted_password": { + "name": "encrypted_password", + "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": {}, + "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": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invitation_token": { + "name": "invitation_token", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "confirmed_at": { + "name": "confirmed_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": { + "memberships_workspace_id_user_id_index": { + "name": "memberships_workspace_id_user_id_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memberships_invitation_token_index": { + "name": "memberships_invitation_token_index", + "columns": [ + { + "expression": "invitation_token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "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": {}, + "uniqueConstraints": { + "memberships_invitation_token_unique": { + "name": "memberships_invitation_token_unique", + "nullsNotDistinct": false, + "columns": [ + "invitation_token" + ] + } + } + }, + "latitude.api_keys": { + "name": "api_keys", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "workspace_id": { + "name": "workspace_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_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": { + "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_token_unique": { + "name": "api_keys_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + } + }, + "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 + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "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" + }, + "commits_user_id_users_id_fk": { + "name": "commits_user_id_users_id_fk", + "tableFrom": "commits", + "tableTo": "users", + "schemaTo": "latitude", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "commits_uuid_unique": { + "name": "commits_uuid_unique", + "nullsNotDistinct": false, + "columns": [ + "uuid" + ] + }, + "unique_commit_version": { + "name": "unique_commit_version", + "nullsNotDistinct": false, + "columns": [ + "version", + "project_id" + ] + } + } + }, + "latitude.document_versions": { + "name": "document_versions", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "document_uuid": { + "name": "document_uuid", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "path": { + "name": "path", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "resolved_content": { + "name": "resolved_content", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "commit_id": { + "name": "commit_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "deleted_at": { + "name": "deleted_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": {}, + "foreignKeys": { + "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": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "unique_document_uuid_commit_id": { + "name": "unique_document_uuid_commit_id", + "nullsNotDistinct": false, + "columns": [ + "document_uuid", + "commit_id" + ] + }, + "unique_path_commit_id_deleted_at": { + "name": "unique_path_commit_id_deleted_at", + "nullsNotDistinct": false, + "columns": [ + "path", + "commit_id", + "deleted_at" + ] + } + } + }, + "latitude.provider_api_keys": { + "name": "provider_api_keys", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "provider", + "typeSchema": "latitude", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_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": { + "provider_apikeys_workspace_id_idx": { + "name": "provider_apikeys_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "provider_apikeys_name_idx": { + "name": "provider_apikeys_name_idx", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "provider_apikeys_user_id_idx": { + "name": "provider_apikeys_user_id_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "provider_api_keys_author_id_users_id_fk": { + "name": "provider_api_keys_author_id_users_id_fk", + "tableFrom": "provider_api_keys", + "tableTo": "users", + "schemaTo": "latitude", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "provider_api_keys_workspace_id_workspaces_id_fk": { + "name": "provider_api_keys_workspace_id_workspaces_id_fk", + "tableFrom": "provider_api_keys", + "tableTo": "workspaces", + "schemaTo": "latitude", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "provider_apikeys_token_provider_unique": { + "name": "provider_apikeys_token_provider_unique", + "nullsNotDistinct": false, + "columns": [ + "token", + "provider", + "workspace_id" + ] + } + } + }, + "latitude.document_logs": { + "name": "document_logs", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_uuid": { + "name": "document_uuid", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "commit_id": { + "name": "commit_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "resolved_content": { + "name": "resolved_content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parameters": { + "name": "parameters", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "custom_identifier": { + "name": "custom_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "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": { + "document_log_uuid_idx": { + "name": "document_log_uuid_idx", + "columns": [ + { + "expression": "document_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_logs_commit_id_commits_id_fk": { + "name": "document_logs_commit_id_commits_id_fk", + "tableFrom": "document_logs", + "tableTo": "commits", + "schemaTo": "latitude", + "columnsFrom": [ + "commit_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "document_logs_uuid_unique": { + "name": "document_logs_uuid_unique", + "nullsNotDistinct": false, + "columns": [ + "uuid" + ] + } + } + }, + "latitude.provider_logs": { + "name": "provider_logs", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "uuid": { + "name": "uuid", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_log_uuid": { + "name": "document_log_uuid", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "messages": { + "name": "messages", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "response_text": { + "name": "response_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "tool_calls": { + "name": "tool_calls", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'[]'::json" + }, + "tokens": { + "name": "tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "cost_in_millicents": { + "name": "cost_in_millicents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "duration": { + "name": "duration", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "log_source", + "typeSchema": "latitude", + "primaryKey": false, + "notNull": true + }, + "apiKeyId": { + "name": "apiKeyId", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "generated_at": { + "name": "generated_at", + "type": "timestamp", + "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": { + "provider_logs_provider_id_provider_api_keys_id_fk": { + "name": "provider_logs_provider_id_provider_api_keys_id_fk", + "tableFrom": "provider_logs", + "tableTo": "provider_api_keys", + "schemaTo": "latitude", + "columnsFrom": [ + "provider_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + }, + "provider_logs_apiKeyId_api_keys_id_fk": { + "name": "provider_logs_apiKeyId_api_keys_id_fk", + "tableFrom": "provider_logs", + "tableTo": "api_keys", + "schemaTo": "latitude", + "columnsFrom": [ + "apiKeyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "provider_logs_uuid_unique": { + "name": "provider_logs_uuid_unique", + "nullsNotDistinct": false, + "columns": [ + "uuid" + ] + } + } + }, + "latitude.datasets": { + "name": "datasets", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "csv_delimiter": { + "name": "csv_delimiter", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "file_key": { + "name": "file_key", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "file_metadata": { + "name": "file_metadata", + "type": "jsonb", + "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": { + "datasets_workspace_idx": { + "name": "datasets_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "datasets_author_idx": { + "name": "datasets_author_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "datasets_workspace_id_name_index": { + "name": "datasets_workspace_id_name_index", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "datasets_workspace_id_workspaces_id_fk": { + "name": "datasets_workspace_id_workspaces_id_fk", + "tableFrom": "datasets", + "tableTo": "workspaces", + "schemaTo": "latitude", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "datasets_author_id_users_id_fk": { + "name": "datasets_author_id_users_id_fk", + "tableFrom": "datasets", + "tableTo": "users", + "schemaTo": "latitude", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "latitude.evaluations": { + "name": "evaluations", + "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()" + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "metadata_id": { + "name": "metadata_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "metadata_type": { + "name": "metadata_type", + "type": "metadata_type", + "typeSchema": "latitude", + "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": { + "evaluation_workspace_idx": { + "name": "evaluation_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "evaluation_metadata_idx": { + "name": "evaluation_metadata_idx", + "columns": [ + { + "expression": "metadata_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metadata_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "unique_name_workspace": { + "name": "unique_name_workspace", + "columns": [ + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "evaluations_workspace_id_workspaces_id_fk": { + "name": "evaluations_workspace_id_workspaces_id_fk", + "tableFrom": "evaluations", + "tableTo": "workspaces", + "schemaTo": "latitude", + "columnsFrom": [ + "workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "evaluations_uuid_unique": { + "name": "evaluations_uuid_unique", + "nullsNotDistinct": false, + "columns": [ + "uuid" + ] + } + } + }, + "latitude.llm_as_judge_evaluation_metadatas": { + "name": "llm_as_judge_evaluation_metadatas", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "metadata_type": { + "name": "metadata_type", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true, + "default": "'llm_as_judge'" + }, + "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()" + }, + "prompt": { + "name": "prompt", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "llm_as_judge_evaluation_metadatas_template_id_idx": { + "name": "llm_as_judge_evaluation_metadatas_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "llm_as_judge_evaluation_metadatas_template_id_evaluations_templates_id_fk": { + "name": "llm_as_judge_evaluation_metadatas_template_id_evaluations_templates_id_fk", + "tableFrom": "llm_as_judge_evaluation_metadatas", + "tableTo": "evaluations_templates", + "schemaTo": "latitude", + "columnsFrom": [ + "template_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "latitude.connected_evaluations": { + "name": "connected_evaluations", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "document_uuid": { + "name": "document_uuid", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "evaluation_id": { + "name": "evaluation_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": { + "connected_document_idx": { + "name": "connected_document_idx", + "columns": [ + { + "expression": "document_uuid", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "connected_evaluation_idx": { + "name": "connected_evaluation_idx", + "columns": [ + { + "expression": "evaluation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "connected_evaluations_evaluation_id_evaluations_id_fk": { + "name": "connected_evaluations_evaluation_id_evaluations_id_fk", + "tableFrom": "connected_evaluations", + "tableTo": "evaluations", + "schemaTo": "latitude", + "columnsFrom": [ + "evaluation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "connected_evaluations_unique": { + "name": "connected_evaluations_unique", + "nullsNotDistinct": false, + "columns": [ + "document_uuid", + "evaluation_id" + ] + } + } + }, + "latitude.evaluation_results": { + "name": "evaluation_results", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "evaluation_id": { + "name": "evaluation_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "document_log_id": { + "name": "document_log_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "provider_log_id": { + "name": "provider_log_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "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": { + "evaluation_idx": { + "name": "evaluation_idx", + "columns": [ + { + "expression": "evaluation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_log_idx": { + "name": "document_log_idx", + "columns": [ + { + "expression": "document_log_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "provider_log_idx": { + "name": "provider_log_idx", + "columns": [ + { + "expression": "provider_log_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "evaluation_results_evaluation_id_evaluations_id_fk": { + "name": "evaluation_results_evaluation_id_evaluations_id_fk", + "tableFrom": "evaluation_results", + "tableTo": "evaluations", + "schemaTo": "latitude", + "columnsFrom": [ + "evaluation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "evaluation_results_document_log_id_document_logs_id_fk": { + "name": "evaluation_results_document_log_id_document_logs_id_fk", + "tableFrom": "evaluation_results", + "tableTo": "document_logs", + "schemaTo": "latitude", + "columnsFrom": [ + "document_log_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "evaluation_results_provider_log_id_provider_logs_id_fk": { + "name": "evaluation_results_provider_log_id_provider_logs_id_fk", + "tableFrom": "evaluation_results", + "tableTo": "provider_logs", + "schemaTo": "latitude", + "columnsFrom": [ + "provider_log_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "latitude.evaluations_templates": { + "name": "evaluations_templates", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "prompt": { + "name": "prompt", + "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": { + "evaluations_templates_category_evaluations_template_categories_id_fk": { + "name": "evaluations_templates_category_evaluations_template_categories_id_fk", + "tableFrom": "evaluations_templates", + "tableTo": "evaluations_template_categories", + "schemaTo": "latitude", + "columnsFrom": [ + "category" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "latitude.evaluations_template_categories": { + "name": "evaluations_template_categories", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar(256)", + "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.magic_link_tokens": { + "name": "magic_link_tokens", + "schema": "latitude", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "token": { + "name": "token", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "expired_at": { + "name": "expired_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "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": { + "magic_link_tokens_user_id_users_id_fk": { + "name": "magic_link_tokens_user_id_users_id_fk", + "tableFrom": "magic_link_tokens", + "tableTo": "users", + "schemaTo": "latitude", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "magic_link_tokens_token_unique": { + "name": "magic_link_tokens_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + } + } + }, + "enums": { + "latitude.provider": { + "name": "provider", + "schema": "latitude", + "values": [ + "openai", + "anthropic", + "groq", + "mistral", + "azure", + "google" + ] + }, + "latitude.log_source": { + "name": "log_source", + "schema": "latitude", + "values": [ + "playground", + "api", + "evaluation" + ] + }, + "latitude.metadata_type": { + "name": "metadata_type", + "schema": "latitude", + "values": [ + "llm_as_judge" + ] + } + }, + "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 36dd7e63d..2b988af29 100644 --- a/packages/core/drizzle/meta/_journal.json +++ b/packages/core/drizzle/meta/_journal.json @@ -309,6 +309,13 @@ "when": 1725520066626, "tag": "0043_naive_tyger_tiger", "breakpoints": true + }, + { + "idx": 44, + "version": "7", + "when": 1725725525176, + "tag": "0044_needy_the_hood", + "breakpoints": true } ] } \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json index 37e1b97f1..dc631ff68 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -51,6 +51,7 @@ "@types/uuid": "^10.0.0", "ai": "^3.2.42", "argon2": "^0.41.0", + "csv-parse": "^5.5.6", "drizzle-kit": "^0.22.8", "drizzle-orm": "^0.33.0", "eslint": "8", @@ -82,6 +83,7 @@ "@t3-oss/env-core": "^0.11.1", "ai": "^3.2.42", "argon2": "^0.41.0", + "csv-parse": "^5.5.6", "drizzle-orm": "^0.33.0", "flydrive": "^1.1.0", "lodash-es": "^4.17.21", diff --git a/packages/core/src/lib/disk.ts b/packages/core/src/lib/disk.ts index 119992cc7..79cdcd60e 100644 --- a/packages/core/src/lib/disk.ts +++ b/packages/core/src/lib/disk.ts @@ -39,6 +39,10 @@ export class DiskWrapper { this.disk = new Disk(this.buildDisk(args)) } + file(key: string) { + return this.disk.file(key) + } + async getUrl(key: string | undefined | null) { if (!key) return null diff --git a/packages/core/src/lib/readCsv.ts b/packages/core/src/lib/readCsv.ts new file mode 100644 index 000000000..4c2b6257c --- /dev/null +++ b/packages/core/src/lib/readCsv.ts @@ -0,0 +1,34 @@ +import { CsvError, parse } from 'csv-parse/sync' + +import { Result } from './Result' + +type ParseCsvOptions = { delimiter?: string } +export async function syncReadCsv( + file: File, + { delimiter = ';' }: ParseCsvOptions = {}, +) { + try { + const data = await file.text() + const records = parse(data, { + delimiter, + relax_column_count: true, + trim: true, + skip_empty_lines: true, + columns: true, + info: true, + }) as { + record: Record + info: { columns: { name: string }[] } + }[] // not typed + + if (records.length < 1) + return Result.ok({ headers: [], rowCount: 0, data: [] }) + + const firstRecord = records[0]! + const headers = firstRecord.info.columns.map((column) => column.name) + return Result.ok({ rowCount: records.length, headers, data: records }) + } catch (e) { + const error = e as CsvError + return Result.error(error) + } +} diff --git a/packages/core/src/repositories/datasetsRepository.ts b/packages/core/src/repositories/datasetsRepository.ts new file mode 100644 index 000000000..99d66f150 --- /dev/null +++ b/packages/core/src/repositories/datasetsRepository.ts @@ -0,0 +1,33 @@ +import { eq, sql } from 'drizzle-orm' + +import { Dataset } from '../browser' +import { datasets, users } from '../schema' +import Repository from './repository' + +export const datasetColumns = { + id: datasets.id, + name: datasets.name, + workspaceId: datasets.workspaceId, + authorId: datasets.authorId, + fileKey: datasets.fileKey, + fileMetadata: datasets.fileMetadata, + createdAt: datasets.createdAt, + updatedAt: datasets.updatedAt, + author: { + id: sql`${users.id}`.as('users_id'), + name: sql`${users.name}`.as('users_name'), + }, +} +export class DatasetsRepository extends Repository< + typeof datasetColumns, + Dataset +> { + get scope() { + return this.db + .select(datasetColumns) + .from(datasets) + .leftJoin(users, eq(users.id, datasets.authorId)) + .where(eq(datasets.workspaceId, this.workspaceId)) + .as('datasetsScope') + } +} diff --git a/packages/core/src/repositories/index.ts b/packages/core/src/repositories/index.ts index a5eed5b90..2fc7d35d9 100644 --- a/packages/core/src/repositories/index.ts +++ b/packages/core/src/repositories/index.ts @@ -9,3 +9,4 @@ export * from './providerLogsRepository' export * from './documentLogsRepository' export * from './membershipsRepository' export * from './evaluationsRepository' +export * from './datasetsRepository' diff --git a/packages/core/src/schema/index.ts b/packages/core/src/schema/index.ts index 39b342b94..36c9b9eca 100644 --- a/packages/core/src/schema/index.ts +++ b/packages/core/src/schema/index.ts @@ -19,6 +19,7 @@ export * from './models/documentLogs' export * from './models/providerLogs' // Evaluations tables +export * from './models/datasets' export * from './models/evaluations' export * from './models/llmAsJudgeEvaluationMetadatas' export * from './models/connectedEvaluations' diff --git a/packages/core/src/schema/models/datasets.ts b/packages/core/src/schema/models/datasets.ts new file mode 100644 index 000000000..a55bd90a4 --- /dev/null +++ b/packages/core/src/schema/models/datasets.ts @@ -0,0 +1,43 @@ +import { + bigint, + bigserial, + index, + jsonb, + text, + uniqueIndex, + varchar, +} from 'drizzle-orm/pg-core' +import { type FileSnapshot } from 'flydrive/types' + +import { latitudeSchema } from '../db-schema' +import { timestamps } from '../schemaHelpers' +import { users } from './users' +import { workspaces } from './workspaces' + +type FileMetadata = FileSnapshot & { headers: string[]; rowCount: number } + +export const datasets = latitudeSchema.table( + 'datasets', + { + id: bigserial('id', { mode: 'number' }).notNull().primaryKey(), + name: varchar('name', { length: 256 }).notNull(), + csvDelimiter: varchar('csv_delimiter', { length: 256 }).notNull(), + workspaceId: bigint('workspace_id', { mode: 'number' }) + .references(() => workspaces.id, { onDelete: 'cascade' }) + .notNull(), + authorId: text('author_id').references(() => users.id, { + onDelete: 'set null', + }), + fileKey: varchar('file_key', { length: 256 }).notNull(), + fileMetadata: jsonb('file_metadata').$type().notNull(), + ...timestamps(), + }, + (table) => ({ + datasetWorkspaceIdx: index('datasets_workspace_idx').on(table.workspaceId), + authorIdx: index('datasets_author_idx').on(table.authorId), + uniqueDatasetNameInWorkspace: uniqueIndex().on( + table.workspaceId, + table.name, + ), + }), +) diff --git a/packages/core/src/schema/relations.ts b/packages/core/src/schema/relations.ts index 259cdec83..00f122a84 100644 --- a/packages/core/src/schema/relations.ts +++ b/packages/core/src/schema/relations.ts @@ -4,6 +4,7 @@ import { evaluationTemplates, llmAsJudgeEvaluationMetadatas } from '.' import { apiKeys } from './models/apiKeys' import { commits } from './models/commits' import { connectedEvaluations } from './models/connectedEvaluations' +import { datasets } from './models/datasets' import { documentLogs } from './models/documentLogs' import { documentVersions } from './models/documentVersions' import { evaluationResults } from './models/evaluationResults' @@ -29,7 +30,11 @@ export const userRelations = relations(users, ({ many }) => ({ magicLinkTokens: many(magicLinkTokens), })) -export const workspaceRelations = relations(workspaces, ({ many }) => ({ +export const workspaceRelations = relations(workspaces, ({ one, many }) => ({ + author: one(users, { + fields: [workspaces.creatorId], + references: [users.id], + }), memberships: many(memberships), })) @@ -183,3 +188,14 @@ export const magicLinkTokensRelations = relations( }), }), ) + +export const datasetsRelations = relations(datasets, ({ one }) => ({ + author: one(users, { + fields: [datasets.authorId], + references: [users.id], + }), + workspace: one(workspaces, { + fields: [datasets.workspaceId], + references: [workspaces.id], + }), +})) diff --git a/packages/core/src/schema/types.ts b/packages/core/src/schema/types.ts index bfd8b9e9a..e055118d1 100644 --- a/packages/core/src/schema/types.ts +++ b/packages/core/src/schema/types.ts @@ -4,6 +4,7 @@ import { type InferSelectModel } from 'drizzle-orm' import { apiKeys } from './models/apiKeys' import { commits } from './models/commits' import { connectedEvaluations } from './models/connectedEvaluations' +import { datasets } from './models/datasets' import { documentLogs } from './models/documentLogs' import { documentVersions } from './models/documentVersions' import { evaluationResults } from './models/evaluationResults' @@ -63,3 +64,7 @@ export type EvaluationDto = Evaluation & { 'metadataType' | 'createdAt' | 'updatedAt' > } + +export type Dataset = InferSelectModel & { + author: Pick | undefined +} diff --git a/packages/core/src/services/datasets/create.ts b/packages/core/src/services/datasets/create.ts index 5c018d08c..94470eee9 100644 --- a/packages/core/src/services/datasets/create.ts +++ b/packages/core/src/services/datasets/create.ts @@ -4,12 +4,14 @@ import slugify from '@sindresorhus/slugify' import { SafeWorkspace, User } from '../../browser' import { database } from '../../client' -import { Result } from '../../lib' +import { Result, Transaction } from '../../lib' import { DiskWrapper } from '../../lib/disk' +import { syncReadCsv } from '../../lib/readCsv' +import { datasets } from '../../schema' export const createDataset = async ( { - author: _, + author, workspace, disk, data, @@ -19,18 +21,52 @@ export const createDataset = async ( data: { name: string file: File + csvDelimiter: string } disk: DiskWrapper }, - _db = database, + db = database, ) => { const name = slugify(data.name) const extension = path.extname(data.file.name) - const fileName = `workspaces/${workspace.id}/datasets/${name}${extension}` - - const diskResult = await disk.putFile(fileName, data.file) + const key = `workspaces/${workspace.id}/datasets/${name}${extension}` + const diskResult = await disk.putFile(key, data.file) if (diskResult.error) return diskResult - return Result.ok(true) + const file = disk.file(key) + const fileMetadata = await file.toSnapshot() + const readCsvResult = await syncReadCsv(data.file) + + if (readCsvResult.error) return readCsvResult + + const { headers, rowCount } = readCsvResult.value + + return Transaction.call(async (trx) => { + const inserts = await trx + .insert(datasets) + .values({ + name: data.name, + csvDelimiter: data.csvDelimiter, + workspaceId: workspace.id, + authorId: author.id, + fileKey: key, + fileMetadata: { + ...fileMetadata, + headers, + rowCount, + }, + }) + .returning() + + const dataset = inserts[0]! + + return Result.ok({ + ...dataset, + author: { + id: author.id, + name: author.name, + }, + }) + }, db) } diff --git a/packages/core/src/services/datasets/destroy.ts b/packages/core/src/services/datasets/destroy.ts new file mode 100644 index 000000000..367bf8bba --- /dev/null +++ b/packages/core/src/services/datasets/destroy.ts @@ -0,0 +1,31 @@ +import { eq } from 'drizzle-orm' + +import { Dataset } from '../../browser' +import { database } from '../../client' +import { Result, Transaction } from '../../lib' +import { DiskWrapper } from '../../lib/disk' +import { datasets } from '../../schema' + +export async function destroyDataset( + { + dataset, + disk, + }: { + dataset: Dataset + disk: DiskWrapper + }, + db = database, +) { + const deleteResult = await disk.delete(dataset.fileKey) + if (deleteResult.error) return deleteResult + + return Transaction.call(async (tx) => { + const result = await tx + .delete(datasets) + .where(eq(datasets.id, dataset.id)) + .returning() + const deleted = result[0]! + + return Result.ok(deleted) + }, db) +} diff --git a/packages/web-ui/src/ds/atoms/Dropzone/index.tsx b/packages/web-ui/src/ds/atoms/Dropzone/index.tsx index cc2edd029..ed9a6e3e3 100644 --- a/packages/web-ui/src/ds/atoms/Dropzone/index.tsx +++ b/packages/web-ui/src/ds/atoms/Dropzone/index.tsx @@ -6,9 +6,13 @@ import { forwardRef, InputHTMLAttributes, JSX, + useCallback, + useRef, useState, } from 'react' +import { useCombinedRefs } from '../../../lib/hooks/useCombineRefs' + export type DropzoneProps = Omit< InputHTMLAttributes, 'onChange' | 'children' @@ -20,23 +24,32 @@ export type DropzoneProps = Omit< export const Dropzone = forwardRef( ({ onChange, children, ...props }, ref) => { const [isDragging, setIsDragging] = useState(false) + const internalRef = useRef(null) + const combineRef = useCombinedRefs(ref, internalRef) - const handleDragOver = (e: DragEvent) => { + const handleDragOver = useCallback((e: DragEvent) => { e.preventDefault() e.stopPropagation() setIsDragging(true) - } + }, []) - const handleDrop = (e: DragEvent) => { - e.preventDefault() - e.stopPropagation() - setIsDragging(false) - onChange?.(e.dataTransfer.files) - } + const handleDrop = useCallback( + (e: DragEvent) => { + e.preventDefault() + e.stopPropagation() + setIsDragging(false) + onChange?.(e.dataTransfer.files) + if (!internalRef.current) return + + // Set files in input because then we can submit a real form + internalRef.current.files = e.dataTransfer.files + }, + [onChange], + ) - const handleDragLeave = () => { + const handleDragLeave = useCallback(() => { setIsDragging(false) - } + }, []) const handleChange = (e: ChangeEvent) => { onChange?.(e.target.files) @@ -49,7 +62,7 @@ export const Dropzone = forwardRef( > {children({ isDragging })} (null) + const [selectedFile, setSelectedFile] = useState(null) const ref = useRef(null) const onClickZone = useCallback(() => { if (!ref.current) return @@ -31,9 +31,9 @@ export function DropzoneInput({ const file = files?.[0] if (!file) return - setFile(file) + setSelectedFile(file) }, - [setFile], + [setSelectedFile], ) const name = selectedFile?.name return ( diff --git a/packages/web-ui/src/ds/atoms/Table/index.tsx b/packages/web-ui/src/ds/atoms/Table/index.tsx index 2f84e681e..10707d47e 100644 --- a/packages/web-ui/src/ds/atoms/Table/index.tsx +++ b/packages/web-ui/src/ds/atoms/Table/index.tsx @@ -1,33 +1,37 @@ -import * as React from 'react' +import { + forwardRef, + HTMLAttributes, + TdHTMLAttributes, + ThHTMLAttributes, +} from 'react' import { cn } from '../../../lib/utils' import Text from '../Text' -const Table = React.forwardRef< - HTMLTableElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
- - -)) +const Table = forwardRef>( + ({ className, ...props }, ref) => ( +
+
+ + ), +) Table.displayName = 'Table' -const TableHeader = React.forwardRef< +const TableHeader = forwardRef< HTMLTableSectionElement, - React.HTMLAttributes + HTMLAttributes >(({ className, ...props }, ref) => ( )) TableHeader.displayName = 'TableHeader' -const TableBody = React.forwardRef< +const TableBody = forwardRef< HTMLTableSectionElement, - React.HTMLAttributes + HTMLAttributes >(({ className, ...props }, ref) => ( + HTMLAttributes >(({ className, ...props }, ref) => ( & { + HTMLAttributes & { verticalPadding?: boolean } >(({ className, verticalPadding, ...props }, ref) => ( @@ -72,9 +76,9 @@ const TableRow = React.forwardRef< )) TableRow.displayName = 'TableRow' -const TableHead = React.forwardRef< +const TableHead = forwardRef< HTMLTableCellElement, - React.ThHTMLAttributes + ThHTMLAttributes >(({ className, ...props }, ref) => ( + ), +) TableCell.displayName = 'TableCell' -const TableCaption = React.forwardRef< +const TableCaption = forwardRef< HTMLTableCaptionElement, - React.HTMLAttributes + HTMLAttributes >(({ className, ...props }, ref) => (
->(({ className, ...props }, ref) => ( - -)) +type CellProps = TdHTMLAttributes & { + align?: 'left' | 'center' | 'right' +} +const TableCell = forwardRef( + ({ className, children, align = 'left', ...props }, ref) => ( + +
+ {children} +
+
= ForwardedRef | undefined + +function setRef(ref: OptionalRef, value: T) { + if (typeof ref === 'function') { + ref(value) + } else if (ref) { + ref.current = value + } +} + +export function useCombinedRefs(...refs: OptionalRef[]) { + const previousRefs = useRef[]>([]) + + return useCallback((value: T | null) => { + let index = 0 + for (; index < refs.length; index++) { + const ref = refs[index] + const prev = previousRefs.current[index] + + if (prev != ref) setRef(prev, null) + + setRef(ref, value) + } + + for (; index < previousRefs.current.length; index++) { + const prev = previousRefs.current[index] + setRef(prev, null) + } + + previousRefs.current = refs + }, refs) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 140bfff59..ac3582022 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -83,6 +83,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + csv-parse: + specifier: ^5.5.6 + version: 5.5.6 dotenv: specifier: ^16.4.5 version: 16.4.5 @@ -557,6 +560,9 @@ importers: argon2: specifier: ^0.41.0 version: 0.41.0 + csv-parse: + specifier: ^5.5.6 + version: 5.5.6 drizzle-kit: specifier: ^0.22.8 version: 0.22.8 @@ -4940,6 +4946,9 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + csv-parse@5.5.6: + resolution: {integrity: sha512-uNpm30m/AGSkLxxy7d9yRXpJQFrZzVWLFBkS+6ngPcZkw/5k3L/jjFuj7tVnEpRn+QgmiXr21nDlhCiUK4ij2A==} + damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -13227,6 +13236,8 @@ snapshots: csstype@3.1.3: {} + csv-parse@5.5.6: {} + damerau-levenshtein@1.0.8: {} data-urls@5.0.0: