@questwork/q-utilities 0.1.14 → 0.1.16

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,15 +1,15 @@
1
1
 
2
2
  ;// ./lib/helpers/authorize/authorize.js
3
- function authorize({ allowOwner, query = {}, required, user }) {
3
+ function authorize({ allowCoordinator, allowOwner, query = {}, required, user }) {
4
4
  if (!user) {
5
5
  throw new Error('Require login.')
6
6
  }
7
7
  if (!user.permission) {
8
- throw new Error('You do not have any permission.')
8
+ // throw new Error('You do not have any permission.')
9
9
  }
10
- const scopes = user.permission.getScopes(required || {})
10
+ const scopes = user.permission.getScopes(required || {}) || []
11
11
  if (!scopes || scopes.length === 0) {
12
- throw new Error('You are not allowed in this scope.')
12
+ // throw new Error('You are not allowed in this scope.')
13
13
  }
14
14
  if (!scopes.includes('*')) {
15
15
  query.tenantCode = user.tenantCode
@@ -17,8 +17,21 @@ function authorize({ allowOwner, query = {}, required, user }) {
17
17
  if (!scopes.includes('TENANT')) {
18
18
  query.eventShortCode = user.eventShortCode
19
19
  }
20
- if (!scopes.includes('EVENT')) {
21
- query.eventRegistrationCode = user.eventRegistrationCode
20
+ // if (!scopes.includes('EVENT')) {
21
+ // query.eventRegistrationCode = user.eventRegistrationCode
22
+ // }
23
+ if (allowCoordinator) {
24
+ if (query.registrationGroupCode && user.myManagedRegistrationGroupCodes.includes(query.registrationGroupCode)) {
25
+ query.__ALLOW_COORDINATOR = true
26
+ } else {
27
+ if (!scopes.includes('EVENT')) {
28
+ query.eventRegistrationCode = user.eventRegistrationCode
29
+ }
30
+ }
31
+ } else {
32
+ if (!scopes.includes('EVENT')) {
33
+ query.eventRegistrationCode = user.eventRegistrationCode
34
+ }
22
35
  }
23
36
  if (allowOwner) {
24
37
  query.__ALLOW_OWNER = true
@@ -34,6 +47,29 @@ function authorize({ allowOwner, query = {}, required, user }) {
34
47
  ;// ./lib/helpers/authorize/index.js
35
48
 
36
49
 
50
+ ;// ./lib/helpers/changeCreatorOwner/changeCreatorOwner.js
51
+ function changeCreatorOwner(that, { source, target }) {
52
+ if (that.meta) {
53
+ if (!that.meta.creator || that.meta.creator === source.getId()) {
54
+ that.meta.creator = target.getId()
55
+ }
56
+ if (!that.meta.owner || that.meta.owner === source.getId()) {
57
+ that.meta.owner = target.getId()
58
+ }
59
+ } else {
60
+ if (!that.creator || that.creator === source.getId()) {
61
+ that.creator = target.getId()
62
+ }
63
+ if (!that.owner || that.owner === source.getId()) {
64
+ that.owner = target.getId()
65
+ }
66
+ }
67
+ return that
68
+ }
69
+
70
+ ;// ./lib/helpers/changeCreatorOwner/index.js
71
+
72
+
37
73
  ;// ./lib/helpers/getValidation/getValidation.js
38
74
  function getValidation(rule, data, getDataByKey, KeyValueObject) {
39
75
  if (!rule) {
@@ -42,10 +78,10 @@ function getValidation(rule, data, getDataByKey, KeyValueObject) {
42
78
  if (typeof getDataByKey !== 'function' || (KeyValueObject && typeof KeyValueObject !== 'function')) {
43
79
  return false
44
80
  }
45
- const { key = '', value, keyValuePath = '' } = rule
81
+ const { key = '', value, placeholder, keyValuePath = '' } = rule
46
82
  const [valueAttribute] = Object.keys(value)
47
83
 
48
- if (!key) {
84
+ if (!key && typeof placeholder === 'undefined') {
49
85
  switch (valueAttribute) {
50
86
  case '$and': {
51
87
  return value['$and'].reduce((acc, item) => (acc && getValidation(item, data, getDataByKey, KeyValueObject)), true)
@@ -57,14 +93,10 @@ function getValidation(rule, data, getDataByKey, KeyValueObject) {
57
93
  return false
58
94
  }
59
95
  }
60
-
61
- let rowValue = getDataByKey(key, data)
62
-
63
- // debugger
96
+ let rowValue = typeof placeholder === 'undefined' ? getDataByKey(key, data) : placeholder
64
97
 
65
98
  // if KeyValue object
66
99
  if (keyValuePath) {
67
- console.log('keyValuePath', keyValuePath)
68
100
  const rowValueData = KeyValueObject.toObject(rowValue)
69
101
  rowValue = getDataByKey(keyValuePath, rowValueData)
70
102
  }
@@ -83,6 +115,9 @@ function getValidation(rule, data, getDataByKey, KeyValueObject) {
83
115
  case '$gte': {
84
116
  return rowValue >= value['$gte']
85
117
  }
118
+ case '$hasOverlap': {
119
+ return _hasOverlap(rowValue, value['$hasOverlap'])
120
+ }
86
121
  case '$lt': {
87
122
  return rowValue < value['$lt']
88
123
  }
@@ -159,6 +194,20 @@ function getValidation(rule, data, getDataByKey, KeyValueObject) {
159
194
  default:
160
195
  return false
161
196
  }
197
+
198
+ }
199
+
200
+ function _hasOverlap(item1, item2) {
201
+ let arr1 = item1
202
+ let arr2 = item2
203
+ if (typeof arr1 === 'string') {
204
+ arr1 = arr1.split(',')
205
+ }
206
+ if (typeof arr2 === 'string') {
207
+ arr2 = arr2.split(',')
208
+ }
209
+ const set1 = new Set(arr1)
210
+ return arr2.find((i) => (set1.has(i)))
162
211
  }
163
212
 
164
213
  /* harmony default export */ const getValidation_getValidation = ({
@@ -1362,7 +1411,7 @@ function convertString(string, patternMatch = /\$\{(.+?)\}/g, value, getValueByK
1362
1411
  if (!string) {
1363
1412
  return ''
1364
1413
  }
1365
- let _getValueByKeys = typeof getValueByKeys !== 'function' ? getValueByKeys : getValueByKeys_getValueByKeys
1414
+ let _getValueByKeys = typeof getValueByKeys === 'function' ? getValueByKeys : getValueByKeys_getValueByKeys
1366
1415
  const reg = new RegExp(patternMatch, 'g')
1367
1416
  return string.replace(reg, (match, key) => {
1368
1417
  const result = _getValueByKeys({ keys: key.split('.'), obj: value })
@@ -1381,6 +1430,161 @@ function convertString(string, patternMatch = /\$\{(.+?)\}/g, value, getValueByK
1381
1430
  ;// ./lib/helpers/convertString/index.js
1382
1431
 
1383
1432
 
1433
+ ;// ./lib/helpers/escapeRegex/escapeRegex.js
1434
+ function escapeRegex(string) {
1435
+ return String(string).replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
1436
+ }
1437
+
1438
+ ;// ./lib/helpers/escapeRegex/index.js
1439
+
1440
+
1441
+ ;// ./lib/helpers/expressHelper/customHandler.js
1442
+ function customHandler({ responseHelper, handler, ignoreError = false }) {
1443
+ return async (req, res, next) => {
1444
+ try {
1445
+ await handler({ req, res })
1446
+ await next()
1447
+ } catch (err) {
1448
+ if (ignoreError) {
1449
+ await next()
1450
+ } else {
1451
+ res.status(400).json(responseHelper.standardizeResponse({ err, message: err.message || err }))
1452
+ }
1453
+ }
1454
+ }
1455
+ }
1456
+
1457
+ ;// ./lib/helpers/expressHelper/findAllResult.js
1458
+ function findAllResult({ responseHelper, service }) {
1459
+ return async (req, res, next) => {
1460
+ try {
1461
+ const { query } = req
1462
+ const result = await service.findAll({ query })
1463
+ res.locals.findAllResult = result
1464
+ await next()
1465
+ } catch (err) {
1466
+ res.status(400).json(responseHelper.standardizeResponse({ err, message: err.message || err }))
1467
+ }
1468
+ }
1469
+ }
1470
+
1471
+ ;// ./lib/helpers/expressHelper/findOneResult.js
1472
+ function findOneResult({ responseHelper, service }) {
1473
+ return async (req, res, next) => {
1474
+ try {
1475
+ const { params, query } = req
1476
+ const { id } = params
1477
+ const result = await service.findOne({
1478
+ query: {
1479
+ ...query,
1480
+ id
1481
+ }
1482
+ })
1483
+ res.locals.findOneResult = result
1484
+ await next()
1485
+ } catch (err) {
1486
+ res.status(400).json(responseHelper.standardizeResponse({ err, message: err.message || err }))
1487
+ }
1488
+ }
1489
+ }
1490
+
1491
+ ;// ./lib/helpers/expressHelper/postResult.js
1492
+ function postResult({ responseHelper, service }) {
1493
+ return async (req, res, next) => {
1494
+ try {
1495
+ const { body } = req
1496
+ let result
1497
+ if (Array.isArray(body)) {
1498
+ result = await service.saveAll({ docs: body })
1499
+ } else {
1500
+ result = await service.saveOne({ doc: body })
1501
+ }
1502
+ res.locals.postResult = result
1503
+ await next()
1504
+ } catch (err) {
1505
+ res.status(400).json(responseHelper.standardizeResponse({ err, message: err.message || err }))
1506
+ }
1507
+ }
1508
+ }
1509
+
1510
+ ;// ./lib/helpers/expressHelper/updateOneResult.js
1511
+ function updateOneResult({ responseHelper, service }) {
1512
+ return async (req, res, next) => {
1513
+ try {
1514
+ const { body, params } = req
1515
+ const { id } = params
1516
+ if (id !== body.id) {
1517
+ throw new Error('id in params and body must be same')
1518
+ }
1519
+ const { data } = await service.findOne({ query: { id } })
1520
+ const doc = data[0]
1521
+ doc.update(body)
1522
+ const result = await service.saveOne({ doc })
1523
+ res.locals.updateOneResult = result
1524
+ await next()
1525
+ } catch (err) {
1526
+ res.status(400).json(responseHelper.standardizeResponse({ err, message: err.message || err }))
1527
+ }
1528
+ }
1529
+ }
1530
+
1531
+ ;// ./lib/helpers/expressHelper/index.js
1532
+
1533
+
1534
+
1535
+
1536
+
1537
+
1538
+
1539
+ const expressHelper = {
1540
+ customHandler: customHandler,
1541
+ findAllResult: findAllResult,
1542
+ findOneResult: findOneResult,
1543
+ postResult: postResult,
1544
+ updateOneResult: updateOneResult,
1545
+ }
1546
+
1547
+ ;// ./lib/helpers/extractEmails/extractEmails.js
1548
+ /**
1549
+ * Extracts and normalizes unique email addresses from an array containing messy entries
1550
+ * @param {Array} dirtyArray - Array that may contain emails in various formats (may include null/empty entries)
1551
+ * @returns {Array} Sorted array of unique, lowercase email addresses
1552
+ */
1553
+ function extractEmails(dirtyArray) {
1554
+ const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g
1555
+ const emails = new Set()
1556
+
1557
+ // Handle null/undefined input array
1558
+ if (!dirtyArray) return []
1559
+
1560
+ dirtyArray.forEach(entry => {
1561
+ // Skip null, undefined, empty, or whitespace-only entries
1562
+ if (!entry || typeof entry !== 'string' || !entry.trim()) return
1563
+
1564
+ try {
1565
+ const cleanEntry = entry
1566
+ .replace(/[\u200B-\u200D\uFEFF\u202A-\u202E]/g, '') // Remove hidden chars
1567
+ .replace(/[<>]/g, ' ') // Convert email delimiters to spaces
1568
+ .replace(/\s+/g, ' ') // Collapse multiple whitespace
1569
+ .trim()
1570
+
1571
+ // Extract all email matches
1572
+ const matches = cleanEntry.match(emailRegex)
1573
+ if (matches) {
1574
+ matches.forEach(email => emails.add(email.toLowerCase())) // Normalize to lowercase
1575
+ }
1576
+ } catch (e) {
1577
+ console.warn('Failed to process entry:', entry, e)
1578
+ }
1579
+ })
1580
+
1581
+ // Convert Set to array and sort alphabetically
1582
+ return Array.from(emails).sort((a, b) => a.localeCompare(b))
1583
+ }
1584
+
1585
+ ;// ./lib/helpers/extractEmails/index.js
1586
+
1587
+
1384
1588
  ;// ./lib/helpers/objectHelper/objectHelper.js
1385
1589
  const objectHelper = {
1386
1590
  get(obj, path) {
@@ -1664,19 +1868,19 @@ class Repo {
1664
1868
  })
1665
1869
  }
1666
1870
 
1667
- saveAll({ docs, systemLog }) {
1871
+ saveAll({ config = {}, docs, systemLog }) {
1668
1872
  let isNew
1669
1873
  const log = _makeLog({
1670
1874
  systemLog,
1671
1875
  label: 'REPO_WRITE',
1672
1876
  message: `fn ${this._classname}.prototype.saveAll`,
1673
- input: [{ docs: [...docs], systemLog: { ...systemLog } }]
1877
+ input: [{ config, docs: [...docs], systemLog: { ...systemLog } }]
1674
1878
  })
1675
1879
  const promise = typeof this.model.saveAll === 'function'
1676
- ? this.model.saveAll({ docs })
1880
+ ? this.model.saveAll({ config, docs })
1677
1881
  : Promise.all(docs.map(async (doc) => {
1678
1882
  if (doc) {
1679
- const result = await this.saveOne({ doc })
1883
+ const result = await this.saveOne({ config, doc })
1680
1884
  isNew = result.isNew
1681
1885
  const _data = result._data || result.data
1682
1886
  return _data[0]
@@ -1698,15 +1902,19 @@ class Repo {
1698
1902
  })
1699
1903
  }
1700
1904
 
1701
- saveOne({ doc, systemLog }) {
1905
+ saveOne({ config = {}, doc, systemLog }) {
1702
1906
  const log = _makeLog({
1703
1907
  systemLog,
1704
1908
  label: 'REPO_WRITE',
1705
1909
  message: `fn ${this._classname}.prototype.saveOne`,
1706
- input: [{ doc: { ...doc }, systemLog: { ...systemLog } }]
1910
+ input: [{ config, doc: { ...doc }, systemLog: { ...systemLog } }]
1707
1911
  })
1912
+ const saveOptions = {
1913
+ ...this.saveOptions,
1914
+ ...config,
1915
+ }
1708
1916
  return new Promise((resolve, reject) => {
1709
- this.model.saveOne(doc, this.saveOptions, (err, result) => {
1917
+ this.model.saveOne(doc, saveOptions, (err, result) => {
1710
1918
  if (err) {
1711
1919
  log({ level: 'warn', output: err.toString() })
1712
1920
  reject(err)
@@ -1848,11 +2056,11 @@ class Service {
1848
2056
  return this.initFromArray(arr).filter((i) => i)
1849
2057
  }
1850
2058
 
1851
- async saveAll({ docs = [], config = {}, systemLog } = {}) {
2059
+ async saveAll({ config = {}, docs = [], systemLog } = {}) {
1852
2060
  const copies = docs.map((doc) => {
1853
2061
  return config.skipInit ? doc : this.init(doc)
1854
2062
  })
1855
- const result = await this.repo.saveAll({ docs: copies, systemLog })
2063
+ const result = await this.repo.saveAll({ config, docs: copies, systemLog })
1856
2064
  return makeApiResponse({
1857
2065
  repo: this.repo,
1858
2066
  result
@@ -1860,10 +2068,10 @@ class Service {
1860
2068
  }
1861
2069
 
1862
2070
  // set skipInit to true if we want to use POST for query
1863
- async saveOne({ doc = {}, config = {}, systemLog } = {}) {
2071
+ async saveOne({ config = {}, doc = {}, systemLog } = {}) {
1864
2072
  const copy = config.skipInit ? doc : this.init(doc)
1865
2073
  if (copy) {
1866
- const result = await this.repo.saveOne({ doc: copy, systemLog })
2074
+ const result = await this.repo.saveOne({ config, doc: copy, systemLog })
1867
2075
  return makeApiResponse({
1868
2076
  repo: this.repo,
1869
2077
  result
@@ -1992,6 +2200,64 @@ function initOnlyValidFromArray(_class, arr) {
1992
2200
  ;// ./lib/helpers/initOnlyValidFromArray/index.js
1993
2201
 
1994
2202
 
2203
+ ;// ./lib/helpers/mergeArraysByKey/mergeArraysByKey.js
2204
+ function mergeArraysByKey(arr1, arr2) {
2205
+ // Handle undefined/null inputs by defaulting to empty arrays
2206
+ const safeArr1 = Array.isArray(arr1) ? arr1 : []
2207
+ const safeArr2 = Array.isArray(arr2) ? arr2 : []
2208
+
2209
+ const mergedMap = new Map()
2210
+
2211
+ // Helper function to merge values based on their type
2212
+ const mergeValues = (existingValue, newValue) => {
2213
+ if (existingValue === undefined) return newValue
2214
+
2215
+ // Handle arrays by concatenating
2216
+ if (Array.isArray(existingValue) && Array.isArray(newValue)) {
2217
+ return [...new Set([...existingValue, ...newValue])]
2218
+ }
2219
+
2220
+ // Handle objects by merging
2221
+ if (typeof existingValue === 'object' && typeof newValue === 'object' &&
2222
+ !Array.isArray(existingValue) && !Array.isArray(newValue)) {
2223
+ return { ...existingValue, ...newValue }
2224
+ }
2225
+
2226
+ // // Handle numbers by adding
2227
+ // if (typeof existingValue === 'number' && typeof newValue === 'number') {
2228
+ // return existingValue
2229
+ // }
2230
+
2231
+ // // Handle strings by concatenating
2232
+ // if (typeof existingValue === 'string' && typeof newValue === 'string') {
2233
+ // return existingValue
2234
+ // }
2235
+
2236
+ // Default: use the new value
2237
+ return newValue
2238
+ }
2239
+
2240
+ // Process first array
2241
+ safeArr1.forEach(item => {
2242
+ mergedMap.set(item.key, item.value)
2243
+ })
2244
+
2245
+ // Process second array and merge values
2246
+ safeArr2.forEach(item => {
2247
+ const existingValue = mergedMap.get(item.key)
2248
+ mergedMap.set(item.key, mergeValues(existingValue, item.value))
2249
+ })
2250
+
2251
+ // Convert back to array format
2252
+ return Array.from(mergedMap.entries()).map(([key, value]) => ({
2253
+ key,
2254
+ value
2255
+ }))
2256
+ }
2257
+
2258
+ ;// ./lib/helpers/mergeArraysByKey/index.js
2259
+
2260
+
1995
2261
  ;// ./lib/helpers/padZeros/padZeros.js
1996
2262
  function padZeros(num, minLength = 6) {
1997
2263
  num = num.toString()
@@ -2013,6 +2279,102 @@ function padZeros(num, minLength = 6) {
2013
2279
 
2014
2280
 
2015
2281
 
2282
+ ;// ./lib/helpers/replacePlaceholders/replacePlaceholders.js
2283
+ function replacePlaceholders({ content, mapping }) {
2284
+ let isObjectMode = false
2285
+
2286
+ if (typeof content === 'object' && content !== null) {
2287
+ content = JSON.stringify(content)
2288
+ isObjectMode = true
2289
+ }
2290
+
2291
+ // [[ eventRegistration.eventRegistrationCode | 0 ]]
2292
+ const regex = /(\[*)\[\[\s*([\w.\-_]+)(?:\s*\|\s*([^:\]\s]+))?(?:\s*:\s*([^\]\s]+))?\s*\]\](\]*)/g;
2293
+
2294
+ const result = content.replace(regex, (match, leadingBrackets, path, defaultValue, type, trailingBrackets) => {
2295
+
2296
+ // Split the path into parts
2297
+ const keys = path.trim().split('.')
2298
+
2299
+ // Traverse the nested object structure
2300
+ let value = mapping
2301
+ for (const key of keys) {
2302
+ // Handle empty keys (in case of double dots or leading/trailing dots)
2303
+ if (!key) continue
2304
+
2305
+ value = value?.[key]
2306
+ if (value === undefined) {
2307
+ break
2308
+ }
2309
+ }
2310
+
2311
+ // Apply default if missing
2312
+ if (value === undefined) value = defaultValue?.trim();
2313
+ if (value === undefined) return isObjectMode ? undefined : match;
2314
+
2315
+ value = value !== undefined
2316
+ ? leadingBrackets + value + trailingBrackets
2317
+ : match
2318
+
2319
+ // Return replacement or original if not found
2320
+ return value
2321
+ })
2322
+
2323
+ if (isObjectMode) {
2324
+ return JSON.parse(result)
2325
+ }
2326
+ return result
2327
+ }
2328
+
2329
+ ;// ./lib/helpers/replacePlaceholders/index.js
2330
+
2331
+
2332
+ ;// ./lib/helpers/sanitizeText/sanitizeText.js
2333
+ /**
2334
+ * Sanitizes input by removing hidden/control characters with customizable whitespace handling.
2335
+ * @param {string} input - The string to sanitize.
2336
+ * @param {Object} [options] - Configuration options.
2337
+ * @param {boolean} [options.normalizeWhitespace=true] - Collapse multiple spaces/tabs into one space.
2338
+ * @param {boolean} [options.removeNewlines=false] - If true, replaces newlines with spaces.
2339
+ * @param {boolean} [options.trim=true] - If true, trims leading/trailing whitespace.
2340
+ * @returns {string} The sanitized string.
2341
+ */
2342
+ function sanitizeText(input, options = {}) {
2343
+ const {
2344
+ normalizeWhitespace = true,
2345
+ removeNewlines = false,
2346
+ trim = true,
2347
+ } = options
2348
+
2349
+ if (typeof input !== 'string') {
2350
+ return input
2351
+ }
2352
+
2353
+ let result = input
2354
+
2355
+ // Phase 1: Remove hidden/control characters
2356
+ result = result.replace(/[\u200B-\u200D\uFEFF\u202A-\u202E]/g, '')
2357
+
2358
+ // Phase 2: Handle whitespace transformations
2359
+ if (removeNewlines) {
2360
+ result = result.replace(/[\r\n]+/g, ' ') // Convert newlines to spaces
2361
+ }
2362
+
2363
+ if (normalizeWhitespace) {
2364
+ result = result.replace(/[ \t]+/g, ' ') // Collapse spaces/tabs to single space
2365
+ }
2366
+
2367
+ // Phase 3: Final trimming
2368
+ if (trim) {
2369
+ result = result.trim()
2370
+ }
2371
+
2372
+ return result
2373
+ }
2374
+
2375
+ ;// ./lib/helpers/sanitizeText/index.js
2376
+
2377
+
2016
2378
  ;// ./lib/helpers/stringFormatter/stringFormatter.js
2017
2379
  function stringFormatter(str, delimiter = '_') {
2018
2380
  if (str === null || typeof str === 'undefined' || typeof str.toString === 'undefined') {
@@ -2138,10 +2500,18 @@ function toCamelCase(str) {
2138
2500
  .join('')
2139
2501
  }
2140
2502
 
2503
+ function toLowerCase(str) {
2504
+ if (!str) return ''
2505
+ return str
2506
+ .trim()
2507
+ .toLowerCase()
2508
+ }
2509
+
2141
2510
  const stringHelper = {
2142
2511
  isSame,
2143
2512
  setCode,
2144
2513
  toCamelCase,
2514
+ toLowerCase,
2145
2515
  }
2146
2516
 
2147
2517
 
@@ -2171,6 +2541,19 @@ function trackingPlugin(schema, options) {
2171
2541
  next()
2172
2542
  })
2173
2543
 
2544
+ // Add core indexes
2545
+ schema.index({
2546
+ 'meta.active': 1,
2547
+ 'meta.deleted': 1
2548
+ }, {
2549
+ name: 'tracking_status_index',
2550
+ background: true,
2551
+ partialFilterExpression: {
2552
+ 'meta.active': true,
2553
+ 'meta.deleted': false
2554
+ }
2555
+ })
2556
+
2174
2557
  // Optional: Add helper methods
2175
2558
  // schema.methods.touch = function(userId) {
2176
2559
  // this.meta.updatedAt = new Date()
@@ -2178,6 +2561,57 @@ function trackingPlugin(schema, options) {
2178
2561
  // }
2179
2562
  }
2180
2563
 
2564
+ ;// ./lib/helpers/tenantPlugin/tenantPlugin.js
2565
+
2566
+
2567
+ function tenantPlugin(schema, options) {
2568
+ // Apply tracking plugin first if not already present
2569
+ if (!schema.path('meta')) {
2570
+ trackingPlugin(schema, options)
2571
+ }
2572
+
2573
+ // Add tenant-specific fields
2574
+ schema.add({
2575
+ metadata: [{ type: Object }], // Instead of Schema.Types.Mixed
2576
+ remarks: [{ type: Object }],
2577
+ tenantCode: { type: String, required: true }
2578
+ })
2579
+
2580
+ // Add core indexes
2581
+ schema.index({
2582
+ 'tenantCode': 1
2583
+ }, {
2584
+ name: 'tenant_core_index',
2585
+ background: true
2586
+ })
2587
+
2588
+ // 1. ENHANCE EXISTING TRACKING INDEXES
2589
+ const existingIndexes = schema.indexes()
2590
+
2591
+ // Check if tracking_status_index exists
2592
+ const hasTenantStatusIndex = existingIndexes.some(idx =>
2593
+ idx.name === 'tenant_status_index' // Check by name for reliability
2594
+ )
2595
+
2596
+ if (!hasTenantStatusIndex) {
2597
+ schema.index({
2598
+ 'tenantCode': 1, // Unique field first
2599
+ _type: 1, // Low-cardinality field last
2600
+ }, {
2601
+ name: 'tenant_status_index',
2602
+ background: true,
2603
+ partialFilterExpression: {
2604
+ '_type': 'Tenant',
2605
+ 'meta.active': true,
2606
+ 'meta.deleted': false
2607
+ }
2608
+ })
2609
+ }
2610
+ }
2611
+
2612
+ ;// ./lib/helpers/tenantPlugin/index.js
2613
+
2614
+
2181
2615
  ;// ./lib/helpers/trackingPlugin/index.js
2182
2616
 
2183
2617
 
@@ -2194,6 +2628,14 @@ function trackingPlugin(schema, options) {
2194
2628
 
2195
2629
 
2196
2630
 
2631
+
2632
+
2633
+
2634
+
2635
+
2636
+
2637
+
2638
+
2197
2639
 
2198
2640
 
2199
2641
 
@@ -2205,6 +2647,196 @@ function trackingPlugin(schema, options) {
2205
2647
 
2206
2648
 
2207
2649
 
2650
+ ;// ./lib/models/awsStsS3Client/awsStsS3Client.js
2651
+ class AwsStsS3Client {
2652
+ constructor(options) {
2653
+ options = options || {}
2654
+
2655
+ this.expiration = options.expiration || null
2656
+ this.s3Client = options.s3Client || null
2657
+ this.getIdToken = options.getIdToken
2658
+ this.region = options.region || 'ap-east-1'
2659
+ this.roleArn = options.roleArn
2660
+ this.roleSessionName = options.roleSessionName || 'web-identity-session'
2661
+ this.durationSession = options.durationSession || 3600
2662
+ this.awsClientSts = options.awsClientSts
2663
+ this.awsClientS3 = options.awsClientS3
2664
+ }
2665
+
2666
+ static dummyData() {
2667
+ return {
2668
+ getIdToken: () => 'mock-web-identity-token',
2669
+ roleArn: 'arn:aws:iam::846252828949:role/oidcS3Jccpa',
2670
+ awsClientSts: {
2671
+ STSClient: class {},
2672
+ AssumeRoleWithWebIdentityCommand: class {}
2673
+ },
2674
+ awsClientS3: {
2675
+ S3Client: class {},
2676
+ PutObjectCommand: class {},
2677
+ GetObjectCommand: class {},
2678
+ DeleteObjectCommand: class {}
2679
+ }
2680
+ }
2681
+ }
2682
+
2683
+ static init(options = {}) {
2684
+ if (options instanceof this) {
2685
+ return options
2686
+ }
2687
+ try {
2688
+ const instance = new this(options)
2689
+ if (!instance.isValid) {
2690
+ return null
2691
+ }
2692
+ return instance
2693
+ } catch (error) {
2694
+ return null
2695
+ }
2696
+ }
2697
+
2698
+ get isExpired() {
2699
+ if (!this.expiration) return true;
2700
+ const now = new Date();
2701
+ const bufferMs = 1 * 60 * 1000; // 一分钟缓冲
2702
+ return now >= new Date(this.expiration.getTime() - bufferMs);
2703
+ }
2704
+
2705
+ get isValid() {
2706
+ if (!this.getIdToken) {
2707
+ throw new Error('Missing required configuration: getIdToken function')
2708
+ }
2709
+ if (!this.roleArn) {
2710
+ throw new Error('Missing required configuration: roleArn')
2711
+ }
2712
+ if (!this.awsClientSts) {
2713
+ throw new Error('Missing required AWS awsClientSts client configuration')
2714
+ }
2715
+ if (!this.awsClientSts.STSClient) {
2716
+ throw new Error('Missing STSClient in AWS awsClientSts client configuration')
2717
+ }
2718
+ if (!this.awsClientSts.AssumeRoleWithWebIdentityCommand) {
2719
+ throw new Error('Missing AssumeRoleWithWebIdentityCommand in AWS awsClientSts client configuration')
2720
+ }
2721
+ if (!this.awsClientS3) {
2722
+ throw new Error('Missing required AWS awsClientS3 client configuration')
2723
+ }
2724
+
2725
+ const requiredS3Components = [
2726
+ 'S3Client',
2727
+ 'PutObjectCommand',
2728
+ 'GetObjectCommand',
2729
+ 'DeleteObjectCommand'
2730
+ ]
2731
+
2732
+ for (const component of requiredS3Components) {
2733
+ if (!this.awsClientS3[component]) {
2734
+ throw new Error(`Missing ${component} in AWS awsClientS3 client configuration`)
2735
+ }
2736
+ }
2737
+
2738
+ return true
2739
+ }
2740
+
2741
+ async refreshCredentials() {
2742
+ try {
2743
+ const webIdentityToken = await this.getIdToken()
2744
+ if (!webIdentityToken) {
2745
+ throw new Error('getIdToken function returned empty or invalid token')
2746
+ }
2747
+
2748
+ const stsClient = new this.awsClientSts.STSClient({ region: this.region })
2749
+
2750
+ const stsResponse = await stsClient.send(
2751
+ new this.awsClientSts.AssumeRoleWithWebIdentityCommand({
2752
+ RoleArn: this.roleArn,
2753
+ RoleSessionName: this.roleSessionName,
2754
+ WebIdentityToken: await this.getIdToken(),
2755
+ DurationSeconds: this.durationSession,
2756
+ })
2757
+ )
2758
+
2759
+ const credentials = stsResponse.Credentials
2760
+ if (!credentials) {
2761
+ throw new Error('No credentials returned from awsClientSts')
2762
+ }
2763
+
2764
+ this.expiration = credentials.Expiration
2765
+
2766
+ this.s3Client = new this.awsClientS3.S3Client({
2767
+ region: this.region,
2768
+ credentials: {
2769
+ accessKeyId: credentials.AccessKeyId,
2770
+ secretAccessKey: credentials.SecretAccessKey,
2771
+ sessionToken: credentials.SessionToken,
2772
+ }
2773
+ })
2774
+
2775
+ return this
2776
+ } catch (error) {
2777
+ throw new Error(`Failed to refresh credentials: ${error.message}`)
2778
+ }
2779
+ }
2780
+
2781
+ async getS3Client() {
2782
+ if (this.isExpired || !this.s3Client) {
2783
+ await this.refreshCredentials()
2784
+ }
2785
+ return this.s3Client
2786
+ }
2787
+
2788
+ async putObject(params) {
2789
+ try {
2790
+ const client = await this.getS3Client()
2791
+ const command = new this.awsClientS3.PutObjectCommand(params)
2792
+ await client.send(command)
2793
+ const fileArr = params.Key.split('/')
2794
+ return {
2795
+ url: `https://s3.${this.region}.amazonaws.com/${params.Bucket}/${params.Key}`,
2796
+ filename: fileArr.pop(),
2797
+ folder: params.Bucket,
2798
+ subFolders: fileArr
2799
+ }
2800
+ } catch (error) {
2801
+ throw new Error(`Failed to put object: ${error.message}`)
2802
+ }
2803
+ }
2804
+
2805
+ async getObject(params) {
2806
+ try {
2807
+ const client = await this.getS3Client()
2808
+ const command = new this.awsClientS3.GetObjectCommand(params)
2809
+ const response = await client.send(command)
2810
+ return {
2811
+ body: response.Body,
2812
+ contentType: response.ContentType,
2813
+ lastModified: response.LastModified,
2814
+ contentLength: response.ContentLength,
2815
+ }
2816
+ } catch (error) {
2817
+ throw new Error(`Failed to get object: ${error.message}`)
2818
+ }
2819
+ }
2820
+
2821
+ async deleteObject(params) {
2822
+ try {
2823
+ const client = await this.getS3Client()
2824
+ const command = new this.awsClientS3.DeleteObjectCommand(params)
2825
+ await client.send(command)
2826
+ return true
2827
+ } catch (error) {
2828
+ throw new Error(`Failed to delete object: ${error.message}`)
2829
+ }
2830
+ }
2831
+ }
2832
+
2833
+
2834
+
2835
+ ;// ./lib/models/awsStsS3Client/index.js
2836
+
2837
+
2838
+
2839
+
2208
2840
  ;// ./lib/models/keyValueObject/keyValueObject.js
2209
2841
  class KeyValueObject {
2210
2842
  constructor(options = {}) {
@@ -2332,7 +2964,7 @@ class KeyValueObject {
2332
2964
  return to.key === from.key
2333
2965
  })
2334
2966
  if (found) {
2335
- found.value = (found.value || []).concat(from.value)
2967
+ found.value = _mergeValues(from.value, found.value)
2336
2968
  } else {
2337
2969
  toArr.push(from)
2338
2970
  }
@@ -2348,7 +2980,10 @@ class KeyValueObject {
2348
2980
  }, [])
2349
2981
  }
2350
2982
  static sameKey(item, key) {
2351
- return _isSame(item.key, key)
2983
+ if (item) {
2984
+ return _isSame(item.key, key)
2985
+ }
2986
+ return false
2352
2987
  }
2353
2988
  static toObject(arr = []) {
2354
2989
  if (Array.isArray(arr)) {
@@ -2417,6 +3052,34 @@ class KeyValueObject {
2417
3052
  }
2418
3053
  }
2419
3054
 
3055
+ function _mergeValues(existingValue, newValue) {
3056
+ if (existingValue === undefined) return newValue
3057
+
3058
+ // Handle arrays by concatenating
3059
+ if (Array.isArray(existingValue) && Array.isArray(newValue)) {
3060
+ return [...new Set([...existingValue, ...newValue])]
3061
+ }
3062
+
3063
+ // Handle objects by merging
3064
+ if (typeof existingValue === 'object' && typeof newValue === 'object' &&
3065
+ !Array.isArray(existingValue) && !Array.isArray(newValue)) {
3066
+ return { ...existingValue, ...newValue }
3067
+ }
3068
+
3069
+ // // Handle numbers by adding
3070
+ // if (typeof existingValue === 'number' && typeof newValue === 'number') {
3071
+ // return existingValue
3072
+ // }
3073
+
3074
+ // // Handle strings by concatenating
3075
+ // if (typeof existingValue === 'string' && typeof newValue === 'string') {
3076
+ // return existingValue
3077
+ // }
3078
+
3079
+ // Default: use the new value
3080
+ return newValue
3081
+ }
3082
+
2420
3083
  function _isSame(key1, key2) {
2421
3084
  return key1 === key2
2422
3085
  }
@@ -2455,7 +3118,7 @@ class Metadata extends KeyValueObject {
2455
3118
  return metadata_isSame(to.key, from.key)
2456
3119
  })
2457
3120
  if (found) {
2458
- found.value = (found.value || []).concat(from.value)
3121
+ found.value = metadata_mergeValues(from.value, found.value)
2459
3122
  } else {
2460
3123
  toArr.push(from)
2461
3124
  }
@@ -2471,6 +3134,34 @@ function metadata_isSame(key1, key2) {
2471
3134
  return stringFormatter(key1, DELIMITER) === stringFormatter(key2, DELIMITER)
2472
3135
  }
2473
3136
 
3137
+ function metadata_mergeValues(existingValue, newValue) {
3138
+ if (existingValue === undefined) return newValue
3139
+
3140
+ // Handle arrays by concatenating
3141
+ if (Array.isArray(existingValue) && Array.isArray(newValue)) {
3142
+ return [...new Set([...existingValue, ...newValue])]
3143
+ }
3144
+
3145
+ // Handle objects by merging
3146
+ if (typeof existingValue === 'object' && typeof newValue === 'object' &&
3147
+ !Array.isArray(existingValue) && !Array.isArray(newValue)) {
3148
+ return { ...existingValue, ...newValue }
3149
+ }
3150
+
3151
+ // // Handle numbers by adding
3152
+ // if (typeof existingValue === 'number' && typeof newValue === 'number') {
3153
+ // return existingValue
3154
+ // }
3155
+
3156
+ // // Handle strings by concatenating
3157
+ // if (typeof existingValue === 'string' && typeof newValue === 'string') {
3158
+ // return existingValue
3159
+ // }
3160
+
3161
+ // Default: use the new value
3162
+ return newValue
3163
+ }
3164
+
2474
3165
 
2475
3166
 
2476
3167
  ;// ./lib/models/metadata/index.js
@@ -2546,17 +3237,19 @@ class TrackedEntity {
2546
3237
  constructor(options = {}) {
2547
3238
  options = options || {}
2548
3239
  const timestamp = Date.now()
2549
- const _tracking = {
2550
- active: options.active ?? true,
2551
- created: options.created ?? timestamp,
2552
- creator: options.creator ?? '',
2553
- deleted: options.deleted ?? false,
2554
- modified: options.modified ?? timestamp,
2555
- owner: options.owner ?? '',
3240
+ this.meta = {
3241
+ active: options.meta?.active ?? options.active ?? true,
3242
+ created: options.meta?.created ?? (options.created
3243
+ ? new Date(options.created).getTime()
3244
+ : timestamp),
3245
+ creator: options.meta?.creator ?? options.creator ?? '',
3246
+ deleted: options.meta?.deleted ?? options.deleted ?? false,
3247
+ modified: options.meta?.modified ?? (options.modified
3248
+ ? new Date(options.modified).getTime()
3249
+ : timestamp),
3250
+ owner: options.meta?.owner ?? options.owner ?? '',
2556
3251
  }
2557
3252
 
2558
- this.meta = { ..._tracking, ...options.meta }
2559
-
2560
3253
  // if (trackFlat) {
2561
3254
  // Object.assign(this, _tracking)
2562
3255
  // } else {
@@ -2608,6 +3301,9 @@ class TrackedEntity {
2608
3301
  get owner() {
2609
3302
  return this.meta?.owner ?? this.owner
2610
3303
  }
3304
+ changeCreatorOwner({ source, target }) {
3305
+ return changeCreatorOwner(this, { source, target }).setModified()
3306
+ }
2611
3307
  delete() {
2612
3308
  return this.setDeleted()
2613
3309
  }
@@ -2814,6 +3510,7 @@ function _makeSetCode(fieldName, options) {
2814
3510
 
2815
3511
 
2816
3512
 
3513
+
2817
3514
  ;// ./lib/index.js
2818
3515
 
2819
3516
 
@@ -2821,4 +3518,4 @@ function _makeSetCode(fieldName, options) {
2821
3518
  ;// ./index.js
2822
3519
 
2823
3520
 
2824
- export { ApiResponse, KeyValueObject, Metadata, QMeta, Repo, Service, TemplateCompiler, TenantAwareEntity, TrackedEntity, UniqueKeyGenerator, authorize, concatStringByArray, convertString, formatDate, generalPost, getValidation, getValueByKeys_getValueByKeys as getValueByKeys, init, initFromArray, initOnlyValidFromArray, makeApiResponse, makeService, objectHelper, pReduce, padZeros, stringFormatter, stringHelper, trackingPlugin };
3521
+ export { ApiResponse, AwsStsS3Client, KeyValueObject, Metadata, QMeta, Repo, Service, TemplateCompiler, TenantAwareEntity, TrackedEntity, UniqueKeyGenerator, authorize, changeCreatorOwner, concatStringByArray, convertString, escapeRegex, expressHelper, extractEmails, formatDate, generalPost, getValidation, getValueByKeys_getValueByKeys as getValueByKeys, init, initFromArray, initOnlyValidFromArray, makeApiResponse, makeService, mergeArraysByKey, objectHelper, pReduce, padZeros, replacePlaceholders, sanitizeText, stringFormatter, stringHelper, tenantPlugin, trackingPlugin };