w-serv-orm 1.1.11 → 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.
@@ -83,7 +83,8 @@ mix之各步驟遇單筆失敗不中斷後續步驟,待全部步驟完成後
83
83
  主鍵欄為字串型(或型別未知)之資料表,主鍵為有限數字者一律以其字串形式處置(如123視為'123'):寫入、resolve回傳、del與mix之刪除,以及select、delAll條件中主鍵之比較($eq、$ne、$gt、$gte、$lt、$lte、$in、$nin、$all,含$not、$and、$or、$nor之內)皆同,故以數字或其字串形式寫入、查詢與刪除者為同一筆,範圍比較依字串之字典序;主鍵欄為整數型者不轉型。轉型只作用於procOrm,經woItems直接操作者不轉型。舊版w-orm-mongodb(2025-04-29以前)保留之數字主鍵資料,以數字查詢將查不到。
84
84
  reject時不回傳補出之主鍵,故重送未帶主鍵之數據會再產生新主鍵而重複寫入,需冪等重送者請自帶主鍵(非空字串或有限數字);save帶主鍵者重送以主鍵覆寫,del重送時已刪除者為主鍵未命中。
85
85
  回傳之id為本套件依輸入(含補值與轉型)所得之主鍵,非ORM之寫入結果;ORM之比對規則較寬者(如w-orm-mdb之Jet不分大小寫、忽略尾端空白),insert略過或save命中之對象可能與回傳之id不同。lmdb、lowdb、level、mongodb等主鍵固定為字串id之ORM,其資料表之id須為字串型pk或不標型別;本套件以id為主鍵,主鍵欄非id之ORM(如w-orm-postgresql預設為time)之回傳id不具主鍵意義。
86
- 輸入之形狀:insert與save之各筆、mix之新增與修改之各筆須為物件,mix之add、del、diff若給予須為陣列,delAll之條件若給予須為物件,否則於任何寫入之前reject,如'failed to insert data, not an object: data[0]'、'invalid find of delAll, need an object'。
86
+ 輸入之形狀:insert與save之各筆、mix之新增與修改之各筆須為物件,mix之add、del、diff若給予須為陣列,select與delAll之條件若給予須為純物件(原型為null或Object.prototype;未給、undefined與空物件{}為無條件:select回全部、delAll刪除全部;null、陣列、字串、數字、布林、函數、Date、RegExp、Map、Set與類別實例皆reject),否則於任何寫入或查詢之前reject,如'failed to insert data, not an object: data[0]'、'invalid find of select, need an object'、'invalid find of delAll, need an object'。
87
+ 資料表名(procOrm之woName)須為本實例資料表之自有名稱,否則reject 'can not find table[x] in ORM'(原型鏈上之名稱如constructor、toString與路徑字串如tests.select亦然);select與delAll之條件之巢狀深度(物件與陣列各算一層)超過64層者reject 'invalid find of select, too deep'。條件與寫入列(insert、save、mix之新增與修改)之任何深度(一般物件之值、陣列之元素)含自有__proto__鍵(JSON.parse、Object.fromEntries、structuredClone等可產生;物件字面量之__proto__為設原型而不算)或存取器屬性(getter、setter)者整批reject,如'invalid find of select, unsupported key[<strong>proto</strong>]'、'failed to insert data, unsupported accessor property: data[0]',訊息不回顯客戶端之值。含整數length鍵之條件與寫入列本身依其自有鍵處置,不被當成陣列樣物件;欄位值內之巢狀物件之XSS清除由wsemi之clearXSS進行,不在此限(見規則帳本R-OBJ-01之已知限制)。mix之del各筆與單獨del之各筆不做上述檢查。
87
88
  mix之add與del、del與diff含同一主鍵(依上述轉型後)者,於任何步驟之前整批reject且不寫入任何一筆,訊息列其位置而不回顯主鍵,如'invalid input of mix, same id in add and del: add[0], del[1]';因mix依add、del、diff之序執行,同鍵將使新增因已存在而略過後即被刪除。
88
89
  insert(含mix之新增)為主鍵已存在即略過之冪等新增,回傳之id含被略過者;recover亦以insert還原,故資料表須先清空。
89
90
  insert、save與mix之新增、修改,寫入前各欄位之字串皆經wsemi之clearXSS清除(白名單外之標籤轉義,白名單內之標籤刪除白名單外之屬性與危險網址,標籤外之文字只轉義會開始標籤之&lt;,故'a &lt; b'等數學式不改寫);select與del不經清除。
@@ -92,7 +93,7 @@ insert、save與mix之新增、修改,寫入前各欄位之字串皆經wsemi
92
93
  呼叫契約:check以reject或拋錯阻擋,回傳值不影響結果,且早於前處理執行;前處理逐一呼叫,各收新建之參數物件{woName,userId,mode,input},input為前一個回傳之input,回傳須為含input之物件否則reject;後處理逐一呼叫,各收新建之參數物件且其output為前一個之回傳值,最後之回傳值即為procOrm之回傳值。
93
94
  後處理只在check、前處理與ORM操作皆成功後執行,逐筆失敗(含del)或mix部分寫入後reject者皆不執行;後處理自身亦可reject。後處理之input為前處理後之數據(無前處理者即呼叫端傳入之數據),不含本套件之補值與主鍵轉型,主鍵見output。
94
95
  如需觀察ORM實際寫入,可另監聽woItems內各ORM之change與error事件;惟事件由ORM發出,不含userId,mix會依步驟分次發出,經woItems直接寫入者亦會發出,insert之結果為聚合計數而非逐筆。
