@lenne.tech/nest-server 11.1.14 → 11.3.0
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/unified-field.decorator.d.ts +2 -0
- package/dist/core/common/decorators/unified-field.decorator.js +26 -9
- package/dist/core/common/decorators/unified-field.decorator.js.map +1 -1
- package/dist/core/common/helpers/gridfs.helper.d.ts +42 -0
- package/dist/core/common/helpers/gridfs.helper.js +107 -0
- package/dist/core/common/helpers/gridfs.helper.js.map +1 -0
- package/dist/core/common/models/core-persistence.model.js +2 -2
- package/dist/core/common/models/core-persistence.model.js.map +1 -1
- package/dist/core/common/services/brevo.service.d.ts +1 -0
- package/dist/core/common/services/brevo.service.js +19 -18
- package/dist/core/common/services/brevo.service.js.map +1 -1
- package/dist/core/common/services/crud.service.js +1 -1
- package/dist/core/common/services/crud.service.js.map +1 -1
- package/dist/core/modules/file/core-file-info.model.js +41 -23
- package/dist/core/modules/file/core-file-info.model.js.map +1 -1
- package/dist/core/modules/file/core-file.controller.d.ts +2 -1
- package/dist/core/modules/file/core-file.controller.js +1 -1
- package/dist/core/modules/file/core-file.controller.js.map +1 -1
- package/dist/core/modules/file/core-file.service.d.ts +7 -7
- package/dist/core/modules/file/core-file.service.js +28 -51
- package/dist/core/modules/file/core-file.service.js.map +1 -1
- package/dist/core/modules/file/interfaces/file-upload.interface.d.ts +1 -1
- package/dist/core/modules/user/core-user.model.js +95 -54
- package/dist/core/modules/user/core-user.model.js.map +1 -1
- package/dist/core/modules/user/core-user.service.js +2 -3
- package/dist/core/modules/user/core-user.service.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/main.js +22 -0
- package/dist/main.js.map +1 -1
- package/dist/server/common/models/persistence.model.js +13 -11
- package/dist/server/common/models/persistence.model.js.map +1 -1
- package/dist/server/modules/auth/auth.model.js +6 -2
- package/dist/server/modules/auth/auth.model.js.map +1 -1
- package/dist/server/modules/file/file-info.model.d.ts +7 -3
- package/dist/server/modules/file/file.controller.d.ts +6 -4
- package/dist/server/modules/file/file.controller.js +15 -4
- package/dist/server/modules/file/file.controller.js.map +1 -1
- package/dist/server/modules/file/file.resolver.js +1 -1
- package/dist/server/modules/file/file.resolver.js.map +1 -1
- package/dist/server/modules/file/multer-config.service.d.ts +0 -2
- package/dist/server/modules/file/multer-config.service.js +3 -22
- package/dist/server/modules/file/multer-config.service.js.map +1 -1
- package/dist/server/modules/user/user.controller.d.ts +19 -0
- package/dist/server/modules/user/user.controller.js +256 -0
- package/dist/server/modules/user/user.controller.js.map +1 -0
- package/dist/server/modules/user/user.model.d.ts +7 -3
- package/dist/server/modules/user/user.model.js +37 -24
- package/dist/server/modules/user/user.model.js.map +1 -1
- package/dist/server/modules/user/user.module.js +2 -1
- package/dist/server/modules/user/user.module.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/dist/types/graphql-upload.d.ts +25 -0
- package/package.json +41 -44
- package/src/core/common/decorators/unified-field.decorator.ts +49 -10
- package/src/core/common/helpers/gridfs.helper.ts +227 -0
- package/src/core/common/models/core-persistence.model.ts +3 -3
- package/src/core/common/services/brevo.service.ts +20 -18
- package/src/core/common/services/crud.service.ts +3 -3
- package/src/core/modules/file/core-file-info.model.ts +40 -22
- package/src/core/modules/file/core-file.controller.ts +3 -2
- package/src/core/modules/file/core-file.service.ts +49 -60
- package/src/core/modules/file/interfaces/file-upload.interface.ts +2 -1
- package/src/core/modules/user/core-user.model.ts +120 -78
- package/src/core/modules/user/core-user.service.ts +3 -3
- package/src/index.ts +1 -0
- package/src/main.ts +25 -0
- package/src/server/common/models/persistence.model.ts +15 -13
- package/src/server/modules/auth/auth.model.ts +7 -3
- package/src/server/modules/file/file.controller.ts +25 -7
- package/src/server/modules/file/file.resolver.ts +1 -2
- package/src/server/modules/file/multer-config.service.ts +6 -21
- package/src/server/modules/user/user.controller.ts +242 -0
- package/src/server/modules/user/user.model.ts +39 -26
- package/src/server/modules/user/user.module.ts +2 -1
- package/src/types/graphql-upload.d.ts +25 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for graphql-upload
|
|
3
|
+
* graphql-upload uses deep imports, so we declare types for the specific modules
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare module 'graphql-upload/graphqlUploadExpress.js' {
|
|
7
|
+
import { RequestHandler } from 'express';
|
|
8
|
+
|
|
9
|
+
interface GraphQLUploadOptions {
|
|
10
|
+
maxFiles?: number;
|
|
11
|
+
maxFileSize?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function graphqlUploadExpress(options?: GraphQLUploadOptions): RequestHandler;
|
|
15
|
+
|
|
16
|
+
export = graphqlUploadExpress;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare module 'graphql-upload/GraphQLUpload.js' {
|
|
20
|
+
import { GraphQLScalarType } from 'graphql';
|
|
21
|
+
|
|
22
|
+
const GraphQLUpload: GraphQLScalarType;
|
|
23
|
+
|
|
24
|
+
export = GraphQLUpload;
|
|
25
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.3.0",
|
|
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",
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
"homepage": "https://github.com/lenneTech/nest-server",
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"scripts": {
|
|
17
|
-
"build": "rimraf dist && nest build",
|
|
17
|
+
"build": "rimraf dist && nest build && npm run build:copy-types && npm run build:add-type-references",
|
|
18
|
+
"build:copy-types": "mkdir -p dist/types && cp src/types/*.d.ts dist/types/",
|
|
19
|
+
"build:add-type-references": "node scripts/add-type-references.js",
|
|
18
20
|
"build:pack": "npm pack && echo 'use file:/ROOT_PATH_TO_TGZ_FILE to integrate the package'",
|
|
19
21
|
"build:dev": "npm run build && yalc push --private",
|
|
20
22
|
"docs": "npm run docs:ci && open http://127.0.0.1:8080/ && open ./public/index.html && compodoc -p tsconfig.json -s ",
|
|
@@ -65,79 +67,74 @@
|
|
|
65
67
|
"node": ">= 20"
|
|
66
68
|
},
|
|
67
69
|
"dependencies": {
|
|
68
|
-
"@apollo/gateway": "2.
|
|
69
|
-
"@getbrevo/brevo": "
|
|
70
|
-
"@lenne.tech/mongoose-gridfs": "1.4.2",
|
|
71
|
-
"@lenne.tech/multer-gridfs-storage": "5.0.8",
|
|
70
|
+
"@apollo/gateway": "2.12.0",
|
|
71
|
+
"@getbrevo/brevo": "3.0.1",
|
|
72
72
|
"@nestjs/apollo": "13.1.0",
|
|
73
|
-
"@nestjs/common": "11.1.
|
|
74
|
-
"@nestjs/core": "11.1.
|
|
73
|
+
"@nestjs/common": "11.1.8",
|
|
74
|
+
"@nestjs/core": "11.1.8",
|
|
75
75
|
"@nestjs/graphql": "13.1.0",
|
|
76
|
-
"@nestjs/jwt": "11.0.
|
|
76
|
+
"@nestjs/jwt": "11.0.1",
|
|
77
77
|
"@nestjs/mongoose": "11.0.3",
|
|
78
78
|
"@nestjs/passport": "11.0.5",
|
|
79
|
-
"@nestjs/platform-express": "11.1.
|
|
79
|
+
"@nestjs/platform-express": "11.1.8",
|
|
80
80
|
"@nestjs/schedule": "6.0.1",
|
|
81
|
-
"@nestjs/swagger": "11.2.
|
|
81
|
+
"@nestjs/swagger": "11.2.1",
|
|
82
82
|
"@nestjs/terminus": "11.0.0",
|
|
83
83
|
"apollo-server-core": "3.13.0",
|
|
84
84
|
"apollo-server-express": "3.13.0",
|
|
85
|
-
"bcrypt": "
|
|
85
|
+
"bcrypt": "6.0.0",
|
|
86
86
|
"class-transformer": "0.5.1",
|
|
87
87
|
"class-validator": "0.14.2",
|
|
88
88
|
"compression": "1.8.1",
|
|
89
89
|
"cookie-parser": "1.4.7",
|
|
90
90
|
"dotenv": "17.2.3",
|
|
91
91
|
"ejs": "3.1.10",
|
|
92
|
-
"graphql": "16.
|
|
92
|
+
"graphql": "16.12.0",
|
|
93
93
|
"graphql-query-complexity": "1.1.0",
|
|
94
94
|
"graphql-subscriptions": "3.0.0",
|
|
95
95
|
"graphql-upload": "15.0.2",
|
|
96
96
|
"js-sha256": "0.11.1",
|
|
97
97
|
"json-to-graphql-query": "2.3.0",
|
|
98
|
-
"light-my-request": "6.6.0",
|
|
99
98
|
"lodash": "4.17.21",
|
|
100
|
-
"mongodb": "
|
|
101
|
-
"mongoose": "
|
|
102
|
-
"multer": "
|
|
103
|
-
"node-mailjet": "6.0.
|
|
104
|
-
"nodemailer": "7.0.
|
|
105
|
-
"nodemon": "3.1.10",
|
|
99
|
+
"mongodb": "7.0.0",
|
|
100
|
+
"mongoose": "8.19.3",
|
|
101
|
+
"multer": "2.0.2",
|
|
102
|
+
"node-mailjet": "6.0.11",
|
|
103
|
+
"nodemailer": "7.0.10",
|
|
106
104
|
"passport": "0.7.0",
|
|
107
105
|
"passport-jwt": "4.0.1",
|
|
108
106
|
"reflect-metadata": "0.2.2",
|
|
109
107
|
"rfdc": "1.4.1",
|
|
110
|
-
"rimraf": "6.0.1",
|
|
111
108
|
"rxjs": "7.8.2",
|
|
112
109
|
"yuml-diagram": "1.2.0"
|
|
113
110
|
},
|
|
114
111
|
"devDependencies": {
|
|
115
112
|
"@babel/plugin-proposal-private-methods": "7.18.6",
|
|
116
|
-
"@compodoc/compodoc": "1.1.
|
|
117
|
-
"@lenne.tech/eslint-config-ts": "2.1.
|
|
113
|
+
"@compodoc/compodoc": "1.1.32",
|
|
114
|
+
"@lenne.tech/eslint-config-ts": "2.1.4",
|
|
118
115
|
"@nestjs/cli": "11.0.10",
|
|
119
|
-
"@nestjs/schematics": "11.0.
|
|
120
|
-
"@nestjs/testing": "11.1.
|
|
121
|
-
"@swc/cli": "0.7.
|
|
122
|
-
"@swc/core": "1.
|
|
116
|
+
"@nestjs/schematics": "11.0.9",
|
|
117
|
+
"@nestjs/testing": "11.1.8",
|
|
118
|
+
"@swc/cli": "0.7.9",
|
|
119
|
+
"@swc/core": "1.15.0",
|
|
123
120
|
"@swc/jest": "0.2.39",
|
|
124
121
|
"@types/compression": "1.8.1",
|
|
125
|
-
"@types/cookie-parser": "1.4.
|
|
122
|
+
"@types/cookie-parser": "1.4.10",
|
|
126
123
|
"@types/ejs": "3.1.5",
|
|
127
124
|
"@types/express": "4.17.21",
|
|
128
|
-
"@types/jest": "
|
|
125
|
+
"@types/jest": "30.0.0",
|
|
129
126
|
"@types/lodash": "4.17.20",
|
|
130
|
-
"@types/multer": "
|
|
131
|
-
"@types/node": "
|
|
132
|
-
"@types/nodemailer": "
|
|
127
|
+
"@types/multer": "2.0.0",
|
|
128
|
+
"@types/node": "24.10.0",
|
|
129
|
+
"@types/nodemailer": "7.0.3",
|
|
133
130
|
"@types/passport": "1.0.17",
|
|
134
131
|
"@types/supertest": "6.0.3",
|
|
135
|
-
"@typescript-eslint/eslint-plugin": "8.46.
|
|
136
|
-
"@typescript-eslint/parser": "8.46.
|
|
132
|
+
"@typescript-eslint/eslint-plugin": "8.46.3",
|
|
133
|
+
"@typescript-eslint/parser": "8.46.3",
|
|
137
134
|
"coffeescript": "2.7.0",
|
|
138
|
-
"eslint": "9.
|
|
135
|
+
"eslint": "9.39.1",
|
|
139
136
|
"eslint-config-prettier": "10.1.8",
|
|
140
|
-
"eslint-plugin-unused-imports": "4.
|
|
137
|
+
"eslint-plugin-unused-imports": "4.3.0",
|
|
141
138
|
"find-file-up": "2.0.1",
|
|
142
139
|
"grunt": "1.6.1",
|
|
143
140
|
"grunt-bg-shell": "2.3.3",
|
|
@@ -145,28 +142,28 @@
|
|
|
145
142
|
"grunt-contrib-watch": "1.1.0",
|
|
146
143
|
"grunt-sync": "0.8.2",
|
|
147
144
|
"husky": "9.1.7",
|
|
148
|
-
"jest": "
|
|
145
|
+
"jest": "30.2.0",
|
|
146
|
+
"nodemon": "3.1.10",
|
|
149
147
|
"npm-watch": "0.13.0",
|
|
150
148
|
"pm2": "6.0.13",
|
|
151
149
|
"prettier": "3.6.2",
|
|
152
150
|
"pretty-quick": "4.2.2",
|
|
151
|
+
"rimraf": "6.1.0",
|
|
153
152
|
"supertest": "7.1.4",
|
|
154
|
-
"ts-jest": "29.4.
|
|
153
|
+
"ts-jest": "29.4.5",
|
|
155
154
|
"ts-loader": "9.5.4",
|
|
156
|
-
"ts-morph": "
|
|
155
|
+
"ts-morph": "27.0.2",
|
|
157
156
|
"ts-node": "10.9.2",
|
|
158
157
|
"tsconfig-paths": "4.2.0",
|
|
159
|
-
"typescript": "5.
|
|
158
|
+
"typescript": "5.9.3",
|
|
160
159
|
"yalc": "1.0.0-pre.53"
|
|
161
160
|
},
|
|
162
161
|
"overrides": {
|
|
163
|
-
"multer-gridfs-storage": {
|
|
164
|
-
"multer": "$multer"
|
|
165
|
-
},
|
|
166
162
|
"@lykmapipo/common": {
|
|
167
163
|
"flat": "5.0.2",
|
|
168
164
|
"mime": "2.6.0"
|
|
169
|
-
}
|
|
165
|
+
},
|
|
166
|
+
"ts-morph": "27.0.2"
|
|
170
167
|
},
|
|
171
168
|
"jest": {
|
|
172
169
|
"collectCoverage": true,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Field, FieldOptions } from '@nestjs/graphql';
|
|
2
|
+
import { Prop, PropOptions } from '@nestjs/mongoose';
|
|
2
3
|
import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger';
|
|
3
4
|
import { EnumAllowedTypes } from '@nestjs/swagger/dist/interfaces/schema-object-metadata.interface';
|
|
4
5
|
import { Type } from 'class-transformer';
|
|
@@ -38,6 +39,8 @@ export interface UnifiedFieldOptions {
|
|
|
38
39
|
isArray?: boolean;
|
|
39
40
|
/** Default: false */
|
|
40
41
|
isOptional?: boolean;
|
|
42
|
+
/** Whether to apply Mongoose @Prop decorator. Optional, used for database models. Default: false */
|
|
43
|
+
mongoose?: boolean | PropOptions;
|
|
41
44
|
/** Restricted roles */
|
|
42
45
|
roles?: RestrictedType | RoleEnum | RoleEnum[];
|
|
43
46
|
/** Options for swagger api documentation */
|
|
@@ -48,8 +51,12 @@ export interface UnifiedFieldOptions {
|
|
|
48
51
|
*
|
|
49
52
|
* Enums should be defined via the enum option.
|
|
50
53
|
*
|
|
54
|
+
* For array fields, you can use either:
|
|
55
|
+
* - `type: () => ItemType` (recommended, decorator adds array wrapping automatically)
|
|
56
|
+
* - `type: () => [ItemType]` (also supported, decorator extracts ItemType to avoid double-nesting)
|
|
57
|
+
*
|
|
51
58
|
* Supports:
|
|
52
|
-
* - A factory function that returns the type: `() => MyType`
|
|
59
|
+
* - A factory function that returns the type: `() => MyType` or `() => [MyType]`
|
|
53
60
|
* - A GraphQL scalar: `GraphQLScalarType`
|
|
54
61
|
* - A class constructor: `MyClass`
|
|
55
62
|
* */
|
|
@@ -96,7 +103,16 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
|
|
|
96
103
|
return metadataType;
|
|
97
104
|
};
|
|
98
105
|
|
|
99
|
-
|
|
106
|
+
// Resolve the type and extract item type if user provided array syntax
|
|
107
|
+
const resolvedType = resolvedTypeFn();
|
|
108
|
+
|
|
109
|
+
// If this is an array field and the user provided type: () => [ItemType],
|
|
110
|
+
// extract the ItemType to avoid double-nesting (e.g., [[ItemType]])
|
|
111
|
+
// We check: isArrayField (should be array) && Array.isArray (is actually an array) && length === 1 (GraphQL array syntax)
|
|
112
|
+
const baseType =
|
|
113
|
+
isArrayField && Array.isArray(resolvedType) && resolvedType.length === 1
|
|
114
|
+
? resolvedType[0] // Extract item type from [ItemType]
|
|
115
|
+
: resolvedType; // Use as-is
|
|
100
116
|
|
|
101
117
|
// Prepare merged options
|
|
102
118
|
const gqlOpts: FieldOptions = { ...opts.gqlOptions };
|
|
@@ -161,8 +177,14 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
|
|
|
161
177
|
}
|
|
162
178
|
|
|
163
179
|
// Type function for gql
|
|
180
|
+
// We need to keep the factory pattern (calling resolvedTypeFn inside the arrow function)
|
|
181
|
+
// to support circular references. But we also need to extract array item types to avoid double-nesting.
|
|
164
182
|
const gqlTypeFn = isArrayField
|
|
165
|
-
? () =>
|
|
183
|
+
? () => {
|
|
184
|
+
const resolved = opts.enum?.enum || opts.gqlType || resolvedTypeFn();
|
|
185
|
+
// Extract item type if user provided [ItemType] syntax to avoid [[ItemType]]
|
|
186
|
+
return [Array.isArray(resolved) && resolved.length === 1 ? resolved[0] : resolved];
|
|
187
|
+
}
|
|
166
188
|
: () => opts.enum?.enum || opts.gqlType || resolvedTypeFn();
|
|
167
189
|
|
|
168
190
|
// Gql decorator
|
|
@@ -178,7 +200,7 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
|
|
|
178
200
|
|
|
179
201
|
// Completely skip validation if its any
|
|
180
202
|
if (opts.validator) {
|
|
181
|
-
opts.validator(valOpts).forEach(d => d(target, propertyKey));
|
|
203
|
+
opts.validator(valOpts).forEach((d) => d(target, propertyKey));
|
|
182
204
|
} else if (!opts.isAny) {
|
|
183
205
|
const validator = getBuiltInValidator(baseType, valOpts, isArrayField, target);
|
|
184
206
|
if (validator) {
|
|
@@ -199,6 +221,23 @@ export function UnifiedField(opts: UnifiedFieldOptions = {}): PropertyDecorator
|
|
|
199
221
|
const rolesArr = Array.isArray(opts.roles) ? opts.roles : [opts.roles];
|
|
200
222
|
Restricted(...rolesArr)(target, propertyKey);
|
|
201
223
|
}
|
|
224
|
+
|
|
225
|
+
// Mongoose @Prop decorator (optional)
|
|
226
|
+
if (opts.mongoose) {
|
|
227
|
+
const propOptions: any = typeof opts.mongoose === 'object' ? opts.mongoose : {};
|
|
228
|
+
|
|
229
|
+
// Set type for Prop if not already defined in propOptions
|
|
230
|
+
if (typeof propOptions === 'object' && !Array.isArray(propOptions) && !propOptions.type && baseType) {
|
|
231
|
+
propOptions.type = baseType;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Apply array type if needed
|
|
235
|
+
if (typeof propOptions === 'object' && !Array.isArray(propOptions) && isArrayField && !propOptions.type) {
|
|
236
|
+
propOptions.type = [baseType];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
Prop(propOptions)(target, propertyKey);
|
|
240
|
+
}
|
|
202
241
|
};
|
|
203
242
|
}
|
|
204
243
|
|
|
@@ -228,12 +267,12 @@ function getBuiltInValidator(
|
|
|
228
267
|
function isGraphQLScalar(type: any): boolean {
|
|
229
268
|
// CustomScalar check (The CustomScalar interface implements these functions below)
|
|
230
269
|
return (
|
|
231
|
-
(type
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
270
|
+
(type &&
|
|
271
|
+
typeof type === 'function' &&
|
|
272
|
+
typeof type.prototype?.serialize === 'function' &&
|
|
273
|
+
typeof type.prototype?.parseValue === 'function' &&
|
|
274
|
+
typeof type.prototype?.parseLiteral === 'function') ||
|
|
275
|
+
type instanceof GraphQLScalarType
|
|
237
276
|
);
|
|
238
277
|
}
|
|
239
278
|
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { mongo, Types } from 'mongoose';
|
|
2
|
+
import { Readable } from 'stream';
|
|
3
|
+
|
|
4
|
+
// Use Mongoose's MongoDB types to avoid BSON version conflicts
|
|
5
|
+
const ObjectId = Types.ObjectId;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GridFS File Info interface matching the structure from GridFS
|
|
9
|
+
* This is the normalized structure with contentType at root level
|
|
10
|
+
*/
|
|
11
|
+
export interface GridFSFileInfo {
|
|
12
|
+
_id: Types.ObjectId;
|
|
13
|
+
chunkSize: number;
|
|
14
|
+
/**
|
|
15
|
+
* Content type of the file
|
|
16
|
+
* Note: Stored in metadata.contentType in MongoDB, normalized to root level by helper
|
|
17
|
+
*/
|
|
18
|
+
contentType?: string;
|
|
19
|
+
filename: string;
|
|
20
|
+
length: number;
|
|
21
|
+
metadata?: Record<string, any> & { contentType?: string };
|
|
22
|
+
uploadDate: Date;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Options for reading files from GridFS
|
|
27
|
+
*/
|
|
28
|
+
export interface GridFSReadOptions {
|
|
29
|
+
_id?: string | Types.ObjectId;
|
|
30
|
+
filename?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Options for writing files to GridFS
|
|
34
|
+
*/
|
|
35
|
+
export interface GridFSWriteOptions {
|
|
36
|
+
contentType?: string;
|
|
37
|
+
filename: string;
|
|
38
|
+
metadata?: Record<string, any>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type GridFSBucket = mongo.GridFSBucket;
|
|
42
|
+
|
|
43
|
+
type GridFSBucketReadStream = mongo.GridFSBucketReadStream;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Raw GridFS file structure as returned by MongoDB
|
|
47
|
+
* This is the internal structure before normalization
|
|
48
|
+
*/
|
|
49
|
+
interface RawGridFSFile {
|
|
50
|
+
_id: Types.ObjectId;
|
|
51
|
+
chunkSize: number;
|
|
52
|
+
filename: string;
|
|
53
|
+
length: number;
|
|
54
|
+
metadata?: Record<string, any> & { contentType?: string };
|
|
55
|
+
uploadDate: Date;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Helper class for GridFS operations using native MongoDB driver
|
|
60
|
+
* Provides Promise-based API for all GridFS operations
|
|
61
|
+
*/
|
|
62
|
+
export class GridFSHelper {
|
|
63
|
+
/**
|
|
64
|
+
* Normalize file info to ensure contentType is accessible at root level
|
|
65
|
+
* MongoDB stores contentType in metadata, but our API expects it at root
|
|
66
|
+
*/
|
|
67
|
+
private static normalizeFileInfo(fileInfo: RawGridFSFile): GridFSFileInfo {
|
|
68
|
+
return {
|
|
69
|
+
...fileInfo,
|
|
70
|
+
contentType: fileInfo.metadata?.contentType,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Write a file to GridFS from a stream
|
|
76
|
+
*/
|
|
77
|
+
static writeFileFromStream(
|
|
78
|
+
bucket: GridFSBucket,
|
|
79
|
+
stream: Readable,
|
|
80
|
+
options: GridFSWriteOptions,
|
|
81
|
+
): Promise<GridFSFileInfo> {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
// Store contentType in metadata to avoid deprecation warning
|
|
84
|
+
const metadata = { ...options.metadata };
|
|
85
|
+
if (options.contentType) {
|
|
86
|
+
metadata.contentType = options.contentType;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const uploadStream = bucket.openUploadStream(options.filename, {
|
|
90
|
+
metadata,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
uploadStream.on('error', (error) => {
|
|
94
|
+
reject(error);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
uploadStream.on('finish', () => {
|
|
98
|
+
// Fetch the file info after upload completes
|
|
99
|
+
bucket
|
|
100
|
+
.find({ _id: uploadStream.id })
|
|
101
|
+
.toArray()
|
|
102
|
+
.then((files) => {
|
|
103
|
+
if (files && files.length > 0) {
|
|
104
|
+
resolve(GridFSHelper.normalizeFileInfo(files[0]));
|
|
105
|
+
} else {
|
|
106
|
+
reject(new Error('File uploaded but metadata not found'));
|
|
107
|
+
}
|
|
108
|
+
})
|
|
109
|
+
.catch(reject);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
stream.pipe(uploadStream);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Read a file from GridFS to a buffer
|
|
118
|
+
*/
|
|
119
|
+
static readFileToBuffer(bucket: GridFSBucket, options: GridFSReadOptions): Promise<Buffer> {
|
|
120
|
+
return new Promise((resolve, reject) => {
|
|
121
|
+
const chunks: Buffer[] = [];
|
|
122
|
+
let downloadStream: GridFSBucketReadStream;
|
|
123
|
+
|
|
124
|
+
if (options._id) {
|
|
125
|
+
const objectId = typeof options._id === 'string' ? new ObjectId(options._id) : options._id;
|
|
126
|
+
downloadStream = bucket.openDownloadStream(objectId);
|
|
127
|
+
} else if (options.filename) {
|
|
128
|
+
downloadStream = bucket.openDownloadStreamByName(options.filename);
|
|
129
|
+
} else {
|
|
130
|
+
return reject(new Error('Either _id or filename must be provided'));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
downloadStream.on('data', (chunk) => {
|
|
134
|
+
chunks.push(chunk);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
downloadStream.on('error', (error) => {
|
|
138
|
+
reject(error);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
downloadStream.on('end', () => {
|
|
142
|
+
resolve(Buffer.concat(chunks));
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Find file metadata by ID
|
|
149
|
+
*/
|
|
150
|
+
static async findFileById(bucket: GridFSBucket, id: string | Types.ObjectId): Promise<GridFSFileInfo | null> {
|
|
151
|
+
const objectId = typeof id === 'string' ? new ObjectId(id) : id;
|
|
152
|
+
const files = await bucket.find({ _id: objectId }).toArray();
|
|
153
|
+
return files.length > 0 ? GridFSHelper.normalizeFileInfo(files[0]) : null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Find file metadata by filename
|
|
158
|
+
*/
|
|
159
|
+
static async findFileByName(bucket: GridFSBucket, filename: string): Promise<GridFSFileInfo | null> {
|
|
160
|
+
const files = await bucket.find({ filename }).toArray();
|
|
161
|
+
return files.length > 0 ? GridFSHelper.normalizeFileInfo(files[0]) : null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Find files with filter and options
|
|
166
|
+
*/
|
|
167
|
+
static async findFiles(bucket: GridFSBucket, filter: any = {}, options: any = {}): Promise<GridFSFileInfo[]> {
|
|
168
|
+
const files = await bucket.find(filter, options).toArray();
|
|
169
|
+
return files.map(file => GridFSHelper.normalizeFileInfo(file));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Delete a file from GridFS
|
|
174
|
+
*/
|
|
175
|
+
static async deleteFile(bucket: GridFSBucket, id: string | Types.ObjectId): Promise<void> {
|
|
176
|
+
const objectId = typeof id === 'string' ? new ObjectId(id) : id;
|
|
177
|
+
await bucket.delete(objectId);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Open download stream by ID
|
|
182
|
+
*/
|
|
183
|
+
static openDownloadStream(bucket: GridFSBucket, id: string | Types.ObjectId): GridFSBucketReadStream {
|
|
184
|
+
const objectId = typeof id === 'string' ? new ObjectId(id) : id;
|
|
185
|
+
return bucket.openDownloadStream(objectId);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Open download stream by name
|
|
190
|
+
*/
|
|
191
|
+
static openDownloadStreamByName(bucket: GridFSBucket, filename: string): GridFSBucketReadStream {
|
|
192
|
+
return bucket.openDownloadStreamByName(filename);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Open upload stream
|
|
197
|
+
*/
|
|
198
|
+
static openUploadStream(
|
|
199
|
+
bucket: GridFSBucket,
|
|
200
|
+
filename: string,
|
|
201
|
+
options?: { contentType?: string },
|
|
202
|
+
): mongo.GridFSBucketWriteStream {
|
|
203
|
+
// Store contentType in metadata to avoid deprecation warning
|
|
204
|
+
if (options?.contentType) {
|
|
205
|
+
const metadata = { contentType: options.contentType };
|
|
206
|
+
return bucket.openUploadStream(filename, { metadata });
|
|
207
|
+
}
|
|
208
|
+
return bucket.openUploadStream(filename, options);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Open upload stream with specific ID
|
|
213
|
+
*/
|
|
214
|
+
static openUploadStreamWithId(
|
|
215
|
+
bucket: GridFSBucket,
|
|
216
|
+
id: Types.ObjectId,
|
|
217
|
+
filename: string,
|
|
218
|
+
options?: { contentType?: string },
|
|
219
|
+
): mongo.GridFSBucketWriteStream {
|
|
220
|
+
// Store contentType in metadata to avoid deprecation warning
|
|
221
|
+
if (options?.contentType) {
|
|
222
|
+
const metadata = { contentType: options.contentType };
|
|
223
|
+
return bucket.openUploadStreamWithId(id, filename, { metadata });
|
|
224
|
+
}
|
|
225
|
+
return bucket.openUploadStreamWithId(id, filename, options);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ObjectType } from '@nestjs/graphql';
|
|
2
|
-
import {
|
|
2
|
+
import { Schema } from '@nestjs/mongoose';
|
|
3
3
|
import { Types } from 'mongoose';
|
|
4
4
|
|
|
5
5
|
import { Restricted } from '../decorators/restricted.decorator';
|
|
@@ -85,10 +85,10 @@ export abstract class CorePersistenceModel extends CoreModel {
|
|
|
85
85
|
/**
|
|
86
86
|
* Created date, is set automatically by mongoose
|
|
87
87
|
*/
|
|
88
|
-
@Prop()
|
|
89
88
|
@UnifiedField({
|
|
90
89
|
description: 'Created date',
|
|
91
90
|
isOptional: true,
|
|
91
|
+
mongoose: true,
|
|
92
92
|
roles: RoleEnum.S_EVERYONE,
|
|
93
93
|
swaggerApiOptions: { example: 1740037703939, format: 'int64', type: Date },
|
|
94
94
|
type: Date,
|
|
@@ -98,10 +98,10 @@ export abstract class CorePersistenceModel extends CoreModel {
|
|
|
98
98
|
/**
|
|
99
99
|
* Updated date is set automatically by mongoose
|
|
100
100
|
*/
|
|
101
|
-
@Prop()
|
|
102
101
|
@UnifiedField({
|
|
103
102
|
description: 'Updated date',
|
|
104
103
|
isOptional: true,
|
|
104
|
+
mongoose: true,
|
|
105
105
|
roles: RoleEnum.S_EVERYONE,
|
|
106
106
|
swaggerApiOptions: { example: 1740037703939, format: 'int64', type: Date },
|
|
107
107
|
type: Date,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { SendSmtpEmail, TransactionalEmailsApi, TransactionalEmailsApiApiKeys } from '@getbrevo/brevo';
|
|
2
2
|
import { Injectable } from '@nestjs/common';
|
|
3
3
|
|
|
4
4
|
import { ConfigService } from './config.service';
|
|
@@ -9,15 +9,15 @@ import { ConfigService } from './config.service';
|
|
|
9
9
|
@Injectable()
|
|
10
10
|
export class BrevoService {
|
|
11
11
|
brevoConfig: ConfigService['configFastButReadOnly']['brevo'];
|
|
12
|
+
private apiInstance: TransactionalEmailsApi;
|
|
12
13
|
|
|
13
14
|
constructor(protected configService: ConfigService) {
|
|
14
|
-
const defaultClient = brevo.ApiClient.instance;
|
|
15
|
-
const apiKey = defaultClient.authentications['api-key'];
|
|
16
15
|
this.brevoConfig = configService.configFastButReadOnly.brevo;
|
|
17
16
|
if (!this.brevoConfig) {
|
|
18
17
|
throw new Error('Brevo configuration not set!');
|
|
19
18
|
}
|
|
20
|
-
|
|
19
|
+
this.apiInstance = new TransactionalEmailsApi();
|
|
20
|
+
this.apiInstance.setApiKey(TransactionalEmailsApiApiKeys.apiKey, this.brevoConfig.apiKey);
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
@@ -37,14 +37,15 @@ export class BrevoService {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// Prepare data
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
const sendSmtpEmail: SendSmtpEmail = {
|
|
41
|
+
params,
|
|
42
|
+
templateId,
|
|
43
|
+
to: [{ email: to }],
|
|
44
|
+
};
|
|
45
45
|
|
|
46
46
|
// Send email
|
|
47
|
-
|
|
47
|
+
const result = await this.apiInstance.sendTransacEmail(sendSmtpEmail);
|
|
48
|
+
return result.body;
|
|
48
49
|
} catch (error) {
|
|
49
50
|
console.error(error);
|
|
50
51
|
}
|
|
@@ -75,16 +76,17 @@ export class BrevoService {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
// Prepare data
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
const sendSmtpEmail: SendSmtpEmail = {
|
|
80
|
+
htmlContent: html,
|
|
81
|
+
params: options?.params,
|
|
82
|
+
sender: this.brevoConfig.sender,
|
|
83
|
+
subject,
|
|
84
|
+
to: [{ email: to }],
|
|
85
|
+
};
|
|
85
86
|
|
|
86
87
|
// Send email
|
|
87
|
-
|
|
88
|
+
const result = await this.apiInstance.sendTransacEmail(sendSmtpEmail);
|
|
89
|
+
return result.body;
|
|
88
90
|
} catch (error) {
|
|
89
91
|
console.error(error);
|
|
90
92
|
}
|
|
@@ -105,7 +105,7 @@ export abstract class CrudService<
|
|
|
105
105
|
* Get distinct values of a property
|
|
106
106
|
*/
|
|
107
107
|
distinct(property: string): Promise<string[]> {
|
|
108
|
-
return this.mainDbModel.distinct(property)
|
|
108
|
+
return this.mainDbModel.distinct(property).exec() as Promise<string[]>;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/**
|
|
@@ -178,7 +178,7 @@ export abstract class CrudService<
|
|
|
178
178
|
find = find.collation(collation);
|
|
179
179
|
}
|
|
180
180
|
if (serviceOptions?.select) {
|
|
181
|
-
find = find.select(serviceOptions.select);
|
|
181
|
+
find = find.select(serviceOptions.select) as any;
|
|
182
182
|
}
|
|
183
183
|
return find.exec();
|
|
184
184
|
},
|
|
@@ -401,7 +401,7 @@ export abstract class CrudService<
|
|
|
401
401
|
find = find.collation(collation);
|
|
402
402
|
}
|
|
403
403
|
if (serviceOptions?.select) {
|
|
404
|
-
find = find.select(serviceOptions.select);
|
|
404
|
+
find = find.select(serviceOptions.select) as any;
|
|
405
405
|
}
|
|
406
406
|
return find.exec();
|
|
407
407
|
},
|