velocious 1.0.652 → 1.0.653
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/README.md +11 -0
- package/build/database/query/model-class-query.js +3 -1
- package/build/database/query/where-hash.js +12 -1
- package/build/database/query/where-in.js +48 -0
- package/build/database/query/where-model-class-hash.js +34 -1
- package/build/src/database/query/model-class-query.js +5 -2
- package/build/src/database/query/where-hash.d.ts.map +1 -1
- package/build/src/database/query/where-hash.js +14 -2
- package/build/src/database/query/where-in.d.ts +22 -0
- package/build/src/database/query/where-in.d.ts.map +1 -0
- package/build/src/database/query/where-in.js +40 -0
- package/build/src/database/query/where-model-class-hash.d.ts +11 -0
- package/build/src/database/query/where-model-class-hash.d.ts.map +1 -1
- package/build/src/database/query/where-model-class-hash.js +33 -2
- package/package.json +1 -1
- package/src/database/query/model-class-query.js +3 -1
- package/src/database/query/where-hash.js +12 -1
- package/src/database/query/where-in.js +48 -0
- package/src/database/query/where-model-class-hash.js +34 -1
package/README.md
CHANGED
|
@@ -1683,9 +1683,20 @@ const tasks = await Task.all().toArray()
|
|
|
1683
1683
|
|
|
1684
1684
|
### Filtering
|
|
1685
1685
|
|
|
1686
|
+
Use `{in: [...]}` for explicit null-aware membership. Mixed lists match either
|
|
1687
|
+
IN members or SQL NULL, grouped locally so sibling and chained filters still
|
|
1688
|
+
apply. Null-only lists use IS NULL; empty lists match nothing. Direct arrays keep
|
|
1689
|
+
their existing driver-specific behavior, including string-null quoting and
|
|
1690
|
+
column collation. See [Query filtering](docs/query-filtering.md) for
|
|
1691
|
+
validation, normalization, negation, relationships and raw-query boundaries.
|
|
1692
|
+
|
|
1686
1693
|
```js
|
|
1687
1694
|
const tasks = await Task.where({status: "open"}).toArray()
|
|
1688
1695
|
|
|
1696
|
+
const nullOrManualTasks = await Task
|
|
1697
|
+
.where({projectId: project.id(), description: {in: [null, "manual"]}})
|
|
1698
|
+
.toArray()
|
|
1699
|
+
|
|
1689
1700
|
const tasksForActiveProjects = await Task.where({
|
|
1690
1701
|
project: {projectDetail: {isActive: true}}
|
|
1691
1702
|
}).toArray()
|
|
@@ -1468,7 +1468,9 @@ function splitWhereHash({hash, modelClass}) {
|
|
|
1468
1468
|
const isNested = isPlainObject(value)
|
|
1469
1469
|
const relationship = getRelationshipByName(modelClass, key)
|
|
1470
1470
|
|
|
1471
|
-
if (isNested) {
|
|
1471
|
+
if (isNested && !relationship && resolveColumnName(modelClass, key)) {
|
|
1472
|
+
resolvedHash[key] = value
|
|
1473
|
+
} else if (isNested) {
|
|
1472
1474
|
if (relationship) {
|
|
1473
1475
|
const rawTargetModelClass = relationship.getTargetModelClass()
|
|
1474
1476
|
if (!rawTargetModelClass) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
3
|
import WhereBase from "./where-base.js"
|
|
4
|
+
import WhereIn from "./where-in.js"
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* VelociousDatabaseQueryWhereHash class.
|
|
@@ -50,7 +51,17 @@ export default class VelociousDatabaseQueryWhereHash extends WhereBase {
|
|
|
50
51
|
if (index > 0) sql += " AND "
|
|
51
52
|
sql += "1=0"
|
|
52
53
|
} else if (!Array.isArray(whereValue) && whereValue !== null && typeof whereValue == "object") {
|
|
53
|
-
|
|
54
|
+
if (tableName && "in" in whereValue) {
|
|
55
|
+
if (index > 0) sql += " AND "
|
|
56
|
+
|
|
57
|
+
sql += WhereIn.toSql({
|
|
58
|
+
columnSql: `${options.quoteTableName(tableName)}.${options.quoteColumnName(whereKey)}`,
|
|
59
|
+
options,
|
|
60
|
+
values: WhereIn.values(whereValue)
|
|
61
|
+
})
|
|
62
|
+
} else {
|
|
63
|
+
sql += this._whereSQLFromHash(whereValue, whereKey, index)
|
|
64
|
+
}
|
|
54
65
|
} else {
|
|
55
66
|
if (index > 0) sql += " AND "
|
|
56
67
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** @typedef {string | number | boolean | null} InValue */
|
|
4
|
+
|
|
5
|
+
export default class WhereIn {
|
|
6
|
+
/**
|
|
7
|
+
* Validates an explicit membership descriptor at a column boundary.
|
|
8
|
+
* @param {unknown} condition - Untrusted column condition, narrowed before use.
|
|
9
|
+
* @returns {InValue[]} - Validated members in a new array.
|
|
10
|
+
*/
|
|
11
|
+
static values(condition) {
|
|
12
|
+
if (condition === null || typeof condition !== "object" ||
|
|
13
|
+
!Object.hasOwn(condition, "in") || !("in" in condition) ||
|
|
14
|
+
Reflect.ownKeys(condition).length !== 1 || !Array.isArray(condition.in)) {
|
|
15
|
+
throw new Error("Invalid IN condition: expected an object with only an own 'in' array")
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** @type {InValue[]} */
|
|
19
|
+
const values = []
|
|
20
|
+
|
|
21
|
+
for (const value of condition.in) {
|
|
22
|
+
if (value !== null && typeof value !== "string" && typeof value !== "boolean" &&
|
|
23
|
+
!(typeof value === "number" && Number.isFinite(value))) {
|
|
24
|
+
throw new Error("Invalid IN condition: members must be strings, finite numbers, booleans or null")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
values.push(value)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return values
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Renders membership without letting its null branch escape sibling filters.
|
|
35
|
+
* @param {{columnSql: string, inColumnSql?: string, values: InValue[], options: import("../query-parser/options.js").default}} args - Quoted column operands, normalized members and driver quoting.
|
|
36
|
+
* @returns {string} - Complete membership predicate.
|
|
37
|
+
*/
|
|
38
|
+
static toSql({columnSql, inColumnSql = columnSql, values, options}) {
|
|
39
|
+
const nonNullValues = values.filter((value) => value !== null)
|
|
40
|
+
const includesNull = values.includes(null)
|
|
41
|
+
|
|
42
|
+
if (nonNullValues.length === 0) return includesNull ? `${columnSql} IS NULL` : "1=0"
|
|
43
|
+
|
|
44
|
+
const membershipSql = `${inColumnSql} IN (${nonNullValues.map((value) => options.quote(value)).join(", ")})`
|
|
45
|
+
|
|
46
|
+
return includesNull ? `(${membershipSql} OR ${columnSql} IS NULL)` : membershipSql
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import * as inflection from "inflection"
|
|
4
4
|
import {isPlainObject} from "is-plain-object"
|
|
5
5
|
import WhereBase from "./where-base.js"
|
|
6
|
+
import WhereIn from "./where-in.js"
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* No match.
|
|
@@ -378,6 +379,34 @@ export default class VelociousDatabaseQueryWhereModelClassHash extends WhereBase
|
|
|
378
379
|
return normalized
|
|
379
380
|
}
|
|
380
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Normalizes explicit membership through the model's column metadata.
|
|
384
|
+
* @param {{modelClass: typeof import("../record/index.js").default, columnName: string, tableName?: string, condition: unknown}} args - Resolved column and untrusted membership descriptor.
|
|
385
|
+
* @returns {string} - Complete column membership predicate.
|
|
386
|
+
*/
|
|
387
|
+
_whereSQLFromInCondition({modelClass, columnName, tableName, condition}) {
|
|
388
|
+
const options = this.getOptions()
|
|
389
|
+
const values = WhereIn.values(condition)
|
|
390
|
+
const normalizedValues = this._normalizeSqliteBooleanValue({columnName, modelClass, value: values})
|
|
391
|
+
/** @type {import("./where-in.js").InValue[] | typeof NO_MATCH} */
|
|
392
|
+
const typedValues = this._normalizeValueForColumnType({columnName, modelClass, value: normalizedValues})
|
|
393
|
+
|
|
394
|
+
if (typedValues === NO_MATCH) return "1=0"
|
|
395
|
+
|
|
396
|
+
const columnSql = tableName
|
|
397
|
+
? `${options.quoteTableName(tableName)}.${options.quoteColumnName(columnName)}`
|
|
398
|
+
: options.quoteColumnName(columnName)
|
|
399
|
+
const columnType = modelClass.getColumnTypeByName(columnName)
|
|
400
|
+
const castText = this.getQuery().driver.getType() === "mssql" && columnType?.toLowerCase() === "text"
|
|
401
|
+
|
|
402
|
+
return WhereIn.toSql({
|
|
403
|
+
columnSql,
|
|
404
|
+
inColumnSql: castText ? `CAST(${columnSql} AS NVARCHAR(MAX))` : columnSql,
|
|
405
|
+
options,
|
|
406
|
+
values: typedValues
|
|
407
|
+
})
|
|
408
|
+
}
|
|
409
|
+
|
|
381
410
|
/**
|
|
382
411
|
* Runs where sqlfrom hash.
|
|
383
412
|
* @param {WhereHash} hash - Hash.
|
|
@@ -400,7 +429,11 @@ export default class VelociousDatabaseQueryWhereModelClassHash extends WhereBase
|
|
|
400
429
|
: null
|
|
401
430
|
const resolvedColumnName = this._resolveColumnName(modelClass, whereKey)
|
|
402
431
|
|
|
403
|
-
if (relationship &&
|
|
432
|
+
if (resolvedColumnName && !relationship && isPlainObject(whereValue)) {
|
|
433
|
+
if (index > 0) sql += " AND "
|
|
434
|
+
|
|
435
|
+
sql += this._whereSQLFromInCondition({columnName: resolvedColumnName, condition: whereValue, modelClass, tableName})
|
|
436
|
+
} else if (relationship && tuples) {
|
|
404
437
|
if (index > 0) sql += " AND "
|
|
405
438
|
|
|
406
439
|
const rawTargetModelClass = relationship.getTargetModelClass()
|