xcraft-core-pickaxe 0.1.4 → 0.1.6

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.
@@ -21,6 +21,9 @@ const operators = {
21
21
  if (typeof value === 'boolean') {
22
22
  return value ? 'TRUE' : 'FALSE';
23
23
  }
24
+ if (!values) {
25
+ return escape(value);
26
+ }
24
27
  values.push(value);
25
28
  return '?';
26
29
  },
@@ -121,6 +124,24 @@ const operators = {
121
124
  return `(${sqlConditions.join(' OR ')})`;
122
125
  },
123
126
 
127
+ abs({value}, values) {
128
+ return `ABS(${sql(value, values)})`;
129
+ },
130
+
131
+ plus({values: list}, values) {
132
+ return `(${list
133
+ .map((value) => sql(value, values))
134
+ .filter(Boolean)
135
+ .join(' + ')})`;
136
+ },
137
+
138
+ minus({values: list}, values) {
139
+ return `(${list
140
+ .map((value) => sql(value, values))
141
+ .filter(Boolean)
142
+ .join(' - ')})`;
143
+ },
144
+
124
145
  length({list}, values) {
125
146
  return `json_array_length(${sql(list, values)})`;
126
147
  },
@@ -174,6 +195,33 @@ const operators = {
174
195
  desc({value}, values) {
175
196
  return `${sql(value, values)} DESC`;
176
197
  },
198
+
199
+ count({field, distinct}, values) {
200
+ if (!field) {
201
+ return `COUNT(*)`;
202
+ }
203
+ return `COUNT(${distinct ? 'DISTINCT ' : ''}${sql(field, values)})`;
204
+ },
205
+
206
+ avg({field}, values) {
207
+ return `AVG(${sql(field, values)})`;
208
+ },
209
+
210
+ max({field}, values) {
211
+ return `MAX(${sql(field, values)})`;
212
+ },
213
+
214
+ min({field}, values) {
215
+ return `MIN(${sql(field, values)})`;
216
+ },
217
+
218
+ sum({field, distinct}, values) {
219
+ return `SUM(${distinct ? 'DISTINCT ' : ''}${sql(field, values)})`;
220
+ },
221
+
222
+ groupArray({field}, values) {
223
+ return `json_group_array(${sql(field, values)})`;
224
+ },
177
225
  };
178
226
 
179
227
  /**
@@ -189,7 +237,7 @@ function getOperator(operatorOrPick) {
189
237
 
190
238
  /**
191
239
  * @param {Operator | AnyPick} operatorOrPick
192
- * @param {any[]} values
240
+ * @param {any[] | null} values
193
241
  * @returns {string}
194
242
  */
