@lenne.tech/nest-server 11.1.10 → 11.1.12

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.
Files changed (37) hide show
  1. package/dist/core/common/helpers/scim.helper.d.ts +1 -0
  2. package/dist/core/common/helpers/scim.helper.js +164 -0
  3. package/dist/core/common/helpers/scim.helper.js.map +1 -0
  4. package/dist/core/common/interfaces/scim-array-filter-node.interface.d.ts +6 -0
  5. package/dist/core/common/interfaces/scim-array-filter-node.interface.js +3 -0
  6. package/dist/core/common/interfaces/scim-array-filter-node.interface.js.map +1 -0
  7. package/dist/core/common/interfaces/scim-condition-node.interface.d.ts +7 -0
  8. package/dist/core/common/interfaces/scim-condition-node.interface.js +3 -0
  9. package/dist/core/common/interfaces/scim-condition-node.interface.js.map +1 -0
  10. package/dist/core/common/interfaces/scim-logical-node.interface.d.ts +7 -0
  11. package/dist/core/common/interfaces/scim-logical-node.interface.js +3 -0
  12. package/dist/core/common/interfaces/scim-logical-node.interface.js.map +1 -0
  13. package/dist/core/common/types/scim-comparator.type.d.ts +1 -0
  14. package/dist/core/common/types/scim-comparator.type.js +3 -0
  15. package/dist/core/common/types/scim-comparator.type.js.map +1 -0
  16. package/dist/core/common/types/scim-logical-operator.type.d.ts +1 -0
  17. package/dist/core/common/types/scim-logical-operator.type.js +3 -0
  18. package/dist/core/common/types/scim-logical-operator.type.js.map +1 -0
  19. package/dist/core/common/types/scim-node.type.d.ts +4 -0
  20. package/dist/core/common/types/scim-node.type.js +3 -0
  21. package/dist/core/common/types/scim-node.type.js.map +1 -0
  22. package/dist/core/modules/auth/services/core-auth.service.js +6 -1
  23. package/dist/core/modules/auth/services/core-auth.service.js.map +1 -1
  24. package/dist/index.d.ts +1 -0
  25. package/dist/index.js +1 -0
  26. package/dist/index.js.map +1 -1
  27. package/dist/tsconfig.build.tsbuildinfo +1 -1
  28. package/package.json +3 -3
  29. package/src/core/common/helpers/scim.helper.ts +254 -0
  30. package/src/core/common/interfaces/scim-array-filter-node.interface.ts +8 -0
  31. package/src/core/common/interfaces/scim-condition-node.interface.ts +9 -0
  32. package/src/core/common/interfaces/scim-logical-node.interface.ts +9 -0
  33. package/src/core/common/types/scim-comparator.type.ts +2 -0
  34. package/src/core/common/types/scim-logical-operator.type.ts +1 -0
  35. package/src/core/common/types/scim-node.type.ts +6 -0
  36. package/src/core/modules/auth/services/core-auth.service.ts +6 -1
  37. 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.10",
3
+ "version": "11.1.12",
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.0",
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.26.0",
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",
@@ -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,8 @@
1
+ import { ScimNode } from '../types/scim-node.type';
2
+
3
+ /** Represents a SCIM array filter node, e.g. emails[type eq "work"] */
4
+ export interface ArrayFilterNode {
5
+ expr: ScimNode;
6
+ path: string;
7
+ type: 'array';
8
+ }
@@ -0,0 +1,9 @@
1
+ import { Comparator } from '../types/scim-comparator.type';
2
+
3
+ /** Represents a single SCIM condition such as userName eq "Joe" */
4
+ export interface ConditionNode {
5
+ attributePath: string;
6
+ comparator: Comparator;
7
+ type: 'condition';
8
+ value?: string;
9
+ }
@@ -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,2 @@
1
+ /** Supported SCIM comparison operators */
2
+ export type Comparator = 'aco' | 'co' | 'eq' | 'ew' | 'ge' | 'gt' | 'le' | 'lt' | 'pr' | 'sw';
@@ -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;
@@ -289,7 +289,12 @@ export class CoreAuthService {
289
289
  if (!deviceId) {
290
290
  deviceId = payload.deviceId;
291
291
  }
292
- user.refreshTokens[deviceId] = { ...data, deviceId, tokenId: payload.tokenId };
292
+ user.refreshTokens[deviceId] = {
293
+ ...data,
294
+ deviceDescription: payload.deviceDescription || data.deviceDescription,
295
+ deviceId,
296
+ tokenId: payload.tokenId,
297
+ };
293
298
  user.tempTokens[deviceId] = { createdAt: new Date().getTime(), deviceId, tokenId: payload.tokenId };
294
299
  await this.userService.update(
295
300
  getStringIds(user),
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';