@storecraft/database-sql-base 1.0.23 → 1.2.5

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,261 +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'
6
- * @import {DirectedOrderByStringReference, QueryableTables} from './utils.types.js'
5
+ * @import { ExpressionBuilder, SelectQueryBuilder } from 'kysely'
6
+ * @import { legal_value_types } from "@storecraft/core/vql";
7
7
  */
8
-
9
- 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";
10
10
 
11
11
  /**
12
- * Convert an API Query cursor into mongo dialect, also sanitize.
13
- *
14
- * 1. (a1, a2) > (b1, b2) ==> (a1 > b1) || (a1=b1 & a2>b2)
15
- * 2. (a1, a2) >= (b1, b2) ==> (a1 > b1) || (a1=b1 & a2>=b2)
16
- * 3. (a1, a2, a3) > (b1, b2, b3) ==> (a1 > b1) || (a1=b1 & a2>b2) || (a1=b1 & a2=b2 & a3>b3)
17
- * 4. (a1, a2, a3) >= (b1, b2, b3) ==> (a1 > b1) || (a1=b1 & a2>b2) || (a1=b1 & a2=b2 & a3>=b3)
18
- *
19
- * @param {ExpressionBuilder<Database>} eb
20
- * @param {Cursor} c
21
- * @param {'>' | '>=' | '<' | '<='} relation
22
- * @param {(x: [k: string, v: any]) => [k: string, v: any]} transformer
23
- * 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
24
16
  */
25
- export const query_cursor_to_eb = (eb, c, relation, transformer=(x)=>x) => {
26
-
27
- /** @type {BinaryOperator} */
28
- let rel_key_1; // relation in last conjunction term in [0, n-1] disjunctions
29
- /** @type {BinaryOperator} */
30
- let rel_key_2; // relation in last conjunction term in last disjunction
17
+ export const query_vql_root_to_eb = (eb, vql, table_name) => {
31
18
 
32
- if (relation==='>' || relation==='>=') {
33
- rel_key_1 = rel_key_2 = '>';
34
- if(relation==='>=')
35
- rel_key_2='>=';
19
+ /** @template T @param {T} fn @returns {T} */
20
+ const identity = (fn) => {
21
+ return fn;
36
22
  }
37
- else if (relation==='<' || relation==='<=') {
38
- rel_key_1 = rel_key_2 = '<';
39
- if(relation==='<=')
40
- rel_key_2='<=';
41
- } else return undefined;
42
23
 
43
-
44
- const disjunctions = [];
45
- // each disjunction clause
46
- for (let ix = 0; ix < c.length; ix++) {
47
- const is_last_disjunction = ix==c.length-1;
48
- const conjunctions = [];
49
- // each conjunction clause up until the last term (not inclusive)
50
- for (let jx = 0; jx < ix; jx++) {
51
- // the a_n=b_n
52
- const r = transformer(c[jx]);
53
-
54
- // conjunctions.push({ [r[0]] : r[1] });
55
- conjunctions.push(eb(r[0], '=', r[1]));
56
- }
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
+ // );
57
84
 
58
- // Last conjunction term
59
- const relation_key = is_last_disjunction ? rel_key_2 : rel_key_1;
60
- const r = transformer(c[ix]);
61
- // conjunctions.push({ [r[0]] : { [relation_key]: r[1] } });
62
- conjunctions.push(eb(r[0], relation_key, r[1]));
63
- // Add to disjunctions list
64
- // disjunctions.push({ $and: conjunctions });
65
- disjunctions.push(eb.and(conjunctions));
85
+ return result;
86
+ }
87
+ );
88
+
89
+ return eb.and(values);
66
90
  }
67
91
 
68
- if(disjunctions.length==0)
69
- return undefined;
92
+ const reduced = utils.reduce_vql(
93
+ {
94
+ vql,
70
95
 
71
- // const result = {
72
- // $or: disjunctions
73
- // };
96
+ map_leaf: (node) => {
97
+ return leaf_ops(
98
+ node.op,
99
+ /** @type {any} */(node.name)
100
+ );
101
+ },
74
102
 
75
- return eb.or(disjunctions);
103
+ reduce_AND: identity(
104
+ (nodes) => {
105
+ return eb.and(nodes);
106
+ }
107
+ ),
76
108
 
77
- // return result;
78
- }
109
+ reduce_OR: (nodes) => {
110
+ return eb.or(nodes);
111
+ },
79
112
 
80
- /**
81
- * @template {QueryableTables} T
82
- * @param {ExpressionBuilder<Database>} eb
83
- * @param {VQL.Node} node
84
- * @param {T} table_name
85
- */
86
- export const query_vql_node_to_eb = (eb, node, table_name) => {
87
- if(node.op==='LEAF') {
88
- // console.log('value', node.value)
89
- return eb
90
- .exists(
91
- eb => eb
92
- .selectFrom('entity_to_search_terms')
93
- .select('id')
94
- .where(
95
- eb => eb.and(
96
- [
97
- 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(
98
125
  [
99
- eb(
100
- `entity_to_search_terms.entity_id`, '=',
101
- eb.ref(`${table_name}.id`)
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
+ ]
102
137
  ),
103
138
  eb(
104
- `entity_to_search_terms.entity_handle`, '=',
105
- eb.ref(`${table_name}.handle`)
106
- ),
139
+ `entity_to_search_terms.value`, 'like',
140
+ `%${String(value.toLowerCase())}%`
141
+ )
107
142
  ]
108
- ),
109
- eb(
110
- `entity_to_search_terms.value`, 'like',
111
- node.value.toLowerCase()
112
143
  )
113
- ]
144
+ )
114
145
  )
115
- )
116
- )
117
- }
118
-
119
- let conjunctions = [];
120
- for(let arg of node?.args) {
121
- conjunctions.push(query_vql_node_to_eb(eb, arg, table_name));
122
- }
123
-
124
- switch (node.op) {
125
- case '&':
126
- return eb.and(conjunctions)
127
- case '|':
128
- return eb.or(conjunctions)
129
- case '!':
130
- return eb.not(conjunctions[0])
131
- default:
132
- throw new Error('VQL-failed')
133
- }
146
+ },
134
147
 
