@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.
- package/.eslintrc +13 -0
- package/README.md +37 -4
- package/jest.config.ts +27 -0
- package/package.json +2 -45
- package/project.json +43 -0
- package/src/index.ts +4 -0
- package/src/lib/controllers/crud/crud.controller.spec.ts +111 -0
- package/src/lib/controllers/crud/crud.controller.ts +353 -0
- package/src/lib/controllers/crud/query-to-mongo.spec.ts +32 -0
- package/src/lib/controllers/crud/query-to-mongo.ts +295 -0
- package/src/lib/controllers/index.ts +5 -0
- package/src/lib/gateways/crud/crud.gateway.spec.ts +122 -0
- package/src/lib/gateways/crud/crud.gateway.ts +72 -0
- package/src/lib/gateways/index.ts +3 -0
- package/src/lib/guards/auth/auth.guard.spec.ts +60 -0
- package/src/lib/guards/auth/auth.guard.ts +22 -0
- package/src/lib/nestjs.module.ts +94 -0
- package/tsconfig.json +13 -0
- package/tsconfig.lib.json +11 -0
- package/tsconfig.spec.json +20 -0
- package/src/index.d.ts +0 -4
- package/src/index.js +0 -8
- package/src/index.js.map +0 -1
- package/src/lib/controllers/crud/crud.controller.d.ts +0 -39
- package/src/lib/controllers/crud/crud.controller.js +0 -327
- package/src/lib/controllers/crud/crud.controller.js.map +0 -1
- package/src/lib/controllers/crud/query-to-mongo.d.ts +0 -5
- package/src/lib/controllers/crud/query-to-mongo.js +0 -286
- package/src/lib/controllers/crud/query-to-mongo.js.map +0 -1
- package/src/lib/controllers/index.d.ts +0 -3
- package/src/lib/controllers/index.js +0 -10
- package/src/lib/controllers/index.js.map +0 -1
- package/src/lib/gateways/crud/crud.gateway.d.ts +0 -18
- package/src/lib/gateways/crud/crud.gateway.js +0 -56
- package/src/lib/gateways/crud/crud.gateway.js.map +0 -1
- package/src/lib/gateways/index.d.ts +0 -2
- package/src/lib/gateways/index.js +0 -8
- package/src/lib/gateways/index.js.map +0 -1
- package/src/lib/guards/auth/auth.guard.d.ts +0 -10
- package/src/lib/guards/auth/auth.guard.js +0 -32
- package/src/lib/guards/auth/auth.guard.js.map +0 -1
- package/src/lib/nestjs.module.d.ts +0 -31
- package/src/lib/nestjs.module.js +0 -81
- package/src/lib/nestjs.module.js.map +0 -1
- package/test-setup.js +0 -1
- package/test-setup.js.map +0 -1
- /package/{test-setup.d.ts → test-setup.ts} +0 -0
package/.eslintrc
ADDED
package/README.md
CHANGED
|
@@ -1,7 +1,40 @@
|
|
|
1
|
-
# crud-shell-nestjs
|
|
1
|
+
# 📦 @smartsoft001/crud-shell-nestjs
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
 
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 🚀 Usage
|
|
6
6
|
|
|
7
|
-
|
|
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.
|
|
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,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
|
+
});
|