@smartsoft001/crud-shell-nestjs 1.1.90 → 1.2.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.
Files changed (47) hide show
  1. package/.eslintrc +13 -0
  2. package/README.md +37 -4
  3. package/jest.config.ts +27 -0
  4. package/package.json +2 -45
  5. package/project.json +43 -0
  6. package/src/index.ts +4 -0
  7. package/src/lib/controllers/crud/crud.controller.spec.ts +111 -0
  8. package/src/lib/controllers/crud/crud.controller.ts +353 -0
  9. package/src/lib/controllers/crud/query-to-mongo.spec.ts +32 -0
  10. package/src/lib/controllers/crud/query-to-mongo.ts +295 -0
  11. package/src/lib/controllers/index.ts +5 -0
  12. package/src/lib/gateways/crud/crud.gateway.spec.ts +122 -0
  13. package/src/lib/gateways/crud/crud.gateway.ts +72 -0
  14. package/src/lib/gateways/index.ts +3 -0
  15. package/src/lib/guards/auth/auth.guard.spec.ts +60 -0
  16. package/src/lib/guards/auth/auth.guard.ts +22 -0
  17. package/src/lib/nestjs.module.ts +94 -0
  18. package/tsconfig.json +13 -0
  19. package/tsconfig.lib.json +11 -0
  20. package/tsconfig.spec.json +20 -0
  21. package/src/index.d.ts +0 -4
  22. package/src/index.js +0 -8
  23. package/src/index.js.map +0 -1
  24. package/src/lib/controllers/crud/crud.controller.d.ts +0 -39
  25. package/src/lib/controllers/crud/crud.controller.js +0 -327
  26. package/src/lib/controllers/crud/crud.controller.js.map +0 -1
  27. package/src/lib/controllers/crud/query-to-mongo.d.ts +0 -5
  28. package/src/lib/controllers/crud/query-to-mongo.js +0 -286
  29. package/src/lib/controllers/crud/query-to-mongo.js.map +0 -1
  30. package/src/lib/controllers/index.d.ts +0 -3
  31. package/src/lib/controllers/index.js +0 -10
  32. package/src/lib/controllers/index.js.map +0 -1
  33. package/src/lib/gateways/crud/crud.gateway.d.ts +0 -18
  34. package/src/lib/gateways/crud/crud.gateway.js +0 -56
  35. package/src/lib/gateways/crud/crud.gateway.js.map +0 -1
  36. package/src/lib/gateways/index.d.ts +0 -2
  37. package/src/lib/gateways/index.js +0 -8
  38. package/src/lib/gateways/index.js.map +0 -1
  39. package/src/lib/guards/auth/auth.guard.d.ts +0 -10
  40. package/src/lib/guards/auth/auth.guard.js +0 -32
  41. package/src/lib/guards/auth/auth.guard.js.map +0 -1
  42. package/src/lib/nestjs.module.d.ts +0 -31
  43. package/src/lib/nestjs.module.js +0 -81
  44. package/src/lib/nestjs.module.js.map +0 -1
  45. package/test-setup.js +0 -1
  46. package/test-setup.js.map +0 -1
  47. /package/{test-setup.d.ts → test-setup.ts} +0 -0
package/.eslintrc ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../../../.eslintrc",
3
+ "rules": {
4
+ "import/order": "off"
5
+ },
6
+ "ignorePatterns": ["!**/*"],
7
+ "overrides": [
8
+ {
9
+ "files": ["*.json"],
10
+ "parser": "jsonc-eslint-parser"
11
+ }
12
+ ]
13
+ }
package/README.md CHANGED
@@ -1,7 +1,40 @@
1
- # crud-shell-nestjs
1
+ # 📦 @smartsoft001/crud-shell-nestjs
2
2
 
