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