@storecraft/database-sql-base 1.0.23 → 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.
- package/package.json +1 -1
- package/src/con.auth_users.js +17 -13
- package/src/con.collections.js +24 -16
- package/src/con.customers.js +24 -17
- package/src/con.discounts.js +30 -19
- package/src/con.discounts.utils.js +8 -4
- package/src/con.images.js +18 -13
- package/src/con.notifications.js +22 -15
- package/src/con.orders.js +25 -18
- package/src/con.posts.js +28 -28
- package/src/con.products.js +52 -34
- package/src/con.search.js +12 -12
- package/src/con.shared.js +17 -35
- package/src/con.shipping.js +23 -16
- package/src/con.storefronts.js +23 -17
- package/src/con.tags.js +22 -14
- package/src/con.templates.js +21 -24
- package/src/utils.query.OLD.js +259 -0
- package/src/utils.query.js +212 -204
- package/tests/sandbox.js +5 -6
package/src/utils.query.js
CHANGED
@@ -1,261 +1,269 @@
|
|
1
1
|
/**
|
2
|
-
* @import { ApiQuery
|
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 {
|
6
|
-
* @import {
|
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
|
-
*
|
13
|
-
*
|
14
|
-
*
|
15
|
-
*
|
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
|
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
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
69
|
-
|
92
|
+
const reduced = utils.reduce_vql(
|
93
|
+
{
|
94
|
+
vql,
|
70
95
|
|
71
|
-
|
72
|
-
|
73
|
-
|
96
|
+
map_leaf: (node) => {
|
97
|
+
return leaf_ops(
|
98
|
+
node.op,
|
99
|
+
/** @type {any} */(node.name)
|
100
|
+
);
|
101
|
+
},
|
74
102
|
|
75
|
-
|
103
|
+
reduce_AND: identity(
|
104
|
+
(nodes) => {
|
105
|
+
return eb.and(nodes);
|
106
|
+
}
|
107
|
+
),
|
76
108
|
|
77
|
-
|
78
|
-
|
109
|
+
reduce_OR: (nodes) => {
|
110
|
+
return eb.or(nodes);
|
111
|
+
},
|
79
112
|
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
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
|
-
|
101
|
-
|
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.
|
105
|
-
|
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
|
-
*
|
152
|
-
* @param {
|
153
|
-
* @returns {
|
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
|
156
|
-
|
157
|
-
|
158
|
-
|
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
|
168
|
-
*
|
169
|
-
* @template {any} [
|
170
|
-
*
|
171
|
-
* @param {
|
172
|
-
* @param {
|
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
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
)
|
189
|
-
);
|
190
|
-
}
|
191
|
-
|
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.
|
199
|
-
|
200
|
-
|
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
|
235
|
-
* @template
|
236
|
-
* @template {keyof Database} [Table=keyof Database]
|
237
|
-
*
|
238
|
-
* @param {ApiQuery<
|
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 {
|
216
|
+
* @returns {SelectQueryBuilder<Database, Table, O>}
|
241
217
|
*/
|
242
|
-
export const
|
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 =
|
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
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
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(
|
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(
|
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(
|
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(
|
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(
|
151
|
+
.orderBy('updated_at', 'asc')
|
153
152
|
.limit(limit),
|
154
153
|
app.db.dialectType
|
155
154
|
).as('posts'),
|