Commit b306aad6 authored by nanahira's avatar nanahira

first

parent 38f5ff8f
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
/data
/output
/config.yaml
.git*
Dockerfile
.dockerignore
/tests
webpack.config.js
dist/*
build/*
*.js
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir : __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
/data
/output
/config.yaml
stages:
- build
- deploy
variables:
GIT_DEPTH: "1"
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
.build-image:
stage: build
script:
- docker build --pull -t $TARGET_IMAGE .
- docker push $TARGET_IMAGE
build-x86:
extends: .build-image
tags:
- docker
variables:
TARGET_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-x86
build-arm:
extends: .build-image
tags:
- docker-arm
variables:
TARGET_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-arm
.deploy:
stage: deploy
tags:
- docker
script:
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-x86
- docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-arm
- docker manifest create $TARGET_IMAGE --amend $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-x86 --amend
$CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG-arm
- docker manifest push $TARGET_IMAGE
deploy_latest:
extends: .deploy
variables:
TARGET_IMAGE: $CI_REGISTRY_IMAGE:latest
only:
- master
deploy_branch:
extends: .deploy
variables:
TARGET_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
stages:
- install
- build
- deploy
variables:
GIT_DEPTH: "1"
npm_ci:
stage: install
tags:
- linux
script:
- npm ci
artifacts:
paths:
- node_modules
.build_base:
stage: build
tags:
- linux
dependencies:
- npm_ci
build:
extends: .build_base
script: npm run build
artifacts:
paths:
- dist/
upload_to_minio:
stage: deploy
dependencies:
- build
tags:
- linux
script:
- aws s3 --endpoint=https://minio.momobako.com:9000 sync --delete dist/ s3://nanahira/path
only:
- master
stages:
- install
- build
- deploy
variables:
GIT_DEPTH: "1"
npm_ci:
stage: install
tags:
- linux
script:
- npm ci
artifacts:
paths:
- node_modules
.build_base:
stage: build
tags:
- linux
dependencies:
- npm_ci
build:
extends:
- .build_base
script:
- npm run build
artifacts:
paths:
- dist/
unit-test:
extends:
- .build_base
script:
- npm run test
deploy_npm:
stage: deploy
dependencies:
- build
tags:
- linux
script:
- apt update;apt -y install coreutils
- echo $NPMRC | base64 --decode > ~/.npmrc
- npm publish . --access public && curl -X PUT "https://registry-direct.npmmirror.com/$(cat package.json | jq '.name' | sed 's/\"//g')/sync?sync_upstream=true" || true
only:
- master
/install-npm.sh
.git*
/data
/output
/config.yaml
.idea
.dockerignore
Dockerfile
/src
/coverage
/tests
/dist/tests
{
"singleQuote": true,
"trailingComma": "all"
}
\ No newline at end of file
FROM node:lts-bookworm-slim as base
LABEL Author="Nanahira <nanahira@momobako.com>"
RUN apt update && apt -y install python3 build-essential && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /var/log/*
WORKDIR /usr/src/app
COPY ./package*.json ./
FROM base as builder
RUN npm ci && npm cache clean --force
COPY . ./
RUN npm run build
FROM base
ENV NODE_ENV production
RUN npm ci && npm cache clean --force
COPY --from=builder /usr/src/app/dist ./dist
CMD [ "npm", "start" ]
The MIT License (MIT)
Copyright (c) 2024 Nanahira
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# Nesties
**Nest.js utilities**
Nesties is a utility library for Nest.js applications, designed to simplify and enhance common patterns such as decorators, response structures, and request validation. This library provides a set of utilities to streamline your development workflow and improve code reuse and clarity when working with Nest.js.
## Features
- **Decorator Merging**: Merge multiple property, method, class, and parameter decorators.
- **Predefined API Responses**: Simplified and consistent response structures for APIs.
- **Data Validation Pipes**: Validation pipe utilities to handle query and body validation effortlessly.
- **Custom Guards**: Easily implement token-based guards and API header validation.
- **Pagination and Return DTOs**: DTOs for standard and paginated API responses.
## Installation
To install Nesties, use npm or yarn:
```bash
npm install nesties
```
or
```bash
yarn add nesties
```
## Usage
### 1. Merging Decorators
Nesties allows you to merge multiple decorators of the same type (property, method, class, or parameter). This is useful when you want to combine the functionality of several decorators into one.
- **Property Decorator**
```typescript
import { MergePropertyDecorators } from 'nesties';
const CombinedPropertyDecorator = MergePropertyDecorators([Decorator1, Decorator2]);
```
- **Method Decorator**
```typescript
import { MergeMethodDecorators } from 'nesties';
const CombinedMethodDecorator = MergeMethodDecorators([Decorator1, Decorator2]);
```
- **Class Decorator**
```typescript
import { MergeClassDecorators } from 'nesties';
const CombinedClassDecorator = MergeClassDecorators([Decorator1, Decorator2]);
```
- **Parameter Decorator**
```typescript
import { MergeParameterDecorators } from 'nesties';
const CombinedParameterDecorator = MergeParameterDecorators([Decorator1, Decorator2]);
```
### 2. API Response Decorators
Nesties includes a utility for defining API error responses conveniently.
```typescript
import { ApiError } from 'nesties';
@ApiError(401, 'Unauthorized access')
```
### 3. Validation Pipes
Nesties provides utilities for creating validation pipes with automatic data transformation and validation.
- **Data Pipe**
```typescript
import { DataPipe } from 'nesties';
const validationPipe = DataPipe();
```
- **Decorators for Request Validation**
```typescript
import { DataQuery, DataBody } from 'nesties';
class ExampleController {
myMethod(@DataQuery() query: MyQueryDto, @DataBody() body: MyBodyDto) {
// ...
}
}
```
## Usage
### 4. Return Message DTOs
Nesties provides a set of DTOs for consistent API response structures, and it also includes a utility function `ReturnMessageDto` to generate DTOs dynamically based on the provided class type.
- **BlankReturnMessageDto**: A basic structure for returning status and message information.
- **GenericReturnMessageDto**: A generic version for responses that include data.
- **PaginatedReturnMessageDto**: For paginated responses, including metadata about pagination.
- **ReturnMessageDto**: A utility function for generating DTOs based on a class type.
```typescript
import { BlankReturnMessageDto, GenericReturnMessageDto, PaginatedReturnMessageDto, ReturnMessageDto } from 'nesties';
const response = new GenericReturnMessageDto(200, 'Operation successful', myData);
```
#### Example Usage of `ReturnMessageDto`
`ReturnMessageDto` allows you to generate a DTO dynamically based on the structure of a provided class. This is useful when you want to create a standardized response that includes custom data types.
Suppose we have a `User` class:
```typescript
import { ApiProperty } from '@nestjs/swagger';
class User {
@ApiProperty({ description: 'The unique ID of the user', type: Number })
id: number;
@ApiProperty({ description: 'The name of the user', type: String })
name: string;
@ApiProperty({ description: 'The email address of the user', type: String })
email: string;
}
```
You can create a return message DTO for this class:
```typescript
import { ReturnMessageDto } from 'nesties';
class UserReturnMessageDto extends ReturnMessageDto(User) {}
const response = new UserReturnMessageDto(200, 'Success', { id: 1, name: 'John Doe', email: 'john.doe@example.com' });
```
This approach automatically creates a DTO structure with the properties of `User` integrated as the data field, ensuring consistency and reusability in your API responses.
```
### 5. Token Guard
Nesties includes a `TokenGuard` class that validates server tokens from the request headers. This can be used with the `RequireToken` decorator for routes requiring token validation.
```typescript
import { RequireToken } from 'nesties';
@Controller('secure')
export class SecureController {
@Get()
@RequireToken()
secureEndpoint() {
// This endpoint requires a token
}
}
```
#### How to Use `TokenGuard`
1. **Set the `SERVER_TOKEN` in the Configuration**
In your Nest.js configuration, make sure to set up the `SERVER_TOKEN` using the `@nestjs/config` package.
```typescript
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule.forRoot()],
})
export class AppModule {}
```
In your environment file (`.env`), define your token:
```
SERVER_TOKEN=your-secure-token
```
2. **Token Validation with `TokenGuard`**
`TokenGuard` checks the request headers for a token called `x-server-token`. If this token matches the one defined in your configuration, the request is allowed to proceed. If the token is missing or incorrect, a `401 Unauthorized` error is thrown.
This approach is ideal for simple token-based authentication for APIs. It provides a lightweight method to protect routes without implementing a full OAuth or JWT-based system.
3. **Use `RequireToken` Decorator**
Apply the `RequireToken` decorator to your controller methods to enforce token validation:
```typescript
import { Controller, Get } from '@nestjs/common';
import { RequireToken } from 'nesties';
@Controller('api')
export class ApiController {
@Get('protected')
@RequireToken()
protectedRoute() {
return { message: 'This is a protected route' };
}
}
```
In this example, the `protectedRoute` method will only be accessible if the request includes the correct `x-server-token` header.
## DTO Classes
- **BlankReturnMessageDto**: A basic DTO for standardized API responses.
- **BlankPaginatedReturnMessageDto**: A DTO for paginated API responses.
- **GenericReturnMessageDto**: A generic DTO for returning data of any type.
- **StringReturnMessageDto**: A simple DTO for string responses.
```typescript
import { StringReturnMessageDto } from 'nesties';
const response = new StringReturnMessageDto(200, 'Success', 'This is a string response');
```
## Configuration
The `TokenGuard` class uses the `ConfigService` from `@nestjs/config` to access configuration values, such as the `SERVER_TOKEN`. Make sure you have `@nestjs/config` installed and configured in your Nest.js project.
```typescript
import { ConfigModule } from '@nestjs/config';
@Module({
imports: [ConfigModule.forRoot()],
})
export class AppModule {}
```
## Contributing
Contributions are welcome! Please feel free to submit a pull request or report issues.
## License
Nesties is MIT licensed.
export * from './src/insert-field';
export * from './src/merge';
export * from './src/return-message';
export * from './src/openapi';
export * from './src/pipe';
export * from './src/token.guard';
#!/bin/bash
npm i --save-exact --save-dev eslint@8.22.0
npm install --save-dev \
@types/node \
typescript \
'@typescript-eslint/eslint-plugin@^6.0.0' \
'@typescript-eslint/parser@^6.0.0 '\
'eslint-config-prettier@^9.0.0' \
'eslint-plugin-prettier@^5.0.0' \
prettier \
jest \
@types/jest \
ts-jest \
rimraf
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "nesties",
"description": "Nest.js utilities",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"lint": "eslint --fix .",
"build": "rimraf dist && tsc",
"test": "jest --passWithNoTests",
"start": "node dist/index.js"
},
"repository": {
"type": "git",
"url": "https://code.mycard.moe/3rdeye/nesties.git"
},
"author": "Nanahira <nanahira@momobako.com>",
"license": "MIT",
"keywords": [],
"bugs": {
"url": "https://code.mycard.moe/3rdeye/nesties/issues"
},
"homepage": "https://code.mycard.moe/3rdeye/nesties",
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "tests",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
},
"devDependencies": {
"@nestjs/common": "^10.4.6",
"@nestjs/config": "^3.3.0",
"@nestjs/swagger": "^7.4.2",
"@types/jest": "^29.5.14",
"@types/node": "^22.8.1",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"eslint": "8.22.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"jest": "^29.7.0",
"prettier": "^3.3.3",
"rimraf": "^6.0.1",
"ts-jest": "^29.2.5",
"typescript": "^5.6.3"
},
"peerDependencies": {
"@nestjs/common": "^9.4.0 || ^10.0.0",
"@nestjs/swagger": "^7.1.8 || ^6.3.0",
"@nestjs/config": "^3.0.0"
}
}
import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger';
export type AnyClass = new (...args: any[]) => any;
export type ClassOrArray = AnyClass | [AnyClass];
export type ClassType<T> = new (...args: any[]) => T;
export type TypeFromClass<T> = T extends new (...args: any[]) => infer U
? U
: never;
export type ParamsFromClass<T> = T extends new (...args: infer U) => any
? U
: never;
export type ParseType<IC extends ClassOrArray> = IC extends [infer U]
? TypeFromClass<U>[]
: TypeFromClass<IC>;
export function getClassFromClassOrArray(o: ClassOrArray) {
return o instanceof Array ? o[0] : o;
}
export interface InsertOptions<C extends ClassOrArray = ClassOrArray> {
type: C;
options?: ApiPropertyOptions;
}
type TypeFromInsertOptions<O extends InsertOptions> =
O extends InsertOptions<infer C>
?
| ParseType<C>
| (O extends { options: { required: true } } ? never : undefined)
: never;
type Merge<T, U> = {
[K in keyof T | keyof U]: K extends keyof T
? T[K]
: K extends keyof U
? U[K]
: never;
};
export function InsertField<
C extends AnyClass,
M extends Record<string, InsertOptions>,
>(
cl: C,
map: M,
newName?: string,
): new (...args: ParamsFromClass<C>) => Merge<
{
[F in keyof M]: TypeFromInsertOptions<M[F]>;
},
TypeFromClass<C>
> {
const extendedCl = class extends cl {};
for (const key in map) {
ApiProperty({
type: map[key].type,
...(map[key].options || {}),
})(extendedCl.prototype, key);
}
Object.defineProperty(extendedCl, 'name', {
value: newName || cl.name,
});
return extendedCl;
}
export function MergePropertyDecorators(
decs: PropertyDecorator[],
): PropertyDecorator {
return (obj, key) => {
for (const dec of decs) {
dec(obj, key);
}
};
}
export function MergeMethodDecorators(
decs: MethodDecorator[],
): MethodDecorator {
return (obj, key, descriptor) => {
for (const dec of decs) {
dec(obj, key, descriptor);
}
};
}
export function MergeClassDecorators(decs: ClassDecorator[]): ClassDecorator {
return (obj) => {
for (const dec of decs) {
dec(obj);
}
};
}
export function MergeParameterDecorators(
decs: ParameterDecorator[],
): ParameterDecorator {
return (obj, key, index) => {
for (const dec of decs) {
dec(obj, key, index);
}
};
}
import { BlankReturnMessageDto } from './return-message';
import { ApiResponse } from '@nestjs/swagger';
export const ApiError = (status: number, description: string) =>
ApiResponse({ status, type: BlankReturnMessageDto, description });
import {
Body,
PipeTransform,
Query,
Type,
ValidationPipe,
} from '@nestjs/common';
export const DataPipe = () =>
new ValidationPipe({
transform: true,
transformOptions: { enableImplicitConversion: true },
});
const createDataPipeDec =
(
nestFieldDec: (
...pipes: (Type<PipeTransform> | PipeTransform)[]
) => ParameterDecorator,
) =>
(...extraPipes: (Type<PipeTransform> | PipeTransform)[]) =>
nestFieldDec(DataPipe(), ...extraPipes);
export const DataQuery = createDataPipeDec(Query);
export const DataBody = createDataPipeDec(Body);
import { ApiProperty } from '@nestjs/swagger';
import { HttpException } from '@nestjs/common';
import {
AnyClass,
ClassOrArray,
getClassFromClassOrArray,
InsertField,
ParseType,
} from './insert-field';
export interface PageSettingsWise {
pageCount: number;
recordsPerPage: number;
}
export interface BlankReturnMessage {
statusCode: number;
message: string;
success: boolean;
}
export interface ReturnMessage<T> extends BlankReturnMessage {
data?: T;
}
export class BlankReturnMessageDto implements BlankReturnMessage {
@ApiProperty({ description: 'Return code', type: Number })
statusCode: number;
@ApiProperty({ description: 'Return message', type: String })
message: string;
@ApiProperty({ description: 'Whether success.', type: Boolean })
success: boolean;
constructor(statusCode: number, message?: string) {
this.statusCode = statusCode;
this.message = message || 'success';
this.success = statusCode < 400;
}
toException() {
return new HttpException(this, this.statusCode);
}
}
export class BlankPaginatedReturnMessageDto
extends BlankReturnMessageDto
implements PageSettingsWise
{
@ApiProperty({ description: 'Total record count.', type: Number })
total: number;
@ApiProperty({ description: 'Total page count.', type: Number })
totalPages: number;
@ApiProperty({ description: 'Current page.', type: Number })
pageCount: number;
@ApiProperty({ description: 'Records per page.', type: Number })
recordsPerPage: number;
constructor(
statusCode: number,
message: string,
total: number,
pageSettings: PageSettingsWise,
) {
super(statusCode, message);
this.total = total;
this.pageCount = pageSettings.pageCount;
this.recordsPerPage = pageSettings.recordsPerPage;
this.totalPages = Math.ceil(total / pageSettings.recordsPerPage);
}
}
export class GenericReturnMessageDto<T>
extends BlankReturnMessageDto
implements ReturnMessage<T>
{
data?: T;
constructor(statusCode: number, message?: string, data?: T) {
super(statusCode, message);
this.data = data;
}
}
export function ReturnMessageDto<T extends ClassOrArray>(
type: T,
): new (
statusCode: number,
message: string,
data: ParseType<T>,
) => GenericReturnMessageDto<ParseType<T>> {
return InsertField(
GenericReturnMessageDto,
{
data: {
type,
options: {
required: false,
description: 'Return data.',
},
},
},
`${getClassFromClassOrArray(type).name}ReturnMessageDto`,
);
}
export class GenericPaginatedReturnMessageDto<T>
extends BlankPaginatedReturnMessageDto
implements PageSettingsWise, ReturnMessage<T[]>
{
data: T[];
constructor(
statusCode: number,
message: string,
data: T[],
total: number,
pageSettings: PageSettingsWise,
) {
super(statusCode, message, total, pageSettings);
this.data = data;
}
}
export function PaginatedReturnMessageDto<T extends AnyClass>(
type: T,
): new (
statusCode: number,
message: string,
data: InstanceType<T>[],
total: number,
pageSettings: PageSettingsWise,
) => GenericPaginatedReturnMessageDto<InstanceType<T>> {
return InsertField(
GenericPaginatedReturnMessageDto,
{
data: {
type: [type],
options: {
required: false,
description: 'Return data.',
},
},
},
`${getClassFromClassOrArray(type).name}PaginatedReturnMessageDto`,
);
}
export class StringReturnMessageDto extends GenericReturnMessageDto<string> {
@ApiProperty({ description: 'Return data.', type: String, required: false })
data: string;
}
import {
CanActivate,
ExecutionContext,
Injectable,
UseGuards,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ApiHeader } from '@nestjs/swagger';
import { BlankReturnMessageDto } from './return-message';
import { MergeMethodDecorators } from './merge';
import { ApiError } from './openapi';
@Injectable()
export class TokenGuard implements CanActivate {
private token = this.config.get<string>('SERVER_TOKEN');
constructor(private config: ConfigService) {}
async canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
const token = request.headers['x-server-token'];
if (this.token && token !== this.token) {
throw new BlankReturnMessageDto(401, 'Unauthorized').toException();
}
return true;
}
}
export const RequireToken = () =>
MergeMethodDecorators([
UseGuards(TokenGuard),
ApiHeader({
name: 'x-server-token',
description: '服务器 token',
required: false,
}),
ApiError(401, '服务器 Token 不正确'),
]);
describe('Sample test.', () => {
it('should pass', () => {
expect(true).toBe(true);
});
});
{
"compilerOptions": {
"outDir": "dist",
"module": "commonjs",
"target": "es2021",
"esModuleInterop": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"declaration": true,
"sourceMap": true
},
"compileOnSave": true,
"allowJs": true,
"include": [
"*.ts",
"src/**/*.ts",
"test/**/*.ts",
"tests/**/*.ts"
]
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment