w-serv-orm 1.1.13 → 1.1.14

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.
@@ -1,8 +1,11 @@
1
1
  import fs from 'fs'
2
+ import vm from 'vm'
3
+ import util from 'util'
4
+ import { createSecretKey } from 'crypto'
2
5
  import assert from 'assert'
3
6
  import WOrm from 'w-orm-lowdb/src/WOrmLowdb.mjs'
4
7
  import WServOrm from '../src/WServOrm.mjs'
5
- import { getReject, quiet, same } from './tools/ormStub.mjs'
8
+ import { getReject, quiet, same, mkForgedObjProto } from './tools/ormStub.mjs'
6
9
 
7
10
 
8
11
  //以真ORM(w-orm-lowdb, mingo查詢)經procOrm驗證R-OBJ-01: 含整數length鍵之find與寫入列, 結果須等於以JS參考實作(預言機)算得者
@@ -19,7 +22,7 @@ let seed = [
19
22
  { id: 'a5', name: 'delta', length: 0 },
20
23
  ]
21
24
 
22
- //refMatch, JS參考實作: 欄位等值(嚴格相等)、$and、$or、$nor、欄位層$in/$nin/$eq/$ne/$not; 缺欄視為undefined
25
+ //refMatch, JS參考實作(系列條件白名單內之子集): 欄位等值(嚴格相等)、$and、$or、欄位層$in/$nin/$eq/$ne; 缺欄視為undefined
23
26
  function refMatch(row, find) {
24
27
  let opMatch = (v, c) => {
25
28
  if (c !== null && typeof c === 'object' && !Array.isArray(c)) {
@@ -28,7 +31,6 @@ function refMatch(row, find) {
28
31
  if (op === '$ne') return v !== c[op]
29
32
  if (op === '$in') return c[op].includes(v)
30
33
  if (op === '$nin') return !c[op].includes(v)
31
- if (op === '$not') return !opMatch(v, c[op])
32
34
  throw new Error(`參考實作不支援${op}`)
33
35
  })
34
36
  }
@@ -38,7 +40,6 @@ function refMatch(row, find) {
38
40
  let c = find[k]
39
41
  if (k === '$and') return c.every((f) => refMatch(row, f))
40
42
  if (k === '$or') return c.some((f) => refMatch(row, f))
41
- if (k === '$nor') return !c.some((f) => refMatch(row, f))
42
43
  return opMatch(row[k], c)
43
44
  })
44
45
  }
