UNPKG

supabase-typed-query

Version:

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

653 lines (652 loc) 22.5 kB
import { Ok, Option, Err, List } from "functype"; import { Err as Err2, List as List2, Ok as Ok2, Option as Option2 } from "functype"; const log = { error: (msg) => console.error(`[supabase-typed-query] ${msg}`), warn: (msg) => console.warn(`[supabase-typed-query] ${msg}`), info: (msg) => console.info(`[supabase-typed-query] ${msg}`) }; const TABLES_WITHOUT_DELETED = /* @__PURE__ */ new Set([]); const wrapAsync$1 = (fn) => { return fn(); }; const QueryBuilder = (client, config) => { const buildSupabaseQuery = () => { const { table, conditions, order, limit, offset } = config; const baseQuery = client.from(table); const queryWithConditions = conditions.length === 1 ? applyCondition(baseQuery, conditions[0]) : applyOrConditions(baseQuery, conditions); const queryWithOrder = order ? queryWithConditions.order(order[0], order[1]) : queryWithConditions; const finalQuery = (() => { if (limit && offset !== void 0) { return queryWithOrder.range(offset, offset + limit - 1); } else if (limit) { return queryWithOrder.limit(limit); } else if (offset !== void 0) { return queryWithOrder.range(offset, Number.MAX_SAFE_INTEGER); } return queryWithOrder; })(); return finalQuery; }; const applyCondition = (query2, condition) => { const { where, is, wherein, gt, gte, lt, lte, neq, like, ilike } = condition; const processedWhere = {}; const extractedOperators = {}; if (where) { const { gt: whereGt, gte: whereGte, lt: whereLt, lte: whereLte, neq: whereNeq, like: whereLike, ilike: whereIlike, ...rest } = where; if (whereGt) extractedOperators.gt = whereGt; if (whereGte) extractedOperators.gte = whereGte; if (whereLt) extractedOperators.lt = whereLt; if (whereLte) extractedOperators.lte = whereLte; if (whereNeq) extractedOperators.neq = whereNeq; if (whereLike) extractedOperators.like = whereLike; if (whereIlike) extractedOperators.ilike = whereIlike; for (const [key, value] of Object.entries(rest)) { if (value && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date)) { const ops = value; if (ops.gte !== void 0) { extractedOperators.gte = { ...extractedOperators.gte, [key]: ops.gte }; } if (ops.gt !== void 0) { extractedOperators.gt = { ...extractedOperators.gt, [key]: ops.gt }; } if (ops.lte !== void 0) { extractedOperators.lte = { ...extractedOperators.lte, [key]: ops.lte }; } if (ops.lt !== void 0) { extractedOperators.lt = { ...extractedOperators.lt, [key]: ops.lt }; } if (ops.neq !== void 0) { extractedOperators.neq = { ...extractedOperators.neq, [key]: ops.neq }; } if (ops.like !== void 0) { extractedOperators.like = { ...extractedOperators.like, [key]: ops.like }; } if (ops.ilike !== void 0) { extractedOperators.ilike = { ...extractedOperators.ilike, [key]: ops.ilike }; } if (ops.in !== void 0) { if (!wherein) { const cond = condition; cond.wherein = {}; } const whereinObj = condition.wherein; whereinObj[key] = ops.in; } if (ops.is !== void 0) { if (!is) { const cond = condition; cond.is = {}; } const isObj = condition.is; isObj[key] = ops.is; } if (!ops.gte && !ops.gt && !ops.lte && !ops.lt && !ops.neq && !ops.like && !ops.ilike && !ops.in && !ops.is) { processedWhere[key] = value; } } else { processedWhere[key] = value; } } } const mergedGt = { ...gt, ...extractedOperators.gt }; const mergedGte = { ...gte, ...extractedOperators.gte }; const mergedLt = { ...lt, ...extractedOperators.lt }; const mergedLte = { ...lte, ...extractedOperators.lte }; const mergedNeq = { ...neq, ...extractedOperators.neq }; const mergedLike = { ...like, ...extractedOperators.like }; const mergedIlike = { ...ilike, ...extractedOperators.ilike }; const baseQuery = !TABLES_WITHOUT_DELETED.has(config.table) ? query2.select("*").match(processedWhere).is("deleted", null) : query2.select("*").match(processedWhere); const queryWithWhereIn = wherein ? List(Object.entries(wherein)).foldLeft(baseQuery)((q, [column, values]) => q.in(column, values)) : baseQuery; const queryWithIs = is ? List(Object.entries(is)).foldLeft(queryWithWhereIn)( (q, [column, value]) => q.is(column, value) ) : queryWithWhereIn; const queryWithGt = Object.keys(mergedGt).length > 0 ? Object.entries(mergedGt).reduce((q, [key, value]) => q.gt(key, value), queryWithIs) : queryWithIs; const queryWithGte = Object.keys(mergedGte).length > 0 ? Object.entries(mergedGte).reduce((q, [key, value]) => q.gte(key, value), queryWithGt) : queryWithGt; const queryWithLt = Object.keys(mergedLt).length > 0 ? Object.entries(mergedLt).reduce((q, [key, value]) => q.lt(key, value), queryWithGte) : queryWithGte; const queryWithLte = Object.keys(mergedLte).length > 0 ? Object.entries(mergedLte).reduce((q, [key, value]) => q.lte(key, value), queryWithLt) : queryWithLt; const queryWithNeq = Object.keys(mergedNeq).length > 0 ? Object.entries(mergedNeq).reduce((q, [key, value]) => q.neq(key, value), queryWithLte) : queryWithLte; const queryWithLike = Object.keys(mergedLike).length > 0 ? Object.entries(mergedLike).reduce((q, [key, pattern]) => q.like(key, pattern), queryWithNeq) : queryWithNeq; const queryWithIlike = Object.keys(mergedIlike).length > 0 ? Object.entries(mergedIlike).reduce((q, [key, pattern]) => q.ilike(key, pattern), queryWithLike) : queryWithLike; return queryWithIlike; }; const applyOrConditions = (query2, conditions) => { const baseQuery = !TABLES_WITHOUT_DELETED.has(config.table) ? query2.select("*").is("deleted", null) : query2.select("*"); const commonConditions = /* @__PURE__ */ new Map(); const varyingConditions = []; if (conditions.length > 0) { const firstCondition = conditions[0]; Object.entries(firstCondition.where).forEach(([key, value]) => { const isCommonCondition = conditions.every( (condition) => condition.where[key] === value ); if (isCommonCondition) { commonConditions.set(key, value); } }); varyingConditions.push( ...conditions.map((condition) => { const newWhere = { ...condition.where }; commonConditions.forEach((_, key) => { delete newWhere[key]; }); return { where: newWhere, is: condition.is, wherein: condition.wherein }; }) ); } const queryWithCommon = Array.from(commonConditions.entries()).reduce((query22, [key, value]) => { if (value === null) { return query22.is(key, null); } else { return query22.eq(key, value); } }, baseQuery); if (varyingConditions.every((condition) => Object.keys(condition.where).length === 0)) { return queryWithCommon; } const orConditions = varyingConditions.map((condition) => { const parts = []; Object.entries(condition.where).forEach(([key, value]) => { if (value === null) { parts.push(`${key}.is.null`); } else { parts.push(`${key}.eq."${value}"`); } }); if (condition.is) { Object.entries(condition.is).forEach(([key, value]) => { if (value === null) { parts.push(`${key}.is.null`); } else { parts.push(`${key}.is.${value}`); } }); } if (condition.wherein) { Object.entries(condition.wherein).forEach(([key, values]) => { if (values && Array.isArray(values) && values.length > 0) { const valueList = values.map((v) => `"${v}"`).join(","); parts.push(`${key}.in.(${valueList})`); } }); } if (condition.gt) { Object.entries(condition.gt).forEach(([key, value]) => { parts.push(`${key}.gt.${value}`); }); } if (condition.gte) { Object.entries(condition.gte).forEach(([key, value]) => { parts.push(`${key}.gte.${value}`); }); } if (condition.lt) { Object.entries(condition.lt).forEach(([key, value]) => { parts.push(`${key}.lt.${value}`); }); } if (condition.lte) { Object.entries(condition.lte).forEach(([key, value]) => { parts.push(`${key}.lte.${value}`); }); } if (condition.neq) { Object.entries(condition.neq).forEach(([key, value]) => { if (value === null) { parts.push(`${key}.not.is.null`); } else { parts.push(`${key}.neq."${value}"`); } }); } if (condition.like) { Object.entries(condition.like).forEach(([key, pattern]) => { parts.push(`${key}.like."${pattern}"`); }); } if (condition.ilike) { Object.entries(condition.ilike).forEach(([key, pattern]) => { parts.push(`${key}.ilike."${pattern}"`); }); } return parts.join(","); }).filter((condition) => condition.length > 0); const finalQuery = orConditions.length > 0 ? queryWithCommon.or(orConditions.join(",")) : queryWithCommon; return finalQuery; }; return { /** * Add OR condition to the query */ or: (where, is) => { const newConditions = [...config.conditions, { where, is }]; return QueryBuilder(client, { ...config, conditions: newConditions }); }, /** * Filter by branded ID with type safety */ whereId: (id) => { const newConditions = [ ...config.conditions, { where: { id } } ]; return QueryBuilder(client, { ...config, conditions: newConditions }); }, /** * Add OR condition with branded ID */ orWhereId: (id) => { return QueryBuilder(client, config).or({ id }); }, /** * Apply mapping function to query results */ map: (fn) => { return createMappedQuery(QueryBuilder(client, config), fn); }, /** * Apply filter function to query results */ filter: (predicate) => { return QueryBuilder(client, { ...config, filterFn: config.filterFn ? (item) => config.filterFn(item) && predicate(item) : predicate }); }, /** * Limit the number of results */ limit: (count) => { return QueryBuilder(client, { ...config, limit: count }); }, /** * Offset the results for pagination */ offset: (count) => { return QueryBuilder(client, { ...config, offset: count }); }, /** * Execute query expecting exactly one result */ one: () => { return wrapAsync$1(async () => { try { const query2 = buildSupabaseQuery(); const { data, error } = await query2.single(); if (error) { log.error(`Error getting ${config.table} item: ${String(error)}`); return Err(error); } const result = data; const filteredResult = config.filterFn ? config.filterFn(result) : true; if (!filteredResult) { return Ok(Option.none()); } return Ok(Option(result)); } catch (error) { log.error(`Error executing single query on ${config.table}: ${String(error)}`); return Err(error); } }); }, /** * Execute query expecting zero or more results */ many: () => { return wrapAsync$1(async () => { try { const query2 = buildSupabaseQuery(); const { data, error } = await query2; if (error) { log.error(`Error getting ${config.table} items: ${String(error)}`); return Err(error); } const rawResults = data; const results = config.filterFn ? rawResults.filter(config.filterFn) : rawResults; return Ok(List(results)); } catch (error) { log.error(`Error executing multi query on ${config.table}: ${String(error)}`); return Err(error); } }); }, /** * Execute query expecting first result from potentially multiple */ first: () => { return wrapAsync$1(async () => { const manyResult = await QueryBuilder(client, config).many(); const list = manyResult.getOrThrow(); if (list.isEmpty) { return Ok(Option.none()); } return Ok(Option(list.head)); }); }, /** * Execute query expecting exactly one result, throw if error or not found */ oneOrThrow: async () => { const result = await QueryBuilder(client, config).one(); const option = result.getOrThrow(); return option.getOrThrow(new Error(`No record found in ${config.table}`)); }, /** * Execute query expecting zero or more results, throw if error */ manyOrThrow: async () => { const result = await QueryBuilder(client, config).many(); return result.getOrThrow(); }, /** * Execute query expecting first result, throw if error or empty */ firstOrThrow: async () => { const result = await QueryBuilder(client, config).first(); const option = result.getOrThrow(); return option.getOrThrow(new Error(`No records found in ${config.table}`)); } }; }; const createMappedQuery = (sourceQuery, mapFn) => { return { map: (fn) => { return createMappedQuery(sourceQuery, (item) => fn(mapFn(item))); }, filter: (predicate) => { const filteredQuery = sourceQuery.filter((item) => predicate(mapFn(item))); return createMappedQuery(filteredQuery, mapFn); }, one: () => { return wrapAsync$1(async () => { const maybeItemResult = await sourceQuery.one(); const maybeItem = maybeItemResult.getOrThrow(); return maybeItem.fold( () => Ok(Option.none()), (item) => Ok(Option(mapFn(item))) ); }); }, many: () => { return wrapAsync$1(async () => { const itemsResult = await sourceQuery.many(); const items = itemsResult.getOrThrow(); return Ok(items.map(mapFn)); }); }, first: () => { return wrapAsync$1(async () => { const maybeItemResult = await sourceQuery.first(); const maybeItem = maybeItemResult.getOrThrow(); return maybeItem.fold( () => Ok(Option.none()), (item) => Ok(Option(mapFn(item))) ); }); }, /** * Execute mapped query expecting exactly one result, throw if error or not found */ oneOrThrow: async () => { const result = await createMappedQuery(sourceQuery, mapFn).one(); const option = result.getOrThrow(); return option.getOrThrow(new Error(`No record found`)); }, /** * Execute mapped query expecting zero or more results, throw if error */ manyOrThrow: async () => { const result = await createMappedQuery(sourceQuery, mapFn).many(); return result.getOrThrow(); }, /** * Execute mapped query expecting first result, throw if error or empty */ firstOrThrow: async () => { const result = await createMappedQuery(sourceQuery, mapFn).first(); const option = result.getOrThrow(); return option.getOrThrow(new Error(`No records found`)); } }; }; const createQuery = (client, table, where = {}, is, wherein, order) => { const config = { table, conditions: [{ where, is, wherein }], order }; return QueryBuilder(client, config); }; const isQuery = (obj) => { return typeof obj === "object" && obj !== null && "one" in obj && "many" in obj && "first" in obj && "or" in obj && "map" in obj && "filter" in obj; }; const isMappedQuery = (obj) => { return typeof obj === "object" && obj !== null && "one" in obj && "many" in obj && "first" in obj && "map" in obj && "filter" in obj; }; const wrapAsync = (fn) => { return fn(); }; const getEntity = (client, table, where, is) => wrapAsync(async () => { try { const baseQuery = client.from(table).select("*").match(where); const queryWithIs = is ? List(Object.entries(is)).foldLeft(baseQuery)( (query2, [column, value]) => query2.is(column, value) ) : baseQuery; const { data, error } = await queryWithIs.single(); if (error) { return Err(error); } return Ok(data); } catch (error) { return Err(error); } }); const getEntities = (client, table, where = {}, is, wherein, order = [ "id", { ascending: true } ]) => wrapAsync(async () => { try { const baseQuery = client.from(table).select("*").match(where); const queryWithIn = wherein ? List(Object.entries(wherein)).foldLeft(baseQuery)( (query2, [column, values]) => query2.in(column, values) ) : baseQuery; const queryWithIs = is ? List(Object.entries(is)).foldLeft(queryWithIn)( (query2, [column, value]) => query2.is(column, value) ) : queryWithIn; const queryOrderBy = queryWithIs.order(order[0], order[1]); const { data, error } = await queryOrderBy; if (error) { return Err(error); } return Ok(List(data)); } catch (error) { return Err(error); } }); const addEntities = (client, table, entities) => wrapAsync(async () => { try { const { data, error } = await client.from(table).insert(entities).select(); if (error) { return Err(error); } return Ok(List(data)); } catch (error) { return Err(error); } }); const updateEntity = (client, table, entities, where, is, wherein) => wrapAsync(async () => { try { const baseQuery = client.from(table).update(entities).match(where); const queryWithIn = wherein ? List(Object.entries(wherein)).foldLeft(baseQuery)( (query2, [column, values]) => query2.in(column, values) ) : baseQuery; const queryWithIs = is ? List(Object.entries(is)).foldLeft(queryWithIn)( (query2, [column, value]) => query2.is(column, value) ) : queryWithIn; const { data, error } = await queryWithIs.select().single(); if (error) { return Err(error); } return Ok(data); } catch (error) { return Err(error); } }); const updateEntities = (client, table, entities, identity = "id", where, is, wherein) => wrapAsync(async () => { try { const onConflict = Array.isArray(identity) ? identity.join(",") : identity; const baseQuery = client.from(table).upsert(entities, { onConflict }).match(where ?? {}); const queryWithIn = wherein ? List(Object.entries(wherein)).foldLeft(baseQuery)( (query2, [column, values]) => query2.in(column, values) ) : baseQuery; const queryWithIs = is ? List(Object.entries(is)).foldLeft(queryWithIn)( (query2, [column, value]) => query2.is(column, value) ) : queryWithIn; const { data, error } = await queryWithIs.select(); if (error) { return Err(error); } return Ok(List(data)); } catch (error) { return Err(error); } }); const query = (client, table, where = {}, is, wherein, order) => { return createQuery(client, table, where, is, wherein, order); }; function MultiMutationQuery(promise) { const result = Object.assign(promise, { // Standard MultiExecution interface many: () => promise, manyOrThrow: async () => { const taskResult = await promise; return taskResult.getOrThrow(); }, // Standard ExecutableQuery interface execute: () => promise, executeOrThrow: async () => { const taskResult = await promise; return taskResult.getOrThrow(); } }); return result; } function SingleMutationQuery(promise) { const result = Object.assign(promise, { // Standard SingleExecution interface one: () => promise.then((outcome) => outcome.map((value) => Option(value))), oneOrThrow: async () => { const taskResult = await promise; return taskResult.getOrThrow(); }, // Standard ExecutableQuery interface execute: () => promise.then((outcome) => outcome.map((value) => Option(value))), executeOrThrow: async () => { const taskResult = await promise; const value = taskResult.getOrThrow(); return Option(value); } }); return result; } const Entity = (client, name) => { function getGlobalItems({ where, is, wherein, order } = {}) { return query(client, name, where, is, wherein, order); } function addGlobalItems({ items }) { return MultiMutationQuery(addEntities(client, name, items)); } function getItem({ id, where, is }) { return query(client, name, { ...where, id }, is); } function getItems({ where, is, wherein, order } = {}) { return query(client, name, where, is, wherein, order); } function addItems({ items }) { return MultiMutationQuery(addEntities(client, name, items)); } function updateItem({ id, item, where, is, wherein }) { return SingleMutationQuery( updateEntity(client, name, item, { ...where, id }, is, wherein) ); } function updateItems({ items, identity = "id", where, is, wherein }) { return MultiMutationQuery(updateEntities(client, name, items, identity, where, is, wherein)); } return { getGlobalItems, addGlobalItems, getItem, getItems, addItems, updateItem, updateItems }; }; export { Entity, Err2 as Err, List2 as List, MultiMutationQuery, Ok2 as Ok, Option2 as Option, SingleMutationQuery, addEntities, getEntities, getEntity, isMappedQuery, isQuery, query, updateEntities, updateEntity }; //# sourceMappingURL=index.mjs.map