• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

MrSwitch / dare / #85

15 Jun 2026 11:12PM UTC coverage: 70.965%. First build
#85

Pull #463

MrSwitch
docs(README): update
Pull Request #463: feat(engine): sqlite

371 of 529 branches covered (70.13%)

Branch coverage included in aggregate %.

223 of 229 new or added lines in 4 files covered. (97.38%)

3679 of 5178 relevant lines covered (71.05%)

7.26 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.29
/src/format_request.js
1
import SQL, {empty, join, raw} from 'sql-template-tag';
15✔
2
import DareError from './utils/error.js';
15✔
3
import fieldReducer from './format/field_reducer.js';
15✔
4
import groupbyReducer from './format/groupby_reducer.js';
15✔
5
import orderbyReducer from './format/orderby_reducer.js';
15✔
6
import reduceConditions from './format/reducer_conditions.js';
15✔
7
import limitClause from './format/limit_clause.js';
15✔
8
import joinHandler from './format/join_handler.js';
15✔
9
import getFieldAttributes from './utils/field_attributes.js';
15✔
10
import extend from './utils/extend.js';
15✔
11
import buildQuery, {generateSQLSelect} from './get.js';
15✔
12
import toArray from './utils/toArray.js';
15✔
13

15✔
14
/**
15✔
15
 * @import {Sql} from 'sql-template-tag'
15✔
16
 * @import Dare, {QueryOptions} from './index.js'
15✔
17
 */
15✔
18

15✔
19
/**
15✔
20
 * Format Request initiation
15✔
21
 *
15✔
22
 * @param {object} options - Options object
15✔
23
 * @returns {object} Formatted options
15✔
24
 */
15✔
25
export default function (options) {
15✔
26
        return format_request(options, this);
12✔
27
}
12✔
28

15✔
29
/**
15✔
30
 * @typedef {object} SimpleNode
15✔
31
 * @property {string} alias - Alias
15✔
32
 * @property {string} field_alias_path - Field alias path
15✔
33
 * @property {string} table - Table name
15✔
34
 */
15✔
35

15✔
36
/**
15✔
37
 * Format Request
15✔
38
 *
15✔
39
 * @param {QueryOptions} options - Current iteration
15✔
40
 * @param {Dare} dareInstance - Instance of Dare
15✔
41
 * @returns {Promise<QueryOptions>} formatted object with all the joins
15✔
42
 */