@@ -56,7 +57,8 @@ let finds = [
56
57
  { length: 3.5 },
57
58
  { $or: [{ length: 3 }, { name: 'gamma' }] },
58
59
  { $and: [{ length: 1 }, { name: 'beta' }] },
59
- { $nor: [{ length: 3 }] },
60
+ { length: { $ne: 3 } },
61
+ { length: { $nin: [3, 1] } },
60
62
  { id: { $in: ['a1', 'a2'] }, length: 1 },
61
63
  { name: 'alpha' },
62
64
  {},
@@ -229,4 +231,384 @@ describe('R-OBJ-01 接w-orm-lowdb(以JS參考實作為預言機)', function() {
229
231
  same(await rows(), before, '表內容不變')
230
232
  })
231
233
 
234
+ //R-OBJ-01 規則6(外審 sol F1 與第二輪 A-01~A-03、B-01): 位置 x 變形, 預言機為直呼真ORM(wsemi cvOrmFind白名單)
235
+ // must: 直呼reject => 經procOrm之select與delAll亦reject固定字串, ORM不被呼叫, 表不變
236
+ // ok: 直呼通過 => 經procOrm之select回傳同直呼, delAll刪除之列集合同直呼select所得
237
+ // superset: 直呼通過而本層仍fail-closed reject(帶標籤之真陣列等, 見帳本), ORM不被呼叫, 表不變
238
+ let hide = (o, k, v) => Object.defineProperty(o, k, { value: v, enumerable: false, configurable: true, writable: true })
239
+ class Cls {
240
+ constructor(o) {
241
+ Object.assign(this, o)
242
+ }
243
+ }
244
+ let msgHidden = 'unsupported symbol key or non-enumerable property'
245
+ let msgIrreg = 'unsupported irregular object or array'
246
+ let ids = (rs) => rs.map((v) => v.id).sort()
247
+ //oracle: 直呼真ORM(不經procOrm); 回傳 {ok: 列id} 或 {err}
248
+ let oracle = (find) => so.woItems.tabA.select(find).then((r) => ({ ok: ids(r) }), (e) => ({ err: e }))
249
+
250
+ //runCell: 一格(位置 x 變形)於select與delAll之斷言; msg 為 procOrm 之 reject 字串 `invalid find of <mode>, <msg>`
251
+ let modesRun = 0 //runCell 實際跑完之 mode 數(各案以差值斷言: 每格 2 個 mode, 防止 mode 迴圈被縮短而仍綠)
252
+ async function runCell(label, kind, mk, msg) {
253
+ await reseed()
254
+ let before = await rows()
255
+ let direct = await quiet(() => oracle(mk()))
256
+ if (kind === 'must') {
257
+ assert.ok(direct.err !== undefined, `${label} 預言機: 直呼ORM應reject`)
258
+ }
259
+ else {
260
+ assert.ok(direct.ok !== undefined, `${label} 預言機: 直呼ORM應通過`)
261
+ }
262
+ //表於本格開始時重灌一次: select 不改表, must 與 superset 格之 delAll 亦不得改表(每個 mode 後皆斷言表不變), 故 mode 之間不必重灌; ok 格之 delAll 在最後
263
+ for (let mode of ['select', 'delAll']) {
264
+ modesRun++
265
+ seen = []
266
+ if (kind === 'ok') {
267
+ let r = await proc(mode, mk())
268
+ if (mode === 'select') {
269
+ same(ids(r), direct.ok, `${label} select 回傳`)
270
+ }
271
+ else {
272
+ same((await rows()).map((v) => v.id), ids(before).filter((id) => !direct.ok.includes(id)), `${label} delAll 剩餘`)
273
+ }
274
+ continue
275
+ }
276
+ let err = await quiet(() => getReject(proc(mode, mk()))).catch((e) => assert.fail(`${label} ${mode} ${e.message}`))
277
+ let sawOrm = seen.length
278
+ assert.strictEqual(err, `invalid find of ${mode}, ${msg}`, `${label} ${mode}`)
279
+ assert.strictEqual(sawOrm, 0, `${label} ${mode} ORM不應被呼叫`)
280
+ same(await rows(), before, `${label} ${mode} 表內容不變`)
281
+ }
282
+ }
283
+
284
+ it('A20 find之物件節點含Symbol鍵、不可列舉鍵、類別實例、品牌物件、偽原型、帶標籤函數: 直呼ORM reject者經procOrm亦reject, 表不變(7位置 x 13變形)', async function() {
285
+ //R-OBJ-01 規則6: 第一版修前以 tmp/verify/f1-diff.mjs 實測, 物件節點 18 格直呼reject而經procOrm通過; 外審第二輪(tmp/verify/f1-sol.mjs)另證 brand、fakeRoot、fnTag 三類
286
+ let positions = [
287
+ ['top', () => ({ id: 'a1' }), (n) => n],
288
+ ['$and元素', () => ({ id: 'a1' }), (n) => ({ $and: [n] })],
289
+ ['$or元素', () => ({ id: 'a1' }), (n) => ({ $or: [n] })],
290
+ ['巢狀$and→$or元素', () => ({ id: 'a1' }), (n) => ({ $and: [{ $or: [n] }] })],
291
+ ['id運算子物件($eq)', () => ({ $eq: 'a1' }), (n) => ({ id: n })],
292
+ ['id運算子物件($in)', () => ({ $in: ['a1'] }), (n) => ({ id: n })],
293
+ ['非id欄位運算子物件', () => ({ $eq: 'alpha' }), (n) => ({ name: n })],
294
+ ]
295
+ let muts = [
296
+ ['control', 'ok', (o) => o, null],
297
+ ['sym', 'must', (o) => Object.assign(o, { [Symbol('s')]: 1 }), msgHidden],
298
+ ['hiddenExtra', 'must', (o) => hide(o, 'zz', 1), msgHidden],
299
+ ['hiddenFirst', 'must', (o) => {
300
+ let k = Object.keys(o)[0]
301
+ let v = o[k]
302
+ delete o[k]
303
+ return hide(o, k, v)
304
+ }, msgHidden],
305
+ ['cls', 'must', (o) => new Cls(o), msgIrreg],
306
+ ['nullProto', 'ok', (o) => Object.assign(Object.create(null), o), null],
307
+ //內建品牌物件之原型改為null: 內部槽不在自有鍵中, 複本洗成一般物件(A-01)
308
+ ['brand', 'must', (o) => Object.assign(Object.setPrototypeOf(new Map(), null), o), msgIrreg],
309
+ //原型僅有constructor一鍵之偽原型: 通過lodash式純物件判準而後端要求本realm之Object.prototype或null
310
+ ['fakeRoot', 'must', (o) => Object.assign(Object.create(Object.assign(Object.create(null), { constructor: Object })), o), msgIrreg],
311
+ //帶Symbol.toStringTag為Object之函數: 被isobj當物件複製, 函數身分與Symbol鍵被洗掉(A-03)
312
+ ['fnTag', 'must', (o) => Object.assign(function() {}, o, { [Symbol.toStringTag]: 'Object' }), msgIrreg],
313
+ //外審第二輪: 仿Object.prototype之偽原型(鍵名同、constructor指向真Object, 但其prototype不指回偽原型)(A-01)
314
+ ['fakeRoot12', 'must', (o) => Object.assign(Object.create(mkForgedObjProto()), o), msgIrreg],
315
+ //constructor 為 getter 之偽原型(A-03): 檢查不得執行它
316
+ ['fakeRootGetter', 'must', (o) => Object.assign(Object.create(mkForgedObjProto({ get: () => Object, enumerable: false, configurable: true })), o), msgIrreg],
317
+ //不在 util.types 品牌清單內之可結構化複製品牌物件(KeyObject, 原型改為null, 帶自有鍵): 由 structuredClone 網抓; 後端之品牌探針清單不含 KeyObject 而接受(superset)
318
+ ['keyObject', 'superset', (o) => Object.assign(Object.setPrototypeOf(createSecretKey(Buffer.alloc(16)), null), o), msgIrreg],
319
+ //Proxy: 後端接受而本層一律reject(陷阱可令檢查之值不等於使用之值; superset)
320
+ ['proxy', 'superset', (o) => new Proxy(o, {}), msgIrreg],
321
+ ]
322
+ let n = 0
323
+ let m0 = modesRun
324
+ for (let [pn, base, build] of positions) {
325
+ for (let [mn, kind, mut, msg] of muts) {
326
+ //頂層之非純物件(類別實例、偽原型、函數)由 getErrFind 先以 need an object 回絕
327
+ let m = pn === 'top' && ['cls', 'fakeRoot', 'fnTag', 'fakeRoot12', 'fakeRootGetter'].includes(mn) ? 'need an object' : msg
328
+ await runCell(`${pn} | ${mn}`, kind, () => build(mut(base())), m)
329
+ n++
330
+ }
331
+ }
332
+ assert.strictEqual(n, 91, '格數')
333
+ assert.strictEqual(modesRun - m0, 91 * 2, '完成數(格 × 2 mode)')
334
+ })
335
+
336
+ it('A21 find之陣列節點含額外屬性、Symbol鍵、不可列舉元素、空洞、帶標籤: 直呼ORM reject者經procOrm亦reject, 表不變(5位置 x 12變形)', async function() {
337
+ //R-OBJ-01 規則6: 第一版修前以 tmp/verify/f1-diff.mjs 實測, 陣列節點 20 格直呼reject而經procOrm通過; 外審第二輪另證 arrHoleInherited(A-02)、fnTagArray(A-03)、arrTagGetter(B-01)
338
+ //arrProtoTag、arrTagGetter 直呼通過而本層仍 reject(superset: 不依標籤歸類, 且不執行標籤 getter)
339
+ let positions = [
340
+ ['id.$in', () => ['a1'], (n) => ({ id: { $in: n } })],
341
+ ['id.$nin', () => ['a9'], (n) => ({ id: { $nin: n } })],
342
+ ['非id欄位.$in', () => ['alpha'], (n) => ({ name: { $in: n } })],
343
+ ['$and值', () => [{ id: 'a1' }], (n) => ({ $and: n })],
344
+ ['$or值', () => [{ id: 'a1' }], (n) => ({ $or: n })],
345
+ ]
346
+ let tagProto = (tag) => Object.create(Array.prototype, { [Symbol.toStringTag]: { value: tag } })
347
+ let muts = [
348
+ ['control', 'ok', (a) => a, null],
349
+ ['arrExtra', 'must', (a) => Object.assign(a, { extra: 1 }), msgIrreg],
350
+ ['arrHiddenExtra', 'must', (a) => hide(a, 'extra', 1), msgIrreg],
351
+ ['arrSym', 'must', (a) => Object.assign(a, { [Symbol('s')]: 1 }), msgIrreg],
352
+ ['arrHole', 'must', (a) => {
353
+ a[a.length + 1] = a[0]
354
+ return a
355
+ }, msgIrreg],
356
+ ['arrHiddenEl', 'must', (a) => hide(a, '0', a[0]), msgIrreg],
357
+ ['arrFakeTag', 'must', (a) => Object.assign({ length: a.length, [Symbol.toStringTag]: 'Array' }, a), msgIrreg],
358
+ ['arrProtoTag', 'superset', (a) => Object.setPrototypeOf(a, tagProto('Object')), msgIrreg],
359
+ //空洞陣列之原型帶資料索引: 檢查跳過空洞而map以array[index]讀到繼承值(A-02)
360
+ ['arrHoleInherited', 'must', (a) => Object.setPrototypeOf(new Array(a.length), Object.assign(Object.create(Array.prototype), a)), msgIrreg],
361
+ //標籤為繼承之getter: 分類時被執行(檢查與使用各一次, 可回傳不同值)(B-01)
362
+ ['arrTagGetter', 'superset', (a) => Object.setPrototypeOf(a, Object.create(Array.prototype, { [Symbol.toStringTag]: { get: () => 'Array' } })), msgIrreg],
363
+ //帶Symbol.toStringTag為Array之函數(A-03)
364
+ ['fnTagArray', 'must', (a) => Object.assign(function() {}, a, { [Symbol.toStringTag]: 'Array' }), msgIrreg],
365
+ //原型為Proxy之真陣列(外審第二輪 A-02): 後端接受, 本層一律reject(分類不得觸發原型之陷阱; superset)
366
+ ['arrProtoProxy', 'superset', (a) => Object.setPrototypeOf(a, new Proxy(Array.prototype, {})), msgIrreg],
367
+ ]
368
+ let n = 0
369
+ let m0 = modesRun
370
+ for (let [pn, base, build] of positions) {
371
+ for (let [mn, kind, mut, msg] of muts) {
372
+ await runCell(`${pn} | ${mn}`, kind, () => build(mut(base())), msg)
373
+ n++
374
+ }
375
+ }
376
+ assert.strictEqual(n, 60, '格數')
377
+ assert.strictEqual(modesRun - m0, 60 * 2, '完成數(格 × 2 mode)')
378
+ })
379
+
380
+ it('A22 sol F1 原情境: 頂層僅含不可列舉id或Symbol鍵之find, delAll不刪全表(修前剩0列), select不回全表', async function() {
381
+ //R-OBJ-01 規則6、dspec 一之2②(無法轉譯之值必須reject, 不得擴大刪除範圍); 重現腳本 tmp/verify/f1.mjs
382
+ let mks = [
383
+ ['不可列舉id', () => Object.defineProperty({}, 'id', { value: 'a1', enumerable: false })],
384
+ ['Symbol鍵', () => ({ [Symbol('s')]: 1 })],
385
+ ['id.$in另有額外屬性(sol 另測)', () => ({ id: { $in: Object.assign(['a1'], { extra: 1 }) } })],
386
+ ['id運算子為類別實例(sol 另測)', () => ({ id: new Cls({ $in: ['a1'] }) })],
387
+ ['原型為null之Map(第二輪 A-01)', () => Object.setPrototypeOf(new Map([['secret', 'a2']]), null)],
388
+ ['原型僅有constructor之偽原型(第二輪)', () => Object.create(Object.assign(Object.create(null), { constructor: Object }))],
389
+ ]
390
+ let done = 0
391
+ for (let [label, mk] of mks) {
392
+ let before = await rows()
393
+ assert.strictEqual(before.length, seed.length)
394
+ let direct = await quiet(() => oracle(mk()))
395
+ assert.ok(direct.err !== undefined, `${label} 預言機: 直呼ORM應reject`)
396
+ for (let mode of ['select', 'delAll']) {
397
+ seen = []
398
+ let err = await quiet(() => getReject(proc(mode, mk()))).catch((e) => assert.fail(`${label} ${mode} ${e.message}`))
399
+ assert.ok(typeof err === 'string' && err.startsWith(`invalid find of ${mode}, `), `${label} ${mode} ${err}`)
400
+ assert.strictEqual(seen.length, 0, `${label} ${mode} ORM不應被呼叫`)
401
+ done++
402
+ }
403
+ same(await rows(), before, `${label} 表內容不變(delAll不刪)`)
404
+ }
405
+ assert.strictEqual(mks.length, 6, '語料數')
406
+ assert.strictEqual(done, 6 * 2, '完成數(語料 × 2 mode)')
407
+ })
408
+
409
+ it('A23 壞節點在兄弟之前後與深度邊界(含品牌物件、Proxy、函數在較後位置): 皆被檢查到, 直呼ORM reject者經procOrm亦reject, 表不變(外審第二輪 C-01)', async function() {
410
+ //R-OBJ-01 規則6: 檢查須走訪每個兄弟與每一層(變異「只遞迴第0元素」「只遞迴最後一個值」「第64層略過strict」須轉紅)
411
+ //容器層數: 根為第1層, 每包一層 {$and:[…]} 增2層; 第65層起由深度上限先回絕(too deep)
412
+ let bad = (o) => hide(o, 'zz', 1)
413
+ let wrap = (inner, k) => {
414
+ for (let i = 0; i < k; i++) {
415
+ inner = { $and: [inner] }
416
+ }
417
+ return inner
418
+ }
419
+ let nulP = (o) => Object.setPrototypeOf(o, null)
420
+ let keyObj = () => nulP(createSecretKey(Buffer.alloc(16)))
421
+ let cases = [
422
+ ['id在前壞、name在後', () => ({ id: bad({ $eq: 'a1' }), name: 'alpha' }), msgHidden],
423
+ ['name在前壞、id在後', () => ({ name: bad({ $eq: 'alpha' }), id: 'a1' }), msgHidden],
424
+ ['壞運算子物件夾在中間', () => ({ id: 'a1', name: bad({ $eq: 'alpha' }), length: 3 }), msgHidden],
425
+ ['$and第二個元素壞', () => ({ $and: [{ id: 'a1' }, bad({ name: 'alpha' })] }), msgHidden],
426
+ ['$or最後一個元素壞(共三個)', () => ({ $or: [{ id: 'a1' }, { id: 'a2' }, bad({ id: 'a3' })] }), msgHidden],
427
+ ['巢狀$and→$or第三個元素壞', () => ({ $and: [{ id: 'a1' }, { $or: [{ id: 'a1' }, { id: 'a2' }, bad({ id: 'a3' })] }] }), msgHidden],
428
+ ['壞元素在$and之後且有其他欄位並列', () => ({ name: 'alpha', $and: [{ id: 'a1' }, bad({ id: 'a1' })], length: 3 }), msgHidden],
429
+ ['壞陣列在較前欄位、好陣列在後', () => ({ id: { $in: Object.assign(['a1'], { extra: 1 }) }, name: { $in: ['alpha'] } }), msgIrreg],
430
+ ['壞陣列在較後欄位、好陣列在前', () => ({ name: { $in: ['alpha'] }, id: { $in: Object.assign(['a1'], { extra: 1 }) } }), msgIrreg],
431
+ ['壞物件恰在第63層', () => wrap(bad({ id: 'a1' }), 31), msgHidden],
432
+ ['壞陣列(額外屬性)恰在第64層', () => wrap({ $and: Object.assign([{ id: 'a1' }], { extra: 1 }) }, 31), msgIrreg],
433
+ ['壞物件在第65層: 深度上限先回絕', () => wrap(bad({ id: 'a1' }), 32), 'too deep'],
434
+ //由 util.types 或 structuredClone 網抓之品牌物件、Proxy、函數置於較後之兄弟位置(外審第二輪 C-02: 只走訪第0個鍵/元素之變異須轉紅)
435
+ ['品牌物件(Map)為第2個鍵', () => ({ name: 'alpha', id: nulP(new Map()) }), msgIrreg],
436
+ ['KeyObject為第2個鍵', () => ({ name: 'alpha', id: keyObj() }), msgIrreg],
437
+ ['KeyObject為id運算子物件之第2個鍵', () => ({ id: { $eq: 'a1', $ne: keyObj() } }), msgIrreg],
438
+ ['KeyObject為$and第2個元素', () => ({ $and: [{ id: 'a1' }, keyObj()] }), msgIrreg],
439
+ ['KeyObject為$or第3個元素', () => ({ $or: [{ id: 'a1' }, { id: 'a2' }, keyObj()] }), msgIrreg],
440
+ ['KeyObject為$in第2個元素', () => ({ id: { $in: ['a1', keyObj()] } }), msgIrreg],
441
+ ['帶標籤之函數為$or第3個元素', () => ({ $or: [{ id: 'a1' }, { id: 'a2' }, Object.assign(function() {}, { id: 'a3', [Symbol.toStringTag]: 'Object' })] }), msgIrreg],
442
+ ['Proxy為$and第2個元素', () => ({ $and: [{ id: 'a1' }, new Proxy({ id: 'a2' }, {})] }), msgIrreg, 'superset'],
443
+ ['原型為Proxy之陣列在較後欄位', () => ({ name: { $in: ['alpha'] }, id: { $in: Object.setPrototypeOf(['a1'], new Proxy(Array.prototype, {})) } }), msgIrreg, 'superset'],
444
+ ]
445
+ let m0 = modesRun
446
+ for (let [label, mk, msg, kind = 'must'] of cases) {
447
+ await runCell(label, kind, mk, msg)
448
+ }
449
+ assert.strictEqual(cases.length, 21, '語料數')
450
+ assert.strictEqual(modesRun - m0, 21 * 2, '完成數(例 × 2 mode)')
451
+ })
452
+
453
+ it('A24 合法特殊形狀照常(接真ORM): JSON.parse與structuredClone產生者、null原型、Date、1萬元素$in皆同直呼; 跨realm之純物件於複製位置被正規化、於不複製位置由後端決定(外審第二輪 C-03、A-05)', async function() {
454
+ //R-OBJ-01 規則6、R-INPUT-01 規則3: 規則6之檢查不誤擋 JSON 可表達之資料與合法特殊形狀(unit-objcopy D06 僅接替身, 此處接真 lowdb)
455
+ let big = Array.from({ length: 10000 }, (_, i) => `x${i}`)
456
+ let oks = [
457
+ ['JSON.parse產生者', () => JSON.parse('{"$or":[{"id":"a1"},{"name":{"$in":["alpha","beta"]}}],"length":{"$gte":1}}')],
458
+ ['structuredClone產生者', () => structuredClone({ $and: [{ id: 'a1' }, { name: { $ne: 'beta' } }] })],
459
+ ['null原型之$or元素與id運算子物件', () => ({ $or: [Object.assign(Object.create(null), { id: 'a1' }), { id: Object.assign(Object.create(null), { $in: ['a2'] }) }] })],
460
+ ['Date為欄位值', () => ({ name: new Date(0) })],
461
+ ['1萬個元素之$in', () => ({ id: { $in: big } })],
462
+ ['合理複雜之查詢', () => ({ $and: [{ $or: [{ id: { $in: ['a1', 'a2'] } }, { name: 'gamma' }] }, { length: { $ne: 3 } }] })],
463
+ ['空陣列之$in', () => ({ id: { $in: [] } })],
464
+ ]
465
+ for (let [label, mk] of oks) {
466
+ await runCell(label, 'ok', mk, null)
467
+ }
468
+ //跨realm之純物件: 後端要求本realm之 Object.prototype/Array.prototype 或 null, 直呼會reject;
469
+ //字串型主鍵表之複製位置(頂層、$and/$or元素、id運算子物件與其陣列)被複製為本realm物件(正規化, 內容等價, 不算洩漏)
470
+ let cross = (src) => vm.runInNewContext(src)
471
+ let norms = [
472
+ ['頂層', () => cross('({ id: "a1" })')],
473
+ ['$and元素', () => ({ $and: [cross('({ id: "a1" })')] })],
474
+ ['id運算子物件', () => ({ id: cross('({ $in: ["a1", "a2"] })') })],
475
+ ['id.$in陣列', () => ({ id: { $in: cross('["a1"]') } })],
476
+ ]
477
+ for (let [label, mk] of norms) {
478
+ await reseed()
479
+ let before = await rows()
480
+ let expected = (await quiet(() => oracle(structuredClone(mk())))).ok //同內容之本realm物件
481
+ assert.ok(expected !== undefined, `跨realm ${label} 預言機: 本realm等價物件應通過`)
482
+ seen = []
483
+ same(ids(await proc('select', mk())), expected, `跨realm ${label} select`)
484
+ await proc('delAll', mk())
485
+ same((await rows()).map((v) => v.id), ids(before).filter((id) => !expected.includes(id)), `跨realm ${label} delAll 剩餘`)
486
+ }
487
+ //不複製之位置(非id欄位之運算子物件): 原參考交後端, 由後端決定(現行 wsemi 要求本realm, 故reject; 若後端放寬則須等價); 表不變
488
+ await reseed()
489
+ let before = await rows()
490
+ let find = () => ({ name: cross('({ $eq: "alpha" })') })
491
+ let r = await quiet(() => proc('select', find()).then((x) => ({ ok: ids(x) }), (e) => ({ err: e })))
492
+ if (r.err !== undefined) {
493
+ assert.strictEqual(r.err, 'failed to select data', '不複製之位置由後端reject')
494
+ same(await rows(), before, '表不變')
495
+ }
496
+ else {
497
+ same(r.ok, (await quiet(() => oracle(structuredClone(find())))).ok, '後端放寬時須等價')
498
+ }
499
+ })
500
+
501
+ it('A25 字串碼元總長逾品牌網預算(4,000,000)之find: 不做結構化複製, 直呼ORM的結果與經procOrm者等價(合法條件同結果、後端reject之品牌物件仍reject、表不變)', async function() {
502
+ //R-OBJ-01 規則6 ⑥(sol 外審最終版 R3-02): 預算內走結構化複製網, 逾預算改走精確探針(WeakRef、FinalizationRegistry); 預言機為直呼真ORM
503
+ let str = 'x'.repeat(65536)
504
+ let bigIn = () => Array(70).fill(str) //70 x 65536 = 4.6M 碼元, 記憶體內僅一個字串
505
+ let nul = (o) => Object.setPrototypeOf(o, null)
506
+ let nClone = 0
507
+ let cloneOrig = globalThis.structuredClone
508
+ let cases = [
509
+ ['合法: 大$in與id等值', 'ok', () => ({ id: 'a1', name: { $in: bigIn() } })],
510
+ ['合法: $or內id與大$in', 'ok', () => ({ $or: [{ id: 'a1' }, { name: { $in: bigIn() } }] })],
511
+ ['WeakRef於較後鍵之$and元素', 'must', () => ({ name: { $in: bigIn() }, $and: [nul(new WeakRef({}))] })],
512
+ ['FinalizationRegistry於$or之較後兄弟', 'must', () => ({ $or: [{ name: { $in: bigIn() } }, nul(new FinalizationRegistry(() => {}))] })],
513
+ ['Map於id運算子位置', 'must', () => ({ name: { $in: bigIn() }, id: nul(new Map([['$eq', 'a1']])) })],
514
+ //後端實測接受(原型改為null之KeyObject視同無條件之物件): 逾預算時本層與直呼一致, 不再超集reject
515
+ ['KeyObject於id運算子位置(後端決定)', 'oracle', () => ({ name: { $in: bigIn() }, id: nul(createSecretKey(Buffer.alloc(8))) })],
516
+ ]
517
+ let done = 0
518
+ try {
519
+ globalThis.structuredClone = (...a) => {
520
+ nClone++
521
+ return cloneOrig(...a)
522
+ }
523
+ for (let [label, kind, mk] of cases) {
524
+ await reseed()
525
+ let before = await rows()
526
+ let direct = await quiet(() => oracle(mk()))
527
+ if (kind === 'ok') {
528
+ assert.ok(direct.ok !== undefined, `${label} 預言機: 直呼ORM應通過`)
529
+ }
530
+ if (kind === 'must') {
531
+ assert.ok(direct.err !== undefined, `${label} 預言機: 直呼ORM應reject`)
532
+ }
533
+ nClone = 0
534
+ seen = []
535
+ let sel = await quiet(() => proc('select', mk()).then((r) => ({ ok: ids(r) }), (e) => ({ err: e })))
536
+ assert.strictEqual(nClone, 0, `${label} 逾預算不做結構化複製`)
537
+ let sawSel = seen.length
538
+ if (direct.ok !== undefined) {
539
+ same(sel.ok, direct.ok, `${label} select 同直呼`)
540
+ await quiet(() => proc('delAll', mk()))
541
+ same((await rows()).map((v) => v.id), ids(before).filter((id) => !direct.ok.includes(id)), `${label} delAll 剩餘`)
542
+ }
543
+ else {
544
+ assert.ok(typeof sel.err === 'string', `${label} select 應以字串reject: ${sel.err}`)
545
+ seen = []
546
+ let del = await quiet(() => getReject(proc('delAll', mk())))
547
+ let sawDel = seen.length //須在 rows() 之前: rows() 亦經 spy
548
+ assert.ok(typeof del === 'string', `${label} delAll 應以字串reject`)
549
+ same(await rows(), before, `${label} 表內容不變`)
550
+ if (kind === 'must') {
551
+ assert.strictEqual(sel.err, `invalid find of select, ${msgIrreg}`, `${label} select 訊息`)
552
+ assert.strictEqual(del, `invalid find of delAll, ${msgIrreg}`, `${label} delAll 訊息`)
553
+ assert.strictEqual(sawSel + sawDel, 0, `${label} ORM不應被呼叫`)
554
+ }
555
+ }
556
+ done++
557
+ }
558
+ }
559
+ finally {
560
+ globalThis.structuredClone = cloneOrig
561
+ }
562
+ assert.strictEqual(done, cases.length, '完成數')
563
+ assert.strictEqual(done, 6, '語料數')
564
+ })
565
+
566
+ it('A26 被拒之find再被呼叫端重用: 診斷輸出不執行原型上之標籤getter(其副作用會刪除id條件), 同一物件再送delAll仍reject, 表不變(直呼ORM從不讀該標籤, 只刪a1)', async function() {
567
+ //R-OBJ-01 規則6 ⑤(sol 外審最終版 R3-01): 修前拒收之後 console.log(find) 格式化時getter被執行, find變成{}, 再送delAll即刪全表(5/5列)
568
+ let logOrig = console.log
569
+ console.log = (...a) => {
570
+ util.format(...a) //同真 console.log 格式化引數後丟棄(不用 quiet: 其不格式化引數)
571
+ }
572
+ let reads = 0
573
+ let mk = () => {
574
+ let a = ['a1']
575
+ let find = { id: { $in: a } }
576
+ Object.setPrototypeOf(a, Object.create(Array.prototype, {
577
+ [Symbol.toStringTag]: {
578
+ get() {
579
+ reads++
580
+ delete find.id
581
+ return 'Array'
582
+ },
583
+ },
584
+ }))
585
+ return find
586
+ }
587
+ try {
588
+ //對照: 直呼ORM從不讀該標籤, 兩次呼叫皆保留id條件, delAll只刪a1
589
+ await reseed()
590
+ let f0 = mk()
591
+ same(ids(await so.woItems.tabA.select(f0)), ['a1'], '直呼 select')
592
+ assert.ok('id' in f0, '直呼後find不變')
593
+ await so.woItems.tabA.delAll(f0)
594
+ same((await rows()).map((v) => v.id), ['a2', 'a3', 'a4', 'a5'], '直呼 delAll 只刪a1')
595
+ assert.strictEqual(reads, 0, '直呼ORM不讀標籤getter')
596
+ //經procOrm: 首次拒收, getter 不被執行, 同一物件再送delAll仍拒收, 表不變
597
+ await reseed()
598
+ let before = await rows()
599
+ let f = mk()
600
+ let e1 = await getReject(proc('select', f))
601
+ assert.strictEqual(e1, `invalid find of select, ${msgIrreg}`)
602
+ assert.strictEqual(reads, 0, '診斷輸出不得執行標籤getter')
603
+ assert.ok('id' in f, '拒收後find不被改動')
604
+ let e2 = await getReject(proc('delAll', f))
605
+ assert.strictEqual(e2, `invalid find of delAll, ${msgIrreg}`, '同一物件再送delAll仍reject')
606
+ assert.strictEqual(reads, 0)
607
+ same(await rows(), before, '表內容不變(修前剩0列)')
608
+ }
609
+ finally {
610
+ console.log = logOrig
611
+ }
612
+ })
613
+
232
614
  })