95
- 錯誤型別:procOrm由本套件產生之reject一律為英文訊息字串(供前端顯示或對應多語系;ORM之錯誤原文另由console.log輸出而不轉出),擴充函數自身之reject或拋錯則原樣轉出;建構(設定或ORM無效)、擴充函數註冊、backup與recover由本套件產生之錯誤一律為Error,ORM之錯誤則原樣轉出。</p>
96
+ 錯誤型別:procOrm由本套件產生之reject一律為英文訊息字串(供前端顯示或對應多語系;ORM之錯誤原文另由console.log輸出而不轉出;檢查與轉型階段之非預期例外,如使用端物件之Proxy陷阱拋錯、遞迴爆堆疊,統一為'failed to process input'),擴充函數自身之reject或拋錯則原樣轉出;建構(設定或ORM無效)、擴充函數註冊、backup與recover由本套件產生之錯誤一律為Error,ORM之錯誤則原樣轉出。</p>
96
97
  <p>授權:procOrm不做授權,不檢查使用者,亦不依使用者過濾數據;userId只供寫入稽核欄位(userId、userIdUpdate)與交給擴充函數。procOrm開放給前端時(如讓前端經資料通道直接操作資料表),須由開放處負責授權;addFunCheck可掛額外檢查,惟伺服端程式之呼叫亦會經過。
97
98
  已移除之設定:useCheckUser、getUserById、useExcludeWhenNotAdmin,未給、null或false者照常建構,給予其他值即於建構時拋錯。</p></li></ul></dd>
98
99
 
@@ -100,7 +101,7 @@ insert、save與mix之新增、修改,寫入前各欄位之字串皆經wsemi
100
101
 
101
102
  <dt class="tag-source">Source:</dt>
102
103
  <dd class="tag-source"><ul class="dummy"><li>
103
- <a href="WServOrm.mjs.html">WServOrm.mjs</a>, <a href="WServOrm.mjs.html#line100">line 100</a>
104
+ <a href="WServOrm.mjs.html">WServOrm.mjs</a>, <a href="WServOrm.mjs.html#line101">line 101</a>
104
105
  </li></ul></dd>
105
106
 
106
107
 
@@ -443,7 +444,7 @@ insert、save與mix之新增、修改,寫入前各欄位之字串皆經wsemi
443
444
  <br class="clear">
444
445
 
445
446
  <footer>
446
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Wed Oct 14 2026 07:49:10 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
447
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Oct 17 2026 05:14:34 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
447
448
  </footer>
448
449
 
449
450
  <script>prettyPrint();</script>
@@ -67,7 +67,8 @@ import getProc from './getProc.mjs'
67
67
  * 主鍵欄為字串型(或型別未知)之資料表,主鍵為有限數字者一律以其字串形式處置(如123視為'123'):寫入、resolve回傳、del與mix之刪除,以及select、delAll條件中主鍵之比較($eq、$ne、$gt、$gte、$lt、$lte、$in、$nin、$all,含$not、$and、$or、$nor之內)皆同,故以數字或其字串形式寫入、查詢與刪除者為同一筆,範圍比較依字串之字典序;主鍵欄為整數型者不轉型。轉型只作用於procOrm,經woItems直接操作者不轉型。舊版w-orm-mongodb(2025-04-29以前)保留之數字主鍵資料,以數字查詢將查不到。
68
68
  * reject時不回傳補出之主鍵,故重送未帶主鍵之數據會再產生新主鍵而重複寫入,需冪等重送者請自帶主鍵(非空字串或有限數字);save帶主鍵者重送以主鍵覆寫,del重送時已刪除者為主鍵未命中。
69
69
  * 回傳之id為本套件依輸入(含補值與轉型)所得之主鍵,非ORM之寫入結果;ORM之比對規則較寬者(如w-orm-mdb之Jet不分大小寫、忽略尾端空白),insert略過或save命中之對象可能與回傳之id不同。lmdb、lowdb、level、mongodb等主鍵固定為字串id之ORM,其資料表之id須為字串型pk或不標型別;本套件以id為主鍵,主鍵欄非id之ORM(如w-orm-postgresql預設為time)之回傳id不具主鍵意義。
70
- * 輸入之形狀:insert與save之各筆、mix之新增與修改之各筆須為物件,mix之add、del、diff若給予須為陣列,delAll之條件若給予須為物件,否則於任何寫入之前reject,如'failed to insert data, not an object: data[0]'、'invalid find of delAll, need an object'。
70
+ * 輸入之形狀:insert與save之各筆、mix之新增與修改之各筆須為物件,mix之add、del、diff若給予須為陣列,select與delAll之條件若給予須為純物件(原型為null或Object.prototype;未給、undefined與空物件{}為無條件:select回全部、delAll刪除全部;null、陣列、字串、數字、布林、函數、Date、RegExp、Map、Set與類別實例皆reject),否則於任何寫入或查詢之前reject,如'failed to insert data, not an object: data[0]'、'invalid find of select, need an object'、'invalid find of delAll, need an object'。
71
+ * 資料表名(procOrm之woName)須為本實例資料表之自有名稱,否則reject 'can not find table[x] in ORM'(原型鏈上之名稱如constructor、toString與路徑字串如tests.select亦然);select與delAll之條件之巢狀深度(物件與陣列各算一層)超過64層者reject 'invalid find of select, too deep'。條件與寫入列(insert、save、mix之新增與修改)之任何深度(一般物件之值、陣列之元素)含自有__proto__鍵(JSON.parse、Object.fromEntries、structuredClone等可產生;物件字面量之__proto__為設原型而不算)或存取器屬性(getter、setter)者整批reject,如'invalid find of select, unsupported key[__proto__]'、'failed to insert data, unsupported accessor property: data[0]',訊息不回顯客戶端之值。含整數length鍵之條件與寫入列本身依其自有鍵處置,不被當成陣列樣物件;欄位值內之巢狀物件之XSS清除由wsemi之clearXSS進行,不在此限(見規則帳本R-OBJ-01之已知限制)。mix之del各筆與單獨del之各筆不做上述檢查。
71
72
  * mix之add與del、del與diff含同一主鍵(依上述轉型後)者,於任何步驟之前整批reject且不寫入任何一筆,訊息列其位置而不回顯主鍵,如'invalid input of mix, same id in add and del: add[0], del[1]';因mix依add、del、diff之序執行,同鍵將使新增因已存在而略過後即被刪除。
