@lenne.tech/nest-server 11.10.6 → 11.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "11.10.6",
3
+ "version": "11.11.1",
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",
@@ -0,0 +1,101 @@
1
+ import { ObjectType } from '@nestjs/graphql';
2
+
3
+ import { UnifiedField } from '../decorators/unified-field.decorator';
4
+
5
+ /**
6
+ * Pagination information for paginated queries
7
+ *
8
+ * Provides metadata about the current page, total pages, and navigation flags
9
+ * to facilitate frontend pagination UI implementation.
10
+ */
11
+ @ObjectType({ description: 'Pagination information for paginated queries' })
12
+ export class PaginationInfo {
13
+ /**
14
+ * Total number of items across all pages
15
+ */
16
+ @UnifiedField({
17
+ description: 'Total number of items across all pages',
18
+ })
19
+ totalCount: number;
20
+
21
+ /**
22
+ * Total number of pages
23
+ */
24
+ @UnifiedField({
25
+ description: 'Total number of pages',
26
+ })
27
+ pageCount: number;
28
+
29
+ /**
30
+ * Current page number (1-based)
31
+ */
32
+ @UnifiedField({
33
+ description: 'Current page number (1-based)',
34
+ })
35
+ currentPage: number;
36
+
37
+ /**
38
+ * Number of items per page
39
+ */
40
+ @UnifiedField({
41
+ description: 'Number of items per page',
42
+ })
43
+ perPage: number;
44
+
45
+ /**
46
+ * Indicates if there is a next page
47
+ */
48
+ @UnifiedField({
49
+ description: 'Indicates if there is a next page',
50
+ })
51
+ hasNextPage: boolean;
52
+
53
+ /**
54
+ * Indicates if there is a previous page
55
+ */
56
+ @UnifiedField({
57
+ description: 'Indicates if there is a previous page',
58
+ })
59
+ hasPreviousPage: boolean;
60
+
61
+ /**
62
+ * Create PaginationInfo from query parameters and total count
63
+ */
64
+ static create(options: {
65
+ limit?: number;
66
+ offset?: number;
67
+ skip?: number;
68
+ take?: number;
69
+ totalCount: number;
70
+ }): PaginationInfo {
71
+ const { totalCount } = options;
72
+ const skip = options.skip ?? options.offset ?? 0;
73
+ const perPage = options.take ?? options.limit ?? 0;
74
+
75
+ // Handle edge case: no limit specified (return all items as single page)
76
+ if (perPage === 0) {
77
+ return {
78
+ currentPage: totalCount > 0 ? 1 : 0,
79
+ hasNextPage: false,
80
+ hasPreviousPage: false,
81
+ pageCount: totalCount > 0 ? 1 : 0,
82
+ perPage: 0,
83
+ totalCount,
84
+ };
85
+ }
86
+
87
+ const pageCount = Math.ceil(totalCount / perPage);
88
+ const currentPage = totalCount === 0 ? 0 : Math.floor(skip / perPage) + 1;
89
+ const hasNextPage = currentPage < pageCount;
90
+ const hasPreviousPage = currentPage > 1;
91
+
92
+ return {
93
+ currentPage,
94
+ hasNextPage,
95
+ hasPreviousPage,
96
+ pageCount,
97
+ perPage,
98
+ totalCount,
99
+ };
100
+ }
101
+ }
@@ -15,6 +15,7 @@ import { convertFilterArgsToQuery } from '../helpers/filter.helper';
15
15
  import { mergePlain, prepareServiceOptionsForCreate } from '../helpers/input.helper';
16
16
  import { ServiceOptions } from '../interfaces/service-options.interface';
17
17
  import { CoreModel } from '../models/core-model.model';
18
+ import { PaginationInfo } from '../models/pagination-info.model';
18
19
  import { PlainObject } from '../types/plain-object.type';
19
20
  import { ConfigService } from './config.service';
20
21
  import { ModuleService } from './module.service';
