@storecraft/database-sql-base 1.0.22 → 1.0.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,231 +1,269 @@
1
1
  /**
2
- * @import { ApiQuery, Cursor } from '@storecraft/core/api'
3
- * @import { VQL } from '@storecraft/core/vql'
2
+ * @import { ApiQuery } from '@storecraft/core/api'
3
+ * @import { VQL, VQL_OPS } from '@storecraft/core/vql'
4
4
  * @import { Database } from '../types.sql.tables.js'
5
- * @import { BinaryOperator, ExpressionBuilder } from 'kysely'
5
+ * @import { ExpressionBuilder, SelectQueryBuilder } from 'kysely'
6
+ * @import { legal_value_types } from "@storecraft/core/vql";
6
7
  */
7
-
8
- import { parse } from "@storecraft/core/vql";
8
+ import { legacy_query_with_cursors_to_vql_string } from "@storecraft/core/api/query.legacy.js";
9
+ import { parse, utils } from "@storecraft/core/vql";
9
10
 
10
11
  /**
11
- * Convert an API Query cursor into mongo dialect, also sanitize.
12
- *
13
- * 1. (a1, a2) > (b1, b2) ==> (a1 > b1) || (a1=b1 & a2>b2)
14
- * 2. (a1, a2) >= (b1, b2) ==> (a1 > b1) || (a1=b1 & a2>=b2)
15
- * 3. (a1, a2, a3) > (b1, b2, b3) ==> (a1 > b1) || (a1=b1 & a2>b2) || (a1=b1 & a2=b2 & a3>b3)
16
- * 4. (a1, a2, a3) >= (b1, b2, b3) ==> (a1 > b1) || (a1=b1 & a2>b2) || (a1=b1 & a2=b2 & a3>=b3)
17
- *
18
- * @param {ExpressionBuilder<Database>} eb
19
- * @param {Cursor} c
20
- * @param {'>' | '>=' | '<' | '<='} relation
21
- * @param {(x: [k: string, v: any]) => [k: string, v: any]} transformer Your chance to change key and value
12
+ * @template {keyof Database} [Table=(keyof Database)]
13
+ * @param {ExpressionBuilder<Database, Table>} eb
14
+ * @param {VQL} vql
15
+ * @param {Table} table_name
22
16
  */