72
73
  * insert(含mix之新增)為主鍵已存在即略過之冪等新增,回傳之id含被略過者;recover亦以insert還原,故資料表須先清空。
73
74
  * insert、save與mix之新增、修改,寫入前各欄位之字串皆經wsemi之clearXSS清除(白名單外之標籤轉義,白名單內之標籤刪除白名單外之屬性與危險網址,標籤外之文字只轉義會開始標籤之&lt;,故'a &lt; b'等數學式不改寫);select與del不經清除。
@@ -77,7 +78,7 @@ import getProc from './getProc.mjs'
77
78
  * 呼叫契約:check以reject或拋錯阻擋,回傳值不影響結果,且早於前處理執行;前處理逐一呼叫,各收新建之參數物件{woName,userId,mode,input},input為前一個回傳之input,回傳須為含input之物件否則reject;後處理逐一呼叫,各收新建之參數物件且其output為前一個之回傳值,最後之回傳值即為procOrm之回傳值。
78
79
  * 後處理只在check、前處理與ORM操作皆成功後執行,逐筆失敗(含del)或mix部分寫入後reject者皆不執行;後處理自身亦可reject。後處理之input為前處理後之數據(無前處理者即呼叫端傳入之數據),不含本套件之補值與主鍵轉型,主鍵見output。
79
80
  * 如需觀察ORM實際寫入,可另監聽woItems內各ORM之change與error事件;惟事件由ORM發出,不含userId,mix會依步驟分次發出,經woItems直接寫入者亦會發出,insert之結果為聚合計數而非逐筆。
80
- * 錯誤型別:procOrm由本套件產生之reject一律為英文訊息字串(供前端顯示或對應多語系;ORM之錯誤原文另由console.log輸出而不轉出),擴充函數自身之reject或拋錯則原樣轉出;建構(設定或ORM無效)、擴充函數註冊、backup與recover由本套件產生之錯誤一律為Error,ORM之錯誤則原樣轉出。
81
+ * 錯誤型別:procOrm由本套件產生之reject一律為英文訊息字串(供前端顯示或對應多語系;ORM之錯誤原文另由console.log輸出而不轉出;檢查與轉型階段之非預期例外,如使用端物件之Proxy陷阱拋錯、遞迴爆堆疊,統一為'failed to process input'),擴充函數自身之reject或拋錯則原樣轉出;建構(設定或ORM無效)、擴充函數註冊、backup與recover由本套件產生之錯誤一律為Error,ORM之錯誤則原樣轉出。
81
82
  *
82
83
  * 授權:procOrm不做授權,不檢查使用者,亦不依使用者過濾數據;userId只供寫入稽核欄位(userId、userIdUpdate)與交給擴充函數。procOrm開放給前端時(如讓前端經資料通道直接操作資料表),須由開放處負責授權;addFunCheck可掛額外檢查,惟伺服端程式之呼叫亦會經過。
83
84
  * 已移除之設定:useCheckUser、getUserById、useExcludeWhenNotAdmin,未給、null或false者照常建構,給予其他值即於建構時拋錯。
@@ -195,7 +196,7 @@ export default WServOrm
195
196
  <br class="clear">
196
197
 
197
198
  <footer>
198
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Wed Oct 14 2026 07:49:10 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
199
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Oct 17 2026 05:14:34 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
199
200
  </footer>
200
201
 
201
202
  <script>prettyPrint();</script>
package/docs/index.html CHANGED
@@ -71,7 +71,7 @@
71
71
  <br class="clear">
72
72
 
73
73
  <footer>
74
- Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Wed Oct 14 2026 07:49:10 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
74
+ Documentation generated by <a href="https://github.com/jsdoc3/jsdoc">JSDoc 4.0.5</a> on Sat Oct 17 2026 05:14:34 GMT+0800 (台北標準時間) using the <a href="https://github.com/clenemt/docdash">docdash</a> theme.
75
75
  </footer>
76
76
 
77
77
  <script>prettyPrint();</script>
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "w-serv-orm",
3
- "version": "1.1.11",
3
+ "version": "1.1.13",
4
4
  "main": "dist/w-serv-orm.umd.js",
5
5
  "dependencies": {
6
6
  "json5": "^2.2.3",
7
7
  "w-data-collector": "^1.0.46",
8
- "wsemi": "^1.9.15"
8
+ "wsemi": "^1.9.18"
9
9
  },
10
10
  "devDependencies": {
11
11
  "w-orm-lowdb": "^1.0.31",
12
- "w-package-tools": "^1.1.18"
12
+ "w-package-tools": "^1.1.20"
13
13
  },