195
243
  function sql(operatorOrPick, values) {
package/lib/operators.js CHANGED
@@ -1,5 +1,13 @@
1
+ /* eslint-disable jsdoc/require-returns */
1
2
  // @ts-check
2
3
 
4
+ const {number, array} = require('xcraft-core-stones');
5
+
6
+ /**
7
+ * @template {AnyTypeOrShape} T
8
+ * @typedef {import("./picks.js").ValuePick<T>} ValuePick
9
+ */
10
+
3
11
  function op(value) {
4
12
  if (value === null) {
5
13
  return operators.null();
@@ -160,6 +168,33 @@ const operators = {
160
168
  });
161
169
  },
162
170
 
171
+ abs(value) {
172
+ value = op(value);
173
+ return /** @type {const} */ ({
174
+ operator: 'abs',
175
+ type: number,
176
+ value,
177
+ });
178
+ },
179
+
180
+ plus(...values) {
181
+ values = values.map(op);
182
+ return /** @type {const} */ ({
183
+ operator: 'plus',
184
+ type: number,
185
+ values,
186
+ });
187
+ },
188
+
189
+ minus(...values) {
190
+ values = values.map(op);
191
+ return /** @type {const} */ ({
192
+ operator: 'minus',
193
+ type: number,
194
+ values,
195
+ });
196
+ },
197
+
163
198
  length(list) {
164
199
  return /** @type {const} */ ({
165
200
  operator: 'length',
@@ -223,6 +258,75 @@ const operators = {
223
258
  value,
224
259
  });
225
260
  },
261
+
262
+ count(field, distinct) {
263
+ return /** @type {const} */ ({
264
+ operator: 'count',
265
+ type: number,
266
+ field,
267
+ distinct,
268
+ });
269
+ },
270
+
271
+ /**
272
+ * @template {AnyTypeOrShape} T
273
+ * @param {ValuePick<T>} field
274
+ */
275
+ avg(field) {
276
+ return /** @type {const} */ ({
277
+ operator: 'avg',
278
+ type: field.type,
279
+ field: field.value,
280
+ });
281
+ },
282
+
283
+ /**
284
+ * @template {AnyTypeOrShape} T
285
+ * @param {ValuePick<T>} field
286
+ */
287
+ max(field) {
288
+ return /** @type {const} */ ({
289
+ operator: 'max',
290
+ type: field.type,
291
+ field: field.value,
292
+ });
293
+ },
294
+
295
+ /**
296
+ * @template {AnyTypeOrShape} T
297
+ * @param {ValuePick<T>} field
298
+ */
299
+ min(field) {
300
+ return /** @type {const} */ ({
301
+ operator: 'min',
302
+ type: field.type,
303
+ field: field.value,
304
+ });
305
+ },
306
+
307
+ /**
308
+ * @template {AnyTypeOrShape} T
309
+ * @param {ValuePick<T>} field
310
+ */
311
+ sum(field) {
312
+ return /** @type {const} */ ({
313
+ operator: 'sum',
314
+ type: field.type,
315
+ field: field.value,
316
+ });
317
+ },
318
+
319
+ /**
320
+ * @template {AnyTypeOrShape} T
321
+ * @param {ValuePick<T>} field
322
+ */
323
+ groupArray(field) {
324
+ return /** @type {const} */ ({
325
+ operator: 'groupArray',
326
+ type: array(field.type),
327
+ field: field.value,
328
+ });
329
+ },
226
330
  };
227
331
 
228
332
  /**
package/lib/picks.js CHANGED
@@ -12,6 +12,7 @@ const {
12
12
  number,
13
13
  StringType,
14
14
  string,
15
+ NumberType,
15
16
  } = require('xcraft-core-stones');
16
17
 
17
18
  const $o = require('./operators.js');
@@ -49,7 +50,7 @@ const $o = require('./operators.js');
49
50
 
50
51
  /**
51
52
  * @template {Type} T
52
- * @typedef { [T] extends [ArrayType<infer V>] ? ArrayPick<V> : [T] extends [ObjectType<infer S>] ? ObjectPick<S> : [T] extends [ObjectMapType<infer V>] ? RecordPick<StringType,V> : [T] extends [RecordType<infer K,infer V>] ? RecordPick<K,V> : ValuePick<T>} PickOfType
53
+ * @typedef { [T] extends [ArrayType<infer V>] ? ArrayPick<V> : [T] extends [ObjectType<infer S>] ? ObjectPick<S> : [T] extends [ObjectMapType<infer V>] ? RecordPick<StringType,V> : [T] extends [RecordType<infer K,infer V>] ? RecordPick<K,V> : [T] extends [NumberType] ? NumberPick : ValuePick<T>} PickOfType
53
54
  */
54
55
  /**
55
56
  * @template {AnyTypeOrShape} T
@@ -155,6 +156,29 @@ class ValuePick {
155
156
  }
156
157
  }
157
158
 
159
+ /**
160
+ * @extends {ValuePick<NumberType>}
161
+ */
162
+ class NumberPick extends ValuePick {
163
+ abs() {
164
+ return $o.abs(this.value);
165
+ }
166
+
167
+ /**
168
+ * @param {number | ValuePick<NumberType>} value
169
+ */
170
+ plus(value) {
171
+ return $o.plus(this.value, value);
172
+ }
173
+
174
+ /**
175
+ * @param {number | ValuePick<NumberType>} value
176
+ */
177
+ minus(value) {
178
+ return $o.minus(this.value, value);
179
+ }
180
+ }
181
+
158
182
  /**
159
183
  * @template {ObjectShape} T
160
184
  */
@@ -340,7 +364,7 @@ class ArrayPick {
340
364
  }
341
365
 
342
366
  get length() {
343
- return new ValuePick(number, {
367
+ return new NumberPick(number, {
344
368
  field: $o.length(this.value),
345
369
  path: [],
346
370
  });
@@ -437,6 +461,9 @@ function makeTypePick(type, context) {
437
461
  if (type instanceof RecordType) {
438
462
  return /** @type {any} */ (new RecordPick(type, context));
439
463
  }
464
+ if (type instanceof NumberType) {
465
+ return /** @type {any} */ (new NumberPick(type, context));
466
+ }
440
467
  return /** @type {any} */ (new ValuePick(type, context));
441
468
  }
442
469
 
@@ -28,6 +28,28 @@ const {queryToSql} = require('./query-to-sql.js');
28
28
  * @typedef {import("./picks.js").Path} Path
29
29
  */
30
30
 
31
+ /**
32
+ * @typedef {Operators["count"] | Operators["avg"] | Operators["max"] | Operators["min"] | Operators["sum"] | Operators["groupArray"]} Aggregator
33
+ */
34
+ /**
35
+ * @typedef {Operators["abs"] | Operators["plus"] | Operators["minus"]} MathOperator
36
+ */
37
+
38
+ /**
39
+ * @typedef {AnyPick | Aggregator | MathOperator} SelectValue
40
+ */
41
+ /**
42
+ * @template T
43
+ * @typedef {T extends SelectValue ? T["type"]: never} SelectValueType
44
+ */
45
+ /**
46
+ * @typedef {Record<string, SelectValue> | [SelectValue, ...SelectValue[]]} SelectResult
47
+ */
48
+ /**
49
+ * @template {SelectResult} T
50
+ * @typedef {{[K in keyof T]: t<SelectValueType<T[K]>>}} QueryResultOf
51
+ */
52
+
31
53
  /**
32
54
  * @typedef {ValuePick<any> | Operators["desc"] | Operators["asc"]} OrderByValue
33
55
  */
@@ -35,15 +57,21 @@ const {queryToSql} = require('./query-to-sql.js');
35
57
  * @typedef {OrderByValue | OrderByValue[]} OrderByResult
36
58
  */
37
59
 
60
+ /**
61
+ * @typedef {ValuePick<any> | ValuePick<any>[]} GroupByResult
62
+ */
63
+
38
64
  /**
39
65
  * @typedef {{
40
66
  * db: string,
41
67
  * from: string,
42
68
  * scope?: ObjectPick<any>
43
- * select: Record<string, AnyPick>,
69
+ * select: SelectResult,
44
70
  * selectOneField?: boolean,
71
+ * distinct?: boolean,
45
72
  * where?: Operator,
46
- * orderBy?: OrderByResult
73
+ * orderBy?: OrderByResult,
74
+ * groupBy?: GroupByResult
47
75
  * }} QueryObj
48
76
  */
49
77
 
@@ -76,7 +104,10 @@ class FinalQuery {
76
104
  }
77
105
 
78
106
  #useRaw() {
79
- return Boolean(this.#queryParts.selectOneField);
107
+ return (
108
+ Array.isArray(this.#queryParts.select) ||
109
+ Boolean(this.#queryParts.selectOneField)
110
+ );
80
111
  }
81
112
 
82
113
  #getStatement() {
@@ -156,6 +187,10 @@ class FinalQuery {
156
187
  return this.#queryParts;
157
188
  }
158
189
 
190
+ sql() {
191
+ return queryToSql(this.#queryParts, false).sql;
192
+ }
193
+
159
194
  /**
160
195
  * @returns {R | undefined}
161
196
  */
@@ -173,6 +208,24 @@ class FinalQuery {
173
208
  return Array.from(this.#getStatement().iterate(), mapRow);
174
209
  }
175
210
 
211
+ #resultIsEntry() {
212
+ return (
213
+ Array.isArray(this.#queryParts.select) &&
214
+ this.#queryParts.select.length === 2
215
+ );
216
+ }
217
+
218
+ /**
219
+ * @returns {R extends [infer K, infer V] ? Record<K,V> : never}
220
+ */
221
+ toObject() {
222
+ if (!this.#resultIsEntry()) {
223
+ throw new Error('Select result must be a [key,value] entry');
224
+ }
225
+ const entries = /** @type {[any, any]} */ (this.all());
226
+ return /** @type {any} */ (Object.fromEntries(entries));
227
+ }
228
+
176
229
  /**
177
230
  * @returns {Generator<R>}
178
231
  */
@@ -206,6 +259,17 @@ class ScopedSelectQuery extends FinalQuery {
206
259
  this.#queryParts = queryParts;
207
260
  }
208
261
 
262
+ /**
263
+ * @returns {ScopedSelectQuery<T,R>}
264
+ */
265
+ distinct() {
266
+ const queryParts = {
267
+ ...this.#queryParts,
268
+ distinct: true,
269
+ };
270
+ return new ScopedSelectQuery(this.#database, this.#type, queryParts);
271
+ }
272
+
209
273
  /**
210
274
  * @param {(obj: ObjectPick<T>, $: typeof operators) => Operator} fct
211
275
  * @returns {ScopedSelectQuery<T,R>}
@@ -231,6 +295,19 @@ class ScopedSelectQuery extends FinalQuery {
231
295
  };
232
296
  return new ScopedSelectQuery(this.#database, this.#type, queryParts);
233
297
  }
298
+
299
+ /**
300
+ * @param {(obj: ObjectPick<T>, $: typeof operators) => GroupByResult} fct
301
+ * @returns {ScopedSelectQuery<T,R>}
302
+ */
303
+ groupBy(fct) {
304
+ const scope = this.#queryParts.scope;
305
+ const queryParts = {
306
+ ...this.#queryParts,
307
+ groupBy: fct(scope, operators),
308
+ };
309
+ return new ScopedSelectQuery(this.#database, this.#type, queryParts);
310
+ }
234
311
  }
235
312
 
236
313
  /**
@@ -255,6 +332,17 @@ class SelectQuery extends FinalQuery {
255
332
  this.#queryParts = queryParts;
256
333
  }
257
334
 
335
+ /**
336
+ * @returns {SelectQuery<T,R>}
337
+ */
338
+ distinct() {
339
+ const queryParts = {
340
+ ...this.#queryParts,
341
+ distinct: true,
342
+ };
343
+ return new SelectQuery(this.#database, this.#type, queryParts);
344
+ }
345
+
258
346
  /**
259
347
  * @param {(obj: RowPick<T>, $: typeof operators) => Operator} fct
260
348
  * @returns {SelectQuery<T,R>}
@@ -271,7 +359,7 @@ class SelectQuery extends FinalQuery {
271
359
  }
272
360
 
273
361
  /**
274
- * @param {(obj: RowPick<T>, $: typeof operators) => ValuePick<any> | ValuePick<any>[]} fct
362
+ * @param {(obj: RowPick<T>, $: typeof operators) => OrderByResult} fct
275
363
  * @returns {SelectQuery<T,R>}
276
364
  */
277
365
  orderBy(fct) {
@@ -281,6 +369,18 @@ class SelectQuery extends FinalQuery {
281
369
  };
282
370
  return new SelectQuery(this.#database, this.#type, queryParts);
283
371
  }
372
+
373
+ /**
374
+ * @param {(obj: RowPick<T>, $: typeof operators) => GroupByResult} fct
375
+ * @returns {SelectQuery<T,R>}
376
+ */
377
+ groupBy(fct) {
378
+ const queryParts = {
379
+ ...this.#queryParts,
380
+ groupBy: fct(rowPick(this.#type), operators),
381
+ };
382
+ return new SelectQuery(this.#database, this.#type, queryParts);
383
+ }
284
384
  }
285
385
 
286
386
  /**
@@ -336,15 +436,15 @@ class ScopedFromQuery {
336
436
  }
337
437
 
338
438
  /**
339
- * @template {Record<string, AnyPick>} R
340
- * @param {(obj: ObjectPick<T>) => R} fct
341
- * @returns {ScopedSelectQuery<T, t<{[K in keyof R]: R[K]["type"]}>>}
439
+ * @template {SelectResult} R
440
+ * @param {(obj: ObjectPick<T>, $: typeof operators) => R} fct
441
+ * @returns {ScopedSelectQuery<T, QueryResultOf<R>>}
342
442
  */
343
443
  select(fct) {
344
444
  const scope = this.#queryParts.scope;
345
445
  const queryParts = {
346
446
  ...this.#queryParts,
347
- select: fct(scope),
447
+ select: fct(scope, operators),
348
448
  };
349
449
  return new ScopedSelectQuery(this.#database, this.#type, queryParts);
350
450
  }
@@ -414,14 +514,14 @@ class FromQuery {
414
514
  }
415
515
 
416
516
  /**
417
- * @template {Record<string, AnyPick>} R
418
- * @param {(obj: RowPick<T>) => R} fct
419
- * @returns {SelectQuery<T, t<{[K in keyof R]: R[K]["type"]}>>}
517
+ * @template {SelectResult} R
518
+ * @param {(obj: RowPick<T>, $: typeof operators) => R} fct
519
+ * @returns {SelectQuery<T, QueryResultOf<R>>}
420
520
  */
421
521
  select(fct) {
422
522
  const queryParts = {
423
523
  ...this.#queryParts,
424
- select: fct(rowPick(this.#type)),
524
+ select: fct(rowPick(this.#type), operators),
425
525
  };
426
526
  return new SelectQuery(this.#database, this.#type, queryParts);
427
527
  }
@@ -7,10 +7,13 @@ const {sql} = require('./operator-to-sql.js');
7
7
 
8
8
  /**
9
9
  * @param {QueryObj["select"]} select
10
- * @param {any[]} values
10
+ * @param {any[] | null} values
11
11
  * @returns {string}
12
12
  */
13
13
  function selectFields(select, values) {
14
+ if (Array.isArray(select)) {
15
+ return select.map((value) => sql(value, values)).join(', ');
16
+ }
14
17
  return Object.entries(select)
15
18
  .map(([name, value]) => `${sql(value, values)} AS ${name}`)
16
19
  .join(', ');
@@ -18,7 +21,7 @@ function selectFields(select, values) {
18
21
 
19
22
  /**
20
23
  * @param {NonNullable<QueryObj["orderBy"]>} orderBy
21
- * @param {any[]} values
24
+ * @param {any[] | null} values
22
25
  * @returns {string}
23
26
  */
24
27
  function orderByFields(orderBy, values) {
@@ -28,13 +31,27 @@ function orderByFields(orderBy, values) {
28
31
  return sql(orderBy, values);
29
32
  }
30
33
 
34
+ /**
35
+ * @param {NonNullable<QueryObj["groupBy"]>} groupBy
36
+ * @param {any[] | null} values
37
+ * @returns {string}
38
+ */
39
+ function groupByFields(groupBy, values) {
40
+ if (Array.isArray(groupBy)) {
41
+ return groupBy.map((value) => sql(value, values)).join(', ');
42
+ }
43
+ return sql(groupBy, values);
44
+ }
45
+
31
46
  /**
32
47
  * @param {QueryObj} query
33
- * @returns {{sql: string, values: any[]}}
48
+ * @param {boolean} [useBindedValues=true]
49
+ * @returns {{sql: string, values: any[] | null}}
34
50
  */
35
- function queryToSql(query) {
36
- const values = [];
37
- let result = `SELECT ${selectFields(query.select, values)}`;
51
+ function queryToSql(query, useBindedValues = true) {
52
+ const values = useBindedValues ? [] : null;
53
+ const distinct = query.distinct ? 'DISTINCT ' : '';
54
+ let result = `SELECT ${distinct}${selectFields(query.select, values)}`;
38
55
  result += '\n' + `FROM ${query.from}`;
39
56
  if (query.where) {
40
57
  result += '\n' + `WHERE ${sql(query.where, values)}`;
@@ -42,6 +59,9 @@ function queryToSql(query) {
42
59
  if (query.orderBy) {
43
60
  result += '\n' + `ORDER BY ${orderByFields(query.orderBy, values)}`;
44
61
  }
62
+ if (query.groupBy) {
63
+ result += '\n' + `GROUP BY ${groupByFields(query.groupBy, values)}`;
64
+ }
45
65
  return {sql: result, values};
46
66
  }
47
67
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xcraft-core-pickaxe",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Query builder",
5
5
  "main": "index.js",
6
6
  "scripts": {