import { column, IndexShorthand, Schema, SchemaTableType, Table, type BaseColumnType, type TableV2Options } from '@powersync/common'; import { entityKind, InferSelectModel, isTable, Relations, type Casing } from 'drizzle-orm'; import { CasingCache } from 'drizzle-orm/casing'; import { getTableConfig, SQLiteBoolean, SQLiteCustomColumn, SQLiteInteger, SQLiteReal, SQLiteText, SQLiteTextJson, SQLiteTimestamp, type SQLiteColumn, type SQLiteTableWithColumns, type TableConfig } from 'drizzle-orm/sqlite-core'; export type ExtractPowerSyncColumns> = { [K in keyof InferSelectModel as K extends 'id' ? never : K]: BaseColumnType[K]>; }; export type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; export function toPowerSyncTable>( table: T, options?: Omit & { casingCache?: CasingCache } ): Table>> { const { columns: drizzleColumns, indexes: drizzleIndexes } = getTableConfig(table); const { casingCache } = options ?? {}; const columns: { [key: string]: BaseColumnType } = {}; for (const drizzleColumn of drizzleColumns) { const name = casingCache?.getColumnCasing(drizzleColumn) ?? drizzleColumn.name; // Skip the id column if (name === 'id') { continue; } columns[name] = mapDrizzleColumnToType(drizzleColumn); } const indexes: IndexShorthand = {}; for (const index of drizzleIndexes) { index.config; if (!index.config.columns.length) { continue; } const columns: string[] = []; for (const indexColumn of index.config.columns) { const name = casingCache?.getColumnCasing(indexColumn as SQLiteColumn) ?? (indexColumn as { name: string }).name; columns.push(name); } indexes[index.config.name] = columns; } return new Table(columns, { ...options, indexes }) as Table>>; } function mapDrizzleColumnToType(drizzleColumn: SQLiteColumn): BaseColumnType { switch (drizzleColumn.columnType) { case SQLiteText[entityKind]: case SQLiteTextJson[entityKind]: return column.text; case SQLiteInteger[entityKind]: case SQLiteTimestamp[entityKind]: case SQLiteBoolean[entityKind]: return column.integer; case SQLiteReal[entityKind]: return column.real; case SQLiteCustomColumn[entityKind]: const sqlName = (drizzleColumn as SQLiteCustomColumn).getSQLType(); switch (sqlName) { case 'text': return column.text; case 'integer': return column.integer; case 'real': return column.real; default: throw new Error(`Unsupported custom column type: ${drizzleColumn.columnType}: ${sqlName}`); } default: throw new Error(`Unsupported column type: ${drizzleColumn.columnType}`); } } export type DrizzleTablePowerSyncOptions = Omit; export type DrizzleTableWithPowerSyncOptions = { tableDefinition: SQLiteTableWithColumns; options?: DrizzleTablePowerSyncOptions; }; export type TableName = T extends SQLiteTableWithColumns ? T['_']['name'] : T extends DrizzleTableWithPowerSyncOptions ? T['tableDefinition']['_']['name'] : never; export type TablesFromSchemaEntries = { [K in keyof T as T[K] extends Relations ? never : T[K] extends SQLiteTableWithColumns | DrizzleTableWithPowerSyncOptions ? TableName : never]: T[K] extends SQLiteTableWithColumns ? Table>> : T[K] extends DrizzleTableWithPowerSyncOptions ? Table>> : never; }; function toPowerSyncTables< T extends Record | Relations | DrizzleTableWithPowerSyncOptions> >(schemaEntries: T, options?: DrizzleAppSchemaOptions) { const casingCache = options?.casing ? new CasingCache(options?.casing) : undefined; const tables: Record = {}; for (const schemaEntry of Object.values(schemaEntries)) { let maybeTable: SQLiteTableWithColumns | Relations | undefined = undefined; let maybeOptions: DrizzleTablePowerSyncOptions | undefined = undefined; if (typeof schemaEntry === 'object' && 'tableDefinition' in schemaEntry) { const tableWithOptions = schemaEntry as DrizzleTableWithPowerSyncOptions; maybeTable = tableWithOptions.tableDefinition; maybeOptions = tableWithOptions.options; } else { maybeTable = schemaEntry; } if (isTable(maybeTable)) { const { name } = getTableConfig(maybeTable); tables[name] = toPowerSyncTable(maybeTable as SQLiteTableWithColumns, { ...maybeOptions, casingCache }); } } return tables; } export type DrizzleAppSchemaOptions = { casing?: Casing; }; export class DrizzleAppSchema< T extends Record | Relations | DrizzleTableWithPowerSyncOptions> > extends Schema { constructor(drizzleSchema: T, options?: DrizzleAppSchemaOptions) { super(toPowerSyncTables(drizzleSchema, options)); // This is just used for typing this.types = {} as SchemaTableType>>; } readonly types: SchemaTableType>>; }