@lenne.tech/nest-server 9.0.0 → 9.0.3

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 (35) hide show
  1. package/dist/core/common/args/pagination.args.js +2 -2
  2. package/dist/core/common/args/pagination.args.js.map +1 -1
  3. package/dist/core/common/helpers/db.helper.d.ts +2 -4
  4. package/dist/core/common/helpers/db.helper.js +18 -8
  5. package/dist/core/common/helpers/db.helper.js.map +1 -1
  6. package/dist/core/common/helpers/filter.helper.d.ts +3 -1
  7. package/dist/core/common/helpers/filter.helper.js +14 -9
  8. package/dist/core/common/helpers/filter.helper.js.map +1 -1
  9. package/dist/core/common/services/core-cron-jobs.service.d.ts +3 -1
  10. package/dist/core/common/services/core-cron-jobs.service.js +5 -0
  11. package/dist/core/common/services/core-cron-jobs.service.js.map +1 -1
  12. package/dist/core/common/services/crud.service.d.ts +7 -0
  13. package/dist/core/common/services/crud.service.js +36 -0
  14. package/dist/core/common/services/crud.service.js.map +1 -1
  15. package/dist/core/common/services/module.service.d.ts +1 -0
  16. package/dist/core/common/services/module.service.js +10 -2
  17. package/dist/core/common/services/module.service.js.map +1 -1
  18. package/dist/server/modules/file/file-info.model.d.ts +2 -1
  19. package/dist/server/modules/user/outputs/find-and-count-users-result.output.d.ts +5 -0
  20. package/dist/server/modules/user/outputs/find-and-count-users-result.output.js +29 -0
  21. package/dist/server/modules/user/outputs/find-and-count-users-result.output.js.map +1 -0
  22. package/dist/server/modules/user/user.model.d.ts +2 -1
  23. package/dist/server/modules/user/user.resolver.d.ts +4 -0
  24. package/dist/server/modules/user/user.resolver.js +16 -0
  25. package/dist/server/modules/user/user.resolver.js.map +1 -1
  26. package/dist/tsconfig.build.tsbuildinfo +1 -1
  27. package/package.json +21 -21
  28. package/src/core/common/args/pagination.args.ts +4 -4
  29. package/src/core/common/helpers/db.helper.ts +16 -8
  30. package/src/core/common/helpers/filter.helper.ts +20 -10
  31. package/src/core/common/services/core-cron-jobs.service.ts +12 -1
  32. package/src/core/common/services/crud.service.ts +64 -1
  33. package/src/core/common/services/module.service.ts +15 -3
  34. package/src/server/modules/user/outputs/find-and-count-users-result.output.ts +11 -0
  35. package/src/server/modules/user/user.resolver.ts +13 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "9.0.0",
3
+ "version": "9.0.3",
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",
@@ -59,17 +59,17 @@
59
59
  },
60
60
  "dependencies": {
61
61
  "@apollo/gateway": "2.0.5",
62
- "@nestjs/apollo": "10.0.17",
63
- "@nestjs/common": "9.0.3",
64
- "@nestjs/core": "9.0.3",
65
- "@nestjs/graphql": "10.0.18",
62
+ "@nestjs/apollo": "10.0.19",
63
+ "@nestjs/common": "9.0.9",
64
+ "@nestjs/core": "9.0.9",
65
+ "@nestjs/graphql": "10.0.21",
66
66
  "@nestjs/jwt": "9.0.0",
67
67
  "@nestjs/mongoose": "9.2.0",
68
68
  "@nestjs/passport": "9.0.0",
69
- "@nestjs/platform-express": "9.0.3",
69
+ "@nestjs/platform-express": "9.0.9",
70
70
  "@nestjs/schedule": "2.1.0",
71
- "apollo-server-core": "3.10.0",
72
- "apollo-server-express": "3.10.0",
71
+ "apollo-server-core": "3.10.1",
72
+ "apollo-server-express": "3.10.1",
73
73
  "bcrypt": "5.0.1",
74
74
  "class-transformer": "0.5.1",
75
75
  "class-validator": "0.13.2",
@@ -79,14 +79,14 @@
79
79
  "graphql-upload": "15.0.2",
80
80
  "js-sha256": "0.9.0",
81
81
  "json-to-graphql-query": "2.2.4",
82
- "light-my-request": "5.1.0",
82
+ "light-my-request": "5.4.0",
83
83
  "lodash": "4.17.21",
84
- "mongodb": "4.8.0",
85
- "mongoose": "6.4.4",
84
+ "mongodb": "4.8.1",
85
+ "mongoose": "6.5.2",
86
86
  "mongoose-gridfs": "1.3.0",
87
87
  "multer": "1.4.5-lts.1",
88
- "node-mailjet": "5.0.1",
89
- "nodemailer": "6.7.7",
88
+ "node-mailjet": "5.1.1",
89
+ "nodemailer": "6.7.8",
90
90
  "nodemon": "2.0.19",
91
91
  "passport": "0.6.0",
92
92
  "passport-jwt": "4.0.0",
@@ -96,20 +96,20 @@
96
96
  "rxjs": "7.5.6"
97
97
  },
