UNPKG

supabase-typed-query

Version:

Type-safe query builder and entity pattern for Supabase with TypeScript

1 lines 73.5 kB
{"version":3,"file":"index.mjs","sources":["../src/utils/errors.ts","../src/query/QueryBuilder.ts","../src/query/Query.ts","../src/query/index.ts","../src/entity/Entity.ts"],"sourcesContent":["/**\n * Supabase/Postgrest error structure\n */\nexport type SupabaseErrorObject = {\n message: string\n code?: string\n details?: string\n hint?: string\n}\n\n/**\n * Custom Error class that preserves Supabase error details\n */\nexport class SupabaseError extends Error {\n readonly code?: string\n readonly details?: string\n readonly hint?: string\n\n constructor(error: SupabaseErrorObject | unknown) {\n // Check for Error instances FIRST before checking for Supabase error objects\n // because Error instances also have a message property\n if (error instanceof Error) {\n super(error.message)\n this.name = error.name\n this.stack = error.stack\n } else if (isSupabaseError(error)) {\n super(error.message)\n this.name = \"SupabaseError\"\n this.code = error.code\n this.details = error.details\n this.hint = error.hint\n } else {\n super(String(error))\n this.name = \"SupabaseError\"\n }\n }\n\n /**\n * Override toString to include all error details\n */\n override toString(): string {\n const parts = [this.message]\n if (this.code) parts.push(`[Code: ${this.code}]`)\n if (this.details) parts.push(`Details: ${this.details}`)\n if (this.hint) parts.push(`Hint: ${this.hint}`)\n return parts.join(\" | \")\n }\n}\n\n/**\n * Type guard for Supabase error objects\n */\nfunction isSupabaseError(error: unknown): error is SupabaseErrorObject {\n return (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof (error as SupabaseErrorObject).message === \"string\"\n )\n}\n\n/**\n * Convert any error to a proper Error instance\n */\nexport const toError = (error: unknown): Error => {\n if (error instanceof Error) {\n return error\n }\n return new SupabaseError(error)\n}\n","import type { SupabaseClientType, TableNames, TableRow } from \"@/types\"\nimport { toError } from \"@/utils/errors\"\n\nimport type { Brand, FPromise, TaskOutcome } from \"functype\"\nimport { Err, List, Ok, Option } from \"functype\"\n\nimport type { IsConditions, MappedQuery, Query, QueryBuilderConfig, QueryCondition, WhereConditions } from \"./Query\"\n\n// Simple console logging for open source version\n// Suppress logs during tests to avoid stderr noise in test output\nconst log = {\n error: (msg: string) => process.env.NODE_ENV !== \"test\" && console.error(`[supabase-typed-query] ${msg}`),\n warn: (msg: string) => process.env.NODE_ENV !== \"test\" && console.warn(`[supabase-typed-query] ${msg}`),\n info: (msg: string) => process.env.NODE_ENV !== \"test\" && console.info(`[supabase-typed-query] ${msg}`),\n}\n\n// Tables that don't have a deleted field (like version tracking tables)\n// Tables that don't have a deleted field - consumers can override this\nconst TABLES_WITHOUT_DELETED = new Set<string>([])\n\n/**\n * Functional QueryBuilder implementation using closures instead of classes\n */\n// Helper to wrap async operations with error handling\nconst wrapAsync = <T>(fn: () => Promise<TaskOutcome<T>>): FPromise<TaskOutcome<T>> => {\n // FPromise in newer functype versions is just a promise with additional methods\n // We can use the FPromise constructor if available, or cast it\n return fn() as unknown as FPromise<TaskOutcome<T>>\n}\n\nexport const QueryBuilder = <T extends TableNames>(\n client: SupabaseClientType,\n config: QueryBuilderConfig<T>,\n): Query<T> => {\n /**\n * Build the Supabase query from accumulated conditions\n */\n const buildSupabaseQuery = () => {\n const { table, conditions, order, limit, offset } = config\n\n // Start with base query (just the table reference)\n const baseQuery = client.from(table)\n\n // Handle multiple conditions with OR logic\n const queryWithConditions =\n conditions.length === 1 ? applyCondition(baseQuery, conditions[0]) : applyOrConditions(baseQuery, conditions)\n\n // Apply ordering if specified\n const queryWithOrder = order ? queryWithConditions.order(order[0], order[1]) : queryWithConditions\n\n // Apply pagination\n const finalQuery = (() => {\n if (limit && offset !== undefined) {\n // Use range for offset + limit\n return queryWithOrder.range(offset, offset + limit - 1)\n } else if (limit) {\n // Just limit\n return queryWithOrder.limit(limit)\n } else if (offset !== undefined) {\n // Just offset (need to use a large upper bound)\n return queryWithOrder.range(offset, Number.MAX_SAFE_INTEGER)\n }\n return queryWithOrder\n })()\n\n return finalQuery\n }\n\n /**\n * Apply a single condition to the query\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const applyCondition = (query: any, condition: QueryCondition<T>): any => {\n const { where, is, wherein, gt, gte, lt, lte, neq, like, ilike } = condition\n\n // Process WHERE conditions, extracting operators from the where object\n const processedWhere: Record<string, unknown> = {}\n const extractedOperators: {\n gt?: Record<string, unknown>\n gte?: Record<string, unknown>\n lt?: Record<string, unknown>\n lte?: Record<string, unknown>\n neq?: Record<string, unknown>\n like?: Record<string, string>\n ilike?: Record<string, string>\n } = {}\n\n if (where) {\n // Extract top-level operators from where object\n const {\n gt: whereGt,\n gte: whereGte,\n lt: whereLt,\n lte: whereLte,\n neq: whereNeq,\n like: whereLike,\n ilike: whereIlike,\n ...rest\n } = where as Record<string, unknown>\n\n // Store extracted operators\n if (whereGt) extractedOperators.gt = whereGt as Record<string, unknown>\n if (whereGte) extractedOperators.gte = whereGte as Record<string, unknown>\n if (whereLt) extractedOperators.lt = whereLt as Record<string, unknown>\n if (whereLte) extractedOperators.lte = whereLte as Record<string, unknown>\n if (whereNeq) extractedOperators.neq = whereNeq as Record<string, unknown>\n if (whereLike) extractedOperators.like = whereLike as Record<string, string>\n if (whereIlike) extractedOperators.ilike = whereIlike as Record<string, string>\n\n // Process remaining fields\n for (const [key, value] of Object.entries(rest)) {\n if (value && typeof value === \"object\" && !Array.isArray(value) && !(value instanceof Date)) {\n // Check if it's an operator object\n const ops = value as Record<string, unknown>\n if (ops.gte !== undefined) {\n extractedOperators.gte = {\n ...extractedOperators.gte,\n [key]: ops.gte,\n }\n }\n if (ops.gt !== undefined) {\n extractedOperators.gt = { ...extractedOperators.gt, [key]: ops.gt }\n }\n if (ops.lte !== undefined) {\n extractedOperators.lte = {\n ...extractedOperators.lte,\n [key]: ops.lte,\n }\n }\n if (ops.lt !== undefined) {\n extractedOperators.lt = { ...extractedOperators.lt, [key]: ops.lt }\n }\n if (ops.neq !== undefined) {\n extractedOperators.neq = {\n ...extractedOperators.neq,\n [key]: ops.neq,\n }\n }\n if (ops.like !== undefined) {\n extractedOperators.like = {\n ...extractedOperators.like,\n [key]: ops.like as string,\n }\n }\n if (ops.ilike !== undefined) {\n extractedOperators.ilike = {\n ...extractedOperators.ilike,\n [key]: ops.ilike as string,\n }\n }\n if (ops.in !== undefined) {\n // Handle IN operator\n if (!wherein) {\n const cond = condition as unknown as Record<string, unknown>\n cond.wherein = {}\n }\n const whereinObj = condition.wherein as Record<string, unknown>\n whereinObj[key] = ops.in\n }\n if (ops.is !== undefined) {\n // Handle IS operator\n if (!is) {\n const cond = condition as unknown as Record<string, unknown>\n cond.is = {}\n }\n const isObj = condition.is as Record<string, unknown>\n isObj[key] = ops.is\n }\n // If no operators found, treat as regular value\n if (!ops.gte && !ops.gt && !ops.lte && !ops.lt && !ops.neq && !ops.like && !ops.ilike && !ops.in && !ops.is) {\n processedWhere[key] = value\n }\n } else {\n // Regular value\n processedWhere[key] = value\n }\n }\n }\n\n // Merge extracted operators with explicitly passed operators\n const mergedGt = { ...gt, ...extractedOperators.gt }\n const mergedGte = { ...gte, ...extractedOperators.gte }\n const mergedLt = { ...lt, ...extractedOperators.lt }\n const mergedLte = { ...lte, ...extractedOperators.lte }\n const mergedNeq = { ...neq, ...extractedOperators.neq }\n const mergedLike = { ...like, ...extractedOperators.like }\n const mergedIlike = { ...ilike, ...extractedOperators.ilike }\n\n // Apply WHERE conditions\n const baseQuery = query.select(\"*\").match(processedWhere)\n\n // Apply soft delete filter based on softDeleteMode\n const queryWithSoftDelete = (() => {\n if (TABLES_WITHOUT_DELETED.has(config.table)) {\n return baseQuery\n }\n if (config.softDeleteMode === \"exclude\") {\n return baseQuery.is(\"deleted\", null)\n }\n if (config.softDeleteMode === \"only\") {\n return baseQuery.not(\"deleted\", \"is\", null)\n }\n // Default: \"include\" - no filter\n return baseQuery\n })()\n\n // Apply WHERE IN conditions\n const queryWithWhereIn = wherein\n ? List(Object.entries(wherein)).foldLeft(queryWithSoftDelete)((q, [column, values]) =>\n q.in(column, values as never),\n )\n : queryWithSoftDelete\n\n // Apply IS conditions\n const queryWithIs = is\n ? List(Object.entries(is)).foldLeft(queryWithWhereIn)((q, [column, value]) =>\n q.is(column as keyof TableRow<T> & string, value as boolean | null),\n )\n : queryWithWhereIn\n\n // Apply comparison operators using merged values\n const queryWithGt =\n Object.keys(mergedGt).length > 0\n ? Object.entries(mergedGt).reduce((q, [key, value]) => q.gt(key, value), queryWithIs)\n : queryWithIs\n\n const queryWithGte =\n Object.keys(mergedGte).length > 0\n ? Object.entries(mergedGte).reduce((q, [key, value]) => q.gte(key, value), queryWithGt)\n : queryWithGt\n\n const queryWithLt =\n Object.keys(mergedLt).length > 0\n ? Object.entries(mergedLt).reduce((q, [key, value]) => q.lt(key, value), queryWithGte)\n : queryWithGte\n\n const queryWithLte =\n Object.keys(mergedLte).length > 0\n ? Object.entries(mergedLte).reduce((q, [key, value]) => q.lte(key, value), queryWithLt)\n : queryWithLt\n\n const queryWithNeq =\n Object.keys(mergedNeq).length > 0\n ? Object.entries(mergedNeq).reduce((q, [key, value]) => q.neq(key, value), queryWithLte)\n : queryWithLte\n\n // Apply pattern matching using merged values\n const queryWithLike =\n Object.keys(mergedLike).length > 0\n ? Object.entries(mergedLike).reduce((q, [key, pattern]) => q.like(key, pattern as string), queryWithNeq)\n : queryWithNeq\n\n const queryWithIlike =\n Object.keys(mergedIlike).length > 0\n ? Object.entries(mergedIlike).reduce((q, [key, pattern]) => q.ilike(key, pattern as string), queryWithLike)\n : queryWithLike\n\n return queryWithIlike\n }\n\n /**\n * Apply multiple conditions with OR logic\n */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const applyOrConditions = (query: any, conditions: QueryCondition<T>[]): any => {\n // Start with select\n const selectQuery = query.select(\"*\")\n\n // Apply soft delete filter based on softDeleteMode\n const baseQuery = (() => {\n if (TABLES_WITHOUT_DELETED.has(config.table)) {\n return selectQuery\n }\n if (config.softDeleteMode === \"exclude\") {\n return selectQuery.is(\"deleted\", null)\n }\n if (config.softDeleteMode === \"only\") {\n return selectQuery.not(\"deleted\", \"is\", null)\n }\n // Default: \"include\" - no filter\n return selectQuery\n })()\n\n // Separate common conditions from varying conditions\n const commonConditions = new Map<string, unknown>()\n const varyingConditions: QueryCondition<T>[] = []\n\n // Find conditions that are common across all OR branches\n if (conditions.length > 0) {\n const firstCondition = conditions[0]\n\n // Check each key-value pair in the first condition\n Object.entries(firstCondition.where).forEach(([key, value]) => {\n // If this key-value pair exists in ALL conditions, it's common\n const isCommonCondition = conditions.every(\n (condition) => (condition.where as Record<string, unknown>)[key] === value,\n )\n\n if (isCommonCondition) {\n commonConditions.set(key, value)\n }\n })\n\n // Create new conditions with common parts removed\n varyingConditions.push(\n ...conditions.map((condition) => {\n const newWhere = { ...condition.where } as Record<string, unknown>\n commonConditions.forEach((_, key) => {\n delete newWhere[key]\n })\n return {\n where: newWhere as WhereConditions<TableRow<T>>,\n is: condition.is,\n wherein: condition.wherein,\n }\n }),\n )\n }\n\n // Apply common conditions first\n const queryWithCommon = Array.from(commonConditions.entries()).reduce((query, [key, value]) => {\n if (value === null) {\n return query.is(key, null)\n } else {\n return query.eq(key, value)\n }\n }, baseQuery)\n\n // If no varying conditions remain, we're done\n if (varyingConditions.every((condition) => Object.keys(condition.where).length === 0)) {\n return queryWithCommon\n }\n\n // Build OR conditions from the varying parts only\n const orConditions = varyingConditions\n .map((condition) => {\n const parts: string[] = []\n\n // Add WHERE conditions (only the varying ones)\n Object.entries(condition.where).forEach(([key, value]) => {\n if (value === null) {\n parts.push(`${key}.is.null`)\n } else {\n parts.push(`${key}.eq.\"${value}\"`)\n }\n })\n\n // Add IS conditions\n if (condition.is) {\n Object.entries(condition.is).forEach(([key, value]) => {\n if (value === null) {\n parts.push(`${key}.is.null`)\n } else {\n parts.push(`${key}.is.${value}`)\n }\n })\n }\n\n // Add WHERE IN conditions\n if (condition.wherein) {\n Object.entries(condition.wherein).forEach(([key, values]) => {\n if (values && Array.isArray(values) && values.length > 0) {\n const valueList = values.map((v: unknown) => `\"${v}\"`).join(\",\")\n parts.push(`${key}.in.(${valueList})`)\n }\n })\n }\n\n // Add comparison operators\n if (condition.gt) {\n Object.entries(condition.gt).forEach(([key, value]) => {\n parts.push(`${key}.gt.${value}`)\n })\n }\n if (condition.gte) {\n Object.entries(condition.gte).forEach(([key, value]) => {\n parts.push(`${key}.gte.${value}`)\n })\n }\n if (condition.lt) {\n Object.entries(condition.lt).forEach(([key, value]) => {\n parts.push(`${key}.lt.${value}`)\n })\n }\n if (condition.lte) {\n Object.entries(condition.lte).forEach(([key, value]) => {\n parts.push(`${key}.lte.${value}`)\n })\n }\n if (condition.neq) {\n Object.entries(condition.neq).forEach(([key, value]) => {\n if (value === null) {\n parts.push(`${key}.not.is.null`)\n } else {\n parts.push(`${key}.neq.\"${value}\"`)\n }\n })\n }\n\n // Add pattern matching\n if (condition.like) {\n Object.entries(condition.like).forEach(([key, pattern]) => {\n parts.push(`${key}.like.\"${pattern}\"`)\n })\n }\n if (condition.ilike) {\n Object.entries(condition.ilike).forEach(([key, pattern]) => {\n parts.push(`${key}.ilike.\"${pattern}\"`)\n })\n }\n\n return parts.join(\",\")\n })\n .filter((condition) => condition.length > 0)\n\n // Apply OR conditions if any remain\n\n const finalQuery = orConditions.length > 0 ? queryWithCommon.or(orConditions.join(\",\")) : queryWithCommon\n\n return finalQuery\n }\n\n // Return the Query interface implementation\n return {\n /**\n * Add OR condition to the query\n */\n or: (where: WhereConditions<TableRow<T>>, is?: IsConditions<TableRow<T>>): Query<T> => {\n const newConditions = [...config.conditions, { where, is }]\n return QueryBuilder(client, {\n ...config,\n conditions: newConditions,\n })\n },\n\n /**\n * Filter by branded ID with type safety\n */\n whereId: <ID extends Brand<string, string>>(id: ID): Query<T> => {\n const newConditions = [\n ...config.conditions,\n {\n where: { id: id as unknown } as unknown as WhereConditions<TableRow<T>>,\n },\n ]\n return QueryBuilder(client, {\n ...config,\n conditions: newConditions,\n })\n },\n\n /**\n * Add OR condition with branded ID\n */\n orWhereId: <ID extends Brand<string, string>>(id: ID): Query<T> => {\n return QueryBuilder(client, config).or({\n id: id as unknown,\n } as unknown as WhereConditions<TableRow<T>>)\n },\n\n /**\n * Apply mapping function to query results\n */\n map: <U>(fn: (item: TableRow<T>) => U): MappedQuery<U> => {\n return createMappedQuery(QueryBuilder(client, config), fn)\n },\n\n /**\n * Apply filter function to query results\n */\n filter: (predicate: (item: TableRow<T>) => boolean): Query<T> => {\n return QueryBuilder(client, {\n ...config,\n filterFn: config.filterFn ? (item: TableRow<T>) => config.filterFn!(item) && predicate(item) : predicate,\n })\n },\n\n /**\n * Limit the number of results\n */\n limit: (count: number): Query<T> => {\n return QueryBuilder(client, {\n ...config,\n limit: count,\n })\n },\n\n /**\n * Offset the results for pagination\n */\n offset: (count: number): Query<T> => {\n return QueryBuilder(client, {\n ...config,\n offset: count,\n })\n },\n\n /**\n * Include all records (no soft delete filter)\n */\n includeDeleted: (): Query<T> => {\n if (config.softDeleteAppliedByDefault && config.softDeleteMode === \"include\") {\n log.warn(`[${config.table}] includeDeleted() called but already including deleted by default`)\n }\n return QueryBuilder(client, {\n ...config,\n softDeleteMode: \"include\",\n softDeleteAppliedByDefault: false,\n })\n },\n\n /**\n * Exclude soft-deleted records (apply deleted IS NULL filter)\n */\n excludeDeleted: (): Query<T> => {\n if (config.softDeleteAppliedByDefault && config.softDeleteMode === \"exclude\") {\n log.warn(`[${config.table}] excludeDeleted() called but already excluding deleted by default`)\n }\n return QueryBuilder(client, {\n ...config,\n softDeleteMode: \"exclude\",\n softDeleteAppliedByDefault: false,\n })\n },\n\n /**\n * Query only soft-deleted records (apply deleted IS NOT NULL filter)\n */\n onlyDeleted: (): Query<T> => {\n return QueryBuilder(client, {\n ...config,\n softDeleteMode: \"only\",\n softDeleteAppliedByDefault: false,\n })\n },\n\n /**\n * Execute query expecting exactly one result\n */\n one: (): FPromise<TaskOutcome<Option<TableRow<T>>>> => {\n return wrapAsync(async () => {\n try {\n const query = buildSupabaseQuery()\n const { data, error } = await query.single()\n\n if (error) {\n log.error(`Error getting ${config.table} item: ${toError(error).toString()}`)\n return Err<Option<TableRow<T>>>(toError(error))\n }\n\n const result = data as TableRow<T>\n const filteredResult = config.filterFn ? config.filterFn(result) : true\n\n if (!filteredResult) {\n return Ok(Option.none<TableRow<T>>())\n }\n\n return Ok(Option(result))\n } catch (error) {\n log.error(`Error executing single query on ${config.table}: ${toError(error).toString()}`)\n return Err<Option<TableRow<T>>>(toError(error))\n }\n })\n },\n\n /**\n * Execute query expecting zero or more results\n */\n many: (): FPromise<TaskOutcome<List<TableRow<T>>>> => {\n return wrapAsync(async () => {\n try {\n const query = buildSupabaseQuery()\n const { data, error } = await query\n\n if (error) {\n log.error(`Error getting ${config.table} items: ${toError(error).toString()}`)\n return Err<List<TableRow<T>>>(toError(error))\n }\n\n const rawResults = data as TableRow<T>[]\n\n // Apply filter if present\n const results = config.filterFn ? rawResults.filter(config.filterFn) : rawResults\n\n return Ok(List(results))\n } catch (error) {\n log.error(`Error executing multi query on ${config.table}: ${toError(error).toString()}`)\n return Err<List<TableRow<T>>>(toError(error))\n }\n })\n },\n\n /**\n * Execute query expecting first result from potentially multiple\n */\n first: (): FPromise<TaskOutcome<Option<TableRow<T>>>> => {\n return wrapAsync(async () => {\n const manyResult = await QueryBuilder(client, config).many()\n const list = manyResult.orThrow()\n if (list.isEmpty) {\n return Ok(Option.none<TableRow<T>>())\n }\n return Ok(Option(list.head))\n })\n },\n\n /**\n * Execute query expecting exactly one result, throw if error or not found\n */\n oneOrThrow: async (): Promise<TableRow<T>> => {\n const result = await QueryBuilder(client, config).one()\n const option = result.orThrow()\n return option.orThrow(new Error(`No record found in ${config.table}`))\n },\n\n /**\n * Execute query expecting zero or more results, throw if error\n */\n manyOrThrow: async (): Promise<List<TableRow<T>>> => {\n const result = await QueryBuilder(client, config).many()\n return result.orThrow()\n },\n\n /**\n * Execute query expecting first result, throw if error or empty\n */\n firstOrThrow: async (): Promise<TableRow<T>> => {\n const result = await QueryBuilder(client, config).first()\n const option = result.orThrow()\n return option.orThrow(new Error(`No records found in ${config.table}`))\n },\n }\n}\n\n/**\n * Functional MappedQuery implementation\n */\nconst createMappedQuery = <T extends TableNames, U>(\n sourceQuery: Query<T>,\n mapFn: (item: TableRow<T>) => U,\n): MappedQuery<U> => {\n return {\n map: <V>(fn: (item: U) => V): MappedQuery<V> => {\n return createMappedQuery(sourceQuery, (item: TableRow<T>) => fn(mapFn(item)))\n },\n\n filter: (predicate: (item: U) => boolean): MappedQuery<U> => {\n const filteredQuery = sourceQuery.filter((item: TableRow<T>) => predicate(mapFn(item)))\n return createMappedQuery(filteredQuery, mapFn)\n },\n\n one: (): FPromise<TaskOutcome<Option<U>>> => {\n return wrapAsync(async () => {\n const maybeItemResult = await sourceQuery.one()\n const maybeItem = maybeItemResult.orThrow()\n return maybeItem.fold(\n () => Ok(Option.none<U>()),\n (item) => Ok(Option(mapFn(item))),\n )\n })\n },\n\n many: (): FPromise<TaskOutcome<List<U>>> => {\n return wrapAsync(async () => {\n const itemsResult = await sourceQuery.many()\n const items = itemsResult.orThrow()\n return Ok(items.map(mapFn))\n })\n },\n\n first: (): FPromise<TaskOutcome<Option<U>>> => {\n return wrapAsync(async () => {\n const maybeItemResult = await sourceQuery.first()\n const maybeItem = maybeItemResult.orThrow()\n return maybeItem.fold(\n () => Ok(Option.none<U>()),\n (item) => Ok(Option(mapFn(item))),\n )\n })\n },\n\n /**\n * Execute mapped query expecting exactly one result, throw if error or not found\n */\n oneOrThrow: async (): Promise<U> => {\n const result = await createMappedQuery(sourceQuery, mapFn).one()\n const option = result.orThrow()\n return option.orThrow(new Error(`No record found`))\n },\n\n /**\n * Execute mapped query expecting zero or more results, throw if error\n */\n manyOrThrow: async (): Promise<List<U>> => {\n const result = await createMappedQuery(sourceQuery, mapFn).many()\n return result.orThrow()\n },\n\n /**\n * Execute mapped query expecting first result, throw if error or empty\n */\n firstOrThrow: async (): Promise<U> => {\n const result = await createMappedQuery(sourceQuery, mapFn).first()\n const option = result.orThrow()\n return option.orThrow(new Error(`No records found`))\n },\n }\n}\n\n/**\n * Factory function to create new functional QueryBuilder instances\n */\nexport const createQuery = <T extends TableNames>(\n client: SupabaseClientType,\n table: T,\n where: WhereConditions<TableRow<T>> = {},\n is?: IsConditions<TableRow<T>>,\n wherein?: Partial<Record<keyof TableRow<T>, unknown[]>>,\n order?: [keyof TableRow<T> & string, { ascending?: boolean; nullsFirst?: boolean }],\n softDeleteConfig?: { mode?: \"include\" | \"exclude\" | \"only\"; appliedByDefault?: boolean },\n): Query<T> => {\n const config: QueryBuilderConfig<T> = {\n table,\n conditions: [{ where, is, wherein }],\n order,\n softDeleteMode: softDeleteConfig?.mode,\n softDeleteAppliedByDefault: softDeleteConfig?.appliedByDefault,\n }\n return QueryBuilder(client, config)\n}\n","import type { EmptyObject, TableNames, TableRow } from \"@/types\"\n\nimport type { Brand, FPromise, List, Option, TaskOutcome } from \"functype\"\n\n// Comparison operators for advanced queries\nexport type ComparisonOperators<V> = {\n gte?: V // Greater than or equal\n gt?: V // Greater than\n lte?: V // Less than or equal\n lt?: V // Less than\n neq?: V // Not equal\n like?: string // LIKE pattern (for string fields)\n ilike?: string // Case-insensitive LIKE\n in?: V[] // IN array\n is?: null | boolean // IS NULL/TRUE/FALSE\n}\n\n// Type-safe WHERE conditions that provide IntelliSense for table columns\n// Supports both direct values and operator objects for advanced queries\nexport type WhereConditions<T extends object> = Partial<{\n [K in keyof T]: T[K] | null | ComparisonOperators<T[K]>\n}> & {\n // Special operators that work across columns with type-safe values\n gte?: Partial<{ [K in keyof T]?: T[K] }>\n gt?: Partial<{ [K in keyof T]?: T[K] }>\n lte?: Partial<{ [K in keyof T]?: T[K] }>\n lt?: Partial<{ [K in keyof T]?: T[K] }>\n neq?: Partial<{ [K in keyof T]?: T[K] }>\n like?: Partial<{ [K in keyof T]?: Extract<T[K], string> }>\n ilike?: Partial<{ [K in keyof T]?: Extract<T[K], string> }>\n}\n\n// Enhanced type for IS conditions with field-level type safety\nexport type IsConditions<T extends object = EmptyObject> = Partial<Record<keyof T, null | boolean>>\n\n// Soft delete mode for controlling how deleted records are handled\nexport type SoftDeleteMode = \"include\" | \"exclude\" | \"only\"\n\n// =============================================================================\n// Standard Execution Interfaces for Consistent OrThrow Pattern\n// =============================================================================\n\n/**\n * Base execution interface that all database operations implement\n */\nexport interface ExecutableQuery<T> {\n // TaskOutcome version (for explicit error handling)\n execute(): FPromise<TaskOutcome<T>>\n\n // OrThrow version (for simple error handling)\n executeOrThrow(): Promise<T>\n}\n\n/**\n * Standard interface for operations that return a single result\n */\nexport interface SingleExecution<T> extends ExecutableQuery<Option<T>> {\n one(): FPromise<TaskOutcome<Option<T>>>\n oneOrThrow(): Promise<T>\n}\n\n/**\n * Standard interface for operations that return multiple results\n */\nexport interface MultiExecution<T> extends ExecutableQuery<List<T>> {\n many(): FPromise<TaskOutcome<List<T>>>\n manyOrThrow(): Promise<List<T>>\n}\n\n// Branded type support for query conditions\nexport type BrandedWhereParams<T extends object = EmptyObject> = {\n [K in keyof T]?: T[K] | unknown // Simplified to avoid complex conditional types\n}\n\n// Helper type for branded field values\nexport type BrandedFieldValue<T> = T extends Brand<string, infer BaseType> ? T | BaseType : T\n\n// Core Query interface with branded type support\nexport interface Query<T extends TableNames> {\n // Execution methods - explicit about expected results\n one(): FPromise<TaskOutcome<Option<TableRow<T>>>>\n many(): FPromise<TaskOutcome<List<TableRow<T>>>>\n first(): FPromise<TaskOutcome<Option<TableRow<T>>>>\n\n // OrThrow methods - throw errors instead of returning TaskOutcome (v0.8.0+)\n oneOrThrow(): Promise<TableRow<T>>\n manyOrThrow(): Promise<List<TableRow<T>>>\n firstOrThrow(): Promise<TableRow<T>>\n\n // Query composition - chainable OR logic with type-safe where conditions\n or(where: WhereConditions<TableRow<T>>, is?: IsConditions<TableRow<T>>): Query<T>\n\n // Branded type-aware query methods (simplified)\n whereId<ID extends Brand<string, string>>(id: ID): Query<T>\n orWhereId<ID extends Brand<string, string>>(id: ID): Query<T>\n\n // Functional operations - maintain composability\n map<U>(fn: (item: TableRow<T>) => U): MappedQuery<U>\n filter(predicate: (item: TableRow<T>) => boolean): Query<T>\n\n // Pagination\n limit(count: number): Query<T>\n offset(count: number): Query<T>\n\n // Soft delete filtering\n includeDeleted(): Query<T>\n excludeDeleted(): Query<T>\n onlyDeleted(): Query<T>\n}\n\n// Mapped query for transformed results\nexport interface MappedQuery<U> {\n one(): FPromise<TaskOutcome<Option<U>>>\n many(): FPromise<TaskOutcome<List<U>>>\n first(): FPromise<TaskOutcome<Option<U>>>\n\n // OrThrow methods - throw errors instead of returning TaskOutcome (v0.8.0+)\n oneOrThrow(): Promise<U>\n manyOrThrow(): Promise<List<U>>\n firstOrThrow(): Promise<U>\n\n // Continue chaining\n map<V>(fn: (item: U) => V): MappedQuery<V>\n filter(predicate: (item: U) => boolean): MappedQuery<U>\n}\n\n// Query condition for internal state management with type-safe where\nexport interface QueryCondition<T extends TableNames> {\n where: WhereConditions<TableRow<T>>\n is?: IsConditions<TableRow<T>>\n wherein?: Partial<Record<keyof TableRow<T>, unknown[]>>\n // Comparison operators\n gt?: Partial<Record<keyof TableRow<T>, number | string | Date>>\n gte?: Partial<Record<keyof TableRow<T>, number | string | Date>>\n lt?: Partial<Record<keyof TableRow<T>, number | string | Date>>\n lte?: Partial<Record<keyof TableRow<T>, number | string | Date>>\n neq?: Partial<Record<keyof TableRow<T>, unknown>>\n // Pattern matching\n like?: Partial<Record<keyof TableRow<T>, string>>\n ilike?: Partial<Record<keyof TableRow<T>, string>>\n}\n\n// Entity-specific query interfaces for better type safety\nexport interface EntityQuery<T extends TableNames> extends Query<T> {\n // Entity-specific methods can be added here\n normalize(): NormalizedQuery<T>\n}\n\nexport interface NormalizedQuery<T extends TableNames> {\n one(): FPromise<TaskOutcome<Option<TableRow<T>>>>\n many(): FPromise<TaskOutcome<List<TableRow<T>>>>\n first(): FPromise<TaskOutcome<Option<TableRow<T>>>>\n}\n\n// Type guards for runtime type checking\nexport const isQuery = <T extends TableNames>(obj: unknown): obj is Query<T> => {\n return (\n typeof obj === \"object\" &&\n obj !== null &&\n \"one\" in obj &&\n \"many\" in obj &&\n \"first\" in obj &&\n \"or\" in obj &&\n \"map\" in obj &&\n \"filter\" in obj\n )\n}\n\nexport const isMappedQuery = <U>(obj: unknown): obj is MappedQuery<U> => {\n return (\n typeof obj === \"object\" &&\n obj !== null &&\n \"one\" in obj &&\n \"many\" in obj &&\n \"first\" in obj &&\n \"map\" in obj &&\n \"filter\" in obj\n )\n}\n\n// Utility types for query parameters with type safety\nexport type QueryWhereParams<T extends TableNames> = WhereConditions<TableRow<T>>\nexport type QueryIsParams<T extends TableNames> = IsConditions<TableRow<T>>\nexport type QueryWhereinParams<T extends TableNames> = Partial<Record<keyof TableRow<T>, unknown[]>>\nexport type QueryOrderParams<T extends TableNames> = [\n keyof TableRow<T> & string,\n { ascending?: boolean; nullsFirst?: boolean },\n]\n\n// Builder configuration for query construction\nexport interface QueryBuilderConfig<T extends TableNames> {\n table: T\n conditions: QueryCondition<T>[]\n order?: QueryOrderParams<T>\n mapFn?: (item: TableRow<T>) => unknown\n filterFn?: (item: TableRow<T>) => boolean\n limit?: number\n offset?: number\n softDeleteMode?: SoftDeleteMode\n softDeleteAppliedByDefault?: boolean\n}\n","import type { EmptyObject, SupabaseClientType, TableInsert, TableNames, TableRow, TableUpdate } from \"@/types\"\nimport { toError } from \"@/utils/errors\"\n\nimport type { FPromise, TaskOutcome } from \"functype\"\nimport { Err, List, Ok } from \"functype\"\n\nimport type { Query, WhereConditions } from \"./Query\"\nimport { createQuery } from \"./QueryBuilder\"\n\n// Re-export query types\nexport type {\n ComparisonOperators,\n EntityQuery,\n ExecutableQuery,\n IsConditions,\n MappedQuery,\n MultiExecution,\n Query,\n QueryBuilderConfig,\n QueryCondition,\n QueryIsParams,\n QueryOrderParams,\n QueryWhereinParams,\n QueryWhereParams,\n SingleExecution,\n SoftDeleteMode,\n WhereConditions,\n} from \"./Query\"\n\n// Re-export type guards\nexport { isMappedQuery, isQuery } from \"./Query\"\n\n// Local type for IS conditions\ntype IsConditionsLocal<T extends object = EmptyObject> = Partial<Record<keyof T, null | boolean>>\n\n// Helper to wrap async operations with error handling\nconst wrapAsync = <T>(fn: () => Promise<TaskOutcome<T>>): FPromise<TaskOutcome<T>> => {\n return fn() as unknown as FPromise<TaskOutcome<T>>\n}\n\n/**\n * Retrieves a single entity from the specified table.\n * @template T - The table name\n * @param client - The Supabase client instance\n * @param table - The table to query\n * @param where - Conditions to filter by\n * @param is - IS conditions to filter by\n * @returns A promise resolving to the entity if found\n */\nexport const getEntity = <T extends TableNames>(\n client: SupabaseClientType,\n table: T,\n where: WhereConditions<TableRow<T>>,\n is?: IsConditionsLocal<TableRow<T>>,\n): FPromise<TaskOutcome<TableRow<T>>> =>\n wrapAsync(async () => {\n try {\n const baseQuery = client.from(table).select(\"*\").match(where)\n\n const queryWithIs = is\n ? List(Object.entries(is)).foldLeft(baseQuery)((query, [column, value]) =>\n query.is(column as keyof TableRow<T> & string, value as boolean | null),\n )\n : baseQuery\n\n const { data, error } = await queryWithIs.single()\n\n if (error) {\n return Err<TableRow<T>>(toError(error))\n }\n\n return Ok(data as TableRow<T>)\n } catch (error) {\n return Err<TableRow<T>>(toError(error))\n }\n })\n\n/**\n * Retrieves multiple entities from the specified table.\n * @template T - The table name\n * @param client - The Supabase client instance\n * @param table - The table to query\n * @param where - Conditions to filter by\n * @param is - IS conditions to filter by\n * @param wherein - WHERE IN conditions to filter by\n * @param order - Optional ordering parameters\n * @returns A promise resolving to the entities if found\n */\nexport const getEntities = <T extends TableNames>(\n client: SupabaseClientType,\n table: T,\n where: WhereConditions<TableRow<T>> = {},\n is?: IsConditionsLocal<TableRow<T>>,\n wherein?: Partial<Record<keyof TableRow<T>, unknown[]>>,\n order: [keyof TableRow<T> & string, { ascending?: boolean; nullsFirst?: boolean }] = [\n \"id\" as keyof TableRow<T> & string,\n { ascending: true },\n ],\n): FPromise<TaskOutcome<List<TableRow<T>>>> =>\n wrapAsync(async () => {\n try {\n const baseQuery = client.from(table).select(\"*\").match(where)\n\n const queryWithIn = wherein\n ? List(Object.entries(wherein)).foldLeft(baseQuery)((query, [column, values]) =>\n query.in(column, values as never),\n )\n : baseQuery\n\n const queryWithIs = is\n ? List(Object.entries(is)).foldLeft(queryWithIn)((query, [column, value]) =>\n query.is(column as keyof TableRow<T> & string, value as boolean | null),\n )\n : queryWithIn\n\n const queryOrderBy = queryWithIs.order(order[0], order[1])\n\n const { data, error } = await queryOrderBy\n\n if (error) {\n return Err<List<TableRow<T>>>(toError(error))\n }\n\n return Ok(List(data as TableRow<T>[]))\n } catch (error) {\n return Err<List<TableRow<T>>>(toError(error))\n }\n })\n\n/**\n * Adds multiple entities to the specified table.\n * @template T - The table name\n * @param client - The Supabase client instance\n * @param table - The table to insert into\n * @param entities - The entities to add\n * @returns A promise resolving to the added entities\n */\nexport const addEntities = <T extends TableNames>(\n client: SupabaseClientType,\n table: T,\n entities: TableInsert<T>[],\n): FPromise<TaskOutcome<List<TableRow<T>>>> =>\n wrapAsync(async () => {\n try {\n const { data, error } = await client\n .from(table)\n .insert(entities as never)\n .select()\n\n if (error) {\n return Err<List<TableRow<T>>>(toError(error))\n }\n\n return Ok(List(data as unknown as TableRow<T>[]))\n } catch (error) {\n return Err<List<TableRow<T>>>(toError(error))\n }\n })\n\n/**\n * Updates a single entity in the specified table.\n * @template T - The table name\n * @param client - The Supabase client instance\n * @param table - The table to update\n * @param entities - The entity data to update\n * @param where - Conditions to filter by\n * @param is - IS conditions to filter by\n * @param wherein - WHERE IN conditions to filter by\n * @returns A promise resolving to the updated entity\n */\nexport const updateEntity = <T extends TableNames>(\n client: SupabaseClientType,\n table: T,\n entities: TableUpdate<T>,\n where: WhereConditions<TableRow<T>>,\n is?: IsConditionsLocal<TableRow<T>>,\n wherein?: Partial<Record<keyof TableRow<T>, unknown[]>>,\n): FPromise<TaskOutcome<TableRow<T>>> =>\n wrapAsync(async () => {\n try {\n const baseQuery = client\n .from(table)\n .update(entities as never)\n .match(where)\n\n const queryWithIn = wherein\n ? List(Object.entries(wherein)).foldLeft(baseQuery)((query, [column, values]) =>\n query.in(column, values as never),\n )\n : baseQuery\n\n const queryWithIs = is\n ? List(Object.entries(is)).foldLeft(queryWithIn)((query, [column, value]) =>\n query.is(column as keyof TableRow<T> & string, value as boolean | null),\n )\n : queryWithIn\n\n const { data, error } = await queryWithIs.select().single()\n\n if (error) {\n return Err<TableRow<T>>(toError(error))\n }\n\n return Ok(data as TableRow<T>)\n } catch (error) {\n return Err<TableRow<T>>(toError(error))\n }\n })\n\n/**\n * Updates multiple entities in the specified table.\n * @template T - The table name\n * @param client - The Supabase client instance\n * @param table - The table to update\n * @param entities - The entities to update\n * @param identity - The column(s) to use as the identity\n * @param where - Conditions to filter by\n * @param is - IS conditions to filter by\n * @param wherein - WHERE IN conditions to filter by\n * @returns A promise resolving to the updated entities\n */\nexport const updateEntities = <T extends TableNames>(\n client: SupabaseClientType,\n table: T,\n entities: TableUpdate<T>[],\n identity: (keyof TableRow<T> & string) | (keyof TableRow<T> & string)[] = \"id\" as keyof TableRow<T> & string,\n where?: WhereConditions<TableRow<T>>,\n is?: IsConditionsLocal<TableRow<T>>,\n wherein?: Partial<Record<keyof TableRow<T>, unknown[]>>,\n): FPromise<TaskOutcome<List<TableRow<T>>>> =>\n wrapAsync(async () => {\n try {\n const onConflict = Array.isArray(identity) ? identity.join(\",\") : identity\n\n const baseQuery = client\n .from(table)\n .upsert(entities as never, { onConflict })\n .match(where ?? {})\n\n const queryWithIn = wherein\n ? List(Object.entries(wherein)).foldLeft(baseQuery)((query, [column, values]) =>\n query.in(column, values as never),\n )\n : baseQuery\n\n const queryWithIs = is\n ? List(Object.entries(is)).foldLeft(queryWithIn)((query, [column, value]) =>\n query.is(column as keyof TableRow<T> & string, value as boolean | null),\n )\n : queryWithIn\n\n const { data, error } = await queryWithIs.select()\n\n if (error) {\n return Err<List<TableRow<T>>>(toError(error))\n }\n\n return Ok(List(data as TableRow<T>[]))\n } catch (error) {\n return Err<List<TableRow<T>>>(toError(error))\n }\n })\n\n/**\n * Creates a new Query for the specified table with initial conditions.\n * This is the new Query-based API that supports OR chaining and functional operations.\n *\n * @template T - The table name\n * @param client - The Supabase client instance\n * @param table - The table to query\n * @param where - Initial WHERE conditions to filter by\n * @param is - Initial IS conditions to filter by\n * @param wherein - Initial WHERE IN conditions to filter by\n * @param order - Optional ordering parameters\n * @returns A Query<T> instance that supports chaining and lazy evaluation\n *\n * @example\n * // Simple query\n * const user = await query(client, \"users\", { id: \"123\" }).one()\n *\n * @example\n * // Query with OR logic\n * const users = await query(client, \"users\", { role: \"admin\" })\n * .or({ role: \"moderator\" })\n * .many()\n *\n * @example\n * // Query with functional operations\n * const names = await query(client, \"users\", { active: true })\n * .map(user => user.name)\n * .filter(name => name.startsWith('A'))\n * .many()\n */\nexport const query = <T extends TableNames>(\n client: SupabaseClientType,\n table: T,\n where: WhereConditions<TableRow<T>> = {},\n is?: IsConditionsLocal<TableRow<T>>,\n wherein?: Partial<Record<keyof TableRow<T>, unknown[]>>,\n order?: [keyof TableRow<T> & string, { ascending?: boolean; nullsFirst?: boolean }],\n): Query<T> => {\n return createQuery(client, table, where, is, wherein, order)\n}\n","import { addEntities, updateEntities, updateEntity } from \"@/query\"\nimport type { MultiExecution, Query, SingleExecution, WhereConditions } from \"@/query/Query\"\nimport { createQuery } from \"@/query/QueryBuilder\"\nimport type { EmptyObject, SupabaseClientType, TableInsert, TableNames, TableRow, TableUpdate } from \"@/types\"\n\nimport type { FPromise, List, TaskOutcome } from \"functype\"\nimport { Option } from \"functype\"\n\n// Field-level type safety for queries\nexport type TypedRecord<T, V> = Partial<Record<keyof T, V>>\n\n// Entity configuration\nexport type EntityConfig = {\n /** Soft delete filtering. true = exclude deleted items, false = include deleted items */\n softDelete: boolean\n /** Partition key for multi-tenant isolation. e.g., { tenant_id: \"123\" } */\n partitionKey?: Record<string, unknown>\n}\n\n// Base parameter types with field-level type safety\nexport type WhereParams<T extends object = EmptyObject> = {\n where?: WhereConditions<T>\n}\n\nexport type IsParams<T extends object = EmptyObject> = {\n is?: TypedRecord<T, null | boolean>\n}\n\nexport type WhereinParams<T extends object = EmptyObject> = {\n wherein?: TypedRecord<T, unknown[]>\n}\n\nexport type OrderParams<T extends object = EmptyObject> = {\n order?: [keyof T & string, { ascending?: boolean; nullsFirst?: boolean }]\n}\n\nexport type IdParam = {\n id: string\n}\n\n// Composable parameter types with field-level type safety\nexport type GetItemParams<T extends object = EmptyObject> = IdParam & WhereParams<T> & IsParams<T>\n\nexport type GetItemsParams<T extends object = EmptyObject> = WhereParams<T> &\n IsParams<T> &\n WhereinParams<T> &\n OrderParams<T>\n\nexport type AddItemsParams<T extends TableNames> = {\n items: TableInsert<T>[]\n}\n\nexport type UpdateItemParams<T extends TableNames, Row extends object = EmptyObject> = IdParam & {\n item: TableUpdate<T>\n} & WhereParams<Row> &\n IsParams<Row> &\n WhereinParams<Row>\n\nexport type UpdateItemsParams<T extends TableNames, Row extends object = EmptyObject> = {\n items: TableUpdate<T>[]\n identity?: (keyof Row & string) | (keyof Row & string)[]\n} & WhereParams<Row> &\n IsParams<Row> &\n WhereinParams<Row>\n\n// =============================================================================\n// Mutation Query Wrappers for Consistent OrThrow Pattern\n// =============================================================================\n\n/**\n * Wrapper type for multi-result mutation operations that implements standard execution interface\n */\nexport type MutationMultiExecution<T> = FPromise<TaskOutcome<List<T>>> & MultiExecution<T>\n\n/**\n * Wrapper type for single-result mutation operations that implements standard execution interface\n */\nexport type MutationSingleExecution<T> = FPromise<TaskOutcome<T>> & SingleExecution<T>\n\n/**\n * Creates a multi-result mutation query that implements the standard execution interface\n */\nexport function MultiMutationQuery<T>(promise: FPromise<TaskOutcome<List<T>>>): MutationMultiExecution<T> {\n const result = Object.assign(promise, {\n // Standard MultiExecution interface\n many: () => promise,\n manyOrThrow: async (): Promise<List<T>> => {\n const taskResult = await promise\n return taskResult.orThrow()\n },\n\n // Standard ExecutableQuery interface\n execute: () => promise,\n executeOrThrow: async (): Promise<List<T>> => {\n const taskResult = await promise\n return taskResult.orThrow()\n },\n })\n return result as MutationMultiExecution<T>\n}\n\n/**\n * Creates a single-result mutation query that implements the standard execution interface\n */\nexport function SingleMutationQuery<T>(promise: FPromise<TaskOutcome<T>>): MutationSingleExecution<T> {\n const result = Object.assign(promise, {\n // Standard SingleExecution interface\n one: () => promise.then((outcome: TaskOutcome<T>) => outcome.map((value: T) => Option(value))),\n oneOrThrow: async (): Promise<T> => {\n const taskResult = await promise\n return taskResult.orThrow()\n },\n\n // Standard ExecutableQuery interface\n execute: () => promise.then((outcome: TaskOutcome<T>) => outcome.map((value: T) => Option(value))),\n executeOrThrow: async (): Promise<Option<T>> => {\n const taskResult = await promise\n const value = taskResult.orThrow()\n return Option(value)\n },\n })\n return result as MutationSingleExecution<T>\n}\n\n/**\n * Base interface for Entity instances\n */\nexport type IEntity<T extends TableNames> = {\n getItem(params: GetItemParams<TableRow<T>>): Query<T>\n getItems(params?: GetItemsParams<TableRow<T>>): Query<T>\n addItems(params: AddItemsParams<T>): MutationMultiExecution<TableRow<T>>\n updateItem(params: UpdateItemParams<T, TableRow<T>>): MutationSingleExecution<TableRow<T>>\n updateItems(params: UpdateItemsParams<T, TableRow<T>>): MutationMultiExecution<TableRow<T>>\n}\n\n/**\n * Creates an entity interface with methods for interacting with the given table.\n * @param client The Supabase client instance to use for queries.\n * @param name The name of the table to interact with.\n * @param config Configuration for entity behavior (required).\n * @returns An object with methods for interacting with the table.\n */\nexport const Entity = <T extends TableNames>(client: SupabaseClientType, name: T, config: EntityConfig): IEntity<T> => {\n type ROW = TableRow<T>\n\n