w-orm-reladb 1.1.8 → 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 +61 -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 +84 -30
- package/docs/cvFind.mjs.html +349 -382
- package/docs/global.html +49 -12
- 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 +83 -29
- package/src/cvFind.mjs +348 -381
- package/test/api-find-whitelist.mjs +390 -0
- package/test/api-models.test.mjs +23 -0
- package/test/api-mssql.test.mjs +2 -0
- package/test/api-sqlite.test.mjs +2 -0
- package/test/api-suite.mjs +560 -76
- package/test/unit-cvfind.test.mjs +928 -104
package/README.md
CHANGED
|
@@ -53,28 +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`.
|
|
79
|
+
|
|
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.
|
|
75
81
|
|
|
76
82
|
Model files are only read, never rewritten; an `id` column that lacks `primaryKey: true` is treated as the primary key in memory.
|
|
77
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
|
+
|
|
78
111
|
### Concurrency
|
|
79
112
|
|
|
80
113
|
The scope of the atomicity guarantee, by range:
|
|
@@ -244,9 +277,9 @@ async function test() {
|
|
|
244
277
|
console.log('select by $or, $and, $ne, $in, $nin', spc)
|
|
245
278
|
|
|
246
279
|
|
|
247
|
-
//select by
|
|
248
|
-
let sr = await w.select({ name:
|
|
249
|
-
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)
|
|
250
283
|
|
|
251
284
|
|
|
252
285
|
//del
|
|
@@ -297,7 +330,7 @@ test()
|
|
|
297
330
|
// { id: 'id-rosemary', name: 'rosemary(modify)', value: 123.456 },
|
|
298
331
|
// { id: '{random id}', name: 'kettle', value: 456 }
|
|
299
332
|
// ]
|
|
300
|
-
//
|
|
333
|
+
// selectEq [
|
|
301
334
|
// { id: 'id-peter', name: 'peter(modify)', value: 123 }
|
|
302
335
|
// ]
|
|
303
336
|
// change del
|
|
@@ -441,7 +474,7 @@ async function testCommit() {
|
|
|
441
474
|
|
|
442
475
|
|
|
443
476
|
//select all
|
|
444
|
-
let ssBeforeCommit = await w.select(
|
|
477
|
+
let ssBeforeCommit = await w.select({}, connState)
|
|
445
478
|
console.log('select all (before commit)', ssBeforeCommit) //此時select可查到暫時有效的數據
|
|
446
479
|
// => [
|
|
447
480
|
// { id: 'id-peter', name: 'peter(modify)', value: 123 },
|
|
@@ -658,7 +691,7 @@ async function testRollback() {
|
|
|
658
691
|
|
|
659
692
|
|
|
660
693
|
//select all
|
|
661
|
-
let ssBeforeRollback = await w.select(
|
|
694
|
+
let ssBeforeRollback = await w.select({}, connState)
|
|
662
695
|
console.log('select all (before rollback)', ssBeforeRollback) //此時select可查到暫時有效的數據
|
|
663
696
|
// => [
|
|
664
697
|
// { id: 'id-peter', name: 'peter(modify)', value: 123 },
|
|
@@ -881,9 +914,9 @@ async function test() {
|
|
|
881
914
|
console.log('select by $or, $and, $ne, $in, $nin', spc)
|
|
882
915
|
|
|
883
916
|
|
|
884
|
-
//select by
|
|
885
|
-
let sr = await w.select({ name:
|
|
886
|
-
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)
|
|
887
920
|
|
|
888
921
|
|
|
889
922
|
//del
|
|
@@ -934,7 +967,7 @@ test()
|
|
|
934
967
|
// { id: 'id-rosemary', name: 'rosemary(modify)', value: 123.456 },
|
|
935
968
|
// { id: '{random id}', name: 'kettle', value: 456 }
|
|
936
969
|
// ]
|
|
937
|
-
//
|
|
970
|
+
// selectEq [
|
|
938
971
|
// { id: 'id-peter', name: 'peter(modify)', value: 123 }
|
|
939
972
|
// ]
|
|
940
973
|
// change del
|
|
@@ -1085,7 +1118,7 @@ async function testCommit() {
|
|
|
1085
1118
|
|
|
1086
1119
|
|
|
1087
1120
|
//select all
|
|
1088
|
-
let ssBeforeCommit = await w.select(
|
|
1121
|
+
let ssBeforeCommit = await w.select({}, connState)
|
|
1089
1122
|
console.log('select all (before commit)', ssBeforeCommit) //此時select可查到暫時有效的數據
|
|
1090
1123
|
// => [
|
|
1091
1124
|
// { id: 'id-peter', name: 'peter(modify)', value: 123 },
|
|
@@ -1309,7 +1342,7 @@ async function testRollback() {
|
|
|
1309
1342
|
|
|
1310
1343
|
|
|
1311
1344
|
//select all
|
|
1312
|
-
let ssBeforeRollback = await w.select(
|
|
1345
|
+
let ssBeforeRollback = await w.select({}, connState)
|
|
1313
1346
|
console.log('select all (before rollback)', ssBeforeRollback) //此時select可查到暫時有效的數據
|
|
1314
1347
|
// => [
|
|
1315
1348
|
// { id: 'id-peter', name: 'peter(modify)', value: 123 },
|
|
@@ -1539,9 +1572,9 @@ async function test() {
|
|
|
1539
1572
|
console.log('select by $or, $and, $ne, $in, $nin', spc)
|
|
1540
1573
|
|
|
1541
1574
|
|
|
1542
|
-
//select by
|
|
1543
|
-
let sr = await w.select({ name:
|
|
1544
|
-
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)
|
|
1545
1578
|
|
|
1546
1579
|
|
|
1547
1580
|
//del
|
|
@@ -1592,7 +1625,7 @@ test()
|
|
|
1592
1625
|
// { id: 'id-rosemary', name: 'rosemary(modify)', value: 123.456 },
|
|
1593
1626
|
// { id: '{random id}', name: 'kettle', value: 456 }
|
|
1594
1627
|
// ]
|
|
1595
|
-
//
|
|
1628
|
+
// selectEq [
|
|
1596
1629
|
// { id: 'id-peter', name: 'peter(modify)', value: 123 }
|
|
1597
1630
|
// ]
|
|
1598
1631
|
// change del
|