98
98
  "devDependencies": {
99
- "@nestjs/testing": "9.0.3",
99
+ "@nestjs/testing": "9.0.9",
100
100
  "@types/cron": "2.0.0",
101
101
  "@types/ejs": "3.1.1",
102
102
  "@types/jest": "28.1.6",
103
103
  "@types/lodash": "4.14.182",
104
104
  "@types/multer": "1.4.7",
105
- "@types/node": "18.0.5",
106
- "@types/nodemailer": "6.4.4",
107
- "@types/passport": "1.0.9",
105
+ "@types/node": "18.7.3",
106
+ "@types/nodemailer": "6.4.5",
107
+ "@types/passport": "1.0.10",
108
108
  "@types/supertest": "2.0.12",
109
- "@typescript-eslint/eslint-plugin": "5.30.6",
110
- "@typescript-eslint/parser": "5.30.6",
109
+ "@typescript-eslint/eslint-plugin": "5.33.0",
110
+ "@typescript-eslint/parser": "5.33.0",
111
111
  "coffeescript": "2.7.0",
112
- "eslint": "8.20.0",
112
+ "eslint": "8.22.0",
113
113
  "eslint-config-prettier": "8.5.0",
114
114
  "find-file-up": "2.0.1",
115
115
  "grunt": "1.5.3",
@@ -126,7 +126,7 @@
126
126
  "ts-jest": "28.0.7",
127
127
  "ts-morph": "15.1.0",
128
128
  "ts-node": "10.9.1",
129
- "tsconfig-paths": "4.0.0",
129
+ "tsconfig-paths": "4.1.0",
130
130
  "typescript": "4.7.4"
131
131
  },
