w-orm-reladb 1.1.12 → 1.1.13

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.
@@ -0,0 +1,322 @@
1
+ import fs from 'fs'
2
+ import assert from 'assert'
3
+ import wo from '../src/WOrmReladb.mjs'
4
+ import { normPk, chkPkWf, chkWf } from '../src/cvFind.mjs'
5
+ import cvStrCases from 'wsemi/src/_cvOrmStrCases.mjs'
6
+
7
+
8
+ //字串良構之純函數與建構設定測試(不連線, CLAUDE_rulebook.md之R13)
9
+ //規格句(dspec〈一之三〉9.): 主鍵不得含孤立代理碼元或NUL, 寫入資料之字串值與鍵名不得含孤立代理碼元(值之NUL不拒), opt.db/opt.cl/opt.url(路徑承載者)含孤立代理碼元者建構時throw;
10
+ //訊息為固定詞彙且不回顯值: invalid id: <原因> at [data[<k>].<主鍵欄>]、invalid data: <原因> at [data[<k>].<路徑>]、invalid opt.<名>: <原因>
11
+ //案例表(wsemi之_cvOrmStrCases.mjs)之形態表forms供逐形態斷言; 對公開API之逐案執行見api-wellformed.mjs(sqlite與mssql)
12
+
13
+
14
+ let { forms, reasons } = cvStrCases
15
+
16
+ let formKeys = Object.keys(forms).filter((k) => !forms[k].long)
17
+
18
+ //whyId/whyVal, 形態在主鍵位置與資料位置之期望原因(主鍵併入NUL且NUL先於孤立代理; 資料值只檢孤立代理)
19
+ let whyId = (f) => (f.nul ? reasons.nul : f.lone ? reasons.lone : null)
20
+ let whyVal = (f) => (f.lone ? reasons.lone : null)
21
+
22
+ let msgOf = (err) => String(err && err.message !== undefined ? err.message : err)
23
+
24
+
25
+ describe('normPk 字串分支: 孤立代理碼元與NUL為無效主鍵(R13)', function() {
26
+
27
+ for (let kind of ['str', 'int', 'num', 'any']) {
28
+ for (let k of formKeys) {
29
+ let f = forms[k]
30
+ let bad = whyId(f) !== null
31
+ if (!bad) {
32
+ continue
33
+ }
34
+ it(`${kind}欄 ${k}(${f.label}) → {kind:'invalid'}, 形狀不變`, function() {
35
+ assert.deepStrictEqual(normPk(f.str, kind), { kind: 'invalid' })
36
+ assert.deepStrictEqual(normPk(Object(f.str), kind), { kind: 'invalid' }, 'String包裝物件取原始值後同處置')
37
+ })
38
+ }
39
+ }
40
+
41
+ it('良構字串(ASCII、中文、emoji、CJK擴充B、U+FFFD、組合字元、ZWJ)於字串類主鍵欄原樣接受', function() {
42
+ for (let k of formKeys) {
43
+ let f = forms[k]
44
+ if (whyId(f) !== null) {
45
+ continue
46
+ }
47
+ assert.deepStrictEqual(normPk(f.str, 'str'), { kind: 'ok', v: f.str }, k)
48
+ assert.deepStrictEqual(normPk(Object(f.str), 'str'), { kind: 'ok', v: f.str }, `${k} 包裝物件`)
49
+ assert.deepStrictEqual(normPk(f.str, 'any'), { kind: 'ok', v: f.str }, `${k} any`)
50
+ }
51
+ })
52
+
53
+ it('不合格字串於整數類與其他數值類主鍵欄亦為無效(而非型別不符)', function() {
54
+ for (let s of ['12\u0000', '\u00001', '1\uD800', '\uDC001']) {
55
+ assert.deepStrictEqual(normPk(s, 'int'), { kind: 'invalid' }, s.length + '')
56
+ assert.deepStrictEqual(normPk(s, 'num'), { kind: 'invalid' }, s.length + '')
57
+ }
58
+ //對照: 良構但型別不符者仍為mismatch
59
+ assert.deepStrictEqual(normPk('012', 'int'), { kind: 'mismatch' })
60
+ assert.deepStrictEqual(normPk('1e3', 'num'), { kind: 'mismatch' })
61
+ })
62
+
63
+ it('既有行為不變: 空字串無效、有限數字於字串欄轉字串、整數寫法字串於整數欄轉數值、NaN與物件無效', function() {
64
+ assert.deepStrictEqual(normPk('', 'str'), { kind: 'invalid' })
65
+ assert.deepStrictEqual(normPk(123, 'str'), { kind: 'ok', v: '123' })
66
+ assert.deepStrictEqual(normPk('12', 'int'), { kind: 'ok', v: 12 })
67
+ assert.deepStrictEqual(normPk('9007199254740993', 'int'), { kind: 'ok', v: '9007199254740993' })
68
+ assert.deepStrictEqual(normPk(NaN, 'str'), { kind: 'invalid' })
69
+ assert.deepStrictEqual(normPk({}, 'str'), { kind: 'invalid' })
70
+ assert.deepStrictEqual(normPk(null, 'any'), { kind: 'invalid' })
71
+ assert.deepStrictEqual(normPk(undefined, 'any'), { kind: 'invalid' })
72
+ })
73
+
74
+ })
75
+
76
+
77
+ describe('chkPkWf: 主鍵值之字串良構判定', function() {
78
+
79
+ for (let k of formKeys) {
80
+ let f = forms[k]
81
+ it(`${k}(${f.label}) → ${whyId(f)}`, function() {
82
+ assert.strictEqual(chkPkWf(f.str), whyId(f))
83
+ assert.strictEqual(chkPkWf(Object(f.str)), whyId(f), '包裝物件')
84
+ })
85
+ }
86
+
87
+ it('非字串回null(其有效性由normPk判定)', function() {
88
+ for (let v of [undefined, null, 1, NaN, true, {}, [], new Date(), Symbol('a')]) {
89
+ assert.strictEqual(chkPkWf(v), null)
90
+ }
91
+ })
92
+
93
+ })
94
+
95
+
96
+ describe('chkWf: 逐筆檢核寫入數據(主鍵 + 資料), 訊息固定詞彙且不回顯值', function() {
97
+
98
+ describe('主鍵位置: 逐形態', function() {
99
+ for (let k of formKeys) {
100
+ let f = forms[k]
101
+ let why = whyId(f)
102
+ it(`${k}(${f.label}) → ${why === null ? 'null' : `invalid id: ${why}`}`, function() {
103
+ let ms = chkWf([{ id: 'ok' }, { id: f.str, name: 'n' }], 'id', true)
104
+ assert.strictEqual(ms.length, 2)
105
+ assert.strictEqual(ms[0], null)
106
+ assert.strictEqual(ms[1], why === null ? null : `invalid id: ${why} at [data[1].id]`)
107
+ let ms2 = chkWf([{ id: f.str }], 'id', false)
108
+ assert.strictEqual(ms2[0], why === null ? null : `invalid id: ${why} at [data[0].id]`, 'withData:false 只檢主鍵亦同')
109
+ })
110
+ }
111
+ })
112
+
113
+ describe('資料位置: 逐形態 x 位置(頂層、巢狀、陣列、陣列內物件、鍵名、巢狀鍵名)', function() {
114
+ let pos = {
115
+ name: [(s) => ({ name: s }), 'name'],
116
+ nested: [(s) => ({ meta: { a: { s } } }), 'meta.a.s'],
117
+ array: [(s) => ({ tags: ['ok', s] }), 'tags[1]'],
118
+ arrayObj: [(s) => ({ list: [{ s }] }), 'list[0].s'],
119
+ key: [(s) => ({ [s]: 1 }), '<invalid key>'],
120
+ nestedKey: [(s) => ({ meta: { [s]: 1 } }), 'meta.<invalid key>'],
121
+ }
122
+ for (let [p, [make, path]] of Object.entries(pos)) {
123
+ for (let k of formKeys) {
124
+ let f = forms[k]
125
+ let why = whyVal(f)
126
+ it(`${p} x ${k}(${f.label}) → ${why === null ? 'null' : why}`, function() {
127
+ let ms = chkWf([{ id: 'a', name: 'n' }, { id: 'b', name: 'n', ...make(f.str) }], 'id', true)
128
+ assert.strictEqual(ms[0], null)
129
+ assert.strictEqual(ms[1], why === null ? null : `invalid data: ${why} at [data[1].${path}]`)
130
+ //del只檢主鍵: 資料不檢
131
+ assert.deepStrictEqual(chkWf([{ id: 'b', name: 'n', ...make(f.str) }], 'id', false), [null])
132
+ //訊息不回顯值
133
+ if (ms[1] !== null) {
134
+ assert.ok(!ms[1].includes(f.str), '訊息不得含被拒之字串')
135
+ assert.ok(ms[1].isWellFormed(), '訊息本身須良構')
136
+ }
137
+ })
138
+ }
139
+ }
140
+ })
141
+
142
+ it('同一文件主鍵先於資料; 同一物件鍵名先於值; 以索引最小者為準(各筆各自回報)', function() {
143
+ let H = '\uD800'
144
+ let L = '\uDC00'
145
+ let ms = chkWf([
146
+ { id: 'g0' },
147
+ { id: H, name: L },
148
+ { id: 'g2', name: L, [H]: 1 },
149
+ { id: 'g3', a: { s: H }, b: L },
150
+ ], 'id', true)
151
+ assert.deepStrictEqual(ms, [
152
+ null,
153
+ 'invalid id: string contains lone surrogate at [data[1].id]',
154
+ 'invalid data: string contains lone surrogate at [data[2].<invalid key>]',
155
+ 'invalid data: string contains lone surrogate at [data[3].a.s]',
156
+ ])
157
+ })
158
+
159
+ it('主鍵欄名可自訂(訊息之路徑用該欄名); 主鍵欄名不為id時仍檢該欄', function() {
160
+ let ms = chkWf([{ key: 'a\uD800', id: 'ok' }], 'key', true)
161
+ assert.deepStrictEqual(ms, ['invalid id: string contains lone surrogate at [data[0].key]'])
162
+ })
163
+
164
+ it('資料之NUL不拒(值與鍵名), 主鍵之NUL拒', function() {
165
+ let kNul = 'k\u0000'
166
+ assert.deepStrictEqual(chkWf([{ id: 'a', name: 'x\u0000y', [kNul]: 1, meta: { s: '\u0000' } }], 'id', true), [null])
167
+ assert.deepStrictEqual(chkWf([{ id: 'a\u0000b' }], 'id', true), ['invalid id: string contains NUL at [data[0].id]'])
168
+ })
169
+
170
+ it('非物件與空值之筆: 不拋錯(字串值依資料檢核, 其餘無字串可檢)', function() {
171
+ assert.deepStrictEqual(chkWf([null, undefined, 1, 'ok', {}], 'id', true), [null, null, null, null, null])
172
+ assert.deepStrictEqual(chkWf(['a\uD800'], 'id', true), ['invalid data: string contains lone surrogate at [data[0]]'])
173
+ })
174
+
175
+ it('實存型別之內容: Map(值與鍵)、Set、String包裝、Error、RegExp、繼承屬性皆檢核(不論欄位是否在model內)', function() {
176
+ let H = '\uD800'
177
+ let cases = [
178
+ [{ m: new Map([['k', H]]) }, 'm[0]'],
179
+ [{ m: new Map([[H, 'v']]) }, 'm[0].<key>'],
180
+ [{ st: new Set(['ok', H]) }, 'st[1]'],
181
+ [{ w: Object(H) }, 'w'],
182
+ [{ e: new Error(H) }, 'e.message'],
183
+ [{ r: new RegExp(H) }, 'r.source'],
184
+ [{ o: Object.create({ inh: H }) }, 'o.inh'],
185
+ [{ list: [{ [H]: 1 }] }, 'list[0].<invalid key>'],
186
+ ]
187
+ for (let [extra, path] of cases) {
188
+ let ms = chkWf([{ id: 'a', ...extra }], 'id', true)
189
+ assert.deepStrictEqual(ms, [`invalid data: string contains lone surrogate at [data[0].${path}]`], path)
190
+ }
191
+ })
192
+
193
+ it('包裝物件主鍵: 合格者將原始字串寫回該筆(令其後之正規化讀到的即被檢查者), valueOf只讀一次', function() {
194
+ let n = 0
195
+ let evil = {
196
+ [Symbol.toStringTag]: 'String',
197
+ valueOf() {
198
+ n++
199
+ return n === 1 ? 'evil-ok' : 'evil\uD800'
200
+ },
201
+ }
202
+ let d = [{ id: Object('wrap') }, { id: evil }]
203
+ assert.deepStrictEqual(chkWf(d, 'id', true), [null, null])
204
+ assert.strictEqual(n, 1)
205
+ assert.strictEqual(d[0].id, 'wrap')
206
+ assert.strictEqual(d[1].id, 'evil-ok')
207
+ assert.strictEqual(typeof d[0].id, 'string')
208
+ })
209
+
210
+ it('Number包裝物件主鍵: valueOf只求值一次, 原始數值寫回該筆', function() {
211
+ let n = 0
212
+ let num = {
213
+ [Symbol.toStringTag]: 'Number',
214
+ valueOf() {
215
+ n++
216
+ return n === 1 ? 42 : NaN
217
+ },
218
+ }
219
+ let d = [{ id: num }, { id: Object(7) }]
220
+ assert.deepStrictEqual(chkWf(d, 'id', true), [null, null])
221
+ assert.strictEqual(n, 1)
222
+ assert.strictEqual(d[0].id, 42)
223
+ assert.strictEqual(d[1].id, 7)
224
+ assert.strictEqual(typeof d[1].id, 'number')
225
+ })
226
+
227
+ it('不修改合格筆之其他欄位, 不為不合格筆寫回', function() {
228
+ let d = [{ id: 'a\uD800', name: 'x' }, { id: Object('ok'), name: 'y', n: { a: [1, 2] } }]
229
+ chkWf(d, 'id', true)
230
+ assert.strictEqual(d[0].id, 'a\uD800')
231
+ assert.deepStrictEqual(d[1], { id: 'ok', name: 'y', n: { a: [1, 2] } })
232
+ })
233
+
234
+ })
235
+
236
+
237
+ describe('建構設定: opt.db、opt.cl、opt.url、opt.storage含孤立代理碼元者建構時throw', function() {
238
+
239
+ let base = { url: 'sqlite://:', db: 'worm', cl: 'users', fdModels: './models', storage: './tmp/unit-wellformed.sqlite' }
240
+
241
+ for (let name of ['db', 'cl', 'url', 'storage']) {
242
+ for (let k of formKeys) {
243
+ let f = forms[k]
244
+ if (f.nul || f.unit) {
245
+ continue
246
+ }
247
+ let bad = f.lone
248
+ it(`opt.${name} ${k}(${f.label}) → ${bad ? 'throw' : '不throw'}`, function() {
249
+ let opt = { ...base, [name]: name === 'url' ? `${base.url}${f.str}` : f.str }
250
+ if (bad) {
251
+ assert.throws(() => wo(opt), (err) => err instanceof Error && msgOf(err) === `invalid opt.${name}: ${reasons.lone}`)
252
+ }
253
+ else {
254
+ assert.doesNotThrow(() => wo(opt))
255
+ }
256
+ })
257
+ }
258
+ }
259
+
260
+ it('訊息固定詞彙, 不回顯值; 先於opt.url之解析(url含孤立代理且dialect亦無效時仍回報良構錯誤)', function() {
261
+ assert.throws(() => wo({ ...base, url: 'nodialect://a\uD800' }), (err) => msgOf(err) === 'invalid opt.url: string contains lone surrogate')
262
+ assert.throws(() => wo({ ...base, url: 'sqlite://:\uD800' }), (err) => !msgOf(err).includes('\uD800'))
263
+ })
264
+
265
+ it('不套用預設值之前的非字串: 非字串之opt.db/opt.cl/opt.url/opt.storage改用預設值, 不throw(既有行為)', function() {
266
+ assert.doesNotThrow(() => wo({ ...base, db: 1, cl: null, storage: 5 }))
267
+ })
268
+
269
+ it('opt名稱之NUL不屬本規則(不throw本規則之錯誤)', function() {
270
+ assert.doesNotThrow(() => wo({ ...base, db: 'a\u0000b', cl: 'users\u0000' }))
271
+ })
272
+
273
+ it('同時多個不合格時依db、cl、url、storage順序回報第一個', function() {
274
+ assert.throws(() => wo({ ...base, cl: 'a\uD800', storage: 'b\uD800' }), (err) => msgOf(err) === 'invalid opt.cl: string contains lone surrogate')
275
+ assert.throws(() => wo({ ...base, db: 'a\uD800', cl: 'b\uD800' }), (err) => msgOf(err) === 'invalid opt.db: string contains lone surrogate')
276
+ })
277
+
278
+ })
279
+
280
+
281
+ describe('單一出處與接線站點掃描(盤點指令見R13)', function() {
282
+
283
+ //src, 讀原始碼並去除註解(區塊與行尾註解), 掃描對象為程式碼而非說明文字
284
+ let src = (fn) => fs.readFileSync(`./src/${fn}`, 'utf8').replace(/\r\n/g, '\n').replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '')
285
+ let count = (s, re) => (s.match(re) || []).length
286
+
287
+ it('src內不自寫孤立代理判定(單一擁有者為wsemi之chkOrmStr、chkOrmData)', function() {
288
+ for (let fn of fs.readdirSync('./src').filter((v) => /\.(mjs|js)$/.test(v))) {
289
+ let s = src(fn)
290
+ assert.strictEqual(count(s, /isWellFormed/g), 0, `${fn}: isWellFormed`)
291
+ assert.strictEqual(count(s, /\\uD800|\\uDC00|\\uDFFF|\\uDBFF/gi), 0, `${fn}: 代理碼元範圍`)
292
+ }
293
+ })
294
+
295
+ it('chkOrmStr、chkOrmData只在cvFind.mjs與WOrmReladb.mjs(opt名稱)接線', function() {
296
+ let c = src('cvFind.mjs')
297
+ assert.strictEqual(count(c, /import chkOrmStr from 'wsemi\/src\/chkOrmStr\.mjs'/g), 1)
298
+ assert.strictEqual(count(c, /import chkOrmData from 'wsemi\/src\/chkOrmData\.mjs'/g), 1)
299
+ assert.strictEqual(count(c, /chkOrmStr\(/g), 2, 'normPk與chkPkWf各一')
300
+ assert.strictEqual(count(c, /chkOrmData\(/g), 1, 'chkWf')
301
+ let w = src('WOrmReladb.mjs')
302
+ assert.strictEqual(count(w, /import chkOrmStr from 'wsemi\/src\/chkOrmStr\.mjs'/g), 1)
303
+ assert.strictEqual(count(w, /chkOrmStr\(/g), 1, '建構時opt名稱')
304
+ assert.strictEqual(count(w, /chkOrmData/g), 0)
305
+ })
306
+
307
+ it('寫入入口(insert、insertBulk、save、del)各一處chkWf, 皆在initSequelize之前; 數據於呼叫當下複製(snap), 函數內不再複製', function() {
308
+ let w = src('WOrmReladb.mjs')
309
+ assert.strictEqual(count(w, /chkWf\(/g), 4)
310
+ for (let name of ['insert', 'insertBulk', 'save', 'del']) {
311
+ let i = w.indexOf(` async function ${name}(`)
312
+ let m = w.slice(i + 10).match(/\n {4}(async )?function \w+\(/) //下一個函數(註解已去除)
313
+ let seg = w.slice(i, m === null ? w.length : i + 10 + m.index)
314
+ assert.strictEqual(count(seg, /chkWf\(/g), 1, `${name} chkWf`)
315
+ assert.ok(seg.indexOf('chkWf(') < seg.indexOf('await initSequelize()'), `${name}: chkWf須先於initSequelize`)
316
+ assert.ok(seg.indexOf('procClosed(instance)') < seg.indexOf('chkWf('), `${name}: procClosed須先於chkWf`)
317
+ assert.strictEqual(count(seg, /cloneDeep\(/g), 0, `${name}: 函數內不再複製(已於snap複製)`)
318
+ assert.strictEqual(count(w, new RegExp(`ee\\.${name} = snap\\(`, 'g')), 2, `${name}: 穩定與非穩定模式皆經snap`)
319
+ }
320
+ })
321
+
322
+ })