mm-share-lib 0.0.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/.eslintrc.js +25 -0
- package/.nvmrc +1 -0
- package/.prettierrc +4 -0
- package/README.md +73 -0
- package/index.ts +1 -0
- package/nest-cli.json +8 -0
- package/package.json +77 -0
- package/src/common/constant/entity-state.constant.ts +4 -0
- package/src/common/constant/index.ts +1 -0
- package/src/common/generic/entity/entity.generic.ts +34 -0
- package/src/common/generic/entity/index.ts +1 -0
- package/src/common/generic/index.ts +2 -0
- package/src/common/generic/service/index.ts +1 -0
- package/src/common/generic/service/service.generic.spec.ts +0 -0
- package/src/common/generic/service/service.generic.ts +112 -0
- package/src/common/index.ts +2 -0
- package/src/dto/base-filter.dto.ts +4 -0
- package/src/dto/index.ts +2 -0
- package/src/dto/pagination.dto.ts +4 -0
- package/src/index.ts +5 -0
- package/src/lib/index.ts +1 -0
- package/src/lib/search-engine/document/base.document.ts +3 -0
- package/src/lib/search-engine/document/index.ts +1 -0
- package/src/lib/search-engine/index.ts +1 -0
- package/src/lib/search-engine/interface/index.ts +2 -0
- package/src/lib/search-engine/interface/search-document.interface.ts +5 -0
- package/src/lib/search-engine/interface/transform-service.interface.ts +10 -0
- package/src/lib/search-engine/schema/generic.schema.ts +1 -0
- package/src/lib/search-engine/schema/index.ts +1 -0
- package/src/lib/search-engine/typesense/index.ts +3 -0
- package/src/lib/search-engine/typesense/metadata/index.ts +2 -0
- package/src/lib/search-engine/typesense/metadata/schema.metadata.ts +13 -0
- package/src/lib/search-engine/typesense/metadata/typesense.metadata-registry.ts +28 -0
- package/src/lib/search-engine/typesense/service/client.service.ts +258 -0
- package/src/lib/search-engine/typesense/service/index.ts +1 -0
- package/src/lib/search-engine/typesense/typesense-module.interface.ts +36 -0
- package/src/lib/search-engine/typesense/typesense.constants.ts +1 -0
- package/src/lib/search-engine/typesense/typesense.module.test.ts +94 -0
- package/src/lib/search-engine/typesense/typesense.module.ts +76 -0
- package/src/lib/search-engine/typesense/typesense.providers.ts +42 -0
- package/src/response/index.ts +1 -0
- package/src/response/pagination.response.ts +37 -0
- package/src/util/date.util.ts +1 -0
- package/src/util/generator.util.spec.ts +35 -0
- package/src/util/generator.util.ts +18 -0
- package/src/util/index.ts +2 -0
- package/test/app.e2e-spec.ts +24 -0
- package/test/jest-e2e.json +9 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +21 -0
package/.eslintrc.js
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
module.exports = {
|
2
|
+
parser: '@typescript-eslint/parser',
|
3
|
+
parserOptions: {
|
4
|
+
project: 'tsconfig.json',
|
5
|
+
tsconfigRootDir: __dirname,
|
6
|
+
sourceType: 'module',
|
7
|
+
},
|
8
|
+
plugins: ['@typescript-eslint/eslint-plugin'],
|
9
|
+
extends: [
|
10
|
+
'plugin:@typescript-eslint/recommended',
|
11
|
+
'plugin:prettier/recommended',
|
12
|
+
],
|
13
|
+
root: true,
|
14
|
+
env: {
|
15
|
+
node: true,
|
16
|
+
jest: true,
|
17
|
+
},
|
18
|
+
ignorePatterns: ['.eslintrc.js'],
|
19
|
+
rules: {
|
20
|
+
'@typescript-eslint/interface-name-prefix': 'off',
|
21
|
+
'@typescript-eslint/explicit-function-return-type': 'off',
|
22
|
+
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
23
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
24
|
+
},
|
25
|
+
};
|
package/.nvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
18.16.0
|
package/.prettierrc
ADDED
package/README.md
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
<p align="center">
|
2
|
+
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
|
3
|
+
</p>
|
4
|
+
|
5
|
+
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
6
|
+
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
7
|
+
|
8
|
+
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
9
|
+
<p align="center">
|
10
|
+
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
11
|
+
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
12
|
+
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
13
|
+
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
14
|
+
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
|
15
|
+
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
16
|
+
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
17
|
+
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
18
|
+
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
|
19
|
+
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
20
|
+
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
|
21
|
+
</p>
|
22
|
+
<!--[](https://opencollective.com/nest#backer)
|
23
|
+
[](https://opencollective.com/nest#sponsor)-->
|
24
|
+
|
25
|
+
## Description
|
26
|
+
|
27
|
+
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
28
|
+
|
29
|
+
## Installation
|
30
|
+
|
31
|
+
```bash
|
32
|
+
$ npm install
|
33
|
+
```
|
34
|
+
|
35
|
+
## Running the app
|
36
|
+
|
37
|
+
```bash
|
38
|
+
# development
|
39
|
+
$ npm run start
|
40
|
+
|
41
|
+
# watch mode
|
42
|
+
$ npm run start:dev
|
43
|
+
|
44
|
+
# production mode
|
45
|
+
$ npm run start:prod
|
46
|
+
```
|
47
|
+
|
48
|
+
## Test
|
49
|
+
|
50
|
+
```bash
|
51
|
+
# unit tests
|
52
|
+
$ npm run test
|
53
|
+
|
54
|
+
# e2e tests
|
55
|
+
$ npm run test:e2e
|
56
|
+
|
57
|
+
# test coverage
|
58
|
+
$ npm run test:cov
|
59
|
+
```
|
60
|
+
|
61
|
+
## Support
|
62
|
+
|
63
|
+
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
64
|
+
|
65
|
+
## Stay in touch
|
66
|
+
|
67
|
+
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
|
68
|
+
- Website - [https://nestjs.com](https://nestjs.com/)
|
69
|
+
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
70
|
+
|
71
|
+
## License
|
72
|
+
|
73
|
+
Nest is [MIT licensed](LICENSE).
|
package/index.ts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export * from './src';
|
package/nest-cli.json
ADDED
package/package.json
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
{
|
2
|
+
"name": "mm-share-lib",
|
3
|
+
"version": "0.0.1",
|
4
|
+
"description": "Share the generic service, entity, dto.",
|
5
|
+
"author": "Mesa SOT",
|
6
|
+
"license": "MIT",
|
7
|
+
"homepage": "https://github.com/memotechs/share-lib",
|
8
|
+
"scripts": {
|
9
|
+
"build": "nest build",
|
10
|
+
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
11
|
+
"start": "nest start",
|
12
|
+
"start:dev": "nest start --watch",
|
13
|
+
"start:debug": "nest start --debug --watch",
|
14
|
+
"start:prod": "node dist/main",
|
15
|
+
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
16
|
+
"test": "jest",
|
17
|
+
"test:watch": "jest --watch",
|
18
|
+
"test:cov": "jest --coverage",
|
19
|
+
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
20
|
+
"test:e2e": "jest --config ./test/jest-e2e.json",
|
21
|
+
"prepare": "husky install"
|
22
|
+
},
|
23
|
+
"dependencies": {
|
24
|
+
"@babel/runtime": "^7.22.5",
|
25
|
+
"@nestjs/common": "^9.4.2",
|
26
|
+
"@nestjs/core": "^9.4.2",
|
27
|
+
"@nestjs/platform-express": "^9.4.2",
|
28
|
+
"@nestjs/typeorm": "^9.0.1",
|
29
|
+
"class-transformer": "^0.5.1",
|
30
|
+
"class-validator": "^0.14.0",
|
31
|
+
"reflect-metadata": "^0.1.13",
|
32
|
+
"rxjs": "^7.8.1",
|
33
|
+
"typeorm": "^0.3.16",
|
34
|
+
"typesense": "^1.5.4"
|
35
|
+
},
|
36
|
+
"devDependencies": {
|
37
|
+
"@nestjs/cli": "^9.5.0",
|
38
|
+
"@nestjs/schematics": "^9.2.0",
|
39
|
+
"@nestjs/testing": "^9.4.2",
|
40
|
+
"@types/express": "^4.17.17",
|
41
|
+
"@types/jest": "29.5.2",
|
42
|
+
"@types/node": "20.2.5",
|
43
|
+
"@types/supertest": "^2.0.12",
|
44
|
+
"@typescript-eslint/eslint-plugin": "^5.59.9",
|
45
|
+
"@typescript-eslint/parser": "^5.59.9",
|
46
|
+
"eslint": "^8.42.0",
|
47
|
+
"eslint-config-prettier": "^8.8.0",
|
48
|
+
"eslint-plugin-prettier": "^4.2.1",
|
49
|
+
"husky": "^8.0.3",
|
50
|
+
"jest": "29.5.0",
|
51
|
+
"prettier": "^2.8.8",
|
52
|
+
"source-map-support": "^0.5.21",
|
53
|
+
"supertest": "^6.3.3",
|
54
|
+
"ts-jest": "29.1.0",
|
55
|
+
"ts-loader": "^9.4.3",
|
56
|
+
"ts-node": "^10.9.1",
|
57
|
+
"tsconfig-paths": "4.2.0",
|
58
|
+
"typescript": "^5.1.3"
|
59
|
+
},
|
60
|
+
"jest": {
|
61
|
+
"moduleFileExtensions": [
|
62
|
+
"js",
|
63
|
+
"json",
|
64
|
+
"ts"
|
65
|
+
],
|
66
|
+
"rootDir": "src",
|
67
|
+
"testRegex": ".*\\.spec\\.ts$",
|
68
|
+
"transform": {
|
69
|
+
"^.+\\.(t|j)s$": "ts-jest"
|
70
|
+
},
|
71
|
+
"collectCoverageFrom": [
|
72
|
+
"**/*.(t|j)s"
|
73
|
+
],
|
74
|
+
"coverageDirectory": "../coverage",
|
75
|
+
"testEnvironment": "node"
|
76
|
+
}
|
77
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from './entity-state.constant';
|
@@ -0,0 +1,34 @@
|
|
1
|
+
import {
|
2
|
+
Column,
|
3
|
+
VersionColumn,
|
4
|
+
CreateDateColumn,
|
5
|
+
UpdateDateColumn,
|
6
|
+
PrimaryGeneratedColumn,
|
7
|
+
} from 'typeorm';
|
8
|
+
|
9
|
+
export class EntityGeneric {
|
10
|
+
constructor(id?: number) {
|
11
|
+
this.id = id;
|
12
|
+
}
|
13
|
+
|
14
|
+
@PrimaryGeneratedColumn()
|
15
|
+
id: number;
|
16
|
+
|
17
|
+
@CreateDateColumn()
|
18
|
+
createdAt: Date;
|
19
|
+
|
20
|
+
@UpdateDateColumn()
|
21
|
+
updatedAt: Date;
|
22
|
+
|
23
|
+
@Column()
|
24
|
+
createdBy: number;
|
25
|
+
|
26
|
+
@Column({ select: false })
|
27
|
+
updatedBy: number;
|
28
|
+
|
29
|
+
@Column()
|
30
|
+
state: number;
|
31
|
+
|
32
|
+
@VersionColumn({ select: false })
|
33
|
+
version: number;
|
34
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from './entity.generic';
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from './service.generic';
|
File without changes
|
@@ -0,0 +1,112 @@
|
|
1
|
+
import {
|
2
|
+
Repository,
|
3
|
+
Connection,
|
4
|
+
EntityManager,
|
5
|
+
SelectQueryBuilder,
|
6
|
+
} from 'typeorm';
|
7
|
+
import { EntityGeneric } from '../entity';
|
8
|
+
import { PaginationDto, BaseFilterDto } from '../../../dto';
|
9
|
+
import { EntityStateConstant } from '../../constant';
|
10
|
+
import { PaginationResponse } from '../../../response';
|
11
|
+
|
12
|
+
export abstract class ServiceGeneric<
|
13
|
+
Entity extends EntityGeneric,
|
14
|
+
CustomRepository extends Repository<Entity>,
|
15
|
+
> {
|
16
|
+
protected readonly entityName: string;
|
17
|
+
protected readonly loggable: boolean = false;
|
18
|
+
protected repository: CustomRepository;
|
19
|
+
protected connection: Connection;
|
20
|
+
constructor(
|
21
|
+
protected readonly connectionOrManager: Connection | EntityManager,
|
22
|
+
repositoryType: { new (connection: Connection): CustomRepository },
|
23
|
+
) {
|
24
|
+
if (connectionOrManager instanceof EntityManager) {
|
25
|
+
this.connection = connectionOrManager.connection;
|
26
|
+
} else {
|
27
|
+
this.connection = connectionOrManager;
|
28
|
+
}
|
29
|
+
this.repository = this.connection.getCustomRepository(repositoryType);
|
30
|
+
}
|
31
|
+
|
32
|
+
create = async (entity: Entity): Promise<Entity> => {
|
33
|
+
return this.repository.save(entity);
|
34
|
+
};
|
35
|
+
|
36
|
+
update = async (entity: Entity): Promise<Entity> => {
|
37
|
+
return this.repository.save(entity);
|
38
|
+
};
|
39
|
+
|
40
|
+
getListWithPagination = async (
|
41
|
+
paginationDto: PaginationDto,
|
42
|
+
callback?: (query: SelectQueryBuilder<Entity>) => void,
|
43
|
+
): Promise<PaginationResponse<Entity>> => {
|
44
|
+
const { limit = 25, offset = 0 } = paginationDto;
|
45
|
+
const query = this.repository.createQueryBuilder(this.entityName);
|
46
|
+
query.limit(limit);
|
47
|
+
query.offset(offset);
|
48
|
+
query.where(`${this.entityName}.state != :state`, {
|
49
|
+
state: EntityStateConstant.Archived,
|
50
|
+
});
|
51
|
+
const defaultSelectable = ['createdAt', 'updatedAt'];
|
52
|
+
query.orderBy(`${this.entityName}.updatedAt`, 'DESC');
|
53
|
+
const selection = defaultSelectable.map(
|
54
|
+
(column: string) => `${this.entityName}.${column}`,
|
55
|
+
);
|
56
|
+
query.addSelect(selection);
|
57
|
+
if (callback != null) {
|
58
|
+
callback(query);
|
59
|
+
}
|
60
|
+
const entities = await query.getMany();
|
61
|
+
const total = await query.getCount();
|
62
|
+
const response = new PaginationResponse(entities, total, limit, offset);
|
63
|
+
return response;
|
64
|
+
};
|
65
|
+
|
66
|
+
getAutocompleteWithPagination = async (
|
67
|
+
paginationDto: PaginationDto,
|
68
|
+
filter: BaseFilterDto,
|
69
|
+
callback?: (query: SelectQueryBuilder<Entity>) => void,
|
70
|
+
): Promise<PaginationResponse<Entity>> => {
|
71
|
+
const { limit = 25, offset = 0 } = paginationDto;
|
72
|
+
const query = this.repository.createQueryBuilder(this.entityName);
|
73
|
+
query.limit(limit);
|
74
|
+
query.offset(offset);
|
75
|
+
query.where(`${this.entityName}.state != :state`, {
|
76
|
+
state: EntityStateConstant.Archived,
|
77
|
+
});
|
78
|
+
const defaultSelectable = ['createdAt', 'updatedAt'];
|
79
|
+
query.orderBy(`${this.entityName}.updatedAt`, 'DESC');
|
80
|
+
const selection = defaultSelectable.map(
|
81
|
+
(column: string) => `${this.entityName}.${column}`,
|
82
|
+
);
|
83
|
+
query.addSelect(selection);
|
84
|
+
if (callback != null) {
|
85
|
+
callback(query);
|
86
|
+
}
|
87
|
+
const { excludeIds = [], includeIds = [] } = { ...filter };
|
88
|
+
// Exclude some ids from the list.
|
89
|
+
if (excludeIds?.length > 0) {
|
90
|
+
query.andWhere(`${this.entityName}.id NOT IN (:ids)`, {
|
91
|
+
ids: excludeIds,
|
92
|
+
});
|
93
|
+
}
|
94
|
+
let entities = await query.getMany();
|
95
|
+
const total = await query.getCount();
|
96
|
+
// Include some ids to the list.
|
97
|
+
const allIds: number[] = [];
|
98
|
+
for (const id of includeIds) {
|
99
|
+
const entity = entities.filter((entity: Entity) => entity.id === id);
|
100
|
+
if (entity.length == 0) {
|
101
|
+
allIds.push(id);
|
102
|
+
}
|
103
|
+
}
|
104
|
+
if (allIds.length > 0) {
|
105
|
+
query.where(`${this.entityName}.id IN (:ids)`, { ids: allIds });
|
106
|
+
const data = await query.getMany();
|
107
|
+
entities = data.concat(entities);
|
108
|
+
}
|
109
|
+
const response = new PaginationResponse(entities, total, limit, offset);
|
110
|
+
return response;
|
111
|
+
};
|
112
|
+
}
|
package/src/dto/index.ts
ADDED
package/src/index.ts
ADDED
package/src/lib/index.ts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
export * from './search-engine';
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from './base.document';
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from 'typesense';
|
@@ -0,0 +1,5 @@
|
|
1
|
+
import { SearchOptions, SearchParams, SearchResponse, DocumentSchema } from 'typesense/lib/Typesense/Documents';
|
2
|
+
|
3
|
+
export interface SearchDocumentService<Document extends DocumentSchema> {
|
4
|
+
searchDocument(searchParameters: SearchParams, options: SearchOptions): Promise<SearchResponse<any>>;
|
5
|
+
}
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import { EntityGeneric } from '../../../common';
|
2
|
+
import { BaseDocument } from '../document';
|
3
|
+
|
4
|
+
export interface TransformerService<
|
5
|
+
T extends EntityGeneric,
|
6
|
+
Document extends BaseDocument,
|
7
|
+
> {
|
8
|
+
transform(data: T): Document;
|
9
|
+
transforms(datas: T[]): Document[];
|
10
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export class GenericSchema {}
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from './generic.schema';
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import { Injectable, Logger } from '@nestjs/common'
|
2
|
+
|
3
|
+
import { Schema } from './schema.metadata'
|
4
|
+
|
5
|
+
type Constructor = new (...args: any[]) => Record<string, unknown> // {}
|
6
|
+
|
7
|
+
@Injectable()
|
8
|
+
export class TypesenseMetadataRegistry {
|
9
|
+
private logger = new Logger(TypesenseMetadataRegistry.name)
|
10
|
+
|
11
|
+
private schemas: Map<Constructor, Schema> = new Map()
|
12
|
+
|
13
|
+
addSchema(target: Constructor, schema: Schema) {
|
14
|
+
if (this.schemas.has(target)) {
|
15
|
+
this.logger.warn(`Schema ${target} already exists`)
|
16
|
+
}
|
17
|
+
|
18
|
+
this.schemas.set(target, schema)
|
19
|
+
}
|
20
|
+
|
21
|
+
getSchemaByTarget(target: Constructor) {
|
22
|
+
return this.schemas.get(target)
|
23
|
+
}
|
24
|
+
|
25
|
+
getTargets() {
|
26
|
+
return this.schemas.keys()
|
27
|
+
}
|
28
|
+
}
|
@@ -0,0 +1,258 @@
|
|
1
|
+
import { Client } from 'typesense';
|
2
|
+
import { Logger, Type } from '@nestjs/common';
|
3
|
+
import { plainToClass } from 'class-transformer';
|
4
|
+
import { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
|
5
|
+
import {
|
6
|
+
SearchParams,
|
7
|
+
SearchOptions,
|
8
|
+
SearchResponse,
|
9
|
+
DeleteResponse,
|
10
|
+
ImportResponse,
|
11
|
+
} from 'typesense/lib/Typesense/Documents';
|
12
|
+
import { BaseDocument } from '../../document';
|
13
|
+
import { SearchDocumentService } from '../../interface';
|
14
|
+
import { EntityGeneric } from '../../../../common';
|
15
|
+
import { PaginationResponse } from '../../../../response';
|
16
|
+
|
17
|
+
export abstract class ClientService<
|
18
|
+
Document extends BaseDocument,
|
19
|
+
Entity extends EntityGeneric,
|
20
|
+
> implements SearchDocumentService<Document>
|
21
|
+
{
|
22
|
+
protected readonly entity: Type<Entity>;
|
23
|
+
protected abstract cache_s: number;
|
24
|
+
protected readonly skipCheckSchema: boolean = false;
|
25
|
+
constructor(
|
26
|
+
protected readonly client: Client,
|
27
|
+
protected readonly schema: CollectionCreateSchema,
|
28
|
+
protected readonly prefix: string,
|
29
|
+
) {
|
30
|
+
if (!this.skipCheckSchema && schema != null) {
|
31
|
+
this.ensureCollection();
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
// searchDocument(searchParameters: SearchParams, options: SearchOptions) {
|
36
|
+
// const {
|
37
|
+
// per_page = 25,
|
38
|
+
// filter_by,
|
39
|
+
// archived = false,
|
40
|
+
// } = { ...searchParameters };
|
41
|
+
// searchParameters.per_page = per_page;
|
42
|
+
// return this.client.collections(this?.schema?.name || this.prefix).documents().search(searchParameters, options);
|
43
|
+
// }
|
44
|
+
|
45
|
+
async searchDocument(
|
46
|
+
searchParameters: SearchParams,
|
47
|
+
options: SearchOptions,
|
48
|
+
): Promise<SearchResponse<any>> {
|
49
|
+
try {
|
50
|
+
const { includeIds = [], per_page = 25 } = { ...searchParameters };
|
51
|
+
// TODO: should be support with include/exclude ids.
|
52
|
+
const includeDocuments = [];
|
53
|
+
if (includeIds.length > 0) {
|
54
|
+
const includeOpts = {
|
55
|
+
...searchParameters,
|
56
|
+
filter_by: `id:=[${includeIds.join(', ')}]`,
|
57
|
+
};
|
58
|
+
const includeDocs = await this.client
|
59
|
+
.collections(this?.schema?.name || this.prefix)
|
60
|
+
.documents()
|
61
|
+
.search(includeOpts, options);
|
62
|
+
if (includeDocs?.hits?.length > 0) {
|
63
|
+
const names = includeDocs.hits.map(
|
64
|
+
(data: Record<string, any>) => data.document.name,
|
65
|
+
);
|
66
|
+
searchParameters.filter_by =
|
67
|
+
searchParameters.filter_by == null
|
68
|
+
? `name:!=[${names.join(', ')}]`
|
69
|
+
: `${searchParameters.filter_by} && name:!=[${names.join(', ')}]`;
|
70
|
+
searchParameters.per_page = per_page - names.length;
|
71
|
+
}
|
72
|
+
includeDocuments.push(...includeDocs?.hits);
|
73
|
+
}
|
74
|
+
const documents = await this.client
|
75
|
+
.collections(this?.schema?.name || this.prefix)
|
76
|
+
.documents()
|
77
|
+
.search(searchParameters, options);
|
78
|
+
if (includeDocuments.length > 0) {
|
79
|
+
documents.hits.push(...includeDocuments);
|
80
|
+
documents.request_params.per_page = per_page;
|
81
|
+
}
|
82
|
+
return documents;
|
83
|
+
} catch (error) {
|
84
|
+
return null;
|
85
|
+
}
|
86
|
+
}
|
87
|
+
|
88
|
+
getAllRawDocs = async (
|
89
|
+
searchParameters: SearchParams,
|
90
|
+
options: SearchOptions,
|
91
|
+
) => {
|
92
|
+
searchParameters.per_page = searchParameters.per_page ?? 250;
|
93
|
+
options.cacheSearchResultsForSeconds = 1;
|
94
|
+
const response = await this.searchDocument(searchParameters, options);
|
95
|
+
const { hits = [], found = 0, page = 1 } = response;
|
96
|
+
let documents = hits;
|
97
|
+
const hasNext = hits.length * page < found;
|
98
|
+
if (hasNext) {
|
99
|
+
searchParameters.page = page + 1;
|
100
|
+
documents = await this.getAllRawDocs(searchParameters, options);
|
101
|
+
}
|
102
|
+
return documents;
|
103
|
+
};
|
104
|
+
|
105
|
+
async importIndex(data: Document[]): Promise<ImportResponse[]> {
|
106
|
+
if (data.length > 0) {
|
107
|
+
try {
|
108
|
+
return this.client
|
109
|
+
.collections(this?.schema?.name || this.prefix)
|
110
|
+
.documents()
|
111
|
+
.import(data, { action: 'upsert' });
|
112
|
+
} catch (error) {
|
113
|
+
console.log(error);
|
114
|
+
}
|
115
|
+
}
|
116
|
+
}
|
117
|
+
|
118
|
+
async deleteOutOfDate(data: string[] | number[], key: string): Promise<any> {
|
119
|
+
if (data?.length > 0) {
|
120
|
+
const deleteParameters = {
|
121
|
+
filter_by: `${key}:!=[${data.join(', ')}]`,
|
122
|
+
};
|
123
|
+
return await this.client
|
124
|
+
.collections(this?.schema?.name || this.prefix)
|
125
|
+
.documents()
|
126
|
+
.delete(deleteParameters);
|
127
|
+
}
|
128
|
+
}
|
129
|
+
|
130
|
+
async insertIndex(data: Document): Promise<any> {
|
131
|
+
return this.client
|
132
|
+
.collections(this?.schema?.name || this.prefix)
|
133
|
+
.documents()
|
134
|
+
.create(data, { action: 'upsert' });
|
135
|
+
}
|
136
|
+
|
137
|
+
async updateIndex(data: Document): Promise<any> {
|
138
|
+
return this.client
|
139
|
+
.collections(this?.schema?.name || this.prefix)
|
140
|
+
.documents()
|
141
|
+
.upsert(data, { action: 'upsert' });
|
142
|
+
}
|
143
|
+
|
144
|
+
updateDocumentById = async (data: Document) => {
|
145
|
+
if (data?.id) {
|
146
|
+
const exist = await this.client
|
147
|
+
.collections(this?.schema?.name || this.prefix)
|
148
|
+
.documents(data.id)
|
149
|
+
.retrieve();
|
150
|
+
if (exist) {
|
151
|
+
return this.client
|
152
|
+
.collections(this?.schema?.name || this.prefix)
|
153
|
+
.documents(data.id)
|
154
|
+
.update(data);
|
155
|
+
}
|
156
|
+
}
|
157
|
+
};
|
158
|
+
|
159
|
+
async upsertOrDeleteIndex(datas: Document) {
|
160
|
+
throw new Error('Method is not implement.');
|
161
|
+
}
|
162
|
+
|
163
|
+
async deleteIndex(data: Document): Promise<DeleteResponse> {
|
164
|
+
return this.client
|
165
|
+
.collections(this?.schema?.name || this.prefix)
|
166
|
+
.documents()
|
167
|
+
.delete({ filter_by: `id: ${data.id}` });
|
168
|
+
}
|
169
|
+
|
170
|
+
async deleteBatchIndex(ids: string[]): Promise<DeleteResponse> {
|
171
|
+
return this.client
|
172
|
+
.collections(this?.schema?.name || this.prefix)
|
173
|
+
.documents()
|
174
|
+
.delete({ filter_by: `id: [${ids.join(',')}]` });
|
175
|
+
}
|
176
|
+
|
177
|
+
async deleteIndexByKeyValue(
|
178
|
+
key = 'id',
|
179
|
+
value: number,
|
180
|
+
): Promise<DeleteResponse> {
|
181
|
+
return this.client
|
182
|
+
.collections(this?.schema?.name || this.prefix)
|
183
|
+
.documents()
|
184
|
+
.delete({ filter_by: `${key}:=${value}` });
|
185
|
+
}
|
186
|
+
|
187
|
+
transforms = (
|
188
|
+
searchResponse: SearchResponse<Record<string, unknown>>,
|
189
|
+
): Entity[] => {
|
190
|
+
const entities: Entity[] = [];
|
191
|
+
const { hits = [] } = { ...searchResponse };
|
192
|
+
for (const hit of hits) {
|
193
|
+
const document = hit?.document;
|
194
|
+
if (document) {
|
195
|
+
const entity = plainToClass(this.entity, document);
|
196
|
+
entity.id = Number(document.id);
|
197
|
+
entities.push(entity);
|
198
|
+
}
|
199
|
+
}
|
200
|
+
return entities;
|
201
|
+
};
|
202
|
+
|
203
|
+
transform = (
|
204
|
+
searchResponse: SearchResponse<Record<string, unknown>>,
|
205
|
+
): Entity => {
|
206
|
+
let entity: Entity = null;
|
207
|
+
const { hits = [] } = { ...searchResponse };
|
208
|
+
for (const hit of hits) {
|
209
|
+
const document = hit?.document;
|
210
|
+
if (document) {
|
211
|
+
entity = plainToClass(this.entity, document);
|
212
|
+
entity.id = Number(document.id);
|
213
|
+
}
|
214
|
+
break;
|
215
|
+
}
|
216
|
+
return entity;
|
217
|
+
};
|
218
|
+
|
219
|
+
responseList = (
|
220
|
+
searchResponse: SearchResponse<Record<string, unknown>>,
|
221
|
+
offset = 0,
|
222
|
+
): PaginationResponse<Entity> => {
|
223
|
+
const entities: Entity[] = [];
|
224
|
+
const {
|
225
|
+
hits = [],
|
226
|
+
found = 0,
|
227
|
+
page = 1,
|
228
|
+
request_params,
|
229
|
+
} = { ...searchResponse };
|
230
|
+
for (const hit of hits) {
|
231
|
+
const document = hit?.document;
|
232
|
+
if (document) {
|
233
|
+
const entity = plainToClass(this.entity, document);
|
234
|
+
entity.id = Number(document.id);
|
235
|
+
entities.push(entity);
|
236
|
+
}
|
237
|
+
}
|
238
|
+
|
239
|
+
const limit = request_params?.per_page;
|
240
|
+
|
241
|
+
// offset = offset ?? (page - 1) * limit;
|
242
|
+
|
243
|
+
return new PaginationResponse(entities, found, limit, offset);
|
244
|
+
};
|
245
|
+
|
246
|
+
private ensureCollection = async (): Promise<void> => {
|
247
|
+
this.checkSchemaName();
|
248
|
+
const exists = await this.client.collections(this.schema.name).exists();
|
249
|
+
if (!exists) {
|
250
|
+
await this.client.collections().create(this.schema);
|
251
|
+
}
|
252
|
+
};
|
253
|
+
|
254
|
+
private checkSchemaName = (): void => {
|
255
|
+
const schemaName = this.schema.name.replace(/.*?\_/gi, '');
|
256
|
+
this.schema.name = `${this.prefix}_${schemaName}`;
|
257
|
+
};
|
258
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from './client.service';
|
@@ -0,0 +1,36 @@
|
|
1
|
+
import { ModuleMetadata, Type } from '@nestjs/common/interfaces';
|
2
|
+
import { LogLevelDesc } from 'loglevel';
|
3
|
+
|
4
|
+
export interface TypesenseNodeOptions {
|
5
|
+
host: string;
|
6
|
+
port: number;
|
7
|
+
protocol: string;
|
8
|
+
path?: string;
|
9
|
+
url?: string;
|
10
|
+
}
|
11
|
+
|
12
|
+
export interface TypesenseModuleOptions {
|
13
|
+
nodes?: Array<TypesenseNodeOptions>;
|
14
|
+
numRetries?: number;
|
15
|
+
apiKey?: string;
|
16
|
+
connectionTimeoutSeconds?: number;
|
17
|
+
retryIntervalSeconds?: number;
|
18
|
+
healthcheckIntervalSeconds?: number;
|
19
|
+
logLevel?: LogLevelDesc;
|
20
|
+
}
|
21
|
+
|
22
|
+
export interface TypesenseOptionsFactory {
|
23
|
+
createTypesenseOptions():
|
24
|
+
| Promise<TypesenseModuleOptions>
|
25
|
+
| TypesenseModuleOptions;
|
26
|
+
}
|
27
|
+
|
28
|
+
export interface TypesenseModuleAsyncOptions
|
29
|
+
extends Pick<ModuleMetadata, 'imports'> {
|
30
|
+
useExisting?: Type<TypesenseOptionsFactory>;
|
31
|
+
useClass?: Type<TypesenseOptionsFactory>;
|
32
|
+
useFactory?: (
|
33
|
+
...args: any[]
|
34
|
+
) => Promise<TypesenseModuleOptions> | TypesenseModuleOptions;
|
35
|
+
inject?: any[];
|
36
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export const TYPESENSE_MODULE_OPTIONS = 'TYPESENSE_MODULE_OPTIONS';
|
@@ -0,0 +1,94 @@
|
|
1
|
+
/* eslint-disable max-classes-per-file */
|
2
|
+
|
3
|
+
import { Module } from '@nestjs/common'
|
4
|
+
import { Test } from '@nestjs/testing'
|
5
|
+
|
6
|
+
import { TypesenseModuleOptions } from './typesense-module.interface'
|
7
|
+
import { TYPESENSE_MODULE_OPTIONS } from './typesense.constants'
|
8
|
+
import { TypesenseModule } from './typesense.module'
|
9
|
+
|
10
|
+
describe('typesense', () => {
|
11
|
+
describe('module', () => {
|
12
|
+
let module
|
13
|
+
|
14
|
+
afterEach(async () => {
|
15
|
+
await module.close()
|
16
|
+
})
|
17
|
+
|
18
|
+
it(`register`, async () => {
|
19
|
+
module = await Test.createTestingModule({
|
20
|
+
imports: [
|
21
|
+
TypesenseModule.register({
|
22
|
+
apiKey: 'test',
|
23
|
+
}),
|
24
|
+
],
|
25
|
+
}).compile()
|
26
|
+
|
27
|
+
expect(module.get(TYPESENSE_MODULE_OPTIONS)).toBeDefined()
|
28
|
+
})
|
29
|
+
|
30
|
+
it(`register async use factory`, async () => {
|
31
|
+
module = await Test.createTestingModule({
|
32
|
+
imports: [
|
33
|
+
TypesenseModule.registerAsync({
|
34
|
+
useFactory: () => ({
|
35
|
+
apiKey: 'test',
|
36
|
+
}),
|
37
|
+
}),
|
38
|
+
],
|
39
|
+
}).compile()
|
40
|
+
|
41
|
+
expect(module.get(TYPESENSE_MODULE_OPTIONS)).toBeDefined()
|
42
|
+
})
|
43
|
+
|
44
|
+
it(`register async use class`, async () => {
|
45
|
+
class TestTypesenseModuleOptions {
|
46
|
+
createTypesenseOptions(): TypesenseModuleOptions {
|
47
|
+
return {
|
48
|
+
apiKey: 'test',
|
49
|
+
}
|
50
|
+
}
|
51
|
+
}
|
52
|
+
|
53
|
+
module = await Test.createTestingModule({
|
54
|
+
imports: [
|
55
|
+
TypesenseModule.registerAsync({
|
56
|
+
useClass: TestTypesenseModuleOptions,
|
57
|
+
}),
|
58
|
+
],
|
59
|
+
}).compile()
|
60
|
+
|
61
|
+
expect(module.get(TYPESENSE_MODULE_OPTIONS)).toBeDefined()
|
62
|
+
})
|
63
|
+
|
64
|
+
it(`register async use exists`, async () => {
|
65
|
+
class TestTypesenseModuleOptions {
|
66
|
+
createTypesenseOptions(): TypesenseModuleOptions {
|
67
|
+
return {
|
68
|
+
apiKey: 'test',
|
69
|
+
}
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
@Module({})
|
74
|
+
class TestTypesenseModule {}
|
75
|
+
|
76
|
+
module = await Test.createTestingModule({
|
77
|
+
imports: [
|
78
|
+
TypesenseModule.registerAsync({
|
79
|
+
imports: [
|
80
|
+
{
|
81
|
+
module: TestTypesenseModule,
|
82
|
+
providers: [TestTypesenseModuleOptions],
|
83
|
+
exports: [TestTypesenseModuleOptions],
|
84
|
+
},
|
85
|
+
],
|
86
|
+
useExisting: TestTypesenseModuleOptions,
|
87
|
+
}),
|
88
|
+
],
|
89
|
+
}).compile()
|
90
|
+
|
91
|
+
expect(module.get(TYPESENSE_MODULE_OPTIONS)).toBeDefined()
|
92
|
+
})
|
93
|
+
})
|
94
|
+
})
|
@@ -0,0 +1,76 @@
|
|
1
|
+
import { DynamicModule, Module, Provider } from '@nestjs/common';
|
2
|
+
import { DiscoveryModule } from '@nestjs/core';
|
3
|
+
import {
|
4
|
+
TypesenseModuleAsyncOptions,
|
5
|
+
TypesenseOptionsFactory,
|
6
|
+
TypesenseModuleOptions,
|
7
|
+
} from './typesense-module.interface';
|
8
|
+
import { TYPESENSE_MODULE_OPTIONS } from './typesense.constants';
|
9
|
+
import {
|
10
|
+
createTypesenseExportsProvider,
|
11
|
+
createTypesenseOptionsProvider,
|
12
|
+
} from './typesense.providers';
|
13
|
+
|
14
|
+
@Module({
|
15
|
+
imports: [DiscoveryModule],
|
16
|
+
})
|
17
|
+
export class TypesenseModule {
|
18
|
+
static register(options: TypesenseModuleOptions = {}): DynamicModule {
|
19
|
+
const optionsProviders = createTypesenseOptionsProvider(options);
|
20
|
+
const exportsProviders = createTypesenseExportsProvider();
|
21
|
+
|
22
|
+
return {
|
23
|
+
global: true,
|
24
|
+
module: TypesenseModule,
|
25
|
+
providers: [...optionsProviders, ...exportsProviders],
|
26
|
+
exports: exportsProviders,
|
27
|
+
};
|
28
|
+
}
|
29
|
+
|
30
|
+
static registerAsync(options: TypesenseModuleAsyncOptions): DynamicModule {
|
31
|
+
const exportsProviders = createTypesenseExportsProvider();
|
32
|
+
|
33
|
+
return {
|
34
|
+
global: true,
|
35
|
+
module: TypesenseModule,
|
36
|
+
imports: options.imports || [],
|
37
|
+
providers: [...this.createAsyncProviders(options), ...exportsProviders],
|
38
|
+
exports: exportsProviders,
|
39
|
+
};
|
40
|
+
}
|
41
|
+
|
42
|
+
private static createAsyncProviders(
|
43
|
+
options: TypesenseModuleAsyncOptions,
|
44
|
+
): Provider[] {
|
45
|
+
if (options.useExisting || options.useFactory) {
|
46
|
+
return [this.createAsyncOptionsProvider(options)];
|
47
|
+
}
|
48
|
+
|
49
|
+
return [
|
50
|
+
this.createAsyncOptionsProvider(options),
|
51
|
+
{
|
52
|
+
provide: options?.useClass,
|
53
|
+
useClass: options?.useClass,
|
54
|
+
},
|
55
|
+
];
|
56
|
+
}
|
57
|
+
|
58
|
+
private static createAsyncOptionsProvider(
|
59
|
+
options: TypesenseModuleAsyncOptions,
|
60
|
+
): Provider {
|
61
|
+
if (options.useFactory) {
|
62
|
+
return {
|
63
|
+
provide: TYPESENSE_MODULE_OPTIONS,
|
64
|
+
useFactory: options.useFactory,
|
65
|
+
inject: options.inject || [],
|
66
|
+
};
|
67
|
+
}
|
68
|
+
|
69
|
+
return {
|
70
|
+
provide: TYPESENSE_MODULE_OPTIONS,
|
71
|
+
useFactory: (optionsFactory: TypesenseOptionsFactory) =>
|
72
|
+
optionsFactory.createTypesenseOptions(),
|
73
|
+
inject: [options?.useExisting || options?.useClass],
|
74
|
+
};
|
75
|
+
}
|
76
|
+
}
|
@@ -0,0 +1,42 @@
|
|
1
|
+
import { Provider } from '@nestjs/common';
|
2
|
+
import { Client } from 'typesense';
|
3
|
+
import { TypesenseMetadataRegistry } from './metadata';
|
4
|
+
import { TypesenseModuleOptions } from './typesense-module.interface';
|
5
|
+
import { TYPESENSE_MODULE_OPTIONS } from './typesense.constants';
|
6
|
+
|
7
|
+
export const createTypesenseOptionsProvider = (
|
8
|
+
options: TypesenseModuleOptions = {},
|
9
|
+
): Provider[] => [
|
10
|
+
{
|
11
|
+
provide: TYPESENSE_MODULE_OPTIONS,
|
12
|
+
useValue: options,
|
13
|
+
},
|
14
|
+
];
|
15
|
+
|
16
|
+
export const createTypesenseExportsProvider = (): Provider[] => [
|
17
|
+
TypesenseMetadataRegistry,
|
18
|
+
{
|
19
|
+
provide: Client,
|
20
|
+
useFactory: (options: TypesenseModuleOptions) =>
|
21
|
+
new Client({
|
22
|
+
nodes: options.nodes || [
|
23
|
+
{
|
24
|
+
host:
|
25
|
+
process.env.TYPESENSE_HOST ||
|
26
|
+
process.env.NODE_ENV === 'production'
|
27
|
+
? 'ts.typesense.svc.cluster.local'
|
28
|
+
: 'localhost',
|
29
|
+
port: 8108,
|
30
|
+
protocol: 'http',
|
31
|
+
},
|
32
|
+
],
|
33
|
+
numRetries: options.numRetries || 10,
|
34
|
+
apiKey: options.apiKey || process.env.TYPESENSE_API_KEY,
|
35
|
+
connectionTimeoutSeconds: options.connectionTimeoutSeconds || 10,
|
36
|
+
retryIntervalSeconds: options.retryIntervalSeconds || 0.1,
|
37
|
+
healthcheckIntervalSeconds: options.healthcheckIntervalSeconds || 2,
|
38
|
+
logLevel: options.logLevel || 'info',
|
39
|
+
}),
|
40
|
+
inject: [TYPESENSE_MODULE_OPTIONS],
|
41
|
+
},
|
42
|
+
];
|
@@ -0,0 +1 @@
|
|
1
|
+
export * from './pagination.response';
|
@@ -0,0 +1,37 @@
|
|
1
|
+
export class PaginationResponse<T> {
|
2
|
+
data: T[];
|
3
|
+
total?: number;
|
4
|
+
offset?: number;
|
5
|
+
limit?: number;
|
6
|
+
hasNext?: boolean;
|
7
|
+
|
8
|
+
constructor(
|
9
|
+
data: T[],
|
10
|
+
total: number = data?.length || 0,
|
11
|
+
limit?: number,
|
12
|
+
offset?: number,
|
13
|
+
) {
|
14
|
+
this.data = data;
|
15
|
+
this.total = total;
|
16
|
+
this.offset = offset;
|
17
|
+
this.limit = limit;
|
18
|
+
this.hasNext = this.offset + this.limit < this.total;
|
19
|
+
}
|
20
|
+
|
21
|
+
toJSON() {
|
22
|
+
return {
|
23
|
+
data: this.data,
|
24
|
+
total: this.total,
|
25
|
+
count: this.data.length,
|
26
|
+
offset: this.offset,
|
27
|
+
limit: this.limit,
|
28
|
+
hasNext: this.hasNext,
|
29
|
+
};
|
30
|
+
}
|
31
|
+
|
32
|
+
promise(): Promise<PaginationResponse<T>> {
|
33
|
+
return new Promise((resolve, reject) => {
|
34
|
+
resolve(this);
|
35
|
+
});
|
36
|
+
}
|
37
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export class Test {}
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import { generateKey, generateDigitCode } from './generator.util';
|
2
|
+
|
3
|
+
describe('Utils', () => {
|
4
|
+
describe('generateKey', () => {
|
5
|
+
it('should generate a key of the specified length', () => {
|
6
|
+
const keyLength = 10;
|
7
|
+
const key = generateKey(keyLength);
|
8
|
+
expect(key).toHaveLength(keyLength);
|
9
|
+
});
|
10
|
+
|
11
|
+
it('should generate a key with default length if not specified', () => {
|
12
|
+
const defaultKeyLength = 36;
|
13
|
+
const key = generateKey();
|
14
|
+
expect(key).toHaveLength(defaultKeyLength);
|
15
|
+
});
|
16
|
+
});
|
17
|
+
|
18
|
+
describe('generateDigitCode', () => {
|
19
|
+
it('should generate a digit code of the specified length', () => {
|
20
|
+
const codeLength = 6;
|
21
|
+
const code = generateDigitCode(codeLength);
|
22
|
+
expect(code).toHaveLength(codeLength);
|
23
|
+
expect(Number(code)).toBeGreaterThanOrEqual(100000);
|
24
|
+
expect(Number(code)).toBeLessThanOrEqual(999999);
|
25
|
+
});
|
26
|
+
|
27
|
+
it('should generate a digit code with default length if not specified', () => {
|
28
|
+
const defaultCodeLength = 4;
|
29
|
+
const code = generateDigitCode();
|
30
|
+
expect(code).toHaveLength(defaultCodeLength);
|
31
|
+
expect(Number(code)).toBeGreaterThanOrEqual(1000);
|
32
|
+
expect(Number(code)).toBeLessThanOrEqual(9999);
|
33
|
+
});
|
34
|
+
});
|
35
|
+
});
|
@@ -0,0 +1,18 @@
|
|
1
|
+
export const generateKey = (length = 36) => {
|
2
|
+
const chars =
|
3
|
+
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
4
|
+
let result = '';
|
5
|
+
for (let i = length; i > 0; i--) {
|
6
|
+
result += chars[Math.floor(Math.random() * chars.length)];
|
7
|
+
}
|
8
|
+
return result;
|
9
|
+
};
|
10
|
+
|
11
|
+
export const generateDigitCode = (length = 4): string => {
|
12
|
+
const startAt = 1 + '0'.repeat(length - 1);
|
13
|
+
const endAt = 9 + '0'.repeat(length - 1);
|
14
|
+
const generateCode = `${Math.floor(
|
15
|
+
Number(startAt) + Math.random() * Number(endAt),
|
16
|
+
)}`;
|
17
|
+
return generateCode;
|
18
|
+
};
|
@@ -0,0 +1,24 @@
|
|
1
|
+
import { Test, TestingModule } from '@nestjs/testing';
|
2
|
+
import { INestApplication } from '@nestjs/common';
|
3
|
+
import * as request from 'supertest';
|
4
|
+
import { AppModule } from './../src/app.module';
|
5
|
+
|
6
|
+
describe('AppController (e2e)', () => {
|
7
|
+
let app: INestApplication;
|
8
|
+
|
9
|
+
beforeEach(async () => {
|
10
|
+
const moduleFixture: TestingModule = await Test.createTestingModule({
|
11
|
+
imports: [AppModule],
|
12
|
+
}).compile();
|
13
|
+
|
14
|
+
app = moduleFixture.createNestApplication();
|
15
|
+
await app.init();
|
16
|
+
});
|
17
|
+
|
18
|
+
it('/ (GET)', () => {
|
19
|
+
return request(app.getHttpServer())
|
20
|
+
.get('/')
|
21
|
+
.expect(200)
|
22
|
+
.expect('Hello World!');
|
23
|
+
});
|
24
|
+
});
|
package/tsconfig.json
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
{
|
2
|
+
"compilerOptions": {
|
3
|
+
"module": "commonjs",
|
4
|
+
"declaration": true,
|
5
|
+
"removeComments": true,
|
6
|
+
"emitDecoratorMetadata": true,
|
7
|
+
"experimentalDecorators": true,
|
8
|
+
"allowSyntheticDefaultImports": true,
|
9
|
+
"target": "es2017",
|
10
|
+
"sourceMap": true,
|
11
|
+
"outDir": "./dist",
|
12
|
+
"baseUrl": "./",
|
13
|
+
"incremental": true,
|
14
|
+
"skipLibCheck": true,
|
15
|
+
"strictNullChecks": false,
|
16
|
+
"noImplicitAny": false,
|
17
|
+
"strictBindCallApply": false,
|
18
|
+
"forceConsistentCasingInFileNames": false,
|
19
|
+
"noFallthroughCasesInSwitch": false
|
20
|
+
}
|
21
|
+
}
|