@@ -94,14 +94,16 @@ describe('數字主鍵接w-orm-lowdb', function() {
94
94
  assert.deepStrictEqual(await rows(), [['999', 'p']])
95
95
  })
96
96
 
97
- it('A12 查詢與delAll之條件以數字給予: 等值、$in、$ne、$not命中字串主鍵, 範圍依字典序', async function() {
98
- //R-ID-02 規則2(查詢條件)與已知限制(範圍依字典序: '40' < '5')
97
+ it('A12 查詢與delAll之條件以數字給予: 等值、$in、$ne、$nin與$and、$or巢狀命中字串主鍵, 範圍依字典序', async function() {
98
+ //R-ID-02 規則2(查詢條件)與已知限制(範圍依字典序: '40' < '5'); R-WL-01 規則3(只轉型白名單內之運算子)
99
99
  await proc('insert', [{ id: 5, isActive: 'y' }, { id: 40, isActive: 'y' }, { id: 'abc', isActive: 'y' }])
100
100
  let ids = async (find) => (await proc('select', find)).map((v) => v.id).sort()
101
101
  assert.deepStrictEqual(await ids({ id: 5 }), ['5'])
102
102
  assert.deepStrictEqual(await ids({ id: { $in: [5, 40] } }), ['40', '5'])
103
103
  assert.deepStrictEqual(await ids({ id: { $ne: 5 } }), ['40', 'abc'])
104
- assert.deepStrictEqual(await ids({ id: { $not: { $eq: 5 } } }), ['40', 'abc'])
104
+ assert.deepStrictEqual(await ids({ id: { $nin: [5] } }), ['40', 'abc'])
105
+ assert.deepStrictEqual(await ids({ $or: [{ id: 5 }, { id: 40 }] }), ['40', '5'])
106
+ assert.deepStrictEqual(await ids({ $and: [{ id: { $in: [5, 40] } }, { id: { $ne: 40 } }] }), ['5'])
105
107
  assert.deepStrictEqual(await ids({ id: { $lt: 5 } }), ['40'])
106
108
  let r1 = await proc('delAll', { id: { $in: [40] } })
107
109
  assert.deepStrictEqual(r1, { n: 1, nDeleted: 1, ok: 1 })
@@ -110,4 +112,26 @@ describe('數字主鍵接w-orm-lowdb', function() {
110
112
  assert.deepStrictEqual(await rows(), [['abc', 'y']])
111
113
  })
112
114
 
115
+ it('A19 白名單外之運算子: 後端reject, select以failed to select data、delAll以failed to delete data固定字串回傳, 表內容不變', async function() {
116
+ //R-WL-01 規則3、4; R-ERR-01 規則1: 白名單由後端(w-orm-lowdb >= 1.0.32之cvOrmFind)把關, 本套件不轉型亦不放行, 後端原文只進log
117
+ await proc('insert', [{ id: 5, isActive: 'y' }, { id: 40, isActive: 'y' }, { id: 'abc', isActive: 'y' }])
118
+ let before = await rows()
119
+ let finds = [
120
+ { id: { $not: { $eq: 5 } } },
121
+ { id: { $all: [5] } },
122
+ { $nor: [{ id: 5 }] },
123
+ { id: { $regex: '^a' } },
124
+ { isActive: { $exists: true } },
125
+ { $where: 'true' },
126
+ { $expr: true },
127
+ ]
128
+ for (let find of finds) {
129
+ let es = await quiet(() => getReject(proc('select', find)))
130
+ assert.strictEqual(es, 'failed to select data', `select ${JSON.stringify(find)}`)
131
+ let ed = await quiet(() => getReject(proc('delAll', find)))
132
+ assert.strictEqual(ed, 'failed to delete data', `delAll ${JSON.stringify(find)}`)
133
+ }
134
+ assert.deepStrictEqual(await rows(), before, 'delAll被拒後表內容不變')
135
+ })
136
+
113
137
  })
@@ -112,4 +112,18 @@ function same(actual, expected, label = '') {
112
112
  }
113
113
 
114
114
 
115
- export { createStub, getReject, quiet, same }
115
+ //mkForgedObjProto: 仿 Object.prototype 之偽原型(R-OBJ-01 規則 6, 外審第二輪 A-01、A-03): 原型為 null, 自有鍵名與描述子皆同本 realm 之 Object.prototype(鍵數相同),
116
+ //constructor 預設為本 realm 之 Object(但 Object.prototype 不是此偽原型, 後端要求原型須為本 realm 之 Object.prototype 或 null); 可傳 ctor 描述子覆寫 constructor
117
+ function mkForgedObjProto(ctor) {
118
+ let p = Object.create(null)
119
+ for (let k of Reflect.ownKeys(Object.prototype)) {
120
+ Object.defineProperty(p, k, Object.getOwnPropertyDescriptor(Object.prototype, k))
121
+ }
122
+ if (ctor !== undefined) {
123
+ Object.defineProperty(p, 'constructor', ctor)
124
+ }
125
+ return p
126
+ }
127
+
128
+
129
+ export { createStub, getReject, quiet, same, mkForgedObjProto }