@lenne.tech/nest-server 11.1.11 → 11.1.13
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/dist/core/common/decorators/translatable.decorator.js +13 -1
- package/dist/core/common/decorators/translatable.decorator.js.map +1 -1
- package/dist/core/common/helpers/scim.helper.d.ts +1 -0
- package/dist/core/common/helpers/scim.helper.js +164 -0
- package/dist/core/common/helpers/scim.helper.js.map +1 -0
- package/dist/core/common/interfaces/scim-array-filter-node.interface.d.ts +6 -0
- package/dist/core/common/interfaces/scim-array-filter-node.interface.js +3 -0
- package/dist/core/common/interfaces/scim-array-filter-node.interface.js.map +1 -0
- package/dist/core/common/interfaces/scim-condition-node.interface.d.ts +7 -0
- package/dist/core/common/interfaces/scim-condition-node.interface.js +3 -0
- package/dist/core/common/interfaces/scim-condition-node.interface.js.map +1 -0
- package/dist/core/common/interfaces/scim-logical-node.interface.d.ts +7 -0
- package/dist/core/common/interfaces/scim-logical-node.interface.js +3 -0
- package/dist/core/common/interfaces/scim-logical-node.interface.js.map +1 -0
- package/dist/core/common/types/scim-comparator.type.d.ts +1 -0
- package/dist/core/common/types/scim-comparator.type.js +3 -0
- package/dist/core/common/types/scim-comparator.type.js.map +1 -0
- package/dist/core/common/types/scim-logical-operator.type.d.ts +1 -0
- package/dist/core/common/types/scim-logical-operator.type.js +3 -0
- package/dist/core/common/types/scim-logical-operator.type.js.map +1 -0
- package/dist/core/common/types/scim-node.type.d.ts +4 -0
- package/dist/core/common/types/scim-node.type.js +3 -0
- package/dist/core/common/types/scim-node.type.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/core/common/decorators/translatable.decorator.ts +18 -5
- package/src/core/common/helpers/scim.helper.ts +254 -0
- package/src/core/common/interfaces/scim-array-filter-node.interface.ts +8 -0
- package/src/core/common/interfaces/scim-condition-node.interface.ts +9 -0
- package/src/core/common/interfaces/scim-logical-node.interface.ts +9 -0
- package/src/core/common/types/scim-comparator.type.ts +2 -0
- package/src/core/common/types/scim-logical-operator.type.ts +1 -0
- package/src/core/common/types/scim-node.type.ts +6 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "11.1.
|
|
3
|
+
"version": "11.1.13",
|
|
4
4
|
"description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node",
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"bcrypt": "5.1.1",
|
|
86
86
|
"class-transformer": "0.5.1",
|
|
87
87
|
"class-validator": "0.14.2",
|
|
88
|
-
"compression": "1.8.
|
|
88
|
+
"compression": "1.8.1",
|
|
89
89
|
"cookie-parser": "1.4.7",
|
|
90
90
|
"dotenv": "16.5.0",
|
|
91
91
|
"ejs": "3.1.10",
|
|
@@ -135,7 +135,7 @@
|
|
|
135
135
|
"@typescript-eslint/eslint-plugin": "8.32.0",
|
|
136
136
|
"@typescript-eslint/parser": "8.32.0",
|
|
137
137
|
"coffeescript": "2.7.0",
|
|
138
|
-
"eslint": "9.
|
|
138
|
+
"eslint": "9.32.0",
|
|
139
139
|
"eslint-config-prettier": "10.1.5",
|
|
140
140
|
"eslint-plugin-unused-imports": "4.1.4",
|
|
141
141
|
"find-file-up": "2.0.1",
|
|
@@ -147,7 +147,7 @@
|
|
|
147
147
|
"husky": "9.1.7",
|
|
148
148
|
"jest": "29.7.0",
|
|
149
149
|
"npm-watch": "0.13.0",
|
|
150
|
-
"pm2": "6.0.
|
|
150
|
+
"pm2": "6.0.10",
|
|
151
151
|
"prettier": "3.5.3",
|
|
152
152
|
"pretty-quick": "4.1.1",
|
|
153
153
|
"supertest": "7.1.0",
|
|
@@ -30,16 +30,29 @@ export function updateLanguage<T extends Record<string, any>, K extends readonly
|
|
|
30
30
|
translatableFields: string[],
|
|
31
31
|
): T {
|
|
32
32
|
const changedFields: Partial<Pick<T, K[number]>> = {};
|
|
33
|
-
|
|
34
33
|
for (const key of translatableFields) {
|
|
35
|
-
const k = key
|
|
34
|
+
const k = key;
|
|
35
|
+
|
|
36
|
+
// For languages other than 'de', compare with existing translation value instead of main value
|
|
37
|
+
let compareValue;
|
|
38
|
+
if (language !== 'de' && oldValue._translations?.[language]?.[k] !== undefined) {
|
|
39
|
+
compareValue = oldValue._translations[language][k];
|
|
40
|
+
} else {
|
|
41
|
+
compareValue = oldValue[k];
|
|
42
|
+
}
|
|
36
43
|
|
|
37
|
-
if (input[k] !==
|
|
44
|
+
if (input[k] !== compareValue && input[k] !== undefined) {
|
|
38
45
|
changedFields[k] = input[k];
|
|
39
|
-
|
|
46
|
+
// Only reset if the current field isn't in german
|
|
47
|
+
if (language !== 'de') {
|
|
48
|
+
input[k] = oldValue[k];
|
|
49
|
+
}
|
|
50
|
+
} else if (language !== 'de' && input[k] !== undefined) {
|
|
51
|
+
// If no change detected but we have input for this field, reset to original value
|
|
52
|
+
// to prevent overwriting the default with translation values
|
|
53
|
+
input[k] = oldValue[k];
|
|
40
54
|
}
|
|
41
55
|
}
|
|
42
|
-
|
|
43
56
|
input._translations = input._translations ?? {};
|
|
44
57
|
input._translations[language] = {
|
|
45
58
|
...(input._translations[language] ?? {}),
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import type { Comparator } from '../types/scim-comparator.type';
|
|
2
|
+
import type { LogicalOperator } from '../types/scim-logical-operator.type';
|
|
3
|
+
|
|
4
|
+
import { ScimNode } from '../types/scim-node.type';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
export function scimToMongo(scim: string): any {
|
|
8
|
+
if (!scim) {
|
|
9
|
+
return {};
|
|
10
|
+
}
|
|
11
|
+
const tokens = tokenize(scim);
|
|
12
|
+
const ast = parseTokens(tokens);
|
|
13
|
+
return transformAstToMongo(ast);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Escapes Regex Chars */
|
|
17
|
+
function escapeRegex(input: string): string {
|
|
18
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Flattens consecutive logical operators of the same type to avoid nested structures */
|
|
22
|
+
function flattenLogicalOperator(node: ScimNode, targetOperator: LogicalOperator): ScimNode[] {
|
|
23
|
+
if (!('operator' in node)) {
|
|
24
|
+
return [node];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (node.operator !== targetOperator) {
|
|
28
|
+
return [node];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const leftConditions = flattenLogicalOperator(node.left, targetOperator);
|
|
32
|
+
const rightConditions = flattenLogicalOperator(node.right, targetOperator);
|
|
33
|
+
|
|
34
|
+
return [...leftConditions, ...rightConditions];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Parses tokenized SCIM filter into an Abstract Syntax Tree (AST) */
|
|
38
|
+
function parseTokens(tokens: string[]): ScimNode {
|
|
39
|
+
let pos = 0;
|
|
40
|
+
|
|
41
|
+
/** Parses a full logical expression (e.g., A and B or C) */
|
|
42
|
+
function parseExpression(): ScimNode {
|
|
43
|
+
let left = parseTerm();
|
|
44
|
+
while (tokens[pos] && /^(and|or)$/i.test(tokens[pos])) {
|
|
45
|
+
const op: LogicalOperator = tokens[pos++].toLowerCase() as LogicalOperator;
|
|
46
|
+
const right = parseTerm();
|
|
47
|
+
left = { left, operator: op, right };
|
|
48
|
+
}
|
|
49
|
+
return left;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Parses a single term: either a nested expression, array filter, or condition */
|
|
53
|
+
function parseTerm(): ScimNode {
|
|
54
|
+
if (tokens[pos] === '(') { // Start of a nested filter
|
|
55
|
+
pos++; // skip '('
|
|
56
|
+
const expr = parseExpression();
|
|
57
|
+
if (tokens[pos] !== ')') {
|
|
58
|
+
throw new Error(`Expected ')' at position ${pos}`);
|
|
59
|
+
}
|
|
60
|
+
pos++; // skip ')'
|
|
61
|
+
return expr;
|
|
62
|
+
}
|
|
63
|
+
if (tokens[pos + 1] === '[') { // Start of an array Filter
|
|
64
|
+
const path = tokens[pos++];
|
|
65
|
+
pos++; // skip '['
|
|
66
|
+
const expr = parseExpression();
|
|
67
|
+
if (tokens[pos] !== ']') {
|
|
68
|
+
throw new Error(`Expected ']' at position ${pos}`);
|
|
69
|
+
}
|
|
70
|
+
pos++; // skip ']'
|
|
71
|
+
return { expr, path, type: 'array' };
|
|
72
|
+
}
|
|
73
|
+
return parseCondition(); // If its neither a nested nor array filter its a simple "propertyKey eq Value"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Parses a basic SCIM condition (e.g., userName eq "Joe") */
|
|
77
|
+
function parseCondition(): ScimNode {
|
|
78
|
+
const attr = tokens[pos++]; // First token is the attribute
|
|
79
|
+
const op: Comparator = tokens[pos++].toLowerCase() as Comparator; // Second token is the operator
|
|
80
|
+
if (!['aco', 'co', 'eq', 'ew', 'ge', 'gt', 'le', 'lt', 'pr', 'sw'].includes(op)) {
|
|
81
|
+
throw new Error(`Unsupported comparator: ${op}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let value: any = null;
|
|
85
|
+
|
|
86
|
+
if (op !== 'pr') { // "Is Present" doesnt require a value
|
|
87
|
+
let rawValue = tokens[pos++]; // Third token is the value
|
|
88
|
+
if (!attr || !op || rawValue === undefined) {
|
|
89
|
+
throw new Error(`Invalid condition syntax at token ${pos}`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Handle quoted strings
|
|
93
|
+
if (rawValue?.startsWith('"')) {
|
|
94
|
+
rawValue = rawValue.slice(1, -1);
|
|
95
|
+
// For quoted strings, keep as string (don't parse to number/boolean)
|
|
96
|
+
value = rawValue;
|
|
97
|
+
} else {
|
|
98
|
+
// For unquoted values, parse to appropriate type (number, boolean, or string)
|
|
99
|
+
value = parseValue(rawValue);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { attributePath: attr, comparator: op, type: 'condition', value };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return parseExpression();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
/** Converts string values to appropriate types (number, boolean, or string) */
|
|
111
|
+
function parseValue(value: string): any {
|
|
112
|
+
if (value === null || value === undefined) {
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check if it's a number (integer or float)
|
|
117
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
118
|
+
const numValue = Number(value);
|
|
119
|
+
return isNaN(numValue) ? value : numValue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check if it's a boolean
|
|
123
|
+
if (value.toLowerCase() === 'true') {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
if (value.toLowerCase() === 'false') {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Return as string for everything else
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Tokenizes a SCIM filter string into meaningful parts.
|
|
136
|
+
* e.g., 'userName eq "john"' → ['userName', 'eq', '"john"']
|
|
137
|
+
*/
|
|
138
|
+
function tokenize(input: string): string[] {
|
|
139
|
+
// Space out brackets, but not inside quoted strings
|
|
140
|
+
let result = '';
|
|
141
|
+
let insideQuotes = false;
|
|
142
|
+
|
|
143
|
+
for (let i = 0; i < input.length; i++) {
|
|
144
|
+
const char = input[i];
|
|
145
|
+
|
|
146
|
+
if (char === '"' && (i === 0 || input[i - 1] !== '\\')) {
|
|
147
|
+
insideQuotes = !insideQuotes;
|
|
148
|
+
result += char;
|
|
149
|
+
} else if (!insideQuotes && /[()[\]]/.test(char)) {
|
|
150
|
+
result += ` ${char} `;
|
|
151
|
+
} else {
|
|
152
|
+
result += char;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return result
|
|
157
|
+
.replace(/\s+/g, ' ') // Normalise whitespaces
|
|
158
|
+
.trim()
|
|
159
|
+
.match(/\[|]|\(|\)|[a-zA-Z0-9_.]+|"(?:[^"\\]|\\.)*"/g) || []; // Match tokens: brackets, identifiers, quoted strings
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Converts the parsed SCIM AST to an equivalent MongoDB query object */
|
|
163
|
+
function transformAstToMongo(node: ScimNode): any {
|
|
164
|
+
if (!node) {
|
|
165
|
+
return {};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if ('operator' in node) {
|
|
169
|
+
const operator = node.operator;
|
|
170
|
+
const conditions = flattenLogicalOperator(node, operator);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
[`$${operator}`]: conditions.map(transformAstToMongo),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (node.type === 'array') {
|
|
178
|
+
return {
|
|
179
|
+
[node.path]: {
|
|
180
|
+
$elemMatch: transformAstToMongo(node.expr),
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const { attributePath, comparator, value } = node;
|
|
186
|
+
switch (comparator) {
|
|
187
|
+
case 'aco': // ARRAY-CONTAINS (Not case sensitive)
|
|
188
|
+
return { [attributePath]: value };
|
|
189
|
+
case 'co': // CONTAINS
|
|
190
|
+
return { [attributePath]: { $options: 'i', $regex: escapeRegex(value) } };
|
|
191
|
+
case 'eq': // EQUALS
|
|
192
|
+
return { [attributePath]: { $eq: value } };
|
|
193
|
+
case 'ew': // ENDSWITH
|
|
194
|
+
return { [attributePath]: { $options: 'i', $regex: `${escapeRegex(value)}$` } };
|
|
195
|
+
case 'ge': // GREATER THAN OR EQUAL
|
|
196
|
+
return { [attributePath]: { $gte: value } };
|
|
197
|
+
case 'gt': // GREATER THAN
|
|
198
|
+
return { [attributePath]: { $gt: value } };
|
|
199
|
+
case 'le': // LESS THAN OR EQUAL
|
|
200
|
+
return { [attributePath]: { $lte: value } };
|
|
201
|
+
case 'lt': // LESS THAN
|
|
202
|
+
return { [attributePath]: { $lt: value } };
|
|
203
|
+
case 'pr': // PRESENT (exists)
|
|
204
|
+
return { [attributePath]: { $exists: true } };
|
|
205
|
+
case 'sw': // STARTSWITH
|
|
206
|
+
return { [attributePath]: { $options: 'i', $regex: `^${escapeRegex(value)}` } };
|
|
207
|
+
default:
|
|
208
|
+
throw new Error(`Unsupported comparator: ${comparator}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/*
|
|
213
|
+
================ EXAMPLES ================
|
|
214
|
+
|
|
215
|
+
Simple condition:
|
|
216
|
+
SCIM: 'userName eq "Joe"'
|
|
217
|
+
→ Tokens: ['userName', 'eq', '"Joe"']
|
|
218
|
+
→ AST: { attributePath: 'userName', comparator: 'eq', value: 'Joe' }
|
|
219
|
+
→ Mongo: { userName: { $eq: 'Joe' } }
|
|
220
|
+
|
|
221
|
+
Logical combination:
|
|
222
|
+
SCIM: 'userName eq "Joe" and drinksCoffee eq true'
|
|
223
|
+
→ Tokens: ['userName', 'eq', '"Joe"', 'and', 'drinksCoffee', 'eq', 'true']
|
|
224
|
+
→ AST:
|
|
225
|
+
{
|
|
226
|
+
operator: 'and',
|
|
227
|
+
left: { attributePath: 'userName', comparator: 'eq', value: 'Joe' },
|
|
228
|
+
right: { attributePath: 'drinksCoffee', comparator: 'eq', value: 'true' }
|
|
229
|
+
}
|
|
230
|
+
→ Mongo:
|
|
231
|
+
{
|
|
232
|
+
$and: [
|
|
233
|
+
{ userName: { $eq: 'Joe' } },
|
|
234
|
+
{ drinksCoffee: { $eq: 'true' } }
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
Array filter:
|
|
239
|
+
SCIM: 'emails[type eq "work"]'
|
|
240
|
+
→ Tokens: ['emails', '[', 'type', 'eq', '"work"', ']']
|
|
241
|
+
→ AST:
|
|
242
|
+
{
|
|
243
|
+
type: 'array',
|
|
244
|
+
path: 'emails',
|
|
245
|
+
expr: { attributePath: 'type', comparator: 'eq', value: 'work' }
|
|
246
|
+
}
|
|
247
|
+
→ Mongo:
|
|
248
|
+
{
|
|
249
|
+
emails: {
|
|
250
|
+
$elemMatch: { type: { $eq: 'work' } }
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
*/
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { LogicalOperator } from '../types/scim-logical-operator.type';
|
|
2
|
+
import { ScimNode } from '../types/scim-node.type';
|
|
3
|
+
|
|
4
|
+
/** Represents a logical operator node (e.g., X and Y, A or B) */
|
|
5
|
+
export interface LogicalNode {
|
|
6
|
+
left: ScimNode;
|
|
7
|
+
operator: LogicalOperator;
|
|
8
|
+
right: ScimNode;
|
|
9
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type LogicalOperator = 'and' | 'or';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ArrayFilterNode } from '../interfaces/scim-array-filter-node.interface';
|
|
2
|
+
import { ConditionNode } from '../interfaces/scim-condition-node.interface';
|
|
3
|
+
import { LogicalNode } from '../interfaces/scim-logical-node.interface';
|
|
4
|
+
|
|
5
|
+
/** Union type representing any valid SCIM node */
|
|
6
|
+
export type ScimNode = ArrayFilterNode | ConditionNode | LogicalNode;
|
package/src/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ export * from './core/common/helpers/filter.helper';
|
|
|
36
36
|
export * from './core/common/helpers/graphql.helper';
|
|
37
37
|
export * from './core/common/helpers/input.helper';
|
|
38
38
|
export * from './core/common/helpers/model.helper';
|
|
39
|
+
export * from './core/common/helpers/scim.helper';
|
|
39
40
|
export * from './core/common/helpers/service.helper';
|
|
40
41
|
export * from './core/common/helpers/table.helper';
|
|
41
42
|
export * from './core/common/inputs/combined-filter.input';
|