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.
package/src/getMapOrm.mjs CHANGED
@@ -423,6 +423,119 @@ function getRowId(row) {
423
423
  }
424
424
 
425
425
 
426
+ function hasOwnerUserId(row) {
427
+ //寫入列上之userId是否為擁有者: 只認自有、可列舉之資料屬性且為非空字串(與getRowId同一認定, 不執行getter), 其餘(缺欄、空字串、非字串、繼承、不可列舉)視為未提供
428
+ if (typeof row !== 'object' || row === null) {
429
+ return false
430
+ }
431
+ let d = Object.getOwnPropertyDescriptor(row, 'userId')
432
+ return d !== undefined && d.enumerable && !isAcc(d) && typeof d.value === 'string' && d.value !== ''
433
+ }
434
+
435
+
436
+ //procOrm第5參數opt允許之鍵(R-ONLYMOD-01、R-KEEPUSER-01), 皆為布林
437
+ const OPT_KEYS_PROC = ['mustExist', 'keepUserId']
438
+
439
+ //mustExist之存在檢查每次select之主鍵數上限(R-ONLYMOD-01 規則 11)
440
+ const EXIST_CHUNK = 1000
441
+
442
+
443
+ function getOptProc(mode, opt) {
444
+ //procOrm之第5參數opt: 未給(undefined、null)為空設定; 否則須為純物件且只含mustExist、keepUserId(皆布林), 其餘鍵、存取器、非布林者於任何讀寫之前reject
445
+ //拼錯之鍵(如mustexist)若被靜默忽略, 會令只改不增失效而重新長出已刪之列(keepUserId拼錯則擁有者被覆寫成操作者), 故fail-closed; 訊息不回顯呼叫端之值
446
+ //mustExist只適用save與del; keepUserId只適用insert(save現行即不覆寫userId, mix之新增為內部步驟不開放); 先逐一驗型別再驗mode, 結果與鍵之次序無關
447
+ let bad = (err) => ({ err, mustExist: false, keepUserId: false })
448
+ if (opt === undefined || opt === null) {
449
+ return { err: null, mustExist: false, keepUserId: false }
450
+ }
451
+ if (!isPlainObj(opt)) {
452
+ return bad('invalid opt of procOrm, need an object')
453
+ }
454
+ for (let k of Reflect.ownKeys(opt)) {
455
+ if (OPT_KEYS_PROC.indexOf(k) < 0) {
456
+ return bad('invalid opt of procOrm, unsupported key')
457
+ }
458
+ }
459
+ let flags = { mustExist: false, keepUserId: false }
460
+ for (let key of OPT_KEYS_PROC) {
461
+ let d = Object.getOwnPropertyDescriptor(opt, key)
462
+ if (d !== undefined) {
463
+ if (isAcc(d) || typeof d.value !== 'boolean') {
464
+ return bad(`invalid opt of procOrm, ${key} needs a boolean`)
465
+ }
466
+ flags[key] = d.value
467
+ }
468
+ }
469
+ if (flags.mustExist && mode !== 'save' && mode !== 'del') {
470
+ return bad('invalid opt of procOrm, mustExist supports only save and del')
471
+ }
472
+ if (flags.keepUserId && mode !== 'insert') {
473
+ return bad('invalid opt of procOrm, keepUserId supports only insert')
474
+ }
475
+ return { err: null, mustExist: flags.mustExist, keepUserId: flags.keepUserId }
476
+ }
477
+
478
+
479
+ async function hasMissingRow(wo, rows) {
480
+ //rows(寫入或刪除之列)是否有任一列不存在於資料表(含無有效主鍵者: 無從識別即視為不存在), 以select(id $in)分批查詢; true為有缺
481
+ //只認自有可列舉之資料屬性之id(getRowId), 與寫入與刪除實際使用之id同一; 比對以字串形式(整數主鍵與其字串視為同一)
482
+ //此讀取為ORM之select, ORM開啟useCache時可能非新鮮, 屬部署前提(單一行程、同表同一ORM實例、不經快取), 見規則帳本R-ONLYMOD-01
483
+ let ids = []
484
+ let seen = new Set()
485
+ for (let v of (isArr(rows) ? rows : [rows])) {
486
+ let id = getRowId(v)
487
+ if (!((typeof id === 'string' && id !== '') || isIdNumFinite(id))) {
488
+ return true
489
+ }
490
+ if (!seen.has(String(id))) {
491
+ seen.add(String(id))
492
+ ids.push(id)
493
+ }
494
+ }
495
+ //分批查詢: 各w-orm-*共用之條件檢核(wsemi cvOrmFind)限$in之元素數(預設10000, 只可下調), 超過者整批reject; 每批EXIST_CHUNK個, 任一批有缺即止
496
+ for (let k = 0; k < ids.length; k += EXIST_CHUNK) {
497
+ let part = ids.slice(k, k + EXIST_CHUNK)
498
+ let rs = await wo.select({ id: { $in: part } })
499
+ let found = new Set(map(isArr(rs) ? rs : [], (r) => String(get(r, 'id'))))
500
+ if (part.some((id) => !found.has(String(id)))) {
501
+ return true
502
+ }
503
+ }
504
+ return false
505
+ }
506
+
507
+
508
+ function getItemsDupId(rows) {
509
+ //mustExist之del: 同批重複之有效主鍵(第2次以後出現者)之位置[{k}]; 無效主鍵者不計(由存在檢查處置)
510
+ //重複者若放行, 第1筆刪除後第2筆必未命中, 事後偵測雖會reject卻已刪除, 違反[任一列不存在即零刪除], 故於刪除之前整批拒絕
511
+ let seen = new Set()
512
+ let rs = []
513
+ let arr = isArr(rows) ? rows : [rows]
514
+ for (let k = 0; k < arr.length; k++) {
515
+ let id = getRowId(arr[k])
516
+ if (!((typeof id === 'string' && id !== '') || isIdNumFinite(id))) {
517
+ continue
518
+ }
519
+ let s = String(id)
520
+ if (seen.has(s)) {
521
+ rs.push({ k })
522
+ }
523
+ else {
524
+ seen.add(s)
525
+ }
526
+ }
527
+ return rs
528
+ }
529
+
530
+
531
+ function hasNoHit(r, keyDel) {
532
+ //ORM之逐筆結果是否有已正常處理(ok非0)而未命中者(n為0; keyDel非null者另認該鍵為0, 如del之nDeleted): 表鎖紀律被破壞(檢查後、寫入前列被他方刪除)時之事後偵測
533
+ return (isArr(r) ? r : [r]).some((x) => {
534
+ return x !== null && typeof x === 'object' && x.ok !== 0 && (x.n === 0 || (keyDel !== null && x[keyDel] === 0))
535
+ })
536
+ }
537
+
538
+
426
539
  function firstBadIndex(a) {
427
540
  //陣列首個空洞或存取器索引, 回傳[索引, 'sparse'|'accessor'], 無則null; 成本為該索引之位置, 稀疏巨陣列於索引0即返回
428
541
  //索引為存取器者, 其後各步驟(each、map以array[index]讀取)會執行使用端程式並可每次回傳不同列(外審 sol F01)
@@ -922,7 +1035,7 @@ function setOwnData(o, k, v) {
922
1035
  }
923
1036
 
924
1037
 
925
- function adjustData(ds, woName, userId, data, mode = 'save') { //mode: 'insert', 'save', 'insert+save'
1038
+ function adjustData(ds, woName, userId, data, mode = 'save', keepUserId = false) { //mode: 'insert', 'save', 'insert+save'; keepUserId只由insert傳true(R-KEEPUSER-01)
926
1039
 
927
1040
  //check
928
1041
  if (!iseobj(ds)) {
@@ -946,9 +1059,12 @@ function adjustData(ds, woName, userId, data, mode = 'save') { //mode: 'insert',
946
1059
  //userId, timeCreate
947
1060
  if (mode.indexOf('insert') >= 0) {
948
1061
 
949
- //userId
1062
+ //userId, keepUserId者(opt.keepUserId, 列上userId為擁有者而非建立者)逐列保留自有非空字串之userId, 其餘以操作者補; userIdUpdate不受影響, 恆為操作者
950
1063
  if (keys.indexOf('userId') >= 0) {
951
1064
  each(data, (v, k) => {
1065
+ if (keepUserId && hasOwnerUserId(data[k])) {
1066
+ return
1067
+ }
952
1068
  setOwnData(data[k], 'userId', userId)
953
1069
  })
954
1070
  }
@@ -1028,11 +1144,11 @@ function adjustData(ds, woName, userId, data, mode = 'save') { //mode: 'insert',
1028
1144
  }
1029
1145
 
1030
1146
 
1031
- async function mapOrm(ds, woItems, userId, woName, mode, input) {
1147
+ async function mapOrm(ds, woItems, userId, woName, mode, input, opt) {
1032
1148
  //邊界保證(R-ERR-01 規則5): execMapOrm之檢查、轉型與ORM呼叫階段, 非字串之例外(RangeError、使用端物件之Proxy陷阱/toString拋錯、woItems之非ORM值等)
1033
1149
  //一律console.log原文後以固定字串reject, 不轉出例外物件與其拋出之值; 字串(各處之固定訊息)原樣轉出
1034
1150
  try {
1035
- return await execMapOrm(ds, woItems, userId, woName, mode, input)
1151
+ return await execMapOrm(ds, woItems, userId, woName, mode, input, opt)
1036
1152
  }
1037
1153
  catch (e) {
1038
1154
  if (typeof e === 'string') {
@@ -1044,7 +1160,7 @@ async function mapOrm(ds, woItems, userId, woName, mode, input) {
1044
1160
  }
1045
1161
 
1046
1162
 
1047
- async function execMapOrm(ds, woItems, userId, woName, mode, input) {
1163
+ async function execMapOrm(ds, woItems, userId, woName, mode, input, opt) {
1048
1164
  //由woName自動綁定至woItems內的ORM物件
1049
1165
  let err = null
1050
1166
  let r
@@ -1065,6 +1181,12 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
1065
1181
  //idCast, 主鍵之有限數字以其字串形式處置(R-ID-02), 寫入、刪除與查詢條件共用同一判定
1066
1182
  let idCast = isIdCast(ds, woName)
1067
1183
 
1184
+ //optProc, 第5參數opt(只改不增、刪除回報未命中, R-ONLYMOD-01), 於任何讀寫之前檢查
1185
+ let optProc = getOptProc(mode, opt)
1186
+ if (optProc.err !== null) {
1187
+ return Promise.reject(optProc.err)
1188
+ }
1189
+
1068
1190
  if (mode === 'select') {
1069
1191
 
1070
1192
  //check, 條件若給予須為一般物件且不含自有__proto__鍵(R-INPUT-01 規則3、R-OBJ-01 規則3), 於轉型與查詢之前
@@ -1130,7 +1252,7 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
1130
1252
 
1131
1253
  //adjustData
1132
1254
  try {
1133
- input = adjustData(ds, woName, userId, input, mode)
1255
+ input = adjustData(ds, woName, userId, input, mode, optProc.keepUserId)
1134
1256
  }
1135
1257
  catch (e) {
1136
1258
  console.log(`${woName}.adjustData catch`, e.toString())
@@ -1138,6 +1260,23 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
1138
1260
  return Promise.reject(err)
1139
1261
  }
1140
1262
 
1263
+ //exist, 只改不增(opt.mustExist, R-ONLYMOD-01): 寫入之前檢查全部列(含補主鍵後)皆已存在, 任一列不存在(含無主鍵之列)即整批reject 'rowNotFound'而不寫入任何一筆
1264
+ //檢查與寫入非原子, 呼叫端須已持有該表之表鎖(withTabLocks、guardWrite)
1265
+ if (mode === 'save' && optProc.mustExist) {
1266
+ let missing = await hasMissingRow(wo, input)
1267
+ .catch((msg) => {
1268
+ console.log(`${woName}.save exist check catch`, msg)
1269
+ err = 'failed to save data'
1270
+ })
1271
+ if (isestr(err)) {
1272
+ return Promise.reject(err)
1273
+ }
1274
+ if (missing) {
1275
+ console.log(`${woName}.save row not found`)
1276
+ return Promise.reject('rowNotFound')
1277
+ }
1278
+ }
1279
+
1141
1280
  //save
1142
1281
  if (mode === 'insert') {
1143
1282
  r = await wo.insert(input)
@@ -1147,7 +1286,8 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
1147
1286
  })
1148
1287
  }
1149
1288
  else if (mode === 'save') {
1150
- r = await wo.save(input)
1289
+ //只改不增者轉傳autoInsert:false(各ORM於未命中時不寫入); 其餘維持單參數呼叫
1290
+ r = await (optProc.mustExist ? wo.save(input, { autoInsert: false }) : wo.save(input))
1151
1291
  .catch((msg) => {
1152
1292
  console.log(`${woName}.save catch`, msg)
1153
1293
  err = 'failed to save data'
@@ -1167,6 +1307,12 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
1167
1307
  console.log(`${woName}.save fail`, fails)
1168
1308
  return Promise.reject(getMsgFail('failed to save data', getItemsFail(r, inputOrig)))
1169
1309
  }
1310
+
1311
+ //check, 只改不增者之事後偵測: 逐筆n為0表檢查後、寫入前該列被他方刪除(表鎖紀律被破壞), 已寫入之筆無從回復, 仍回報rowNotFound
1312
+ if (optProc.mustExist && hasNoHit(r, null)) {
1313
+ console.log(`${woName}.save row not found after check`)
1314
+ return Promise.reject('rowNotFound')
1315
+ }
1170
1316
  }
1171
1317
 
1172
1318
  //僅回傳id避免大量數據時造成負擔
@@ -1195,6 +1341,31 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
1195
1341
  input = castIdRows(input)
1196
1342
  }
1197
1343
 
1344
+ //exist, 刪除回報未命中(opt.mustExist, R-ONLYMOD-01): 刪除之前檢查全部列皆存在, 任一列不存在(含無主鍵之列)即整批reject 'rowNotFound'而不刪除任何一筆
1345
+ //檢查與刪除非原子, 呼叫端須已持有該表之表鎖(withTabLocks、guardWrite)
1346
+ if (optProc.mustExist) {
1347
+
1348
+ //check, 同批重複主鍵整批拒絕, 訊息列其位置而不回顯主鍵(同R-MIX-01)
1349
+ let failsDup = getItemsDupId(input)
1350
+ if (size(failsDup) > 0) {
1351
+ console.log(`${woName}.del duplicate id`, failsDup)
1352
+ return Promise.reject(getMsgFail('invalid input of del, duplicate id', failsDup))
1353
+ }
1354
+
1355
+ let missing = await hasMissingRow(wo, input)
1356
+ .catch((msg) => {
1357
+ console.log(`${woName}.del exist check catch`, msg)
1358
+ err = 'failed to delete data'
1359
+ })
1360
+ if (isestr(err)) {
1361
+ return Promise.reject(err)
1362
+ }
1363
+ if (missing) {
1364
+ console.log(`${woName}.del row not found`)
1365
+ return Promise.reject('rowNotFound')
1366
+ }
1367
+ }
1368
+
1198
1369
  //del
1199
1370
  r = await wo.del(input)
1200
1371
  .catch((msg) => {
@@ -1211,6 +1382,12 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
1211
1382
  }
1212
1383
  }
1213
1384
 
1385
+ //check, 刪除回報未命中者之事後偵測: 逐筆nDeleted或n為0表檢查後、刪除前該列被他方刪除(表鎖紀律被破壞), 已刪除之筆無從回復, 仍回報rowNotFound
1386
+ if (!isestr(err) && optProc.mustExist && hasNoHit(r, 'nDeleted')) {
1387
+ console.log(`${woName}.del row not found after check`)
1388
+ err = 'rowNotFound'
1389
+ }
1390
+
1214
1391
  }
1215
1392
  else if (mode === 'delAll') {
1216
1393
 
@@ -1465,11 +1642,12 @@ async function execMapOrm(ds, woItems, userId, woName, mode, input) {
1465
1642
 
1466
1643
 
1467
1644
  function getMapOrm(ds, woItems) {
1468
- //每次呼叫各自綁定ds與woItems並回傳新函數, 令同一行程內之多個實例互不干擾; 回傳函數之參數形狀維持(userId, woName, mode, input)
1469
- return async (userId, woName, mode, input) => {
1470
- return mapOrm(ds, woItems, userId, woName, mode, input)
1645
+ //每次呼叫各自綁定ds與woItems並回傳新函數, 令同一行程內之多個實例互不干擾; 回傳函數之參數形狀為(userId, woName, mode, input, opt), 前四個維持, opt為選用(R-ONLYMOD-01)
1646
+ return async (userId, woName, mode, input, opt) => {
1647
+ return mapOrm(ds, woItems, userId, woName, mode, input, opt)
1471
1648
  }
1472
1649
  }
1473
1650
 
1474
1651
 
1652
+ export { getOptProc }
1475
1653
  export default getMapOrm
package/src/getProc.mjs CHANGED
@@ -2,6 +2,7 @@ import iseobj from 'wsemi/src/iseobj.mjs'
2
2
  import pmSeries from 'wsemi/src/pmSeries.mjs'
3
3
  import isfun from 'wsemi/src/isfun.mjs'
4
4
  import checkOptRemoved from './checkOptRemoved.mjs'
5
+ import { getOptProc } from './getMapOrm.mjs'
5
6
 
6
7
 
7
8
  function addFun(fs, f, name, useGuard = false) {
@@ -28,11 +29,18 @@ function addFun(fs, f, name, useGuard = false) {
28
29
  }
29
30
 
30
31
 
31
- async function procOrm(st, userId, woName, mode, input) {
32
+ async function procOrm(st, userId, woName, mode, input, opt) {
32
33
  //封裝mapOrm, 依序執行擴充檢查、前處理、ORM操作與後處理, st為該實例之狀態
33
34
  //不做授權: 不檢查使用者亦不依使用者過濾數據, userId只供寫入稽核欄位與交給擴充函數; 開放給前端時須由開放處負責授權
34
35
  //st.mapOrm已於getProc建立st時驗證為函數, 且st不外露, 故此處不再檢查
35
36
 
37
+ //opt(第5參數: mustExist, R-ONLYMOD-01; keepUserId, R-KEEPUSER-01)先於任何擴充函數驗證: 無效之opt以固定字串拒收, 不得先執行擴充函數(其可能有寫入等副作用, 且check拋錯會遮蔽固定拒收字串);
38
+ //mapOrm內仍會再驗(直接使用getMapOrm者), 結果相同; opt不交給擴充函數(其參數物件形狀維持)
39
+ let optProc = getOptProc(mode, opt)
40
+ if (optProc.err !== null) {
41
+ return Promise.reject(optProc.err)
42
+ }
43
+
36
44
  //funChecks, 擴充檢查, 提供給各 mOrm[OOO].mjs 檢查
37
45
  await pmSeries(st.funChecks, (v) => {
38
46
  return v({ woName, userId, mode, input })
@@ -50,7 +58,8 @@ async function procOrm(st, userId, woName, mode, input) {
50
58
  }
51
59
 
52
60
  //output
53
- let output = await st.mapOrm(userId, woName, mode, input)
61
+ //opt(第5參數, R-ONLYMOD-01、R-KEEPUSER-01)交給mapOrm使用
62
+ let output = await st.mapOrm(userId, woName, mode, input, opt)
54
63
 
55
64
  //funPostProcessing, 擴充後處理output, 提供給各 mOrm[OOO].mjs 後處理
56
65
  //逐一呼叫, 各後處理皆收新建之參數物件, output為前一個之回傳值, 回傳值成為新output; 迭代前先取快照
@@ -96,8 +105,8 @@ function getProc(mapOrm, opt = {}) {
96
105
  //由外部添加擴充後置處理函數
97
106
  addFun(st.funPostProcessing, f, 'addFunPostProcessing')
98
107
  },
99
- procOrm: async (userId, woName, mode, input) => {
100
- return procOrm(st, userId, woName, mode, input)
108
+ procOrm: async (userId, woName, mode, input, opt) => {
109
+ return procOrm(st, userId, woName, mode, input, opt)
101
110
  },
102
111
  }
103
112
  }