import type { CollectionUtils } from "@firtoz/db-helpers"; import { type GenericBaseSyncConfig, type GenericSyncBackend, type GenericSyncFunctionResult, createGenericSyncFunction, createGenericCollectionConfig, USE_DEDUPE as _USE_DEDUPE, } from "@firtoz/db-helpers"; import { type Table, SQL, getTableColumns } from "drizzle-orm"; import type { BuildSchema } from "drizzle-valibot"; import { createInsertSchema } from "drizzle-valibot"; import * as v from "valibot"; import type { Collection, UtilsRecord, CollectionConfig, InferSchemaOutput, SyncMode, } from "@tanstack/db"; /** * Utility type for branded IDs */ export type Branded = T & { __brand: Brand }; export type TableId = Branded< string, `${TTableName}_id` >; /** * Utility type to extract the ID type from a table */ export type IdOf = TTable extends { $inferSelect: { id: infer TId extends string | number }; } ? TId : string | number; /** * Utility function to safely create branded IDs */ export function makeId( _table: TTable, value: string, ): IdOf { return value as IdOf; } /** * Select schema type helper */ export type SelectSchema = BuildSchema< "select", TTable["_"]["columns"], undefined >; /** * Insert schema type helper */ export type InsertSchema = BuildSchema< "insert", TTable["_"]["columns"], undefined >; /** * Schema type with insert input (optionals for defaults) and select output (all fields present). * Represents the standard input/output pair for collection schemas. */ export type InsertToSelectSchema = v.GenericSchema< v.InferInput>, v.InferOutput> >; /** * Helper type to get the table from schema by name */ export type GetTableFromSchema< TSchema extends Record, TTableName extends keyof TSchema, > = TSchema[TTableName] extends Table ? TSchema[TTableName] : never; /** * Helper type to infer the collection type from table * This provides proper typing for Collection insert/update operations */ export type InferCollectionFromTable = Collection< TTable["$inferSelect"], IdOf, UtilsRecord, InsertToSelectSchema, Omit< TTable["$inferInsert"], "id" // "createdAt" | "updatedAt" | "deletedAt" | "id" > & { id?: IdOf; } >; export const USE_DEDUPE = _USE_DEDUPE; /** * Base configuration for sync lifecycle management. * Extends the generic (Drizzle-free) config with a Drizzle table reference. */ export interface BaseSyncConfig extends GenericBaseSyncConfig>> { table: TTable; } /** * Backend-specific implementations required for sync. * Drizzle-typed alias for GenericSyncBackend. */ export type SyncBackend = GenericSyncBackend< InferSchemaOutput> >; /** * Return type for createSyncFunction. * Drizzle-typed alias for GenericSyncFunctionResult. */ export type SyncFunctionResult = GenericSyncFunctionResult>>; /** * Creates the sync function with common lifecycle management. * Delegates to the generic (Drizzle-free) implementation in @firtoz/db-helpers. */ export function createSyncFunction( config: BaseSyncConfig, backend: SyncBackend, ): SyncFunctionResult { return createGenericSyncFunction(config, backend); } /** * Creates an insert schema with default value handling * Validates that SQL expressions are not used for defaults (IndexedDB compatibility) */ export function createInsertSchemaWithDefaults( table: TTable, ): InsertToSelectSchema { const insertSchema = createInsertSchema(table); const columns = getTableColumns(table); // Validate that no SQL expressions are used as defaults for (const columnName in columns) { const column = columns[columnName]; let defaultValue: unknown | undefined; if (column.defaultFn) { defaultValue = column.defaultFn(); } else if (column.default !== undefined) { defaultValue = column.default; } if (defaultValue instanceof SQL) { throw new Error( `Default value for column ${columnName} is a SQL expression, which is not supported for IndexedDB`, ); } } // Transform the schema to apply defaults return v.pipe( insertSchema, v.transform((input) => { const result = { ...input } as Record; for (const columnName in columns) { const column = columns[columnName]; if (result[columnName] !== undefined) continue; let defaultValue: unknown | undefined; if (column.defaultFn) { defaultValue = column.defaultFn(); } else if (column.default !== undefined) { defaultValue = column.default; } if (defaultValue instanceof SQL) { throw new Error( `Default value for column ${columnName} is a SQL expression, which is not supported for IndexedDB`, ); } if (defaultValue !== undefined) { result[columnName] = defaultValue; continue; } if (column.notNull) { throw new Error(`Column ${columnName} is not nullable`); } result[columnName] = null; } return result; }), ) as InsertToSelectSchema; } /** * Creates a minimal insert schema that only applies ID defaults * Other defaults (like timestamps) are handled by the database */ export function createInsertSchemaWithIdDefault( table: TTable, ): InsertToSelectSchema { const insertSchema = createInsertSchema(table); const columns = getTableColumns(table); const idColumn = columns.id; return v.pipe( insertSchema, v.transform((input) => { const result = { ...input } as Record; // Apply ID default if missing if (result.id === undefined && idColumn?.defaultFn) { result.id = idColumn.defaultFn(); } return result; }), ) as InsertToSelectSchema; } /** * Standard getKey function for collections */ export function createGetKeyFunction() { type TItem = InferSchemaOutput>; type TKey = IdOf; return (item: TItem): TKey => (item as { id: TKey }).id; } /** * Base collection config factory. * Delegates to the generic (Drizzle-free) implementation in @firtoz/db-helpers. */ export function createCollectionConfig< TTable extends Table, TSchema extends v.GenericSchema, >(config: { schema: TSchema; getKey: (item: InferSchemaOutput>) => IdOf; syncResult: SyncFunctionResult; onInsert?: CollectionConfig< InferSchemaOutput>, string, TSchema >["onInsert"]; onUpdate?: CollectionConfig< InferSchemaOutput>, string, TSchema >["onUpdate"]; onDelete?: CollectionConfig< InferSchemaOutput>, string, TSchema >["onDelete"]; syncMode?: SyncMode; }): Omit< CollectionConfig< InferSchemaOutput>, IdOf, TSchema, CollectionUtils>> >, "utils" > & { schema: TSchema; utils: CollectionUtils>>; } { type TItem = InferSchemaOutput>; type ReturnType = Omit< CollectionConfig, TSchema, CollectionUtils>, "utils" > & { schema: TSchema; utils: CollectionUtils; }; const { getKey: getId, ...rest } = config; return createGenericCollectionConfig({ ...rest, // Generic sync is typed with string keys; runtime id may be number — same value as Drizzle row id. getKey: (item: TItem) => getId(item) as string, }) as ReturnType; }