w-orm-reladb 1.1.9 → 1.1.11

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 CHANGED
@@ -53,30 +53,102 @@ 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
- ### Conditions (`find`) for `select` and `delAll`
56
+ ### Condition (`find`) whitelist for `select` and `delAll`
57
57
 
58
- `select(find)` and `delAll(find)` translate `find` through one shared function, so **the rows `delAll` deletes are exactly the rows `select` returns** for the same `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
- | `{ col: value }` | equality; `null` means `IS NULL`; an array means `$in` |
63
- | `$eq` `$ne` `$gt` `$gte` `$lt` `$lte` | comparison; the value must be a string, finite number, boolean, `Date` or (for `$eq` / `$ne`) `null` |
64
- | `$in` `$nin` | an array of the values above; `null` inside matches / excludes `NULL` like `{ col: null }` does |
65
- | `$not` | `{ col: { $not: { $gt: 2 } } }` becomes `NOT (col > 2)` |
66
- | `$exists` | `true` is `IS NOT NULL`, `false` is `IS NULL` |
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
- Anything that cannot be translated with its meaning intact **rejects** (and emits `error`) instead of being ignored: unknown operators (including sequelize names such as `$like` and a top level `$not`), objects or arrays where a value is expected, 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), `$regex` patterns containing unescaped regular expression metacharacters (escape them, e.g. `'a\\.c'`), and, on sqlite, `$options: 'i'` with non ASCII letters (sqlite's `LOWER` folds ASCII only). sequelize's own `Op` symbols are still accepted.
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
- Differences from MongoDB you should know: `NULL` follows SQL three valued logic, so `$ne`, `$nin`, `$not` and `$nor` do not match rows whose column is `NULL`; a plain array value is treated as `$in`; without `i`, case sensitivity follows the database collation (sqlite's `LIKE` and mssql's default collation are case insensitive). A `find` that is not an object (`null`, an array, a string) is treated as `{}`, i.e. all rows.
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
- 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, trailing spaces, 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`: empty names, names containing quote characters, brackets or dots, names that start and end with `$` (even with a line break in between, or a lone `$`), names that are members of `Object.prototype` (or `__proto__`), an attribute whose `field` is set but is not a string, and a column starting with `*` under `$regex` with `i`; 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`. In the native operators, `Op.values` and `Op.join` are rejected (the ORM does not know them as comparators), and `Op.between` / `Op.notBetween` take an array of exactly two values. `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` / `$nin` included (`delAll({ misspelled: { $nin: [] } })` rejects instead of matching every row), and so is an operator object that yields no condition (`{ s: { $options: undefined } }`). 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`; `Op.startsWith`, `Op.endsWith` and `Op.substring` are rejected on a `DATE` column.
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
+
111
+ ### Dates on write: years 1 to 9999
112
+
113
+ `insert`, `insertBulk` and `save` **reject** a `DATE` or `DATEONLY` value that is not a valid time in the years 1 to 9999, on every dialect. Why: sqlite stores a date as text (`'2026-01-01 00:00:00.000 +00:00'`), and a year outside the range gives text that does not sort like the time it stands for. `new Date(8.64e15)`, the usual "never expires" sentinel, is stored as `'275760-09-13 …'`, which sorts *before* `'3000-…'`, so `delAll({ d: { $lt: someDate } })` deleted it and `$gt` missed it. mssql (`datetimeoffset`, `date`) only covers years 1 to 9999 anyway and fails in the database; this package now refuses before sending, so both dialects behave the same.
114
+
115
+ | Column | The value is checked as | Rejected examples | Accepted examples |
116
+ |---|---|---|---|
117
+ | `DATE` | the time it stands for (a `Date`, an epoch number, a date string, whatever `new Date(value)` accepts), by its UTC year | `new Date(8.64e15)`, `new Date(-8.64e15)`, `new Date('+010000-01-01T00:00:00Z')`, `'0000-12-31T23:59:59.999Z'`, `8.64e15` | `new Date('0001-01-01T00:00:00.000Z')`, `new Date('9999-12-31T23:59:59.999Z')`, `null` |
118
+ | `DATEONLY` | the `YYYY-MM-DD` text that is actually stored (the calendar year of the host time zone for a `Date`) | the same values as above, `'+010000-01-01'`, `'0000-12-31'` | `'0001-01-01'`, `'9999-12-31'`, `'2026-06-15'` |
119
+
120
+ The whole call rejects (`save` too: it does not turn this into a per row `ok: 0`) and **nothing is written**, even for the valid rows of the same batch. The error is emitted once as the `error` event before the promise rejects, and no `change` event is emitted. The message never quotes the value, it names the position and the column of the first offender (in the order the model lists its attributes):
121
+
122
+ ```alias
123
+ invalid data: date is not a valid time in years 1-9999 at [data[1].expireAt]
124
+ ```
125
+
126
+ For a "never expires" marker write `null` (the usual choice, and `{ expireAt: null }` is queryable), or `new Date('9999-12-31T23:59:59.999Z')`.
127
+
128
+ A value that is not a valid time at all is rejected the same way: `new Date(NaN)`, `NaN`, `Infinity`, `''`, `false`, `'garbage'`, an epoch beyond ±8.64e15, a `BigInt` or a `Symbol` (only `0`, the epoch, is a valid falsy value). sqlite would store the text `Invalid date`, which sorts after every year, so `$gt` and `delAll` would match it by mistake, and mssql fails in the database. Objects that sequelize converts more than once (`Date` subclasses, `new Number(…)`, arrays, objects with their own `valueOf` / `toString`) are evaluated once and replaced in the copy of your data by the checked value, so what is checked is exactly what is stored. `Sequelize.fn` / `literal` / `col` values are SQL expressions you chose and are not checked. Only the value given for the attribute is checked: a custom `set` function of the model that rewrites it afterwards is not.
129
+
130
+ **Rows written before this version are not migrated or read differently.** A row that already holds an out of range date (sqlite text such as `275760-09-13 …`, `-271821-…`, `10000-…`, `0000-…`) still sorts wrongly: conditions on that column can match it by mistake or miss it, and `delAll` follows the same comparison. Clean it up yourself, for example by reading the rows without a date condition and writing `null` (or the 9999 value above) over the stored text:
131
+
132
+ ```alias
133
+ let rows = await w.select({})
134
+ // sqlite returns the stored text; a well formed date starts with a 4 digit year from 0001
135
+ let bad = rows.filter((v) => typeof v.expireAt === 'string' && (!/^\d{4}-/.test(v.expireAt) || v.expireAt.startsWith('0000-')))
136
+ await w.save(bad.map((v) => ({ id: v.id, expireAt: null })))
137
+ ```
138
+
139
+ ### Breaking changes: dates on write
140
+
141
+ Checked by running the previous release (1.1.10) on sqlite.
142
+
143
+ | Written value (`DATE` / `DATEONLY` column) | before (1.1.10) | now |
144
+ |---|---|---|
145
+ | `new Date(8.64e15)`, `new Date(-8.64e15)`, `8.64e15`, `-8.64e15`, `'+010000-01-01T00:00:00Z'`, `'0000-12-31T23:59:59.999Z'` | stored as the text `275760-09-13 …`, `-271821-04-20 …`, `10000-01-01 …`, `0000-12-31 …` (in a `DATEONLY` column `275760-09-13`, `Invalid date`, `10000-01-01`, `0000-12-31`) | `insert` / `insertBulk` / `save` reject as a whole (`invalid data: date is not a valid time in years 1-9999 at [data[i].column]`), nothing is written |
146
+ | the same values on mssql | the database failed the conversion (`Conversion failed when converting date and/or time from character string.`): `insert` rejected but kept the valid rows before the offender, `insertBulk` rejected and wrote nothing, `save` resolved with `ok: 0` for that row and wrote the others | rejected before sending as a whole, nothing is written, with the same message |
147
+ | years 1 to 9999 (`0001-01-01T00:00:00.000Z` … `9999-12-31T23:59:59.999Z`) | stored | stored (unchanged) |
148
+ | `new Date(NaN)`, `NaN`, `''`, `false`, `'garbage'`, `1e16`, a `BigInt`, a `Symbol` | sqlite: stored as the text `Invalid date` (it sorts after every year); mssql: the database failed the conversion | rejected as a whole, nothing is written (`0`, the epoch, stays valid) |
149
+
150
+ `inDateRange` and `genDateChecker` are new exports of `src/cvFind.mjs`; the condition side (`select` / `delAll`) and the write side use the same range test.
151
+
80
152
  ### Concurrency