15✔
43
async function format_request(options, dareInstance) {
16✔
44
        if (!options) {
16!
45
                throw new DareError(
×
46
                        DareError.INVALID_REQUEST,
×
47
                        `Invalid options '${options}'`
×
48
                );
×
49
        }
×
50

16✔
51
        // Use the alias to find the real table name
16✔
52
        if (!options.alias) {
16✔
53
                const alias = options.table;
12✔
54
                options.alias = alias;
12✔
55
        }
12✔
56

16✔
57
        /*
16✔
58
         * Reject when the table is not provided
16✔
59
         */
16✔
60
        if (!options.table) {
16!
61
                throw new DareError(
×
62
                        DareError.INVALID_REQUEST,
×
63
                        '`table` option is undefined'
×
64
                );
×
65
        }
×
66

16✔
67
        /*
16✔
68
         * Get option settings
16✔
69
         */
16✔
70
        const {conditional_operators_in_value, method, models, state} =
16✔
71
                dareInstance.options;
16✔
72

16✔
73
        /**
16✔
74
         * Assign the state to the options if it is not already defined
16✔
75
         */
16✔
76
        options.state ??= state;
16✔
77

16✔
78
        /*
16✔
79
         * Options name defines the model name
16✔
80
         */
16✔
81
        options.name = dareInstance.table_alias_handler(options.table);
16✔
82

16✔
83
        /*
16✔
84
         * Retrieve the model based upon the model name (alias)
16✔
85
         */
16✔
86
        const model = models?.[options.name] || {};
16✔
87

16✔
88
        /*
16✔
89
         * Set the SQL Table, If the model redefines the table name otherwise use the model Name
16✔
90
         */
16✔
91
        options.sql_table = model.table || options.name;
16✔
92

16✔
93
        /**
16✔
94
         * Hack
16✔
95
         * To resolve mysql bug with aliasing in DELETE operations with a LIMIT https://bugs.mysql.com/bug.php?id=89410
16✔
96
         * Preset the sql_table based upon the actual table name conditions with the same alias will work.
16✔
97
         * TODO [MySQL-5.6/5.7] remove when only supporting MySQL-8
16✔
98
         * TODO [mysql#89410]: Remove the conditional assignment when we're not presetting sql_alias in DELETE operation
16✔
99
         */
16✔
100
        if (options.method === 'del' && !options.parent) {
16✔
101
                options.sql_alias = options.sql_table;
2✔
102
        } else if (
14✔
103
                options.method === 'patch' &&
14✔
104
                !options.parent &&
14✔
105
                !dareInstance.applyAliasesOnUpdate
2✔
106
        ) {
14✔
107
                options.sql_alias = options.sql_table;
2✔
108
        } else {
14✔
109
                /** EOF Hack */
12✔
110
                options.sql_alias = dareInstance.get_unique_alias();
12✔
111
        }
12✔
112

16✔
113
        const {sql_alias} = options;
16✔
114

16✔
115
        /*
16✔
116
         * Call bespoke table handler
16✔
117
         * This may modify the incoming options object, ammend after handler, etc...
16✔
118
         */
16✔
119
        {
16✔
120
                /*
16✔
121
                 * Mutation operations only exist on the root level table
16✔
122
                 * Nested tables should use the get() handler as they only filter
16✔
123
                 */
16✔
124
                const derivedMethod = options.parent ? 'get' : method;
16✔
125

16✔
126
                // If the model does not define the method
16✔
127
                const handler =
16✔
128
                        derivedMethod in model
16✔
129
                                ? model[derivedMethod]
16!
130
                                : // Or use the default model
16✔
131
                                        models?.default?.[derivedMethod];
16!
132

16✔
133
                if (handler) {
16!
134
                        // Trigger the handler which alters the options...
×
135
                        await handler.call(dareInstance, options, dareInstance);
×
136
                }
×
137
        }
16✔
138

16✔
139
        // Get the schema
16✔
140
        const {schema: table_schema = {}} = model;
16✔
141

16✔
142
        /*
16✔
143
         * Apply defaultValues to join
16✔
144
         */
16✔
145
        {
16✔
146
                Object.keys(table_schema).forEach(key => {
16✔
147
                        const {defaultValue} = getFieldAttributes(
4✔
148
                                key,
4✔
149
                                table_schema,
4✔
150
                                dareInstance
4✔
151
                        );
4✔
152

4✔
153
                        /*
4✔
154
                         * Check the defaultValue for the method has been assigned
4✔
155
                         * -> That there is no definition for the value in the filter and join options
4✔
156
                         * -> That we're trying to get their original field names
4✔
157
                         */
4✔
158
                        if (defaultValue !== undefined) {
4!
159
                                // Does the fields exist?
×
160
                                const filterHasKey = Object.keys({
×
161
                                        ...options.filter,
×
162
                                        ...options.join,
×
163
                                })
×
164
                                        .map(
×
165
                                                filterKey =>
×
166
                                                        dareInstance.getFieldKey(filterKey, table_schema) ||
×
167
                                                        filterKey
×
168
                                        )
×
169
                                        .includes(key);
×
170

×
171
                                // If there is no match
×
172
                                if (!filterHasKey) {
×
173
                                        // Extend the join object with the default value
×
174
                                        extend(options, {join: {[key]: defaultValue}});
×
175
                                }
×
176
                        }
×
177
                });
16✔
178
        }
16✔
179

16✔
180
        // Set the prefix if not already
16✔
181
        options.field_alias_path = options.field_alias_path || '';
16✔
182

16✔
183
        const {field_alias_path} = options;
16✔
184

16✔
185
        // Current Path
16✔
186
        const current_path = field_alias_path || `${options.alias}.`;
16✔
187

16✔
188
        // Create a shared object to provide nested objects
16✔
189
        const joined = {};
16✔
190

16✔
191
        /**
16✔
192
         * Extract nested Handler
16✔
193
         * @param {string} propName - Type of item
16✔
194
         * @param {boolean} isArray - Is array, otherwise expect object
16✔
195
         * @param {string} key - Key to extract
16✔
196
         * @param {*} value - Value to extract
16✔
197
         * @returns {void} - Nothing
16✔
198
         */
16✔
199
        function extractJoined(propName, isArray, key, value) {
16✔
200
                if (!joined[key]) {
4✔
201
                        joined[key] = {};
4✔
202
                }
4✔
203

4✔
204
                // Set default...
4✔
205
                joined[key][propName] = joined[key][propName] || (isArray ? [] : {});
4✔
206

4✔
207
                // Handle differently
4✔
208
                if (isArray) {
4✔
209
                        joined[key][propName].push(...value);
2✔
210
                } else {
2✔
211
                        joined[key][propName] = {...joined[key][propName], ...value};
2✔
212
                }
2✔
213
        }
4✔
214

16✔
215
        /** @type {Array<Sql>} */
16✔
216
        const sql_filters = [];
16✔
217

16✔
218
        // Format filters
16✔
219
        if (options.filter) {
16✔
220
                // Filter must be an object with key=>values
6✔
221
                if (typeof options.filter !== 'object') {
6!
222
                        throw new DareError(
×
223
                                DareError.INVALID_REFERENCE,
×
224
                                `The filter property value '${options.filter}' is invalid. Expected a JS object`
×
225
                        );
×
226
                }
×
227

6✔
228
                // Extract nested filters handler
6✔
229
                const extract = extractJoined.bind(null, 'filter', false);
6✔
230

6✔
231
                // Return array of immediate props
6✔
232
                const arr = reduceConditions(options.filter, {
6✔
233
                        extract,
6✔
234
                        sql_alias,
6✔
235
                        sql_table: options.sql_table,
6✔
236
                        table_schema,
6✔
237
                        conditional_operators_in_value,
6✔
238
                        dareInstance,
6✔
239
                });
6✔
240

6✔
241
                // Add to filters
6✔
242
                sql_filters.push(...arr);
6✔
243
        }
6✔
244

16✔
245
        // Format fields
16✔
246
        if (options.fields) {
16✔
247
                // Fields must be an array, or a dictionary (aka object)
4✔
248
                if (typeof options.fields !== 'object') {
4!
249
                        throw new DareError(
×
250
                                DareError.INVALID_REFERENCE,
×
251
                                `The field definition '${options.fields}' is invalid.`
×
252
                        );
×
253
                }
×
254

4✔
255
                // Extract nested fields handler
4✔
256
                const extract = extractJoined.bind(null, 'fields', true);
4✔
257

4✔
258
                // Set reducer options
4✔
259
                const reducer = fieldReducer({
4✔
260
                        field_alias_path,
4✔
261
                        extract,
4✔
262
                        table_schema,
4✔
263
                        dareInstance,
4✔
264
                });
4✔
265

4✔
266
                // Return array of immediate props
4✔
267
                options.fields = toArray(options.fields).reduce(reducer, []);
4✔
268
        }
4✔
269

16✔
270
        /** @type {Array<Sql>} */
16✔
271
        const sql_join_condition = [];
16✔
272

16✔
273
        // Format conditional joins
16✔
274
        if (options.join) {
16!
275
                // Filter must be an object with key=>values
×
276
                if (typeof options.join !== 'object') {
×
277
                        throw new DareError(
×
278
                                DareError.INVALID_REFERENCE,
×
279
                                `The join property value '${options.join}' is invalid, expected an JS object.`
×
280
                        );
×
281
                }
×
282

×
283
                // Is a required join?
×
284
                if ('_required' in options.join) {
×
285
                        // Has _required join?
×
286
                        options.required_join = options.join._required;
×
287

×
288
                        // Filter out _required
×
289
                        delete options.join._required;
×
290
                }
×
291

×
292
                // Extract nested joins handler
×
293
                const extract = extractJoined.bind(null, 'join', false);
×
294

×
295
                // Return array of immediate props
×
296
                const arrJoins = reduceConditions(options.join, {
×
297
                        extract,
×
298
                        sql_alias,
×
NEW
299
                        sql_table: options.sql_table,
×
300
                        table_schema,
×
301
                        conditional_operators_in_value,
×
302
                        dareInstance,
×
303
                });
×
304

×
305
                /*
×
306
                 * Convert root joins to filters...
×
307
                 */
×
308
                if (arrJoins.length && !options.parent) {
×
309
                        sql_filters.push(...arrJoins);
×
310
                } else {
×
311
                        sql_join_condition.push(...arrJoins);
×
312
                }
×
313
        }
×
314

16✔
315
        /**
16✔
316
         * Can we stop here?
16✔
317
         */
16✔
318
        if (
16✔
319
                options.parent &&
16✔
320
                !options.required_join &&
16✔
321
                !options.has_fields &&
16✔
322
                !options.has_filter
2✔
323
        ) {
16!
324
                // Prevent this join from being included.
×
325
                return;
×
326
        }
×
327

16✔
328
        /*
16✔
329
         * Groupby
16✔
330
         * If the content is grouped
16✔
331
         */
16✔
332
        if (options.groupby) {
16!
333
                // Extract nested groupby handler
×
334
                const extract = extractJoined.bind(null, 'groupby', true);
×
335

×
336
                // Set reducer options
×
337
                const reducer = groupbyReducer({current_path, extract});
×
338

×
339
                // Return array of immediate props
×
340
                options.groupby = toArray(options.groupby).reduce(reducer, []);
×
341
        }
×
342

16✔
343
        /*
16✔
344
         * Orderby
16✔
345
         * If the content is ordered
16✔
346
         */
16✔
347
        if (options.orderby) {
16!
348
                // Extract nested orderby handler
×
349
                const extract = extractJoined.bind(null, 'orderby', true);
×
350

×
351
                // Set reducer options
×
352
                const reducer = orderbyReducer({
×
353
                        current_path,
×
354
                        extract,
×
355
                        table_schema,
×
356
                        dareInstance,
×
357
                });
×
358

×
359
                // Return array of immediate props
×
360
                options.orderby = toArray(options.orderby).reduce(reducer, []);
×
361
        }
×
362

16✔
363
        // Set default limit
16✔
364
        {
16✔
365
                const limits = limitClause(options, dareInstance.MAX_LIMIT);
16✔
366
                Object.assign(options, limits);
16✔
367
        }
16✔
368

16✔
369
        // Joins
16✔
370
        {
16✔
371
                /** @type {Array<QueryOptions>} */
16✔
372
                const joins = options.joins || [];
16✔
373

16✔
374
                // Add additional joins which have been derived from nested fields and filters...
16✔
375
                for (const alias in joined) {
16✔
376
                        // Furnish the join table a little more...
4✔
377
                        const join_object = Object.assign(joined[alias], {
4✔
378
                                alias,
4✔
379
                                field_alias_path: `${options.field_alias_path}${alias}.`,
4✔
380
                                table: dareInstance.table_alias_handler(alias),
4✔
381
                        });
4✔
382

4✔
383
                        /*
4✔
384
                         * Join referrencing
4✔
385
                         * Create the join_conditions which link two tables together
4✔
386
                         */
4✔
387
                        const new_join_object = joinHandler(
4✔
388
                                join_object,
4✔
389
                                options,
4✔
390
                                dareInstance
4✔
391
                        );
4✔
392

4✔
393
                        // Reject if the join handler returned a falsy value
4✔
394
                        if (!new_join_object) {
4!
395
                                throw new DareError(
×
396
                                        DareError.INVALID_REFERENCE,
×
397
                                        `Could not understand field '${alias}'`
×
398
                                );
×
399
                        }
×
400

4✔
401
                        // Mark the join object to negate
4✔
402
                        new_join_object.negate = alias.startsWith('-');
4✔
403

4✔
404
                        // Help the GET parser
4✔
405

4✔
406
                        // Does this contain a nested filter, orderby or groupby?
4✔
407
                        join_object.has_filter = new_join_object.has_filter = Boolean(
4✔
408
                                join_object.filter || join_object.orderby || join_object.groupby
4✔
409
                        );
4✔
410

4✔
411
                        // Does this contain nested fields
4✔
412
                        join_object.has_fields = new_join_object.has_fields =
4✔
413
                                !!(Array.isArray(join_object.fields)
4✔
414
                                        ? join_object.fields.length
4✔
415
                                        : join_object.fields);
4✔
416

4✔
417
                        // Update the request with this table join
4✔
418
                        joins.push(new_join_object);
4✔
419
                }
4✔
420

16✔
421
                // Loop through the joins array
16✔
422
                if (joins.length) {
16✔
423
                        // Loop through the joins and pass through the formatter
4✔
424
                        const a = joins.map(async join_object => {
4✔
425
                                // Set the parent
4✔
426
                                join_object.parent = options;
4✔
427

4✔
428
                                // Format join...
4✔
429
                                const formatedObject = await format_request(
4✔
430
                                        join_object,
4✔
431
                                        dareInstance
4✔
432
                                );
4✔
433

4✔
434
                                // If this is present
4✔
435
                                if (formatedObject) {
4✔
436
                                        // The handler may have assigned filters when their previously wasn't any
4✔
437
                                        formatedObject.has_filter ||= Boolean(
4✔
438
                                                formatedObject.filter
4✔
439
                                        );
4✔
440
                                }
4✔
441

4✔
442
                                return formatedObject;
4✔
443
                        });
4✔
444

4✔
445
                        // Add Joins
4✔
446
                        const arr = await Promise.all(a);
4✔
447

4✔
448
                        options._joins = arr.filter(Boolean);
4✔
449
                }
4✔
450
        }
16✔
451

16✔
452
        /*
16✔
453
         * Construct the SQL WHERE Condition
16✔
454
         */
16✔
455

16✔
456
        {
16✔
457
                // Place holder
16✔
458

16✔
459
                // Get nested filters
16✔
460
                if (options._joins) {
16✔
461
                        sql_filters.push(
4✔
462
                                ...options._joins.flatMap(
4✔
463
                                        ({sql_where_conditions}) => sql_where_conditions
4✔
464
                                )
4✔
465
                        );
4✔
466
                }
4✔
467

16✔
468
                // Assign
16✔
469
                /** @type {Array<Sql>} */
16✔
470
                options.sql_where_conditions = sql_filters.filter(Boolean);
16✔
471
        }
16✔
472

16✔
473
        // Initial SQL JOINS reference
16✔
474
        options.sql_joins = [];
16✔
475

16✔
476
        /**
16✔
477
         * Construct the join conditions
16✔
478
         * If this item has a parent, it'll require a join statement with conditions
16✔
479
         */
16✔
480
        if (options.parent) {
16✔
481
                // Join_conditions, defines how a node is linked to its parent
4✔
482
                for (const x in options.join_conditions) {
4✔
483
                        const val = options.join_conditions[x];
4✔
484
                        sql_join_condition.push(
4✔
485
                                raw(
4✔
486
                                        `${options.sql_alias}.${x} = ${options.parent.sql_alias}.${val}`
4✔
487
                                )
4✔
488
                        );
4✔
489
                }
4✔
490

4✔
491
                options.sql_join_condition = join(sql_join_condition, ' AND ');
4✔
492

4✔
493
                // Create the SQL JOIN conditions syntax
4✔
494
                options.sql_joins.push(
4✔
495
                        SQL`JOIN ${raw(options.sql_table)} ${raw(options.sql_alias)} ON (${
4✔
496
                                options.sql_join_condition
4✔
497
                        })`
4✔
498
                );
4✔
499
        }
4✔
500

16✔
501
        // Add nested joins
16✔
502
        if (Array.isArray(options._joins)) {
16✔
503
                // Update sql_joins
4✔
504
                options.sql_joins.push(
4✔
505
                        ...options._joins
4✔
506
                                .flatMap(({sql_joins}) => sql_joins)
4✔
507
                                .filter(Boolean)
4✔
508
                );
4✔
509
        }
4✔
510

16✔
511
        /**
16✔
512
         * Negate
16✔
513
         * NOT EXIST (SELECT 1 FROM alias WHERE join_conditions)
16✔
514
         */
16✔
515
        if (options.negate || options.parent?.forceSubquery) {
16✔
516
                // Mark as another subquery
2✔
517
                let sql_where_conditions = [];
2✔
518

2✔
519
                const sql_negate = options.negate ? raw('NOT') : empty;
2!
520

2✔
521
                if (method === 'get') {
2!
522
                        // Get queries can be much simpler, we're allowed to use the same table in an exist statement like...
×
523
                        options.is_subquery = true;
×
524

×
525
                        // Create sub_query
×
526
                        const sub_query = buildQuery(options, dareInstance);
×
527
                        // Create the SQL
×
528
                        const sql_sub_query = generateSQLSelect(sub_query);
×
529

×
530
                        sql_where_conditions = [
×
531
                                SQL`${sql_negate} EXISTS (${sql_sub_query})`,
×
532
                        ];
×
533
                } else {
2✔
534
                        /*
2✔
535
                         * Whilst patch and delete will throw an ER_UPDATE_TABLE_USED error
2✔
536
                         * The query must not reference the table, so we need to be quite sneaky
2✔
537
                         */
2✔
538

2✔
539
                        const parentReferences = Object.values(options.join_conditions).map(
2✔
540
                                val => `${options.parent.sql_alias}.${val}`
2✔
541
                        );
2✔
542

2✔
543
                        // Create sub_query
2✔
544
                        options.fields = Object.keys(options.join_conditions);
2✔
545
                        options.limit = null; // MySQL 5.6 doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'
2✔
546
                        options.many = null; // Do not attempt to CONCAT the fields
2✔
547
                        options.field_alias_path = ''; // Do not prefix the fields labels
2✔
548
                        options.parent = null; // Do not add superfluous joins
2✔
549

2✔
550
                        const sub_query = buildQuery(options, dareInstance);
2✔
551
                        const sql_sub_query = generateSQLSelect(sub_query);
2✔
552

2✔
553
                        sql_where_conditions = [
2✔
554
                                SQL`${raw(parentReferences[0])}
2✔
555
                                ${sql_negate} IN (
2✔
556
                                        SELECT ${join(options.fields.map(field => raw(String(field))))} FROM (
2✔
557
                                                ${sql_sub_query}
2✔
558
                                        ) AS ${raw(options.sql_alias)}_tmp
2✔
559
                                )
2✔
560
                        `,
2✔
561
                        ];
2✔
562
                }
2✔
563

2✔
564
                // Update the filters
2✔
565
                return {
2✔
566
                        sql_where_conditions,
2✔
567
                };
2✔
568
        }
2✔
569

14✔
570
        return options;
14✔
571
}
16✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc