import type { BetterOmit, DocumentByName, Expand, GenericDatabaseWriter, WithoutSystemFields, } from "convex/server"; import type { GenericId } from "convex/values"; import { pipe } from "effect/Function"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Record from "effect/Record"; import * as DatabaseSchema from "./DatabaseSchema"; import type * as DataModel from "./DataModel"; import type { DocumentByName as DocumentByName_ } from "./DataModel"; import * as Document from "./Document"; import * as QueryInitializer from "./QueryInitializer"; import type * as Table from "./Table"; /** * The argument accepted by `patch`: like `Partial` without system fields, * but the fields that are already optional also accept `undefined`, since * setting a field to `undefined` unsets it. */ export type PatchValue = { [ K in keyof Doc as K extends "_id" | "_creationTime" ? never : K ]?: undefined extends Doc[K] ? Doc[K] | undefined : Doc[K]; }; export interface DatabaseWriterTableAccessor< DataModel_ extends DataModel.AnyWithProps, TableName extends DataModel.TableNames, Doc = DocumentByName_, > { readonly insert: ( document: Document.WithoutSystemFields, ) => Effect.Effect, Document.DocumentEncodeError>; readonly patch: ( id: GenericId, patchedValues: PatchValue, ) => Effect.Effect< void, | QueryInitializer.GetByIdFailure | Document.DocumentDecodeError | Document.DocumentEncodeError >; readonly replace: ( id: GenericId, value: Document.WithoutSystemFields, ) => Effect.Effect; readonly delete: (id: GenericId) => Effect.Effect; } /** * The service shape backing the `DatabaseWriter` tag. Named (rather than an * inferred anonymous object) so declaration emit prints * `DatabaseWriterService<…>` by reference instead of expanding the data model. * `Docs` is the optional named document registry (see * `DatabaseReaderService`). */ export interface DatabaseWriterService< DatabaseSchema_ extends DatabaseSchema.AnyWithProps, Docs = {}, > { readonly table: < const TableName extends DataModel.TableNames< DataModel.FromSchema >, >( tableName: TableName, ) => DatabaseWriterTableAccessor< DataModel.FromSchema, TableName, TableName extends keyof Docs ? Docs[TableName] : DocumentByName_, TableName> >; } /** * The tag's _Identifier_ is `Docs`-independent (see `DatabaseReaderTag`); only * the _Service_ carries `Docs` so writer inputs print the named doc * interfaces. */ export type DatabaseWriterTag< DatabaseSchema_ extends DatabaseSchema.AnyWithProps, Docs = {}, > = Context.Service< DatabaseWriterService, DatabaseWriterService >; export const make = ( databaseSchema: DatabaseSchema_, convexDatabaseWriter: GenericDatabaseWriter< DataModel.ToConvex> >, ): DatabaseWriterService => { type DataModel_ = DataModel.FromSchema; const table = >( tableName: TableName, ) => { const tableDef = DatabaseSchema.tables(databaseSchema)[tableName]; const insert = Effect.fn("DatabaseWriter.insert")(function* ( document: Document.WithoutSystemFields< DocumentByName_ >, ) { const encodedDocument = yield* Document.encode(document, tableDef); const id = yield* Effect.promise(() => convexDatabaseWriter.insert( tableName, encodedDocument as WithoutSystemFields< DocumentByName, TableName> >, ), ); return id; }); const patch = Effect.fn("DatabaseWriter.patch")(function* ( id: GenericId, patchedValues: PatchValue>, ) { const originalDecodedDoc = yield* QueryInitializer.getById< Table.AnyWithProps, TableName >( convexDatabaseWriter as any, tableDef, )(id); const updatedEncodedDoc = yield* pipe( patchedValues, Record.reduce(originalDecodedDoc, (acc, value, key) => value === undefined ? Record.remove(acc, key) : Record.set(acc, key, value), ), Document.encode(tableDef), ); yield* Effect.promise(() => convexDatabaseWriter.replace( id, updatedEncodedDoc as Expand< BetterOmit< DocumentByName, TableName>, "_creationTime" | "_id" > >, ), ); }); const replace = Effect.fn("DatabaseWriter.replace")(function* ( id: GenericId, value: Document.WithoutSystemFields< DocumentByName_ >, ) { const updatedEncodedDoc = yield* Document.encode(value, tableDef); yield* Effect.promise(() => convexDatabaseWriter.replace( id, updatedEncodedDoc as Expand< BetterOmit< DocumentByName, TableName>, "_creationTime" | "_id" > >, ), ); }); const delete_ = (id: GenericId) => Effect.promise(() => convexDatabaseWriter.delete(id)); return { insert, patch, replace, delete: delete_, }; }; return { table, } as DatabaseWriterService; }; export const DatabaseWriter = < DatabaseSchema_ extends DatabaseSchema.AnyWithProps, Docs = {}, >(): DatabaseWriterTag => Context.Service< DatabaseWriterService, DatabaseWriterService >("@confect/server/DatabaseWriter"); export type DatabaseWriter< DatabaseSchema_ extends DatabaseSchema.AnyWithProps, > = DatabaseWriterService; export const layer = ( databaseSchema: DatabaseSchema_, convexDatabaseWriter: GenericDatabaseWriter< DataModel.ToConvex> >, ) => Layer.succeed( DatabaseWriter(), make(databaseSchema, convexDatabaseWriter), );