xcraft-core-pickaxe 0.1.0

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/.editorconfig ADDED
@@ -0,0 +1,9 @@
1
+ root = true
2
+
3
+ [*.{js,jsx,json}]
4
+ indent_style = space
5
+ indent_size = 2
6
+ charset = utf-8
7
+ trim_trailing_whitespace = true
8
+ insert_final_newline = true
9
+ max_line_length = 80
package/.eslintrc.js ADDED
@@ -0,0 +1,39 @@
1
+ 'use strict';
2
+
3
+ module.exports = {
4
+ root: true,
5
+ parserOptions: {
6
+ sourceType: 'module',
7
+ ecmaFeatures: {
8
+ jsx: true,
9
+ },
10
+ },
11
+ env: {
12
+ browser: true,
13
+ es2022: true,
14
+ mocha: true,
15
+ node: true,
16
+ },
17
+ plugins: ['react', 'babel', 'jsdoc'],
18
+ extends: [
19
+ 'prettier',
20
+ 'eslint:recommended',
21
+ 'plugin:react/recommended',
22
+ 'plugin:jsdoc/recommended',
23
+ ],
24
+ rules: {
25
+ // Other rules
26
+ 'no-console': 'off',
27
+ 'eqeqeq': 'error',
28
+ 'react/display-name': 'off',
29
+ 'no-unused-vars': [
30
+ 'error',
31
+ {
32
+ vars: 'all',
33
+ args: 'none',
34
+ ignoreRestSiblings: true,
35
+ destructuredArrayIgnorePattern: '^_',
36
+ },
37
+ ],
38
+ },
39
+ };
package/.zou-flow ADDED
@@ -0,0 +1,2 @@
1
+ [update-version]
2
+ package-json = package.json
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Xcraft
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,3 @@
1
+ # xcraft-core-pickaxe
2
+
3
+ Convenient tool to extract well shaped objects from deep data mines.
@@ -0,0 +1,130 @@
1
+ /* eslint-disable no-inner-declarations */
2
+ // @ts-check
3
+ const {operatorToSql} = require('./operator-to-sql.js');
4
+ const {
5
+ string,
6
+ number,
7
+ array,
8
+ object,
9
+ dateTime,
10
+ any,
11
+ value,
12
+ } = require('xcraft-core-stones');
13
+ const $ = require('./operators.js');
14
+ const {queryToSql} = require('./query-to-sql.js');
15
+ const {QueryBuilder, ScopedFromQuery} = require('./query-builder.js');
16
+ const {makePick} = require('./picks.js');
17
+
18
+ class TestUserShape {
19
+ firstname = string;
20
+ lastname = string;
21
+ age = number;
22
+ mails = array(string);
23
+ address = class {
24
+ streetName = string;
25
+ townName = string;
26
+ };
27
+ }
28
+
29
+ const TestUserShape2 = {
30
+ firstname: string,
31
+ lastname: string,
32
+ age: number,
33
+ mails: array(string),
34
+ address: class {
35
+ streetName = string;
36
+ townName = string;
37
+ },
38
+ };
39
+
40
+ const TestUserShape3 = object({
41
+ firstname: string,
42
+ lastname: string,
43
+ age: number,
44
+ mails: array(string),
45
+ address: class {
46
+ streetName = string;
47
+ townName = string;
48
+ },
49
+ });
50
+
51
+ /**
52
+ * @param {AnyObjectShape} shape
53
+ */
54
+ function ActionShape(shape) {
55
+ return object({
56
+ name: string,
57
+ action: object({
58
+ meta: any,
59
+ payload: object({
60
+ state: shape,
61
+ }),
62
+ type: value('test'),
63
+ }),
64
+ timestamp: dateTime,
65
+ });
66
+ }
67
+
68
+ example1: {
69
+ const user = makePick(TestUserShape, {field: 'value', path: []});
70
+
71
+ const filter1 = user.get('firstname').eq('toto');
72
+
73
+ const userIds = ['user@toto', 'user@tata'];
74
+ const filter2 = $.and(
75
+ user.get('firstname').eq('Toto'),
76
+ user.get('age').eq(42),
77
+ user.get('lastname').in(userIds),
78
+ user.get('address').get('streetName').eq('Mine road')
79
+ );
80
+
81
+ console.log(operatorToSql(filter1));
82
+ console.log(operatorToSql(filter2));
83
+ }
84
+
85
+ example2: {
86
+ const userIds = ['user@toto', 'user@tata'];
87
+ const builder = new QueryBuilder()
88
+ .db('test_db')
89
+ .from('test_table', TestUserShape3)
90
+ .fields(['firstname', 'age'])
91
+ .where((user, $) =>
92
+ $.and(
93
+ user.field('firstname').eq('Toto'),
94
+ user.field('age').eq(42),
95
+ user.field('lastname').in(userIds),
96
+ user.field('address').get('streetName').eq('Mine road')
97
+ )
98
+ );
99
+
100
+ console.log(queryToSql(builder.query));
101
+ }
102
+
103
+ example3: {
104
+ const builder = new QueryBuilder()
105
+ .db('test_db')
106
+ .from('test_table', ActionShape(TestUserShape))
107
+ .scope((row) => row.field('action').get('payload').get('state'))
108
+ .fields(['firstname', 'age']);
109
+
110
+ console.log(queryToSql(builder.query));
111
+ }
112
+
113
+ example4: {
114
+ /**
115
+ * @template {AnyObjectShape} T
116
+ * @param {T} shape
117
+ * @returns {ScopedFromQuery<t<T>>}
118
+ */
119
+ function queryAction(shape) {
120
+ const builder = new QueryBuilder()
121
+ .db('test_db')
122
+ .from('test_table', ActionShape(shape))
123
+ .scope((row) => row.field('action').get('payload').get('state'));
124
+ return /** @type {ScopedFromQuery<t<T>>} */ (builder);
125
+ }
126
+
127
+ const builder = queryAction(TestUserShape).fields(['firstname', 'age']);
128
+
129
+ console.log(queryToSql(builder.query));
130
+ }
@@ -0,0 +1,140 @@
1
+ // @ts-check
2
+
3
+ function escape(value) {
4
+ if (typeof value === 'string') {
5
+ return `'${value.replace(/'/g, "''")}'`;
6
+ }
7
+ return value;
8
+ }
9
+
10
+ const operators = {
11
+ value({value}, values) {
12
+ values.push(value);
13
+ return '?';
14
+ },
15
+
16
+ null(_, values) {
17
+ return 'NULL';
18
+ },
19
+
20
+ field({field}, values) {
21
+ // Note: field is not validated
22
+ return `${field}`;
23
+ },
24
+
25
+ get({value, path}, values) {
26
+ // TODO: try values.push(path) => '?'
27
+ // return `json_extract(${field}, ${sql(path, values)})`;
28
+ return `json_extract(${sql(value)}, '$.' || ${escape(path.join('.'))})`;
29
+ },
30
+
31
+ not({value}, values) {
32
+ return `NOT ${sql(value, values)}`;
33
+ },
34
+
35
+ stringConcat({values: list}, values) {
36
+ return `(${list
37
+ .map((value) => sql(value, values))
38
+ .filter(Boolean)
39
+ .join(' || ')})`;
40
+ },
41
+
42
+ eq({a, b}, values) {
43
+ return `${sql(a, values)} = ${sql(b, values)}`;
44
+ },
45
+
46
+ neq({a, b}, values) {
47
+ return `${sql(a, values)} <> ${sql(b, values)}`;
48
+ },
49
+
50
+ gte({a, b}, values) {
51
+ return `${sql(a, values)} >= ${sql(b, values)}`;
52
+ },
53
+
54
+ gt({a, b}, values) {
55
+ return `${sql(a, values)} > ${sql(b, values)}`;
56
+ },
57
+
58
+ lte({a, b}, values) {
59
+ return `${sql(a, values)} <= ${sql(b, values)}`;
60
+ },
61
+
62
+ lt({a, b}, values) {
63
+ return `${sql(a, values)} < ${sql(b, values)}`;
64
+ },
65
+
66
+ in({value, list}, values) {
67
+ return `${sql(value, values)} IN (${list
68
+ .map((v) => sql(v, values))
69
+ .join(',')})`;
70
+ },
71
+
72
+ like({value, pattern}, values) {
73
+ return `${sql(value, values)} LIKE ${sql(pattern, values)}`;
74
+ },
75
+
76
+ and({conditions}, values) {
77
+ if (conditions.length === 0) {
78
+ return '';
79
+ }
80
+ return `(${conditions
81
+ .map((condition) => sql(condition, values))
82
+ .filter(Boolean)
83
+ .join(' AND ')})`;
84
+ },
85
+
86
+ or({conditions}, values) {
87
+ if (conditions.length === 0) {
88
+ return '';
89
+ }
90
+ return `(${conditions
91
+ .map((condition) => sql(condition, values))
92
+ .filter(Boolean)
93
+ .join(' OR ')})`;
94
+ },
95
+
96
+ includes({list, value}, values) {
97
+ return `EXISTS (
98
+ SELECT *
99
+ FROM json_each(${list})
100
+ WHERE json_each.value = ${sql(value, values)}
101
+ )`;
102
+ },
103
+
104
+ some({list, condition}, values) {
105
+ return `EXISTS (
106
+ SELECT *
107
+ FROM json_each(${list})
108
+ WHERE ${sql(condition, values)}
109
+ )`;
110
+ },
111
+
112
+ someValue(_, values) {
113
+ return 'json_each.value';
114
+ },
115
+
116
+ keys() {},
117
+
118
+ length() {},
119
+ };
120
+
121
+ function sql(operator, values) {
122
+ const operatorName = operator.operator;
123
+ if (!(operatorName in operators)) {
124
+ throw new Error(`Unknown operator '${operator}'`);
125
+ }
126
+ return operators[operatorName](operator, values);
127
+ }
128
+
129
+ function operatorToSql(operator) {
130
+ const values = [];
131
+ return {
132
+ sql: sql(operator, values),
133
+ values,
134
+ };
135
+ }
136
+
137
+ module.exports = {
138
+ sql,
139
+ operatorToSql,
140
+ };
@@ -0,0 +1,192 @@
1
+ // @ts-check
2
+
3
+ function op(value) {
4
+ if (value === null) {
5
+ return operators.null();
6
+ }
7
+ const type = typeof value;
8
+ if (['string', 'number', 'boolean'].includes(type)) {
9
+ return operators.value(value);
10
+ }
11
+ if (type === 'object' && 'operator' in value) {
12
+ return value;
13
+ }
14
+
15
+ throw new Error(`Bad value '${value}'`);
16
+ }
17
+
18
+ const operators = {
19
+ value(value) {
20
+ return /** @type {const} */ ({
21
+ operator: 'value',
22
+ value,
23
+ });
24
+ },
25
+
26
+ null() {
27
+ return /** @type {const} */ ({
28
+ operator: 'null',
29
+ });
30
+ },
31
+
32
+ field(field) {
33
+ return /** @type {const} */ ({
34
+ operator: 'field',
35
+ field,
36
+ });
37
+ },
38
+
39
+ get(value, path) {
40
+ return /** @type {const} */ ({
41
+ operator: 'get',
42
+ value,
43
+ path,
44
+ });
45
+ },
46
+
47
+ not(value) {
48
+ return /** @type {const} */ ({
49
+ operator: 'not',
50
+ value,
51
+ });
52
+ },
53
+
54
+ stringConcat(...values) {
55
+ values = values.map(op);
56
+ return /** @type {const} */ ({
57
+ operator: 'stringConcat',
58
+ values,
59
+ });
60
+ },
61
+
62
+ eq(a, b) {
63
+ a = op(a);
64
+ b = op(b);
65
+ return /** @type {const} */ ({
66
+ operator: 'eq',
67
+ a,
68
+ b,
69
+ });
70
+ },
71
+
72
+ neq(a, b) {
73
+ a = op(a);
74
+ b = op(b);
75
+ return /** @type {const} */ ({
76
+ operator: 'neq',
77
+ a,
78
+ b,
79
+ });
80
+ },
81
+
82
+ gte(a, b) {
83
+ a = op(a);
84
+ b = op(b);
85
+ return /** @type {const} */ ({
86
+ operator: 'gte',
87
+ a,
88
+ b,
89
+ });
90
+ },
91
+
92
+ gt(a, b) {
93
+ a = op(a);
94
+ b = op(b);
95
+ return /** @type {const} */ ({
96
+ operator: 'gt',
97
+ a,
98
+ b,
99
+ });
100
+ },
101
+
102
+ lte(a, b) {
103
+ a = op(a);
104
+ b = op(b);
105
+ return /** @type {const} */ ({
106
+ operator: 'lte',
107
+ a,
108
+ b,
109
+ });
110
+ },
111
+
112
+ lt(a, b) {
113
+ a = op(a);
114
+ b = op(b);
115
+ return /** @type {const} */ ({
116
+ operator: 'lt',
117
+ a,
118
+ b,
119
+ });
120
+ },
121
+
122
+ in(value, list) {
123
+ list = list.map(op);
124
+ return /** @type {const} */ ({
125
+ operator: 'in',
126
+ value,
127
+ list,
128
+ });
129
+ },
130
+
131
+ like(value, pattern) {
132
+ value = op(value);
133
+ pattern = op(pattern);
134
+ return /** @type {const} */ ({
135
+ operator: 'like',
136
+ value,
137
+ pattern,
138
+ });
139
+ },
140
+
141
+ and(...conditions) {
142
+ return /** @type {const} */ ({
143
+ operator: 'and',
144
+ conditions,
145
+ });
146
+ },
147
+
148
+ or(...conditions) {
149
+ return /** @type {const} */ ({
150
+ operator: 'or',
151
+ conditions,
152
+ });
153
+ },
154
+
155
+ includes(list, value) {
156
+ value = op(value);
157
+ return /** @type {const} */ ({
158
+ operator: 'includes',
159
+ list,
160
+ value,
161
+ });
162
+ },
163
+
164
+ some(list, condition) {
165
+ return /** @type {const} */ ({
166
+ operator: 'some',
167
+ list,
168
+ condition,
169
+ });
170
+ },
171
+
172
+ someValue() {
173
+ return /** @type {const} */ ({
174
+ operator: 'someValue',
175
+ });
176
+ },
177
+ };
178
+
179
+ /**
180
+ * @template T
181
+ * @typedef {T[keyof T]} Values
182
+ */
183
+
184
+ /**
185
+ * @typedef {{[k in keyof typeof operators] : ReturnType<typeof operators[k]>}} Operators
186
+ */
187
+
188
+ /**
189
+ * @typedef {Values<{[k in keyof typeof operators] : ReturnType<typeof operators[k]>}>} Operator
190
+ */
191
+
192
+ module.exports = operators;
package/lib/picks.js ADDED
@@ -0,0 +1,348 @@
1
+ /* eslint-disable jsdoc/require-returns */
2
+ // @ts-check
3
+
4
+ const {
5
+ ArrayType,
6
+ ObjectType,
7
+ Type,
8
+ getTypeInstance,
9
+ } = require('xcraft-core-stones');
10
+
11
+ const $o = require('./operators.js');
12
+
13
+ /**
14
+ * @typedef {import('./operators.js').Operators} Operators
15
+ */
16
+ /**
17
+ * @typedef {import('./operators.js').Operator} Operator
18
+ */
19
+
20
+ /**
21
+ * @typedef {boolean | number | bigint | string | symbol | undefined | null} primitive
22
+ */
23
+ /**
24
+ * @typedef {primitive | Function | Date | Error | RegExp} Builtin
25
+ */
26
+
27
+ /**
28
+ * @typedef {(keyof any)} PathElement
29
+ */
30
+ /**
31
+ * @typedef {PathElement[]} Path
32
+ */
33
+
34
+ /**
35
+ * @typedef {{field: Operators["someValue"] | (keyof any), path: Path}} Context
36
+ */
37
+
38
+ /**
39
+ * @template T
40
+ * @typedef {0 extends (1 & T) ? true : never} IsAny
41
+ */
42
+
43
+ /**
44
+ * @typedef {ValuePick<any> | ArrayPick<any> | ObjectPick<any>} AnyPick
45
+ */
46
+
47
+ /**
48
+ * @template T
49
+ * @typedef {true extends IsAny<T> ? AnyPick : [T] extends [Builtin] ? ValuePick<T> : [T] extends [Array<infer V>] ? ArrayPick<V> : [T] extends [{}] ? ObjectPick<T> : never} PickOf
50
+ */
51
+
52
+ /**
53
+ * @template {{}} T
54
+ * @typedef {ObjectType<{[K in keyof T] : Type<T[K]>}>} ObjectTypeOf
55
+ */
56
+
57
+ /**
58
+ * @param {Context} context
59
+ * @param {PathElement} path
60
+ * @returns {Context}
61
+ */
62
+ function contextWithPath(context, path) {
63
+ return {
64
+ ...context,
65
+ path: [...context.path, path],
66
+ };
67
+ }
68
+
69
+ /**
70
+ * @template T
71
+ */
72
+ class ValuePick {
73
+ /** @type {Type<T>} */
74
+ #type;
75
+ /** @type {Context} */
76
+ #context;
77
+
78
+ /**
79
+ * @param {Type<T>} type
80
+ * @param {Context} context
81
+ */
82
+ constructor(type, context) {
83
+ this.#type = type;
84
+ this.#context = context;
85
+ }
86
+
87
+ get value() {
88
+ const {field, path} = this.#context;
89
+ if (path.length > 0) {
90
+ return $o.get($o.field(field), path);
91
+ }
92
+ return $o.field(field);
93
+ }
94
+
95
+ get type() {
96
+ return this.#type;
97
+ }
98
+
99
+ /**
100
+ * @param {T | ValuePick<T>} value
101
+ */
102
+ eq(value) {
103
+ return $o.eq(this.value, value);
104
+ }
105
+
106
+ /**
107
+ * @param {T | ValuePick<T>} value
108
+ */
109
+ neq(value) {
110
+ return $o.neq(this.value, value);
111
+ }
112
+
113
+ /**
114
+ * @param {T | ValuePick<T>} value
115
+ */
116
+ gte(value) {
117
+ return $o.gte(this.value, value);
118
+ }
119
+
120
+ /**
121
+ * @param {T | ValuePick<T>} value
122
+ */
123
+ gt(value) {
124
+ return $o.gt(this.value, value);
125
+ }
126
+
127
+ /**
128
+ * @param {T | ValuePick<T>} value
129
+ */
130
+ lte(value) {
131
+ return $o.lte(this.value, value);
132
+ }
133
+
134
+ /**
135
+ * @param {T | ValuePick<T>} value
136
+ */
137
+ lt(value) {
138
+ return $o.lt(this.value, value);
139
+ }
140
+
141
+ /**
142
+ * @param {(T | ValuePick<T>)[]} list
143
+ */
144
+ in(list) {
145
+ return $o.in(this.value, list);
146
+ }
147
+
148
+ /**
149
+ * @param {string | ValuePick<string>} value
150
+ */
151
+ like(value) {
152
+ return $o.like(this.value, value);
153
+ }
154
+ }
155
+
156
+ /**
157
+ * @template {{}} T
158
+ */
159
+ class ObjectPick {
160
+ /** @type {ObjectTypeOf<T>} */
161
+ #type;
162
+ /** @type {Context} */
163
+ #context;
164
+
165
+ /**
166
+ * @param {ObjectTypeOf<T>} type
167
+ * @param {Context} context
168
+ */
169
+ constructor(type, context) {
170
+ this.#type = type;
171
+ this.#context = context;
172
+ }
173
+
174
+ get value() {
175
+ const {field, path} = this.#context;
176
+ if (path.length > 0) {
177
+ return $o.get($o.field(field), path);
178
+ }
179
+ return $o.field(field);
180
+ }
181
+
182
+ get type() {
183
+ return this.#type;
184
+ }
185
+
186
+ /**
187
+ * @template {keyof T} K
188
+ * @param {K} key
189
+ * @returns {PickOf<T[K]>}
190
+ */
191
+ get(key) {
192
+ return makeTypePick(
193
+ getTypeInstance(this.#type.properties[key]),
194
+ contextWithPath(this.#context, key)
195
+ );
196
+ }
197
+ }
198
+
199
+ /**
200
+ * @template T
201
+ */
202
+ class ArrayPick {
203
+ /** @type {ArrayType<Type<T>>} */
204
+ #type;
205
+ /** @type {Context} */
206
+ #context;
207
+
208
+ /**
209
+ * @param {ArrayType<Type<T>>} type
210
+ * @param {Context} context
211
+ */
212
+ constructor(type, context) {
213
+ this.#type = type;
214
+ this.#context = context;
215
+ }
216
+
217
+ get value() {
218
+ const {field, path} = this.#context;
219
+ return $o.get($o.field(field), path);
220
+ }
221
+
222
+ get type() {
223
+ return this.#type;
224
+ }
225
+
226
+ /**
227
+ * @param {number} index
228
+ * @returns {PickOf<T>}
229
+ */
230
+ get(index) {
231
+ return makeTypePick(
232
+ getTypeInstance(this.#type.valuesType),
233
+ contextWithPath(this.#context, index)
234
+ );
235
+ }
236
+
237
+ /**
238
+ * @param {T | ValuePick<T>} value
239
+ */
240
+ includes(value) {
241
+ return $o.includes(this.value, value);
242
+ }
243
+
244
+ /**
245
+ * @param {(value: PickOf<T>) => Operator} func
246
+ */
247
+ some(func) {
248
+ return $o.some(
249
+ this.value,
250
+ func(
251
+ makeTypePick(getTypeInstance(this.#type.valuesType), {
252
+ field: $o.someValue(),
253
+ path: [],
254
+ })
255
+ )
256
+ );
257
+ }
258
+ }
259
+
260
+ /**
261
+ * @param {{}} value
262
+ * @returns {value is AnyPick}
263
+ */
264
+ function isAnyPick(value) {
265
+ return (
266
+ value instanceof ValuePick ||
267
+ value instanceof ObjectPick ||
268
+ value instanceof ArrayPick
269
+ );
270
+ }
271
+
272
+ /**
273
+ * @template {Record<string,any>} T
274
+ */
275
+ class RowPick {
276
+ /** @type {ObjectTypeOf<T>} */
277
+ #type;
278
+
279
+ /**
280
+ * @param {ObjectTypeOf<T>} type
281
+ */
282
+ constructor(type) {
283
+ this.#type = type;
284
+ }
285
+
286
+ /**
287
+ * @template {keyof T} K
288
+ * @param {K} fieldName
289
+ * @returns {PickOf<T[K]>}
290
+ */
291
+ field(fieldName) {
292
+ const context = {
293
+ field: fieldName,
294
+ path: [],
295
+ };
296
+ return makeTypePick(
297
+ getTypeInstance(this.#type.properties[fieldName]),
298
+ context
299
+ );
300
+ }
301
+ }
302
+
303
+ /**
304
+ * @template T
305
+ * @param {Type<T>} type
306
+ * @param {Context} context
307
+ * @returns {PickOf<T>}
308
+ */
309
+ function makeTypePick(type, context) {
310
+ if (type instanceof ArrayType) {
311
+ return /** @type {any} */ (new ArrayPick(type, context));
312
+ }
313
+ if (type instanceof ObjectType) {
314
+ return /** @type {any} */ (new ObjectPick(type, context));
315
+ }
316
+ return /** @type {any} */ (new ValuePick(type, context));
317
+ }
318
+
319
+ /**
320
+ * @template {AnyTypeOrShape} T
321
+ * @param {T} typeOrShape
322
+ * @param {Context} context
323
+ * @returns {PickOf<t<T>>}
324
+ */
325
+ function makePick(typeOrShape, context) {
326
+ const type = getTypeInstance(typeOrShape);
327
+ return /** @type {any} */ (makeTypePick(type, context));
328
+ }
329
+
330
+ /**
331
+ * @template {Record<string,any>} T
332
+ * @param {ObjectTypeOf<T>} type
333
+ * @returns {RowPick<T>}
334
+ */
335
+ function rowPick(type) {
336
+ return new RowPick(type);
337
+ }
338
+
339
+ module.exports = {
340
+ makeTypePick,
341
+ makePick,
342
+ rowPick,
343
+ isAnyPick,
344
+ ValuePick,
345
+ ObjectPick,
346
+ ArrayPick,
347
+ RowPick,
348
+ };
@@ -0,0 +1,415 @@
1
+ // @ts-check
2
+
3
+ const {ObjectType, getTypeInstance} = require('xcraft-core-stones');
4
+ const operators = require('./operators.js');
5
+ const {rowPick, ValuePick, RowPick, ObjectPick} = require('./picks.js');
6
+ const {queryToSql} = require('./query-to-sql.js');
7
+
8
+ /**
9
+ * @typedef {import("./operators.js").Operator} Operator
10
+ */
11
+
12
+ /**
13
+ * @template T
14
+ * @typedef {import("./picks.js").PickOf<T>} PickOf
15
+ */
16
+ /**
17
+ * @typedef {import("./picks.js").AnyPick} AnyPick
18
+ */
19
+ /**
20
+ * @typedef {import("./picks.js").Path} Path
21
+ */
22
+ /**
23
+ * @template {{}} T
24
+ * @typedef {import("./picks.js").ObjectTypeOf<T>} ObjectTypeOf
25
+ */
26
+
27
+ /**
28
+ * @typedef {{
29
+ * db: string,
30
+ * from: string,
31
+ * scope?: ObjectPick<any>
32
+ * select: AnyPick[] | Record<string, AnyPick> | AnyPick,
33
+ * selectOneField?: boolean,
34
+ * where?: Operator,
35
+ * orderBy?: ValuePick<any> | ValuePick<any>[]
36
+ * }} QueryObj
37
+ */
38
+
39
+ /**
40
+ * @template {keyof QueryObj} K
41
+ * @typedef {flatten<Pick<Required<QueryObj>, K> & Omit<Partial<QueryObj>, K>>} QueryParts
42
+ */
43
+
44
+ function mergeWhere(currentWhere, newWhere) {
45
+ if (!currentWhere) {
46
+ return newWhere;
47
+ }
48
+ return operators.and(currentWhere, newWhere);
49
+ }
50
+
51
+ /**
52
+ * @template R
53
+ */
54
+ class FinalQuery {
55
+ #database;
56
+ #queryParts;
57
+
58
+ /**
59
+ * @param {*} database
60
+ * @param {QueryParts<'db' | 'from' | 'select'>} queryParts
61
+ */
62
+ constructor(database, queryParts) {
63
+ this.#database = database;
64
+ this.#queryParts = queryParts;
65
+ }
66
+
67
+ #getStatement() {
68
+ const {sql, values} = queryToSql(this.#queryParts);
69
+ return this.#database.prepare(sql).bind(values);
70
+ }
71
+
72
+ /**
73
+ * @returns {QueryObj}
74
+ */
75
+ get query() {
76
+ return this.#queryParts;
77
+ }
78
+
79
+ /**
80
+ * @returns {R}
81
+ */
82
+ get() {
83
+ const result = this.#getStatement().get();
84
+ if (result && this.#queryParts.selectOneField) {
85
+ return Object.values(result)[0];
86
+ }
87
+ return result;
88
+ }
89
+
90
+ /**
91
+ * @returns {R[]}
92
+ */
93
+ all() {
94
+ if (this.#queryParts.selectOneField) {
95
+ return Array.from(this.#getStatement().raw().iterate(), (row) => row[0]);
96
+ }
97
+ return this.#getStatement().all();
98
+ }
99
+
100
+ /**
101
+ * @returns {Generator<R>}
102
+ */
103
+ *iterate() {
104
+ if (this.#queryParts.selectOneField) {
105
+ for (const row of this.#getStatement().raw().iterate()) {
106
+ yield row[0];
107
+ }
108
+ return;
109
+ }
110
+ yield* this.#getStatement().iterate();
111
+ }
112
+ }
113
+
114
+ /**
115
+ * @template {Record<string, any>} T
116
+ * @template R
117
+ * @extends {FinalQuery<R>}
118
+ */
119
+ class ScopedSelectQuery extends FinalQuery {
120
+ #database;
121
+ #type;
122
+ #queryParts;
123
+
124
+ /**
125
+ * @param {*} database
126
+ * @param {ObjectTypeOf<T>} type
127
+ * @param {QueryParts<'db' | 'from' | 'select' | 'scope'>} queryParts
128
+ */
129
+ constructor(database, type, queryParts) {
130
+ super(database, queryParts);
131
+ this.#database = database;
132
+ this.#type = type;
133
+ this.#queryParts = queryParts;
134
+ }
135
+
136
+ /**
137
+ * @param {(obj: ObjectPick<T>, $: typeof operators) => Operator} fct
138
+ * @returns {ScopedSelectQuery<T,R>}
139
+ */
140
+ where(fct) {
141
+ const scope = this.#queryParts.scope;
142
+ const queryParts = {
143
+ ...this.#queryParts,
144
+ where: mergeWhere(this.#queryParts.where, fct(scope, operators)),
145
+ };
146
+ return new ScopedSelectQuery(this.#database, this.#type, queryParts);
147
+ }
148
+
149
+ /**
150
+ * @param {(obj: ObjectPick<T>, $: typeof operators) => ValuePick<any> | ValuePick<any>[]} fct
151
+ * @returns {ScopedSelectQuery<T,R>}
152
+ */
153
+ orderBy(fct) {
154
+ const scope = this.#queryParts.scope;
155
+ const queryParts = {
156
+ ...this.#queryParts,
157
+ orderBy: fct(scope, operators),
158
+ };
159
+ return new ScopedSelectQuery(this.#database, this.#type, queryParts);
160
+ }
161
+ }
162
+
163
+ /**
164
+ * @template {Record<string, any>} T
165
+ * @template R
166
+ * @extends {FinalQuery<R>}
167
+ */
168
+ class SelectQuery extends FinalQuery {
169
+ #database;
170
+ #type;
171
+ #queryParts;
172
+
173
+ /**
174
+ * @param {*} database
175
+ * @param {ObjectTypeOf<T>} type
176
+ * @param {QueryParts<'db' | 'from' | 'select'>} queryParts
177
+ */
178
+ constructor(database, type, queryParts) {
179
+ super(database, queryParts);
180
+ this.#database = database;
181
+ this.#type = type;
182
+ this.#queryParts = queryParts;
183
+ }
184
+
185
+ /**
186
+ * @param {(obj: RowPick<T>, $: typeof operators) => Operator} fct
187
+ * @returns {SelectQuery<T,R>}
188
+ */
189
+ where(fct) {
190
+ const queryParts = {
191
+ ...this.#queryParts,
192
+ where: mergeWhere(
193
+ this.#queryParts.where,
194
+ fct(rowPick(this.#type), operators)
195
+ ),
196
+ };
197
+ return new SelectQuery(this.#database, this.#type, queryParts);
198
+ }
199
+
200
+ /**
201
+ * @param {(obj: RowPick<T>, $: typeof operators) => ValuePick<any> | ValuePick<any>[]} fct
202
+ * @returns {SelectQuery<T,R>}
203
+ */
204
+ orderBy(fct) {
205
+ const queryParts = {
206
+ ...this.#queryParts,
207
+ orderBy: fct(rowPick(this.#type), operators),
208
+ };
209
+ return new SelectQuery(this.#database, this.#type, queryParts);
210
+ }
211
+ }
212
+
213
+ /**
214
+ * @template {Record<string, any>} T
215
+ */
216
+ class ScopedFromQuery {
217
+ #database;
218
+ #type;
219
+ #queryParts;
220
+
221
+ /**
222
+ * @param {*} database
223
+ * @param {ObjectTypeOf<T>} type
224
+ * @param {QueryParts<'db' | 'from' | 'scope'>} queryParts
225
+ */
226
+ constructor(database, type, queryParts) {
227
+ this.#database = database;
228
+ this.#type = type;
229
+ this.#queryParts = queryParts;
230
+ }
231
+
232
+ /**
233
+ * Select field
234
+ * @template {keyof T} F
235
+ * @param {F} value
236
+ * @returns {ScopedSelectQuery<T, T[F]>}
237
+ */
238
+ field(value) {
239
+ const scope = this.#queryParts.scope;
240
+ const queryParts = {
241
+ ...this.#queryParts,
242
+ select: scope.get(value),
243
+ selectOneField: true,
244
+ };
245
+ return new ScopedSelectQuery(this.#database, this.#type, queryParts);
246
+ }
247
+
248
+ /**
249
+ * Select fields
250
+ * @template {(keyof T)[]} F
251
+ * @param {F} values
252
+ * @returns {ScopedSelectQuery<T, flatten<Pick<T, F[number]>>>}
253
+ */
254
+ fields(values) {
255
+ const scope = this.#queryParts.scope;
256
+ const queryParts = {
257
+ ...this.#queryParts,
258
+ select: values.map((value) => scope.get(value)),
259
+ };
260
+ return new ScopedSelectQuery(this.#database, this.#type, queryParts);
261
+ }
262
+
263
+ /**
264
+ * @template {Record<string, any>} R
265
+ * @param {(obj: ObjectPick<T>) => {[K in keyof R]: PickOf<R[K]>}} fct
266
+ * @returns {ScopedSelectQuery<T, R>}
267
+ */
268
+ select(fct) {
269
+ const scope = this.#queryParts.scope;
270
+ const selectedFields = fct(scope);
271
+ const queryParts = {...this.#queryParts, select: selectedFields};
272
+ return new ScopedSelectQuery(this.#database, this.#type, queryParts);
273
+ }
274
+
275
+ /**
276
+ * @param {(obj: ObjectPick<T>, $: typeof operators) => Operator} fct
277
+ * @returns {ScopedFromQuery<T>}
278
+ */
279
+ where(fct) {
280
+ const scope = this.#queryParts.scope;
281
+ const queryParts = {
282
+ ...this.#queryParts,
283
+ where: mergeWhere(this.#queryParts.where, fct(scope, operators)),
284
+ };
285
+ return new ScopedFromQuery(this.#database, this.#type, queryParts);
286
+ }
287
+ }
288
+
289
+ /**
290
+ * @template {Record<string, any>} T
291
+ */
292
+ class FromQuery {
293
+ #database;
294
+ #type;
295
+ #queryParts;
296
+
297
+ /**
298
+ * @param {*} database
299
+ * @param {ObjectTypeOf<T>} type
300
+ * @param {QueryParts<'db' | 'from'>} queryParts
301
+ */
302
+ constructor(database, type, queryParts) {
303
+ this.#database = database;
304
+ this.#type = type;
305
+ this.#queryParts = queryParts;
306
+ }
307
+
308
+ /**
309
+ * @template {{}} U
310
+ * @param {(obj: RowPick<T>) => ObjectPick<U>} fct
311
+ * @returns {ScopedFromQuery<U>}
312
+ */
313
+ scope(fct) {
314
+ const value = fct(rowPick(this.#type));
315
+ const queryParts = {
316
+ ...this.#queryParts,
317
+ scope: value,
318
+ };
319
+ return new ScopedFromQuery(this.#database, value.type, queryParts);
320
+ }
321
+
322
+ /**
323
+ * Select fields
324
+ * @template {(keyof T)[]} F
325
+ * @param {F} fields
326
+ * @returns {SelectQuery<T, flatten<Pick<T, F[number]>>>}
327
+ */
328
+ fields(fields) {
329
+ const row = rowPick(this.#type);
330
+ const queryParts = {
331
+ ...this.#queryParts,
332
+ select: fields.map((fieldName) => row.field(fieldName)),
333
+ };
334
+ return new SelectQuery(this.#database, this.#type, queryParts);
335
+ }
336
+
337
+ /**
338
+ * @template {Record<string, any>} R
339
+ * @param {(obj: RowPick<T>) => {[K in keyof R]: PickOf<R[K]>}} fct
340
+ * @returns {SelectQuery<T, R>}
341
+ */
342
+ select(fct) {
343
+ const selectedFields = fct(rowPick(this.#type));
344
+ const queryParts = {...this.#queryParts, select: selectedFields};
345
+ return new SelectQuery(this.#database, this.#type, queryParts);
346
+ }
347
+
348
+ /**
349
+ * @param {(obj: RowPick<T>, $: typeof operators) => Operator} fct
350
+ * @returns {FromQuery<T>}
351
+ */
352
+ where(fct) {
353
+ const queryParts = {
354
+ ...this.#queryParts,
355
+ where: mergeWhere(
356
+ this.#queryParts.where,
357
+ fct(rowPick(this.#type), operators)
358
+ ),
359
+ };
360
+ return new FromQuery(this.#database, this.#type, queryParts);
361
+ }
362
+ }
363
+
364
+ class DbQuery {
365
+ #database;
366
+ #queryParts;
367
+
368
+ /**
369
+ * @param {*} database
370
+ * @param {QueryParts<'db'>} queryParts
371
+ */
372
+ constructor(database, queryParts) {
373
+ this.#database = database;
374
+ this.#queryParts = queryParts;
375
+ }
376
+
377
+ /**
378
+ * @template {AnyObjectShape} T
379
+ * @param {string} tableName
380
+ * @param {T} shape
381
+ * @returns {FromQuery<t<T>>}
382
+ */
383
+ from(tableName, shape) {
384
+ const type = getTypeInstance(shape);
385
+ const queryParts = {
386
+ ...this.#queryParts,
387
+ from: tableName,
388
+ };
389
+ return /** @type {any} */ (new FromQuery(this.#database, type, queryParts));
390
+ }
391
+ }
392
+
393
+ class QueryBuilder {
394
+ #database;
395
+
396
+ /**
397
+ * @param {*} [database]
398
+ */
399
+ constructor(database) {
400
+ this.#database = database;
401
+ }
402
+
403
+ db(dbName) {
404
+ const queryParts = {
405
+ db: dbName,
406
+ };
407
+ return new DbQuery(this.#database, queryParts);
408
+ }
409
+ }
410
+
411
+ module.exports = {
412
+ QueryBuilder,
413
+ FromQuery,
414
+ ScopedFromQuery,
415
+ };
@@ -0,0 +1,57 @@
1
+ // @ts-check
2
+ /**
3
+ * @typedef {import("./query-builder.js").QueryObj} QueryObj
4
+ */
5
+
6
+ const {sql} = require('./operator-to-sql.js');
7
+ const {isAnyPick} = require('./picks.js');
8
+
9
+ /**
10
+ * @param {QueryObj["select"]} select
11
+ * @param {any[]} values
12
+ * @returns {string}
13
+ */
14
+ function selectFields(select, values) {
15
+ if (Array.isArray(select)) {
16
+ return select.map((rep) => sql(rep.value, values)).join(', ');
17
+ }
18
+ if (isAnyPick(select)) {
19
+ return sql(select.value, values);
20
+ }
21
+ return Object.entries(select)
22
+ .map(([name, rep]) => `${sql(rep.value, values)} AS ${name}`)
23
+ .join(', ');
24
+ }
25
+
26
+ /**
27
+ * @param {NonNullable<QueryObj["orderBy"]>} orderBy
28
+ * @param {any[]} values
29
+ * @returns {string}
30
+ */
31
+ function orderByFields(orderBy, values) {
32
+ if (Array.isArray(orderBy)) {
33
+ return orderBy.map((rep) => sql(rep.value, values)).join(', ');
34
+ }
35
+ return sql(orderBy.value, values);
36
+ }
37
+
38
+ /**
39
+ * @param {QueryObj} query
40
+ * @returns {{sql: string, values: any[]}}
41
+ */
42
+ function queryToSql(query) {
43
+ const values = [];
44
+ let result = `SELECT ${selectFields(query.select, values)}`;
45
+ result += '\n' + `FROM ${query.from}`;
46
+ if (query.where) {
47
+ result += '\n' + `WHERE ${sql(query.where, values)}`;
48
+ }
49
+ if (query.orderBy) {
50
+ result += '\n' + `ORDER BY ${orderByFields(query.orderBy, values)}`;
51
+ }
52
+ return {sql: result, values};
53
+ }
54
+
55
+ module.exports = {
56
+ queryToSql,
57
+ };
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "xcraft-core-pickaxe",
3
+ "version": "0.1.0",
4
+ "description": "Query builder",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "test": "echo \"Error: no test specified\" && exit 1"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/Xcraft-Inc/xcraft-core-pickaxe.git"
12
+ },
13
+ "author": "",
14
+ "license": "MIT",
15
+ "bugs": {
16
+ "url": "https://github.com/Xcraft-Inc/xcraft-core-pickaxe/issues"
17
+ },
18
+ "homepage": "https://github.com/Xcraft-Inc/xcraft-core-pickaxe#readme",
19
+ "dependencies": {
20
+ "xcraft-core-stones": "^0.4.0"
21
+ },
22
+ "devDependencies": {
23
+ "prettier": "2.0.4",
24
+ "xcraft-dev-prettier": "^2.0.0",
25
+ "xcraft-dev-rules": "^4.1.0"
26
+ },
27
+ "prettier": "xcraft-dev-prettier"
28
+ }