81
153
 
82
154
  The scope of the atomicity guarantee, by range:
@@ -246,9 +318,9 @@ async function test() {
246
318
  console.log('select by $or, $and, $ne, $in, $nin', spc)
247
319
 
248
320
 
249
- //select by regex
250
- let sr = await w.select({ name: { $regex: 'PeT', $options: 'i' } })
251
- console.log('selectReg', sr)
321
+ //select by equality (there is no pattern matching: filter in memory for substrings)
322
+ let sr = await w.select({ name: 'peter(modify)' })
323
+ console.log('selectEq', sr)
252
324
 
253
325
 
254
326
  //del
@@ -299,7 +371,7 @@ test()
299
371
  // { id: 'id-rosemary', name: 'rosemary(modify)', value: 123.456 },
300
372
  // { id: '{random id}', name: 'kettle', value: 456 }
301
373
  // ]
302
- // selectReg [
374
+ // selectEq [
303
375
  // { id: 'id-peter', name: 'peter(modify)', value: 123 }
304
376
  // ]
305
377
  // change del
@@ -443,7 +515,7 @@ async function testCommit() {
443
515
 
444
516
 
445
517
  //select all
446
- let ssBeforeCommit = await w.select(null, connState)
518
+ let ssBeforeCommit = await w.select({}, connState)
447
519
  console.log('select all (before commit)', ssBeforeCommit) //此時select可查到暫時有效的數據
448
520
  // => [
449
521
  // { id: 'id-peter', name: 'peter(modify)', value: 123 },
@@ -660,7 +732,7 @@ async function testRollback() {
660
732
 
661
733
 
662
734
  //select all
663
- let ssBeforeRollback = await w.select(null, connState)
735
+ let ssBeforeRollback = await w.select({}, connState)
664
736
  console.log('select all (before rollback)', ssBeforeRollback) //此時select可查到暫時有效的數據
665
737
  // => [
666
738
  // { id: 'id-peter', name: 'peter(modify)', value: 123 },
@@ -883,9 +955,9 @@ async function test() {
883
955
  console.log('select by $or, $and, $ne, $in, $nin', spc)
884
956
 
885
957
 
886
- //select by regex
887
- let sr = await w.select({ name: { $regex: 'PeT', $options: 'i' } })
888
- console.log('selectReg', sr)
958
+ //select by equality (there is no pattern matching: filter in memory for substrings)
959
+ let sr = await w.select({ name: 'peter(modify)' })
960
+ console.log('selectEq', sr)
889
961
 
890
962
 
891
963
  //del
@@ -936,7 +1008,7 @@ test()
936
1008
  // { id: 'id-rosemary', name: 'rosemary(modify)', value: 123.456 },
937
1009
  // { id: '{random id}', name: 'kettle', value: 456 }
938
1010
  // ]
939
- // selectReg [
1011
+ // selectEq [
940
1012
  // { id: 'id-peter', name: 'peter(modify)', value: 123 }
941
1013
  // ]
942
1014
  // change del
@@ -1087,7 +1159,7 @@ async function testCommit() {
1087
1159
 
1088
1160
 
1089
1161
  //select all
1090
- let ssBeforeCommit = await w.select(null, connState)
1162
+ let ssBeforeCommit = await w.select({}, connState)
1091
1163
  console.log('select all (before commit)', ssBeforeCommit) //此時select可查到暫時有效的數據
1092
1164
  // => [
1093
1165
  // { id: 'id-peter', name: 'peter(modify)', value: 123 },
@@ -1311,7 +1383,7 @@ async function testRollback() {
1311
1383
 
1312
1384
 
1313
1385
  //select all
1314
- let ssBeforeRollback = await w.select(null, connState)
1386
+ let ssBeforeRollback = await w.select({}, connState)
1315
1387
  console.log('select all (before rollback)', ssBeforeRollback) //此時select可查到暫時有效的數據
1316
1388
  // => [
1317
1389
  // { id: 'id-peter', name: 'peter(modify)', value: 123 },
@@ -1541,9 +1613,9 @@ async function test() {
1541
1613
  console.log('select by $or, $and, $ne, $in, $nin', spc)
1542
1614
 
1543
1615
 
1544
- //select by regex
1545
- let sr = await w.select({ name: { $regex: 'PeT', $options: 'i' } })
1546
- console.log('selectReg', sr)
1616
+ //select by equality (there is no pattern matching: filter in memory for substrings)
1617
+ let sr = await w.select({ name: 'peter(modify)' })
1618
+ console.log('selectEq', sr)
1547
1619
 
1548
1620
 
1549
1621
  //del
@@ -1594,7 +1666,7 @@ test()
1594
1666
  // { id: 'id-rosemary', name: 'rosemary(modify)', value: 123.456 },
1595
1667
  // { id: '{random id}', name: 'kettle', value: 456 }
1596
1668
  // ]
1597
- // selectReg [
1669
+ // selectEq [
1598
1670
  // { id: 'id-peter', name: 'peter(modify)', value: 123 }
1599
1671
  // ]
1600
1672
  // change del