itlab-internal-services 2.16.0 → 2.16.1
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/classes/document-merger.class.js +2 -1
- package/dist/classes/index.d.ts +3 -1
- package/dist/classes/index.js +5 -3
- package/dist/classes/schema-builder.class.d.ts +75 -0
- package/dist/classes/schema-builder.class.js +109 -0
- package/dist/classes/task-manager.class.d.ts +71 -0
- package/dist/classes/task-manager.class.js +99 -0
- package/dist/exceptions/bad-parameter.exception.js +2 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3 -3
- package/dist/interceptors/attribute-sanitizer.interceptor.d.ts +68 -0
- package/dist/interceptors/attribute-sanitizer.interceptor.js +154 -0
- package/dist/interceptors/index.d.ts +1 -0
- package/dist/interceptors/index.js +17 -0
- package/dist/middleware/http-logging.middleware.d.ts +19 -0
- package/dist/middleware/http-logging.middleware.js +50 -0
- package/dist/middleware/index.d.ts +1 -0
- package/dist/middleware/index.js +17 -0
- package/dist/modules/authentication/guards/jwt-auth.guard.js +1 -7
- package/dist/modules/authentication/guards/permissions.guard.js +4 -3
- package/dist/modules/authentication/guards/service-auth.guard.js +1 -1
- package/dist/modules/comment/comment-module-options.interface.d.ts +2 -2
- package/dist/modules/comment/comment.controller.d.ts +5 -5
- package/dist/modules/comment/comment.controller.js +3 -3
- package/dist/modules/comment/comment.service.d.ts +3 -3
- package/dist/modules/comment/comment.service.js +1 -1
- package/dist/modules/like/like-module-options.interface.d.ts +2 -2
- package/dist/modules/like/like.controller.d.ts +2 -2
- package/dist/modules/like/like.controller.js +2 -2
- package/dist/modules/services/providers/accounts.service.js +2 -0
- package/dist/modules/services/providers/books.service.js +2 -0
- package/dist/modules/services/providers/changelog.service.js +2 -0
- package/dist/modules/services/providers/comments/comments.service.d.ts +3 -3
- package/dist/modules/services/providers/comments/comments.service.js +2 -1
- package/dist/modules/services/providers/content/{content-types.d.ts → content-return-types.d.ts} +3 -4
- package/dist/modules/services/providers/content/{content-types.js → content-return-types.js} +2 -2
- package/dist/modules/services/providers/content/content.service.d.ts +7 -5
- package/dist/modules/services/providers/content/content.service.js +29 -12
- package/dist/modules/services/providers/content/index.d.ts +1 -0
- package/dist/modules/services/providers/demo-hive.service.js +2 -0
- package/dist/modules/services/providers/events.service.js +2 -0
- package/dist/modules/services/providers/hackschool.service.js +2 -0
- package/dist/modules/services/providers/ideas.service.js +2 -0
- package/dist/modules/services/providers/lunch-roulette.service.js +2 -0
- package/dist/modules/services/providers/mail/mail.service.js +1 -1
- package/dist/modules/services/providers/newletter.service.js +2 -0
- package/dist/modules/services/providers/newsroom.service.js +2 -0
- package/dist/modules/services/providers/podcasts.service.js +2 -0
- package/dist/modules/services/providers/search/search.service.js +1 -1
- package/dist/modules/services/providers/team.service.js +2 -0
- package/dist/modules/services/providers/tech-radar.service.js +2 -0
- package/dist/properties/content-return-type.property.d.ts +7 -0
- package/dist/properties/content-return-type.property.js +22 -0
- package/dist/properties/index.d.ts +3 -0
- package/dist/properties/index.js +7 -1
- package/dist/properties/likeable.properties.d.ts +1 -0
- package/dist/properties/likeable.properties.js +66 -0
- package/dist/properties/viewable.properties.d.ts +1 -0
- package/dist/properties/viewable.properties.js +66 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +18 -0
- package/dist/types/likeable.type.d.ts +15 -0
- package/dist/types/likeable.type.js +2 -0
- package/dist/types/viewable.type.d.ts +15 -0
- package/dist/types/viewable.type.js +2 -0
- package/package.json +2 -2
- package/dist/classes/document-updater.class.d.ts +0 -39
- package/dist/classes/document-updater.class.js +0 -57
- package/dist/http-logger.middleware.d.ts +0 -12
- package/dist/http-logger.middleware.js +0 -43
- package/dist/likeable.interface.d.ts +0 -41
- package/dist/likeable.interface.js +0 -48
- package/dist/viewable.interface.d.ts +0 -41
- package/dist/viewable.interface.js +0 -48
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DocumentMerger = void 0;
|
|
4
4
|
const common_1 = require("@nestjs/common");
|
|
5
|
+
const itlab_functions_1 = require("itlab-functions");
|
|
5
6
|
/**
|
|
6
7
|
* DocumentMerger
|
|
7
8
|
*
|
|
@@ -75,7 +76,7 @@ class DocumentMerger {
|
|
|
75
76
|
});
|
|
76
77
|
const elapsedTime = Date.now() - startTime;
|
|
77
78
|
// Log completion with fields merged and time taken
|
|
78
|
-
this.logger.log(`Successfully merged fields: ${
|
|
79
|
+
this.logger.log(`Successfully merged fields: ${(0, itlab_functions_1.formatList)(this.fields)} (took ${elapsedTime} ms)`);
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
exports.DocumentMerger = DocumentMerger;
|
package/dist/classes/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { DocumentMerger } from './document-merger.class';
|
|
2
|
-
export { DocumentUpdater } from './document-updater.class';
|
|
3
2
|
export { ReportCategory } from './report-category.class';
|
|
3
|
+
export { SchemaBuilder } from './schema-builder.class';
|
|
4
|
+
export type { SchemaBuilderDocument, SchemaBuilderModel, SchemaBuilderOptions } from './schema-builder.class';
|
|
5
|
+
export { TaskManager } from './task-manager.class';
|
package/dist/classes/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.TaskManager = exports.SchemaBuilder = exports.ReportCategory = exports.DocumentMerger = void 0;
|
|
4
4
|
var document_merger_class_1 = require("./document-merger.class");
|
|
5
5
|
Object.defineProperty(exports, "DocumentMerger", { enumerable: true, get: function () { return document_merger_class_1.DocumentMerger; } });
|
|
6
|
-
var document_updater_class_1 = require("./document-updater.class");
|
|
7
|
-
Object.defineProperty(exports, "DocumentUpdater", { enumerable: true, get: function () { return document_updater_class_1.DocumentUpdater; } });
|
|
8
6
|
var report_category_class_1 = require("./report-category.class");
|
|
9
7
|
Object.defineProperty(exports, "ReportCategory", { enumerable: true, get: function () { return report_category_class_1.ReportCategory; } });
|
|
8
|
+
var schema_builder_class_1 = require("./schema-builder.class");
|
|
9
|
+
Object.defineProperty(exports, "SchemaBuilder", { enumerable: true, get: function () { return schema_builder_class_1.SchemaBuilder; } });
|
|
10
|
+
var task_manager_class_1 = require("./task-manager.class");
|
|
11
|
+
Object.defineProperty(exports, "TaskManager", { enumerable: true, get: function () { return task_manager_class_1.TaskManager; } });
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Type } from '@nestjs/common';
|
|
2
|
+
import { Document, Model, Schema } from 'mongoose';
|
|
3
|
+
import { Likeable, Viewable } from '../types';
|
|
4
|
+
/**
|
|
5
|
+
* Options that determine how the schema should be extended.
|
|
6
|
+
*
|
|
7
|
+
* @template TClass - The document type for the schema.
|
|
8
|
+
*/
|
|
9
|
+
export type SchemaBuilderOptions<TClass extends Document> = {
|
|
10
|
+
/** Whether this schema should support view tracking. */
|
|
11
|
+
viewable?: boolean;
|
|
12
|
+
/** Whether this schema should support like tracking. */
|
|
13
|
+
likeable?: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* A set of keys that uniquely identify the document.
|
|
16
|
+
* These keys are used when checking for duplicates.
|
|
17
|
+
*/
|
|
18
|
+
criticalKeys?: (keyof Omit<TClass, keyof Document>)[];
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Computes the augmented document type based on SchemaOptions.
|
|
22
|
+
*/
|
|
23
|
+
export type SchemaBuilderDocument<TClass extends Document, TOptions extends SchemaBuilderOptions<TClass>> = TClass & (TOptions['viewable'] extends true ? Viewable : object) & (TOptions['likeable'] extends true ? Likeable : object);
|
|
24
|
+
/**
|
|
25
|
+
* Extends a standard Mongoose model with custom static methods
|
|
26
|
+
* injected by the SchemaBuilder.
|
|
27
|
+
*/
|
|
28
|
+
export type SchemaBuilderModel<TClass extends Document> = Model<TClass> & {
|
|
29
|
+
/**
|
|
30
|
+
* Checks if the given object is a duplicate of an existing document.
|
|
31
|
+
*
|
|
32
|
+
* @param dto - Partial object with properties to check for duplicates.
|
|
33
|
+
* @param updateTarget - The document being updated, if applicable.
|
|
34
|
+
* @returns {Promise<boolean>} - True if a duplicate exists, false otherwise.
|
|
35
|
+
*/
|
|
36
|
+
isDuplicate(dto: Partial<TClass>, updateTarget?: TClass): Promise<boolean>;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Generic builder for creating extended Mongoose schemas.
|
|
40
|
+
*
|
|
41
|
+
* This class wraps Nest's SchemaFactory and automatically injects common
|
|
42
|
+
* behaviors such as:
|
|
43
|
+
* - View tracking (who viewed, view counts)
|
|
44
|
+
* - Like tracking (who liked, like counts)
|
|
45
|
+
* - Duplicate checking based on critical keys
|
|
46
|
+
*
|
|
47
|
+
* @template TClass - A class extending Mongoose Document.
|
|
48
|
+
*/
|
|
49
|
+
export declare class SchemaBuilder<TClass extends Document> {
|
|
50
|
+
private schema;
|
|
51
|
+
readonly options: SchemaBuilderOptions<TClass>;
|
|
52
|
+
constructor(target: Type<TClass>, options?: SchemaBuilderOptions<TClass>);
|
|
53
|
+
/**
|
|
54
|
+
* Adds interaction tracking (likes or views) to a schema.
|
|
55
|
+
* This method generalizes the shared logic between "viewable" and "likeable".
|
|
56
|
+
*
|
|
57
|
+
* @param field - The internal array field storing account IDs. (e.g. _viewedBy or _likedBy)
|
|
58
|
+
* @param counter - The numeric counter field tracking totals. (e.g. views or likes)
|
|
59
|
+
*/
|
|
60
|
+
provideInteraction(field: string, counter: string): this;
|
|
61
|
+
/**
|
|
62
|
+
* Adds a duplicate-checking static method to the schema.
|
|
63
|
+
* This method compares critical keys to detect conflicts.
|
|
64
|
+
*/
|
|
65
|
+
private provideDuplicateChecker;
|
|
66
|
+
/**
|
|
67
|
+
* Finalizes the schema and injects any selected behaviors (viewable, likeable, duplicate checking).
|
|
68
|
+
*
|
|
69
|
+
* @returns An object containing:
|
|
70
|
+
* - `schema`: The Mongoose schema.
|
|
71
|
+
* - `modelType`: A strongly typed model with additional static methods.
|
|
72
|
+
* - `documentType`: The inferred document type with injected properties.
|
|
73
|
+
*/
|
|
74
|
+
build(): Schema;
|
|
75
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SchemaBuilder = void 0;
|
|
4
|
+
const mongoose_1 = require("@nestjs/mongoose");
|
|
5
|
+
const class_validator_1 = require("class-validator");
|
|
6
|
+
/**
|
|
7
|
+
* Generic builder for creating extended Mongoose schemas.
|
|
8
|
+
*
|
|
9
|
+
* This class wraps Nest's SchemaFactory and automatically injects common
|
|
10
|
+
* behaviors such as:
|
|
11
|
+
* - View tracking (who viewed, view counts)
|
|
12
|
+
* - Like tracking (who liked, like counts)
|
|
13
|
+
* - Duplicate checking based on critical keys
|
|
14
|
+
*
|
|
15
|
+
* @template TClass - A class extending Mongoose Document.
|
|
16
|
+
*/
|
|
17
|
+
class SchemaBuilder {
|
|
18
|
+
constructor(target, options = {}) {
|
|
19
|
+
this.schema = mongoose_1.SchemaFactory.createForClass(target);
|
|
20
|
+
this.options = options;
|
|
21
|
+
/**
|
|
22
|
+
* Create a wildcard index required for CosmosDB to support sorting on all fields.
|
|
23
|
+
*
|
|
24
|
+
* This is a workaround for CosmosDB's partial MongoDB API compatibility.
|
|
25
|
+
* See: https://learn.microsoft.com/en-us/azure/cosmos-db/mongodb/indexing#wildcard-indexes
|
|
26
|
+
*/
|
|
27
|
+
this.schema.index({ '$**': 1 }, { name: 'Sort Index' });
|
|
28
|
+
this.provideDuplicateChecker();
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Adds interaction tracking (likes or views) to a schema.
|
|
32
|
+
* This method generalizes the shared logic between "viewable" and "likeable".
|
|
33
|
+
*
|
|
34
|
+
* @param field - The internal array field storing account IDs. (e.g. _viewedBy or _likedBy)
|
|
35
|
+
* @param counter - The numeric counter field tracking totals. (e.g. views or likes)
|
|
36
|
+
*/
|
|
37
|
+
provideInteraction(field, counter) {
|
|
38
|
+
// Add fields to the schema
|
|
39
|
+
this.schema.add({ [field]: { type: [String], default: [] }, [counter]: { type: Number, default: 0 } });
|
|
40
|
+
// Expose a virtual property (without underscore) for convenience
|
|
41
|
+
this.schema.virtual(field.replace(/^_/, ''));
|
|
42
|
+
// Add method to increment counter
|
|
43
|
+
this.schema.methods[`add${counter[0].toUpperCase() + counter.slice(1)}`] = async function (accountId) {
|
|
44
|
+
if (!(0, class_validator_1.isMongoId)(accountId))
|
|
45
|
+
return Number(this[counter]);
|
|
46
|
+
// Ensure uniqueness by converting to a Set
|
|
47
|
+
this[field] = Array.from(new Set([...this[field], accountId]));
|
|
48
|
+
this[counter] = this[field].length;
|
|
49
|
+
await this.save({ timestamps: false });
|
|
50
|
+
return this[counter];
|
|
51
|
+
};
|
|
52
|
+
// Add method to decrement counter
|
|
53
|
+
this.schema.methods[`remove${counter[0].toUpperCase() + counter.slice(1)}`] = async function (accountId) {
|
|
54
|
+
if (!(0, class_validator_1.isMongoId)(accountId))
|
|
55
|
+
return Number(this[counter]);
|
|
56
|
+
// Remove the accountId from the list
|
|
57
|
+
this[field] = this[field].filter((id) => id !== accountId);
|
|
58
|
+
this[counter] = this[field].length;
|
|
59
|
+
await this.save({ timestamps: false });
|
|
60
|
+
return this[counter];
|
|
61
|
+
};
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Adds a duplicate-checking static method to the schema.
|
|
66
|
+
* This method compares critical keys to detect conflicts.
|
|
67
|
+
*/
|
|
68
|
+
provideDuplicateChecker() {
|
|
69
|
+
var _a;
|
|
70
|
+
const criticalKeys = ((_a = this.options) === null || _a === void 0 ? void 0 : _a.criticalKeys) || [];
|
|
71
|
+
this.schema.statics.isDuplicate = async function (dto, updateTarget) {
|
|
72
|
+
var _a;
|
|
73
|
+
if (criticalKeys.length === 0)
|
|
74
|
+
return false;
|
|
75
|
+
// Build a filter using the critical keys
|
|
76
|
+
const filterQuery = criticalKeys.reduce((query, key) => {
|
|
77
|
+
if (dto && key in dto)
|
|
78
|
+
query[String(key)] = dto[key];
|
|
79
|
+
else if (updateTarget && key in updateTarget)
|
|
80
|
+
query[String(key)] = updateTarget[key];
|
|
81
|
+
return query;
|
|
82
|
+
}, {});
|
|
83
|
+
// If a matching document exists, check whether it’s the same entity
|
|
84
|
+
const potentialDuplicate = await this.findOne(filterQuery);
|
|
85
|
+
if (!potentialDuplicate)
|
|
86
|
+
return false;
|
|
87
|
+
const potentialId = potentialDuplicate._id.toString().toLowerCase();
|
|
88
|
+
const targetId = (_a = updateTarget === null || updateTarget === void 0 ? void 0 : updateTarget._id) === null || _a === void 0 ? void 0 : _a.toString().toLowerCase();
|
|
89
|
+
return potentialId !== targetId;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Finalizes the schema and injects any selected behaviors (viewable, likeable, duplicate checking).
|
|
94
|
+
*
|
|
95
|
+
* @returns An object containing:
|
|
96
|
+
* - `schema`: The Mongoose schema.
|
|
97
|
+
* - `modelType`: A strongly typed model with additional static methods.
|
|
98
|
+
* - `documentType`: The inferred document type with injected properties.
|
|
99
|
+
*/
|
|
100
|
+
build() {
|
|
101
|
+
var _a, _b;
|
|
102
|
+
if ((_a = this.options) === null || _a === void 0 ? void 0 : _a.viewable)
|
|
103
|
+
this.provideInteraction('_viewedBy', 'views');
|
|
104
|
+
if ((_b = this.options) === null || _b === void 0 ? void 0 : _b.likeable)
|
|
105
|
+
this.provideInteraction('_likedBy', 'likes');
|
|
106
|
+
return this.schema;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
exports.SchemaBuilder = SchemaBuilder;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Account, User } from '../models';
|
|
2
|
+
import { AccountsService } from '../modules';
|
|
3
|
+
/**
|
|
4
|
+
* TaskManager is a utility class that collects asynchronous tasks
|
|
5
|
+
* and executes them in parallel.
|
|
6
|
+
*
|
|
7
|
+
* This is especially useful for orchestrating optional async operations
|
|
8
|
+
* where not all tasks need to be executed every time.
|
|
9
|
+
*
|
|
10
|
+
* Example:
|
|
11
|
+
* ```ts
|
|
12
|
+
* const taskManager = new TaskManager();
|
|
13
|
+
* taskManager.add(async () => { ... });
|
|
14
|
+
* taskManager.add(async () => { ... });
|
|
15
|
+
* await taskManager.run();
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare class TaskManager {
|
|
19
|
+
private tasks;
|
|
20
|
+
/**
|
|
21
|
+
* Fetches all users for the given ID groups and returns a mapping.
|
|
22
|
+
* Ensures each user is fetched only once.
|
|
23
|
+
* @param userIdGroups Array of arrays of user IDs (e.g., authors, likedBy, viewedBy)
|
|
24
|
+
* @param accountsService Service responsible for fetching users
|
|
25
|
+
* @returns Map of userId -> user
|
|
26
|
+
*/
|
|
27
|
+
static fetchAndMapUsers(userIdGroups: string[][], accountsService: AccountsService): Promise<Map<string, User>>;
|
|
28
|
+
/**
|
|
29
|
+
* Fetches all users for the given ID groups and returns a mapping.
|
|
30
|
+
* Ensures each user is fetched only once.
|
|
31
|
+
* @param accountIdGroups Array of arrays of user IDs (e.g., authors, likedBy, viewedBy)
|
|
32
|
+
* @param accountsService Service responsible for fetching users
|
|
33
|
+
* @returns Map of userId -> user
|
|
34
|
+
*/
|
|
35
|
+
static fetchAndMapAccounts(accountIdGroups: string[][], accountsService: AccountsService): Promise<Map<string, Account>>;
|
|
36
|
+
/**
|
|
37
|
+
* Maps an array of IDs to user objects using a userMap.
|
|
38
|
+
* Filters out any IDs that don’t have a corresponding user.
|
|
39
|
+
* @param userIds Array of user IDs to map
|
|
40
|
+
* @param userMap Map of userId -> user object
|
|
41
|
+
* @returns Array of user objects
|
|
42
|
+
*/
|
|
43
|
+
static mapIdsToUsers(userIds: string[], userMap: Map<string, User>): any[];
|
|
44
|
+
/**
|
|
45
|
+
* Maps an array of IDs to user objects using a userMap.
|
|
46
|
+
* Filters out any IDs that don’t have a corresponding user.
|
|
47
|
+
* @param accountIds Array of user IDs to map
|
|
48
|
+
* @param accountMap Map of userId -> user object
|
|
49
|
+
* @returns Array of user objects
|
|
50
|
+
*/
|
|
51
|
+
static mapIdsToAccounts(accountIds: string[], accountMap: Map<string, Account>): any[];
|
|
52
|
+
/**
|
|
53
|
+
* Adds a new asynchronous task to the queue.
|
|
54
|
+
*
|
|
55
|
+
* @param task - A function that returns a Promise.
|
|
56
|
+
* It should contain the actual async logic to be executed.
|
|
57
|
+
*/
|
|
58
|
+
add(task: () => Promise<void>): void;
|
|
59
|
+
/**
|
|
60
|
+
* Executes all queued tasks in parallel and waits for their completion.
|
|
61
|
+
*
|
|
62
|
+
* - Tasks are executed concurrently using `Promise.all`.
|
|
63
|
+
* - If one task fails, the error will propagate and stop execution.
|
|
64
|
+
*/
|
|
65
|
+
run(): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Clears all tasks from the queue without executing them.
|
|
68
|
+
* Useful if you want to reset and reuse the TaskManager instance.
|
|
69
|
+
*/
|
|
70
|
+
clear(): void;
|
|
71
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TaskManager = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* TaskManager is a utility class that collects asynchronous tasks
|
|
6
|
+
* and executes them in parallel.
|
|
7
|
+
*
|
|
8
|
+
* This is especially useful for orchestrating optional async operations
|
|
9
|
+
* where not all tasks need to be executed every time.
|
|
10
|
+
*
|
|
11
|
+
* Example:
|
|
12
|
+
* ```ts
|
|
13
|
+
* const taskManager = new TaskManager();
|
|
14
|
+
* taskManager.add(async () => { ... });
|
|
15
|
+
* taskManager.add(async () => { ... });
|
|
16
|
+
* await taskManager.run();
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
class TaskManager {
|
|
20
|
+
constructor() {
|
|
21
|
+
this.tasks = [];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Fetches all users for the given ID groups and returns a mapping.
|
|
25
|
+
* Ensures each user is fetched only once.
|
|
26
|
+
* @param userIdGroups Array of arrays of user IDs (e.g., authors, likedBy, viewedBy)
|
|
27
|
+
* @param accountsService Service responsible for fetching users
|
|
28
|
+
* @returns Map of userId -> user
|
|
29
|
+
*/
|
|
30
|
+
static async fetchAndMapUsers(userIdGroups, accountsService) {
|
|
31
|
+
// Collect all unique user IDs across all groups
|
|
32
|
+
const uniqueUserIds = new Set(userIdGroups.flat());
|
|
33
|
+
// Fetch all users once
|
|
34
|
+
const userCollection = await accountsService.getUserCollection(Array.from(uniqueUserIds));
|
|
35
|
+
// Build a map for quick lookup
|
|
36
|
+
return new Map(userCollection.map((user) => [user.id, user]));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Fetches all users for the given ID groups and returns a mapping.
|
|
40
|
+
* Ensures each user is fetched only once.
|
|
41
|
+
* @param accountIdGroups Array of arrays of user IDs (e.g., authors, likedBy, viewedBy)
|
|
42
|
+
* @param accountsService Service responsible for fetching users
|
|
43
|
+
* @returns Map of userId -> user
|
|
44
|
+
*/
|
|
45
|
+
static async fetchAndMapAccounts(accountIdGroups, accountsService) {
|
|
46
|
+
// Collect all unique user IDs across all groups
|
|
47
|
+
const uniqueAccountIds = new Set(accountIdGroups.flat());
|
|
48
|
+
// Fetch all users once
|
|
49
|
+
const accountCollection = await accountsService.getAccountCollection(Array.from(uniqueAccountIds));
|
|
50
|
+
// Build a map for quick lookup
|
|
51
|
+
return new Map(accountCollection.map((account) => [account.id, account]));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Maps an array of IDs to user objects using a userMap.
|
|
55
|
+
* Filters out any IDs that don’t have a corresponding user.
|
|
56
|
+
* @param userIds Array of user IDs to map
|
|
57
|
+
* @param userMap Map of userId -> user object
|
|
58
|
+
* @returns Array of user objects
|
|
59
|
+
*/
|
|
60
|
+
static mapIdsToUsers(userIds, userMap) {
|
|
61
|
+
return userIds.map((userId) => userMap.get(userId)).filter(Boolean);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Maps an array of IDs to user objects using a userMap.
|
|
65
|
+
* Filters out any IDs that don’t have a corresponding user.
|
|
66
|
+
* @param accountIds Array of user IDs to map
|
|
67
|
+
* @param accountMap Map of userId -> user object
|
|
68
|
+
* @returns Array of user objects
|
|
69
|
+
*/
|
|
70
|
+
static mapIdsToAccounts(accountIds, accountMap) {
|
|
71
|
+
return accountIds.map((accountId) => accountMap.get(accountId)).filter(Boolean);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Adds a new asynchronous task to the queue.
|
|
75
|
+
*
|
|
76
|
+
* @param task - A function that returns a Promise.
|
|
77
|
+
* It should contain the actual async logic to be executed.
|
|
78
|
+
*/
|
|
79
|
+
add(task) {
|
|
80
|
+
this.tasks.push(task);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Executes all queued tasks in parallel and waits for their completion.
|
|
84
|
+
*
|
|
85
|
+
* - Tasks are executed concurrently using `Promise.all`.
|
|
86
|
+
* - If one task fails, the error will propagate and stop execution.
|
|
87
|
+
*/
|
|
88
|
+
async run() {
|
|
89
|
+
await Promise.all(this.tasks.map((task) => task()));
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Clears all tasks from the queue without executing them.
|
|
93
|
+
* Useful if you want to reset and reuse the TaskManager instance.
|
|
94
|
+
*/
|
|
95
|
+
clear() {
|
|
96
|
+
this.tasks = [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
exports.TaskManager = TaskManager;
|
|
@@ -4,6 +4,7 @@ exports.BadParameterException = void 0;
|
|
|
4
4
|
exports.ApiBadParameterResponse = ApiBadParameterResponse;
|
|
5
5
|
const common_1 = require("@nestjs/common");
|
|
6
6
|
const swagger_1 = require("@nestjs/swagger");
|
|
7
|
+
const itlab_functions_1 = require("itlab-functions");
|
|
7
8
|
/**
|
|
8
9
|
* BadParameterException
|
|
9
10
|
*
|
|
@@ -56,7 +57,7 @@ function ApiBadParameterResponse(...params) {
|
|
|
56
57
|
// Format description with proper pluralization.
|
|
57
58
|
const pluralized = params.length > 1 ? 's' : '';
|
|
58
59
|
const newDescription = params.length
|
|
59
|
-
? `Invalid parameter${pluralized}: ${
|
|
60
|
+
? `Invalid parameter${pluralized}: ${(0, itlab_functions_1.formatList)(params, 'or')}`
|
|
60
61
|
: 'One or more request parameters are invalid.';
|
|
61
62
|
// Merge with existing description if needed.
|
|
62
63
|
const mergedDescription = existingDescription ? `${existingDescription}<br />${newDescription}` : newDescription;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,13 +3,13 @@ export * from './controllers';
|
|
|
3
3
|
export * from './decorators';
|
|
4
4
|
export * from './exceptions';
|
|
5
5
|
export * from './functions';
|
|
6
|
+
export * from './interceptors';
|
|
7
|
+
export * from './middleware';
|
|
6
8
|
export * from './models';
|
|
7
9
|
export * from './modules';
|
|
8
10
|
export * from './pipes';
|
|
9
11
|
export * from './properties';
|
|
10
12
|
export * from './swagger';
|
|
11
13
|
export * from './transform';
|
|
12
|
-
export * from './
|
|
14
|
+
export * from './types';
|
|
13
15
|
export * from './hub-resource.enum';
|
|
14
|
-
export * from './likeable.interface';
|
|
15
|
-
export * from './viewable.interface';
|
package/dist/index.js
CHANGED
|
@@ -19,13 +19,13 @@ __exportStar(require("./controllers"), exports);
|
|
|
19
19
|
__exportStar(require("./decorators"), exports);
|
|
20
20
|
__exportStar(require("./exceptions"), exports);
|
|
21
21
|
__exportStar(require("./functions"), exports);
|
|
22
|
+
__exportStar(require("./interceptors"), exports);
|
|
23
|
+
__exportStar(require("./middleware"), exports);
|
|
22
24
|
__exportStar(require("./models"), exports);
|
|
23
25
|
__exportStar(require("./modules"), exports);
|
|
24
26
|
__exportStar(require("./pipes"), exports);
|
|
25
27
|
__exportStar(require("./properties"), exports);
|
|
26
28
|
__exportStar(require("./swagger"), exports);
|
|
27
29
|
__exportStar(require("./transform"), exports);
|
|
28
|
-
__exportStar(require("./
|
|
30
|
+
__exportStar(require("./types"), exports);
|
|
29
31
|
__exportStar(require("./hub-resource.enum"), exports);
|
|
30
|
-
__exportStar(require("./likeable.interface"), exports);
|
|
31
|
-
__exportStar(require("./viewable.interface"), exports);
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { CallHandler, ExecutionContext, NestInterceptor } from '@nestjs/common';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
/**
|
|
4
|
+
* AttributeSanitizerInterceptor
|
|
5
|
+
*
|
|
6
|
+
* This interceptor removes specified attributes from response objects.
|
|
7
|
+
* It works with plain objects, arrays, sets, maps and also with Mongoose
|
|
8
|
+
* documents by normalizing them to plain objects before sanitization.
|
|
9
|
+
*
|
|
10
|
+
* This is especially useful for removing sensitive fields (e.g., passwords,
|
|
11
|
+
* tokens, SSNs) consistently across deeply nested response structures.
|
|
12
|
+
*/
|
|
13
|
+
export declare class AttributeSanitizerInterceptor implements NestInterceptor {
|
|
14
|
+
private readonly keys;
|
|
15
|
+
private readonly mongooseToObjectOptions;
|
|
16
|
+
/**
|
|
17
|
+
* @param keys - One or more property names to remove from every object.
|
|
18
|
+
* @param mongooseToObjectOptions - Options forwarded to Mongoose `.toObject()`
|
|
19
|
+
* when normalizing documents.
|
|
20
|
+
*/
|
|
21
|
+
constructor(keys: string | string[], mongooseToObjectOptions?: Record<string, unknown>);
|
|
22
|
+
/**
|
|
23
|
+
* Intercepts the response and removes the targeted attributes.
|
|
24
|
+
*
|
|
25
|
+
* @param _context - Execution context (unused here).
|
|
26
|
+
* @param next - Next handler in the pipeline.
|
|
27
|
+
* @returns Sanitized response data.
|
|
28
|
+
*/
|
|
29
|
+
intercept(_context: ExecutionContext, next: CallHandler): Observable<unknown>;
|
|
30
|
+
/**
|
|
31
|
+
* Recursively removes targeted keys from objects, arrays, and nested structures.
|
|
32
|
+
*
|
|
33
|
+
* @param value - Any response value.
|
|
34
|
+
* @returns A sanitized value without the targeted attributes.
|
|
35
|
+
*/
|
|
36
|
+
private sanitizeRecursively;
|
|
37
|
+
/**
|
|
38
|
+
* Converts a Mongoose document to a plain object if needed.
|
|
39
|
+
*
|
|
40
|
+
* @param value - Possible Mongoose document.
|
|
41
|
+
* @returns A plain object if conversion is possible, otherwise the value itself.
|
|
42
|
+
*/
|
|
43
|
+
private normalizeIfMongooseDocument;
|
|
44
|
+
/**
|
|
45
|
+
* Checks whether a value is a plain object (and not a class instance, Date, etc.).
|
|
46
|
+
*
|
|
47
|
+
* @param value - Any value.
|
|
48
|
+
* @returns True if the value is a plain object.
|
|
49
|
+
*/
|
|
50
|
+
private isPlainObject;
|
|
51
|
+
/**
|
|
52
|
+
* Converts the user-provided keys into a Set for O(1) lookup.
|
|
53
|
+
*
|
|
54
|
+
* @param keys - String or array of strings.
|
|
55
|
+
* @returns A Set of keys.
|
|
56
|
+
*/
|
|
57
|
+
private ensureKeySet;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* @UseAttributeSanitizer
|
|
61
|
+
*
|
|
62
|
+
* Convenience decorator that applies the AttributeSanitizerInterceptor
|
|
63
|
+
* to a route or controller.
|
|
64
|
+
*
|
|
65
|
+
* @param keys - One or more keys to remove from all response objects.
|
|
66
|
+
* @param mongooseToObjectOptions - Options for Mongoose `.toObject()` normalization.
|
|
67
|
+
*/
|
|
68
|
+
export declare function UseAttributeSanitizer(keys: string | string[], mongooseToObjectOptions?: Record<string, unknown>): MethodDecorator & ClassDecorator;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.AttributeSanitizerInterceptor = void 0;
|
|
13
|
+
exports.UseAttributeSanitizer = UseAttributeSanitizer;
|
|
14
|
+
const common_1 = require("@nestjs/common");
|
|
15
|
+
const operators_1 = require("rxjs/operators");
|
|
16
|
+
/**
|
|
17
|
+
* AttributeSanitizerInterceptor
|
|
18
|
+
*
|
|
19
|
+
* This interceptor removes specified attributes from response objects.
|
|
20
|
+
* It works with plain objects, arrays, sets, maps and also with Mongoose
|
|
21
|
+
* documents by normalizing them to plain objects before sanitization.
|
|
22
|
+
*
|
|
23
|
+
* This is especially useful for removing sensitive fields (e.g., passwords,
|
|
24
|
+
* tokens, SSNs) consistently across deeply nested response structures.
|
|
25
|
+
*/
|
|
26
|
+
let AttributeSanitizerInterceptor = class AttributeSanitizerInterceptor {
|
|
27
|
+
/**
|
|
28
|
+
* @param keys - One or more property names to remove from every object.
|
|
29
|
+
* @param mongooseToObjectOptions - Options forwarded to Mongoose `.toObject()`
|
|
30
|
+
* when normalizing documents.
|
|
31
|
+
*/
|
|
32
|
+
constructor(keys, mongooseToObjectOptions = {
|
|
33
|
+
virtuals: true,
|
|
34
|
+
getters: true,
|
|
35
|
+
versionKey: false,
|
|
36
|
+
}) {
|
|
37
|
+
this.keys = keys;
|
|
38
|
+
this.mongooseToObjectOptions = mongooseToObjectOptions;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Intercepts the response and removes the targeted attributes.
|
|
42
|
+
*
|
|
43
|
+
* @param _context - Execution context (unused here).
|
|
44
|
+
* @param next - Next handler in the pipeline.
|
|
45
|
+
* @returns Sanitized response data.
|
|
46
|
+
*/
|
|
47
|
+
intercept(_context, next) {
|
|
48
|
+
return next.handle().pipe((0, operators_1.map)((data) => this.sanitizeRecursively(data)));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Recursively removes targeted keys from objects, arrays, and nested structures.
|
|
52
|
+
*
|
|
53
|
+
* @param value - Any response value.
|
|
54
|
+
* @returns A sanitized value without the targeted attributes.
|
|
55
|
+
*/
|
|
56
|
+
sanitizeRecursively(value) {
|
|
57
|
+
const normalized = this.normalizeIfMongooseDocument(value);
|
|
58
|
+
if (normalized === null || typeof normalized !== 'object') {
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
61
|
+
// Handle arrays
|
|
62
|
+
if (Array.isArray(normalized)) {
|
|
63
|
+
return normalized.map((item) => this.sanitizeRecursively(item));
|
|
64
|
+
}
|
|
65
|
+
// Handle Set → array
|
|
66
|
+
if (normalized instanceof Set) {
|
|
67
|
+
return Array.from(normalized).map((item) => this.sanitizeRecursively(item));
|
|
68
|
+
}
|
|
69
|
+
// Handle Map → plain object
|
|
70
|
+
if (normalized instanceof Map) {
|
|
71
|
+
const obj = {};
|
|
72
|
+
for (const [k, v] of normalized.entries()) {
|
|
73
|
+
obj[String(k)] = this.sanitizeRecursively(v);
|
|
74
|
+
}
|
|
75
|
+
return obj;
|
|
76
|
+
}
|
|
77
|
+
// Handle plain objects
|
|
78
|
+
if (this.isPlainObject(normalized)) {
|
|
79
|
+
const forbidden = this.ensureKeySet(this.keys);
|
|
80
|
+
const result = {};
|
|
81
|
+
for (const [prop, nested] of Object.entries(normalized)) {
|
|
82
|
+
if (forbidden.has(prop))
|
|
83
|
+
continue;
|
|
84
|
+
result[prop] = this.sanitizeRecursively(nested);
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
88
|
+
// Other complex types (Date, Buffer, etc.) → leave unchanged
|
|
89
|
+
return normalized;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Converts a Mongoose document to a plain object if needed.
|
|
93
|
+
*
|
|
94
|
+
* @param value - Possible Mongoose document.
|
|
95
|
+
* @returns A plain object if conversion is possible, otherwise the value itself.
|
|
96
|
+
*/
|
|
97
|
+
normalizeIfMongooseDocument(value) {
|
|
98
|
+
// Heuristic: Mongoose documents expose `.toObject()` and an internal `$__`.
|
|
99
|
+
// We avoid importing `mongoose` to keep this interceptor framework-agnostic.
|
|
100
|
+
const maybeDoc = value;
|
|
101
|
+
if (maybeDoc &&
|
|
102
|
+
typeof maybeDoc === 'object' &&
|
|
103
|
+
(typeof maybeDoc.toObject === 'function' || typeof maybeDoc.toJSON === 'function') &&
|
|
104
|
+
'$__' in maybeDoc) {
|
|
105
|
+
try {
|
|
106
|
+
if (typeof maybeDoc.toObject === 'function')
|
|
107
|
+
return maybeDoc.toObject(this.mongooseToObjectOptions);
|
|
108
|
+
if (typeof maybeDoc.toJSON === 'function')
|
|
109
|
+
return maybeDoc.toJSON(this.mongooseToObjectOptions);
|
|
110
|
+
}
|
|
111
|
+
catch (_a) {
|
|
112
|
+
// If normalization fails, fall back to the original value—better to
|
|
113
|
+
// return data than to throw at the last step.
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return value;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Checks whether a value is a plain object (and not a class instance, Date, etc.).
|
|
121
|
+
*
|
|
122
|
+
* @param value - Any value.
|
|
123
|
+
* @returns True if the value is a plain object.
|
|
124
|
+
*/
|
|
125
|
+
isPlainObject(value) {
|
|
126
|
+
return Object.prototype.toString.call(value) === '[object Object]';
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Converts the user-provided keys into a Set for O(1) lookup.
|
|
130
|
+
*
|
|
131
|
+
* @param keys - String or array of strings.
|
|
132
|
+
* @returns A Set of keys.
|
|
133
|
+
*/
|
|
134
|
+
ensureKeySet(keys) {
|
|
135
|
+
return Array.isArray(keys) ? new Set(keys) : new Set([keys]);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
exports.AttributeSanitizerInterceptor = AttributeSanitizerInterceptor;
|
|
139
|
+
exports.AttributeSanitizerInterceptor = AttributeSanitizerInterceptor = __decorate([
|
|
140
|
+
(0, common_1.Injectable)(),
|
|
141
|
+
__metadata("design:paramtypes", [Object, Object])
|
|
142
|
+
], AttributeSanitizerInterceptor);
|
|
143
|
+
/**
|
|
144
|
+
* @UseAttributeSanitizer
|
|
145
|
+
*
|
|
146
|
+
* Convenience decorator that applies the AttributeSanitizerInterceptor
|
|
147
|
+
* to a route or controller.
|
|
148
|
+
*
|
|
149
|
+
* @param keys - One or more keys to remove from all response objects.
|
|
150
|
+
* @param mongooseToObjectOptions - Options for Mongoose `.toObject()` normalization.
|
|
151
|
+
*/
|
|
152
|
+
function UseAttributeSanitizer(keys, mongooseToObjectOptions) {
|
|
153
|
+
return (0, common_1.UseInterceptors)(new AttributeSanitizerInterceptor(keys, mongooseToObjectOptions));
|
|
154
|
+
}
|