@flusys/nestjs-shared 9.1.1 → 9.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/fesm/282.js +1580 -0
- package/fesm/index.js +50 -6
- package/fesm/interfaces/index.js +82 -2
- package/fesm/utils/index.js +16 -3
- package/interfaces/expression.interface.d.ts +45 -0
- package/interfaces/index.d.ts +1 -0
- package/package.json +3 -2
- package/utils/expression.util.d.ts +35 -0
- package/utils/index.d.ts +2 -0
- package/utils/safe-regex.util.d.ts +10 -0
- package/fesm/270.js +0 -659
package/fesm/282.js
ADDED
|
@@ -0,0 +1,1580 @@
|
|
|
1
|
+
import { RE2JS } from "re2js";
|
|
2
|
+
export const __rspack_esm_id = 282;
|
|
3
|
+
export const __rspack_esm_ids = [282];
|
|
4
|
+
export const __webpack_modules__ = {
|
|
5
|
+
79(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
|
|
6
|
+
const EXPRESSION_ARITHMETIC_OPS = [
|
|
7
|
+
'sum',
|
|
8
|
+
'subtract',
|
|
9
|
+
'multiply',
|
|
10
|
+
'divide',
|
|
11
|
+
'modulo',
|
|
12
|
+
'power',
|
|
13
|
+
'average',
|
|
14
|
+
'min',
|
|
15
|
+
'max'
|
|
16
|
+
];
|
|
17
|
+
const EXPRESSION_COMPARISONS = [
|
|
18
|
+
'equals',
|
|
19
|
+
'not_equals',
|
|
20
|
+
'greater_than',
|
|
21
|
+
'less_than',
|
|
22
|
+
'greater_or_equal',
|
|
23
|
+
'less_or_equal',
|
|
24
|
+
'contains',
|
|
25
|
+
'not_contains',
|
|
26
|
+
'starts_with',
|
|
27
|
+
'ends_with',
|
|
28
|
+
'in',
|
|
29
|
+
'not_in',
|
|
30
|
+
'is_empty',
|
|
31
|
+
'is_not_empty',
|
|
32
|
+
'is_true',
|
|
33
|
+
'is_false',
|
|
34
|
+
'matches',
|
|
35
|
+
'not_matches'
|
|
36
|
+
];
|
|
37
|
+
/** Comparisons that only look at the left side. */ const UNARY_EXPRESSION_COMPARISONS = [
|
|
38
|
+
'is_empty',
|
|
39
|
+
'is_not_empty',
|
|
40
|
+
'is_true',
|
|
41
|
+
'is_false'
|
|
42
|
+
];
|
|
43
|
+
const EXPRESSION_DURATION_UNITS = [
|
|
44
|
+
'second',
|
|
45
|
+
'minute',
|
|
46
|
+
'hour',
|
|
47
|
+
'day',
|
|
48
|
+
'week',
|
|
49
|
+
'month',
|
|
50
|
+
'year'
|
|
51
|
+
];
|
|
52
|
+
const EXPRESSION_DATE_PARTS = [
|
|
53
|
+
'year',
|
|
54
|
+
'month',
|
|
55
|
+
'day',
|
|
56
|
+
'weekday',
|
|
57
|
+
'hour',
|
|
58
|
+
'minute'
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
62
|
+
}, {
|
|
63
|
+
Me: EXPRESSION_COMPARISONS,
|
|
64
|
+
Vq: EXPRESSION_DATE_PARTS,
|
|
65
|
+
YH: EXPRESSION_DURATION_UNITS,
|
|
66
|
+
dL: EXPRESSION_ARITHMETIC_OPS,
|
|
67
|
+
kA: UNARY_EXPRESSION_COMPARISONS
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
},
|
|
72
|
+
129(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
|
|
73
|
+
|
|
74
|
+
// EXPORTS
|
|
75
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
76
|
+
s8: () => (/* reexport */ permission_cache_key_util/* .DEFAULT_COMPANY_PERMISSION_KEY_FORMAT */.s8),
|
|
77
|
+
q6: () => (/* reexport */ permission_cache_key_util/* .DEFAULT_USER_PERMISSION_KEY_FORMAT */.q6),
|
|
78
|
+
Dd: () => (/* reexport */ EXPRESSION_FUNCTIONS),
|
|
79
|
+
wj: () => (/* reexport */ EXPRESSION_LIMITS),
|
|
80
|
+
tj: () => (/* reexport */ ExpressionError),
|
|
81
|
+
Df: () => (/* reexport */ (/* inlined export .MAX_PERMISSION_LOGIC_CODES */50)),
|
|
82
|
+
kW: () => (/* reexport */ (/* inlined export .MAX_PERMISSION_LOGIC_DEPTH */5)),
|
|
83
|
+
ZA: () => (/* reexport */ SAFE_REGEX_LIMITS),
|
|
84
|
+
f7: () => (/* reexport */ applyBranchFilter),
|
|
85
|
+
wf: () => (/* reexport */ applyCompanyFilter),
|
|
86
|
+
P: () => (/* reexport */ assertValidConditionGroup),
|
|
87
|
+
bh: () => (/* reexport */ assertValidExpression),
|
|
88
|
+
kQ: () => (/* reexport */ buildCompanyWhereCondition),
|
|
89
|
+
fJ: () => (/* reexport */ buildCookieOptions),
|
|
90
|
+
oV: () => (/* reexport */ buildFrontendUrl),
|
|
91
|
+
nt: () => (/* reexport */ permission_cache_key_util/* .buildPermissionCacheKey */.nt),
|
|
92
|
+
qW: () => (/* reexport */ cache_tenant_util/* .cacheTenantId */.q),
|
|
93
|
+
Sf: () => (/* reexport */ collectBranchAncestorIds),
|
|
94
|
+
qY: () => (/* reexport */ collectBranchDescendantIds),
|
|
95
|
+
Yc: () => (/* reexport */ compileSafeRegex),
|
|
96
|
+
ZD: () => (/* reexport */ escapeHtml),
|
|
97
|
+
qo: () => (/* reexport */ escapeHtmlVariables),
|
|
98
|
+
Sb: () => (/* reexport */ evaluateConditionGroup),
|
|
99
|
+
O4: () => (/* reexport */ evaluateExpression),
|
|
100
|
+
Ng: () => (/* reexport */ permission_match_util/* .evaluatePermissionLogic */.Ng),
|
|
101
|
+
HK: () => (/* reexport */ expressionToNumber),
|
|
102
|
+
zj: () => (/* reexport */ expressionToText),
|
|
103
|
+
hX: () => (/* reexport */ extractFrontendUrl),
|
|
104
|
+
Sd: () => (/* reexport */ formatInTimezone),
|
|
105
|
+
sN: () => (/* reexport */ generateSecureToken),
|
|
106
|
+
z9: () => (/* reexport */ generateSlug),
|
|
107
|
+
oo: () => (/* reexport */ generateUniqueSlug),
|
|
108
|
+
gN: () => (/* reexport */ getBranchPath),
|
|
109
|
+
oB: () => (/* reexport */ getTodayInTimezone),
|
|
110
|
+
f: () => (/* reexport */ hasCompanyId),
|
|
111
|
+
QK: () => (/* reexport */ hashToken),
|
|
112
|
+
QU: () => (/* reexport */ isBranchWithin),
|
|
113
|
+
wh: () => (/* reexport */ isBrowserRequest),
|
|
114
|
+
_d: () => (/* reexport */ isExpired),
|
|
115
|
+
tX: () => (/* reexport */ isExpressionError),
|
|
116
|
+
_c: () => (/* reexport */ event_pattern_util/* .isMultiWildcard */._c),
|
|
117
|
+
lk: () => (/* reexport */ optional_module_util/* .loadOptionalModule */.l),
|
|
118
|
+
CQ: () => (/* reexport */ permission_match_util/* .loadPermissionCodes */.CQ),
|
|
119
|
+
vf: () => (/* reexport */ localToUtc),
|
|
120
|
+
qe: () => (/* reexport */ event_pattern_util/* .matchEventPattern */.qe),
|
|
121
|
+
xY: () => (/* reexport */ permission_match_util/* .matchesPermission */.xY),
|
|
122
|
+
RG: () => (/* reexport */ messageRef),
|
|
123
|
+
Ty: () => (/* reexport */ normalizeBranchIds),
|
|
124
|
+
Ju: () => (/* reexport */ parseDurationToMs),
|
|
125
|
+
MO: () => (/* reexport */ permission_match_util/* .permissionLogicCodes */.MO),
|
|
126
|
+
HU: () => (/* reexport */ permission_match_util/* .permissionLogicProblem */.HU),
|
|
127
|
+
_X: () => (/* reexport */ safeRegexProblem),
|
|
128
|
+
IN: () => (/* reexport */ event_pattern_util/* .sanitizeEventPayload */.IN),
|
|
129
|
+
v4: () => (/* reexport */ utcToLocal),
|
|
130
|
+
bE: () => (/* reexport */ validateCompanyOwnership),
|
|
131
|
+
o0: () => (/* reexport */ withSavepoint)
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
;// CONCATENATED MODULE: ./projects/nestjs-shared/src/utils/branch-hierarchy.util.ts
|
|
135
|
+
/** Each id once, in input order, without empty values. */ function normalizeBranchIds(ids) {
|
|
136
|
+
const list = Array.isArray(ids) ? ids : [
|
|
137
|
+
ids
|
|
138
|
+
];
|
|
139
|
+
return [
|
|
140
|
+
...new Set(list.filter((id)=>typeof id === 'string' && id !== ''))
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
function depthLimit(maxDepth) {
|
|
144
|
+
return typeof maxDepth === 'number' && Number.isFinite(maxDepth) && maxDepth >= 0 ? Math.floor(maxDepth) : Infinity;
|
|
145
|
+
}
|
|
146
|
+
/** The first node per id wins, so duplicates never change the answer. */ function indexNodes(nodes) {
|
|
147
|
+
const byId = new Map();
|
|
148
|
+
for (const node of nodes)if (node?.id && !byId.has(node.id)) byId.set(node.id, node);
|
|
149
|
+
return byId;
|
|
150
|
+
}
|
|
151
|
+
function childrenIndex(nodes) {
|
|
152
|
+
const children = new Map();
|
|
153
|
+
for (const node of nodes){
|
|
154
|
+
if (!node.parentId || node.parentId === node.id) continue;
|
|
155
|
+
const siblings = children.get(node.parentId);
|
|
156
|
+
if (siblings) siblings.push(node.id);
|
|
157
|
+
else children.set(node.parentId, [
|
|
158
|
+
node.id
|
|
159
|
+
]);
|
|
160
|
+
}
|
|
161
|
+
return children;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Every branch under `rootIds`, level by level (children, then grandchildren, ...), from a flat
|
|
165
|
+
* `{ id, parentId }` list. With `includeSelf` the given ids come first - kept even when they are not in
|
|
166
|
+
* `nodes`, so a caller never loses a branch it asked about. Safe against duplicates and parent cycles.
|
|
167
|
+
*/ function collectBranchDescendantIds(nodes, rootIds, { includeSelf = false, maxDepth } = {}) {
|
|
168
|
+
const roots = normalizeBranchIds(rootIds);
|
|
169
|
+
const limit = depthLimit(maxDepth);
|
|
170
|
+
const children = childrenIndex(indexNodes(nodes).values());
|
|
171
|
+
const visited = new Set(roots);
|
|
172
|
+
const descendants = [];
|
|
173
|
+
let level = roots;
|
|
174
|
+
for(let depth = 1; depth <= limit && level.length; depth++){
|
|
175
|
+
const next = [];
|
|
176
|
+
for (const parentId of level){
|
|
177
|
+
for (const id of children.get(parentId) ?? []){
|
|
178
|
+
if (visited.has(id)) continue;
|
|
179
|
+
visited.add(id);
|
|
180
|
+
descendants.push(id);
|
|
181
|
+
next.push(id);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
level = next;
|
|
185
|
+
}
|
|
186
|
+
return includeSelf ? [
|
|
187
|
+
...roots,
|
|
188
|
+
...descendants
|
|
189
|
+
] : descendants;
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* The parent chain of `branchId`, nearest parent first, up to the root. Only branches in `nodes` are
|
|
193
|
+
* returned - the walk stops at a parent that is missing (e.g. soft-deleted). With `includeSelf` the
|
|
194
|
+
* branch itself comes first even when it is not in `nodes`. Safe against parent cycles.
|
|
195
|
+
*/ function collectBranchAncestorIds(nodes, branchId, { includeSelf = false, maxDepth } = {}) {
|
|
196
|
+
if (!branchId) return [];
|
|
197
|
+
const byId = indexNodes(nodes);
|
|
198
|
+
const limit = depthLimit(maxDepth);
|
|
199
|
+
const ancestors = includeSelf ? [
|
|
200
|
+
branchId
|
|
201
|
+
] : [];
|
|
202
|
+
const visited = new Set([
|
|
203
|
+
branchId
|
|
204
|
+
]);
|
|
205
|
+
let parentId = byId.get(branchId)?.parentId;
|
|
206
|
+
for(let depth = 1; depth <= limit && parentId && byId.has(parentId) && !visited.has(parentId); depth++){
|
|
207
|
+
visited.add(parentId);
|
|
208
|
+
ancestors.push(parentId);
|
|
209
|
+
parentId = byId.get(parentId)?.parentId;
|
|
210
|
+
}
|
|
211
|
+
return ancestors;
|
|
212
|
+
}
|
|
213
|
+
/** True when `branchId` is `ancestorId` itself or sits anywhere under it. */ function isBranchWithin(nodes, branchId, ancestorId) {
|
|
214
|
+
if (!branchId || !ancestorId) return false;
|
|
215
|
+
return branchId === ancestorId || collectBranchAncestorIds(nodes, branchId).includes(ancestorId);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* The nodes from the top-most known ancestor down to `branchId` itself (e.g. for "HQ / East / East 1").
|
|
219
|
+
* Empty when `branchId` is not in `nodes`.
|
|
220
|
+
*/ function getBranchPath(nodes, branchId) {
|
|
221
|
+
if (!branchId) return [];
|
|
222
|
+
const byId = indexNodes(nodes);
|
|
223
|
+
if (!byId.has(branchId)) return [];
|
|
224
|
+
return collectBranchAncestorIds(nodes, branchId, {
|
|
225
|
+
includeSelf: true
|
|
226
|
+
}).map((id)=>byId.get(id)).toReversed();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// EXTERNAL MODULE: ./projects/nestjs-shared/src/utils/cache-tenant.util.ts
|
|
230
|
+
var cache_tenant_util = __webpack_require__(154);
|
|
231
|
+
;// CONCATENATED MODULE: ./projects/nestjs-shared/src/utils/date-time.util.ts
|
|
232
|
+
function getOffsetMinutes(utcDate, timezone) {
|
|
233
|
+
const extract = (tz)=>{
|
|
234
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
235
|
+
timeZone: tz,
|
|
236
|
+
year: 'numeric',
|
|
237
|
+
month: '2-digit',
|
|
238
|
+
day: '2-digit',
|
|
239
|
+
hour: '2-digit',
|
|
240
|
+
minute: '2-digit',
|
|
241
|
+
second: '2-digit',
|
|
242
|
+
hour12: false
|
|
243
|
+
}).formatToParts(utcDate);
|
|
244
|
+
const p = Object.fromEntries(parts.filter((x)=>x.type !== 'literal').map((x)=>[
|
|
245
|
+
x.type,
|
|
246
|
+
parseInt(x.value)
|
|
247
|
+
]));
|
|
248
|
+
return Date.UTC(p['year'], p['month'] - 1, p['day'], p['hour'], p['minute'], p['second']);
|
|
249
|
+
};
|
|
250
|
+
return (extract(timezone) - extract('UTC')) / 60_000;
|
|
251
|
+
}
|
|
252
|
+
function localToUtc(dateStr, timeStr, timezone) {
|
|
253
|
+
const [year, month, day] = dateStr.split('-').map(Number);
|
|
254
|
+
const [hours, minutes] = timeStr.split(':').map(Number);
|
|
255
|
+
const approx = new Date(Date.UTC(year, month - 1, day, hours, minutes, 0));
|
|
256
|
+
const offset = getOffsetMinutes(approx, timezone);
|
|
257
|
+
const utcDate = new Date(approx.getTime() - offset * 60_000);
|
|
258
|
+
// DST boundary guard: re-check offset after adjustment
|
|
259
|
+
const finalOffset = getOffsetMinutes(utcDate, timezone);
|
|
260
|
+
return finalOffset !== offset ? new Date(approx.getTime() - finalOffset * 60_000) : utcDate;
|
|
261
|
+
}
|
|
262
|
+
function utcToLocal(utcDate, timezone) {
|
|
263
|
+
const d = typeof utcDate === 'string' ? new Date(utcDate) : utcDate;
|
|
264
|
+
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
265
|
+
timeZone: timezone,
|
|
266
|
+
year: 'numeric',
|
|
267
|
+
month: '2-digit',
|
|
268
|
+
day: '2-digit',
|
|
269
|
+
hour: '2-digit',
|
|
270
|
+
minute: '2-digit',
|
|
271
|
+
hour12: false
|
|
272
|
+
}).formatToParts(d);
|
|
273
|
+
const p = Object.fromEntries(parts.filter((x)=>x.type !== 'literal').map((x)=>[
|
|
274
|
+
x.type,
|
|
275
|
+
x.value
|
|
276
|
+
]));
|
|
277
|
+
// en-CA formats midnight hour as '24' — normalize to '00'
|
|
278
|
+
const hour = p['hour'] === '24' ? '00' : p['hour'];
|
|
279
|
+
return {
|
|
280
|
+
date: `${p['year']}-${p['month']}-${p['day']}`,
|
|
281
|
+
time: `${hour}:${p['minute']}`,
|
|
282
|
+
display: new Intl.DateTimeFormat('en-US', {
|
|
283
|
+
timeZone: timezone,
|
|
284
|
+
dateStyle: 'medium',
|
|
285
|
+
timeStyle: 'short'
|
|
286
|
+
}).format(d)
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function formatInTimezone(utcDate, timezone, options = {
|
|
290
|
+
dateStyle: 'medium',
|
|
291
|
+
timeStyle: 'short'
|
|
292
|
+
}) {
|
|
293
|
+
const d = typeof utcDate === 'string' ? new Date(utcDate) : utcDate;
|
|
294
|
+
return new Intl.DateTimeFormat('en-US', {
|
|
295
|
+
...options,
|
|
296
|
+
timeZone: timezone
|
|
297
|
+
}).format(d);
|
|
298
|
+
}
|
|
299
|
+
function getTodayInTimezone(timezone) {
|
|
300
|
+
return utcToLocal(new Date(), timezone).date;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// EXTERNAL MODULE: ./projects/nestjs-shared/src/utils/event-pattern.util.ts
|
|
304
|
+
var event_pattern_util = __webpack_require__(456);
|
|
305
|
+
// EXTERNAL MODULE: ./projects/nestjs-shared/src/interfaces/expression.interface.ts
|
|
306
|
+
var expression_interface = __webpack_require__(79);
|
|
307
|
+
;// CONCATENATED MODULE: external "re2js"
|
|
308
|
+
|
|
309
|
+
;// CONCATENATED MODULE: ./projects/nestjs-shared/src/utils/safe-regex.util.ts
|
|
310
|
+
|
|
311
|
+
const SAFE_REGEX_LIMITS = {
|
|
312
|
+
MAX_PATTERN_LENGTH: 500,
|
|
313
|
+
MAX_INPUT_LENGTH: 10_000,
|
|
314
|
+
/** Largest `{n,m}` count, also for the product of nested counts. */ MAX_REPEAT: 100,
|
|
315
|
+
/** How much larger counted repeats may make the unrolled pattern than the pattern itself. */ MAX_REPEAT_EXPANSION: 300
|
|
316
|
+
};
|
|
317
|
+
const MAX_CACHED_PATTERNS = 200;
|
|
318
|
+
const compiled = new Map();
|
|
319
|
+
const tooComplex = new Map();
|
|
320
|
+
function remember(cache, pattern, make) {
|
|
321
|
+
if (cache.has(pattern)) return cache.get(pattern);
|
|
322
|
+
const value = make(pattern);
|
|
323
|
+
if (cache.size >= MAX_CACHED_PATTERNS) cache.delete(cache.keys().next().value);
|
|
324
|
+
cache.set(pattern, value);
|
|
325
|
+
return value;
|
|
326
|
+
}
|
|
327
|
+
/** Why an author-written pattern cannot be used, or null when it compiles within the limits. */ function safeRegexProblem(pattern) {
|
|
328
|
+
if (pattern.length > SAFE_REGEX_LIMITS.MAX_PATTERN_LENGTH) return 'too_long';
|
|
329
|
+
if (remember(tooComplex, pattern, isTooComplex)) return 'too_complex';
|
|
330
|
+
try {
|
|
331
|
+
compileSafeRegex(pattern);
|
|
332
|
+
return null;
|
|
333
|
+
} catch {
|
|
334
|
+
return 'invalid';
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/** RE2 (linear-time) compile with a small cache. Call `safeRegexProblem` first for author input. */ function compileSafeRegex(pattern) {
|
|
338
|
+
return remember(compiled, pattern, (p)=>RE2JS.compile(p));
|
|
339
|
+
}
|
|
340
|
+
const newFrame = ()=>({
|
|
341
|
+
size: 0,
|
|
342
|
+
last: 0,
|
|
343
|
+
lastProduct: 1,
|
|
344
|
+
maxProduct: 1
|
|
345
|
+
});
|
|
346
|
+
/**
|
|
347
|
+
* RE2 unrolls counted repeats (`x{2,50}` becomes 50 copies of x), so match time grows with the
|
|
348
|
+
* product of nested counts. Rejects a count above MAX_REPEAT, nested counts whose product
|
|
349
|
+
* is above it, and a pattern whose unrolled size grows by more than MAX_REPEAT_EXPANSION.
|
|
350
|
+
* `*`, `+` and `?` do not unroll and stay allowed, nested or not.
|
|
351
|
+
*/ function isTooComplex(pattern) {
|
|
352
|
+
const { MAX_REPEAT, MAX_REPEAT_EXPANSION } = SAFE_REGEX_LIMITS;
|
|
353
|
+
const stack = [
|
|
354
|
+
newFrame()
|
|
355
|
+
];
|
|
356
|
+
const top = ()=>stack[stack.length - 1];
|
|
357
|
+
let atoms = 0;
|
|
358
|
+
const atom = (size, product)=>{
|
|
359
|
+
const frame = top();
|
|
360
|
+
frame.size += size;
|
|
361
|
+
frame.last = size;
|
|
362
|
+
frame.lastProduct = product;
|
|
363
|
+
frame.maxProduct = Math.max(frame.maxProduct, product);
|
|
364
|
+
};
|
|
365
|
+
let i = 0;
|
|
366
|
+
while(i < pattern.length){
|
|
367
|
+
const c = pattern[i];
|
|
368
|
+
if (c === '\\') {
|
|
369
|
+
const next = pattern[i + 1];
|
|
370
|
+
i += 2;
|
|
371
|
+
if ((next === 'p' || next === 'P' || next === 'x') && pattern[i] === '{') i = closing(pattern, i, '}');
|
|
372
|
+
atoms++;
|
|
373
|
+
atom(1, 1);
|
|
374
|
+
} else if (c === '[') {
|
|
375
|
+
i = classEnd(pattern, i);
|
|
376
|
+
atoms++;
|
|
377
|
+
atom(1, 1);
|
|
378
|
+
} else if (c === '(') {
|
|
379
|
+
i++;
|
|
380
|
+
if (pattern[i] === '?') {
|
|
381
|
+
const flagsOnly = /^\?[a-zA-Z-]*\)/.exec(pattern.slice(i));
|
|
382
|
+
if (flagsOnly) {
|
|
383
|
+
i += flagsOnly[0].length;
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
const marker = /^\?(?:P?<[^>]*>|[a-zA-Z-]*:)/.exec(pattern.slice(i));
|
|
387
|
+
i += marker ? marker[0].length : 1;
|
|
388
|
+
}
|
|
389
|
+
stack.push(newFrame());
|
|
390
|
+
} else if (c === ')') {
|
|
391
|
+
i++;
|
|
392
|
+
if (stack.length === 1) continue;
|
|
393
|
+
const group = stack.pop();
|
|
394
|
+
atom(group.size, group.maxProduct);
|
|
395
|
+
} else if (c === '|') {
|
|
396
|
+
i++;
|
|
397
|
+
top().last = 0;
|
|
398
|
+
top().lastProduct = 1;
|
|
399
|
+
} else if (c === '{') {
|
|
400
|
+
const repeat = /^\{(\d+)(,(\d*))?\}\??/.exec(pattern.slice(i));
|
|
401
|
+
if (!repeat) {
|
|
402
|
+
i++;
|
|
403
|
+
atoms++;
|
|
404
|
+
atom(1, 1);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
i += repeat[0].length;
|
|
408
|
+
const min = Number(repeat[1]);
|
|
409
|
+
const max = repeat[2] === undefined ? min : repeat[3] ? Number(repeat[3]) : min + 1;
|
|
410
|
+
if (min > MAX_REPEAT || max > MAX_REPEAT) return true;
|
|
411
|
+
const frame = top();
|
|
412
|
+
const product = frame.lastProduct * Math.max(max, 1);
|
|
413
|
+
if (product > MAX_REPEAT) return true;
|
|
414
|
+
const extra = frame.last * (Math.max(max, 1) - 1);
|
|
415
|
+
frame.size += extra;
|
|
416
|
+
frame.last += extra;
|
|
417
|
+
frame.lastProduct = product;
|
|
418
|
+
frame.maxProduct = Math.max(frame.maxProduct, product);
|
|
419
|
+
} else {
|
|
420
|
+
i++;
|
|
421
|
+
if (c !== '*' && c !== '+' && c !== '?') {
|
|
422
|
+
atoms++;
|
|
423
|
+
atom(1, 1);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
while(stack.length > 1){
|
|
428
|
+
const group = stack.pop();
|
|
429
|
+
atom(group.size, group.maxProduct);
|
|
430
|
+
}
|
|
431
|
+
return stack[0].size - atoms > MAX_REPEAT_EXPANSION;
|
|
432
|
+
}
|
|
433
|
+
function closing(pattern, from, char) {
|
|
434
|
+
const end = pattern.indexOf(char, from);
|
|
435
|
+
return end < 0 ? pattern.length : end + 1;
|
|
436
|
+
}
|
|
437
|
+
function classEnd(pattern, start) {
|
|
438
|
+
let j = start + 1;
|
|
439
|
+
if (pattern[j] === '^') j++;
|
|
440
|
+
if (pattern[j] === ']') j++;
|
|
441
|
+
while(j < pattern.length && pattern[j] !== ']'){
|
|
442
|
+
if (pattern[j] === '\\') j += 2;
|
|
443
|
+
else if (pattern.startsWith('[:', j)) j = closing(pattern, j + 2, ']');
|
|
444
|
+
else j++;
|
|
445
|
+
}
|
|
446
|
+
return j + 1;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
;// CONCATENATED MODULE: ./projects/nestjs-shared/src/utils/expression.util.ts
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
const EXPRESSION_LIMITS = {
|
|
453
|
+
MAX_DEPTH: 16,
|
|
454
|
+
MAX_NODES: 500,
|
|
455
|
+
MAX_TEMPLATE_LENGTH: 4000,
|
|
456
|
+
MAX_TEXT_LENGTH: 100_000,
|
|
457
|
+
MAX_PAD_LENGTH: 1000
|
|
458
|
+
};
|
|
459
|
+
/** Plain error so each caller maps `code` to its own message key. */ class ExpressionError extends Error {
|
|
460
|
+
code;
|
|
461
|
+
variables;
|
|
462
|
+
constructor(code, variables = {}){
|
|
463
|
+
super(`${code} ${JSON.stringify(variables)}`), this.code = code, this.variables = variables;
|
|
464
|
+
this.name = 'ExpressionError';
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
/** By name, not instanceof: the class may come from another bundle. */ function isExpressionError(error) {
|
|
468
|
+
return error instanceof Error && error.name === 'ExpressionError' && 'code' in error;
|
|
469
|
+
}
|
|
470
|
+
const TEMPLATE_PLACEHOLDER = /\{\{\s*([\w.-]+)\s*\}\}/g;
|
|
471
|
+
const DAY_MS = 86_400_000;
|
|
472
|
+
const FIXED_UNIT_MS = {
|
|
473
|
+
second: 1000,
|
|
474
|
+
minute: 60_000,
|
|
475
|
+
hour: 3_600_000,
|
|
476
|
+
day: DAY_MS,
|
|
477
|
+
week: 7 * DAY_MS
|
|
478
|
+
};
|
|
479
|
+
// ── Value coercion ──────────────────────────────────────────────────────────────────────
|
|
480
|
+
const isBlank = (value)=>value === null || value === undefined || typeof value === 'string' && value.trim() === '';
|
|
481
|
+
/** Text of any value; null for null/undefined. Lists are joined with ", ". */ function expressionToText(value) {
|
|
482
|
+
if (value === null || value === undefined) return null;
|
|
483
|
+
if (typeof value === 'string') return value;
|
|
484
|
+
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
485
|
+
if (value instanceof Date) return value.toISOString();
|
|
486
|
+
if (Array.isArray(value)) return value.map((item)=>expressionToText(item) ?? '').join(', ');
|
|
487
|
+
return JSON.stringify(value);
|
|
488
|
+
}
|
|
489
|
+
/** Number of a value; null when blank, throws for text that is not a number. */ function expressionToNumber(value) {
|
|
490
|
+
if (isBlank(value)) return null;
|
|
491
|
+
if (typeof value === 'number') {
|
|
492
|
+
if (!Number.isFinite(value)) throw new ExpressionError('expected_number', {
|
|
493
|
+
value: String(value)
|
|
494
|
+
});
|
|
495
|
+
return value;
|
|
496
|
+
}
|
|
497
|
+
if (typeof value === 'boolean') return value ? 1 : 0;
|
|
498
|
+
if (typeof value === 'string') {
|
|
499
|
+
const n = Number(value.trim());
|
|
500
|
+
if (Number.isFinite(n)) return n;
|
|
501
|
+
}
|
|
502
|
+
throw new ExpressionError('expected_number', {
|
|
503
|
+
value: expressionToText(value) ?? ''
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
function tryNumber(value) {
|
|
507
|
+
try {
|
|
508
|
+
return expressionToNumber(value);
|
|
509
|
+
} catch {
|
|
510
|
+
return null;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
function toTime(value) {
|
|
514
|
+
if (isBlank(value)) return null;
|
|
515
|
+
const time = value instanceof Date ? value.getTime() : typeof value === 'number' ? value : typeof value === 'string' ? Date.parse(value) : Number.NaN;
|
|
516
|
+
if (!Number.isFinite(time)) throw new ExpressionError('expected_date', {
|
|
517
|
+
value: expressionToText(value) ?? ''
|
|
518
|
+
});
|
|
519
|
+
return time;
|
|
520
|
+
}
|
|
521
|
+
function toInteger(value, fallback) {
|
|
522
|
+
const n = expressionToNumber(value);
|
|
523
|
+
return n === null ? fallback : Math.trunc(n);
|
|
524
|
+
}
|
|
525
|
+
function toUnit(value) {
|
|
526
|
+
if (typeof value === 'string' && expression_interface/* .EXPRESSION_DURATION_UNITS.includes */.YH.includes(value)) return value;
|
|
527
|
+
throw new ExpressionError('invalid_unit', {
|
|
528
|
+
unit: String(value),
|
|
529
|
+
units: expression_interface/* .EXPRESSION_DURATION_UNITS.join */.YH.join(', ')
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
function limitText(text) {
|
|
533
|
+
if (text.length > EXPRESSION_LIMITS.MAX_TEXT_LENGTH) throw new ExpressionError('text_too_long', {
|
|
534
|
+
max: EXPRESSION_LIMITS.MAX_TEXT_LENGTH
|
|
535
|
+
});
|
|
536
|
+
return text;
|
|
537
|
+
}
|
|
538
|
+
const textFn = (map)=>([value])=>{
|
|
539
|
+
const text = expressionToText(value);
|
|
540
|
+
return text === null ? null : map(text);
|
|
541
|
+
};
|
|
542
|
+
const numberFn = (map)=>([value])=>{
|
|
543
|
+
const n = expressionToNumber(value);
|
|
544
|
+
return n === null ? null : map(n);
|
|
545
|
+
};
|
|
546
|
+
// ── Regex (RE2, linear time) ────────────────────────────────────────────────────────────
|
|
547
|
+
function regexProblemError(pattern) {
|
|
548
|
+
switch(safeRegexProblem(pattern)){
|
|
549
|
+
case 'too_long':
|
|
550
|
+
return new ExpressionError('regex_too_long', {
|
|
551
|
+
max: SAFE_REGEX_LIMITS.MAX_PATTERN_LENGTH
|
|
552
|
+
});
|
|
553
|
+
case 'too_complex':
|
|
554
|
+
return new ExpressionError('regex_too_complex', {
|
|
555
|
+
max: SAFE_REGEX_LIMITS.MAX_REPEAT
|
|
556
|
+
});
|
|
557
|
+
case 'invalid':
|
|
558
|
+
return new ExpressionError('regex_invalid', {
|
|
559
|
+
pattern
|
|
560
|
+
});
|
|
561
|
+
default:
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function prepareRegex(input, pattern) {
|
|
566
|
+
const source = expressionToText(pattern) ?? '';
|
|
567
|
+
const problem = regexProblemError(source);
|
|
568
|
+
if (problem) throw problem;
|
|
569
|
+
const text = expressionToText(input);
|
|
570
|
+
if (text === null) return null;
|
|
571
|
+
if (text.length > SAFE_REGEX_LIMITS.MAX_INPUT_LENGTH) throw new ExpressionError('regex_input_too_long', {
|
|
572
|
+
max: SAFE_REGEX_LIMITS.MAX_INPUT_LENGTH
|
|
573
|
+
});
|
|
574
|
+
return {
|
|
575
|
+
regex: compileSafeRegex(source),
|
|
576
|
+
text
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
function regexTest(input, pattern) {
|
|
580
|
+
const prepared = prepareRegex(input, pattern);
|
|
581
|
+
return prepared ? prepared.regex.matcher(prepared.text).find() : false;
|
|
582
|
+
}
|
|
583
|
+
// ── Dates ───────────────────────────────────────────────────────────────────────────────
|
|
584
|
+
function addMonths(time, months) {
|
|
585
|
+
const date = new Date(time);
|
|
586
|
+
const day = date.getUTCDate();
|
|
587
|
+
date.setUTCDate(1);
|
|
588
|
+
date.setUTCMonth(date.getUTCMonth() + months);
|
|
589
|
+
const lastDay = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0)).getUTCDate();
|
|
590
|
+
date.setUTCDate(Math.min(day, lastDay));
|
|
591
|
+
return date.getTime();
|
|
592
|
+
}
|
|
593
|
+
/** Whole calendar months from `from` to `to` (negative when `to` is earlier). */ function monthsBetween(from, to) {
|
|
594
|
+
const a = new Date(from);
|
|
595
|
+
const b = new Date(to);
|
|
596
|
+
let months = (b.getUTCFullYear() - a.getUTCFullYear()) * 12 + (b.getUTCMonth() - a.getUTCMonth());
|
|
597
|
+
const shifted = addMonths(from, months);
|
|
598
|
+
if (months > 0 && shifted > to) months--;
|
|
599
|
+
if (months < 0 && shifted < to) months++;
|
|
600
|
+
return months;
|
|
601
|
+
}
|
|
602
|
+
function dateDiff(a, b, unit) {
|
|
603
|
+
const to = toTime(a);
|
|
604
|
+
const from = toTime(b);
|
|
605
|
+
const resolved = toUnit(unit);
|
|
606
|
+
if (to === null || from === null) return null;
|
|
607
|
+
if (resolved === 'month') return monthsBetween(from, to);
|
|
608
|
+
if (resolved === 'year') return Math.trunc(monthsBetween(from, to) / 12);
|
|
609
|
+
return Math.trunc((to - from) / FIXED_UNIT_MS[resolved]);
|
|
610
|
+
}
|
|
611
|
+
function addDuration(value, amount, unit) {
|
|
612
|
+
const time = toTime(value);
|
|
613
|
+
const n = expressionToNumber(amount) ?? 0;
|
|
614
|
+
const resolved = toUnit(unit);
|
|
615
|
+
if (time === null) return null;
|
|
616
|
+
if (resolved === 'month' || resolved === 'year') return new Date(addMonths(time, Math.trunc(n) * (resolved === 'year' ? 12 : 1))).toISOString();
|
|
617
|
+
return new Date(time + n * FIXED_UNIT_MS[resolved]).toISOString();
|
|
618
|
+
}
|
|
619
|
+
function datePart(value, part) {
|
|
620
|
+
if (typeof part !== 'string' || !expression_interface/* .EXPRESSION_DATE_PARTS.includes */.Vq.includes(part)) {
|
|
621
|
+
throw new ExpressionError('invalid_date_part', {
|
|
622
|
+
part: String(part),
|
|
623
|
+
parts: expression_interface/* .EXPRESSION_DATE_PARTS.join */.Vq.join(', ')
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
const time = toTime(value);
|
|
627
|
+
if (time === null) return null;
|
|
628
|
+
const date = new Date(time);
|
|
629
|
+
const parts = {
|
|
630
|
+
year: date.getUTCFullYear(),
|
|
631
|
+
month: date.getUTCMonth() + 1,
|
|
632
|
+
day: date.getUTCDate(),
|
|
633
|
+
weekday: date.getUTCDay(),
|
|
634
|
+
hour: date.getUTCHours(),
|
|
635
|
+
minute: date.getUTCMinutes()
|
|
636
|
+
};
|
|
637
|
+
return parts[part];
|
|
638
|
+
}
|
|
639
|
+
// ── Built-in functions ──────────────────────────────────────────────────────────────────
|
|
640
|
+
const EXPRESSION_FUNCTIONS = {
|
|
641
|
+
LOWER: {
|
|
642
|
+
minArgs: 1,
|
|
643
|
+
maxArgs: 1,
|
|
644
|
+
call: textFn((t)=>t.toLowerCase())
|
|
645
|
+
},
|
|
646
|
+
UPPER: {
|
|
647
|
+
minArgs: 1,
|
|
648
|
+
maxArgs: 1,
|
|
649
|
+
call: textFn((t)=>t.toUpperCase())
|
|
650
|
+
},
|
|
651
|
+
TRIM: {
|
|
652
|
+
minArgs: 1,
|
|
653
|
+
maxArgs: 1,
|
|
654
|
+
call: textFn((t)=>t.trim())
|
|
655
|
+
},
|
|
656
|
+
CAPITALIZE: {
|
|
657
|
+
minArgs: 1,
|
|
658
|
+
maxArgs: 1,
|
|
659
|
+
call: textFn((t)=>t.toLowerCase().replace(/(^|\s)(\S)/g, (_, space, c)=>space + c.toUpperCase()))
|
|
660
|
+
},
|
|
661
|
+
LENGTH: {
|
|
662
|
+
minArgs: 1,
|
|
663
|
+
maxArgs: 1,
|
|
664
|
+
call: ([value])=>Array.isArray(value) ? value.length : expressionToText(value)?.length ?? 0
|
|
665
|
+
},
|
|
666
|
+
SUBSTRING: {
|
|
667
|
+
minArgs: 2,
|
|
668
|
+
maxArgs: 3,
|
|
669
|
+
call: ([value, start, length])=>{
|
|
670
|
+
const text = expressionToText(value);
|
|
671
|
+
if (text === null) return null;
|
|
672
|
+
const from = Math.max(toInteger(start, 1), 1) - 1;
|
|
673
|
+
return isBlank(length) ? text.slice(from) : text.slice(from, from + Math.max(toInteger(length, 0), 0));
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
LEFT: {
|
|
677
|
+
minArgs: 2,
|
|
678
|
+
maxArgs: 2,
|
|
679
|
+
call: ([value, count])=>{
|
|
680
|
+
const text = expressionToText(value);
|
|
681
|
+
return text === null ? null : text.slice(0, Math.max(toInteger(count, 0), 0));
|
|
682
|
+
}
|
|
683
|
+
},
|
|
684
|
+
RIGHT: {
|
|
685
|
+
minArgs: 2,
|
|
686
|
+
maxArgs: 2,
|
|
687
|
+
call: ([value, count])=>{
|
|
688
|
+
const text = expressionToText(value);
|
|
689
|
+
const n = Math.max(toInteger(count, 0), 0);
|
|
690
|
+
return text === null ? null : n === 0 ? '' : text.slice(-n);
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
REPLACE: {
|
|
694
|
+
minArgs: 3,
|
|
695
|
+
maxArgs: 3,
|
|
696
|
+
call: ([value, search, replacement])=>{
|
|
697
|
+
const text = expressionToText(value);
|
|
698
|
+
if (text === null) return null;
|
|
699
|
+
const needle = expressionToText(search) ?? '';
|
|
700
|
+
const insert = expressionToText(replacement) ?? '';
|
|
701
|
+
return needle ? limitText(text.replaceAll(needle, ()=>insert)) : text;
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
SPLIT_PART: {
|
|
705
|
+
minArgs: 3,
|
|
706
|
+
maxArgs: 3,
|
|
707
|
+
call: ([value, separator, position])=>{
|
|
708
|
+
const text = expressionToText(value);
|
|
709
|
+
const sep = expressionToText(separator);
|
|
710
|
+
if (text === null || !sep) return null;
|
|
711
|
+
const parts = text.split(sep);
|
|
712
|
+
const n = toInteger(position, 1);
|
|
713
|
+
const part = n === 0 ? undefined : parts[n > 0 ? n - 1 : parts.length + n];
|
|
714
|
+
return part === undefined ? null : part.trim();
|
|
715
|
+
}
|
|
716
|
+
},
|
|
717
|
+
PAD_START: {
|
|
718
|
+
minArgs: 2,
|
|
719
|
+
maxArgs: 3,
|
|
720
|
+
call: ([value, length, fill])=>{
|
|
721
|
+
const text = expressionToText(value) ?? '';
|
|
722
|
+
const target = Math.min(Math.max(toInteger(length, 0), 0), EXPRESSION_LIMITS.MAX_PAD_LENGTH);
|
|
723
|
+
return text.padStart(target, expressionToText(fill) || ' ');
|
|
724
|
+
}
|
|
725
|
+
},
|
|
726
|
+
REGEX_EXTRACT: {
|
|
727
|
+
minArgs: 2,
|
|
728
|
+
maxArgs: 3,
|
|
729
|
+
regexArgs: [
|
|
730
|
+
1
|
|
731
|
+
],
|
|
732
|
+
call: ([value, pattern, group])=>{
|
|
733
|
+
const prepared = prepareRegex(value, pattern);
|
|
734
|
+
if (!prepared) return null;
|
|
735
|
+
const count = prepared.regex.groupCount();
|
|
736
|
+
const index = isBlank(group) ? count > 0 ? 1 : 0 : toInteger(group, 0);
|
|
737
|
+
if (index < 0 || index > count) return null;
|
|
738
|
+
const matcher = prepared.regex.matcher(prepared.text);
|
|
739
|
+
return matcher.find() ? matcher.group(index) ?? null : null;
|
|
740
|
+
}
|
|
741
|
+
},
|
|
742
|
+
REGEX_REPLACE: {
|
|
743
|
+
minArgs: 3,
|
|
744
|
+
maxArgs: 3,
|
|
745
|
+
regexArgs: [
|
|
746
|
+
1
|
|
747
|
+
],
|
|
748
|
+
call: ([value, pattern, replacement])=>{
|
|
749
|
+
const prepared = prepareRegex(value, pattern);
|
|
750
|
+
return prepared ? limitText(prepared.regex.matcher(prepared.text).replaceAll(expressionToText(replacement) ?? '')) : null;
|
|
751
|
+
}
|
|
752
|
+
},
|
|
753
|
+
REGEX_TEST: {
|
|
754
|
+
minArgs: 2,
|
|
755
|
+
maxArgs: 2,
|
|
756
|
+
regexArgs: [
|
|
757
|
+
1
|
|
758
|
+
],
|
|
759
|
+
call: ([value, pattern])=>regexTest(value, pattern)
|
|
760
|
+
},
|
|
761
|
+
TO_TEXT: {
|
|
762
|
+
minArgs: 1,
|
|
763
|
+
maxArgs: 1,
|
|
764
|
+
call: ([value])=>expressionToText(value)
|
|
765
|
+
},
|
|
766
|
+
JOIN: {
|
|
767
|
+
minArgs: 1,
|
|
768
|
+
maxArgs: 2,
|
|
769
|
+
call: ([value, separator])=>{
|
|
770
|
+
const items = Array.isArray(value) ? value : isBlank(value) ? [] : [
|
|
771
|
+
value
|
|
772
|
+
];
|
|
773
|
+
const sep = isBlank(separator) ? ', ' : expressionToText(separator);
|
|
774
|
+
return limitText(items.map((item)=>expressionToText(item) ?? '').filter(Boolean).join(sep));
|
|
775
|
+
}
|
|
776
|
+
},
|
|
777
|
+
ROUND: {
|
|
778
|
+
minArgs: 1,
|
|
779
|
+
maxArgs: 2,
|
|
780
|
+
call: ([value, digits])=>{
|
|
781
|
+
const n = expressionToNumber(value);
|
|
782
|
+
if (n === null) return null;
|
|
783
|
+
const factor = 10 ** Math.min(Math.max(toInteger(digits, 0), 0), 10);
|
|
784
|
+
return Math.round(n * factor) / factor;
|
|
785
|
+
}
|
|
786
|
+
},
|
|
787
|
+
FLOOR: {
|
|
788
|
+
minArgs: 1,
|
|
789
|
+
maxArgs: 1,
|
|
790
|
+
call: numberFn(Math.floor)
|
|
791
|
+
},
|
|
792
|
+
CEIL: {
|
|
793
|
+
minArgs: 1,
|
|
794
|
+
maxArgs: 1,
|
|
795
|
+
call: numberFn(Math.ceil)
|
|
796
|
+
},
|
|
797
|
+
ABS: {
|
|
798
|
+
minArgs: 1,
|
|
799
|
+
maxArgs: 1,
|
|
800
|
+
call: numberFn(Math.abs)
|
|
801
|
+
},
|
|
802
|
+
TO_NUMBER: {
|
|
803
|
+
minArgs: 1,
|
|
804
|
+
maxArgs: 1,
|
|
805
|
+
call: ([value])=>tryNumber(value)
|
|
806
|
+
},
|
|
807
|
+
NOW: {
|
|
808
|
+
minArgs: 0,
|
|
809
|
+
maxArgs: 0,
|
|
810
|
+
call: (_, ctx)=>(ctx.now?.() ?? new Date()).toISOString()
|
|
811
|
+
},
|
|
812
|
+
TODAY: {
|
|
813
|
+
minArgs: 0,
|
|
814
|
+
maxArgs: 0,
|
|
815
|
+
call: (_, ctx)=>(ctx.now?.() ?? new Date()).toISOString().slice(0, 10)
|
|
816
|
+
},
|
|
817
|
+
DATE_DIFF: {
|
|
818
|
+
minArgs: 3,
|
|
819
|
+
maxArgs: 3,
|
|
820
|
+
call: ([a, b, unit])=>dateDiff(a, b, unit)
|
|
821
|
+
},
|
|
822
|
+
ADD_DURATION: {
|
|
823
|
+
minArgs: 3,
|
|
824
|
+
maxArgs: 3,
|
|
825
|
+
call: ([value, amount, unit])=>addDuration(value, amount, unit)
|
|
826
|
+
},
|
|
827
|
+
DATE_PART: {
|
|
828
|
+
minArgs: 2,
|
|
829
|
+
maxArgs: 2,
|
|
830
|
+
call: ([value, part])=>datePart(value, part)
|
|
831
|
+
},
|
|
832
|
+
COALESCE: {
|
|
833
|
+
minArgs: 1,
|
|
834
|
+
call: (args)=>args.find((value)=>!isBlank(value)) ?? null
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
function findFunction(name) {
|
|
838
|
+
return Object.hasOwn(EXPRESSION_FUNCTIONS, name) ? EXPRESSION_FUNCTIONS[name] : undefined;
|
|
839
|
+
}
|
|
840
|
+
// ── Comparisons ─────────────────────────────────────────────────────────────────────────
|
|
841
|
+
function looseEquals(a, b) {
|
|
842
|
+
if (isBlank(a) || isBlank(b)) return isBlank(a) && isBlank(b);
|
|
843
|
+
const na = typeof a === 'boolean' || typeof b === 'boolean' ? null : tryNumber(a);
|
|
844
|
+
const nb = na === null ? null : tryNumber(b);
|
|
845
|
+
if (na !== null && nb !== null) return na === nb;
|
|
846
|
+
return expressionToText(a) === expressionToText(b);
|
|
847
|
+
}
|
|
848
|
+
function compareOrdered(a, b) {
|
|
849
|
+
if (isBlank(a) || isBlank(b)) return null;
|
|
850
|
+
const na = tryNumber(a);
|
|
851
|
+
const nb = tryNumber(b);
|
|
852
|
+
if (na !== null && nb !== null) return na - nb;
|
|
853
|
+
const ta = typeof a === 'string' ? Date.parse(a) : Number.NaN;
|
|
854
|
+
const tb = typeof b === 'string' ? Date.parse(b) : Number.NaN;
|
|
855
|
+
if (Number.isFinite(ta) && Number.isFinite(tb)) return ta - tb;
|
|
856
|
+
return (expressionToText(a) ?? '').localeCompare(expressionToText(b) ?? '');
|
|
857
|
+
}
|
|
858
|
+
function toList(value) {
|
|
859
|
+
if (Array.isArray(value)) return value;
|
|
860
|
+
if (isBlank(value)) return [];
|
|
861
|
+
if (typeof value === 'string') return value.split(',').map((item)=>item.trim());
|
|
862
|
+
return [
|
|
863
|
+
value
|
|
864
|
+
];
|
|
865
|
+
}
|
|
866
|
+
function contains(container, item) {
|
|
867
|
+
if (Array.isArray(container)) return container.some((entry)=>looseEquals(entry, item));
|
|
868
|
+
const text = expressionToText(container);
|
|
869
|
+
const needle = expressionToText(item);
|
|
870
|
+
return text !== null && needle !== null && text.includes(needle);
|
|
871
|
+
}
|
|
872
|
+
function isEmpty(value) {
|
|
873
|
+
return isBlank(value) || Array.isArray(value) && value.length === 0;
|
|
874
|
+
}
|
|
875
|
+
function isTrue(value) {
|
|
876
|
+
return value === true || value === 1 || typeof value === 'string' && [
|
|
877
|
+
'true',
|
|
878
|
+
'1',
|
|
879
|
+
'yes'
|
|
880
|
+
].includes(value.trim().toLowerCase());
|
|
881
|
+
}
|
|
882
|
+
function compare(comparison, left, right) {
|
|
883
|
+
switch(comparison){
|
|
884
|
+
case 'equals':
|
|
885
|
+
return looseEquals(left, right);
|
|
886
|
+
case 'not_equals':
|
|
887
|
+
return !looseEquals(left, right);
|
|
888
|
+
case 'greater_than':
|
|
889
|
+
case 'less_than':
|
|
890
|
+
case 'greater_or_equal':
|
|
891
|
+
case 'less_or_equal':
|
|
892
|
+
{
|
|
893
|
+
const d = compareOrdered(left, right);
|
|
894
|
+
if (d === null) return false;
|
|
895
|
+
return comparison === 'greater_than' ? d > 0 : comparison === 'less_than' ? d < 0 : comparison === 'greater_or_equal' ? d >= 0 : d <= 0;
|
|
896
|
+
}
|
|
897
|
+
case 'contains':
|
|
898
|
+
return contains(left, right);
|
|
899
|
+
case 'not_contains':
|
|
900
|
+
return !contains(left, right);
|
|
901
|
+
case 'starts_with':
|
|
902
|
+
return (expressionToText(left) ?? '').startsWith(expressionToText(right) ?? '');
|
|
903
|
+
case 'ends_with':
|
|
904
|
+
return (expressionToText(left) ?? '').endsWith(expressionToText(right) ?? '');
|
|
905
|
+
case 'in':
|
|
906
|
+
return toList(right).some((item)=>Array.isArray(left) ? contains(left, item) : looseEquals(left, item));
|
|
907
|
+
case 'not_in':
|
|
908
|
+
return !toList(right).some((item)=>Array.isArray(left) ? contains(left, item) : looseEquals(left, item));
|
|
909
|
+
case 'is_empty':
|
|
910
|
+
return isEmpty(left);
|
|
911
|
+
case 'is_not_empty':
|
|
912
|
+
return !isEmpty(left);
|
|
913
|
+
case 'is_true':
|
|
914
|
+
return isTrue(left);
|
|
915
|
+
case 'is_false':
|
|
916
|
+
return !isTrue(left);
|
|
917
|
+
case 'matches':
|
|
918
|
+
return regexTest(left, right);
|
|
919
|
+
case 'not_matches':
|
|
920
|
+
return !regexTest(left, right);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
function enter(depth, budget) {
|
|
924
|
+
if (depth > EXPRESSION_LIMITS.MAX_DEPTH) throw new ExpressionError('too_deep', {
|
|
925
|
+
max: EXPRESSION_LIMITS.MAX_DEPTH
|
|
926
|
+
});
|
|
927
|
+
if (++budget.nodes > EXPRESSION_LIMITS.MAX_NODES) throw new ExpressionError('too_many_nodes', {
|
|
928
|
+
max: EXPRESSION_LIMITS.MAX_NODES
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
function applyArithmetic(op, values) {
|
|
932
|
+
if (values.length === 0) return null;
|
|
933
|
+
const [first, ...rest] = values;
|
|
934
|
+
switch(op){
|
|
935
|
+
case 'sum':
|
|
936
|
+
return values.reduce((a, b)=>a + b, 0);
|
|
937
|
+
case 'subtract':
|
|
938
|
+
return rest.reduce((a, b)=>a - b, first);
|
|
939
|
+
case 'multiply':
|
|
940
|
+
return values.reduce((a, b)=>a * b, 1);
|
|
941
|
+
case 'divide':
|
|
942
|
+
case 'modulo':
|
|
943
|
+
return rest.reduce((a, b)=>{
|
|
944
|
+
if (b === 0) throw new ExpressionError('division_by_zero');
|
|
945
|
+
return op === 'divide' ? a / b : a % b;
|
|
946
|
+
}, first);
|
|
947
|
+
case 'power':
|
|
948
|
+
return rest.reduce((a, b)=>a ** b, first);
|
|
949
|
+
case 'average':
|
|
950
|
+
return values.reduce((a, b)=>a + b, 0) / values.length;
|
|
951
|
+
case 'min':
|
|
952
|
+
return Math.min(...values);
|
|
953
|
+
case 'max':
|
|
954
|
+
return Math.max(...values);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
function renderTemplate(template, ctx) {
|
|
958
|
+
if (template.length > EXPRESSION_LIMITS.MAX_TEMPLATE_LENGTH) throw new ExpressionError('template_too_long', {
|
|
959
|
+
max: EXPRESSION_LIMITS.MAX_TEMPLATE_LENGTH
|
|
960
|
+
});
|
|
961
|
+
return limitText(template.replace(TEMPLATE_PLACEHOLDER, (_, ref)=>expressionToText(ctx.resolveRef(ref)) ?? ''));
|
|
962
|
+
}
|
|
963
|
+
function evaluate(expression, ctx, depth, budget) {
|
|
964
|
+
enter(depth, budget);
|
|
965
|
+
switch(expression?.type){
|
|
966
|
+
case 'literal':
|
|
967
|
+
return expression.value;
|
|
968
|
+
case 'field_ref':
|
|
969
|
+
return ctx.resolveRef(expression.ref);
|
|
970
|
+
case 'arithmetic':
|
|
971
|
+
{
|
|
972
|
+
// Blank operands count as 0 so an unanswered optional field does not void the total.
|
|
973
|
+
const values = expression.operands.map((operand)=>expressionToNumber(evaluate(operand, ctx, depth + 1, budget)) ?? 0);
|
|
974
|
+
return applyArithmetic(expression.op, values);
|
|
975
|
+
}
|
|
976
|
+
case 'concat':
|
|
977
|
+
{
|
|
978
|
+
const parts = expression.parts.map((part)=>expressionToText(evaluate(part, ctx, depth + 1, budget)) ?? '');
|
|
979
|
+
return limitText(expression.separator ? parts.filter((part)=>part !== '').join(expression.separator) : parts.join(''));
|
|
980
|
+
}
|
|
981
|
+
case 'template':
|
|
982
|
+
return renderTemplate(expression.template, ctx);
|
|
983
|
+
case 'function':
|
|
984
|
+
{
|
|
985
|
+
const fn = findFunction(expression.name);
|
|
986
|
+
if (!fn) throw new ExpressionError('unknown_function', {
|
|
987
|
+
name: String(expression.name)
|
|
988
|
+
});
|
|
989
|
+
return fn.call(expression.args.map((arg)=>evaluate(arg, ctx, depth + 1, budget)), ctx);
|
|
990
|
+
}
|
|
991
|
+
case 'if':
|
|
992
|
+
return evaluateGroup(expression.condition, ctx, depth + 1, budget) ? evaluate(expression.whenTrue, ctx, depth + 1, budget) : expression.whenFalse ? evaluate(expression.whenFalse, ctx, depth + 1, budget) : null;
|
|
993
|
+
default:
|
|
994
|
+
throw new ExpressionError('invalid_node', {
|
|
995
|
+
type: String(expression?.type)
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
const isGroup = (node)=>'conditions' in node;
|
|
1000
|
+
function evaluateGroup(group, ctx, depth, budget) {
|
|
1001
|
+
enter(depth, budget);
|
|
1002
|
+
const test = (node)=>{
|
|
1003
|
+
if (isGroup(node)) return evaluateGroup(node, ctx, depth + 1, budget);
|
|
1004
|
+
if (!expression_interface/* .EXPRESSION_COMPARISONS.includes */.Me.includes(node.comparison)) {
|
|
1005
|
+
throw new ExpressionError('unknown_comparison', {
|
|
1006
|
+
comparison: String(node.comparison)
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
const left = evaluate(node.left, ctx, depth + 1, budget);
|
|
1010
|
+
const right = node.right ? evaluate(node.right, ctx, depth + 1, budget) : undefined;
|
|
1011
|
+
return compare(node.comparison, left, right);
|
|
1012
|
+
};
|
|
1013
|
+
return group.operator === 'OR' ? group.conditions.some(test) : group.conditions.every(test);
|
|
1014
|
+
}
|
|
1015
|
+
function evaluateExpression(expression, ctx) {
|
|
1016
|
+
return evaluate(expression, ctx, 0, {
|
|
1017
|
+
nodes: 0
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
/** An empty group is true. */ function evaluateConditionGroup(group, ctx) {
|
|
1021
|
+
return evaluateGroup(group, ctx, 0, {
|
|
1022
|
+
nodes: 0
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
// ── Save-time validation ────────────────────────────────────────────────────────────────
|
|
1026
|
+
const isObject = (value)=>typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
1027
|
+
function invalid(type) {
|
|
1028
|
+
return new ExpressionError('invalid_node', {
|
|
1029
|
+
type: String(type)
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
function checkRef(ref, options) {
|
|
1033
|
+
if (typeof ref !== 'string' || !ref) throw new ExpressionError('unknown_ref', {
|
|
1034
|
+
ref: String(ref ?? '')
|
|
1035
|
+
});
|
|
1036
|
+
if (options.isKnownRef && !options.isKnownRef(ref)) throw new ExpressionError('unknown_ref', {
|
|
1037
|
+
ref
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
function checkLiteralRegex(node) {
|
|
1041
|
+
if (isObject(node) && node['type'] === 'literal') {
|
|
1042
|
+
const problem = regexProblemError(expressionToText(node['value']) ?? '');
|
|
1043
|
+
if (problem) throw problem;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
function validateNode(node, options, depth, budget) {
|
|
1047
|
+
enter(depth, budget);
|
|
1048
|
+
if (!isObject(node)) throw invalid(node);
|
|
1049
|
+
const each = (items, min)=>{
|
|
1050
|
+
if (!Array.isArray(items) || items.length < min) throw invalid(node['type']);
|
|
1051
|
+
for (const item of items)validateNode(item, options, depth + 1, budget);
|
|
1052
|
+
};
|
|
1053
|
+
switch(node['type']){
|
|
1054
|
+
case 'literal':
|
|
1055
|
+
{
|
|
1056
|
+
const value = node['value'];
|
|
1057
|
+
if (value !== null && ![
|
|
1058
|
+
'string',
|
|
1059
|
+
'number',
|
|
1060
|
+
'boolean'
|
|
1061
|
+
].includes(typeof value)) throw invalid('literal');
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
case 'field_ref':
|
|
1065
|
+
return checkRef(node['ref'], options);
|
|
1066
|
+
case 'arithmetic':
|
|
1067
|
+
if (!expression_interface/* .EXPRESSION_ARITHMETIC_OPS.includes */.dL.includes(node['op'])) throw invalid('arithmetic');
|
|
1068
|
+
return each(node['operands'], 1);
|
|
1069
|
+
case 'concat':
|
|
1070
|
+
if (node['separator'] !== undefined && typeof node['separator'] !== 'string') throw invalid('concat');
|
|
1071
|
+
return each(node['parts'], 1);
|
|
1072
|
+
case 'template':
|
|
1073
|
+
{
|
|
1074
|
+
const template = node['template'];
|
|
1075
|
+
if (typeof template !== 'string') throw invalid('template');
|
|
1076
|
+
if (template.length > EXPRESSION_LIMITS.MAX_TEMPLATE_LENGTH) throw new ExpressionError('template_too_long', {
|
|
1077
|
+
max: EXPRESSION_LIMITS.MAX_TEMPLATE_LENGTH
|
|
1078
|
+
});
|
|
1079
|
+
for (const match of template.matchAll(TEMPLATE_PLACEHOLDER))checkRef(match[1], options);
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
case 'function':
|
|
1083
|
+
{
|
|
1084
|
+
const name = String(node['name']);
|
|
1085
|
+
const fn = findFunction(name);
|
|
1086
|
+
if (!fn) throw new ExpressionError('unknown_function', {
|
|
1087
|
+
name
|
|
1088
|
+
});
|
|
1089
|
+
const args = node['args'];
|
|
1090
|
+
if (!Array.isArray(args)) throw invalid('function');
|
|
1091
|
+
if (args.length < fn.minArgs || fn.maxArgs !== undefined && args.length > fn.maxArgs) {
|
|
1092
|
+
throw new ExpressionError('wrong_arg_count', {
|
|
1093
|
+
name,
|
|
1094
|
+
min: fn.minArgs,
|
|
1095
|
+
max: fn.maxArgs ?? 'any'
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
for (const index of fn.regexArgs ?? [])checkLiteralRegex(args[index]);
|
|
1099
|
+
return each(args, 0);
|
|
1100
|
+
}
|
|
1101
|
+
case 'if':
|
|
1102
|
+
validateGroup(node['condition'], options, depth + 1, budget);
|
|
1103
|
+
validateNode(node['whenTrue'], options, depth + 1, budget);
|
|
1104
|
+
if (node['whenFalse'] !== undefined) validateNode(node['whenFalse'], options, depth + 1, budget);
|
|
1105
|
+
return;
|
|
1106
|
+
default:
|
|
1107
|
+
throw invalid(node['type']);
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
function validateGroup(group, options, depth, budget) {
|
|
1111
|
+
enter(depth, budget);
|
|
1112
|
+
if (!isObject(group) || group['operator'] !== 'AND' && group['operator'] !== 'OR' || !Array.isArray(group['conditions'])) throw invalid('condition');
|
|
1113
|
+
for (const node of group['conditions']){
|
|
1114
|
+
if (isObject(node) && 'conditions' in node) {
|
|
1115
|
+
validateGroup(node, options, depth + 1, budget);
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
if (!isObject(node)) throw invalid('condition');
|
|
1119
|
+
const comparison = node['comparison'];
|
|
1120
|
+
if (!expression_interface/* .EXPRESSION_COMPARISONS.includes */.Me.includes(comparison)) {
|
|
1121
|
+
throw new ExpressionError('unknown_comparison', {
|
|
1122
|
+
comparison: String(comparison)
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
validateNode(node['left'], options, depth + 1, budget);
|
|
1126
|
+
if (!expression_interface/* .UNARY_EXPRESSION_COMPARISONS.includes */.kA.includes(comparison)) {
|
|
1127
|
+
validateNode(node['right'], options, depth + 1, budget);
|
|
1128
|
+
if (comparison === 'matches' || comparison === 'not_matches') checkLiteralRegex(node['right']);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
/** Throws an ExpressionError for the first problem: shape, unknown function / reference, arity, limits, bad literal regex. */ function assertValidExpression(expression, options = {}) {
|
|
1133
|
+
validateNode(expression, options, 0, {
|
|
1134
|
+
nodes: 0
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
function assertValidConditionGroup(group, options = {}) {
|
|
1138
|
+
validateGroup(group, options, 0, {
|
|
1139
|
+
nodes: 0
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
;// CONCATENATED MODULE: ./projects/nestjs-shared/src/utils/html-sanitizer.util.ts
|
|
1144
|
+
/**
|
|
1145
|
+
* HTML Sanitizer Utilities
|
|
1146
|
+
*
|
|
1147
|
+
* Provides functions for escaping HTML content to prevent XSS attacks.
|
|
1148
|
+
* Use these utilities when interpolating user-provided variables into HTML content.
|
|
1149
|
+
*/ /**
|
|
1150
|
+
* HTML entity mapping for escaping special characters
|
|
1151
|
+
*/ const HTML_ESCAPE_MAP = {
|
|
1152
|
+
'&': '&',
|
|
1153
|
+
'<': '<',
|
|
1154
|
+
'>': '>',
|
|
1155
|
+
'"': '"',
|
|
1156
|
+
"'": ''',
|
|
1157
|
+
'/': '/',
|
|
1158
|
+
'`': '`',
|
|
1159
|
+
'=': '='
|
|
1160
|
+
};
|
|
1161
|
+
/**
|
|
1162
|
+
* Regex pattern matching characters that need HTML escaping
|
|
1163
|
+
*/ const HTML_ESCAPE_REGEX = /[&<>"'`=/]/g;
|
|
1164
|
+
/**
|
|
1165
|
+
* Escapes HTML special characters to prevent XSS attacks.
|
|
1166
|
+
*
|
|
1167
|
+
* @param str - The string to escape
|
|
1168
|
+
* @returns The escaped string safe for HTML insertion
|
|
1169
|
+
*
|
|
1170
|
+
* @example
|
|
1171
|
+
* ```typescript
|
|
1172
|
+
* escapeHtml('<script>alert("xss")</script>')
|
|
1173
|
+
* // Returns: '<script>alert("xss")</script>'
|
|
1174
|
+
* ```
|
|
1175
|
+
*/ function escapeHtml(str) {
|
|
1176
|
+
if (!str || typeof str !== 'string') {
|
|
1177
|
+
return str ?? '';
|
|
1178
|
+
}
|
|
1179
|
+
return str.replace(HTML_ESCAPE_REGEX, (char)=>HTML_ESCAPE_MAP[char]);
|
|
1180
|
+
}
|
|
1181
|
+
/**
|
|
1182
|
+
* Escapes all string values in a variables object for safe HTML interpolation.
|
|
1183
|
+
* Non-string values are converted to strings and escaped.
|
|
1184
|
+
*
|
|
1185
|
+
* @param variables - Object containing template variables
|
|
1186
|
+
* @returns New object with all string values HTML-escaped
|
|
1187
|
+
*
|
|
1188
|
+
* @example
|
|
1189
|
+
* ```typescript
|
|
1190
|
+
* escapeHtmlVariables({ name: '<script>xss</script>', count: 5 })
|
|
1191
|
+
* // Returns: { name: '<script>xss</script>', count: '5' }
|
|
1192
|
+
* ```
|
|
1193
|
+
*/ function escapeHtmlVariables(variables) {
|
|
1194
|
+
if (!variables || typeof variables !== 'object') {
|
|
1195
|
+
return {};
|
|
1196
|
+
}
|
|
1197
|
+
const escaped = {};
|
|
1198
|
+
for (const [key, value] of Object.entries(variables)){
|
|
1199
|
+
if (value === null || value === undefined) {
|
|
1200
|
+
escaped[key] = '';
|
|
1201
|
+
} else if (typeof value === 'string') {
|
|
1202
|
+
escaped[key] = escapeHtml(value);
|
|
1203
|
+
} else if (typeof value === 'object') {
|
|
1204
|
+
// For objects/arrays, stringify and escape
|
|
1205
|
+
escaped[key] = escapeHtml(JSON.stringify(value));
|
|
1206
|
+
} else {
|
|
1207
|
+
// For numbers, booleans, etc., convert to string (no escaping needed)
|
|
1208
|
+
escaped[key] = String(value);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return escaped;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
;// CONCATENATED MODULE: ./projects/nestjs-shared/src/utils/message-ref.util.ts
|
|
1215
|
+
function messageRef(messageKey, messageVariables) {
|
|
1216
|
+
return messageVariables ? {
|
|
1217
|
+
messageKey,
|
|
1218
|
+
messageVariables
|
|
1219
|
+
} : {
|
|
1220
|
+
messageKey
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// EXTERNAL MODULE: ./projects/nestjs-shared/src/utils/optional-module.util.ts
|
|
1225
|
+
var optional_module_util = __webpack_require__(368);
|
|
1226
|
+
// EXTERNAL MODULE: ./projects/nestjs-shared/src/utils/permission-cache-key.util.ts
|
|
1227
|
+
var permission_cache_key_util = __webpack_require__(199);
|
|
1228
|
+
// EXTERNAL MODULE: ./projects/nestjs-shared/src/utils/permission-match.util.ts
|
|
1229
|
+
var permission_match_util = __webpack_require__(788);
|
|
1230
|
+
// EXTERNAL MODULE: external "@nestjs/common"
|
|
1231
|
+
var common_ = __webpack_require__(157);
|
|
1232
|
+
// EXTERNAL MODULE: ./projects/nestjs-shared/src/constants/index.ts
|
|
1233
|
+
var constants = __webpack_require__(43);
|
|
1234
|
+
;// CONCATENATED MODULE: ./projects/nestjs-shared/src/utils/query-helpers.util.ts
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
|
|
1238
|
+
/**
|
|
1239
|
+
* Apply company filter to a query builder if company feature is enabled.
|
|
1240
|
+
* Centralizes the common pattern of filtering by user's company.
|
|
1241
|
+
*
|
|
1242
|
+
* @param query - TypeORM SelectQueryBuilder
|
|
1243
|
+
* @param config - Company filter configuration
|
|
1244
|
+
* @param user - Current logged user (may be null)
|
|
1245
|
+
* @returns The modified query builder
|
|
1246
|
+
*
|
|
1247
|
+
* @example
|
|
1248
|
+
* ```typescript
|
|
1249
|
+
* applyCompanyFilter(query, {
|
|
1250
|
+
* isCompanyFeatureEnabled: this.config.isCompanyFeatureEnabled(),
|
|
1251
|
+
* entityAlias: 'file_manager',
|
|
1252
|
+
* }, user);
|
|
1253
|
+
* ```
|
|
1254
|
+
*/ function applyCompanyFilter(query, config, user) {
|
|
1255
|
+
const columnName = config.columnName ?? 'companyId';
|
|
1256
|
+
if (config.isCompanyFeatureEnabled && user?.companyId) {
|
|
1257
|
+
query.andWhere(`${config.entityAlias}.${columnName} = :companyId`, {
|
|
1258
|
+
companyId: user.companyId
|
|
1259
|
+
});
|
|
1260
|
+
}
|
|
1261
|
+
return query;
|
|
1262
|
+
}
|
|
1263
|
+
/**
|
|
1264
|
+
* Restrict a query to rows of the given branches - typically a branch plus the granted branches under
|
|
1265
|
+
* it from `BRANCH_HIERARCHY_RESOLVER.getGrantedDescendantIds(userId, id, { includeSelf: true })`. Empty
|
|
1266
|
+
* ids are dropped; with none left only unassigned rows (if asked for) match, never everything.
|
|
1267
|
+
*/ function applyBranchFilter(query, config, branchIds) {
|
|
1268
|
+
const ids = normalizeBranchIds(branchIds);
|
|
1269
|
+
const column = `${config.entityAlias}.${config.columnName ?? 'branchId'}`;
|
|
1270
|
+
if (!ids.length) return query.andWhere(config.includeUnassigned ? `${column} IS NULL` : '1 = 0');
|
|
1271
|
+
const inList = `${column} IN (:...branchFilterIds)`;
|
|
1272
|
+
return query.andWhere(config.includeUnassigned ? `(${inList} OR ${column} IS NULL)` : inList, {
|
|
1273
|
+
branchFilterIds: ids
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Build a where condition object with optional company filter.
|
|
1278
|
+
*
|
|
1279
|
+
* @param baseWhere - Base where condition object
|
|
1280
|
+
* @param isCompanyFeatureEnabled - Whether company feature is enabled
|
|
1281
|
+
* @param user - Current logged user
|
|
1282
|
+
* @returns Where condition with companyId added if applicable
|
|
1283
|
+
*
|
|
1284
|
+
* @example
|
|
1285
|
+
* ```typescript
|
|
1286
|
+
* const where = buildCompanyWhereCondition(
|
|
1287
|
+
* { id: dto.id },
|
|
1288
|
+
* this.config.isCompanyFeatureEnabled(),
|
|
1289
|
+
* user
|
|
1290
|
+
* );
|
|
1291
|
+
* // Returns { id: dto.id, companyId: user.companyId } if company feature enabled
|
|
1292
|
+
* ```
|
|
1293
|
+
*/ function buildCompanyWhereCondition(baseWhere, isCompanyFeatureEnabled, user) {
|
|
1294
|
+
if (isCompanyFeatureEnabled && user?.companyId) {
|
|
1295
|
+
return {
|
|
1296
|
+
...baseWhere,
|
|
1297
|
+
companyId: user.companyId
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
return baseWhere;
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Type guard to check if entity has companyId property
|
|
1304
|
+
*/ function hasCompanyId(entity) {
|
|
1305
|
+
return entity !== null && typeof entity === 'object' && 'companyId' in entity;
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Validates that an entity with companyId belongs to the user's company.
|
|
1309
|
+
* Throws BadRequestException if the entity belongs to another company.
|
|
1310
|
+
*
|
|
1311
|
+
* @param entity - Entity to validate
|
|
1312
|
+
* @param user - Current logged user
|
|
1313
|
+
* @param isCompanyFeatureEnabled - Whether company feature is enabled
|
|
1314
|
+
* @param entityName - Name of entity for error message (e.g., 'Email configuration')
|
|
1315
|
+
* @throws BadRequestException if entity belongs to another company
|
|
1316
|
+
*
|
|
1317
|
+
* @example
|
|
1318
|
+
* ```typescript
|
|
1319
|
+
* validateCompanyOwnership(config, user, this.config.isCompanyFeatureEnabled(), 'Email configuration');
|
|
1320
|
+
* ```
|
|
1321
|
+
*/ function validateCompanyOwnership(entity, user, isCompanyFeatureEnabled, entityName) {
|
|
1322
|
+
if (isCompanyFeatureEnabled && user?.companyId && hasCompanyId(entity)) {
|
|
1323
|
+
if (entity.companyId && entity.companyId !== user.companyId) {
|
|
1324
|
+
throw new common_.BadRequestException({
|
|
1325
|
+
message: `${entityName} belongs to another company`,
|
|
1326
|
+
messageKey: constants/* .AUTH_MESSAGES.ENTITY_BELOG_ANOTHER_COMPANY */.mK.ENTITY_BELOG_ANOTHER_COMPANY,
|
|
1327
|
+
messageVariables: {
|
|
1328
|
+
entity: entityName
|
|
1329
|
+
}
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Runs `fn` inside a savepoint of an already-open transaction. A failure rolls back only what
|
|
1336
|
+
* `fn` did, so the caller can swallow the error and keep using the transaction - without it,
|
|
1337
|
+
* PostgreSQL aborts the whole transaction after any failed statement.
|
|
1338
|
+
*/ async function withSavepoint(queryRunner, fn) {
|
|
1339
|
+
await queryRunner.startTransaction();
|
|
1340
|
+
try {
|
|
1341
|
+
const result = await fn();
|
|
1342
|
+
await queryRunner.commitTransaction();
|
|
1343
|
+
return result;
|
|
1344
|
+
} catch (error) {
|
|
1345
|
+
await queryRunner.rollbackTransaction();
|
|
1346
|
+
throw error;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// EXTERNAL MODULE: external "@flusys/nestjs-core/config"
|
|
1351
|
+
var config_ = __webpack_require__(307);
|
|
1352
|
+
;// CONCATENATED MODULE: ./projects/nestjs-shared/src/utils/request.util.ts
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
|
|
1356
|
+
/** Time unit multipliers in milliseconds */ const TIME_UNIT_MS = {
|
|
1357
|
+
s: 1000,
|
|
1358
|
+
m: 60 * 1000,
|
|
1359
|
+
h: 60 * 60 * 1000,
|
|
1360
|
+
d: 24 * 60 * 60 * 1000,
|
|
1361
|
+
w: 7 * 24 * 60 * 60 * 1000
|
|
1362
|
+
};
|
|
1363
|
+
/** Get normalized client type from request header */ function getClientType(req) {
|
|
1364
|
+
const clientType = req.headers[constants/* .CLIENT_TYPE_HEADER */.xw];
|
|
1365
|
+
return clientType ? clientType.toLowerCase() : null;
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Detect if request is from a browser client.
|
|
1369
|
+
* Uses x-client-type header first, then falls back to user-agent detection.
|
|
1370
|
+
*/ function isBrowserRequest(req) {
|
|
1371
|
+
const clientType = getClientType(req);
|
|
1372
|
+
if (clientType) {
|
|
1373
|
+
return clientType === 'browser' || clientType === 'web';
|
|
1374
|
+
}
|
|
1375
|
+
const accept = req.headers['accept'] || '';
|
|
1376
|
+
if (accept.includes('text/html')) return true;
|
|
1377
|
+
const userAgent = req.headers['user-agent'] || '';
|
|
1378
|
+
const browserPatterns = /mozilla|chrome|safari|firefox|edge|opera|msie/i;
|
|
1379
|
+
return browserPatterns.test(userAgent) && !userAgent.includes('Postman');
|
|
1380
|
+
}
|
|
1381
|
+
/**
|
|
1382
|
+
* Build secure cookie options based on request context.
|
|
1383
|
+
* Handles HTTPS detection for TLS-terminating proxies.
|
|
1384
|
+
*/ function buildCookieOptions(req) {
|
|
1385
|
+
const hostname = req.hostname || '';
|
|
1386
|
+
const origin = req.headers.origin || '';
|
|
1387
|
+
const isProduction = config_.envConfig.isProduction();
|
|
1388
|
+
const forwardedProto = req.headers['x-forwarded-proto'];
|
|
1389
|
+
const isHttps = isProduction || forwardedProto === 'https' || origin.startsWith('https://') || req.secure;
|
|
1390
|
+
let domain;
|
|
1391
|
+
const domainParts = hostname.split('.');
|
|
1392
|
+
if (domainParts.length > 2 && !hostname.includes('localhost')) {
|
|
1393
|
+
domain = '.' + domainParts.slice(-2).join('.');
|
|
1394
|
+
}
|
|
1395
|
+
return {
|
|
1396
|
+
secure: isHttps,
|
|
1397
|
+
sameSite: isHttps ? 'strict' : 'lax',
|
|
1398
|
+
...domain && {
|
|
1399
|
+
domain
|
|
1400
|
+
}
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
/**
|
|
1404
|
+
* Parse duration string to milliseconds.
|
|
1405
|
+
* @example '7d' → 604800000, '24h' → 86400000, '30m' → 1800000
|
|
1406
|
+
*/ function parseDurationToMs(duration, defaultMs = TIME_UNIT_MS.w) {
|
|
1407
|
+
const match = duration.match(/^(\d+)(s|m|h|d|w)$/);
|
|
1408
|
+
if (!match) return defaultMs;
|
|
1409
|
+
const value = parseInt(match[1], 10);
|
|
1410
|
+
const unit = match[2];
|
|
1411
|
+
return value * (TIME_UNIT_MS[unit] ?? TIME_UNIT_MS.d);
|
|
1412
|
+
}
|
|
1413
|
+
/**
|
|
1414
|
+
* Determine the frontend base URL for auth-related links.
|
|
1415
|
+
* Matches the request's Origin/Referer against configured allowed origins.
|
|
1416
|
+
* Never trusts the request's Host header - it is client-controlled and would
|
|
1417
|
+
* let an attacker redirect password-reset/verification links to their own domain.
|
|
1418
|
+
*/ function extractFrontendUrl(req) {
|
|
1419
|
+
const allowedOrigins = config_.envConfig.getOrigins();
|
|
1420
|
+
const origin = (req.get('origin') ?? req.get('referer') ?? '').trim();
|
|
1421
|
+
if (origin && allowedOrigins.length) {
|
|
1422
|
+
const matched = allowedOrigins.find((u)=>origin.startsWith(u));
|
|
1423
|
+
if (matched) {
|
|
1424
|
+
return matched;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
if (allowedOrigins.length) {
|
|
1428
|
+
return allowedOrigins[0];
|
|
1429
|
+
}
|
|
1430
|
+
throw new common_.BadRequestException({
|
|
1431
|
+
message: 'Unable to determine frontend URL from request',
|
|
1432
|
+
messageKey: constants/* .ERROR_MESSAGES.VALIDATION */.UU.VALIDATION
|
|
1433
|
+
});
|
|
1434
|
+
}
|
|
1435
|
+
function buildFrontendUrl(req, path) {
|
|
1436
|
+
const baseUrl = extractFrontendUrl(req);
|
|
1437
|
+
return `${baseUrl}${path}`;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
;// CONCATENATED MODULE: ./projects/nestjs-shared/src/utils/string.util.ts
|
|
1441
|
+
/**
|
|
1442
|
+
* String utility functions
|
|
1443
|
+
*/ /**
|
|
1444
|
+
* Generate URL-friendly slug from a string
|
|
1445
|
+
* @param text - The text to convert to slug
|
|
1446
|
+
* @param maxLength - Maximum length of the slug (default: 100)
|
|
1447
|
+
* @returns URL-friendly slug
|
|
1448
|
+
*
|
|
1449
|
+
* @example
|
|
1450
|
+
* generateSlug('My Company Name') // 'my-company-name'
|
|
1451
|
+
* generateSlug('Hello World!') // 'hello-world'
|
|
1452
|
+
*/ function generateSlug(text, maxLength = 100) {
|
|
1453
|
+
return text.toLowerCase().trim().replace(/[^a-z0-9\s-]/g, '') // Remove special characters
|
|
1454
|
+
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
|
1455
|
+
.replace(/-+/g, '-') // Replace multiple hyphens with single
|
|
1456
|
+
.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
|
|
1457
|
+
.substring(0, maxLength);
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Generate unique slug using single database query (optimized for large datasets)
|
|
1461
|
+
* Appends numeric suffix if collision exists (e.g., 'my-company-1', 'my-company-2')
|
|
1462
|
+
*
|
|
1463
|
+
* Performance: O(1) database query regardless of collision count
|
|
1464
|
+
* - Works efficiently with millions of records
|
|
1465
|
+
* - Requires index on slug column for best performance
|
|
1466
|
+
*
|
|
1467
|
+
* @param text - The text to convert to slug
|
|
1468
|
+
* @param findMatchingSlugs - Async function that returns all slugs matching base pattern
|
|
1469
|
+
* Should query: WHERE slug = baseSlug OR slug LIKE 'baseSlug-%'
|
|
1470
|
+
* @param maxLength - Maximum length of the base slug (default: 100)
|
|
1471
|
+
* @returns Unique URL-friendly slug
|
|
1472
|
+
*
|
|
1473
|
+
* @example
|
|
1474
|
+
* const slug = await generateUniqueSlug('My Company', async (baseSlug) => {
|
|
1475
|
+
* return repo
|
|
1476
|
+
* .createQueryBuilder('c')
|
|
1477
|
+
* .select('c.slug')
|
|
1478
|
+
* .where('c.slug = :base OR c.slug LIKE :pattern', {
|
|
1479
|
+
* base: baseSlug,
|
|
1480
|
+
* pattern: `${baseSlug}-%`,
|
|
1481
|
+
* })
|
|
1482
|
+
* .getMany()
|
|
1483
|
+
* .then(rows => rows.map(r => r.slug));
|
|
1484
|
+
* });
|
|
1485
|
+
*/ async function generateUniqueSlug(text, findMatchingSlugs, maxLength = 100) {
|
|
1486
|
+
const baseSlug = generateSlug(text, maxLength);
|
|
1487
|
+
// Single query to get all matching slugs
|
|
1488
|
+
const existingSlugs = await findMatchingSlugs(baseSlug);
|
|
1489
|
+
// No collisions - base slug is available
|
|
1490
|
+
if (existingSlugs.length === 0) {
|
|
1491
|
+
return baseSlug;
|
|
1492
|
+
}
|
|
1493
|
+
// Build set for O(1) lookup
|
|
1494
|
+
const existingSet = new Set(existingSlugs);
|
|
1495
|
+
// Base slug not taken (only suffixed versions exist)
|
|
1496
|
+
if (!existingSet.has(baseSlug)) {
|
|
1497
|
+
return baseSlug;
|
|
1498
|
+
}
|
|
1499
|
+
// Find next available suffix in memory (fast)
|
|
1500
|
+
let counter = 1;
|
|
1501
|
+
while(counter < 10000){
|
|
1502
|
+
const slugWithSuffix = `${baseSlug}-${counter}`;
|
|
1503
|
+
if (!existingSet.has(slugWithSuffix)) {
|
|
1504
|
+
return slugWithSuffix;
|
|
1505
|
+
}
|
|
1506
|
+
counter++;
|
|
1507
|
+
}
|
|
1508
|
+
// Fallback: append random string (extremely rare edge case)
|
|
1509
|
+
const randomSuffix = Math.random().toString(36).substring(2, 8);
|
|
1510
|
+
return `${baseSlug}-${randomSuffix}`;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// EXTERNAL MODULE: external "crypto"
|
|
1514
|
+
var external_crypto_ = __webpack_require__(823);
|
|
1515
|
+
;// CONCATENATED MODULE: ./projects/nestjs-shared/src/utils/token.util.ts
|
|
1516
|
+
|
|
1517
|
+
/** 32 random bytes as hex, suitable for a password-reset/OTP/access-style token */ function generateSecureToken() {
|
|
1518
|
+
return external_crypto_.randomBytes(32).toString('hex');
|
|
1519
|
+
}
|
|
1520
|
+
/** SHA-256 hash of a token, for storing/comparing tokens without keeping the raw value */ function hashToken(value) {
|
|
1521
|
+
return external_crypto_.createHash('sha256').update(value).digest('hex');
|
|
1522
|
+
}
|
|
1523
|
+
function isExpired(expiresAt) {
|
|
1524
|
+
return expiresAt < new Date();
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
;// CONCATENATED MODULE: ./projects/nestjs-shared/src/utils/index.ts
|
|
1528
|
+
|
|
1529
|
+
|
|
1530
|
+
|
|
1531
|
+
|
|
1532
|
+
|
|
1533
|
+
|
|
1534
|
+
|
|
1535
|
+
|
|
1536
|
+
|
|
1537
|
+
|
|
1538
|
+
|
|
1539
|
+
|
|
1540
|
+
|
|
1541
|
+
|
|
1542
|
+
|
|
1543
|
+
|
|
1544
|
+
|
|
1545
|
+
},
|
|
1546
|
+
368(__unused_rspack_module, __webpack_exports__, __webpack_require__) {
|
|
1547
|
+
/**
|
|
1548
|
+
* Loads an optional peer dependency at runtime.
|
|
1549
|
+
*
|
|
1550
|
+
* The specifier has to stay invisible to the bundler. A plain `import(name)`
|
|
1551
|
+
* looks dynamic in source but webpack rewrites it into a context module rooted
|
|
1552
|
+
* at this directory - one that can only ever resolve files sitting next to this
|
|
1553
|
+
* one - so `amqplib` and `kafkajs` fail to load in the bundled app no matter how
|
|
1554
|
+
* correctly they are installed. Both lookups below are assembled at runtime,
|
|
1555
|
+
* where no bundler can rewrite them.
|
|
1556
|
+
*
|
|
1557
|
+
* `require` is used first because it is what the CommonJS builds actually run
|
|
1558
|
+
* on; native ESM has no `require`, and there `import()` is the only way in.
|
|
1559
|
+
*/ async function loadOptionalModule(name) {
|
|
1560
|
+
const runtimeRequire = resolveRuntimeRequire();
|
|
1561
|
+
if (runtimeRequire) return runtimeRequire(name);
|
|
1562
|
+
const runtimeImport = new Function('specifier', 'return import(specifier);');
|
|
1563
|
+
return runtimeImport(name);
|
|
1564
|
+
}
|
|
1565
|
+
/** Undefined under native ESM and inside a bundle that shadows nothing back */ function resolveRuntimeRequire() {
|
|
1566
|
+
try {
|
|
1567
|
+
return eval('typeof require === "function" ? require : undefined');
|
|
1568
|
+
} catch {
|
|
1569
|
+
return undefined;
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
__webpack_require__.d(__webpack_exports__, {
|
|
1574
|
+
l: () => (loadOptionalModule)
|
|
1575
|
+
});
|
|
1576
|
+
|
|
1577
|
+
|
|
1578
|
+
},
|
|
1579
|
+
|
|
1580
|
+
};
|