14
14
  "scripts": {
15
15
  "test": "mocha --parallel \"test/*.test.mjs\" --timeout 180000",
package/src/WServOrm.mjs CHANGED
@@ -20,7 +20,8 @@ import getProc from './getProc.mjs'
20
20
  * 主鍵欄為字串型(或型別未知)之資料表,主鍵為有限數字者一律以其字串形式處置(如123視為'123'):寫入、resolve回傳、del與mix之刪除,以及select、delAll條件中主鍵之比較($eq、$ne、$gt、$gte、$lt、$lte、$in、$nin、$all,含$not、$and、$or、$nor之內)皆同,故以數字或其字串形式寫入、查詢與刪除者為同一筆,範圍比較依字串之字典序;主鍵欄為整數型者不轉型。轉型只作用於procOrm,經woItems直接操作者不轉型。舊版w-orm-mongodb(2025-04-29以前)保留之數字主鍵資料,以數字查詢將查不到。
21
21
  * reject時不回傳補出之主鍵,故重送未帶主鍵之數據會再產生新主鍵而重複寫入,需冪等重送者請自帶主鍵(非空字串或有限數字);save帶主鍵者重送以主鍵覆寫,del重送時已刪除者為主鍵未命中。
22
22
  * 回傳之id為本套件依輸入(含補值與轉型)所得之主鍵,非ORM之寫入結果;ORM之比對規則較寬者(如w-orm-mdb之Jet不分大小寫、忽略尾端空白),insert略過或save命中之對象可能與回傳之id不同。lmdb、lowdb、level、mongodb等主鍵固定為字串id之ORM,其資料表之id須為字串型pk或不標型別;本套件以id為主鍵,主鍵欄非id之ORM(如w-orm-postgresql預設為time)之回傳id不具主鍵意義。
23
- * 輸入之形狀:insert與save之各筆、mix之新增與修改之各筆須為物件,mix之add、del、diff若給予須為陣列,delAll之條件若給予須為物件,否則於任何寫入之前reject,如'failed to insert data, not an object: data[0]'、'invalid find of delAll, need an object'。
23
+ * 輸入之形狀:insert與save之各筆、mix之新增與修改之各筆須為物件,mix之add、del、diff若給予須為陣列,select與delAll之條件若給予須為純物件(原型為null或Object.prototype;未給、undefined與空物件{}為無條件:select回全部、delAll刪除全部;null、陣列、字串、數字、布林、函數、Date、RegExp、Map、Set與類別實例皆reject),否則於任何寫入或查詢之前reject,如'failed to insert data, not an object: data[0]'、'invalid find of select, need an object'、'invalid find of delAll, need an object'。
24
+ * 資料表名(procOrm之woName)須為本實例資料表之自有名稱,否則reject 'can not find table[x] in ORM'(原型鏈上之名稱如constructor、toString與路徑字串如tests.select亦然);select與delAll之條件之巢狀深度(物件與陣列各算一層)超過64層者reject 'invalid find of select, too deep'。條件與寫入列(insert、save、mix之新增與修改)之任何深度(一般物件之值、陣列之元素)含自有__proto__鍵(JSON.parse、Object.fromEntries、structuredClone等可產生;物件字面量之__proto__為設原型而不算)或存取器屬性(getter、setter)者整批reject,如'invalid find of select, unsupported key[__proto__]'、'failed to insert data, unsupported accessor property: data[0]',訊息不回顯客戶端之值。含整數length鍵之條件與寫入列本身依其自有鍵處置,不被當成陣列樣物件;欄位值內之巢狀物件之XSS清除由wsemi之clearXSS進行,不在此限(見規則帳本R-OBJ-01之已知限制)。mix之del各筆與單獨del之各筆不做上述檢查。
24
25
  * mix之add與del、del與diff含同一主鍵(依上述轉型後)者,於任何步驟之前整批reject且不寫入任何一筆,訊息列其位置而不回顯主鍵,如'invalid input of mix, same id in add and del: add[0], del[1]';因mix依add、del、diff之序執行,同鍵將使新增因已存在而略過後即被刪除。
25
26
  * insert(含mix之新增)為主鍵已存在即略過之冪等新增,回傳之id含被略過者;recover亦以insert還原,故資料表須先清空。
26
27
  * insert、save與mix之新增、修改,寫入前各欄位之字串皆經wsemi之clearXSS清除(白名單外之標籤轉義,白名單內之標籤刪除白名單外之屬性與危險網址,標籤外之文字只轉義會開始標籤之<,故'a < b'等數學式不改寫);select與del不經清除。
@@ -30,7 +31,7 @@ import getProc from './getProc.mjs'
30
31
  * 呼叫契約:check以reject或拋錯阻擋,回傳值不影響結果,且早於前處理執行;前處理逐一呼叫,各收新建之參數物件{woName,userId,mode,input},input為前一個回傳之input,回傳須為含input之物件否則reject;後處理逐一呼叫,各收新建之參數物件且其output為前一個之回傳值,最後之回傳值即為procOrm之回傳值。
31
32
  * 後處理只在check、前處理與ORM操作皆成功後執行,逐筆失敗(含del)或mix部分寫入後reject者皆不執行;後處理自身亦可reject。後處理之input為前處理後之數據(無前處理者即呼叫端傳入之數據),不含本套件之補值與主鍵轉型,主鍵見output。
32
33
  * 如需觀察ORM實際寫入,可另監聽woItems內各ORM之change與error事件;惟事件由ORM發出,不含userId,mix會依步驟分次發出,經woItems直接寫入者亦會發出,insert之結果為聚合計數而非逐筆。
33
- * 錯誤型別:procOrm由本套件產生之reject一律為英文訊息字串(供前端顯示或對應多語系;ORM之錯誤原文另由console.log輸出而不轉出),擴充函數自身之reject或拋錯則原樣轉出;建構(設定或ORM無效)、擴充函數註冊、backup與recover由本套件產生之錯誤一律為Error,ORM之錯誤則原樣轉出。
34
+ * 錯誤型別:procOrm由本套件產生之reject一律為英文訊息字串(供前端顯示或對應多語系;ORM之錯誤原文另由console.log輸出而不轉出;檢查與轉型階段之非預期例外,如使用端物件之Proxy陷阱拋錯、遞迴爆堆疊,統一為'failed to process input'),擴充函數自身之reject或拋錯則原樣轉出;建構(設定或ORM無效)、擴充函數註冊、backup與recover由本套件產生之錯誤一律為Error,ORM之錯誤則原樣轉出。
34
35
  *
35
36
  * 授權:procOrm不做授權,不檢查使用者,亦不依使用者過濾數據;userId只供寫入稽核欄位(userId、userIdUpdate)與交給擴充函數。procOrm開放給前端時(如讓前端經資料通道直接操作資料表),須由開放處負責授權;addFunCheck可掛額外檢查,惟伺服端程式之呼叫亦會經過。
36
37
  * 已移除之設定:useCheckUser、getUserById、useExcludeWhenNotAdmin,未給、null或false者照常建構,給予其他值即於建構時拋錯。
@@ -9,7 +9,6 @@ import fsIsFile from 'wsemi/src/fsIsFile.mjs'
9
9
  import iseobj from 'wsemi/src/iseobj.mjs'
10
10
  import isearr from 'wsemi/src/isearr.mjs'
11
11
  import isestr from 'wsemi/src/isestr.mjs'
12
- import haskey from 'wsemi/src/haskey.mjs'
13
12
  import isfun from 'wsemi/src/isfun.mjs'
14
13
  import isbol from 'wsemi/src/isbol.mjs'
15
14
  import j2o from 'wsemi/src/j2o.mjs'
@@ -78,7 +77,7 @@ async function recover(fp, woItems, genModels, needCreateStorage = false) {
78
77
  if (!isestr(name)) {
79
78
  throw new Error(`invalid backup data, rs[${k}].name is not a string`)
80
79
  }
81
- if (!haskey(woItems, name)) {
80
+ if (!Object.prototype.hasOwnProperty.call(woItems, name)) { //自有屬性, 不用haskey(in運算含原型鏈, constructor、toString等通過驗證後才在寫入時失敗)
82
81
  throw new Error(`invalid backup data, table[${name}] is not in woItems`)
83
82
  }
84
83
  })
package/src/getMapOrm.mjs CHANGED
@@ -71,9 +71,19 @@ function isPkIdString(dsTab, keys) {
71
71
  }
72
72
 
73
73
 
74
+ function getOwnItem(o, k) {
75
+ //取o之自有屬性k, 非自有、k非字串、o非物件或值為undefined者回傳null(R-INPUT-01 規則5)
76
+ //不用lodash get: 其會取到原型鏈上之值(__proto__、constructor、toString等)並解析路徑字串(如'tests.select'取到ORM之函數); 鍵本身含「.」者為自有屬性, 照常
77
+ if (o === null || typeof o !== 'object' || typeof k !== 'string' || !Object.prototype.hasOwnProperty.call(o, k)) {
78
+ return null
79
+ }
80
+ return o[k] === undefined ? null : o[k]
81
+ }
82
+
83
+
74
84
  function isIdCast(ds, woName) {
75
85
  //資料表之主鍵欄為字串型或型別未知者, 主鍵之有限數字以其字串形式處置(R-ID-02); ds無該表者不轉型, 令select與del照舊原樣交ORM
76
- let dsTab = get(ds, woName, null)
86
+ let dsTab = getOwnItem(ds, woName)
77
87
  let keys = get(dsTab, 'keys', null)
78
88
  if (!isarr(keys)) {
79
89
  return false
@@ -96,6 +106,171 @@ function castIdValue(v) {
96
106
  }
97
107
 
98
108
 
109
+ function mapOwn(o, f) {
110
+ //使用端物件之複製與逐鍵轉換(R-OBJ-01 規則1、2): 只走自有可列舉鍵, 回傳新物件, f收(值, 鍵)
111
+ //不用lodash each: 其對具非負整數length鍵之物件(如{length:1,name:'x'})採陣列樣迭代, 只走索引0..length-1而丟棄全部自有鍵
112
+ //以fromEntries建立自有屬性, 不用r[k]=v: k為__proto__時會改設r之原型而不建立自有屬性
113
+ return Object.fromEntries(Object.keys(o).map((k) => [k, f(o[k], k)]))
114
+ }
115
+
116
+
117
+ //不支援之鍵或屬性的原因與訊息(R-OBJ-01 規則3、5), 訊息只列固定字句, 不回顯客戶端之值
118
+ const MSGS_BAD_KEY = Object.freeze({
119
+ proto: 'unsupported key[__proto__]',
120
+ accessor: 'unsupported accessor property',
121
+ unreadable: 'unreadable value',
122
+ deep: 'too deep',
123
+ })
124
+
125
+ //find之巢狀深度上限, 以容器(一般物件與陣列)層數計, 根為第1層(R-OBJ-01 規則5)
126
+ //castIdFind、castIdCond以遞迴處理$and、$or、$nor與$not, 遞迴深度即呼叫堆疊深度, 實測字串型主鍵表約1173層$and(約2346個容器)起爆堆疊;
127
+ //64個容器層約為32層邏輯巢狀, 遠大於合理查詢(庫內最深約7個容器)而遠小於爆堆疊之閾值
128
+ const MAX_DEPTH_FIND = 64
129
+
130
+
131
+ function isPlainObj(v) {
132
+ //純物件(R-INPUT-01 規則3): isobj為真且原型為null, 或為某realm之Object.prototype(同lodash isPlainObject: 原型之原型為null、有constructor函數、其函數原始碼同Object, 故跨realm之物件亦通過)
133
+ //類別實例、Object.create(proto)、原型為null原型之一般物件、偽造Symbol.toStringTag之Map或包裝物件, 其自有可列舉鍵可能為空而被複製成{}, 令條件消失, 故不算
134
+ if (!isobj(v)) {
135
+ return false
136
+ }
137
+ let p = Object.getPrototypeOf(v)
138
+ if (p === null) {
139
+ return true
140
+ }
141
+ //p之原型為null, 故p.constructor只可能是p之自有屬性
142
+ return Object.getPrototypeOf(p) === null &&
143
+ typeof p.constructor === 'function' &&
144
+ Function.prototype.toString.call(p.constructor) === Function.prototype.toString.call(Object)
145
+ }
146
+
147
+
148
+ function logSafe(label, ...args) {
149
+ //診斷用之console.log: 輸出使用端原物件時會呼叫其util.inspect.custom, 拋錯不得使procOrm之reject變成例外物件(R-ERR-01 規則5)
150
+ try {
151
+ console.log(label, ...args)
152
+ }
153
+ catch (e) {
154
+ try {
155
+ console.log(label, '(unprintable)')
156
+ }
157
+ catch (e2) {
158
+ //放棄輸出
159
+ }
160
+ }
161
+ }
162
+
163
+
164
+ function getReasonBadKey(v, maxDepth = Infinity) {
165
+ //v之任何深度(一般物件之值、陣列之元素)含不支援之鍵或屬性者回傳原因(R-OBJ-01 規則3、5), 通過回傳null:
166
+ // proto: 含自有__proto__鍵, 不論可否列舉; 自有鍵可由JSON.parse、Object.fromEntries、展開、structuredClone等產生, 物件字面量之__proto__為設原型而不算
167
+ // accessor: 含存取器屬性(getter或setter), 其值每讀一次可能不同, 令檢查之值與使用之值無法保證相同, 且讀取可能有副作用
168
+ // unreadable: 檢查時拋錯, 如Proxy之陷阱或已撤銷之Proxy
169
+ // deep: 容器(一般物件與陣列)層數超過maxDepth(根為第1層), 走訪到第maxDepth+1層即停止; 只有find給予maxDepth, 寫入列不設上限(見R-OBJ-01 規則5)
170
+ //只走一般物件與陣列(JSON可表達之資料)之可列舉資料屬性, 以屬性描述子讀值而不呼叫getter; 其餘型別(Date、Map、Set、函數等)不進入
171
+ //以堆疊迭代避免本檢查爆呼叫堆疊(其後castIdFind、castIdCond之遞迴由maxDepth保護), seen避免循環結構無窮迭代(循環不會被判為過深, 見R-OBJ-01 已知限制)
172
+ try {
173
+ let seen = new Set()
174
+ let stack = [v, 1] //成對: 節點, 其層數
175
+ while (stack.length > 0) {
176
+ let depth = stack.pop()
177
+ let o = stack.pop()
178
+ let arr = Array.isArray(o) //不用isarr: 其以Object.prototype.toString判定, 可被Symbol.toStringTag偽造而走訪其length getter; 偽造者isobj亦為假, 故不進入
179
+ if (seen.has(o) || (!arr && !isobj(o))) {
180
+ continue
181
+ }
182
+ if (depth > maxDepth) {
183
+ return 'deep'
184
+ }
185
+ seen.add(o)
186
+ if (Object.prototype.hasOwnProperty.call(o, '__proto__')) {
187
+ return 'proto'
188
+ }
189
+ let ks = arr ? null : Object.keys(o)
190
+ let n = arr ? o.length : ks.length
191
+ for (let i = 0; i < n; i++) {
192
+ let d = Object.getOwnPropertyDescriptor(o, arr ? i : ks[i])
193
+ if (d === undefined) {
194
+ continue //稀疏陣列之空洞
195
+ }
196
+ if (d.get !== undefined || d.set !== undefined) {
197
+ return 'accessor'
198
+ }
199
+ if (typeof d.value === 'object' && d.value !== null) {
200
+ stack.push(d.value, depth + 1)
201
+ }
202
+ }
203
+ }
204
+ return null
205
+ }
206
+ catch (e) {
207
+ return 'unreadable'
208
+ }
209
+ }
210
+
211
+
212
+ function getErrFind(mode, find) {
213
+ //select、delAll之find檢查(R-INPUT-01 規則3、R-OBJ-01 規則3), 通過回傳null, 否則回傳reject之字串
214
+ //find給予(非undefined)須為純物件: 未給、undefined與空物件{}為無條件(select回全部、delAll刪全部), 各後端對非物件之find多視為無條件而回全表或刪全表, 故由本套件先擋
215
+ //任何深度不得含自有__proto__鍵(複製時該鍵會被丟棄, 令ORM所見之條件與呼叫端含授權函數所見者不同)與存取器屬性(見getReasonBadKey), 且巢狀深度不得超過MAX_DEPTH_FIND
216
+ try {
217
+ if (find !== undefined && !isPlainObj(find)) {
218
+ return `invalid find of ${mode}, need an object`
219
+ }
220
+ let reason = getReasonBadKey(find, MAX_DEPTH_FIND)
221
+ if (reason !== null) {
222
+ return `invalid find of ${mode}, ${MSGS_BAD_KEY[reason]}`
223
+ }
224
+ return null
225
+ }
226
+ catch (e) {
227
+ //isobj、取原型時拋錯, 如已撤銷之Proxy
228
+ return `invalid find of ${mode}, ${MSGS_BAD_KEY.unreadable}`
229
+ }
230
+ }
231
+
232
+
233
+ function getMsgsBadKey(msg, data) {
234
+ //寫入之各筆任何深度含不支援之鍵或屬性者(R-OBJ-01 規則3), 依原因分組回傳訊息陣列, 各組列其於輸入內之位置data[k]; 單一物件輸入視為第0筆
235
+ //輸入為陣列時(批次容器)逐索引以屬性描述子取值: 索引為存取器即reject, 使其後各次讀取皆為資料屬性之讀取而與檢查之值一致
236
+ let groups = new Map()
237
+ let addFail = (reason, k) => {
238
+ if (!groups.has(reason)) {
239
+ groups.set(reason, [])
240
+ }
241
+ groups.get(reason).push({ k })
242
+ }
243
+ if (Array.isArray(data)) {
244
+ for (let k = 0; k < data.length; k++) {
245
+ try {
246
+ let d = Object.getOwnPropertyDescriptor(data, k)
247
+ if (d === undefined) {
248
+ continue //空洞, 已於形狀檢查以非物件拒絕
249
+ }
250
+ if (d.get !== undefined || d.set !== undefined) {
251
+ addFail('accessor', k)
252
+ continue
253
+ }
254
+ let reason = getReasonBadKey(d.value)
255
+ if (reason !== null) {
256
+ addFail(reason, k)
257
+ }
258
+ }
259
+ catch (e) { //Proxy陣列之陷阱拋錯
260
+ addFail('unreadable', k)
261
+ }
262
+ }
263
+ }
264
+ else {
265
+ let reason = getReasonBadKey(data)
266
+ if (reason !== null) {
267
+ addFail(reason, 0)
268
+ }
269
+ }
270
+ return map([...groups], ([reason, fails]) => getMsgFail(`${msg}, ${MSGS_BAD_KEY[reason]}`, fails))
271
+ }
272
+
273
+
99
274
  function castIdRows(rows) {
100
275
  //寫入與刪除之數據: 各列之主鍵正規化, 寫於複本, 不改呼叫端之物件; 非物件之列原樣
101
276
  let one = !isarr(rows)
@@ -114,19 +289,15 @@ function castIdCond(c) {
114
289
  if (!isobj(c)) {
115
290
  return castIdValue(c)
116
291
  }
117
- let r = {}
118
- each(c, (v, k) => {
292
+ return mapOwn(c, (v, k) => {
119
293
  if (k === '$not') {
120
- r[k] = castIdCond(v)
294
+ return castIdCond(v)
121
295
  }
122
- else if (['$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$in', '$nin', '$all'].indexOf(k) >= 0) {
123
- r[k] = isarr(v) ? map(v, castIdValue) : castIdValue(v)
124
- }
125
- else {
126
- r[k] = v
296
+ if (['$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$in', '$nin', '$all'].indexOf(k) >= 0) {
297
+ return isarr(v) ? map(v, castIdValue) : castIdValue(v)
127
298
  }
299
+ return v
128
300
  })
129
- return r
130
301
  }
131
302
 
132
303
 
@@ -135,19 +306,15 @@ function castIdFind(find) {
135
306
  if (!isobj(find)) {
136
307
  return find
137
308
  }
138
- let r = {}
139
- each(find, (v, k) => {
309
+ return mapOwn(find, (v, k) => {
140
310
  if (k === 'id') {
141
- r[k] = castIdCond(v)
311
+ return castIdCond(v)
142
312
  }
143
- else if (['$and', '$or', '$nor'].indexOf(k) >= 0 && isarr(v)) {
144
- r[k] = map(v, castIdFind)
145
- }
146
- else {
147
- r[k] = v
313
+ if (['$and', '$or', '$nor'].indexOf(k) >= 0 && isarr(v)) {
314
+ return map(v, castIdFind)
148
315
  }
316
+ return v
149
317
  })
150
- return r
151
318
  }
152
319
 
153
320
 
@@ -310,13 +477,10 @@ function adjustData(ds, woName, userId, data, mode = 'save') { //mode: 'insert',
310
477
 
311
478
  //clearXSS
312
479
  if (true) {
313
- data = map(data, (v, k) => {
314
- let dt = {}
315
- each(v, (vv, kk) => {
316
- vv = clearXSS(vv)
317
- dt[kk] = vv
480
+ data = map(data, (v) => {
481
+ return mapOwn(v, (vv) => {
482
+ return clearXSS(vv)
318
483
  })
319
- return dt
320
484
  })
321
485
  }
322
486
 
@@ -344,6 +508,22 @@ function adjustData(ds, woName, userId, data, mode = 'save') { //mode: 'insert',
344
508
 
345
509
 
346
510
  async function mapOrm(ds, woItems, userId, woName, mode, input) {
511
+ //邊界保證(R-ERR-01 規則5): execMapOrm之檢查、轉型與ORM呼叫階段, 非字串之例外(RangeError、使用端物件之Proxy陷阱/toString拋錯、woItems之非ORM值等)
512
+ //一律console.log原文後以固定字串reject, 不轉出例外物件與其拋出之值; 字串(各處之固定訊息)原樣轉出
513
+ try {
514
+ return await execMapOrm(ds, woItems, userId, woName, mode, input)
515
+ }
516
+ catch (e) {
517
+ if (typeof e === 'string') {
518
+ return Promise.reject(e)
519
+ }
520
+ logSafe(`${typeof woName === 'string' ? woName : ''}.${typeof mode === 'string' ? mode : ''} mapOrm catch`, e)
521
+ return Promise.reject('failed to process input')
522
+ }
523
+ }
524
+
525
+
526
+ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
347
527
  //由woName自動綁定至woItems內的ORM物件
348
528
  let err = null
349
529
  let r
@@ -353,12 +533,12 @@ async function mapOrm(ds, woItems, userId, woName, mode, input) {
353
533
  return Promise.reject(`invalid woItems`)
354
534
  }
355
535
 
356
- //wo
357
- let wo = get(woItems, woName, null)
536
+ //wo, woName須為woItems之自有字串屬性(R-INPUT-01 規則5)
537
+ let wo = getOwnItem(woItems, woName)
358
538
 
359
- //check
539
+ //check, 訊息回顯字串之woName為既有慣例(R-MSG-01 其他已知限制), 非字串者不回顯
360
540
  if (wo === null) {
361
- return Promise.reject(`can not find table[${woName}] in ORM`)
541
+ return Promise.reject(`can not find table[${typeof woName === 'string' ? woName : ''}] in ORM`)
362
542
  }
363
543
 
364
544
  //idCast, 主鍵之有限數字以其字串形式處置(R-ID-02), 寫入、刪除與查詢條件共用同一判定
@@ -366,6 +546,13 @@ async function mapOrm(ds, woItems, userId, woName, mode, input) {
366
546
 
367
547
  if (mode === 'select') {
368
548
 
549
+ //check, 條件若給予須為一般物件且不含自有__proto__鍵(R-INPUT-01 規則3、R-OBJ-01 規則3), 於轉型與查詢之前
550
+ let errFind = getErrFind('select', input)
551
+ if (errFind !== null) {
552
+ logSafe(`${woName}.select invalid find`, input)
553
+ return Promise.reject(errFind)
554
+ }
555
+
369
556
  //select, 條件中主鍵之比較值轉型(R-ID-02)
370
557
  r = await wo.select(idCast ? castIdFind(input) : input)
371
558
  .catch((msg) => {
@@ -394,6 +581,13 @@ async function mapOrm(ds, woItems, userId, woName, mode, input) {
394
581
  return Promise.reject(getMsgFail(`${msgMode}, not an object`, failsNotObj))
395
582
  }
396
583
 
584
+ //check, 各筆任何深度不得含自有__proto__鍵與存取器屬性, 否則整批拒絕且不寫入任何一筆(R-OBJ-01 規則3)
585
+ let msgsBadKey = getMsgsBadKey(msgMode, input)
586
+ if (size(msgsBadKey) > 0) {
587
+ console.log(`${woName}.${mode} data has unsupported key`, msgsBadKey)
588
+ return Promise.reject(join(msgsBadKey, '; '))
589
+ }
590
+
397
591
  //castIdRows, 主鍵之有限數字轉為字串, 寫於複本(R-ID-02)
398
592
  if (idCast) {
399
593
  input = castIdRows(input)
@@ -485,10 +679,11 @@ async function mapOrm(ds, woItems, userId, woName, mode, input) {
485
679
  }
486
680
  else if (mode === 'delAll') {
487
681
 
488
- //check, 條件若給予須為一般物件(R-INPUT-01): 各後端對非物件之條件一律視為無條件而刪除全表; 未給(undefined)與空物件依規格為刪除全部
489
- if (input !== undefined && !isobj(input)) {
490
- console.log(`${woName}.delAll invalid find`, input)
491
- return Promise.reject('invalid find of delAll, need an object')
682
+ //check, 條件若給予須為一般物件(R-INPUT-01 規則3): 各後端對非物件之條件一律視為無條件而刪除全表; 未給(undefined)與空物件依規格為刪除全部; 另不得含自有__proto__鍵(R-OBJ-01 規則3)
683
+ let errFind = getErrFind('delAll', input)
684
+ if (errFind !== null) {
685
+ logSafe(`${woName}.delAll invalid find`, input)
686
+ return Promise.reject(errFind)
492
687
  }
493
688
 
494
689
  //delAll, 條件中主鍵之比較值轉型(R-ID-02)
@@ -506,6 +701,15 @@ async function mapOrm(ds, woItems, userId, woName, mode, input) {
506
701
  return Promise.reject('invalid input of mix, need the result object of ltdtDiffByKey')
507
702
  }
508
703
 
704
+ //check, add、del、diff為存取器者整批拒絕(R-OBJ-01 規則3 批次容器), 並取快照(各讀一次, 其後只用快照), 使各檢查與寫入所見一致
705
+ for (let key of ['add', 'del', 'diff']) {
706
+ let d = Object.getOwnPropertyDescriptor(input, key)
707
+ if (d !== undefined && (d.get !== undefined || d.set !== undefined)) {
708
+ return Promise.reject(`invalid input of mix, ${MSGS_BAD_KEY.accessor}`)
709
+ }
710
+ }
711
+ input = { add: input.add, del: input.del, diff: input.diff }
712
+
509
713
  //check, add、del、diff若給予須為陣列(R-INPUT-01), 否則各段之回傳將逐欄位產生
510
714
  for (let key of ['add', 'del', 'diff']) {
511
715
  if (input[key] !== undefined && !isarr(input[key])) {
@@ -526,6 +730,16 @@ async function mapOrm(ds, woItems, userId, woName, mode, input) {
526
730
  return Promise.reject(join(msgsNotObj, '; '))
527
731
  }
528
732
 
733
+ //check, 新增與修改之各筆任何深度不得含自有__proto__鍵與存取器屬性, 於任何步驟寫入之前整批拒絕(R-OBJ-01 規則3); del之各筆不檢查(R-OBJ-01 已知限制)
734
+ let msgsBadKey = []
735
+ for (let [key, msg] of [['add', 'failed to insert data'], ['diff', 'failed to update data']]) {
736
+ msgsBadKey.push(...getMsgsBadKey(msg, get(input, key, [])))
737
+ }
738
+ if (size(msgsBadKey) > 0) {
739
+ console.log(`${woName}.mix data has unsupported key`, msgsBadKey)
740
+ return Promise.reject(join(msgsBadKey, '; '))
741
+ }
742
+
529
743
  //castIdRows, 各段主鍵之有限數字轉為字串, 寫於複本(R-ID-02)
530
744
  if (idCast) {
531
745
  input = { ...input }
@@ -1,5 +1,5 @@
1
- import each from 'lodash-es/each.js'
2
- import keys from 'lodash-es/keys.js'
1
+ import each from 'lodash-es/each.js'
2
+ import keys from 'lodash-es/keys.js'
3
3
  import iseobj from 'wsemi/src/iseobj.mjs'
4
4
  import isfun from 'wsemi/src/isfun.mjs'
5
5
  import isestr from 'wsemi/src/isestr.mjs'
@@ -41,8 +41,8 @@ function getWoItems(ds, WOrm, url, db) {
41
41
  //wo
42
42
  let wo = new WOrm(useOpt)
43
43
 
44
- //save
45
- woItems[v] = wo
44
+ //save, 以defineProperty建立自有屬性: 不用woItems[v]=wo, ds之鍵為__proto__時會改設woItems之原型而該表不可用(R-INPUT-01 規則5)
45
+ Object.defineProperty(woItems, v, { value: wo, enumerable: true, writable: true, configurable: true })
46
46
 
47
47
  // //change, 當數據有改變時清除快取
48
48
  // wo.on('change', (mode) => {