@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.
@@ -49,6 +49,7 @@ __webpack_require__.r(__webpack_exports__);
49
49
  // EXPORTS
50
50
  __webpack_require__.d(__webpack_exports__, {
51
51
  ApiResponse: () => (/* reexport */ ApiResponse),
52
+ AwsStsS3Client: () => (/* reexport */ AwsStsS3Client),
52
53
  KeyValueObject: () => (/* reexport */ KeyValueObject),
53
54
  Metadata: () => (/* reexport */ Metadata),
54
55
  QMeta: () => (/* reexport */ QMeta),
@@ -59,8 +60,12 @@ __webpack_require__.d(__webpack_exports__, {
59
60
  TrackedEntity: () => (/* reexport */ TrackedEntity),
60
61
  UniqueKeyGenerator: () => (/* reexport */ UniqueKeyGenerator),
61
62
  authorize: () => (/* reexport */ authorize),
63
+ changeCreatorOwner: () => (/* reexport */ changeCreatorOwner),
62
64
  concatStringByArray: () => (/* reexport */ concatStringByArray),
63
65
  convertString: () => (/* reexport */ convertString),
66
+ escapeRegex: () => (/* reexport */ escapeRegex),
67
+ expressHelper: () => (/* reexport */ expressHelper),
68
+ extractEmails: () => (/* reexport */ extractEmails),
64
69
  formatDate: () => (/* reexport */ formatDate),
65
70
  generalPost: () => (/* reexport */ generalPost),
66
71
  getValidation: () => (/* reexport */ getValidation),
@@ -70,25 +75,29 @@ __webpack_require__.d(__webpack_exports__, {
70
75
  initOnlyValidFromArray: () => (/* reexport */ initOnlyValidFromArray),
71
76
  makeApiResponse: () => (/* reexport */ makeApiResponse),
72
77
  makeService: () => (/* reexport */ makeService),
78
+ mergeArraysByKey: () => (/* reexport */ mergeArraysByKey),
73
79
  objectHelper: () => (/* reexport */ objectHelper),
74
80
  pReduce: () => (/* reexport */ pReduce),
75
81
  padZeros: () => (/* reexport */ padZeros),
82
+ replacePlaceholders: () => (/* reexport */ replacePlaceholders),
83
+ sanitizeText: () => (/* reexport */ sanitizeText),
76
84
  stringFormatter: () => (/* reexport */ stringFormatter),
77
85
  stringHelper: () => (/* reexport */ stringHelper),
86
+ tenantPlugin: () => (/* reexport */ tenantPlugin),
78
87
  trackingPlugin: () => (/* reexport */ trackingPlugin)
79
88
  });
80
89
 
81
90
  ;// ./lib/helpers/authorize/authorize.js
82
- function authorize({ allowOwner, query = {}, required, user }) {
91
+ function authorize({ allowCoordinator, allowOwner, query = {}, required, user }) {
83
92
  if (!user) {
84
93
  throw new Error('Require login.')
85
94
  }
86
95
  if (!user.permission) {
87
- throw new Error('You do not have any permission.')
96
+ // throw new Error('You do not have any permission.')
88
97
  }
89
- const scopes = user.permission.getScopes(required || {})
98
+ const scopes = user.permission.getScopes(required || {}) || []
90
99
  if (!scopes || scopes.length === 0) {
91
- throw new Error('You are not allowed in this scope.')
100
+ // throw new Error('You are not allowed in this scope.')
92
101
  }
93
102
  if (!scopes.includes('*')) {
94
103
  query.tenantCode = user.tenantCode
@@ -96,8 +105,21 @@ function authorize({ allowOwner, query = {}, required, user }) {
96
105
  if (!scopes.includes('TENANT')) {
97
106
  query.eventShortCode = user.eventShortCode
98
107
  }
99
- if (!scopes.includes('EVENT')) {
100
- query.eventRegistrationCode = user.eventRegistrationCode
108
+ // if (!scopes.includes('EVENT')) {
109
+ // query.eventRegistrationCode = user.eventRegistrationCode
110
+ // }
111
+ if (allowCoordinator) {
112
+ if (query.registrationGroupCode && user.myManagedRegistrationGroupCodes.includes(query.registrationGroupCode)) {
113
+ query.__ALLOW_COORDINATOR = true
114
+ } else {
115
+ if (!scopes.includes('EVENT')) {
116
+ query.eventRegistrationCode = user.eventRegistrationCode
117
+ }
118
+ }
119
+ } else {
120
+ if (!scopes.includes('EVENT')) {
121
+ query.eventRegistrationCode = user.eventRegistrationCode
122
+ }
101
123
  }
102
124
  if (allowOwner) {
103
125
  query.__ALLOW_OWNER = true
@@ -113,6 +135,29 @@ function authorize({ allowOwner, query = {}, required, user }) {
113
135
  ;// ./lib/helpers/authorize/index.js
114
136
 
115
137
 
138
+ ;// ./lib/helpers/changeCreatorOwner/changeCreatorOwner.js
139
+ function changeCreatorOwner(that, { source, target }) {
140
+ if (that.meta) {
141
+ if (!that.meta.creator || that.meta.creator === source.getId()) {
142
+ that.meta.creator = target.getId()
143
+ }
144
+ if (!that.meta.owner || that.meta.owner === source.getId()) {
145
+ that.meta.owner = target.getId()
146
+ }
147
+ } else {
148
+ if (!that.creator || that.creator === source.getId()) {
149
+ that.creator = target.getId()
150
+ }
151
+ if (!that.owner || that.owner === source.getId()) {
152
+ that.owner = target.getId()
153
+ }
154
+ }
155
+ return that
156
+ }
157
+
158
+ ;// ./lib/helpers/changeCreatorOwner/index.js
159
+
160
+
116
161
  ;// ./lib/helpers/getValidation/getValidation.js
117
162
  function getValidation(rule, data, getDataByKey, KeyValueObject) {
118
163
  if (!rule) {
@@ -121,10 +166,10 @@ function getValidation(rule, data, getDataByKey, KeyValueObject) {
121
166
  if (typeof getDataByKey !== 'function' || (KeyValueObject && typeof KeyValueObject !== 'function')) {
122
167
  return false
123
168
  }
124
- const { key = '', value, keyValuePath = '' } = rule
169
+ const { key = '', value, placeholder, keyValuePath = '' } = rule
125
170
  const [valueAttribute] = Object.keys(value)
126
171
 
127
- if (!key) {
172
+ if (!key && typeof placeholder === 'undefined') {
128
173
  switch (valueAttribute) {
129
174
  case '$and': {
130
175
  return value['$and'].reduce((acc, item) => (acc && getValidation(item, data, getDataByKey, KeyValueObject)), true)
@@ -136,14 +181,10 @@ function getValidation(rule, data, getDataByKey, KeyValueObject) {
136
181
  return false
137
182
  }
138
183
  }
139
-
140
- let rowValue = getDataByKey(key, data)
141
-
142
- // debugger
184
+ let rowValue = typeof placeholder === 'undefined' ? getDataByKey(key, data) : placeholder
143
185
 
144
186
  // if KeyValue object
145
187
  if (keyValuePath) {
146
- console.log('keyValuePath', keyValuePath)
147
188
  const rowValueData = KeyValueObject.toObject(rowValue)
148
189
  rowValue = getDataByKey(keyValuePath, rowValueData)
149
190
  }
@@ -162,6 +203,9 @@ function getValidation(rule, data, getDataByKey, KeyValueObject) {
162
203
  case '$gte': {
163
204
  return rowValue >= value['$gte']
164
205
  }
206
+ case '$hasOverlap': {
207
+ return _hasOverlap(rowValue, value['$hasOverlap'])
208
+ }
165
209
  case '$lt': {
166
210
  return rowValue < value['$lt']
167
211
  }
@@ -238,6 +282,20 @@ function getValidation(rule, data, getDataByKey, KeyValueObject) {
238
282
  default:
239
283
  return false
240
284
  }
285
+
286
+ }
287
+
288
+ function _hasOverlap(item1, item2) {
289
+ let arr1 = item1
290
+ let arr2 = item2
291
+ if (typeof arr1 === 'string') {
292
+ arr1 = arr1.split(',')
293
+ }
294
+ if (typeof arr2 === 'string') {
295
+ arr2 = arr2.split(',')
296
+ }
297
+ const set1 = new Set(arr1)
298
+ return arr2.find((i) => (set1.has(i)))
241
299
  }
242
300
 
243
301
  /* harmony default export */ const getValidation_getValidation = ({
@@ -1441,7 +1499,7 @@ function convertString(string, patternMatch = /\$\{(.+?)\}/g, value, getValueByK
1441
1499
  if (!string) {
1442
1500
  return ''
1443
1501
  }
1444
- let _getValueByKeys = typeof getValueByKeys !== 'function' ? getValueByKeys : getValueByKeys_getValueByKeys
1502
+ let _getValueByKeys = typeof getValueByKeys === 'function' ? getValueByKeys : getValueByKeys_getValueByKeys
1445
1503
  const reg = new RegExp(patternMatch, 'g')
1446
1504
  return string.replace(reg, (match, key) => {
1447
1505
  const result = _getValueByKeys({ keys: key.split('.'), obj: value })
@@ -1460,6 +1518,161 @@ function convertString(string, patternMatch = /\$\{(.+?)\}/g, value, getValueByK
1460
1518
  ;// ./lib/helpers/convertString/index.js
1461
1519
 
1462
1520
 
1521
+ ;// ./lib/helpers/escapeRegex/escapeRegex.js
1522
+ function escapeRegex(string) {
1523
+ return String(string).replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')
1524
+ }
1525
+
1526
+ ;// ./lib/helpers/escapeRegex/index.js
1527
+
1528
+
1529
+ ;// ./lib/helpers/expressHelper/customHandler.js
1530
+ function customHandler({ responseHelper, handler, ignoreError = false }) {
1531
+ return async (req, res, next) => {
1532
+ try {
1533
+ await handler({ req, res })
1534
+ await next()
1535
+ } catch (err) {
1536
+ if (ignoreError) {
1537
+ await next()
1538
+ } else {
1539
+ res.status(400).json(responseHelper.standardizeResponse({ err, message: err.message || err }))
1540
+ }
1541
+ }
1542
+ }
1543
+ }
1544
+
1545
+ ;// ./lib/helpers/expressHelper/findAllResult.js
1546
+ function findAllResult({ responseHelper, service }) {
1547
+ return async (req, res, next) => {
1548
+ try {
1549
+ const { query } = req
1550
+ const result = await service.findAll({ query })
1551
+ res.locals.findAllResult = result
1552
+ await next()
1553
+ } catch (err) {
1554
+ res.status(400).json(responseHelper.standardizeResponse({ err, message: err.message || err }))
1555
+ }
1556
+ }
1557
+ }
1558
+
1559
+ ;// ./lib/helpers/expressHelper/findOneResult.js
1560
+ function findOneResult({ responseHelper, service }) {
1561
+ return async (req, res, next) => {
1562
+ try {
1563
+ const { params, query } = req
1564
+ const { id } = params
1565
+ const result = await service.findOne({
1566
+ query: {
1567
+ ...query,
1568
+ id
1569
+ }
1570
+ })
1571
+ res.locals.findOneResult = result
1572
+ await next()
1573
+ } catch (err) {
1574
+ res.status(400).json(responseHelper.standardizeResponse({ err, message: err.message || err }))
1575
+ }
1576
+ }
1577
+ }
1578
+
1579
+ ;// ./lib/helpers/expressHelper/postResult.js
1580
+ function postResult({ responseHelper, service }) {
1581
+ return async (req, res, next) => {
1582
+ try {
1583
+ const { body } = req
1584
+ let result
1585
+ if (Array.isArray(body)) {
1586
+ result = await service.saveAll({ docs: body })
1587
+ } else {
1588
+ result = await service.saveOne({ doc: body })
1589
+ }
1590
+ res.locals.postResult = result
1591
+ await next()
1592
+ } catch (err) {
1593
+ res.status(400).json(responseHelper.standardizeResponse({ err, message: err.message || err }))
1594
+ }
1595
+ }
1596
+ }
1597
+
1598
+ ;// ./lib/helpers/expressHelper/updateOneResult.js
1599
+ function updateOneResult({ responseHelper, service }) {
1600
+ return async (req, res, next) => {
1601
+ try {
1602
+ const { body, params } = req
1603
+ const { id } = params
1604
+ if (id !== body.id) {
1605
+ throw new Error('id in params and body must be same')
1606
+ }
1607
+ const { data } = await service.findOne({ query: { id } })
1608
+ const doc = data[0]
1609
+ doc.update(body)
1610
+ const result = await service.saveOne({ doc })
1611
+ res.locals.updateOneResult = result
1612
+ await next()
1613
+ } catch (err) {
1614
+ res.status(400).json(responseHelper.standardizeResponse({ err, message: err.message || err }))
1615
+ }
1616
+ }
1617
+ }
1618
+
1619
+ ;// ./lib/helpers/expressHelper/index.js
1620
+
1621
+
1622
+
1623
+
1624
+
1625
+
1626
+
1627
+ const expressHelper = {
1628
+ customHandler: customHandler,
1629
+ findAllResult: findAllResult,
1630
+ findOneResult: findOneResult,
1631
+ postResult: postResult,
1632
+ updateOneResult: updateOneResult,
1633
+ }
1634
+
1635
+ ;// ./lib/helpers/extractEmails/extractEmails.js
1636
+ /**
1637
+ * Extracts and normalizes unique email addresses from an array containing messy entries
1638
+ * @param {Array} dirtyArray - Array that may contain emails in various formats (may include null/empty entries)
1639
+ * @returns {Array} Sorted array of unique, lowercase email addresses
1640
+ */
1641
+ function extractEmails(dirtyArray) {
1642
+ const emailRegex = /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g
1643
+ const emails = new Set()
1644
+
1645
+ // Handle null/undefined input array
1646
+ if (!dirtyArray) return []
1647
+
1648
+ dirtyArray.forEach(entry => {
1649
+ // Skip null, undefined, empty, or whitespace-only entries
1650
+ if (!entry || typeof entry !== 'string' || !entry.trim()) return
1651
+
1652
+ try {
1653
+ const cleanEntry = entry
1654
+ .replace(/[\u200B-\u200D\uFEFF\u202A-\u202E]/g, '') // Remove hidden chars
1655
+ .replace(/[<>]/g, ' ') // Convert email delimiters to spaces
1656
+ .replace(/\s+/g, ' ') // Collapse multiple whitespace
1657
+ .trim()
1658
+
1659
+ // Extract all email matches
1660
+ const matches = cleanEntry.match(emailRegex)
1661
+ if (matches) {
1662
+ matches.forEach(email => emails.add(email.toLowerCase())) // Normalize to lowercase
1663
+ }
1664
+ } catch (e) {
1665
+ console.warn('Failed to process entry:', entry, e)
1666
+ }
1667
+ })
1668
+
1669
+ // Convert Set to array and sort alphabetically
1670
+ return Array.from(emails).sort((a, b) => a.localeCompare(b))
1671
+ }
1672
+
1673
+ ;// ./lib/helpers/extractEmails/index.js
1674
+
1675
+
1463
1676
  ;// ./lib/helpers/objectHelper/objectHelper.js
1464
1677
  const objectHelper = {
1465
1678
  get(obj, path) {
@@ -1743,19 +1956,19 @@ class Repo {
1743
1956
  })
1744
1957
  }
1745
1958
 
1746
- saveAll({ docs, systemLog }) {
1959
+ saveAll({ config = {}, docs, systemLog }) {
1747
1960
  let isNew
1748
1961
  const log = _makeLog({
1749
1962
  systemLog,
1750
1963
  label: 'REPO_WRITE',
1751
1964
  message: `fn ${this._classname}.prototype.saveAll`,
1752
- input: [{ docs: [...docs], systemLog: { ...systemLog } }]
1965
+ input: [{ config, docs: [...docs], systemLog: { ...systemLog } }]
1753
1966
  })
1754
1967
  const promise = typeof this.model.saveAll === 'function'
1755
- ? this.model.saveAll({ docs })
1968
+ ? this.model.saveAll({ config, docs })
1756
1969
  : Promise.all(docs.map(async (doc) => {
1757
1970
  if (doc) {
1758
- const result = await this.saveOne({ doc })
1971
+ const result = await this.saveOne({ config, doc })
1759
1972
  isNew = result.isNew
1760
1973
  const _data = result._data || result.data
1761
1974
  return _data[0]
@@ -1777,15 +1990,19 @@ class Repo {
1777
1990
  })
1778
1991
  }
1779
1992
 
1780
- saveOne({ doc, systemLog }) {
1993
+ saveOne({ config = {}, doc, systemLog }) {
1781
1994
  const log = _makeLog({
1782
1995
  systemLog,
1783
1996
  label: 'REPO_WRITE',
1784
1997
  message: `fn ${this._classname}.prototype.saveOne`,
1785
- input: [{ doc: { ...doc }, systemLog: { ...systemLog } }]
1998
+ input: [{ config, doc: { ...doc }, systemLog: { ...systemLog } }]
1786
1999
  })
2000
+ const saveOptions = {
2001
+ ...this.saveOptions,
2002
+ ...config,
2003
+ }
1787
2004
  return new Promise((resolve, reject) => {
1788
- this.model.saveOne(doc, this.saveOptions, (err, result) => {
2005
+ this.model.saveOne(doc, saveOptions, (err, result) => {
1789
2006
  if (err) {
1790
2007
  log({ level: 'warn', output: err.toString() })
1791
2008
  reject(err)
@@ -1927,11 +2144,11 @@ class Service {
1927
2144
  return this.initFromArray(arr).filter((i) => i)
1928
2145
  }
1929
2146
 
1930
- async saveAll({ docs = [], config = {}, systemLog } = {}) {
2147
+ async saveAll({ config = {}, docs = [], systemLog } = {}) {
1931
2148
  const copies = docs.map((doc) => {
1932
2149
  return config.skipInit ? doc : this.init(doc)
1933
2150
  })
1934
- const result = await this.repo.saveAll({ docs: copies, systemLog })
2151
+ const result = await this.repo.saveAll({ config, docs: copies, systemLog })
1935
2152
  return makeApiResponse({
1936
2153
  repo: this.repo,
1937
2154
  result
@@ -1939,10 +2156,10 @@ class Service {
1939
2156
  }
1940
2157
 
1941
2158
  // set skipInit to true if we want to use POST for query
1942
- async saveOne({ doc = {}, config = {}, systemLog } = {}) {
2159
+ async saveOne({ config = {}, doc = {}, systemLog } = {}) {
1943
2160
  const copy = config.skipInit ? doc : this.init(doc)
1944
2161
  if (copy) {
1945
- const result = await this.repo.saveOne({ doc: copy, systemLog })
2162
+ const result = await this.repo.saveOne({ config, doc: copy, systemLog })
1946
2163
  return makeApiResponse({
1947
2164
  repo: this.repo,
1948
2165
  result
@@ -2071,6 +2288,64 @@ function initOnlyValidFromArray(_class, arr) {
2071
2288
  ;// ./lib/helpers/initOnlyValidFromArray/index.js
2072
2289
 
2073
2290
 
2291
+ ;// ./lib/helpers/mergeArraysByKey/mergeArraysByKey.js
2292
+ function mergeArraysByKey(arr1, arr2) {
2293
+ // Handle undefined/null inputs by defaulting to empty arrays
2294
+ const safeArr1 = Array.isArray(arr1) ? arr1 : []
2295
+ const safeArr2 = Array.isArray(arr2) ? arr2 : []
2296
+
2297
+ const mergedMap = new Map()
2298
+
2299
+ // Helper function to merge values based on their type
2300
+ const mergeValues = (existingValue, newValue) => {
2301
+ if (existingValue === undefined) return newValue
2302
+
2303
+ // Handle arrays by concatenating
2304
+ if (Array.isArray(existingValue) && Array.isArray(newValue)) {
2305
+ return [...new Set([...existingValue, ...newValue])]
2306
+ }
2307
+
2308
+ // Handle objects by merging
2309
+ if (typeof existingValue === 'object' && typeof newValue === 'object' &&
2310
+ !Array.isArray(existingValue) && !Array.isArray(newValue)) {
2311
+ return { ...existingValue, ...newValue }
2312
+ }
2313
+
2314
+ // // Handle numbers by adding
2315
+ // if (typeof existingValue === 'number' && typeof newValue === 'number') {
2316
+ // return existingValue
2317
+ // }
2318
+
2319
+ // // Handle strings by concatenating
2320
+ // if (typeof existingValue === 'string' && typeof newValue === 'string') {
2321
+ // return existingValue
2322
+ // }
2323
+
2324
+ // Default: use the new value
2325
+ return newValue
2326
+ }
2327
+
2328
+ // Process first array
2329
+ safeArr1.forEach(item => {
2330
+ mergedMap.set(item.key, item.value)
2331
+ })
2332
+
2333
+ // Process second array and merge values
2334
+ safeArr2.forEach(item => {
2335
+ const existingValue = mergedMap.get(item.key)
2336
+ mergedMap.set(item.key, mergeValues(existingValue, item.value))
2337
+ })
2338
+
2339
+ // Convert back to array format
2340
+ return Array.from(mergedMap.entries()).map(([key, value]) => ({
2341
+ key,
2342
+ value
2343
+ }))
2344
+ }
2345
+
2346
+ ;// ./lib/helpers/mergeArraysByKey/index.js
2347
+
2348
+
2074
2349
  ;// ./lib/helpers/padZeros/padZeros.js
2075
2350
  function padZeros(num, minLength = 6) {
2076
2351
  num = num.toString()
@@ -2092,6 +2367,102 @@ function padZeros(num, minLength = 6) {
2092
2367
 
2093
2368
 
2094
2369
 
2370
+ ;// ./lib/helpers/replacePlaceholders/replacePlaceholders.js
2371
+ function replacePlaceholders({ content, mapping }) {
2372
+ let isObjectMode = false
2373
+
2374
+ if (typeof content === 'object' && content !== null) {
2375
+ content = JSON.stringify(content)
2376
+ isObjectMode = true
2377
+ }
2378
+
2379
+ // [[ eventRegistration.eventRegistrationCode | 0 ]]
2380
+ const regex = /(\[*)\[\[\s*([\w.\-_]+)(?:\s*\|\s*([^:\]\s]+))?(?:\s*:\s*([^\]\s]+))?\s*\]\](\]*)/g;
2381
+
2382
+ const result = content.replace(regex, (match, leadingBrackets, path, defaultValue, type, trailingBrackets) => {
2383
+
2384
+ // Split the path into parts
2385
+ const keys = path.trim().split('.')
2386
+
2387
+ // Traverse the nested object structure
2388
+ let value = mapping
2389
+ for (const key of keys) {
2390
+ // Handle empty keys (in case of double dots or leading/trailing dots)
2391
+ if (!key) continue
2392
+
2393
+ value = value?.[key]
2394
+ if (value === undefined) {
2395
+ break
2396
+ }
2397
+ }
2398
+
2399
+ // Apply default if missing
2400
+ if (value === undefined) value = defaultValue?.trim();
2401
+ if (value === undefined) return isObjectMode ? undefined : match;
2402
+
2403
+ value = value !== undefined
2404
+ ? leadingBrackets + value + trailingBrackets
2405
+ : match
2406
+
2407
+ // Return replacement or original if not found
2408
+ return value
2409
+ })
2410
+
2411
+ if (isObjectMode) {
2412
+ return JSON.parse(result)
2413
+ }
2414
+ return result
2415
+ }
2416
+
2417
+ ;// ./lib/helpers/replacePlaceholders/index.js
2418
+
2419
+
2420
+ ;// ./lib/helpers/sanitizeText/sanitizeText.js
2421
+ /**
2422
+ * Sanitizes input by removing hidden/control characters with customizable whitespace handling.
2423
+ * @param {string} input - The string to sanitize.
2424
+ * @param {Object} [options] - Configuration options.
2425
+ * @param {boolean} [options.normalizeWhitespace=true] - Collapse multiple spaces/tabs into one space.
2426
+ * @param {boolean} [options.removeNewlines=false] - If true, replaces newlines with spaces.
2427
+ * @param {boolean} [options.trim=true] - If true, trims leading/trailing whitespace.
2428
+ * @returns {string} The sanitized string.
2429
+ */
2430
+ function sanitizeText(input, options = {}) {
2431
+ const {
2432
+ normalizeWhitespace = true,
2433
+ removeNewlines = false,
2434
+ trim = true,
2435
+ } = options
2436
+
2437
+ if (typeof input !== 'string') {
2438
+ return input
2439
+ }
2440
+
2441
+ let result = input
2442
+
2443
+ // Phase 1: Remove hidden/control characters
2444
+ result = result.replace(/[\u200B-\u200D\uFEFF\u202A-\u202E]/g, '')
2445
+
2446
+ // Phase 2: Handle whitespace transformations
2447
+ if (removeNewlines) {
2448
+ result = result.replace(/[\r\n]+/g, ' ') // Convert newlines to spaces
2449
+ }
2450
+
2451
+ if (normalizeWhitespace) {
2452
+ result = result.replace(/[ \t]+/g, ' ') // Collapse spaces/tabs to single space
2453
+ }
2454
+
2455
+ // Phase 3: Final trimming
2456
+ if (trim) {
2457
+ result = result.trim()
2458
+ }
2459
+
2460
+ return result
2461
+ }
2462
+
2463
+ ;// ./lib/helpers/sanitizeText/index.js
2464
+
2465
+
2095
2466
  ;// ./lib/helpers/stringFormatter/stringFormatter.js
2096
2467
  function stringFormatter(str, delimiter = '_') {
2097
2468
  if (str === null || typeof str === 'undefined' || typeof str.toString === 'undefined') {
@@ -2217,10 +2588,18 @@ function toCamelCase(str) {
2217
2588
  .join('')
2218
2589
  }
2219
2590
 
2591
+ function toLowerCase(str) {
2592
+ if (!str) return ''
2593
+ return str
2594
+ .trim()
2595
+ .toLowerCase()
2596
+ }
2597
+
2220
2598
  const stringHelper = {
2221
2599
  isSame,
2222
2600
  setCode,
2223
2601
  toCamelCase,
2602
+ toLowerCase,
2224
2603
  }
2225
2604
 
2226
2605
 
@@ -2250,6 +2629,19 @@ function trackingPlugin(schema, options) {
2250
2629
  next()
2251
2630
  })
2252
2631
 
2632
+ // Add core indexes
2633
+ schema.index({
2634
+ 'meta.active': 1,
2635
+ 'meta.deleted': 1
2636
+ }, {
2637
+ name: 'tracking_status_index',
2638
+ background: true,
2639
+ partialFilterExpression: {
2640
+ 'meta.active': true,
2641
+ 'meta.deleted': false
2642
+ }
2643
+ })
2644
+
2253
2645
  // Optional: Add helper methods
2254
2646
  // schema.methods.touch = function(userId) {
2255
2647
  // this.meta.updatedAt = new Date()
@@ -2257,6 +2649,57 @@ function trackingPlugin(schema, options) {
2257
2649
  // }
2258
2650
  }
2259
2651
 
2652
+ ;// ./lib/helpers/tenantPlugin/tenantPlugin.js
2653
+
2654
+
2655
+ function tenantPlugin(schema, options) {
2656
+ // Apply tracking plugin first if not already present
2657
+ if (!schema.path('meta')) {
2658
+ trackingPlugin(schema, options)
2659
+ }
2660
+
2661
+ // Add tenant-specific fields
2662
+ schema.add({
2663
+ metadata: [{ type: Object }], // Instead of Schema.Types.Mixed
2664
+ remarks: [{ type: Object }],
2665
+ tenantCode: { type: String, required: true }
2666
+ })
2667
+
2668
+ // Add core indexes
2669
+ schema.index({
2670
+ 'tenantCode': 1
2671
+ }, {
2672
+ name: 'tenant_core_index',
2673
+ background: true
2674
+ })
2675
+
2676
+ // 1. ENHANCE EXISTING TRACKING INDEXES
2677
+ const existingIndexes = schema.indexes()
2678
+
2679
+ // Check if tracking_status_index exists
2680
+ const hasTenantStatusIndex = existingIndexes.some(idx =>
2681
+ idx.name === 'tenant_status_index' // Check by name for reliability
2682
+ )
2683
+
2684
+ if (!hasTenantStatusIndex) {
2685
+ schema.index({
2686
+ 'tenantCode': 1, // Unique field first
2687
+ _type: 1, // Low-cardinality field last
2688
+ }, {
2689
+ name: 'tenant_status_index',
2690
+ background: true,
2691
+ partialFilterExpression: {
2692
+ '_type': 'Tenant',
2693
+ 'meta.active': true,
2694
+ 'meta.deleted': false
2695
+ }
2696
+ })
2697
+ }
2698
+ }
2699
+
2700
+ ;// ./lib/helpers/tenantPlugin/index.js
2701
+
2702
+
2260
2703
  ;// ./lib/helpers/trackingPlugin/index.js
2261
2704
 
2262
2705
 
@@ -2273,6 +2716,14 @@ function trackingPlugin(schema, options) {
2273
2716
 
2274
2717
 
2275
2718
 
2719
+
2720
+
2721
+
2722
+
2723
+
2724
+
2725
+
2726
+
2276
2727
 
2277
2728
 
2278
2729
 
@@ -2284,6 +2735,196 @@ function trackingPlugin(schema, options) {
2284
2735
 
2285
2736
 
2286
2737
 
2738
+ ;// ./lib/models/awsStsS3Client/awsStsS3Client.js
2739
+ class AwsStsS3Client {
2740
+ constructor(options) {
2741
+ options = options || {}
2742
+
2743
+ this.expiration = options.expiration || null
2744
+ this.s3Client = options.s3Client || null
2745
+ this.getIdToken = options.getIdToken
2746
+ this.region = options.region || 'ap-east-1'
2747
+ this.roleArn = options.roleArn
2748
+ this.roleSessionName = options.roleSessionName || 'web-identity-session'
2749
+ this.durationSession = options.durationSession || 3600
2750
+ this.awsClientSts = options.awsClientSts
2751
+ this.awsClientS3 = options.awsClientS3
2752
+ }
2753
+
2754
+ static dummyData() {
2755
+ return {
2756
+ getIdToken: () => 'mock-web-identity-token',
2757
+ roleArn: 'arn:aws:iam::846252828949:role/oidcS3Jccpa',
2758
+ awsClientSts: {
2759
+ STSClient: class {},
2760
+ AssumeRoleWithWebIdentityCommand: class {}
2761
+ },
2762
+ awsClientS3: {
2763
+ S3Client: class {},
2764
+ PutObjectCommand: class {},
2765
+ GetObjectCommand: class {},
2766
+ DeleteObjectCommand: class {}
2767
+ }
2768
+ }
2769
+ }
2770
+
2771
+ static init(options = {}) {
2772
+ if (options instanceof this) {
2773
+ return options
2774
+ }
2775
+ try {
2776
+ const instance = new this(options)
2777
+ if (!instance.isValid) {
2778
+ return null
2779
+ }
2780
+ return instance
2781
+ } catch (error) {
2782
+ return null
2783
+ }
2784
+ }
2785
+
2786
+ get isExpired() {
2787
+ if (!this.expiration) return true;
2788
+ const now = new Date();
2789
+ const bufferMs = 1 * 60 * 1000; // 一分钟缓冲
2790
+ return now >= new Date(this.expiration.getTime() - bufferMs);
2791
+ }
2792
+
2793
+ get isValid() {
2794
+ if (!this.getIdToken) {
2795
+ throw new Error('Missing required configuration: getIdToken function')
2796
+ }
2797
+ if (!this.roleArn) {
2798
+ throw new Error('Missing required configuration: roleArn')
2799
+ }
2800
+ if (!this.awsClientSts) {
2801
+ throw new Error('Missing required AWS awsClientSts client configuration')
2802
+ }
2803
+ if (!this.awsClientSts.STSClient) {
2804
+ throw new Error('Missing STSClient in AWS awsClientSts client configuration')
2805
+ }
2806
+ if (!this.awsClientSts.AssumeRoleWithWebIdentityCommand) {
2807
+ throw new Error('Missing AssumeRoleWithWebIdentityCommand in AWS awsClientSts client configuration')
2808
+ }
2809
+ if (!this.awsClientS3) {
2810
+ throw new Error('Missing required AWS awsClientS3 client configuration')
2811
+ }
2812
+
2813
+ const requiredS3Components = [
2814
+ 'S3Client',
2815
+ 'PutObjectCommand',
2816
+ 'GetObjectCommand',
2817
+ 'DeleteObjectCommand'
2818
+ ]
2819
+
2820
+ for (const component of requiredS3Components) {
2821
+ if (!this.awsClientS3[component]) {
2822
+ throw new Error(`Missing ${component} in AWS awsClientS3 client configuration`)
2823
+ }
2824
+ }
2825
+
2826
+ return true
2827
+ }
2828
+
2829
+ async refreshCredentials() {
2830
+ try {
2831
+ const webIdentityToken = await this.getIdToken()
2832
+ if (!webIdentityToken) {
2833
+ throw new Error('getIdToken function returned empty or invalid token')
2834
+ }
2835
+
2836
+ const stsClient = new this.awsClientSts.STSClient({ region: this.region })
2837
+
2838
+ const stsResponse = await stsClient.send(
2839
+ new this.awsClientSts.AssumeRoleWithWebIdentityCommand({
2840
+ RoleArn: this.roleArn,
2841
+ RoleSessionName: this.roleSessionName,
2842
+ WebIdentityToken: await this.getIdToken(),
2843
+ DurationSeconds: this.durationSession,
2844
+ })
2845
+ )
2846
+
2847
+ const credentials = stsResponse.Credentials
2848
+ if (!credentials) {
2849
+ throw new Error('No credentials returned from awsClientSts')
2850
+ }
2851
+
2852
+ this.expiration = credentials.Expiration
2853
+
2854
+ this.s3Client = new this.awsClientS3.S3Client({
2855
+ region: this.region,
2856
+ credentials: {
2857
+ accessKeyId: credentials.AccessKeyId,
2858
+ secretAccessKey: credentials.SecretAccessKey,
2859
+ sessionToken: credentials.SessionToken,
2860
+ }
2861
+ })
2862
+
2863
+ return this
2864
+ } catch (error) {
2865
+ throw new Error(`Failed to refresh credentials: ${error.message}`)
2866
+ }
2867
+ }
2868
+
2869
+ async getS3Client() {
2870
+ if (this.isExpired || !this.s3Client) {
2871
+ await this.refreshCredentials()
2872
+ }
2873
+ return this.s3Client
2874
+ }
2875
+
2876
+ async putObject(params) {
2877
+ try {
2878
+ const client = await this.getS3Client()
2879
+ const command = new this.awsClientS3.PutObjectCommand(params)
2880
+ await client.send(command)
2881
+ const fileArr = params.Key.split('/')
2882
+ return {
2883
+ url: `https://s3.${this.region}.amazonaws.com/${params.Bucket}/${params.Key}`,
2884
+ filename: fileArr.pop(),
2885
+ folder: params.Bucket,
2886
+ subFolders: fileArr
2887
+ }
2888
+ } catch (error) {
2889
+ throw new Error(`Failed to put object: ${error.message}`)
2890
+ }
2891
+ }
2892
+
2893
+ async getObject(params) {
2894
+ try {
2895
+ const client = await this.getS3Client()
2896
+ const command = new this.awsClientS3.GetObjectCommand(params)
2897
+ const response = await client.send(command)
2898
+ return {
2899
+ body: response.Body,
2900
+ contentType: response.ContentType,
2901
+ lastModified: response.LastModified,
2902
+ contentLength: response.ContentLength,
2903
+ }
2904
+ } catch (error) {
2905
+ throw new Error(`Failed to get object: ${error.message}`)
2906
+ }
2907
+ }
2908
+
2909
+ async deleteObject(params) {
2910
+ try {
2911
+ const client = await this.getS3Client()
2912
+ const command = new this.awsClientS3.DeleteObjectCommand(params)
2913
+ await client.send(command)
2914
+ return true
2915
+ } catch (error) {
2916
+ throw new Error(`Failed to delete object: ${error.message}`)
2917
+ }
2918
+ }
2919
+ }
2920
+
2921
+
2922
+
2923
+ ;// ./lib/models/awsStsS3Client/index.js
2924
+
2925
+
2926
+
2927
+
2287
2928
  ;// ./lib/models/keyValueObject/keyValueObject.js
2288
2929
  class KeyValueObject {
2289
2930
  constructor(options = {}) {
@@ -2411,7 +3052,7 @@ class KeyValueObject {
2411
3052
  return to.key === from.key
2412
3053
  })
2413
3054
  if (found) {
2414
- found.value = (found.value || []).concat(from.value)
3055
+ found.value = _mergeValues(from.value, found.value)
2415
3056
  } else {
2416
3057
  toArr.push(from)
2417
3058
  }
@@ -2427,7 +3068,10 @@ class KeyValueObject {
2427
3068
  }, [])
2428
3069
  }
2429
3070
  static sameKey(item, key) {
2430
- return _isSame(item.key, key)
3071
+ if (item) {
3072
+ return _isSame(item.key, key)
3073
+ }
3074
+ return false
2431
3075
  }
2432
3076
  static toObject(arr = []) {
2433
3077
  if (Array.isArray(arr)) {
@@ -2496,6 +3140,34 @@ class KeyValueObject {
2496
3140
  }
2497
3141
  }
2498
3142
 
3143
+ function _mergeValues(existingValue, newValue) {
3144
+ if (existingValue === undefined) return newValue
3145
+
3146
+ // Handle arrays by concatenating
3147
+ if (Array.isArray(existingValue) && Array.isArray(newValue)) {
3148
+ return [...new Set([...existingValue, ...newValue])]
3149
+ }
3150
+
3151
+ // Handle objects by merging
3152
+ if (typeof existingValue === 'object' && typeof newValue === 'object' &&
3153
+ !Array.isArray(existingValue) && !Array.isArray(newValue)) {
3154
+ return { ...existingValue, ...newValue }
3155
+ }
3156
+
3157
+ // // Handle numbers by adding
3158
+ // if (typeof existingValue === 'number' && typeof newValue === 'number') {
3159
+ // return existingValue
3160
+ // }
3161
+
3162
+ // // Handle strings by concatenating
3163
+ // if (typeof existingValue === 'string' && typeof newValue === 'string') {
3164
+ // return existingValue
3165
+ // }
3166
+
3167
+ // Default: use the new value
3168
+ return newValue
3169
+ }
3170
+
2499
3171
  function _isSame(key1, key2) {
2500
3172
  return key1 === key2
2501
3173
  }
@@ -2534,7 +3206,7 @@ class Metadata extends KeyValueObject {
2534
3206
  return metadata_isSame(to.key, from.key)
2535
3207
  })
2536
3208
  if (found) {
2537
- found.value = (found.value || []).concat(from.value)
3209
+ found.value = metadata_mergeValues(from.value, found.value)
2538
3210
  } else {
2539
3211
  toArr.push(from)
2540
3212
  }
@@ -2550,6 +3222,34 @@ function metadata_isSame(key1, key2) {
2550
3222
  return stringFormatter(key1, DELIMITER) === stringFormatter(key2, DELIMITER)
2551
3223
  }
2552
3224
 
3225
+ function metadata_mergeValues(existingValue, newValue) {
3226
+ if (existingValue === undefined) return newValue
3227
+
3228
+ // Handle arrays by concatenating
3229
+ if (Array.isArray(existingValue) && Array.isArray(newValue)) {
3230
+ return [...new Set([...existingValue, ...newValue])]
3231
+ }
3232
+
3233
+ // Handle objects by merging
3234
+ if (typeof existingValue === 'object' && typeof newValue === 'object' &&
3235
+ !Array.isArray(existingValue) && !Array.isArray(newValue)) {
3236
+ return { ...existingValue, ...newValue }
3237
+ }
3238
+
3239
+ // // Handle numbers by adding
3240
+ // if (typeof existingValue === 'number' && typeof newValue === 'number') {
3241
+ // return existingValue
3242
+ // }
3243
+
3244
+ // // Handle strings by concatenating
3245
+ // if (typeof existingValue === 'string' && typeof newValue === 'string') {
3246
+ // return existingValue
3247
+ // }
3248
+
3249
+ // Default: use the new value
3250
+ return newValue
3251
+ }
3252
+
2553
3253
 
2554
3254
 
2555
3255
  ;// ./lib/models/metadata/index.js
@@ -2625,17 +3325,19 @@ class TrackedEntity {
2625
3325
  constructor(options = {}) {
2626
3326
  options = options || {}
2627
3327
  const timestamp = Date.now()
2628
- const _tracking = {
2629
- active: options.active ?? true,
2630
- created: options.created ?? timestamp,
2631
- creator: options.creator ?? '',
2632
- deleted: options.deleted ?? false,
2633
- modified: options.modified ?? timestamp,
2634
- owner: options.owner ?? '',
3328
+ this.meta = {
3329
+ active: options.meta?.active ?? options.active ?? true,
3330
+ created: options.meta?.created ?? (options.created
3331
+ ? new Date(options.created).getTime()
3332
+ : timestamp),
3333
+ creator: options.meta?.creator ?? options.creator ?? '',
3334
+ deleted: options.meta?.deleted ?? options.deleted ?? false,
3335
+ modified: options.meta?.modified ?? (options.modified
3336
+ ? new Date(options.modified).getTime()
3337
+ : timestamp),
3338
+ owner: options.meta?.owner ?? options.owner ?? '',
2635
3339
  }
2636
3340
 
2637
- this.meta = { ..._tracking, ...options.meta }
2638
-
2639
3341
  // if (trackFlat) {
2640
3342
  // Object.assign(this, _tracking)
2641
3343
  // } else {
@@ -2687,6 +3389,9 @@ class TrackedEntity {
2687
3389
  get owner() {
2688
3390
  return this.meta?.owner ?? this.owner
2689
3391
  }
3392
+ changeCreatorOwner({ source, target }) {
3393
+ return changeCreatorOwner(this, { source, target }).setModified()
3394
+ }
2690
3395
  delete() {
2691
3396
  return this.setDeleted()
2692
3397
  }
@@ -2893,6 +3598,7 @@ function _makeSetCode(fieldName, options) {
2893
3598
 
2894
3599
 
2895
3600
 
3601
+
2896
3602
  ;// ./lib/index.js
2897
3603
 
2898
3604