23
- export const query_cursor_to_eb = (eb, c, relation, transformer=(x)=>x) => {
24
-
25
- /** @type {BinaryOperator} */
26
- let rel_key_1; // relation in last conjunction term in [0, n-1] disjunctions
27
- /** @type {BinaryOperator} */
28
- let rel_key_2; // relation in last conjunction term in last disjunction
17
+ export const query_vql_root_to_eb = (eb, vql, table_name) => {
29
18
 
30
- if (relation==='>' || relation==='>=') {
31
- rel_key_1 = rel_key_2 = '>';
32
- if(relation==='>=')
33
- rel_key_2='>=';
19
+ /** @template T @param {T} fn @returns {T} */
20
+ const identity = (fn) => {
21
+ return fn;
34
22
  }
35
- else if (relation==='<' || relation==='<=') {
36
- rel_key_1 = rel_key_2 = '<';
37
- if(relation==='<=')
38
- rel_key_2='<=';
39
- } else return undefined;
40
23
 
41
-
42
- const disjunctions = [];
43
- // each disjunction clause
44
- for (let ix = 0; ix < c.length; ix++) {
45
- const is_last_disjunction = ix==c.length-1;
46
- const conjunctions = [];
47
- // each conjunction clause up until the last term (not inclusive)
48
- for (let jx = 0; jx < ix; jx++) {
49
- // the a_n=b_n
50
- const r = transformer(c[jx]);
51
-
52
- // conjunctions.push({ [r[0]] : r[1] });
53
- conjunctions.push(eb(r[0], '=', r[1]));
54
- }
24
+ /**
25
+ * @param {VQL_OPS<legal_value_types>} ops `ops` object about this property
26
+ * @param {(keyof (Database[keyof Database]))} name
27
+ * property name in the table
28
+ */
29
+ const leaf_ops = (ops, name) => {
30
+
31
+ const ops_keys = /** @type {(keyof VQL_OPS)[]} */(
32
+ Object.keys(ops)
33
+ );
34
+ const values = ops_keys.map(
35
+ (k) => {
36
+ /** `arg` is basically `op.$eq` `op.$gte` etc.. */
37
+ const arg = boolean_to_0_or_1(ops[k]);
38
+ const arg_any = /** @type {any} */(arg);
39
+ let result;
40
+
41
+ const prop_ref = eb.ref(`${table_name}.${name}`)
42
+ switch (k) {
43
+ case '$eq':
44
+ result = eb(prop_ref, '=', arg_any);
45
+ break;
46
+ case '$ne':
47
+ result = eb(prop_ref, '!=', arg_any);
48
+ break;
49
+ case '$gt':
50
+ result = eb(prop_ref, '>', arg_any);
51
+ break;
52
+ case '$gte':
53
+ result = eb(prop_ref, '>=', arg_any);
54
+ // console.log('$gte ', {value, arg, result, name})
55
+ break;
56
+ case '$lt':
57
+ result = eb(prop_ref, '<', arg_any);
58
+ break;
59
+ case '$lte':
60
+ result = eb(prop_ref, '<=', arg_any);
61
+ break;
62
+ case '$like':
63
+ // @ts-ignore
64
+ result = eb(prop_ref, 'like', `%${String(arg_any)}%`);
65
+ break;
66
+ case '$in': {
67
+ result = eb(prop_ref, 'in', arg_any)
68
+ break;
69
+ }
70
+ case '$nin': {
71
+ result = eb(prop_ref, 'not in', arg_any)
72
+ break;
73
+ }
74
+ default:
75
+ if(result===undefined)
76
+ throw new Error('VQL-ops-failed');
77
+ }
78
+
79
+ // debug
80
+ // console.log(
81
+ // 'test_ops',
82
+ // {k, arg, result}
83
+ // );
55
84
 
56
- // Last conjunction term
57
- const relation_key = is_last_disjunction ? rel_key_2 : rel_key_1;
58
- const r = transformer(c[ix]);
59
- // conjunctions.push({ [r[0]] : { [relation_key]: r[1] } });
60
- conjunctions.push(eb(r[0], relation_key, r[1]));
61
- // Add to disjunctions list
62
- // disjunctions.push({ $and: conjunctions });
63
- disjunctions.push(eb.and(conjunctions));
85
+ return result;
86
+ }
87
+ );
88
+
89
+ return eb.and(values);
64
90
  }
65
91
 
66
- if(disjunctions.length==0)
67
- return undefined;
92
+ const reduced = utils.reduce_vql(
93
+ {
94
+ vql,
68
95
 
69
- // const result = {
70
- // $or: disjunctions
71
- // };
96
+ map_leaf: (node) => {
97
+ return leaf_ops(
98
+ node.op,
99
+ /** @type {any} */(node.name)
100
+ );
101
+ },
72
102
 
73
- return eb.or(disjunctions);
103
+ reduce_AND: identity(
104
+ (nodes) => {
105
+ return eb.and(nodes);
106
+ }
107
+ ),
74
108
 
75
- // return result;
76
- }
109
+ reduce_OR: (nodes) => {
110
+ return eb.or(nodes);
111
+ },
77
112
 
78
- /**
79
- * @template {QueryableTables} T
80
- * @param {ExpressionBuilder<Database>} eb
81
- * @param {VQL.Node} node
82
- * @param {T} table_name
83
- */
84
- export const query_vql_node_to_eb = (eb, node, table_name) => {
85
- if(node.op==='LEAF') {
86
- // console.log('value', node.value)
87
- return eb
88
- .exists(
89
- eb => eb
90
- .selectFrom('entity_to_search_terms')
91
- .select('id')
92
- .where(
93
- eb => eb.and(
94
- [
95
- eb.or(
113
+ reduce_NOT: (node) => {
114
+ return eb.not(node);
115
+ },
116
+
117
+ reduce_SEARCH: (value) => {
118
+ return eb
119
+ .exists(
120
+ eb => eb
121
+ .selectFrom('entity_to_search_terms')
122
+ .select('id')
123
+ .where(
124
+ eb => eb.and(
96
125
  [
97
- eb(`entity_to_search_terms.entity_id`, '=', eb.ref(`${table_name}.id`)),
98
- eb(`entity_to_search_terms.entity_handle`, '=', eb.ref(`${table_name}.handle`)),
126
+ eb.or(
127
+ [
128
+ eb(
129
+ 'entity_to_search_terms.entity_id', '=',
130
+ eb.ref(`${table_name}.id`)
131
+ ),
132
+ eb(
133
+ `entity_to_search_terms.entity_handle`, '=',
134
+ eb.ref(`${table_name}.handle`)
135
+ ),
136
+ ]
137
+ ),
138
+ eb(
139
+ `entity_to_search_terms.value`, 'like',
140
+ `%${String(value.toLowerCase())}%`
141
+ )
99
142
  ]
100
- ),
101
- eb(`entity_to_search_terms.value`, 'like', node.value.toLowerCase())
102
- ]
143
+ )
144
+ )
103
145
  )
104
- )
105
- )
106
- }
107
-
108
- let conjunctions = [];
109
- for(let arg of node?.args) {
110
- conjunctions.push(query_vql_node_to_eb(eb, arg, table_name));
111
- }
112
-
113
- switch (node.op) {
114
- case '&':
115
- return eb.and(conjunctions)
116
- case '|':
117
- return eb.or(conjunctions)
118
- case '!':
119
- return eb.not(conjunctions[0])
120
- default:
121
- throw new Error('VQL-failed')
122
- }
146
+ },
123
147
 
124
- }
148
+ }
149
+ );
125
150
 
126
- /**
127
- * @param {ExpressionBuilder<Database>} eb
128
- * @param {VQL.Node} root
129
- * @param {QueryableTables} table_name
130
- */
131
- export const query_vql_to_eb = (eb, root, table_name) => {
132
- return root ? query_vql_node_to_eb(eb, root, table_name) : undefined;
151
+ return reduced;
133
152
  }
134
153
 
135
-
136
154
  /**
137
- *
138
- * @param {[k: string, v: any]} kv
139
- * @param {keyof Database} table_name
140
- * @returns {[k: string, v: any]}
155
+ * @description Transform booleans to 0 or 1. We write `boolean`
156
+ * types as `0` or `1` in SQL to be uniform with **SQLite**.
157
+ * @param {legal_value_types | legal_value_types[]} value
158
+ * @returns {legal_value_types | legal_value_types[]}
141
159
  */
142
- const transform_boolean_to_0_or_1 = (kv, table_name) => {
143
-
144
- // console.log('transform_boolean_to_0_or_1', kv)
145
- kv = [
146
- table_name ? `${table_name}.${kv[0]}` : kv[0],
147
- typeof kv[1] === 'boolean' ? (kv[1] ? 1 : 0) : kv[1]
148
- ];
149
-
150
- return kv;
160
+ const boolean_to_0_or_1 = (value) => {
161
+ return (typeof value==='boolean') ?
162
+ (value ? 1 : 0) :
163
+ value;
151
164
  }
152
165
 
153
166
  /**
154
- * Convert an API Query into dialect, also sanitize.
155
- *
156
- * @template {any} [T=any]
157
- *
158
- * @param {ExpressionBuilder<Database>} eb
159
- * @param {ApiQuery<T>} q
160
- * @param {QueryableTables} table_name
161
- *
167
+ * @description Convert an {@link ApiQuery} into **SQL** Clause.
168
+ * @template {keyof Database} [Table=(keyof Database)]
169
+ * @template {any} [G=any]
170
+ * @param {ExpressionBuilder<Database, Table>} eb
171
+ * @param {ApiQuery<G>} q
172
+ * @param {Table} table_name
162
173
  */
163
174
  export const query_to_eb = (eb, q={}, table_name) => {
164
175
  const clauses = [];
165
176
 
166
- const sort_sign = q.order === 'asc' ? 1 : -1;
167
- const asc = sort_sign==1;
168
- const transformer = (x) => transform_boolean_to_0_or_1(x, table_name);
169
-
170
- // compute index clauses
171
- if(q.startAt) {
172
- clauses.push(query_cursor_to_eb(eb, q.startAt, asc ? '>=' : '<=', transformer));
173
- } else if(q.startAfter) {
174
- clauses.push(query_cursor_to_eb(eb, q.startAfter, asc ? '>' : '<', transformer));
177
+ try { // compute VQL clauses
178
+ q.vql = /** @type {VQL<G>} */({
179
+ $and: [
180
+ parse(q.vql),
181
+ // supports legacy queries with cursors, will be deprecated
182
+ // in future versions.
183
+ parse(
184
+ legacy_query_with_cursors_to_vql_string(q)
185
+ )
186
+ ].filter(Boolean)
187
+ });
188
+ } catch(e) {
189
+ console.error('VQL parse error:\n', e, '\nfor query:\n', q);
175
190
  }
176
191
 
177
- if(q.endAt) {
178
- clauses.push(query_cursor_to_eb(eb, q.endAt, asc ? '<=' : '>=', transformer));
179
- } else if(q.endBefore) {
180
- clauses.push(query_cursor_to_eb(eb, q.endBefore, asc ? '<' : '>', transformer));
192
+ if(q.vql) {
193
+ const vql_clause = query_vql_root_to_eb(
194
+ eb, /** @type {VQL} */(q.vql), table_name
195
+ );
196
+ vql_clause &&
197
+ clauses.push(vql_clause);
181
198
  }
182
199
 
183
- // compute VQL clauses
184
- try {
185
- if(q.vql && !q.vqlParsed) {
186
- q.vqlParsed = parse(q.vql)
187
- }
188
- } catch(e) {}
189
-
190
- const vql_clause = query_vql_to_eb(eb, q.vqlParsed, table_name)
191
- vql_clause && clauses.push(vql_clause);
192
-
193
200
  return eb.and(clauses);
194
201
  }
195
202
 
196
- const SIGN = {
203
+ const SIGN = /** @type {const} */({
197
204
  '1': 'asc',
198
205
  '-1': 'desc'
199
- }
206
+ });
200
207
 
201
- // export type DirectedOrderByStringReference<DB, TB extends keyof DB, O> = `${StringReference<DB, TB> | (keyof O & string)} ${OrderByDirection}`;
202
208
 
203
209
  /**
204
- * @import {DirectedOrderByStringReference, QueryableTables} from './utils.types.js'
205
- */
206
- // OE extends OrderByExpression<DB, TB, O>
207
- /**
208
- * Convert an API Query into mongo dialect, also sanitize.
209
- * @template {Record<string, any>} [Type=Record<string, any>]
210
- * @template {keyof Database} [Table=keyof Database]
211
- *
212
- * @param {ApiQuery<Type>} q
210
+ * @description Convert an API Query into sort clause.
211
+ * @template O
212
+ * @template {keyof Database} [Table=(keyof Database)]
213
+ * @param {SelectQueryBuilder<Database, Table, O>} qb
214
+ * @param {ApiQuery<any>} q
213
215
  * @param {Table} table
214
- * @returns {DirectedOrderByStringReference<Database, Table, Database[Table]>[]}
216
+ * @returns {SelectQueryBuilder<Database, Table, O>}
215
217
  */
216
- export const query_to_sort = (q={}, table) => {
217
- // const sort_sign = q.order === 'asc' ? 'asc' : 'desc';
218
+ export const withSort = (qb, q={}, table) => {
218
219
  // `reverse_sign=-1` means we need to reverse because of `limitToLast`
219
- const reverse_sign = (q.limitToLast && !q.limit) ? -1 : 1;
220
+ const reverse_sign = q.limitToLast ? -1 : 1;
220
221
  const asc = q.order === 'asc';
221
222
  const sort_sign = (asc ? 1 : -1) * reverse_sign;
222
223
 
223
224
  // compute sort fields and order
224
- const keys = q.sortBy?.length ? q.sortBy : ['updated_at', 'id'];
225
- const sort = keys.map(
226
- s => table ? `${table}.${s} ${SIGN[sort_sign]}` : `${s} ${SIGN[sort_sign]}`
227
- )
228
- // it's too complicated to map each ket to table column.
229
- // kysely was designed to do this in place
230
- return (/** @type {DirectedOrderByStringReference<Database, Table, Database[Table]>[]} */ (sort));
225
+ const props = /** @type {(keyof (Database[Table]))[]} */(
226
+ q.sortBy?.length ?
227
+ q.sortBy :
228
+ ['updated_at', 'id']
229
+ );
230
+
231
+ let next = qb;
232
+
233
+ // we do it iteratively because `kysely` deprecated
234
+ // array of `orderBy` in favor of chaining
235
+ for(const prop of props) {
236
+ // console.log('add_sort_to_eb', {s, table});
237
+ next = next.orderBy(
238
+ `${table}.${prop}`,
239
+ SIGN[sort_sign]
240
+ );
241
+ }
242
+
243
+ return next;
244
+ }
245
+
246
+
247
+ /**
248
+ * @description Apply a {@link ApiQuery} into {@link SelectQueryBuilder},
249
+ * with `sorting`, `filtering` and `limit`.
250
+ * @template O
251
+ * @template {keyof Database} [Table=(keyof Database)]
252
+ * @param {SelectQueryBuilder<Database, Table, O>} qb
253
+ * @param {ApiQuery<any>} query
254
+ * @param {Table} table
255
+ * @returns {SelectQueryBuilder<Database, Table, O>}
256
+ */
257
+ export const withQuery = (qb, query={}, table) => {
258
+ return withSort(
259
+ qb.where(
260
+ (eb) => {
261
+ return query_to_eb(
262
+ eb, query, table
263
+ );
264
+ }
265
+ )
266
+ .limit(query.limitToLast ?? query.limit ?? 10),
267
+ query, table
268
+ );
231
269
  }
package/tests/sandbox.js CHANGED
@@ -12,7 +12,6 @@ import { SqliteDialect } from 'kysely';
12
12
  import { homedir } from 'node:os';
13
13
  import { join } from 'node:path';
14
14
  import { jsonArrayFrom, stringArrayFrom } from '../src/con.helpers.json.js';
15
- import { query_to_sort } from '../src/utils.query.js';
16
15
  import { products_with_collections, products_with_discounts, products_with_related_products, products_with_variants, with_media, with_tags } from '../src/con.shared.js';
17
16
 
18
17
  export const sqlite_dialect = new SqliteDialect({
@@ -81,7 +80,7 @@ async function test() {
81
80
  ]
82
81
  )
83
82
  .where('active', '=', 1)
84
- .orderBy(['updated_at asc'])
83
+ .orderBy('updated_at', 'asc')
85
84
  .limit(limit),
86
85
  app.db.dialectType
87
86
  ).as('collections'),
@@ -101,7 +100,7 @@ async function test() {
101
100
  ]
102
101
  )
103
102
  .where('active', '=', 1)
104
- .orderBy(['updated_at asc'])
103
+ .orderBy('updated_at', 'asc')
105
104
  .limit(limit),
106
105
  app.db.dialectType
107
106
  ).as('products'),
@@ -117,7 +116,7 @@ async function test() {
117
116
  ]
118
117
  )
119
118
  .where('active', '=', 1)
120
- .orderBy(['updated_at asc'])
119
+ .orderBy('updated_at', 'asc')
121
120
  .limit(limit),
122
121
  app.db.dialectType
123
122
  ).as('discounts'),
@@ -133,7 +132,7 @@ async function test() {
133
132
  ]
134
133
  )
135
134
  .where('active', '=', 1)
136
- .orderBy(['updated_at asc'])
135
+ .orderBy('updated_at', 'asc')
137
136
  .limit(limit),
138
137
  app.db.dialectType
139
138
  ).as('shipping_methods'),
@@ -149,7 +148,7 @@ async function test() {
149
148
  ]
150
149
  )
151
150
  .where('active', '=', 1)
152
- .orderBy(['updated_at asc'])
151
+ .orderBy('updated_at', 'asc')
153
152
  .limit(limit),
154
153
  app.db.dialectType
155
154
  ).as('posts'),