3
- This library was generated with [Nx](https://nx.dev).
3
+ ![npm](https://img.shields.io/npm/v/@smartsoft001/crud-shell-nestjs) ![downloads](https://img.shields.io/npm/dm/@smartsoft001/crud-shell-nestjs)
4
4
 
5
- ## Running unit tests
5
+ ## 🚀 Usage
6
6
 
7
- Run `ng test crud-shell-nestjs` to execute the unit tests via [Jest](https://jestjs.io).
7
+ `npm i @smartsoft001/crud-shell-nestjs`
8
+
9
+ ## 🛠️ Modules
10
+
11
+ ### CrudShellNestjsModule
12
+ - Provides the main integration module for CRUD features in a NestJS app.
13
+ - Static method: `forRoot(options)` — Registers controllers, providers, and imports required modules with the given configuration.
14
+
15
+ ### CrudShellNestjsCoreModule
16
+ - Provides a core integration module for CRUD features in a NestJS app.
17
+ - Static method: `forRoot(options)` — Registers providers and imports required modules with the given configuration.
18
+
19
+ ## 🛠️ Controllers & Methods
20
+
21
+ ### CrudController
22
+ <table>
23
+ <tr><td>POST /</td><td>create — Creates a new entity. Returns the new entity's ID.</td></tr>
24
+ <tr><td>POST /bulk</td><td>createMany — Creates multiple entities in bulk.</td></tr>
25
+ <tr><td>GET /:id</td><td>readById — Retrieves an entity by its ID.</td></tr>
26
+ <tr><td>GET /</td><td>read — Retrieves a list of entities with filtering, CSV, and XLSX export support.</td></tr>
27
+ <tr><td>PUT /:id</td><td>update — Updates an entity by its ID.</td></tr>
28
+ <tr><td>PATCH /:id</td><td>updatePartial — Partially updates an entity by its ID.</td></tr>
29
+ <tr><td>DELETE /:id</td><td>delete — Deletes an entity by its ID.</td></tr>
30
+ <tr><td>POST /attachments</td><td>uploadAttachment — Uploads an attachment for an entity.</td></tr>
31
+ <tr><td>GET /attachments/:id</td><td>downloadAttachment — Downloads an attachment by its ID.</td></tr>
32
+ <tr><td>DELETE /attachments/:id</td><td>deleteAttachment — Deletes an attachment by its ID.</td></tr>
33
+ </table>
34
+
35
+ ## 🛠️ Gateways & Methods
36
+
37
+ ### CrudGateway
38
+ <table>
39
+ <tr><td>changes (WebSocket)</td><td>handleFilter — Subscribes to changes for entities and streams updates to the client.</td></tr>
40
+ </table>
package/jest.config.ts ADDED
@@ -0,0 +1,27 @@
1
+ /* eslint-disable */
2
+ export default {
3
+ globals: {},
4
+ transform: {
5
+ '^.+\\.[tj]sx?$': [
6
+ 'ts-jest',
7
+ {
8
+ tsConfig: '<rootDir>/tsconfig.spec.json',
9
+ },
10
+ ],
11
+ },
12
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
13
+ coverageDirectory: '../../../../coverage/packages/crud/shell/nestjs',
14
+ displayName: 'crud-shell-nestjs',
15
+ testEnvironment: 'node',
16
+ preset: '../../../../jest.preset.js',
17
+ /* TODO: Update to latest Jest snapshotFormat
18
+ * By default Nx has kept the older style of Jest Snapshot formats
19
+ * to prevent breaking of any existing tests with snapshots.
20
+ * It's recommend you update to the latest format.
21
+ * You can do this by removing snapshotFormat property
22
+ * and running tests with --update-snapshot flag.
23
+ * Example: From within the project directory, run "nx test --update-snapshot"
24
+ * More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format
25
+ */
26
+ snapshotFormat: { escapeString: true, printBasicPrototype: true },
27
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartsoft001/crud-shell-nestjs",
3
- "version": "1.1.90",
3
+ "version": "1.2.0",
4
4
  "description": "Utils to crud",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,48 +16,5 @@
16
16
  "bugs": {
17
17
  "url": "https://github.com/emiljuchnikowski/smartsoft/issues"
18
18
  },
19
- "homepage": "https://github.com/emiljuchnikowski/smartsoft#readme",
20
- "dependencies": {
21
- "@angular/common": "16.1.3",
22
- "@angular/core": "16.1.3",
23
- "@angular/platform-browser-dynamic": "16.1.3",
24
- "@nestjs/axios": "3.0.0",
25
- "@nestjs/common": "^10.0.5",
26
- "@nestjs/jwt": "^10.1.0",
27
- "@nestjs/passport": "^10.0.0",
28
- "@nestjs/terminus": "^10.0.1",
29
- "@nestjs/typeorm": "^10.0.0",
30
- "@nestjs/websockets": "^10.0.5",
31
- "busboy": "^0.3.1",
32
- "combined-stream": "^1.0.8",
33
- "express": "4.18.2",
34
- "flatted": "^3.2.4",
35
- "guid-typescript": "1.0.9",
36
- "json2csv": "^5.0.5",
37
- "lodash": "^4.17.20",
38
- "lodash-decorators": "^6.0.1",
39
- "md5": "2.3.0",
40
- "moment-timezone": "^0.5.43",
41
- "mongodb": "5.6.0",
42
- "passport-jwt": "4.0.0",
43
- "reflect-metadata": "0.1.13",
44
- "rxjs": "7.8.1",
45
- "socket.io": "4.7.1",
46
- "xlsx": "^0.17.0"
47
- },
48
- "peerDependencies": {
49
- "@smartsoft001/crud-domain": "1.1.90",
50
- "@smartsoft001/crud-shell-app-services": "1.1.90",
51
- "@smartsoft001/crud-shell-dtos": "1.1.90",
52
- "@smartsoft001/domain-core": "1.1.90",
53
- "@smartsoft001/models": "1.1.90",
54
- "@smartsoft001/mongo": "1.1.90",
55
- "@smartsoft001/nestjs": "1.1.90",
56
- "@smartsoft001/users": "1.1.90",
57
- "@smartsoft001/utils": "1.1.90",
58
- "util": "0.12.5",
59
- "tslib": "2.5.3"
60
- },
61
- "main": "./src/index.js",
62
- "types": "./src/index.d.ts"
19
+ "homepage": "https://github.com/emiljuchnikowski/smartsoft#readme"
63
20
  }
package/project.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "crud-shell-nestjs",
3
+ "$schema": "../../../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "packages/crud/shell/nestjs/src",
5
+ "projectType": "library",
6
+ "generators": {},
7
+ "targets": {
8
+ "lint": {
9
+ "executor": "@nx/eslint:lint",
10
+ "outputs": ["{options.outputFile}"],
11
+ "options": {
12
+ "lintFilePatterns": [
13
+ "packages/crud/shell/nestjs/**/*.{ts,tsx,js,jsx}",
14
+ "packages/crud/shell/nestjs/package.json"
15
+ ],
16
+ "tsConfig": [
17
+ "packages/crud/shell/nestjs/tsconfig.lib.json",
18
+ "packages/crud/shell/nestjs/tsconfig.spec.json"
19
+ ]
20
+ }
21
+ },
22
+ "test": {
23
+ "executor": "@nx/jest:jest",
24
+ "options": {
25
+ "jestConfig": "packages/crud/shell/nestjs/jest.config.ts",
26
+ "passWithNoTests": true
27
+ },
28
+ "outputs": ["{workspaceRoot}/coverage/packages/crud/shell/nestjs"]
29
+ },
30
+ "build": {
31
+ "executor": "@nx/esbuild:esbuild",
32
+ "options": {
33
+ "outputPath": "dist/packages/crud/shell/nestjs",
34
+ "tsConfig": "packages/crud/shell/nestjs/tsconfig.lib.json",
35
+ "packageJson": "packages/crud/shell/nestjs/package.json",
36
+ "main": "packages/crud/shell/nestjs/src/index.ts",
37
+ "assets": ["packages/crud/shell/nestjs/*.md"]
38
+ },
39
+ "outputs": ["{options.outputPath}"]
40
+ }
41
+ },
42
+ "tags": ["scope:crud", "type:shell"]
43
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from './lib/nestjs.module';
2
+ export * from './lib/guards/auth/auth.guard';
3
+ export * from './lib/controllers';
4
+ export * from './lib/gateways';
@@ -0,0 +1,111 @@
1
+ import { Request } from 'express';
2
+ import * as XLSX from 'xlsx';
3
+
4
+ import { CrudService } from '@smartsoft001/crud-shell-app-services';
5
+
6
+ import { CrudController } from './crud.controller';
7
+ import * as q2mModule from './query-to-mongo';
8
+
9
+ jest.mock('xlsx');
10
+ jest.mock('json2csv', () => ({
11
+ Parser: jest.fn().mockImplementation(() => ({ parse: jest.fn(() => 'csv') })),
12
+ }));
13
+
14
+ describe('crud-nestjs: CrudController', () => {
15
+ let service: jest.Mocked<CrudService<any>>;
16
+ let controller: CrudController<any>;
17
+
18
+ beforeEach(() => {
19
+ service = {
20
+ create: jest.fn(),
21
+ createMany: jest.fn(),
22
+ readById: jest.fn(),
23
+ read: jest.fn(),
24
+ update: jest.fn(),
25
+ updatePartial: jest.fn(),
26
+ delete: jest.fn(),
27
+ uploadAttachment: jest.fn(),
28
+ getAttachmentInfo: jest.fn(),
29
+ getAttachmentStream: jest.fn(),
30
+ deleteAttachment: jest.fn(),
31
+ } as any;
32
+ controller = new CrudController(service);
33
+ });
34
+
35
+ describe('constructor', () => {
36
+ it('should assign service', () => {
37
+ expect(controller['service']).toBe(service);
38
+ });
39
+ });
40
+
41
+ describe('static getLink', () => {
42
+ it('should return correct link', () => {
43
+ const req = {
44
+ protocol: 'http',
45
+ headers: { host: 'localhost' },
46
+ url: '/api',
47
+ } as Request;
48
+ expect(CrudController.getLink(req)).toBe('http://localhost/api');
49
+ });
50
+ });
51
+
52
+ describe('getQueryObject', () => {
53
+ it('should call q2m and return result', () => {
54
+ const query = { a: 1, b: 2 };
55
+ const result = { criteria: {}, options: {}, links: jest.fn() };
56
+ jest.spyOn(q2mModule, 'q2m').mockReturnValue(result);
57
+ expect(controller['getQueryObject'](query)).toBe(result);
58
+ });
59
+ });
60
+
61
+ describe('parseToCsv', () => {
62
+ it('should return empty string for empty data', () => {
63
+ expect(controller['parseToCsv']([])).toBe('');
64
+ });
65
+ it('should return csv string for data', () => {
66
+ const data = [{ a: '1', b: '2' }];
67
+ jest
68
+ .spyOn(controller as any, 'getDataWithFields')
69
+ .mockReturnValue({ res: data, fields: ['a', 'b'] });
70
+ expect(controller['parseToCsv'](data)).toBe('csv');
71
+ });
72
+ });
73
+
74
+ describe('parseToXlsx', () => {
75
+ it('should return empty string for empty data', () => {
76
+ expect(controller['parseToXlsx']([])).toBe('');
77
+ });
78
+ it('should call XLSX utils for data', () => {
79
+ const data = [{ a: '1', b: '2' }];
80
+ jest
81
+ .spyOn(controller as any, 'getDataWithFields')
82
+ .mockReturnValue({ res: data, fields: ['a', 'b'] });
83
+ const jsonToSheet = jest
84
+ .spyOn(XLSX.utils, 'json_to_sheet')
85
+ .mockReturnValue({} as any);
86
+ const bookNew = jest
87
+ .spyOn(XLSX.utils, 'book_new')
88
+ .mockReturnValue({} as any);
89
+ const bookAppendSheet = jest
90
+ .spyOn(XLSX.utils, 'book_append_sheet')
91
+ .mockImplementation();
92
+ const write = jest.spyOn(XLSX, 'write').mockReturnValue('buffer' as any);
93
+ expect(controller['parseToXlsx'](data)).toBe('buffer');
94
+ });
95
+ });
96
+
97
+ describe('getDataWithFields', () => {
98
+ it('should flatten fields and remove html', () => {
99
+ const data = [{ a: '<b>1</b>', b: { c: '2' } }];
100
+ const result = controller['getDataWithFields'](data);
101
+ expect(result.fields.includes('a')).toBe(true);
102
+ });
103
+ it('should remove keys not in fields', () => {
104
+ const data = [{ a: '1', b: '2' }];
105
+ const result = controller['getDataWithFields'](data);
106
+ expect(
107
+ Object.keys(result.res[0]).every((k) => result.fields.includes(k)),
108
+ ).toBe(true);
109
+ });
110
+ });
111
+ });
@@ -0,0 +1,353 @@
1
+ import {
2
+ Body,
3
+ Controller,
4
+ Delete,
5
+ Get,
6
+ HttpCode,
7
+ NotFoundException,
8
+ Param,
9
+ Patch,
10
+ Post,
11
+ Put,
12
+ Query,
13
+ Req,
14
+ Res,
15
+ UseGuards,
16
+ } from '@nestjs/common';
17
+ import { q2m } from './query-to-mongo';
18
+ import { Response, Request } from 'express';
19
+ import { Parser } from 'json2csv';
20
+ import * as _ from 'lodash';
21
+ import * as XLSX from 'xlsx';
22
+ import * as Busboy from 'busboy';
23
+ import { Readable } from 'stream';
24
+ import * as moment from 'moment-timezone';
25
+
26
+ import { CrudService } from '@smartsoft001/crud-shell-app-services';
27
+ import { IUser } from '@smartsoft001/users';
28
+ import { User } from '@smartsoft001/nestjs';
29
+ import { IEntity } from '@smartsoft001/domain-core';
30
+ import {
31
+ AuthJwtGuard,
32
+ AuthOrAnonymousJwtGuard,
33
+ } from '../../guards/auth/auth.guard';
34
+ import { CreateManyMode } from '@smartsoft001/crud-domain';
35
+ import { GuidService } from '@smartsoft001/utils';
36
+
37
+ @Controller('')
38
+ export class CrudController<T extends IEntity<string>> {
39
+ constructor(protected readonly service: CrudService<T>) {}
40
+
41
+ static getLink(req: Request): string {
42
+ return req.protocol + '://' + req.headers.host + req.url;
43
+ }
44
+
45
+ @UseGuards(AuthJwtGuard)
46
+ @Post()
47
+ @HttpCode(200)
48
+ async create(
49
+ @Body() data: T,
50
+ @User() user: IUser,
51
+ @Res() res: Response,
52
+ ): Promise<Response> {
53
+ const id = await this.service.create(data, user);
54
+ res.set('Location', CrudController.getLink(res.req) + '/' + id);
55
+ return res.send({
56
+ id,
57
+ });
58
+ }
59
+
60
+ @UseGuards(AuthJwtGuard)
61
+ @Post('bulk')
62
+ async createMany(
63
+ @Body() data: T[],
64
+ @User() user: IUser,
65
+ @Res() res: Response,
66
+ @Query('mode') mode: CreateManyMode,
67
+ ): Promise<Response> {
68
+ const result = await this.service.createMany(data, user, { mode });
69
+ return res.send(result);
70
+ }
71
+
72
+ @UseGuards(AuthOrAnonymousJwtGuard)
73
+ @Get(':id')
74
+ async readById(
75
+ @Param() params: { id: string },
76
+ @User() user: IUser,
77
+ ): Promise<T> {
78
+ const result = await this.service.readById(params.id, user);
79
+
80
+ if (!result) {
81
+ throw new NotFoundException('Invalid id');
82
+ }
83
+
84
+ return result;
85
+ }
86
+
87
+ @UseGuards(AuthOrAnonymousJwtGuard)
88
+ @Get()
89
+ async read(
90
+ @User() user: IUser,
91
+ @Req() req: Request,
92
+ @Res() res: Response,
93
+ ): Promise<void> {
94
+ const object = this.getQueryObject(req.query);
95
+
96
+ const { data, totalCount } = await this.service.read(
97
+ object.criteria,
98
+ {
99
+ ...object.options,
100
+ allowDiskUse:
101
+ req.headers['content-type'] === 'text/csv' ||
102
+ req.headers['content-type'] ===
103
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
104
+ },
105
+ user,
106
+ );
107
+
108
+ if (req.headers['content-type'] === 'text/csv') {
109
+ res.set({
110
+ 'Content-Type': 'text/csv',
111
+ });
112
+ res.send(this.parseToCsv(data));
113
+ }
114
+
115
+ if (
116
+ req.headers['content-type'] ===
117
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
118
+ ) {
119
+ res.set({
120
+ 'Content-Type':
121
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
122
+ });
123
+ res.send(this.parseToXlsx(data));
124
+ }
125
+
126
+ res.send({
127
+ data,
128
+ totalCount,
129
+ links: object.links(
130
+ CrudController.getLink(req).split('?')[0],
131
+ totalCount,
132
+ ),
133
+ });
134
+ }
135
+
136
+ @UseGuards(AuthJwtGuard)
137
+ @Put(':id')
138
+ async update(
139
+ @Param() params: { id: string },
140
+ @Body() data: T,
141
+ @User() user: IUser,
142
+ ): Promise<void> {
143
+ await this.service.update(params.id, data, user);
144
+ }
145
+
146
+ @UseGuards(AuthJwtGuard)
147
+ @Patch(':id')
148
+ async updatePartial(
149
+ @Param() params: { id: string },
150
+ @Body() data: Partial<T>,
151
+ @User() user: IUser,
152
+ ): Promise<void> {
153
+ await this.service.updatePartial(params.id, data, user);
154
+ }
155
+
156
+ @UseGuards(AuthJwtGuard)
157
+ @Delete(':id')
158
+ async delete(
159
+ @Param() params: { id: string },
160
+ @User() user: IUser,
161
+ ): Promise<void> {
162
+ await this.service.delete(params.id, user);
163
+ }
164
+
165
+ @Post('attachments')
166
+ uploadAttachment(@Req() request: Request, @Res() response: Response) {
167
+ const busboy = new Busboy({
168
+ headers: request.headers,
169
+ });
170
+ const id = GuidService.create();
171
+ const readable = new Readable();
172
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
173
+ readable._read = () => {};
174
+
175
+ let fileName, encoding, mimeType;
176
+
177
+ busboy.on(
178
+ 'file',
179
+ (field, file, resultFileName, resultEncoding, resultMimeType) => {
180
+ fileName = resultFileName;
181
+ encoding = resultEncoding;
182
+ mimeType = resultMimeType;
183
+
184
+ this.service.uploadAttachment({
185
+ id,
186
+ stream: readable,
187
+ fileName,
188
+ encoding,
189
+ mimeType,
190
+ });
191
+
192
+ file.on('data', (data) => {
193
+ readable.push(data);
194
+ });
195
+ },
196
+ );
197
+
198
+ busboy.on('finish', function () {
199
+ readable.push(null);
200
+ response.set('Location', CrudController.getLink(response.req) + '/' + id);
201
+ response.json({
202
+ id,
203
+ fileName,
204
+ contentType: mimeType,
205
+ length: readable.readableLength,
206
+ });
207
+ response.end();
208
+ });
209
+
210
+ return request.pipe(busboy);
211
+ }
212
+
213
+ @Get('attachments/:id')
214
+ async downloadAttachment(
215
+ @Param('id') id: string,
216
+ @Req() request: Request,
217
+ @Res() response: Response,
218
+ ) {
219
+ const fileInfo = await this.service.getAttachmentInfo(id);
220
+
221
+ if (request.headers.range) {
222
+ const range = request.headers.range.substr(6).split('-');
223
+ const start = parseInt(range[0], 10);
224
+ const end = parseInt(range[1], 10) || null;
225
+
226
+ const readstream = await this.service.getAttachmentStream(id, {
227
+ start,
228
+ end,
229
+ });
230
+
231
+ response.status(206);
232
+ response.set({
233
+ 'Accept-Ranges': 'bytes',
234
+ 'Content-Type': fileInfo.contentType,
235
+ 'Content-Range': `bytes ${start}-${end ? end : fileInfo.length - 1}/${
236
+ fileInfo.length
237
+ }`,
238
+ 'Content-Length': (end ? end : fileInfo.length) - start,
239
+ 'Content-Disposition': `attachment; filename="${encodeURI(fileInfo.fileName)}"`,
240
+ });
241
+
242
+ response.on('close', () => {
243
+ readstream.destroy();
244
+ });
245
+
246
+ readstream.pipe(response);
247
+ } else {
248
+ const readstream = await this.service.getAttachmentStream(id);
249
+
250
+ response.on('close', () => {
251
+ readstream.destroy();
252
+ });
253
+
254
+ response.status(200);
255
+ response.set({
256
+ 'Accept-Range': 'bytes',
257
+ 'Content-Type': fileInfo.contentType,
258
+ 'Content-Length': fileInfo.length,
259
+ 'Content-Disposition': `attachment; filename="${encodeURI(fileInfo.fileName)}"`,
260
+ });
261
+
262
+ readstream.pipe(response);
263
+ }
264
+ }
265
+
266
+ @Delete('attachments/:id')
267
+ async deleteAttachment(@Param('id') id: string): Promise<void> {
268
+ await this.service.deleteAttachment(id);
269
+ }
270
+
271
+ protected getQueryObject(queryObject: any): { criteria; options; links } {
272
+ let q = '';
273
+
274
+ Object.keys(queryObject).forEach((key) => {
275
+ q += `&${key}=${queryObject[key]}`;
276
+ });
277
+
278
+ const result = q2m(q);
279
+
280
+ return result;
281
+ }
282
+
283
+ protected parseToXlsx(data: T[]) {
284
+ if (!data || !data.length) {
285
+ return '';
286
+ }
287
+
288
+ const { res } = this.getDataWithFields(data);
289
+
290
+ const ws = XLSX.utils.json_to_sheet(res);
291
+
292
+ const wb = XLSX.utils.book_new();
293
+ XLSX.utils.book_append_sheet(wb, ws, 'data');
294
+
295
+ return XLSX.write(wb, { bookType: 'xlsx', type: 'buffer' });
296
+ }
297
+
298
+ protected parseToCsv(data: T[]): string {
299
+ if (!data || !data.length) {
300
+ return '';
301
+ }
302
+
303
+ const { res, fields } = this.getDataWithFields(data);
304
+
305
+ return new Parser(fields).parse(data);
306
+ }
307
+
308
+ protected getDataWithFields(data: Array<T>): { res; fields } {
309
+ const fields = [];
310
+
311
+ const execute = (item, baseKey, baseItem) => {
312
+ Object.keys(item).forEach((key) => {
313
+ if (item[key] && typeof item[key] === 'string') {
314
+ item[key] = item[key].replace(/<[^>]*>?/gm, '');
315
+ }
316
+
317
+ if (item[key] && item[key] instanceof Date) {
318
+ item[key] = moment(item[key])
319
+ .tz('Europe/Warsaw')
320
+ .format('YYYY-MM-DD HH:mm:ss');
321
+ }
322
+
323
+ const val = item[key];
324
+
325
+ if (_.isArray(val)) {
326
+ return;
327
+ } else if (_.isObject(val) && Object.keys(val).length) {
328
+ execute(val, baseKey + key + '_', baseItem);
329
+ } else if (baseKey) {
330
+ baseItem[baseKey + key] = val;
331
+ if (!fields.some((f) => f === baseKey + key))
332
+ fields.push(baseKey + key);
333
+ } else {
334
+ if (!fields.some((f) => f === key)) fields.push(key);
335
+ }
336
+ });
337
+ };
338
+
339
+ data.forEach((item) => {
340
+ execute(item, '', item);
341
+ });
342
+
343
+ data.forEach((item) => {
344
+ Object.keys(item).forEach((key) => {
345
+ if (!fields.some((f) => f === key)) {
346
+ delete item[key];
347
+ }
348
+ });
349
+ });
350
+
351
+ return { res: data, fields };
352
+ }
353
+ }
@@ -0,0 +1,32 @@
1
+ import * as queryToMongo from './query-to-mongo';
2
+
3
+ describe('crud-nestjs: query-to-mongo', () => {
4
+ describe('q2m', () => {
5
+ it('should convert query string to mongo object', () => {
6
+ const result = queryToMongo.q2m('a=1&b=2');
7
+ expect(result).toHaveProperty('criteria');
8
+ });
9
+ it('should convert query object to mongo object', () => {
10
+ const result = queryToMongo.q2m({ a: '1', b: '2' });
11
+ expect(result).toHaveProperty('criteria');
12
+ });
13
+ it('should provide links function', () => {
14
+ const result = queryToMongo.q2m({ a: '1', limit: '2', offset: '0' });
15
+ expect(typeof result.links).toBe('function');
16
+ });
17
+ it('links should return null if no limit', () => {
18
+ const result = queryToMongo.q2m({ a: '1' });
19
+ expect(result.links('url', 10)).toBeNull();
20
+ });
21
+ it('links should return prev and first if offset > 0', () => {
22
+ const result = queryToMongo.q2m({ a: '1', limit: '2', offset: '2' });
23
+ const links = result.links('url', 10);
24
+ expect(links).toHaveProperty('prev');
25
+ });
26
+ it('links should return next and last if more pages', () => {
27
+ const result = queryToMongo.q2m({ a: '1', limit: '2', offset: '0' });
28
+ const links = result.links('url', 10);
29
+ expect(links).toHaveProperty('next');
30
+ });
31
+ });
32
+ });