@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.
- package/migrate.js +3 -0
- package/migrations.shared/00003_alter_auth_users.js +6 -0
- package/package.json +2 -2
- package/src/con.auth_users.js +17 -13
- package/src/con.collections.js +24 -16
- package/src/con.customers.js +26 -19
- package/src/con.discounts.js +31 -20
- package/src/con.discounts.utils.js +52 -41
- package/src/con.images.js +18 -13
- package/src/con.notifications.js +26 -17
- package/src/con.orders.js +25 -18
- package/src/con.posts.js +28 -28
- package/src/con.products.js +80 -128
- 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 +217 -179
- package/tests/sandbox.js +5 -6
package/src/utils.query.js
CHANGED
@@ -1,231 +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 {
|
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
|
-
*
|
12
|
-
*
|
13
|
-
*
|
14
|
-
*
|
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
|
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
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
67
|
-
|
92
|
+
const reduced = utils.reduce_vql(
|
93
|
+
{
|
94
|
+
vql,
|
68
95
|
|
69
|
-
|
70
|
-
|
71
|
-
|
96
|
+
map_leaf: (node) => {
|
97
|
+
return leaf_ops(
|
98
|
+
node.op,
|
99
|
+
/** @type {any} */(node.name)
|
100
|
+
);
|
101
|
+
},
|
72
102
|
|
73
|
-
|
103
|
+
reduce_AND: identity(
|
104
|
+
(nodes) => {
|
105
|
+
return eb.and(nodes);
|
106
|
+
}
|
107
|
+
),
|
74
108
|
|
75
|
-
|
76
|
-
|
109
|
+
reduce_OR: (nodes) => {
|
110
|
+
return eb.or(nodes);
|
111
|
+
},
|
77
112
|
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
98
|
-
|
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
|
-
|
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
|
-
*
|
139
|
-
* @param {
|
140
|
-
* @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[]}
|
141
159
|
*/
|
142
|
-
const
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|
155
|
-
*
|
156
|
-
* @template {any} [
|
157
|
-
*
|
158
|
-
* @param {
|
159
|
-
* @param {
|
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
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
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.
|
178
|
-
|
179
|
-
|
180
|
-
|
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
|
-
* @
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
*
|
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 {
|
216
|
+
* @returns {SelectQueryBuilder<Database, Table, O>}
|
215
217
|
*/
|
216
|
-
export const
|
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 =
|
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
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
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(
|
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'),
|