132
132
  "jest": {
@@ -19,10 +19,10 @@ export class PaginationArgs extends CoreInput {
19
19
  limit?: number = undefined;
20
20
 
21
21
  /**
22
- * Offset for pagination
22
+ * Alias for skip
23
23
  */
24
24
  @Field((type) => Int, {
25
- description: 'Offset specifies how many found elements should be skipped on return',
25
+ description: 'Alias for skip',
26
26
  nullable: true,
27
27
  defaultValue: 0,
28
28
  })
@@ -30,10 +30,10 @@ export class PaginationArgs extends CoreInput {
30
30
  offset?: number = undefined;
31
31
 
32
32
  /**
33
- * Alias for offset
33
+ * Skip for pagination
34
34
  */
35
35
  @Field((type) => Int, {
36
- description: 'Alias for offset',
36
+ description: 'Skip specifies how many found elements should be skipped on return',
37
37
  nullable: true,
38
38
  defaultValue: undefined,
39
39
  })
@@ -479,7 +479,7 @@ export async function popAndMap<T extends CoreModel>(
479
479
  }
480
480
  if (queryOrDocument instanceof Query) {
481
481
  // Get result
482
- result = await setPopulates(queryOrDocument, populateOptions, mongooseModel?.schema?.paths);
482
+ result = await setPopulates(queryOrDocument, populateOptions, mongooseModel);
483
483
  if (result instanceof Query) {
484
484
  result = await result.exec();
485
485
  }
@@ -493,13 +493,13 @@ export async function popAndMap<T extends CoreModel>(
493
493
  } else {
494
494
  // Process documents
495
495
  if (Array.isArray(queryOrDocument)) {
496
- await setPopulates(queryOrDocument, populateOptions, mongooseModel?.schema?.paths);
496
+ await setPopulates(queryOrDocument, populateOptions, mongooseModel);
497
497
  result = queryOrDocument.map((item) => (modelClass as any).map(item));
498
498
  }
499
499
 
500
500
  // Process document
501
501
  else {
502
- await setPopulates(queryOrDocument, populateOptions, mongooseModel?.schema?.paths);
502
+ await setPopulates(queryOrDocument, populateOptions, mongooseModel);
503
503
  result = (modelClass as any).map(queryOrDocument);
504
504
  }
505
505
  }
@@ -544,7 +544,7 @@ export function removeIds(source: any[], ids: StringOrObjectId | StringOrObjectI
544
544
  export async function setPopulates<T = Query<any, any> | Document>(
545
545
  queryOrDocument: T,
546
546
  populateOptions: string[] | PopulateOptions[] | (string | PopulateOptions)[],
547
- modelSchemaPaths?: { [key: string]: SchemaType }
547
+ mongooseModel: Model<any>
548
548
  ): Promise<T> {
549
549
  // Check parameters
550
550
  if (!populateOptions?.length || !queryOrDocument) {
@@ -552,13 +552,13 @@ export async function setPopulates<T = Query<any, any> | Document>(
552
552
  }
553
553
 
554
554
  // Filter populate options via model schema paths
555
- if (modelSchemaPaths) {
555
+ if (mongooseModel?.schema?.paths) {
556
556
  populateOptions = populateOptions.filter((option: string | PopulateOptions) => {
557
557
  let key: string = option as string;
558
558
  if ((option as PopulateOptions)?.path) {
559
559
  key = (option as PopulateOptions)?.path;
560
560
  }
561
- return Object.keys(modelSchemaPaths).includes(key);
561
+ return Object.keys(mongooseModel.schema.paths).includes(key);
562
562
  });
563
563
  }
564
564
 
@@ -573,12 +573,20 @@ export async function setPopulates<T = Query<any, any> | Document>(
573
573
  // Array with documents
574
574
  else if (Array.isArray(queryOrDocument)) {
575
575
  const promises = [];
576
- queryOrDocument.forEach((item) => promises.push(item.populate(populateOptions)));
576
+ queryOrDocument.forEach((item) => {
577
+ if (item.populate) {
578
+ promises.push(item.populate(populateOptions));
579
+ } else if (mongooseModel) {
580
+ promises.push(mongooseModel.populate(item, populateOptions as any));
581
+ }
582
+ });
577
583
  await Promise.all(promises);
578
584
  }
579
585
  // Single document
580
- else {
586
+ else if ((queryOrDocument as any).populate) {
581
587
  await (queryOrDocument as any).populate(populateOptions);
588
+ } else {
589
+ return (await mongooseModel.populate(queryOrDocument as any, populateOptions as any)) as any;
582
590
  }
583
591
 
584
592
  // Return populated
@@ -2,6 +2,7 @@ import { FilterQuery, QueryOptions } from 'mongoose';
2
2
  import { FilterArgs } from '../args/filter.args';
3
3
  import { ComparisonOperatorEnum } from '../enums/comparison-operator.enum';
4
4
  import { LogicalOperatorEnum } from '../enums/logical-operator.enum';
5
+ import { SortOrderEnum } from '../enums/sort-order.emum';
5
6
  import { FilterInput } from '../inputs/filter.input';
6
7
  import { SortInput } from '../inputs/sort.input';
7
8
  import { getObjectIds } from './db.helper';
@@ -193,36 +194,45 @@ export function generateFilterQuery<T = any>(filter?: Partial<FilterInput>): Fil
193
194
  /**
194
195
  * Generate find options
195
196
  */
196
- export function generateFindOptions<T = any>(filterArgs: Partial<FilterArgs>): QueryOptions {
197
+ export function generateFindOptions<T = any>(
198
+ filterArgs: Partial<FilterArgs>,
199
+ options?: { maxLimit?: number }
200
+ ): QueryOptions {
197
201
  // Check filterArgs
198
202
  if (!filterArgs) {
199
203
  return {};
200
204
  }
201
205
 
206
+ // Config
207
+ const config = {
208
+ maxLimit: 100,
209
+ ...options,
210
+ };
211
+
202
212
  // Get values
203
213
  const { limit, offset, skip, sort, take } = filterArgs;
204
214
 
205
215
  // Init options
206
- const options: QueryOptions = {
207
- limit: limit ? limit : take,
216
+ const queryOptions: QueryOptions = {
217
+ limit: limit || take,
208
218
  };
209
219
 
210
220
  if (skip > 0 || offset > 0) {
211
- options.skip = offset ? offset : skip;
221
+ queryOptions.skip = skip || offset;
212
222
  }
213
223
 
214
- // Check take
215
- if (!options.limit || options.limit > 100) {
216
- options.limit = 25;
224
+ // Check limit
225
+ if (!queryOptions.limit || queryOptions.limit > config.maxLimit) {
226
+ queryOptions.limit = 25;
217
227
  }
218
228
 
219
229
  // Prepare order
220
230
  if (sort) {
221
- options.sort = {};
231
+ queryOptions.sort = {};
222
232
  sort.forEach((item: SortInput) => {
223
- options.sort[item.field] = item.order;
233
+ queryOptions.sort[item.field] = item.order === SortOrderEnum.DESC ? -1 : 1;
224
234
  });
225
235
  }
226
236
 
227
- return options;
237
+ return queryOptions;
228
238
  }
@@ -1,3 +1,4 @@
1
+ import { OnApplicationBootstrap } from '@nestjs/common';
1
2
  import { CronExpression, SchedulerRegistry } from '@nestjs/schedule';
2
3
  import { CronJob } from 'cron';
3
4
  import { CronJobConfig } from '../interfaces/cron-job-config.interface';
@@ -6,7 +7,7 @@ import { Falsy } from '../types/falsy.type';
6
7
  /**
7
8
  * Cron jobs service to extend
8
9
  */
9
- export abstract class CoreCronJobs {
10
+ export abstract class CoreCronJobs implements OnApplicationBootstrap {
10
11
  /**
11
12
  * Config for cron jobs
12
13
  */
@@ -36,6 +37,16 @@ export abstract class CoreCronJobs {
36
37
  log: true,
37
38
  ...options,
38
39
  };
40
+ }
41
+
42
+ /**
43
+ * Lifecycle hook method: Called once all modules have been initialized, but before listening for connections.
44
+ * Required to ensure that all services have been previously initiated
45
+ */
46
+ onApplicationBootstrap() {
47
+ if (this.config.log) {
48
+ console.info('Init CronJobs after application bootstrap');
49
+ }
39
50
  this.initCronJobs();
40
51
  }
41
52
 
@@ -1,5 +1,5 @@
1
1
  import { NotFoundException } from '@nestjs/common';
2
- import { FilterQuery, QueryOptions } from 'mongoose';
2
+ import { FilterQuery, PipelineStage, QueryOptions } from 'mongoose';
3
3
  import { FilterArgs } from '../args/filter.args';
4
4
  import { merge } from '../helpers/config.helper';
5
5
  import { getStringIds } from '../helpers/db.helper';
@@ -59,6 +59,69 @@ export abstract class CrudService<T extends CoreModel = any> extends ModuleServi
59
59
  );
60
60
  }
61
61
 
62
+ /**
63
+ * Get items and total count via filter
64
+ */
65
+ async findAndCount(
66
+ filter?: FilterArgs | { filterQuery?: FilterQuery<any>; queryOptions?: QueryOptions },
67
+ serviceOptions?: ServiceOptions
68
+ ): Promise<{ items: T[]; totalCount: number }> {
69
+ return this.process(
70
+ async (data) => {
71
+ // Prepare filter query
72
+ const filterQuery = { filterQuery: data?.input?.filterQuery, queryOptions: data?.input?.queryOptions };
73
+ if (data?.input instanceof FilterArgs) {
74
+ const converted = convertFilterArgsToQuery(data.input);
75
+ filterQuery.filterQuery = converted[0];
76
+ filterQuery.queryOptions = converted[1];
77
+ }
78
+
79
+ // Prepare aggregation (with fixed defined sequence)
80
+ const aggregation: PipelineStage[] = [
81
+ {
82
+ // Add pipeline stage 1: match
83
+ $match: filterQuery.filterQuery,
84
+ },
85
+ ];
86
+
87
+ // Prepare $facet
88
+ const facet = {
89
+ items: [],
90
+ totalCount: [{ $count: 'total' }],
91
+ };
92
+
93
+ // Prepare query options
94
+ if (filterQuery.queryOptions) {
95
+ // Add pipeline stage 2: sort (optional)
96
+ const options = filterQuery.queryOptions;
97
+ if (options.sort) {
98
+ aggregation.push({ $sort: options.sort });
99
+ }
100
+
101
+ // Prepare skip / offset in facet
102
+ if (options.skip || options.offset) {
103
+ facet.items.push({ $skip: options.skip || options.offset });
104
+ }
105
+
106
+ // Prepare limit / take in facet
107
+ if (options.limit || options.take) {
108
+ facet.items.push({ $limit: options.limit || options.take });
109
+ }
110
+ }
111
+
112
+ // Set pipeline stage 3: facet => items (with skip & limit) and totalCount
113
+ aggregation.push({ $facet: facet });
114
+
115
+ // Find and process db items
116
+ const dbResult = (await this.mainDbModel.aggregate(aggregation).exec())[0];
117
+ dbResult.totalCount = dbResult.totalCount[0].total;
118
+ dbResult.items = dbResult.items.map((item) => this.mainDbModel.hydrate(item));
119
+ return dbResult;
120
+ },
121
+ { input: filter, outputPath: 'items', serviceOptions }
122
+ );
123
+ }
124
+
62
125
  /**
63
126
  * Find and update
64
127
  */
@@ -1,3 +1,4 @@
1
+ import * as _ from 'lodash';
1
2
  import { Document, Model, Types } from 'mongoose';
2
3
  import { ProcessType } from '../enums/process-type.enum';
3
4
  import { getStringIds, popAndMap } from '../helpers/db.helper';
@@ -65,14 +66,20 @@ export abstract class ModuleService<T extends CoreModel = any> {
65
66
  options?: {
66
67
  [key: string]: any;
67
68
  dbObject?: string | Types.ObjectId | any;
69
+ outputPath?: string | string[];
68
70
  input?: any;
69
71
  serviceOptions?: ServiceOptions;
70
72
  }
71
73
  ) {
72
74
  // Configuration with default values
73
- const config: { dbObject: string | Types.ObjectId | any; input: any } & ServiceOptions = {
75
+ const config: {
76
+ dbObject: string | Types.ObjectId | any;
77
+ outputPath: string | string[];
78
+ input: any;
79
+ } & ServiceOptions = {
74
80
  checkRights: true,
75
81
  dbObject: options?.dbObject,
82
+ outputPath: options?.outputPath,
76
83
  force: false,
77
84
  input: options?.input,
78
85
  processFieldSelection: {},
@@ -136,7 +143,8 @@ export abstract class ModuleService<T extends CoreModel = any> {
136
143
 
137
144
  // Pop and map main model
138
145
  if (config.processFieldSelection && config.fieldSelection && this.processFieldSelection) {
139
- await this.processFieldSelection(result, config.fieldSelection, config.processFieldSelection);
146
+ const field = config.outputPath ? _.get(result, config.outputPath) : result;
147
+ await this.processFieldSelection(field, config.fieldSelection, config.processFieldSelection);
140
148
  }
141
149
 
142
150
  // Prepare output
@@ -145,7 +153,11 @@ export abstract class ModuleService<T extends CoreModel = any> {
145
153
  if (!opts.targetModel && config.outputType) {
146
154
  opts.targetModel = config.outputType;
147
155
  }
148
- result = await this.prepareOutput(result, opts);
156
+ if (config.outputPath) {
157
+ _.set(result, config.outputPath, await this.prepareOutput(_.get(result, config.outputPath), opts));
158
+ } else {
159
+ result = await this.prepareOutput(result, opts);
160
+ }
149
161
  }
150
162
 
151
163
  // Check output rights
@@ -0,0 +1,11 @@
1
+ import { Field, ObjectType } from '@nestjs/graphql';
2
+ import { User } from '../user.model';
3
+
4
+ @ObjectType({ description: 'Result of find and count' })
5
+ export class FindAndCountUsersResult {
6
+ @Field(() => [User], { description: 'Found users' })
7
+ items: User[];
8
+
9
+ @Field({ description: 'Total count (skip/offset and limit/take are ignored in the count)' })
10
+ totalCount: number;
11
+ }
@@ -8,6 +8,7 @@ import { Roles } from '../../../core/common/decorators/roles.decorator';
8
8
  import { RoleEnum } from '../../../core/common/enums/role.enum';
9
9
  import { UserCreateInput } from './inputs/user-create.input';
10
10
  import { UserInput } from './inputs/user.input';
11
+ import { FindAndCountUsersResult } from './outputs/find-and-count-users-result.output';
11
12
  import { User } from './user.model';
12
13
  import { UserService } from './user.service';
13
14
 
@@ -38,6 +39,18 @@ export class UserResolver {
38
39
  });
39
40
  }
40
41
 
42
+ /**
43
+ * Get users and total count (via filter)
44
+ */
45
+ @Roles(RoleEnum.ADMIN)
46
+ @Query(() => FindAndCountUsersResult, { description: 'Find users (via filter)' })
47
+ async findAndCountUsers(@Info() info: GraphQLResolveInfo, @Args() args?: FilterArgs) {
48
+ return await this.userService.findAndCount(args, {
49
+ fieldSelection: { info, select: 'findAndCountUsers' },
50
+ inputType: FilterArgs,
51
+ });
52
+ }
53
+
41
54
  /**
42
55
  * Get user via ID
43
56
  */