w-orm-reladb 1.1.9 → 1.1.10
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 +59 -28
- package/dist/w-orm-reladb.umd.js +2 -2
- package/dist/w-orm-reladb.umd.js.map +1 -1
- package/docs/WOrmReladb.html +52 -30
- package/docs/WOrmReladb.mjs.html +59 -28
- package/docs/cvFind.mjs.html +127 -405
- package/docs/global.html +14 -11
- package/docs/importModels.mjs.html +1 -1
- package/docs/index.html +1 -1
- package/g-mssql-transaction-commit.mjs +1 -1
- package/g-mssql-transaction-rollback.mjs +1 -1
- package/g-mssql.mjs +4 -4
- package/g-sqlite-encryption-transaction-commit.mjs +1 -1
- package/g-sqlite-encryption.mjs +4 -4
- package/g-sqlite-transaction-commit.mjs +1 -1
- package/g-sqlite-transaction-rollback.mjs +1 -1
- package/g-sqlite.mjs +4 -4
- package/models/tfind.js +50 -0
- package/package.json +2 -2
- package/src/WOrmReladb.mjs +58 -27
- package/src/cvFind.mjs +126 -404
- package/test/api-find-whitelist.mjs +390 -0
- package/test/api-mssql.test.mjs +2 -0
- package/test/api-sqlite.test.mjs +2 -0
- package/test/api-suite.mjs +95 -136
- package/test/unit-cvfind.test.mjs +241 -218
package/README.md
CHANGED
|
@@ -53,30 +53,61 @@ The all-or-nothing guarantee is provided by a transaction, not by the batch stat
|
|
|
53
53
|
|
|
54
54
|
Note also that a plain call made without `option.instance` opens and closes its own connection, so it ends any instance you are holding. Do not mix the two styles against the same `WOrmReladb` object.
|
|
55
55
|
|
|
56
|
-
###
|
|
56
|
+
### Condition (`find`) whitelist for `select` and `delAll`
|
|
57
57
|
|
|
58
|
-
`select(find)` and `delAll(find)`
|
|
58
|
+
`select(find)` and `delAll(find)` accept the same series wide whitelist (shared with the other `w-orm-*` packages through `cvOrmFind` of `wsemi`) and translate it through one function, so **the rows `delAll` deletes are exactly the rows `select` returns** for the same `find`. Anything outside the whitelist **rejects as a whole** (and emits `error`) before the model files are loaded and before a connection is made or any SQL is sent: nothing is ignored, reinterpreted or deleted. The error message is `invalid find: <reason> at [<path>]`; the shared whitelist check never echoes the offending value (the column type check described below may quote it).
|
|
59
59
|
|
|
60
60
|
| Form | Meaning |
|
|
61
61
|
|---|---|
|
|
62
|
-
| `
|
|
63
|
-
|
|
|
64
|
-
| `$
|
|
65
|
-
| `$
|
|
66
|
-
| `$
|
|
67
|
-
| `$regex` with `$options: 'i'` | a **literal substring** match; `%` `_` `[` are matched literally |
|
|
68
|
-
| `$and` `$or` `$nor` | arrays of conditions; `$nor: [a, b]` is `NOT (a OR b)` |
|
|
62
|
+
| `find` omitted, `undefined` or `{}` | all rows (for `delAll`: **every row is deleted**, see below) |
|
|
63
|
+
| `{ col: value }` | equality; several columns are combined with AND; the value is a string (no NUL, no lone surrogate), a finite number, a boolean, a valid `Date`, or `null` (`IS NULL`) |
|
|
64
|
+
| `$eq` `$ne` `$gt` `$gte` `$lt` `$lte` | comparison with a scalar (`null` only for `$eq` / `$ne`); several operators on one column are combined with AND |
|
|
65
|
+
| `$in` `$nin` | an array of scalars (no `null`); `$in: []` matches nothing, `$nin: []` rejects |
|
|
66
|
+
| `$and` `$or` | a non empty array of non empty condition objects, **at most 999 items** (SQLite fails at 1000 flat items; 999 is the shared limit for the packages running on SQLite) |
|
|
69
67
|
|
|
70
|
-
|
|
68
|
+
Limits: nesting depth 32, `$in` / `$nin` at most 10000 values, at most 100000 nodes, column names of 1 to 128 characters matching `^[\p{L}_][\p{L}\p{M}\p{N}_]*$` (so no dots, quotes, hyphens, spaces or a leading `$`).
|
|
71
69
|
|
|
72
|
-
|
|
70
|
+
Everything else rejects, among them: a `find` that is not a plain condition object (`null`, an array, a string, a number, a boolean, a function, a `Date`, a `RegExp`, a `Map`, a class instance …, which are **never** treated as `{}`), `$not` `$nor` `$regex` `$options` `$exists` `$where` `$expr` `$like` `$ilike` and any other operator, `$and` / `$or` inside a column, sequelize `Op` symbols, a `RegExp` value, an array as a column value (write `$in`), `undefined`, functions, symbols, `BigInt`, `Buffer`, `NaN`, `±Infinity` and invalid `Date` values, symbol keys, getters and non enumerable keys, sparse arrays or arrays with extra properties, empty operator objects, empty `$and` / `$or` or a `{}` inside them.
|
|
73
71
|
|
|
74
|
-
|
|
72
|
+
On top of the whitelist this package checks, and rejects what cannot be translated with its meaning intact: values that do not fit the column type (for example `'abc'` or `'1e3'` against an integer column, a decimal string with more fractional digits than the column's scale, a date string without a time zone such as `'2020-01-01'` — use a `Date`, an epoch number or `'2020-01-01T00:00:00Z'` — or an integer beyond `Number.MAX_SAFE_INTEGER`, which should be passed as a decimal string instead) and column names that the model does not list (see below).
|
|
73
|
+
|
|
74
|
+
Differences from MongoDB you should know: `NULL` follows SQL three valued logic, so `$ne` and `$nin` do not match rows whose column is `NULL`. To say it explicitly use the portable forms (they return the same rows on every `w-orm-*` package): leave `NULL` out with `{ $and: [{ f: { $ne: null } }, { f: { $ne: x } }] }`, or keep it with `{ $or: [{ f: null }, { f: { $ne: x } }] }`. Equality and comparison of strings follow the database collation (sqlite compares binary and is case sensitive, the default collation of mssql is case insensitive), and there is no pattern matching: to search for a substring, `select` and filter in memory.
|
|
75
|
+
|
|
76
|
+
**`delAll` needs care.** `delAll()`, `delAll(undefined)` and `delAll({})` delete every row without asking; only a condition makes it a partial delete. Use it sparingly and prefer `del` for single rows; a `find` that is not a condition object rejects instead of deleting everything. Check what a condition matches with `select(find)` first. If the condition comes from a variable that may be `undefined`, `null` or an empty array, decide that yourself before calling: `undefined` values and `$nin: []` reject (they are never ignored), `$in: []` matches nothing.
|
|
77
|
+
|
|
78
|
+
Column names in a `find` must match the model exactly: the attribute name, or the database column name (`field`) of exactly one attribute. Any other spelling is rejected with `unknown column name`: a different letter case, full width forms, a column the model does not list (sqlite's hidden `rowid` included) or a virtual attribute. Databases resolve identifiers with their own collation rules (the default collation of mssql treats `id`, `ID`, `ID`, `id ` and `ɪd` as the same column) and the ORM rewrites names before they reach the database, so a spelling that is not an exact match could slip past the type checks above (`{ VALUE: { $lt: 'abc' } }` used to delete every non `NULL` row on sqlite); the error message suggests the closest name. Names that the ORM rewrites or re-interprets are rejected with `unsupported column name`: names outside the grammar above, names that are members of `Object.prototype` (such as `constructor`, `valueOf`), an attribute whose `field` is set but is not a string, or whose database column name is empty, contains quote characters, brackets or dots, or starts and ends with `$`; an attribute whose column is also the name of another attribute, and an attribute that shares its column with an attribute defined later (only the last one defined of a shared column is accepted), are rejected as `ambiguous column name`, because the ORM would serialize the value with the type of the other attribute, or the type of the column is decided by another one. The `attrs` you hand to `cvFind` must be the `rawAttributes` of the model that runs the query (a plain object without symbol keys and with enumerable keys only), otherwise `cvFind` throws `invalid ctx`. `delAll` is also refused for a model whose `defaultScope` has `limit`, `offset`, `group`, `having`, a non-empty `include`, or a `where` that uses `Op.and` (`select` and `delete` would not touch the same rows). The model must therefore list every column you query (a model generated by `genModelsByTabs` does). A name that matches nothing is rejected before the database is reached, an empty `$in` included (`delAll({ misspelled: { $in: [] } })` rejects instead of matching nothing silently). A `DATE` value (a `Date`, an epoch number, truncated to whole milliseconds, or an ISO string with a time zone) must fall in the years 1 to 9999 (UTC) and is always sent as a `Date`.
|
|
75
79
|
|
|
76
80
|
A primary key value whose type does not fit the primary key column (for example `'abc'` or `12.4` for an integer key; a number for a string key is converted to its string form instead) is an invalid key: `selectByPk` returns `null`, `del` reports that row with `ok: 0`, and `insert`, `insertBulk` and `save` reject without writing anything.
|
|
77
81
|
|
|
78
82
|
Model files are only read, never rewritten; an `id` column that lacks `primaryKey: true` is treated as the primary key in memory.
|
|
79
83
|
|
|
84
|
+
### Breaking changes: condition whitelist
|
|
85
|
+
|
|
86
|
+
The condition syntax was cut down to the whitelist above (series wide, issue "condition whitelist"). The "before" column is what the previous release (1.1.9) did, checked by running it on sqlite against six rows; there is no transition period.
|
|
87
|
+
|
|
88
|
+
| `find` | before (1.1.9) | now |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `null`, `[]`, `[{ id: 'a' }]`, a string, a number, `true`, a function, a `BigInt`, a `Symbol`, a `Proxy` around `{}` as the whole `find` | `select` returned **every row** and `delAll` **deleted every row** (treated as `{}`) | rejects: `invalid find: condition must be a plain object …` (a `Proxy` around `{}`: `an empty find must be an ordinary object`) |
|
|
91
|
+
| `$not`, `$nor`, `$exists` | supported | rejects (`unsupported operator`); express it with `$ne` / `$nin` / `$and` / `$or` (see the portable forms above) |
|
|
92
|
+
| `$regex` with `$options`, a `RegExp` value | literal substring match through `LIKE` | rejects; `select` and filter in memory |
|
|
93
|
+
| an array as a column value, `{ col: [1, 2] }` | treated as `$in` (a `null` inside included) | rejects: `array value is not supported, use $in`; write `{ col: { $in: [1, 2] } }` |
|
|
94
|
+
| sequelize `Op` symbols as keys (`Op.or`, `Op.in`, `Op.between`, `Op.like`, `Op.and`, `Op.not` …) | accepted | rejects: `symbol key is not allowed` |
|
|
95
|
+
| `null` inside `$in` | accepted (`IN (…) OR IS NULL`) | rejects; use `{ $or: [{ col: null }, { col: { $in: […] } }] }` |
|
|
96
|
+
| `null` inside `$nin` | accepted (`NOT IN (…) AND IS NOT NULL`) | rejects; drop the `null`: `{ col: { $nin: […] } }` already skips `NULL` rows (SQL three valued logic); for `$nin: [null]` alone use `{ col: { $ne: null } }` |
|
|
97
|
+
| `$nin: []` | matched **every row** (`delAll` deleted all) | rejects |
|
|
98
|
+
| `$and: [{}]`, `$or: [{}]` | matched **every row** | rejects: `empty condition` |
|
|
99
|
+
| a getter or non enumerable property, an array with extra properties, a string with a lone surrogate | accepted (the getter was called) | rejects |
|
|
100
|
+
| a `Buffer` value on a column the model does not classify (for example `BLOB`) | accepted (`= X'…'`) | rejects: `unsupported value type` |
|
|
101
|
+
| a string containing NUL | database error | rejects |
|
|
102
|
+
| `$and` / `$or` with 999 items | `select` failed on sqlite with `Expression tree is too large` while `delAll` went through | accepted (the translation turns long lists, and columns listed side by side, into balanced trees, so nesting and length do not add up to the database limits), the limit is 999 items |
|
|
103
|
+
| `$and` / `$or` with more than 999 items | database error on sqlite | rejects before the database is reached |
|
|
104
|
+
| `$in` / `$nin` with more than 10000 values | accepted | rejects |
|
|
105
|
+
| a chain of 32 nested `$and` levels | rejects (`find is too deep`) | accepted (the limit is 32 levels) |
|
|
106
|
+
| a column name outside `^[\p{L}_][\p{L}\p{M}\p{N}_]*$` (for example `p$x`) | accepted when the model listed that column | rejects: `unsupported column name` |
|
|
107
|
+
| the same object repeated so that the condition expands exponentially | database error or the process ran out of memory | rejects: `condition is too large` |
|
|
108
|
+
|
|
109
|
+
`select(null, connState)` and `delAll(null)` style calls must become `select({}, connState)` / `delAll({})` (or no argument). The options `ctx.escape` and `ctx.dialect` of `cvFind` are no longer used (they were only needed by `$regex`) and are ignored.
|
|
110
|
+
|
|
80
111
|
### Concurrency
|
|
81
112
|
|
|
82
113
|
The scope of the atomicity guarantee, by range:
|
|
@@ -246,9 +277,9 @@ async function test() {
|
|
|
246
277
|
console.log('select by $or, $and, $ne, $in, $nin', spc)
|
|
247
278
|
|
|
248
279
|
|
|
249
|
-
//select by
|
|
250
|
-
let sr = await w.select({ name:
|
|
251
|
-
console.log('
|
|
280
|
+
//select by equality (there is no pattern matching: filter in memory for substrings)
|
|
281
|
+
let sr = await w.select({ name: 'peter(modify)' })
|
|
282
|
+
console.log('selectEq', sr)
|
|
252
283
|
|
|
253
284
|
|
|
254
285
|
//del
|
|
@@ -299,7 +330,7 @@ test()
|
|
|
299
330
|
// { id: 'id-rosemary', name: 'rosemary(modify)', value: 123.456 },
|
|
300
331
|
// { id: '{random id}', name: 'kettle', value: 456 }
|
|
301
332
|
// ]
|
|
302
|
-
//
|
|
333
|
+
// selectEq [
|
|
303
334
|
// { id: 'id-peter', name: 'peter(modify)', value: 123 }
|
|
304
335
|
// ]
|
|
305
336
|
// change del
|
|
@@ -443,7 +474,7 @@ async function testCommit() {
|
|
|
443
474
|
|
|
444
475
|
|
|
445
476
|
//select all
|
|
446
|
-
let ssBeforeCommit = await w.select(
|
|
477
|
+
let ssBeforeCommit = await w.select({}, connState)
|
|
447
478
|
console.log('select all (before commit)', ssBeforeCommit) //此時select可查到暫時有效的數據
|
|
448
479
|
// => [
|
|
449
480
|
// { id: 'id-peter', name: 'peter(modify)', value: 123 },
|
|
@@ -660,7 +691,7 @@ async function testRollback() {
|
|
|
660
691
|
|
|
661
692
|
|
|
662
693
|
//select all
|
|
663
|
-
let ssBeforeRollback = await w.select(
|
|
694
|
+
let ssBeforeRollback = await w.select({}, connState)
|
|
664
695
|
console.log('select all (before rollback)', ssBeforeRollback) //此時select可查到暫時有效的數據
|
|
665
696
|
// => [
|
|
666
697
|
// { id: 'id-peter', name: 'peter(modify)', value: 123 },
|
|
@@ -883,9 +914,9 @@ async function test() {
|
|
|
883
914
|
console.log('select by $or, $and, $ne, $in, $nin', spc)
|
|
884
915
|
|
|
885
916
|
|
|
886
|
-
//select by
|
|
887
|
-
let sr = await w.select({ name:
|
|
888
|
-
console.log('
|
|
917
|
+
//select by equality (there is no pattern matching: filter in memory for substrings)
|
|
918
|
+
let sr = await w.select({ name: 'peter(modify)' })
|
|
919
|
+
console.log('selectEq', sr)
|
|
889
920
|
|
|
890
921
|
|
|
891
922
|
//del
|
|
@@ -936,7 +967,7 @@ test()
|
|
|
936
967
|
// { id: 'id-rosemary', name: 'rosemary(modify)', value: 123.456 },
|
|
937
968
|
// { id: '{random id}', name: 'kettle', value: 456 }
|
|
938
969
|
// ]
|
|
939
|
-
//
|
|
970
|
+
// selectEq [
|
|
940
971
|
// { id: 'id-peter', name: 'peter(modify)', value: 123 }
|
|
941
972
|
// ]
|
|
942
973
|
// change del
|
|
@@ -1087,7 +1118,7 @@ async function testCommit() {
|
|
|
1087
1118
|
|
|
1088
1119
|
|
|
1089
1120
|
//select all
|
|
1090
|
-
let ssBeforeCommit = await w.select(
|
|
1121
|
+
let ssBeforeCommit = await w.select({}, connState)
|
|
1091
1122
|
console.log('select all (before commit)', ssBeforeCommit) //此時select可查到暫時有效的數據
|
|
1092
1123
|
// => [
|
|
1093
1124
|
// { id: 'id-peter', name: 'peter(modify)', value: 123 },
|
|
@@ -1311,7 +1342,7 @@ async function testRollback() {
|
|
|
1311
1342
|
|
|
1312
1343
|
|
|
1313
1344
|
//select all
|
|
1314
|
-
let ssBeforeRollback = await w.select(
|
|
1345
|
+
let ssBeforeRollback = await w.select({}, connState)
|
|
1315
1346
|
console.log('select all (before rollback)', ssBeforeRollback) //此時select可查到暫時有效的數據
|
|
1316
1347
|
// => [
|
|
1317
1348
|
// { id: 'id-peter', name: 'peter(modify)', value: 123 },
|
|
@@ -1541,9 +1572,9 @@ async function test() {
|
|
|
1541
1572
|
console.log('select by $or, $and, $ne, $in, $nin', spc)
|
|
1542
1573
|
|
|
1543
1574
|
|
|
1544
|
-
//select by
|
|
1545
|
-
let sr = await w.select({ name:
|
|
1546
|
-
console.log('
|
|
1575
|
+
//select by equality (there is no pattern matching: filter in memory for substrings)
|
|
1576
|
+
let sr = await w.select({ name: 'peter(modify)' })
|
|
1577
|
+
console.log('selectEq', sr)
|
|
1547
1578
|
|
|
1548
1579
|
|
|
1549
1580
|
//del
|
|
@@ -1594,7 +1625,7 @@ test()
|
|
|
1594
1625
|
// { id: 'id-rosemary', name: 'rosemary(modify)', value: 123.456 },
|
|
1595
1626
|
// { id: '{random id}', name: 'kettle', value: 456 }
|
|
1596
1627
|
// ]
|
|
1597
|
-
//
|
|
1628
|
+
// selectEq [
|
|
1598
1629
|
// { id: 'id-peter', name: 'peter(modify)', value: 123 }
|
|
1599
1630
|
// ]
|
|
1600
1631
|
// change del
|