UNPKG

@storecraft/database-sql-base

Version:

Official SQL Database driver for storecraft

260 lines (228 loc) 6.85 kB
/** * @import { ApiQuery, Cursor } from '@storecraft/core/api' * @import { BOOLQL } from '@storecraft/core/vql/bool-ql' * @import { Database } from '../types.sql.tables.js' * @import { BinaryOperator, ExpressionBuilder } from 'kysely' * @import { * DirectedOrderByStringReference, QueryableTables * } from './utils.types.js' */ import { parse } from "@storecraft/core/vql/bool-ql"; /** * Convert an API Query cursor into mongo dialect, also sanitize. * * 1. (a1, a2) > (b1, b2) ==> (a1 > b1) || (a1=b1 & a2>b2) * 2. (a1, a2) >= (b1, b2) ==> (a1 > b1) || (a1=b1 & a2>=b2) * 3. (a1, a2, a3) > (b1, b2, b3) ==> (a1 > b1) || (a1=b1 & a2>b2) || (a1=b1 & a2=b2 & a3>b3) * 4. (a1, a2, a3) >= (b1, b2, b3) ==> (a1 > b1) || (a1=b1 & a2>b2) || (a1=b1 & a2=b2 & a3>=b3) * * @param {ExpressionBuilder<Database>} eb * @param {Cursor} c * @param {'>' | '>=' | '<' | '<='} relation * @param {(x: [k: string, v: any]) => [k: string, v: any]} transformer * Your chance to change key and value */ export const query_cursor_to_eb = ( eb, c, relation, transformer=(x)=>x ) => { /** @type {BinaryOperator} */ let rel_key_1; // relation in last conjunction term in [0, n-1] disjunctions /** @type {BinaryOperator} */ let rel_key_2; // relation in last conjunction term in last disjunction if (relation==='>' || relation==='>=') { rel_key_1 = rel_key_2 = '>'; if(relation==='>=') rel_key_2='>='; } else if (relation==='<' || relation==='<=') { rel_key_1 = rel_key_2 = '<'; if(relation==='<=') rel_key_2='<='; } else return undefined; const disjunctions = []; // each disjunction clause for (let ix = 0; ix < c.length; ix++) { const is_last_disjunction = ix==c.length-1; const conjunctions = []; // each conjunction clause up until the last term (not inclusive) for (let jx = 0; jx < ix; jx++) { // the a_n=b_n const r = transformer(c[jx]); // conjunctions.push({ [r[0]] : r[1] }); conjunctions.push(eb(r[0], '=', r[1])); } // Last conjunction term const relation_key = is_last_disjunction ? rel_key_2 : rel_key_1; const r = transformer(c[ix]); // conjunctions.push({ [r[0]] : { [relation_key]: r[1] } }); conjunctions.push(eb(r[0], relation_key, r[1])); // Add to disjunctions list // disjunctions.push({ $and: conjunctions }); disjunctions.push(eb.and(conjunctions)); } if(disjunctions.length==0) return undefined; // const result = { // $or: disjunctions // }; return eb.or(disjunctions); // return result; } /** * @template {QueryableTables} T * @param {ExpressionBuilder<Database>} eb * @param {BOOLQL.Node} node * @param {T} table_name */ export const query_vql_node_to_eb = (eb, node, table_name) => { if(node.op==='LEAF') { // console.log('value', node.value) return eb .exists( eb => eb .selectFrom('entity_to_search_terms') .select('id') .where( eb => eb.and( [ eb.or( [ eb( 'entity_to_search_terms.entity_id', '=', eb.ref(`${table_name}.id`) ), eb( `entity_to_search_terms.entity_handle`, '=', eb.ref(`${table_name}.handle`) ), ] ), eb( `entity_to_search_terms.value`, 'like', node.value.toLowerCase() ) ] ) ) ) } let conjunctions = []; for(let arg of node?.args) { conjunctions.push(query_vql_node_to_eb(eb, arg, table_name)); } switch (node.op) { case '&': return eb.and(conjunctions) case '|': return eb.or(conjunctions) case '!': return eb.not(conjunctions[0]) default: throw new Error('VQL-failed') } } /** * * @param {[k: string, v: any]} kv * @param {keyof Database} table_name * @returns {[k: string, v: any]} */ const transform_boolean_to_0_or_1 = (kv, table_name) => { // console.log('transform_boolean_to_0_or_1', kv) kv = [ table_name ? `${table_name}.${kv[0]}` : kv[0], typeof kv[1] === 'boolean' ? (kv[1] ? 1 : 0) : kv[1] ]; return kv; } /** * Convert an API Query into dialect, also sanitize. * * @template {any} [T=any] * * @param {ExpressionBuilder<Database>} eb * @param {ApiQuery<T>} q * @param {QueryableTables} table_name * */ export const query_to_eb = (eb, q={}, table_name) => { const clauses = []; const sort_sign = q.order === 'asc' ? 1 : -1; const asc = sort_sign==1; const transformer = (x) => transform_boolean_to_0_or_1(x, table_name); // compute index clauses // if(q.startAt) { // clauses.push( // query_cursor_to_eb( // eb, q.startAt, asc ? '>=' : '<=', transformer // ) // ); // } else if(q.startAfter) { // clauses.push( // query_cursor_to_eb( // eb, q.startAfter, asc ? '>' : '<', transformer // ) // ); // } // if(q.endAt) { // clauses.push( // query_cursor_to_eb( // eb, q.endAt, asc ? '<=' : '>=', transformer // ) // ); // } else if(q.endBefore) { // clauses.push( // query_cursor_to_eb( // eb, q.endBefore, asc ? '<' : '>', transformer // ) // ); // } // compute VQL clauses try { if(q.vql && !q.vqlParsed) { q.vqlParsed = parse(q.vql) } } catch(e) { console.error('VQL parse error', e); } if(q.vqlParsed) { const vql_clause = query_vql_node_to_eb( eb, q.vqlParsed, table_name ); vql_clause && clauses.push(vql_clause); } return eb.and(clauses); } const SIGN = { '1': 'asc', '-1': 'desc' } /** * Convert an API Query into mongo dialect, also sanitize. * @template {Record<string, any>} [Type=Record<string, any>] * @template {keyof Database} [Table=keyof Database] * * @param {ApiQuery<Type>} q * @param {Table} table * @returns {DirectedOrderByStringReference<Database, Table, Database[Table]>[]} */ export const query_to_sort = (q={}, table) => { // const sort_sign = q.order === 'asc' ? 'asc' : 'desc'; // `reverse_sign=-1` means we need to reverse because of `limitToLast` const reverse_sign = (q.limitToLast && !q.limit) ? -1 : 1; const asc = q.order === 'asc'; const sort_sign = (asc ? 1 : -1) * reverse_sign; // compute sort fields and order const keys = q.sortBy?.length ? q.sortBy : ['updated_at', 'id']; const sort = keys.map( s => table ? `${table}.${s} ${SIGN[sort_sign]}` : `${s} ${SIGN[sort_sign]}` ) // it's too complicated to map each ket to table column. // kysely was designed to do this in place return ( /** @type {DirectedOrderByStringReference<Database, Table, Database[Table]>[]} */ ( sort ) ); }