135
- }
148
+ }
149
+ );
136
150
 
137
- /**
138
- * @param {ExpressionBuilder<Database>} eb
139
- * @param {VQL.Node} root
140
- * @param {QueryableTables} table_name
141
- */
142
- export const query_vql_to_eb = (eb, root, table_name) => {
143
- return root ?
144
- query_vql_node_to_eb(eb, root, table_name) :
145
- undefined;
151
+ return reduced;
146
152
  }
147
153
 
148
-
149
154
  /**
150
- *
151
- * @param {[k: string, v: any]} kv
152
- * @param {keyof Database} table_name
153
- * @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[]}
154
159
  */
155
- const transform_boolean_to_0_or_1 = (kv, table_name) => {
156
-
157
- // console.log('transform_boolean_to_0_or_1', kv)
158
- kv = [
159
- table_name ? `${table_name}.${kv[0]}` : kv[0],
160
- typeof kv[1] === 'boolean' ? (kv[1] ? 1 : 0) : kv[1]
161
- ];
162
-
163
- return kv;
160
+ const boolean_to_0_or_1 = (value) => {
161
+ return (typeof value==='boolean') ?
162
+ (value ? 1 : 0) :
163
+ value;
164
164
  }
165
165
 
166
166
  /**
167
- * Convert an API Query into dialect, also sanitize.
168
- *
169
- * @template {any} [T=any]
170
- *
171
- * @param {ExpressionBuilder<Database>} eb
172
- * @param {ApiQuery<T>} q
173
- * @param {QueryableTables} table_name
174
- *
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
175
173
  */
176
174
  export const query_to_eb = (eb, q={}, table_name) => {
177
175
  const clauses = [];
178
176
 
179
- const sort_sign = q.order === 'asc' ? 1 : -1;
180
- const asc = sort_sign==1;
181
- const transformer = (x) => transform_boolean_to_0_or_1(x, table_name);
182
-
183
- // compute index clauses
184
- if(q.startAt) {
185
- clauses.push(
186
- query_cursor_to_eb(
187
- eb, q.startAt, asc ? '>=' : '<=', transformer
188
- )
189
- );
190
- } else if(q.startAfter) {
191
- clauses.push(
192
- query_cursor_to_eb(
193
- eb, q.startAfter, asc ? '>' : '<', transformer
194
- )
195
- );
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);
196
190
  }
197
191
 
198
- if(q.endAt) {
199
- clauses.push(
200
- query_cursor_to_eb(
201
- eb, q.endAt, asc ? '<=' : '>=', transformer
202
- )
203
- );
204
- } else if(q.endBefore) {
205
- clauses.push(
206
- query_cursor_to_eb(
207
- eb, q.endBefore, asc ? '<' : '>', transformer
208
- )
192
+ if(q.vql) {
193
+ const vql_clause = query_vql_root_to_eb(
194
+ eb, /** @type {VQL} */(q.vql), table_name
209
195
  );
196
+ vql_clause &&
197
+ clauses.push(vql_clause);
210
198
  }
211
199
 
212
- // compute VQL clauses
213
- try {
214
- if(q.vql && !q.vqlParsed) {
215
- q.vqlParsed = parse(q.vql)
216
- }
217
- } catch(e) {}
218
-
219
- const vql_clause = query_vql_to_eb(
220
- eb, q.vqlParsed, table_name
221
- );
222
-
223
- vql_clause && clauses.push(vql_clause);
224
-
225
200
  return eb.and(clauses);
226
201
  }
227
202
 
228
- const SIGN = {
203
+ const SIGN = /** @type {const} */({
229
204
  '1': 'asc',
230
205
  '-1': 'desc'
231
- }
206
+ });
207
+
232
208
 
233
209
  /**
234
- * Convert an API Query into mongo dialect, also sanitize.
235
- * @template {Record<string, any>} [Type=Record<string, any>]
236
- * @template {keyof Database} [Table=keyof Database]
237
- *
238
- * @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
239
215
  * @param {Table} table
240
- * @returns {DirectedOrderByStringReference<Database, Table, Database[Table]>[]}
216
+ * @returns {SelectQueryBuilder<Database, Table, O>}
241
217
  */
242
- export const query_to_sort = (q={}, table) => {
243
- // const sort_sign = q.order === 'asc' ? 'asc' : 'desc';
218
+ export const withSort = (qb, q={}, table) => {
244
219
  // `reverse_sign=-1` means we need to reverse because of `limitToLast`
245
- const reverse_sign = (q.limitToLast && !q.limit) ? -1 : 1;
220
+ const reverse_sign = q.limitToLast ? -1 : 1;
246
221
  const asc = q.order === 'asc';
247
222
  const sort_sign = (asc ? 1 : -1) * reverse_sign;
248
223
 
249
224
  // compute sort fields and order
250
- const keys = q.sortBy?.length ? q.sortBy : ['updated_at', 'id'];
251
- const sort = keys.map(
252
- s => table ? `${table}.${s} ${SIGN[sort_sign]}` : `${s} ${SIGN[sort_sign]}`
253
- )
254
- // it's too complicated to map each ket to table column.
255
- // kysely was designed to do this in place
256
- return (
257
- /** @type {DirectedOrderByStringReference<Database, Table, Database[Table]>[]} */ (
258
- 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
+ }
259
265
  )
266
+ .limit(query.limitToLast ?? query.limit ?? 10),
267
+ query, table
260
268
  );
261
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'),
@@ -194,7 +193,7 @@ async function test2() {
194
193
  eb
195
194
  .selectFrom(table_name)
196
195
  .select(props)
197
- .orderBy(['updated_at asc'])
196
+ .orderBy('updated_at', 'asc')
198
197
  .limit(0),
199
198
  app.db.dialectType
200
199
  ).as(table_name)
@@ -19,6 +19,7 @@ import {
19
19
  export interface Database {
20
20
  auth_users: AuthUserTypeTable,
21
21
  tags: TagsTable
22
+ chats: ChatsTable
22
23
  collections: CollectionsTable,
23
24
  shipping_methods: ShippingMethodsTable;
24
25
  posts: PostsTable;
@@ -184,6 +185,12 @@ export interface TagsTable extends Base {
184
185
  values: JSONColumnType<string[]>;
185
186
  }
186
187
 
188
+ export interface ChatsTable extends Base {
189
+ customer_id: string;
190
+ customer_email: string;
191
+ extra: JSONColumnType<object>;
192
+ }
193
+
187
194
  export interface CollectionsTable extends Base {
188
195
  title: string;
189
196
  published: string | undefined;