supabase-typed-query
Version:
Type-safe query builder and entity pattern for Supabase with TypeScript
653 lines (652 loc) • 22.5 kB
JavaScript
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