@@ -218,7 +219,11 @@ export abstract class CrudService<
218
219
  async findAndCount(
219
220
  filter?: FilterArgs | { filterQuery?: QueryFilter<any>; queryOptions?: QueryOptions; samples?: number },
220
221
  serviceOptions?: ServiceOptions,
221
- ): Promise<{ items: Model[]; totalCount: number }> {
222
+ ): Promise<{
223
+ items: Model[];
224
+ pagination: PaginationInfo;
225
+ totalCount: number;
226
+ }> {
222
227
  // If filter is not instance of FilterArgs a simple form with filterQuery and queryOptions is set
223
228
  // and should not be processed as FilterArgs
224
229
  if (!(filter instanceof FilterArgs) && serviceOptions?.inputType === FilterArgs) {
@@ -284,6 +289,17 @@ export abstract class CrudService<
284
289
  (await this.mainDbModel.aggregate(aggregation, collation ? { collation } : {}).exec())[0] || {};
285
290
  dbResult.totalCount = dbResult.totalCount?.[0]?.total || 0;
286
291
  dbResult.items = dbResult.items?.map((item) => this.mainDbModel.hydrate(item)) || [];
292
+
293
+ // Calculate pagination info
294
+ const queryOptions = filterQuery.queryOptions || {};
295
+ dbResult.pagination = PaginationInfo.create({
296
+ limit: queryOptions.limit,
297
+ offset: queryOptions.offset,
298
+ skip: queryOptions.skip,
299
+ take: queryOptions.take,
300
+ totalCount: dbResult.totalCount,
301
+ });
302
+
287
303
  return dbResult;
288
304
  },
289
305
  { input: filter, outputPath: 'items', serviceOptions },
@@ -297,7 +313,7 @@ export abstract class CrudService<
297
313
  async findAndCountForce(
298
314
  filter?: FilterArgs | { filterQuery?: QueryFilter<any>; queryOptions?: QueryOptions; samples?: number },
299
315
  serviceOptions: ServiceOptions = {},
300
- ): Promise<{ items: Model[]; totalCount: number }> {
316
+ ): Promise<{ items: Model[]; pagination: PaginationInfo; totalCount: number; }> {
301
317
  serviceOptions.raw = true;
302
318
  return this.findAndCount(filter, serviceOptions);
303
319
  }
@@ -309,7 +325,7 @@ export abstract class CrudService<
309
325
  async findAndCountRaw(
310
326
  filter?: FilterArgs | { filterQuery?: QueryFilter<any>; queryOptions?: QueryOptions; samples?: number },
311
327
  serviceOptions: ServiceOptions = {},
312
- ): Promise<{ items: Model[]; totalCount: number }> {
328
+ ): Promise<{ items: Model[]; pagination: PaginationInfo; totalCount: number; }> {
313
329
  serviceOptions = serviceOptions || {};
314
330
  serviceOptions.raw = true;
315
331
  return this.findAndCountForce(filter, serviceOptions);
@@ -244,9 +244,12 @@ export class CoreBetterAuthApiMiddleware implements NestMiddleware {
244
244
  }
245
245
  }
246
246
 
247
- // Clean up the used challenge mapping after verification (success or failure)
248
- if (challengeIdToDelete && this.challengeService) {
247
+ // Clean up the used challenge mapping only after SUCCESSFUL verification
248
+ // On failure, keep the challenge so the user can retry with a different passkey
249
+ if (challengeIdToDelete && this.challengeService && response.ok) {
249
250
  await this.challengeService.deleteChallengeMapping(challengeIdToDelete);
251
+ } else if (challengeIdToDelete && !response.ok) {
252
+ this.logger.debug(`Keeping challenge mapping after failed verification (status=${response.status}) for retry`);
250
253
  }
251
254
 
252
255
  // Convert Web Standard Response to Express response using shared helper
package/src/index.ts CHANGED
@@ -62,6 +62,7 @@ export * from './core/common/interfaces/service-options.interface';
62
62
  export * from './core/common/middlewares/to-lower-case.middleware';
63
63
  export * from './core/common/models/core-model.model';
64
64
  export * from './core/common/models/core-persistence.model';
65
+ export * from './core/common/models/pagination-info.model';
65
66
  export * from './core/common/pipes/check-input.pipe';
66
67
  export * from './core/common/pipes/map-and-validate.pipe';
67
68
  export * from './core/common/plugins/complexity.plugin';
@@ -3,6 +3,7 @@ import { ObjectType } from '@nestjs/graphql';
3
3
  import { Restricted } from '../../../../core/common/decorators/restricted.decorator';
4
4
  import { UnifiedField } from '../../../../core/common/decorators/unified-field.decorator';
5
5
  import { RoleEnum } from '../../../../core/common/enums/role.enum';
6
+ import { PaginationInfo } from '../../../../core/common/models/pagination-info.model';
6
7
  import { User } from '../user.model';
7
8
 
8
9
  @ObjectType({ description: 'Result of find and count' })
@@ -21,4 +22,11 @@ export class FindAndCountUsersResult {
21
22
  isOptional: false,
22
23
  })
23
24
  totalCount: number;
25
+
26
+ @UnifiedField({
27
+ description: 'Pagination information',
28
+ isOptional: true,
29
+ type: () => PaginationInfo,
30
+ })
31
+ pagination?: PaginationInfo;
24
32
  }