w-serv-orm 1.1.16 → 1.1.17

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,624 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+
5
+ <meta charset="utf-8">
6
+ <title>baseline.mjs - Documentation</title>
7
+
8
+
9
+ <script src="scripts/prettify/prettify.js"></script>
10
+ <script src="scripts/prettify/lang-css.js"></script>
11
+ <!--[if lt IE 9]>
12
+ <script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
13
+ <![endif]-->
14
+ <link type="text/css" rel="stylesheet" href="styles/prettify.css">
15
+ <link type="text/css" rel="stylesheet" href="styles/jsdoc.css">
16
+ <script src="scripts/nav.js" defer></script>
17
+
18
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
19
+ </head>
20
+ <body>
21
+
22
+ <input type="checkbox" id="nav-trigger" class="nav-trigger" />
23
+ <label for="nav-trigger" class="navicon-button x">
24
+ <div class="navicon"></div>
25
+ </label>
26
+
27
+ <label for="nav-trigger" class="overlay"></label>
28
+
29
+ <nav >
30
+
31
+
32
+ <h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="WServOrm.html">WServOrm</a></li></ul><h3>Global</h3><ul><li><a href="global.html#buildBaseline">buildBaseline</a></li><li><a href="global.html#checkContract">checkContract</a></li><li><a href="global.html#compareBaseline">compareBaseline</a></li><li><a href="global.html#cyrb53">cyrb53</a></li><li><a href="global.html#digestRow">digestRow</a></li><li><a href="global.html#getRejectKey">getRejectKey</a></li><li><a href="global.html#isBaselineValid">isBaselineValid</a></li><li><a href="global.html#pickWriteRow">pickWriteRow</a></li></ul>
33
+
34
+ </nav>
35
+
36
+ <div id="main">
37
+
38
+ <h1 class="page-title">baseline.mjs</h1>
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+ <section>
47
+ <article>
48
+ <pre class="prettyprint source linenums"><code>//多人同時編輯之寫入契約: 列摘要與基準之純函數(規格 h1; 規則帳本 R-BASELINE-01)
49
+ //
50
+ //零 import、不使用任何 Node 專屬 API, 只依賴標準全域: 使用端(瀏覽器端與伺服端)以 w-serv-orm/src/baseline.mjs 深層匯入, 同一份原始碼算出同一摘要(規格 §8、V10)
51
+ //語法限制為 ES2017 以內(不用 ?. 與 ?? 與 ** 與 BigInt 字面值): 使用端之舊版打包器(如 webpack 4)不轉譯 node_modules 內之檔案
52
+ //物件之型別一律以 Object.prototype.toString 與原型鏈判定, 不用 instanceof: 跨 realm(iframe、vm)之 Date 與物件亦同結果
53
+ //末尾須維持單一 export { ... } 敘述: 同構測試以文字替換該敘述後於 vm 執行
54
+ //
55
+ //版本凍結: 演算法與欄位政策同版(h1)凍結, 改動即開 h2(規格 §5); 不識之格式或版本一律拒收 baselineFormat, 不得降級為不帶基準
56
+ //新增 h2 之作法: 新增 digestRowH2, 令 DIGESTERS 登錄 h2, isBaselineValid 之摘要格式改依登錄表判定, compareBaseline 以基準所載版本重算庫內列; h1 之函數與測試不動
57
+
58
+
59
+ //BASELINE_VERSION, 目前產出之摘要版本前綴
60
+ const BASELINE_VERSION = 'h1'
61
+
62
+ //BASELINE_REJECT, 固定英文之拒收鍵, 各使用端轉為自身語系鍵, 一律提示重新載入
63
+ const BASELINE_REJECT = Object.freeze({
64
+ mismatch: 'baselineMismatch',
65
+ rowNotFound: 'rowNotFound',
66
+ format: 'baselineFormat',
67
+ })
68
+
69
+ //摘要之16進位字元數: cyrb53為53位元, 16進位最多14字元, 補零至固定長度, 令格式可嚴格驗證
70
+ const DIGEST_HEX_LEN = 14
71
+
72
+ //單列走訪之深度與節點上限: 超過者拋錯(循環參考、共用子結構展開、異常巨大之欄位值), 不靜默
73
+ const MAX_DEPTH = 100
74
+ const MAX_NODES = 100000
75
+
76
+ //契約允許之鍵: 其餘(含拼錯之鍵, 如 orderkey 會使列序政策靜默失效)一律拋錯
77
+ const CONTRACT_KEYS = ['table', 'pk', 'fields', 'serverOnly', 'orderKey', 'scope']
78
+
79
+
80
+ function hasOwn(o, k) {
81
+ return Object.prototype.hasOwnProperty.call(o, k)
82
+ }
83
+
84
+
85
+ function tagOf(v) {
86
+ return Object.prototype.toString.call(v)
87
+ }
88
+
89
+
90
+ function isPlainObj(v) {
91
+ //純物件: 標籤為Object, 原型為null或其原型之原型為null(本realm或他realm之Object.prototype); 類別實例之原型鏈較長, 不算
92
+ if (v === null || typeof v !== 'object' || tagOf(v) !== '[object Object]') {
93
+ return false
94
+ }
95
+ let p = Object.getPrototypeOf(v)
96
+ return p === null || Object.getPrototypeOf(p) === null
97
+ }
98
+
99
+
100
+ function isStrNe(v) {
101
+ return typeof v === 'string' &amp;&amp; v !== ''
102
+ }
103
+
104
+
105
+ function mkErrKey(key, msg) {
106
+ //帶固定拒收鍵之Error, 供呼叫端依key分流
107
+ let e = new Error(msg)
108
+ e.key = key
109
+ return e
110
+ }
111
+
112
+
113
+ /**
114
+ * cyrb53: 53位元非密碼學雜湊(純JS), 用途為一致性比對, 非安全邊界; 以UTF-16碼元逐一計算, 各引擎結果相同
115
+ *
116
+ * @param {String} str 輸入字串
117
+ * @param {Number} [seed=0] 輸入種子整數,預設0
118
+ * @returns {Number} 回傳0至2^53-1之整數
119
+ */
120
+ function cyrb53(str, seed) {
121
+ let sd = typeof seed === 'number' ? seed : 0
122
+ let h1 = 0xdeadbeef ^ sd
123
+ let h2 = 0x41c6ce57 ^ sd
124
+ for (let i = 0; i &lt; str.length; i++) {
125
+ let ch = str.charCodeAt(i)
126
+ h1 = Math.imul(h1 ^ ch, 2654435761)
127
+ h2 = Math.imul(h2 ^ ch, 1597334677)
128
+ }
129
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909)
130
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909)
131
+ return 4294967296 * (2097151 &amp; h2) + (h1 >>> 0)
132
+ }
133
+
134
+
135
+ function chkNames(v, label, allowEmpty) {
136
+ //欄位名陣列: 非空字串、不重複
137
+ if (!Array.isArray(v) || (!allowEmpty &amp;&amp; v.length === 0)) {
138
+ throw new Error(`invalid contract: ${label} needs ${allowEmpty ? 'an array' : 'a non-empty array'}`)
139
+ }
140
+ let seen = new Set()
141
+ for (let i = 0; i &lt; v.length; i++) {
142
+ if (!isStrNe(v[i])) {
143
+ throw new Error(`invalid contract: ${label}[${i}] needs a non-empty string`)
144
+ }
145
+ if (seen.has(v[i])) {
146
+ throw new Error(`invalid contract: ${label} has a duplicate name at [${i}]`)
147
+ }
148
+ seen.add(v[i])
149
+ }
150
+ }
151
+
152
+
153
+ /**
154
+ * 驗證寫入契約(不合者拋Error,不靜默)
155
+ *
156
+ * 規則: 須為純物件且只含 table、pk、fields、serverOnly、orderKey、scope 六鍵;pk 為非空字串;fields 為非空且不重複之非空字串陣列;
157
+ * serverOnly 若給予須為不重複之非空字串陣列且與 fields 無交集;orderKey 若給予須為非空字串,不得在 fields 內,亦不得等於 pk;scope 若給予須為函數;table 若給予須為非空字串。
158
+ * 契約以讀取當下之值驗證,不快取;建議使用端於啟動時對每份契約呼叫一次。
159
+ *
160
+ * @param {Object} contract 輸入寫入契約
161
+ * @returns {Boolean} 回傳true(不合者拋Error)
162
+ */
163
+ function checkContract(contract) {
164
+ if (!isPlainObj(contract)) {
165
+ throw new Error('invalid contract: need an object')
166
+ }
167
+ //含不可列舉鍵與Symbol鍵(Object.keys只列可列舉之字串鍵, 會漏掉拼錯而不可列舉之鍵)
168
+ for (let k of Reflect.ownKeys(contract)) {
169
+ if (CONTRACT_KEYS.indexOf(k) &lt; 0) {
170
+ throw new Error('invalid contract: unsupported key')
171
+ }
172
+ }
173
+ if (!isStrNe(contract.pk)) {
174
+ throw new Error('invalid contract: pk needs a non-empty string')
175
+ }
176
+ chkNames(contract.fields, 'fields', false)
177
+ if (contract.serverOnly !== undefined &amp;&amp; contract.serverOnly !== null) {
178
+ chkNames(contract.serverOnly, 'serverOnly', true)
179
+ for (let f of contract.serverOnly) {
180
+ if (contract.fields.indexOf(f) >= 0) {
181
+ throw new Error('invalid contract: serverOnly and fields must not overlap')
182
+ }
183
+ }
184
+ }
185
+ if (contract.orderKey !== undefined &amp;&amp; contract.orderKey !== null) {
186
+ if (!isStrNe(contract.orderKey)) {
187
+ throw new Error('invalid contract: orderKey needs a non-empty string')
188
+ }
189
+ if (contract.fields.indexOf(contract.orderKey) >= 0) {
190
+ throw new Error('invalid contract: fields must not contain orderKey')
191
+ }
192
+ if (contract.orderKey === contract.pk) {
193
+ throw new Error('invalid contract: orderKey must not be pk')
194
+ }
195
+ }
196
+ if (contract.scope !== undefined &amp;&amp; contract.scope !== null &amp;&amp; typeof contract.scope !== 'function') {
197
+ throw new Error('invalid contract: scope needs a function')
198
+ }
199
+ if (contract.table !== undefined &amp;&amp; contract.table !== null &amp;&amp; !isStrNe(contract.table)) {
200
+ throw new Error('invalid contract: table needs a non-empty string')
201
+ }
202
+ return true
203
+ }
204
+
205
+
206
+ function canon(v, st, depth, label) {
207
+ //值之正規化並序列化為JSON文字(直接組字串, 不建中介物件: 鍵為__proto__時賦值會改設原型)
208
+ //巢狀值只依JSON語意: 物件鍵之值為undefined者略去(同JSON傳輸), 陣列之undefined元素為null, null與''各自保留不互相等價(遇歧義收嚴: 只有欄位頂層之空值等價)
209
+
210
+ st.n += 1
211
+ if (st.n > MAX_NODES) {
212
+ throw new Error(`unsupported value of ${label}: too many nodes`)
213
+ }
214
+ if (depth > MAX_DEPTH) {
215
+ throw new Error(`unsupported value of ${label}: too deep or circular`)
216
+ }
217
+
218
+ let t = typeof v
219
+
220
+ if (v === null || v === undefined) {
221
+ return 'null'
222
+ }
223
+ if (t === 'string') {
224
+ return JSON.stringify(v)
225
+ }
226
+ if (t === 'boolean') {
227
+ return v ? 'true' : 'false'
228
+ }
229
+ if (t === 'number') {
230
+ //NaN與±Infinity經JSON會靜默變成null, 與真正之null混同, 故拋錯
231
+ if (!isFinite(v)) {
232
+ throw new Error(`unsupported value of ${label}: non-finite number`)
233
+ }
234
+ return JSON.stringify(v)
235
+ }
236
+ if (t === 'bigint') {
237
+ return JSON.stringify(v.toString())
238
+ }
239
+
240
+ if (t === 'object') {
241
+
242
+ if (Array.isArray(v)) {
243
+ //陣列之自有可列舉非索引屬性(如toJSON函數)不入摘要, 但JSON傳輸時toJSON會改變內容而使兩端摘要不同, 故拋錯
244
+ let kv = Object.keys(v)
245
+ for (let i = 0; i &lt; kv.length; i++) {
246
+ let n = Number(kv[i])
247
+ if (!(String(n) === kv[i] &amp;&amp; n >= 0 &amp;&amp; n &lt; v.length &amp;&amp; Math.floor(n) === n)) {
248
+ throw new Error(`unsupported value of ${label}: array with extra properties`)
249
+ }
250
+ }
251
+ let ss = []
252
+ for (let i = 0; i &lt; v.length; i++) {
253
+ ss.push(canon(v[i], st, depth + 1, label))
254
+ }
255
+ return '[' + ss.join(',') + ']'
256
+ }
257
+
258
+ let tag = tagOf(v)
259
+
260
+ if (tag === '[object Date]') {
261
+ let s
262
+ try {
263
+ s = Date.prototype.toISOString.call(v)
264
+ }
265
+ catch (e) {
266
+ throw new Error(`unsupported value of ${label}: invalid Date`)
267
+ }
268
+ return JSON.stringify(s)
269
+ }
270
+
271
+ if (isPlainObj(v)) {
272
+ let ks = Object.keys(v).sort() //預設排序為UTF-16碼元序, 不用localeCompare
273
+ let ss = []
274
+ for (let i = 0; i &lt; ks.length; i++) {
275
+ let vv = v[ks[i]]
276
+ if (vv === undefined) {
277
+ continue
278
+ }
279
+ ss.push(JSON.stringify(ks[i]) + ':' + canon(vv, st, depth + 1, label))
280
+ }
281
+ return '{' + ss.join(',') + '}'
282
+ }
283
+
284
+ throw new Error(`unsupported value of ${label}: ${tag}`)
285
+ }
286
+
287
+ //function、symbol
288
+ throw new Error(`unsupported value of ${label}: ${t}`)
289
+ }
290
+
291
+
292
+ function canonField(v, st, label) {
293
+ //欄位頂層: undefined、null、''為同一空值, 其餘依canon; st為該列共用之節點計數
294
+ if (v === undefined || v === null || v === '') {
295
+ return 'null'
296
+ }
297
+ return canon(v, st, 0, label)
298
+ }
299
+
300
+
301
+ function isRowObj(row) {
302
+ //列須為物件(標籤為Object: 陣列、Map、Set、Date、RegExp等不算; 類別實例與Proxy包裝之一般物件照常, 只讀其自有屬性)
303
+ return row !== null &amp;&amp; typeof row === 'object' &amp;&amp; tagOf(row) === '[object Object]'
304
+ }
305
+
306
+
307
+ function digestOf(row, fields) {
308
+ //已驗證契約之單列摘要; 只讀自有屬性, 缺欄視為undefined
309
+ if (!isRowObj(row)) {
310
+ throw new Error('invalid row: need an object')
311
+ }
312
+ let ss = []
313
+ let st = { n: 0 }
314
+ for (let i = 0; i &lt; fields.length; i++) {
315
+ let f = fields[i]
316
+ ss.push(canonField(hasOwn(row, f) ? row[f] : undefined, st, `field[${f}]`))
317
+ }
318
+ let h = cyrb53('[' + ss.join(',') + ']').toString(16)
319
+ while (h.length &lt; DIGEST_HEX_LEN) {
320
+ h = '0' + h
321
+ }
322
+ return BASELINE_VERSION + ':' + h
323
+ }
324
+
325
+
326
+ /**
327
+ * 計算單列摘要(版本h1)
328
+ *
329
+ * 規則(規格 §5): 取 contract.fields 依宣告之序(orderKey與serverOnly不入);每值正規化後以JSON陣列序列化,取cyrb53,回傳 'h1:' + 14位16進位(補零)。
330
+ * 欄位頂層之 undefined、null、'' 為同一空值(含缺欄);Date 轉ISO字串;BigInt 轉十進位字串;物件鍵遞迴排序(UTF-16碼元序),陣列保序;數字與字串不互轉(1 與 '1' 不同);
331
+ * 巢狀值只依JSON語意(物件鍵之值為undefined者略去,陣列之undefined元素為null,巢狀之null與''不等價);
332
+ * 其餘非JSON值(function、symbol、NaN、±Infinity、無效Date、Map、Set、RegExp、類別實例等)與超過深度100或單列節點10萬者拋Error,不靜默略過;列本身須為一般物件(陣列、Map、Set、Date等拋Error)。
333
+ *
334
+ * @param {Object} row 輸入列物件(只讀自有屬性,不執行原型上之getter)
335
+ * @param {Object} contract 輸入寫入契約,見checkContract
336
+ * @returns {String} 回傳摘要字串,如 'h1:0123456789abcd'
337
+ */
338
+ function digestRow(row, contract) {
339
+ checkContract(contract)
340
+ return digestOf(row, contract.fields)
341
+ }
342
+
343
+
344
+ function pkOf(row, pk) {
345
+ //主鍵轉字串: 非空字串原樣、有限數字與BigInt轉十進位字串, 其餘(含空字串、null、undefined、布林、物件)為缺漏, 回傳null
346
+ //各後端回傳之主鍵型別不一(整數或字串), 轉字串使兩端同口徑; 依schema之轉型(R-ID-02)已由procOrm處理, 本函數只面對讀回之值
347
+ let v = hasOwn(row, pk) ? row[pk] : undefined
348
+ if (typeof v === 'string') {
349
+ return v === '' ? null : v
350
+ }
351
+ if (typeof v === 'number' &amp;&amp; isFinite(v)) {
352
+ return String(v)
353
+ }
354
+ if (typeof v === 'bigint') {
355
+ return v.toString()
356
+ }
357
+ return null
358
+ }
359
+
360
+
361
+ function orderOf(row, orderKey) {
362
+ //名次之排序值: 有限數字、BigInt、數字字串(非空白)依數值, 缺漏或無法轉數字者排最後(Infinity), 其間依主鍵碼元序決定性排列
363
+ //整數字串與BigInt保留為BigInt, 不轉Number(超過2^53即失精度而使不同名次被視為同值, 重排偵測漏判); BigInt與Number以 &lt; 與 > 比較為精確之數學比較
364
+ let v = hasOwn(row, orderKey) ? row[orderKey] : undefined
365
+ if (typeof v === 'number') {
366
+ return isFinite(v) ? v : Infinity
367
+ }
368
+ if (typeof v === 'bigint') {
369
+ return v
370
+ }
371
+ if (typeof v === 'string') {
372
+ let s = v.trim()
373
+ if (s === '') {
374
+ return Infinity
375
+ }
376
+ if (typeof BigInt === 'function' &amp;&amp; /^[+-]?\d+$/.test(s)) {
377
+ return BigInt(s)
378
+ }
379
+ let n = Number(s)
380
+ return isFinite(n) ? n : Infinity
381
+ }
382
+ return Infinity
383
+ }
384
+
385
+
386
+ /**
387
+ * 建立基準: 有序之 [[id, digest], ...]
388
+ *
389
+ * 規則(規格 §5): 先依 contract.scope 過濾(有則只留scope回傳真值之列);主鍵依 pkOf 轉字串,同批重複或缺漏拋帶 key 為 'baselineFormat' 之Error;
390
+ * 排序: 有 orderKey 依 (order數值, 主鍵碼元序),否則依主鍵碼元序,不用localeCompare;回傳陣列之序即名次: 他人重排會被偵測,系統重編號(名次不變)不會。
391
+ * 逐列存檔之基準即只含該列之 [[id, digest]]。
392
+ *
393
+ * @param {Array} rows 輸入列陣列(畫面所見之列,或伺服端新鮮讀取之列)
394
+ * @param {Object} contract 輸入寫入契約,見checkContract
395
+ * @returns {Array} 回傳 [[id, digest], ...]
396
+ */
397
+ function buildBaseline(rows, contract) {
398
+ checkContract(contract)
399
+ if (!Array.isArray(rows)) {
400
+ throw new Error('invalid rows: need an array')
401
+ }
402
+
403
+ let fields = contract.fields
404
+ let hasScope = typeof contract.scope === 'function'
405
+ let hasOrder = isStrNe(contract.orderKey)
406
+
407
+ let items = []
408
+ let seen = new Set()
409
+ for (let k = 0; k &lt; rows.length; k++) {
410
+ let row = rows[k]
411
+ if (!isRowObj(row)) {
412
+ throw new Error(`invalid row: rows[${k}] needs an object`)
413
+ }
414
+ if (hasScope &amp;&amp; !contract.scope(row)) {
415
+ continue
416
+ }
417
+ let id = pkOf(row, contract.pk)
418
+ if (id === null) {
419
+ throw mkErrKey(BASELINE_REJECT.format, `invalid row: rows[${k}] has no valid primary key`)
420
+ }
421
+ if (seen.has(id)) {
422
+ throw mkErrKey(BASELINE_REJECT.format, `invalid row: rows[${k}] has a duplicate primary key`)
423
+ }
424
+ seen.add(id)
425
+ items.push({ id, o: hasOrder ? orderOf(row, contract.orderKey) : 0, d: digestOf(row, fields) })
426
+ }
427
+
428
+ items.sort((a, b) => {
429
+ if (a.o &lt; b.o) {
430
+ return -1
431
+ }
432
+ if (a.o > b.o) {
433
+ return 1
434
+ }
435
+ if (a.id !== b.id) {
436
+ return a.id &lt; b.id ? -1 : 1 //字串比較依UTF-16碼元序
437
+ }
438
+ return 0
439
+ })
440
+
441
+ return items.map((v) => [v.id, v.d])
442
+ }
443
+
444
+
445
+ /**
446
+ * 判斷基準格式是否合法(含版本)
447
+ *
448
+ * 合法: 陣列,每項為長度2之陣列 [非空字串id, 摘要字串],摘要須為本模組已知版本之格式('h1:'加14位小寫16進位),id不得重複;空陣列合法(空表)。
449
+ * 不識之格式或版本一律不合法,使用端不得降級為不帶基準。
450
+ *
451
+ * @param {*} baseline 輸入客戶端送入之基準
452
+ * @returns {Boolean} 回傳是否合法
453
+ */
454
+ function isBaselineValid(baseline) {
455
+ if (!Array.isArray(baseline)) {
456
+ return false
457
+ }
458
+ let re = new RegExp('^' + BASELINE_VERSION + ':[0-9a-f]{' + DIGEST_HEX_LEN + '}$')
459
+ let seen = new Set()
460
+ for (let i = 0; i &lt; baseline.length; i++) {
461
+ let e = baseline[i]
462
+ if (!Array.isArray(e) || e.length !== 2 || !isStrNe(e[0]) || typeof e[1] !== 'string' || !re.test(e[1])) {
463
+ return false
464
+ }
465
+ if (seen.has(e[0])) {
466
+ return false
467
+ }
468
+ seen.add(e[0])
469
+ }
470
+ return true
471
+ }
472
+
473
+
474
+ /**
475
+ * 以基準比對目前之列(伺服端於表鎖內以新鮮讀取之列呼叫)
476
+ *
477
+ * 回傳 { ok: true }(相符:增、刪、改、名次皆無差異),或 { ok: false, reason, added, removed, changed, reordered }:
478
+ * reason 為固定英文拒收鍵,baselineFormat(基準格式或版本不識;此時 added、removed、changed 為空陣列、reordered 為 false,未比對)、
479
+ * rowNotFound(基準只含一列且該列已不在目前之列、且無新增列:逐列存檔時該列已被刪)、baselineMismatch(其餘不符);
480
+ * added 為基準沒有而目前有之id,removed 為基準有而目前已無之id,changed 為兩邊皆有而摘要不同之id,皆依各自陣列之序;
481
+ * reordered 為布林,兩邊共有之列其相對次序不同者為true(他人重排;系統重編號而名次不變者不算)。
482
+ * rowsNow 內之主鍵缺漏或重複拋 key 為 'rowsInvalid' 之Error(庫內資料異常,非客戶端之基準格式錯誤,不歸為baselineFormat),值不可摘要者拋Error;契約不合者拋Error。
483
+ *
484
+ * @param {Array} rowsNow 輸入目前之列(表鎖內之新鮮讀取;經ORM快取之讀取不得用)
485
+ * @param {Array} baseline 輸入客戶端送入之基準 [[id, digest], ...]
486
+ * @param {Object} contract 輸入寫入契約,見checkContract
487
+ * @returns {Object} 回傳比對結果
488
+ */
489
+ function compareBaseline(rowsNow, baseline, contract) {
490
+ checkContract(contract)
491
+
492
+ if (!isBaselineValid(baseline)) {
493
+ return { ok: false, reason: BASELINE_REJECT.format, added: [], removed: [], changed: [], reordered: false }
494
+ }
495
+
496
+ //庫內列之主鍵缺漏或重複為庫內資料異常, 非客戶端之基準格式錯誤: 以key為rowsInvalid之Error拋出, 使用端不得與baselineFormat(客戶端格式或版本不識)混為一類
497
+ let now
498
+ try {
499
+ now = buildBaseline(rowsNow, contract)
500
+ }
501
+ catch (e) {
502
+ if (e !== null &amp;&amp; typeof e === 'object' &amp;&amp; e.key === BASELINE_REJECT.format) {
503
+ let e2 = new Error(`invalid rowsNow: ${e.message}`)
504
+ e2.key = 'rowsInvalid'
505
+ throw e2
506
+ }
507
+ throw e
508
+ }
509
+
510
+ let mb = new Map(baseline)
511
+ let mn = new Map(now)
512
+
513
+ let added = []
514
+ let removed = []
515
+ let changed = []
516
+ for (let [id] of baseline) {
517
+ if (!mn.has(id)) {
518
+ removed.push(id)
519
+ }
520
+ }
521
+ for (let [id, d] of now) {
522
+ if (!mb.has(id)) {
523
+ added.push(id)
524
+ }
525
+ else if (mb.get(id) !== d) {
526
+ changed.push(id)
527
+ }
528
+ }
529
+
530
+ //reordered, 兩邊共有之id依各自次序之序列是否相同
531
+ let sb = baseline.map((v) => v[0]).filter((id) => mn.has(id))
532
+ let sn = now.map((v) => v[0]).filter((id) => mb.has(id))
533
+ let reordered = false
534
+ for (let i = 0; i &lt; sb.length; i++) {
535
+ if (sb[i] !== sn[i]) {
536
+ reordered = true
537
+ break
538
+ }
539
+ }
540
+
541
+ if (added.length === 0 &amp;&amp; removed.length === 0 &amp;&amp; changed.length === 0 &amp;&amp; !reordered) {
542
+ return { ok: true }
543
+ }
544
+
545
+ let reason = BASELINE_REJECT.mismatch
546
+ if (baseline.length === 1 &amp;&amp; removed.length === 1 &amp;&amp; added.length === 0) {
547
+ reason = BASELINE_REJECT.rowNotFound
548
+ }
549
+ return { ok: false, reason, added, removed, changed, reordered }
550
+ }
551
+
552
+
553
+ /**
554
+ * 取得寫入列(存檔採用客戶端值之欄位白名單): 只含 contract.pk 與 contract.fields 內且列上自有、值非undefined之欄位
555
+ *
556
+ * 規格 §4、V5: 伺服器專有欄位(serverOnly)與系統欄位存檔不採用客戶端值; 客戶端若持舊列(含舊之伺服器專有欄位值)整列送出,
557
+ * 寫入前須經此白名單,否則會把系統已改之值回寫成舊值。未列入 fields 者(含 orderKey)不輸出, 由使用端自行補(如依列序重編之 order)。
558
+ * 回傳新物件(淺複製, 巢狀值為同一參考; 寫入 ORM 時 ORM 自行複製)。
559
+ *
560
+ * @param {Object} row 輸入列物件
561
+ * @param {Object} contract 輸入寫入契約,見checkContract
562
+ * @returns {Object} 回傳只含白名單欄位之新物件
563
+ */
564
+ function pickWriteRow(row, contract) {
565
+ checkContract(contract)
566
+ if (!isRowObj(row)) {
567
+ throw new Error('invalid row: need an object')
568
+ }
569
+ let o = {}
570
+ let ks = [contract.pk].concat(contract.fields)
571
+ for (let i = 0; i &lt; ks.length; i++) {
572
+ let k = ks[i]
573
+ if (hasOwn(row, k) &amp;&amp; row[k] !== undefined) {
574
+ //以defineProperty建立自有屬性, 不用賦值: 鍵為__proto__時賦值會改設原型
575
+ Object.defineProperty(o, k, { value: row[k], writable: true, enumerable: true, configurable: true })
576
+ }
577
+ }
578
+ return o
579
+ }
580
+
581
+
582
+ /**
583
+ * 取得拒收鍵: 同時處理 procOrm 之字串 reject(如 'rowNotFound')與 guardWrite 之 Error reject(帶 key)
584
+ *
585
+ * @param {*} err 輸入被 reject 之值
586
+ * @returns {String|null} 回傳拒收鍵字串; 不是字串亦無字串 key 者(如一般例外)回 null
587
+ */
588
+ function getRejectKey(err) {
589
+ if (typeof err === 'string') {
590
+ return err
591
+ }
592
+ if (err !== null &amp;&amp; typeof err === 'object' &amp;&amp; typeof err.key === 'string') {
593
+ return err.key
594
+ }
595
+ return null
596
+ }
597
+
598
+
599
+ export { BASELINE_VERSION, BASELINE_REJECT, cyrb53, checkContract, digestRow, buildBaseline, isBaselineValid, compareBaseline, pickWriteRow, getRejectKey }
600
+ </code></pre>
601
+ </article>
602
+ </section>
603
+
604
+
605
+
606
+
607
+
608
+
609
+ </div>
610
+
611
+ <br class="clear">
612
+
613
+ <footer>
614
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Fri Oct 23 2026 00:29:00 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
615
+ </footer>
616
+
617
+ <script>prettyPrint();</script>
618
+ <script src="scripts/polyfill.js"></script>
619
+ <script src="scripts/linenumber.js"></script>
620
+
621
+
622
+
623
+ </body>
624
+ </html>