Compare commits

..

No commits in common. "77e27a4b9349c8b676a540b85ae6525021dd09f5" and "03e70436c6a0e68d71dc52194caae9b329c79c39" have entirely different histories.

53 changed files with 1686 additions and 5615 deletions

2843
bun.lock

File diff suppressed because it is too large Load Diff

View File

@ -3,20 +3,15 @@
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"assets": [
"assets/**/*"
],
"deleteOutDir": true,
"builder": "swc",
"typeCheck": true,
"plugins": [
{
"plugins": [{
"name": "@nestjs/swagger",
"options":{
"introspectComments": true,
"controllerKeyOfComment": "summary"
}
}
]
}]
}
}

View File

@ -6,8 +6,6 @@
"private": true,
"license": "UNLICENSED",
"scripts": {
"cli": "bun --bun src/console/bin.ts",
"dev": "cross-env NODE_ENV=development pnpm cli start -w",
"prebuild": "rimraf dist",
"build": "cross-env NODE_ENV=production nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
@ -30,7 +28,6 @@
"@nestjs/swagger": "^11.2.0",
"@nestjs/typeorm": "^11.0.0",
"chalk": "^5.4.1",
"chokidar": "^4.0.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"deepmerge": "^4.3.1",
@ -40,18 +37,21 @@
"lodash": "^4.17.21",
"meilisearch": "^0.51.0",
"mysql2": "^3.14.1",
"ora": "^8.2.0",
"pm2": "^6.0.8",
"reflect-metadata": "^0.2.2",
"rimraf": "^6.0.1",
"rxjs": "^7.8.2",
"sanitize-html": "^2.17.0",
"typeorm": "^0.3.24",
"validator": "^13.15.15",
"yaml": "^2.8.0",
"yargs": "^18.0.0"
"yaml": "^2.8.0"
},
"devDependencies": {
"@babel/core": "^7.27.4",
"@babel/plugin-proposal-decorators": "^7.27.1",
"@babel/plugin-transform-runtime": "^7.27.4",
"@babel/preset-env": "^7.27.2",
"@babel/preset-typescript": "^7.27.1",
"@babel/runtime": "^7.27.6",
"@eslint/compat": "^1.3.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.29.0",
@ -69,10 +69,9 @@
"@types/sanitize-html": "^2.16.0",
"@types/supertest": "^6.0.3",
"@types/validator": "^13.15.1",
"@types/yargs": "^17.0.33",
"@typescript-eslint/eslint-plugin": "^8.34.0",
"@typescript-eslint/parser": "^8.34.0",
"bun-types": "^1.2.16",
"babel-jest": "^30.0.0",
"cross-env": "^7.0.3",
"eslint": "^9.29.0",
"eslint-config-airbnb-base": "^15.0.0",
@ -87,6 +86,7 @@
"prettier": "^3.5.3",
"source-map-support": "^0.5.21",
"supertest": "^7.1.1",
"ts-babel": "^6.1.7",
"ts-jest": "29.4.0",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
@ -106,7 +106,7 @@
"<rootDir>/test/**/*.test.ts"
],
"transform": {
"^.+\\.(js|jsx)$": "ts-jest",
"^.+\\.(js|jsx)$": "babel-jest",
"^.+\\.(ts|tsx)?$": "ts-jest"
},
"collectCoverageFrom": [

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +0,0 @@
import { createApp } from '@/modules/core/helpers/app';
import { buildCli } from '@/modules/core/helpers/command';
import { createOptions } from '@/options';
console.error('Raw argv:', process.argv);
console.log('This is the very beginning of bin.ts');
buildCli(createApp(createOptions));

View File

@ -1,34 +1,13 @@
export enum PostBodyType {
/**
* HTML格式
*/
HTML = 'html',
/**
* Markdown格式
*/
MD = 'markdown',
}
export enum PostOrder {
/**
*
*/
CREATED = 'createdAt',
/**
*
*/
UPDATED = 'updatedAt',
/**
*
*/
PUBLISHED = 'publishedAt',
/**
*
*/
COMMENTCOUNT = 'commentCount',
/**
*
*/
CUSTOM = 'custom',
}

View File

@ -9,15 +9,12 @@ import { SearchService } from '@/modules/content/services';
import { SanitizeService } from '@/modules/content/services/SanitizeService';
import { PostService } from '@/modules/content/services/post.service';
import { PostSubscriber } from '@/modules/content/subscribers/post.subscriber';
import { DatabaseModule } from '@/modules/database/database.module';
import { addSubscribers } from '@/modules/database/utils';
import { Configure } from '../config/configure';
import { defauleContentConfig } from './config';
import * as subscribers from './subscribers';
import { ContentConfig } from './types';
@Module({})
@ -26,7 +23,7 @@ export class ContentModule {
const config = await configure.get<ContentConfig>('content', defauleContentConfig);
const providers: ModuleMetadata['providers'] = [
...Object.values(services),
...(await addSubscribers(configure, Object.values(subscribers))),
PostSubscriber,
{
provide: PostService,
inject: [

View File

@ -15,10 +15,8 @@ import { ApiTags } from '@nestjs/swagger';
import { Depends } from '@/modules/restful/decorators/depend.decorator';
import { PaginateDto } from '@/modules/restful/dtos/paginate.dto';
import { ContentModule } from '../content.module';
import { CreateCategoryDto, UpdateCategoryDto } from '../dtos/category.dto';
import { CreateCategoryDto, QueryCategoryDto, UpdateCategoryDto } from '../dtos/category.dto';
import { CategoryService } from '../services';
@ApiTags('Category Operate')
@ -44,7 +42,7 @@ export class CategoryController {
@SerializeOptions({ groups: ['category-list'] })
async list(
@Query()
options: PaginateDto,
options: QueryCategoryDto,
) {
return this.service.paginate(options);
}

View File

@ -15,10 +15,8 @@ import { DeleteDto } from '@/modules/content/dtos/delete.dto';
import { Depends } from '@/modules/restful/decorators/depend.decorator';
import { PaginateDto } from '@/modules/restful/dtos/paginate.dto';
import { ContentModule } from '../content.module';
import { CreateTagDto, UpdateTagDto } from '../dtos/tag.dto';
import { CreateTagDto, QueryTagDto, UpdateTagDto } from '../dtos/tag.dto';
import { TagService } from '../services';
@Depends(ContentModule)
@ -30,7 +28,7 @@ export class TagController {
@SerializeOptions({})
async list(
@Query()
options: PaginateDto,
options: QueryTagDto,
) {
return this.service.paginate(options);
}

View File

@ -16,9 +16,28 @@ import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator
import { IsDataExist } from '@/modules/database/constraints/data.exist.constraint';
import { IsTreeUnique } from '@/modules/database/constraints/tree.unique.constraint';
import { IsTreeUniqueExist } from '@/modules/database/constraints/tree.unique.exist.constraint';
import { PaginateOptions } from '@/modules/database/types';
import { CategoryEntity } from '../entities';
@DtoValidation({ type: 'query' })
export class QueryCategoryDto implements PaginateOptions {
@Transform(({ value }) => toNumber(value))
@Min(1, { always: true, message: 'The current page must be greater than 1.' })
@IsInt()
@IsOptional()
page = 1;
@Transform(({ value }) => toNumber(value))
@Min(1, {
always: true,
message: 'The number of data displayed per page must be greater than 1.',
})
@IsInt()
@IsOptional()
limit = 10;
}
@DtoValidation({ groups: ['create'] })
export class CreateCategoryDto {
@IsTreeUnique(CategoryEntity, {

View File

@ -25,32 +25,16 @@ import { PaginateOptions } from '@/modules/database/types';
import { CategoryEntity, PostEntity, TagEntity } from '../entities';
/**
*
*/
@DtoValidation({ type: 'query' })
export class QueryPostDto implements PaginateOptions {
/**
* (全部文章:不填只查询已发布的:true只查询未发布的:false)
*/
@Transform(({ value }) => toBoolean(value))
@IsBoolean()
@IsOptional()
isPublished?: boolean;
/**
*
*/
@MaxLength(100, {
always: true,
message: '搜索字符串长度不得超过$constraint1',
})
@IsOptional()
search?: string;
/**
* ,
*/
@IsEnum(PostOrder, {
message: `The sorting rule must be one of ${Object.values(PostOrder).join(',')}`,
})
@ -76,17 +60,11 @@ export class QueryPostDto implements PaginateOptions {
@IsOptional()
trashed?: SelectTrashMode;
/**
* ID查询此分类及其后代分类下的文章
*/
@IsDataExist(CategoryEntity, { always: true, message: 'The category does not exist' })
@IsUUID(undefined, { message: 'The ID format is incorrect' })
@IsOptional()
category?: string;
/**
* ID查询
*/
@IsUUID(undefined, { message: 'The ID format is incorrect' })
@IsOptional()
tag?: string;

View File

@ -1,13 +1,34 @@
import { PartialType } from '@nestjs/swagger';
import { IsDefined, IsNotEmpty, IsOptional, IsUUID, MaxLength } from 'class-validator';
import { Transform } from 'class-transformer';
import { IsDefined, IsInt, IsNotEmpty, IsOptional, IsUUID, MaxLength, Min } from 'class-validator';
import { toNumber } from 'lodash';
import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator';
import { IsDataExist } from '@/modules/database/constraints';
import { IsUnique } from '@/modules/database/constraints/unique.constraint';
import { IsUniqueExist } from '@/modules/database/constraints/unique.exist.constraint';
import { PaginateOptions } from '@/modules/database/types';
import { TagEntity } from '../entities';
@DtoValidation({ type: 'query' })
export class QueryTagDto implements PaginateOptions {
@Transform(({ value }) => toNumber(value))
@Min(1, { always: true, message: 'The current page must be greater than 1.' })
@IsInt()
@IsOptional()
page = 1;
@Transform(({ value }) => toNumber(value))
@Min(1, {
always: true,
message: 'The number of data displayed per page must be greater than 1.',
})
@IsInt()
@IsOptional()
limit = 10;
}
@DtoValidation({ groups: ['create'] })
export class CreateTagDto {
@IsUnique(TagEntity, { groups: ['create'], message: 'The label names are repeated' })

View File

@ -5,13 +5,12 @@ import {
Entity,
OneToMany,
PrimaryColumn,
Relation,
Tree,
TreeChildren,
TreeParent,
} from 'typeorm';
import type { Relation } from 'typeorm';
import { PostEntity } from '@/modules/content/entities/post.entity';
@Exclude()

View File

@ -6,13 +6,12 @@ import {
Entity,
ManyToOne,
PrimaryColumn,
Relation,
Tree,
TreeChildren,
TreeParent,
} from 'typeorm';
import type { Relation } from 'typeorm';
import { PostEntity } from '@/modules/content/entities/post.entity';
@Exclude()

View File

@ -10,11 +10,10 @@ import {
ManyToOne,
OneToMany,
PrimaryColumn,
Relation,
UpdateDateColumn,
} from 'typeorm';
import type { Relation } from 'typeorm';
import { PostBodyType } from '@/modules/content/constants';
import { CategoryEntity } from '@/modules/content/entities/category.entity';
import { CommentEntity } from '@/modules/content/entities/comment.entity';

View File

@ -1,6 +1,5 @@
import { Exclude, Expose } from 'class-transformer';
import { Column, Entity, ManyToMany, PrimaryColumn } from 'typeorm';
import type { Relation } from 'typeorm';
import { Column, Entity, ManyToMany, PrimaryColumn, Relation } from 'typeorm';
import { PostEntity } from '@/modules/content/entities/post.entity';

View File

@ -10,7 +10,7 @@ import { PostEntity } from '@/modules/content/entities/post.entity';
import { CategoryRepository } from '@/modules/content/repositories';
import { PostRepository } from '@/modules/content/repositories/post.repository';
import { SearchService } from '@/modules/content/services/search.service';
import type { SearchType } from '@/modules/content/types';
import { SearchType } from '@/modules/content/types';
import { BaseService } from '@/modules/database/base/service';
import { SelectTrashMode } from '@/modules/database/constants';
import { QueryHook } from '@/modules/database/types';

View File

@ -1 +0,0 @@
export * from './post.subscriber';

View File

@ -1,33 +1,33 @@
import { Optional } from '@nestjs/common';
import { isNil } from 'lodash';
import { DataSource, EventSubscriber, ObjectType } from 'typeorm';
import { Configure } from '@/modules/config/configure';
import { PostBodyType } from '@/modules/content/constants';
import { PostEntity } from '@/modules/content/entities/post.entity';
import { PostRepository } from '@/modules/content/repositories/post.repository';
import { SanitizeService } from '@/modules/content/services/SanitizeService';
import { BaseSubscriber } from '@/modules/database/base/subscriber';
@EventSubscriber()
export class PostSubscriber extends BaseSubscriber<PostEntity> {
protected entity: ObjectType<PostEntity> = PostEntity;
constructor(
protected dataSource: DataSource,
protected _configure: Configure,
protected postRepository: PostRepository,
protected configure: Configure,
@Optional() protected sanitizeService: SanitizeService,
) {
super(dataSource, _configure);
}
get configure(): Configure {
return this._configure;
super(dataSource);
}
async afterLoad(entity: PostEntity) {
const sanitizeService = (await this.configure.get('content.htmlEnabled'))
? this.container.get(SanitizeService)
: undefined;
if (!isNil(sanitizeService) && entity.type === PostBodyType.HTML) {
entity.body = sanitizeService.sanitize(entity.body);
if (
(await this.configure.get('content.htmlEnabled')) &&
!isNil(this.sanitizeService) &&
entity.type === PostBodyType.HTML
) {
entity.body = this.sanitizeService.sanitize(entity.body);
}
}
}

View File

@ -1,52 +0,0 @@
import { spawn } from 'node:child_process';
import { exit } from 'process';
import { Arguments } from 'yargs';
import { getCLIConfig } from '@/modules/core/commands/helpers/config';
import { BuildCommandArguments } from '@/modules/core/commands/types';
import { CommandItem } from '@/modules/core/types';
export const createBuildCommand: CommandItem<any, BuildCommandArguments> = async (app) => ({
command: ['build', 'b'],
describe: 'Build application by nest cli.',
builder: {
nestConfig: {
type: 'string',
alias: 'n',
describe: 'nest cli config file path.',
default: 'nest-cli.json',
},
tsConfig: {
type: 'string',
alias: 't',
describe: 'typescript config file path.',
default: 'tsconfig.build.json',
},
watch: {
type: 'boolean',
alias: 'w',
describe: ' Run in watch mode (live-reload).',
default: false,
},
preserveWatchOutput: {
type: 'boolean',
alias: 'po',
describe: 'Use "preserveWatchOutput" option when using tsc watch mode',
default: false,
},
},
handler: async (args: Arguments<BuildCommandArguments>) => {
const config = getCLIConfig(args.tsConfig, args.nestConfig);
const params = ['build', '-c', args.nestConfig, '-p', args.tsConfig];
if (args.watch) {
params.push('-w');
}
if (args.preserveWatchOutput) {
params.push('po');
}
const child = spawn(config.paths.nest, params, config.subprocess.node);
child.on('exit', () => exit());
},
});

View File

@ -1,21 +0,0 @@
import { DemoCommandArguments } from '@/modules/core/commands/types';
import { CommandItem } from '@/modules/core/types';
export const DemoCommand: CommandItem<any, DemoCommandArguments> = async (app) => ({
command: ['demo', 'd'],
describe: 'a demo command',
handler: async (args: DemoCommandArguments) => {
const { configure } = app;
const appName = await configure.get<string>('app.name');
const sleep = args.sleep ? ' will to sleep' : '';
console.log(`just a demo command,app ${appName} ${sleep}`);
},
builder: {
sleep: {
type: 'boolean',
alias: 's',
describe: 'App will sleep ?',
default: false,
},
},
});

View File

@ -1,97 +0,0 @@
import { join } from 'path';
// eslint-disable-next-line import/no-extraneous-dependencies
import { ActionOnFile, AssetEntry } from '@nestjs/cli/lib/configuration';
import chokidar, { FSWatcher } from 'chokidar';
import { get } from 'lodash';
import { CLIConfig } from '@/modules/core/commands/types';
import { toBoolean } from '@/modules/core/helpers';
export class Asset {
private watchAssetsKeyValue: { [key: string]: boolean } = {};
private watchers: FSWatcher[] = [];
private actionInProgress = false;
closeWatchers() {
const timeout = 500;
const closeFn = () => {
if (this.actionInProgress) {
this.actionInProgress = false;
setTimeout(closeFn, timeout);
} else {
this.watchers.forEach((watch) => watch.close());
}
};
setTimeout(closeFn, timeout);
}
watchAssets(config: CLIConfig, codePath: string, changer: () => void) {
const assets = get(config.options.nest, 'compilerOptions.assets', []) as AssetEntry[];
if (assets.length <= 0) {
return;
}
try {
const isWatchEnabled = toBoolean(get(config, 'watchAssets', 'src'));
const filesToWatch = assets.map<AssetEntry>((item) => {
if (typeof item === 'string') {
return {
glob: join(codePath, item),
};
}
return {
glob: join(codePath, item.include!),
exclude: item.exclude ? join(codePath, item.exclude) : undefined,
flat: item.flat,
watchAssets: item.watchAssets,
};
});
for (const file of filesToWatch) {
const option: ActionOnFile = {
action: 'change',
item: file,
path: '',
sourceRoot: codePath,
watchAssetsMode: isWatchEnabled,
};
const watcher = chokidar
.watch(file.glob, { ignored: file.exclude })
.on('add', (path) =>
this.actionOnFIle({ ...option, path, action: 'change' }, changer),
)
.on('change', (path) =>
this.actionOnFIle({ ...option, path, action: 'change' }, changer),
)
.on('unlink', (path) =>
this.actionOnFIle({ ...option, path, action: 'unlink' }, changer),
);
this.watchers.push(watcher);
}
} catch (e) {
throw new Error(
`An error occurred during the assets copying process. ${(e as any).message}`,
);
}
}
protected actionOnFIle(option: ActionOnFile, changer: () => void) {
const { action, item, path, watchAssetsMode } = option;
const isWatchEnabled = watchAssetsMode || item.watchAssets;
if (!isWatchEnabled && this.watchAssetsKeyValue[path]) {
return;
}
this.watchAssetsKeyValue[path] = true;
this.actionInProgress = true;
if (action === 'change') {
changer();
}
}
}

View File

@ -1,113 +0,0 @@
/* eslint-disable import/no-extraneous-dependencies */
import { join, resolve } from 'path';
import { exit } from 'process';
import { Configuration as NestCLIConfig } from '@nestjs/cli/lib/configuration';
import { isNil } from '@nestjs/common/utils/shared.utils';
import { existsSync, readFileSync } from 'fs-extra';
import { get, omit } from 'lodash';
import { StartOptions } from 'pm2';
import ts from 'typescript';
import { Configure } from '@/modules/config/configure';
import { CLIConfig, Pm2Option } from '@/modules/core/commands/types';
import { deepMerge, panic } from '@/modules/core/helpers';
import { AppConfig } from '@/modules/core/types';
const cwdPath = resolve(__dirname, '../../../../..');
export function getCLIConfig(
tsConfigFile: string,
nestConfigFile: string,
tsEntryFile?: string,
): CLIConfig {
let tsConfig: ts.CompilerOptions = {};
const tsConfigPath = join(cwdPath, tsConfigFile);
if (!existsSync(tsConfigPath)) {
panic(`ts config file ${tsConfigPath} not exists!`);
}
try {
const allTsConfig = JSON.parse(readFileSync(tsConfigPath, 'utf8'));
tsConfig = get(allTsConfig, 'compilerOptions', {});
} catch (error) {
panic({ error, message: 'get ts config file failed.' });
}
let nestConfig: NestCLIConfig = {};
const nestConfigPath = join(cwdPath, nestConfigFile);
if (!existsSync(nestConfigPath)) {
panic(`ts config file ${nestConfigPath} not exists!`);
}
try {
nestConfig = JSON.parse(readFileSync(nestConfigPath, 'utf8'));
} catch (error) {
panic({ error, message: 'get nest config file failed.' });
}
const dist = get(tsConfig, 'outDir', 'dist');
const src = get(nestConfig, 'sourceRoot', 'src');
const homeDir = process.env.HOME;
const paths = {
cwd: cwdPath,
dist,
src,
js: join(dist, nestConfig.entryFile ?? 'main.js'),
ts: join(src, tsEntryFile ?? 'main.ts'),
bun: `${homeDir}/.bun/bin/bun`,
nest: './node_modules/@nestjs/cli/bin/nest.js',
};
return {
options: { ts: tsConfig, nest: nestConfig },
paths,
subprocess: {
bun: {
cwd: cwdPath,
stdout: 'inherit',
env: process.env,
onExit: (proc) => {
proc.kill();
if (!isNil(proc.exitCode)) {
exit(0);
}
},
},
node: {
cwd: cwdPath,
env: process.env,
stdio: 'inherit',
},
},
};
}
export async function getPm2Config(
configure: Configure,
option: Pm2Option,
config: CLIConfig,
script: string,
): Promise<StartOptions> {
const { name, pm2: customConfig = {} } = await configure.get<AppConfig>('app');
const defaultConfig: StartOptions = {
name,
cwd: cwdPath,
script,
args: option.command,
autorestart: true,
watch: option.watch,
ignore_watch: ['node_modules'],
env: process.env,
exec_mode: 'fork',
interpreter: config.paths.bun,
};
return deepMerge(
defaultConfig,
omit(customConfig, ['name', 'cwd', 'script', 'args', 'watch', 'interpreter']),
'replace',
);
}

View File

@ -1,125 +0,0 @@
import { isNil } from '@nestjs/common/utils/shared.utils';
import { Subprocess } from 'bun';
import chalk from 'chalk';
import { pick } from 'lodash';
import pm2 from 'pm2';
import { Arguments } from 'yargs';
import { Configure } from '@/modules/config/configure';
import { Asset } from '@/modules/core/commands/helpers/asset';
import { getPm2Config } from '@/modules/core/commands/helpers/config';
import { generateSwaggerMetadata } from '@/modules/core/commands/helpers/swagger';
import { CLIConfig, StartCommandArguments } from '@/modules/core/commands/types';
import { AppConfig } from '@/modules/core/types';
export async function start(
args: Arguments<StartCommandArguments>,
config: CLIConfig,
): Promise<void> {
console.log('command start...');
const script = args.typescript ? config.paths.ts : config.paths.js;
const params = [config.paths.bun, 'run'];
if (args.watch) {
params.push('--watch');
}
if (args.debug) {
const inspectFlag =
typeof args.debug === 'string' ? `--inspect=${args.debug}` : '--inspect';
params.push(inspectFlag);
}
if (args.typescript) {
generateSwaggerMetadata(args, config, false);
}
params.push(script);
let child: Subprocess;
if (args.watch) {
const asset = new Asset();
const restart = () => {
if (!isNil(child)) {
child.kill();
}
child = Bun.spawn(params, config.subprocess.bun);
};
restart();
asset.watchAssets(config, config.paths.cwd, restart);
process.on('exit', () => {
child.kill();
asset.closeWatchers();
process.exit(0);
});
} else {
Bun.spawn(params, {
...config.subprocess.bun,
onExit(proc) {
proc.kill();
process.exit(0);
},
});
}
}
export async function startPM2(
configure: Configure,
args: Arguments<StartCommandArguments>,
config: CLIConfig,
): Promise<void> {
console.log('command startPM2...');
const { name } = await configure.get<AppConfig>('app');
const script = args.typescript ? config.paths.ts : config.paths.js;
const pm2config = await getPm2Config(
configure,
{ command: 'start', ...pick(args, ['watch', 'typescript']) },
config,
script,
);
if (pm2config.exec_mode === 'cluster' && args.typescript) {
console.log(
chalk.yellowBright(
'Cannot directly use bun to run ts code in cluster mode, so it will automatically change to fork mode.',
),
);
console.log();
console.log(
chalk.bgCyanBright(
chalk.blackBright(
'If you really need the app to be started in cluster mode, be sure to compile it into js first, and then add the --no-ts arg when running',
),
),
);
console.log();
pm2config.exec_mode = 'fork';
}
const connectCallback = (error?: any) => {
if (!isNil(error)) {
console.error(error);
process.exit(2);
}
};
const startCallback = (error?: any) => {
if (!isNil(error)) {
console.error(error);
process.exit(1);
}
pm2.disconnect();
};
const restartCallback = (error?: any) => {
if (isNil(error)) {
pm2.disconnect();
} else {
pm2.start(pm2config, (err) => startCallback(err));
}
};
pm2.connect((err: any) => {
connectCallback(err);
generateSwaggerMetadata(args, config, false);
args.restart
? pm2.restart(name, restartCallback)
: pm2.start(pm2config, (e) => startCallback(e));
});
}

View File

@ -1,39 +0,0 @@
/* eslint-disable import/no-extraneous-dependencies */
import { join } from 'path';
import { PluginMetadataGenerator } from '@nestjs/cli/lib/compiler/plugins/plugin-metadata-generator';
import { PluginOptions } from '@nestjs/cli/lib/configuration';
import { ReadonlyVisitor } from '@nestjs/swagger/dist/plugin';
import { get, isNil } from 'lodash';
import { Arguments } from 'yargs';
import { CLIConfig, StartCommandArguments } from '@/modules/core/commands/types';
export function generateSwaggerMetadata(
args: Arguments<StartCommandArguments>,
config: CLIConfig,
watch: boolean,
) {
const cliPlugins = get(config.options.nest, 'compilerOptions.plugins', []) as (
| string
| RecordAny
)[];
const swaggerPlugin = cliPlugins.find(
(item) => item === '@nest/swagger' || (item as any).name === '@nest/swagger',
);
if (!isNil(swaggerPlugin) && args.typescript) {
const srcPath = join(config.paths.cwd, config.paths.src);
const generator = new PluginMetadataGenerator();
let swaggerPluginOption: PluginOptions;
if (typeof swaggerPlugin !== 'string' && 'options' in swaggerPlugin) {
swaggerPluginOption = swaggerPlugin.options;
}
generator.generate({
visitors: [new ReadonlyVisitor({ ...swaggerPluginOption, pathToSource: srcPath })],
outputDir: srcPath,
watch,
tsconfigPath: args.tsConfig,
printDiagnostics: false,
});
}
}

View File

@ -1,3 +0,0 @@
export * from './demo.command';
export * from './build.command';
export * from './start.command';

View File

@ -1,72 +0,0 @@
import { Arguments } from 'yargs';
import { getCLIConfig } from '@/modules/core/commands/helpers/config';
import { start, startPM2 } from '@/modules/core/commands/helpers/start';
import { StartCommandArguments } from '@/modules/core/commands/types';
import { CommandItem } from '@/modules/core/types';
export const createStartCommand: CommandItem<any, StartCommandArguments> = async (app) => ({
command: ['start', 's'],
describe: 'Start app',
builder: {
nestConfig: {
type: 'string',
alias: 'n',
describe: 'nest cli config file path.',
default: 'nest-cli.json',
},
tsConfig: {
type: 'string',
alias: 't',
describe: 'typescript config file path.',
default: 'tsconfig.build.json',
},
entry: {
type: 'string',
alias: 'e',
describe:
'Specify entry file for ts runner, you can specify js entry file in nest-cli.json by entryFile.',
default: 'main.ts',
},
prod: {
type: 'boolean',
alias: 'p',
describe: 'Start app in production by pm2.',
default: false,
},
restart: {
type: 'boolean',
alias: 'r',
describe: 'Restart app(only pm2),pm2 will auto run start if process not exists.',
default: false,
},
typescript: {
type: 'boolean',
alias: 'ts',
describe: 'Run the .ts file directly.',
default: true,
},
watch: {
type: 'boolean',
alias: 'w',
describe: ' Run in watch mode (live-reload).',
default: false,
},
debug: {
type: 'boolean',
alias: 'd',
describe: 'Whether to enable debug mode, only valid for non-production environments',
default: false,
},
},
handler: async (args: Arguments<StartCommandArguments>) => {
console.log('createStartCommand handler start');
const { configure } = app;
const config = getCLIConfig(args.tsConfig, args.nestConfig, args.entry);
if (args.prod || args.restart) {
await startPM2(configure, args, config);
} else {
await start(args, config);
}
},
});

View File

@ -1,64 +0,0 @@
/* eslint-disable import/no-extraneous-dependencies */
import { SpawnOptions as NodeSpawnOptions } from 'child_process';
import { Configuration as NestCLIConfig } from '@nestjs/cli/lib/configuration';
import type { SpawnOptions as BunSpawnOptions } from 'bun';
import ts from 'typescript';
export type DemoCommandArguments = {
sleep?: boolean;
};
export interface CLIConfig {
options: {
ts: ts.CompilerOptions;
nest: NestCLIConfig;
};
paths: Record<'cwd' | 'dist' | 'src' | 'js' | 'ts' | 'bun' | 'nest', string>;
subprocess: {
bun: BunSpawnOptions.OptionsObject<any, any, any>;
node: NodeSpawnOptions;
};
}
export type StartCommandArguments = {
/**
* nest-cli.json的文件路径()
*/
nestConfig?: string;
/**
* tsconfig.build.json的文件路径()
*/
tsConfig?: string;
/**
* 使TS文件的入口文件,main.ts. js文件,nest-cli.json的entryFile指定
*/
entry?: string;
/**
* 使PM2后台静默启动生产环境
*/
prod?: boolean;
/**
* 使TS文件,
*/
typescript?: boolean;
/**
* ,使(PM2启动的生产环境下此选项无效)
*/
watch?: boolean;
/**
* debug模式,
*/
debug?: boolean | string;
/**
* (PM2进程)
*/
restart?: boolean;
};
export type Pm2Option = Pick<StartCommandArguments, 'typescript' | 'watch'> & { command: string };
export type BuildCommandArguments = Pick<StartCommandArguments, 'tsConfig' | 'nestConfig'> & {
watch?: string;
preserveWatchOutput?: boolean;
};

View File

@ -10,8 +10,6 @@ import { Configure } from '@/modules/config/configure';
import { DEFAULT_VALIDATION_CONFIG } from '@/modules/content/constants';
import { createCommands } from '@/modules/core/helpers/command';
import { CoreModule } from '../core.module';
import { AppFilter } from '../providers/app.filter';
import { AppInterceptor } from '../providers/app.interceptor';
@ -20,7 +18,7 @@ import { App, AppConfig, CreateOptions } from '../types';
import { CreateModule } from './utils';
export const app: App = { configure: new Configure(), commands: [] };
export const app: App = { configure: new Configure() };
export const createApp = (options: CreateOptions) => async (): Promise<App> => {
const { config, builder } = options;
@ -36,7 +34,6 @@ export const createApp = (options: CreateOptions) => async (): Promise<App> => {
app.container = await builder({ configure: app.configure, BootModule });
useContainer(app.container.select(BootModule), { fallbackOnErrors: true });
app.commands = await createCommands(options.commands, app as Required<App>);
return app;
};
@ -92,11 +89,11 @@ export async function createBootModule(
}
export async function startApp(
creator: () => Promise<App>,
creater: () => Promise<App>,
listened: (app: App, startTime: Date) => () => Promise<void>,
) {
const startTime = new Date();
const { container, configure } = await creator();
const { container, configure } = await creater();
app.container = container;
app.configure = configure;
const { port, host } = await configure.get<AppConfig>('app');

View File

@ -1,55 +0,0 @@
import chalk from 'chalk';
import yargs, { Arguments, CommandModule } from 'yargs';
import { hideBin } from 'yargs/helpers';
import * as coreCommands from '../commands';
import { App, CommandCollection } from '../types';
export async function buildCli(creator: () => Promise<App>) {
console.log('buildCli start');
const app = await creator();
const bin = yargs(hideBin(process.argv));
app.commands.forEach((cmd) => {
bin.command(cmd);
});
bin.usage('Usage: $0 <command> [args]')
.scriptName('cli')
.demandCommand(1, '')
.fail((msg, err, y) => {
if (!msg && !err) {
bin.showHelp();
process.exit();
}
if (msg) {
console.error(chalk.red(msg));
}
if (err) {
console.error(chalk.red(err.message));
}
process.exit();
})
.strict()
.alias('v', 'version')
.help('h')
.alias('h', 'help')
.parse();
}
export async function createCommands(
factory: () => CommandCollection,
app: Required<App>,
): Promise<CommandModule<any, any>[]> {
const collection: CommandCollection = [...factory(), ...Object.values(coreCommands)];
const commands = await Promise.all(collection.map(async (command) => command(app)));
return commands.map((command) => ({
...command,
handler: async (args: Arguments<RecordAny>) => {
await app.container.close();
await command.handler(args);
if (command.instant) {
process.exit();
}
},
}));
}

View File

@ -14,7 +14,7 @@ export function toBoolean(value?: string | boolean): boolean {
}
try {
return JSON.parse(value.toLowerCase());
} catch {
} catch (error) {
return value as unknown as boolean;
}
}
@ -41,7 +41,7 @@ export function isAsyncFunction<T, P extends Array<any>>(
callback: (...args: P) => T | Promise<T>,
): callback is (...args: P) => Promise<T> {
const AsyncFunction = (async () => {}).constructor;
return callback instanceof AsyncFunction;
return callback instanceof AsyncFunction === true;
}
export function CreateModule(
@ -76,14 +76,8 @@ export async function panic(option: PanicOption | string) {
console.log(chalk.red(`\n❌ ${option}`));
process.exit(1);
}
const { error, message, spinner, exit = true } = option;
if (isNil(error)) {
isNil(spinner)
? console.log(chalk.red(`\n❌ ${message}`))
: spinner.succeed(chalk.red(`\n❌ ${message}`));
} else {
isNil(spinner) ? console.log(chalk.red(error)) : spinner.fail(chalk.red(error));
}
const { error, message, exit = true } = option;
isNil(error) ? console.log(chalk.red(`\n❌ ${message}`)) : console.log(chalk.red(error));
if (exit) {
process.exit(1);
}

View File

@ -1,10 +1,6 @@
import { ModuleMetadata, PipeTransform, Type } from '@nestjs/common';
import { NestFastifyApplication } from '@nestjs/platform-fastify';
import { Ora } from 'ora';
import { StartOptions } from 'pm2';
import { CommandModule } from 'yargs';
import { Configure } from '../config/configure';
import { ConfigStorageOption, ConfigureFactory } from '../config/types';
@ -12,8 +8,6 @@ export type App = {
container?: NestFastifyApplication;
configure: Configure;
commands: CommandModule<RecordAny, RecordAny>[];
};
export interface CreateOptions {
@ -36,8 +30,6 @@ export interface CreateOptions {
storage: ConfigStorageOption;
};
commands: () => CommandCollection;
}
export interface ContainerBuilder {
@ -60,8 +52,6 @@ export interface AppConfig {
url?: string;
prefix?: string;
pm2?: Omit<StartOptions, 'name' | 'cwd' | 'script' | 'args' | 'interpreter' | 'watch'>;
}
export interface PanicOption {
@ -70,20 +60,4 @@ export interface PanicOption {
error?: any;
exit?: boolean;
spinner?: Ora;
}
export interface CommandOption<T = RecordAny, P = RecordAny> extends CommandModule<T, P> {
instant?: boolean;
}
export type CommandItem<T = RecordAny, P = RecordAny> = (
app: Required<App>,
) => Promise<CommandOption<T, P>>;
export type CommandCollection = Array<CommandItem<any, any>>;
export interface CreateOption {
commands: () => CommandCollection;
}

View File

@ -1,5 +1,4 @@
import { Optional } from '@nestjs/common';
import { NestFastifyApplication } from '@nestjs/platform-fastify';
import { isNil } from 'lodash';
import {
DataSource,
@ -18,10 +17,6 @@ import {
UpdateEvent,
} from 'typeorm';
import { Configure } from '@/modules/config/configure';
import { app } from '@/modules/core/helpers/app';
import { RepositoryType } from '../types';
import { getCustomRepository } from '../utils';
@ -41,25 +36,12 @@ export abstract class BaseSubscriber<T extends ObjectLiteral>
{
protected abstract entity: ObjectType<T>;
protected constructor(
@Optional() protected dataSource?: DataSource,
@Optional() protected _configure?: Configure,
) {
protected constructor(@Optional() protected dataSource?: DataSource) {
if (!isNil(this.dataSource)) {
this.dataSource.subscribers.push(this);
}
}
get configure() {
return isNil(this._configure)
? this.container.get(Configure, { strict: false })
: this._configure;
}
get container(): NestFastifyApplication {
return app.container;
}
protected getDataSource(event: SubscriberEvent<T>) {
return this.dataSource ?? event.connection;
}

View File

@ -1,34 +0,0 @@
import { Arguments } from 'yargs';
import { CommandItem } from '@/modules/core/types';
import { MigrationCreateHandler } from '@/modules/database/commands/migration.create.handler';
import { MigrationCreateArguments } from '@/modules/database/commands/types';
/**
*
*
* @param configure
* @constructor
*/
export const CreateMigrationCommand: CommandItem<any, MigrationCreateArguments> = async ({
configure,
}) => ({
source: true,
command: [],
describe: 'Creates a new migration file',
builder: {
connection: {
type: 'string',
alias: 'c',
describe: 'Connection name of typeorm to connect database.',
},
name: {
type: 'string',
alias: 'n',
describe: 'Name of the migration class.',
demandOption: true,
},
},
handler: async (args: Arguments<MigrationCreateArguments>) =>
MigrationCreateHandler(configure, args),
});

View File

@ -1,38 +0,0 @@
import chalk from 'chalk';
import { isNil } from 'lodash';
import ora from 'ora';
import { Arguments } from 'yargs';
import { Configure } from '@/modules/config/configure';
import { panic } from '@/modules/core/helpers';
import { TypeormMigrationCreate } from '@/modules/database/commands/typeorm.migration.create';
import { MigrationCreateArguments } from '@/modules/database/commands/types';
import { DBOptions, TypeormOption } from '@/modules/database/types';
/**
*
*
* @param configure
* @param args
* @constructor
*/
export async function MigrationCreateHandler(
configure: Configure,
args: Arguments<MigrationCreateArguments>,
) {
const spinner = ora('start to create migration').start();
const cname = args.connection ?? 'default';
try {
const { connections = [] } = await configure.get<DBOptions>('database');
const dbConfig: TypeormOption = connections.find(({ name }) => name === cname);
if (isNil(dbConfig)) {
await panic(`database connection ${cname} not found`);
}
const runner = new TypeormMigrationCreate();
console.log();
await runner.handler({ name: cname, dir: dbConfig.paths.migration });
spinner.start(chalk.greenBright.underline('\n 👍 Finished create migration'));
} catch (e) {
await panic({ spinner, message: 'Create migration failed!', error: e });
}
}

View File

@ -1,58 +0,0 @@
import { Arguments } from 'yargs';
import { CommandItem } from '@/modules/core/types';
import { MigrationGenerateHandler } from '@/modules/database/commands/migration.generate.handler';
import { MigrationGenerateArguments } from '@/modules/database/commands/types';
export const GenerateMigrationCommand: CommandItem<any, MigrationGenerateArguments> = async ({
configure,
}) => ({
instant: true,
command: ['db:migration:generate', 'dbmg'],
describe: 'Auto generates a new migration file with sql needs to be executed to update schema.',
builder: {
connection: {
type: 'string',
alias: 'c',
describe: 'Connection name of typeorm to connect database.',
},
name: {
type: 'string',
alias: 'n',
describe: 'Name of the migration class.',
},
run: {
type: 'boolean',
alias: 'r',
describe: 'Run migration after generated.',
default: false,
},
dir: {
type: 'string',
alias: 'd',
describe: 'Which directory where migration should be generated.',
},
pretty: {
type: 'boolean',
alias: 'p',
describe: 'Pretty-print generated SQL',
default: false,
},
dryrun: {
type: 'boolean',
alias: 'dr',
describe: 'Prints out the contents of the migration instead of writing it to a file',
default: false,
},
check: {
type: 'boolean',
alias: 'ch',
describe:
'Verifies that the current database is up to date and that no migrations are needed. Otherwise exits with code 1.',
default: false,
},
} as const,
handler: async (args: Arguments<MigrationGenerateArguments>) =>
MigrationGenerateHandler(configure, args),
});

View File

@ -1,52 +0,0 @@
import chalk from 'chalk';
import { isNil, pick } from 'lodash';
import ora from 'ora';
import { DataSource, DataSourceOptions } from 'typeorm';
import { Arguments } from 'yargs';
import { Configure } from '@/modules/config/configure';
import { getRandomString, panic } from '@/modules/core/helpers';
import { MigrationRunHandler } from '@/modules/database/commands/migration.run.handler';
import { TypeormMigrationGenerate } from '@/modules/database/commands/typeorm.migration.generate';
import { MigrationGenerateArguments } from '@/modules/database/commands/types';
import { DBOptions } from '../types';
export async function MigrationGenerateHandler(
configure: Configure,
args: Arguments<MigrationGenerateArguments>,
) {
await MigrationRunHandler(configure, { connection: args.connection } as any);
console.log();
const spinner = ora('Start to generate migration');
const cname = args.connection ?? 'default';
try {
spinner.start();
console.log();
const { connections = [] }: DBOptions = await configure.get<DBOptions>('database');
const dbConfig = connections.find(({ name }) => name === cname);
if (isNil(dbConfig)) {
await panic(`Database connection named ${cname} not exists!`);
}
console.log();
const runner = new TypeormMigrationGenerate();
const dataSource = new DataSource({ ...dbConfig } as DataSourceOptions);
console.log();
await runner.handler({
name: args.name ?? getRandomString(6),
dir: dbConfig.paths.migration,
dataSource,
...pick(args, ['pretty', 'outputJs', 'dryrun', 'check']),
});
if (dataSource.isInitialized) {
await dataSource.destroy();
}
spinner.succeed(chalk.greenBright.underline('\n 👍 Finished generate migration'));
if (args.run) {
console.log();
await MigrationRunHandler(configure, { connection: args.connection } as any);
}
} catch (error) {
await panic({ spinner, message: 'Generate migration failed!', error });
}
}

View File

@ -1,53 +0,0 @@
import { Arguments } from 'yargs';
import { CommandItem } from '@/modules/core/types';
import { MigrationRunHandler } from '@/modules/database/commands/migration.run.handler';
import { MigrationRunArguments } from '@/modules/database/commands/types';
/**
*
* @param configure
* @constructor
*/
export const RunMigrationCommand: CommandItem<any, MigrationRunArguments> = async ({
configure,
}) => ({
source: true,
command: ['db:migration:run', 'dbmr'],
describe: 'Runs all pending migrations.',
builder: {
connection: {
type: 'string',
alias: 'c',
describe: 'Connection name of typeorm to connect database.',
},
transaction: {
type: 'string',
alias: 't',
describe:
'Indicates if transaction should be used or not for migration run/revert/reflash. Enabled by default.',
default: 'default',
},
fake: {
type: 'boolean',
alias: 'f',
describe:
'Fakes running the migrations if table schema has already been changed manually or externally ' +
'(e.g. through another project)',
},
refresh: {
type: 'boolean',
alias: 'r',
describe: 'drop database schema and run migration',
default: false,
},
onlydrop: {
type: 'boolean',
alias: 'o',
describe: 'only drop database schema',
default: false,
},
} as const,
handler: async (args: Arguments<MigrationRunArguments>) => MigrationRunHandler(configure, args),
});

View File

@ -1,75 +0,0 @@
import { join } from 'path';
import chalk from 'chalk';
import { isNil } from 'lodash';
import ora from 'ora';
import { DataSource, DataSourceOptions } from 'typeorm';
import { Arguments } from 'yargs';
import { Configure } from '@/modules/config/configure';
import { panic } from '@/modules/core/helpers';
import { TypeormMigrationRun } from '@/modules/database/commands/typeorm.migration.run';
import { MigrationRunArguments } from '@/modules/database/commands/types';
import { DBOptions } from '@/modules/database/types';
/**
*
* @param configure
* @param args
* @constructor
*/
export async function MigrationRunHandler(
configure: Configure,
args: Arguments<MigrationRunArguments>,
) {
const spinner = ora('Start to run migration...');
const cname = args.connection ?? 'default';
let dataSource: DataSource | undefined;
try {
spinner.start();
const { connections = [] }: DBOptions = await configure.get<DBOptions>('database');
const dbConfig = connections.find(({ name }) => name === cname);
if (isNil(dbConfig)) {
await panic(`Database connection named ${cname} not exists!`);
}
const dropSchema = args.refresh || args.onlydrop;
console.log();
const runner = new TypeormMigrationRun();
dataSource = new DataSource({ ...dbConfig } as DataSourceOptions);
if (dataSource && dataSource.isInitialized) {
await dataSource.destroy();
}
const options = {
subscribers: [],
synchronize: false,
migrationsRun: false,
dropSchema,
logging: ['error'],
migrations: [
join(dbConfig.paths.migration, '**/*.ts'),
join(dbConfig.paths.migration, '**/*.js'),
],
} as any;
if (dropSchema) {
dataSource.setOptions(options);
await dataSource.initialize();
await dataSource.destroy();
spinner.succeed(chalk.greenBright.underline('\n 👍 Finished drop database schema'));
if (args.onlydrop) {
process.exit();
}
}
dataSource.setOptions({ ...options, dropSchema: false });
await dataSource.initialize();
console.log();
await runner.handler({ dataSource, transaction: args.transaction, fake: args.fake });
spinner.succeed(chalk.greenBright.underline('\n 👍 Finished run migrations'));
} catch (error) {
await panic({ spinner, message: 'Run migrations failed!', error });
} finally {
if (dataSource && dataSource.isInitialized) {
await dataSource.destroy();
}
}
}

View File

@ -1,45 +0,0 @@
import { resolve } from 'path';
import chalk from 'chalk';
import { CommandUtils } from 'typeorm/commands/CommandUtils';
import { PlatformTools } from 'typeorm/platform/PlatformTools';
import { camelCase } from 'typeorm/util/StringUtils';
import { MigrationCreateOptions } from '@/modules/database/commands/types';
type HandleOptions = MigrationCreateOptions & { dir: string };
export class TypeormMigrationCreate {
async handler(args: HandleOptions) {
try {
const timestamp = new Date().getTime();
const directory = args.dir.startsWith('/')
? args.dir
: resolve(process.cwd(), args.dir);
const fileContent = TypeormMigrationCreate.getTemplate(args.name, timestamp);
const fileName = `${timestamp}-${args.name}`;
const filePath = `${directory}/${fileName}`;
await CommandUtils.createFile(`${filePath}.ts`, fileContent);
console.log(
`Migration ${chalk.blue(`${filePath}.ts`)} has been generated successfully.`,
);
} catch (e) {
PlatformTools.logCmdErr('Error during migration creation:', e);
process.exit(1);
}
}
protected static getTemplate(name: string, timestamp: number): string {
return `import typeorm = require('typeorm');
class ${camelCase(name, true)}${timestamp} implements typeorm.MigrationInterface {
public async up(queryRunner: typeorm.QueryRunner): Promise<void> {
}
public async down(queryRunner: typeorm.QueryRunner): Promise<void> {
}
}
`;
}
}

View File

@ -1,152 +0,0 @@
import { resolve } from 'path';
import chalk from 'chalk';
import { upperFirst } from 'lodash';
import { format } from 'mysql2';
import { DataSource } from 'typeorm';
import { CommandUtils } from 'typeorm/commands/CommandUtils';
import { PlatformTools } from 'typeorm/platform/PlatformTools';
import { camelCase } from 'typeorm/util/StringUtils';
import { MigrationGenerateOptions } from '@/modules/database/commands/types';
type HandlerOptions = MigrationGenerateOptions & { dataSource: DataSource } & { dir: string };
export class TypeormMigrationGenerate {
async handler(args: HandlerOptions) {
const timestamp = new Date().getTime();
const fileExt = '.ts';
const directory = args.dir.startsWith('/') ? args.dir : resolve(process.cwd(), args.dir);
const filename = `${timestamp}-${args.name}`;
const filePath = `${directory}/${filename}${fileExt}`;
const { dataSource } = args;
try {
dataSource.setOptions({
synchronize: false,
migrationsRun: false,
dropSchema: false,
logging: false,
});
await dataSource.initialize();
const upSqls: string[] = [];
const downSqls: string[] = [];
try {
const sqlInMemory = await dataSource.driver.createSchemaBuilder().log();
if (args.pretty) {
sqlInMemory.upQueries.forEach((upSql) => {
upSql.query = TypeormMigrationGenerate.prettifyQuery(upSql.query);
});
sqlInMemory.downQueries.forEach((downSql) => {
downSql.query = TypeormMigrationGenerate.prettifyQuery(downSql.query);
});
}
sqlInMemory.upQueries.forEach((upQuery) => {
upSqls.push(
` await queryRunner.query(\`${upQuery.query.replace(
/`/g,
'\\`',
)}\`${TypeormMigrationGenerate.queryParams(upQuery.parameters)});`,
);
});
sqlInMemory.downQueries.forEach((downQuery) => {
downSqls.push(
` await queryRunner.query(\`${downQuery.query.replace(
/`/g,
'\\`',
)}\`${TypeormMigrationGenerate.queryParams(downQuery.parameters)});`,
);
});
} finally {
await dataSource.destroy();
}
if (!upSqls.length) {
console.log(chalk.green(`No changes in database schema were found`));
process.exit(0);
}
const fileContent = TypeormMigrationGenerate.getTemplate(
args.name,
timestamp,
upSqls,
downSqls.reverse(),
);
if (args.check) {
console.log(
chalk.yellow(
`Unexpected changes in database schema were found in check mode:\n\n${chalk.white(
fileContent,
)}`,
),
);
process.exit(1);
}
if (args.dryrun) {
console.log(
chalk.green(
`Migration ${chalk.blue(
filePath,
)} has content:\n\n${chalk.white(fileContent)}`,
),
);
} else {
await CommandUtils.createFile(filePath, fileContent);
console.log(
chalk.green(
`Migration ${chalk.blue(filePath)} has been generated successfully.`,
),
);
}
} catch (e) {
PlatformTools.logCmdErr('Error during migration generation:', e);
process.exit(1);
}
}
protected static queryParams(params: any[] | undefined): string {
if (!params || !params.length) {
return '';
}
return `,${JSON.stringify(params)}`;
}
protected static prettifyQuery(query: string) {
const formatQuery = format(query, { indent: ' ' });
return `\n${formatQuery.replace(/^/gm, ' ')}\n `;
}
protected static getTemplate(
name: string,
timestamp: number,
upSqls: string[],
downSqls: string[],
): string {
const migrationName = `${camelCase(upperFirst(name), true)}${timestamp}`;
return `import typeorm = require('typeorm');
class ${migrationName} implements typeorm.MigrationInterface {
name = '${migrationName}'
public async up(queryRunner: typeorm.QueryRunner): Promise<void> {
${upSqls.join(`
`)}
}
public async down(queryRunner: typeorm.QueryRunner): Promise<void> {
${downSqls.join(`
`)}
}
}
module.exports = ${migrationName}
`;
}
}

View File

@ -1,28 +0,0 @@
import { DataSource } from 'typeorm';
import { MigrationRunOptions } from '@/modules/database/commands/types';
type HandlerOptions = MigrationRunOptions & { dataSource: DataSource };
export class TypeormMigrationRun {
async handler({ transaction, fake, dataSource }: HandlerOptions) {
const options = {
transaction: dataSource.options.migrationsTransactionMode ?? 'all',
fake,
};
switch (transaction) {
case 'all':
options.transaction = 'all';
break;
case 'none':
case 'false':
options.transaction = 'none';
break;
case 'each':
options.transaction = 'each';
break;
default:
}
await dataSource.runMigrations(options);
}
}

View File

@ -1,57 +0,0 @@
import { Arguments } from 'yargs';
/**
*
*/
export type TypeOrmArguments = Arguments<{ connection?: string }>;
/**
*
*/
export type MigrationCreateArguments = TypeOrmArguments & MigrationCreateOptions;
/**
*
*/
export interface MigrationCreateOptions {
name?: string;
}
/**
*
*/
export type MigrationGenerateArguments = TypeOrmArguments & MigrationGenerateOptions;
/**
*
*/
export interface MigrationGenerateOptions {
name?: string;
run?: boolean;
pretty?: boolean;
dryrun?: boolean;
check?: boolean;
}
/**
*
*/
export type MigrationRunArguments = TypeOrmArguments & MigrationRunOptions;
/**
*
*/
export interface MigrationRunOptions extends MigrationRevertOptions {
refresh?: boolean;
onlydrop?: boolean;
}
/**
*
*/
export interface MigrationRevertOptions {
transaction?: string;
fake?: boolean;
}

View File

@ -1,5 +1,3 @@
import { resolve } from 'path';
import { ConfigureFactory, ConfigureRegister } from '../config/types';
import { createConnectionOptions } from '../config/utils';
import { deepMerge } from '../core/helpers';
@ -20,11 +18,7 @@ export const createDBConfig: (
export const createDBOptions = (options: DBConfig) => {
const newOptions: DBOptions = {
common: deepMerge(
{
charset: 'utf8mb4',
logging: ['error'],
paths: { migration: resolve(__dirname, '../../database/migrations') },
},
{ charset: 'utf8mb4', logging: ['error'] },
options.common ?? {},
'replace',
),
@ -35,7 +29,7 @@ export const createDBOptions = (options: DBConfig) => {
const newOption = { ...connection, entities };
return deepMerge(
newOptions.common,
{ ...newOption, autoLoadEntities: true, synchronize: false } as any,
{ ...newOption, autoLoadEntities: true } as any,
'replace',
) as TypeormOption;
});

View File

@ -17,27 +17,12 @@ export enum SelectTrashMode {
}
export enum OrderType {
/**
*
*/
ASC = 'ASC',
/**
*
*/
DESC = 'DESC',
}
export enum TreeChildrenResolve {
/**
*
*/
DELETE = 'delete',
/**
*
*/
UP = 'up',
/**
*
*/
ROOT = 'root',
}

View File

@ -2,8 +2,8 @@ import { TypeOrmModuleOptions } from '@nestjs/typeorm';
import {
FindTreeOptions,
ObjectLiteral,
Repository,
SelectQueryBuilder,
Repository,
TreeRepository,
} from 'typeorm';
@ -70,21 +70,13 @@ export type RepositoryType<T extends ObjectLiteral> =
| BaseTreeRepository<T>;
export type DBConfig = {
common: RecordAny & DBAdditionalOption;
common: RecordAny;
connections: Array<TypeOrmModuleOptions & { name?: string }>;
};
export type TypeormOption = Omit<TypeOrmModuleOptions, 'name' | 'migrations'> & {
name: string;
} & DBAdditionalOption;
export type TypeormOption = Omit<TypeOrmModuleOptions, 'name' | 'migrations'> & { name: string };
export type DBOptions = RecordAny & {
common: RecordAny;
connections: TypeormOption[];
};
type DBAdditionalOption = {
paths?: {
migration?: string;
};
};

View File

@ -1,16 +1,7 @@
import { Type } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { EntityClassOrSchema } from '@nestjs/typeorm/dist/interfaces/entity-class-or-schema.type';
import { isArray, isNil } from 'lodash';
import { DataSource, ObjectLiteral, ObjectType, Repository, SelectQueryBuilder } from 'typeorm';
import { Configure } from '@/modules/config/configure';
import {
DBOptions,
OrderQueryType,
PaginateOptions,
PaginateReturn,
} from '@/modules/database/types';
import { OrderQueryType, PaginateOptions, PaginateReturn } from '@/modules/database/types';
import { CUSTOM_REPOSITORY_METADATA } from './constants';
@ -106,48 +97,3 @@ export const getCustomRepository = <P extends Repository<T>, T extends ObjectLit
const base = dataSource.getRepository<ObjectType<any>>(entity);
return new Repo(base.target, base.manager, base.queryRunner) as P;
};
export const addEntities = async (
configure: Configure,
entities: EntityClassOrSchema[] = [],
dataSource = 'default',
) => {
const database = await configure.get<DBOptions>('database');
if (isNil(database)) {
throw new Error('Database not exists');
}
const dbConfig = database.connections.find(({ name }) => name === dataSource);
if (isNil(dbConfig)) {
throw new Error(`Database connection ${dataSource} not exists`);
}
const oldEntities = (dbConfig.entities ?? []) as ObjectLiteral[];
const newEntities = database.connections.map((conn) =>
conn.name === dataSource ? { ...conn, entities: [...oldEntities, ...entities] } : conn,
);
configure.set('database.connections', newEntities);
return TypeOrmModule.forFeature(entities, dataSource);
};
export async function addSubscribers(
configure: Configure,
subscribers: Type<any>[] = [],
dataSource = 'default',
) {
const database = await configure.get<DBOptions>('database');
if (isNil(database)) {
throw new Error('Database not exists');
}
const dbConfig = database.connections.find(({ name }) => name === dataSource);
if (isNil(dbConfig)) {
throw new Error(`Database connection ${dataSource} not exists`);
}
const oldSubscribers = (dbConfig.subscribers ?? []) as any[];
const newSubscribers = database.connections.map((conn) =>
conn.name === dataSource
? { ...conn, subscribers: [...oldSubscribers, subscribers] }
: conn,
);
configure.set('database.connections', newSubscribers);
return subscribers;
}

View File

@ -1,7 +1,7 @@
import { ConfigureFactory, ConfigureRegister } from '../config/types';
import { createConnectionOptions } from '../config/utils';
import type { MeiliConfig } from './types';
import { MeiliConfig } from './types';
export const createMeiliConfig: (
registre: ConfigureRegister<RePartial<MeiliConfig>>,

View File

@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
import { isNil } from 'lodash';
import { MeiliSearch } from 'meilisearch';
import type { MeiliConfig } from '@/modules/meilisearch/types';
import { MeiliConfig } from '@/modules/meilisearch/types';
@Injectable()
export class MeiliService {

View File

@ -1,5 +1,5 @@
import { Transform } from 'class-transformer';
import { IsNumber, IsOptional, Min } from 'class-validator';
import { Min, IsNumber, IsOptional } from 'class-validator';
import { toNumber } from 'lodash';
import { DtoValidation } from '@/modules/core/decorator/dto.validation.decorator';
@ -14,7 +14,7 @@ export class PaginateDto implements PaginateOptions {
*
*/
@Transform(({ value }) => toNumber(value))
@Min(1, { message: 'The current page must be greater than 1.' })
@Min(1, { message: '当前页必须大于1' })
@IsNumber()
@IsOptional()
page?: number = 1;
@ -23,7 +23,7 @@ export class PaginateDto implements PaginateOptions {
*
*/
@Transform(({ value }) => toNumber(value))
@Min(1, { message: 'The number of data displayed per page must be greater than 1.' })
@Min(1, { message: '每页显示数据必须大于1' })
@IsNumber()
@IsOptional()
limit?: number = 10;

View File

@ -17,7 +17,6 @@ import { RestfulModule } from './modules/restful/restful.module';
import { ApiConfig } from './modules/restful/types';
export const createOptions: CreateOptions = {
commands: () => [],
config: { factories: configs as any, storage: { enable: true } },
modules: async (configure) => [
DatabaseModule.forRoot(configure),

View File

@ -21,8 +21,6 @@ import { createOptions } from '@/options';
import { generateRandomNumber, generateUniqueRandomNumbers } from './generate-mock-data';
import { categoriesData, commentData, INIT_DATA, postData, tagData } from './test-data';
const URL_PREFIX = '/api/v1/content';
describe('nest app test', () => {
let datasource: DataSource;
let app: NestFastifyApplication;
@ -81,7 +79,7 @@ describe('nest app test', () => {
ids.map(async (id) => {
const result = await app.inject({
method: 'GET',
url: `${URL_PREFIX}/category/${id}`,
url: `/category/${id}`,
});
categories.push(result.json());
return result.json();
@ -121,7 +119,7 @@ describe('nest app test', () => {
it('create category without name', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {},
});
expect(result.json()).toEqual({
@ -137,7 +135,7 @@ describe('nest app test', () => {
it('create category with long name', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: { name: 'A'.repeat(30) },
});
expect(result.json()).toEqual({
@ -151,7 +149,7 @@ describe('nest app test', () => {
const rootCategory = categories.find((c) => !c.parent);
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: { name: rootCategory.name },
});
expect(result.json()).toEqual({
@ -165,7 +163,7 @@ describe('nest app test', () => {
const testData = categories.find((item) => !isNil(item.parent));
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
name: testData.name,
parent: testData.parent.id,
@ -181,7 +179,7 @@ describe('nest app test', () => {
it('create category with invalid parent id format', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
name: 'New Category',
parent: 'invalid-uuid',
@ -200,7 +198,7 @@ describe('nest app test', () => {
it('create category with non-existent parent id', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
name: 'New Category',
parent: '74e655b3-b69a-42ae-a101-41c224386e74',
@ -216,7 +214,7 @@ describe('nest app test', () => {
it('create category with negative custom order', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
name: 'New Category',
customOrder: -1,
@ -232,7 +230,7 @@ describe('nest app test', () => {
it('create category with empty name', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: { name: '' },
});
expect(result.json()).toEqual({
@ -245,7 +243,7 @@ describe('nest app test', () => {
it('create category with whitespace name', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: { name: ' ' },
});
expect(result.json()).toEqual({
@ -259,7 +257,7 @@ describe('nest app test', () => {
const name = 'A'.repeat(25);
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: { name },
});
expect(result.statusCode).toEqual(201);
@ -267,14 +265,14 @@ describe('nest app test', () => {
expect(category.name).toBe(name);
await app.inject({
method: 'DELETE',
url: `${URL_PREFIX}/category/${result.json().id}`,
url: `/category/${result.json().id}`,
});
});
it('create category with name one char over limit (26 chars)', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: { name: 'A'.repeat(26) },
});
expect(result.json()).toEqual({
@ -288,7 +286,7 @@ describe('nest app test', () => {
const rootCategory = categories.find((c) => !c.parent);
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: { name: rootCategory.name },
});
expect(result.json()).toEqual({
@ -304,7 +302,7 @@ describe('nest app test', () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
name: existingChild.name,
parent: parentCategory.id,
@ -324,7 +322,7 @@ describe('nest app test', () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
name: childName,
parent: parent2.id,
@ -333,14 +331,14 @@ describe('nest app test', () => {
expect(result.statusCode).toEqual(201);
await app.inject({
method: 'DELETE',
url: `${URL_PREFIX}/category/${result.json().id}`,
url: `/category/${result.json().id}`,
});
});
it('create category with parent set to null string', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
name: 'Root Category',
parent: 'null', // 注意:这里传递字符串 'null'
@ -351,14 +349,14 @@ describe('nest app test', () => {
expect(category.parent).toBeNull();
await app.inject({
method: 'DELETE',
url: `${URL_PREFIX}/category/${result.json().id}`,
url: `/category/${result.json().id}`,
});
});
it('create category with parent set to null value', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
name: 'Root Category',
parent: null,
@ -369,14 +367,14 @@ describe('nest app test', () => {
expect(category.parent).toBeNull();
await app.inject({
method: 'DELETE',
url: `${URL_PREFIX}/category/${result.json().id}`,
url: `/category/${result.json().id}`,
});
});
it('create category with empty parent id', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
name: 'New Category',
parent: '',
@ -395,7 +393,7 @@ describe('nest app test', () => {
it('create category with malformed UUID parent id', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
name: 'New Category',
parent: 'not-a-valid-uuid-123',
@ -414,7 +412,7 @@ describe('nest app test', () => {
it('create category with customOrder as string', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
name: 'New Category',
customOrder: '10', // 字符串形式的数字
@ -425,14 +423,14 @@ describe('nest app test', () => {
expect(category.customOrder).toBe(10);
await app.inject({
method: 'DELETE',
url: `${URL_PREFIX}/category/${result.json().id}`,
url: `/category/${result.json().id}`,
});
});
it('create category with customOrder as float', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
name: 'New Category',
customOrder: 5.5,
@ -448,7 +446,7 @@ describe('nest app test', () => {
it('create category with customOrder as negative number', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
name: 'New Category',
customOrder: -1,
@ -464,7 +462,7 @@ describe('nest app test', () => {
it('create category with customOrder as zero', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
name: 'New Category',
customOrder: 0,
@ -475,14 +473,14 @@ describe('nest app test', () => {
expect(category.customOrder).toBe(0);
await app.inject({
method: 'DELETE',
url: `${URL_PREFIX}/category/${result.json().id}`,
url: `/category/${result.json().id}`,
});
});
it('create category with customOrder as large number', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
name: 'New Category',
customOrder: 999999,
@ -491,7 +489,7 @@ describe('nest app test', () => {
expect(result.statusCode).toEqual(201);
await app.inject({
method: 'DELETE',
url: `${URL_PREFIX}/category/${result.json().id}`,
url: `/category/${result.json().id}`,
});
});
@ -499,7 +497,7 @@ describe('nest app test', () => {
const parent = categories.find((c) => !c.parent);
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
name: 'Valid New Category',
parent: parent.id,
@ -513,7 +511,7 @@ describe('nest app test', () => {
expect(category.customOrder).toBe(5);
await app.inject({
method: 'DELETE',
url: `${URL_PREFIX}/category/${result.json().id}`,
url: `/category/${result.json().id}`,
});
});
@ -522,7 +520,7 @@ describe('nest app test', () => {
const category = categories[0];
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
name: 'Invalid Category',
parent: category.id,
@ -537,7 +535,7 @@ describe('nest app test', () => {
it('update category without id', async () => {
const result = await app.inject({
method: 'PATCH',
url: `${URL_PREFIX}/category`,
url: '/category',
body: { name: 'Updated Category' },
});
expect(result.json()).toEqual({
@ -554,7 +552,7 @@ describe('nest app test', () => {
it('update category with invalid id format', async () => {
const result = await app.inject({
method: 'PATCH',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
id: 'invalid-uuid',
name: 'Updated Category',
@ -574,7 +572,7 @@ describe('nest app test', () => {
it('update category with non-existent id', async () => {
const result = await app.inject({
method: 'PATCH',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
id: '74e655b3-b69a-42ae-a101-41c224386e74',
name: 'Updated Category',
@ -587,7 +585,7 @@ describe('nest app test', () => {
const category = categories[0];
const result = await app.inject({
method: 'PATCH',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
id: category.id,
name: 'A'.repeat(30),
@ -606,7 +604,7 @@ describe('nest app test', () => {
const result = await app.inject({
method: 'PATCH',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
id: child1.id,
name: child2.name,
@ -623,7 +621,7 @@ describe('nest app test', () => {
const category = categories[0];
const result = await app.inject({
method: 'PATCH',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
id: category.id,
parent: 'invalid-uuid',
@ -643,7 +641,7 @@ describe('nest app test', () => {
const category = categories[0];
const result = await app.inject({
method: 'PATCH',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
id: category.id,
parent: '74e655b3-b69a-42ae-a101-41c224386e74',
@ -660,7 +658,7 @@ describe('nest app test', () => {
const category = categories[0];
const result = await app.inject({
method: 'PATCH',
url: `${URL_PREFIX}/category`,
url: '/category',
body: {
id: category.id,
customOrder: -1,
@ -677,7 +675,7 @@ describe('nest app test', () => {
it('query categories with invalid page', async () => {
const result = await app.inject({
method: 'GET',
url: `${URL_PREFIX}/category?page=0`,
url: '/category?page=0',
});
expect(result.json()).toEqual({
message: ['The current page must be greater than 1.'],
@ -689,7 +687,7 @@ describe('nest app test', () => {
it('query categories with invalid limit', async () => {
const result = await app.inject({
method: 'GET',
url: `${URL_PREFIX}/category?limit=0`,
url: '/category?limit=0',
});
expect(result.json()).toEqual({
message: ['The number of data displayed per page must be greater than 1.'],
@ -709,7 +707,7 @@ describe('nest app test', () => {
it('create tag without name', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/tag`,
url: '/tag',
body: {},
});
expect(result.json()).toEqual({
@ -726,7 +724,7 @@ describe('nest app test', () => {
it('create tag with long name', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/tag`,
url: '/tag',
body: { name: 'A'.repeat(256) },
});
expect(result.json()).toEqual({
@ -740,7 +738,7 @@ describe('nest app test', () => {
const existingTag = tags[0];
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/tag`,
url: '/tag',
body: { name: existingTag.name },
});
expect(result.json()).toEqual({
@ -753,7 +751,7 @@ describe('nest app test', () => {
it('create tag with long description', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/tag`,
url: '/tag',
body: {
name: 'NewTag',
desc: 'A'.repeat(501),
@ -770,7 +768,7 @@ describe('nest app test', () => {
it('update tag without id', async () => {
const result = await app.inject({
method: 'PATCH',
url: `${URL_PREFIX}/tag`,
url: '/tag',
body: { name: 'Updated Tag' },
});
expect(result.json()).toEqual({
@ -787,7 +785,7 @@ describe('nest app test', () => {
it('update tag with invalid id format', async () => {
const result = await app.inject({
method: 'PATCH',
url: `${URL_PREFIX}/tag`,
url: '/tag',
body: {
id: 'invalid-uuid',
name: 'Updated Tag',
@ -803,7 +801,7 @@ describe('nest app test', () => {
it('update tag with non-existent id', async () => {
const result = await app.inject({
method: 'PATCH',
url: `${URL_PREFIX}/tag`,
url: '/tag',
body: {
id: '74e655b3-b69a-42ae-a101-41c224386e74',
name: 'Updated Tag',
@ -820,7 +818,7 @@ describe('nest app test', () => {
const tag = tags[0];
const result = await app.inject({
method: 'PATCH',
url: `${URL_PREFIX}/tag`,
url: '/tag',
body: {
id: tag.id,
name: 'A'.repeat(256),
@ -837,7 +835,7 @@ describe('nest app test', () => {
const [tag1, tag2] = tags;
const result = await app.inject({
method: 'PATCH',
url: `${URL_PREFIX}/tag`,
url: '/tag',
body: {
id: tag1.id,
name: tag2.name,
@ -854,7 +852,7 @@ describe('nest app test', () => {
const tag = tags[0];
const result = await app.inject({
method: 'PATCH',
url: `${URL_PREFIX}/tag`,
url: '/tag',
body: {
id: tag.id,
desc: 'A'.repeat(501),
@ -880,7 +878,7 @@ describe('nest app test', () => {
it('create post without title', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/posts`,
url: '/posts',
body: { body: 'Post content' },
});
expect(result.json()).toEqual({
@ -896,7 +894,7 @@ describe('nest app test', () => {
it('create post without body', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/posts`,
url: '/posts',
body: { title: 'New Post' },
});
expect(result.json()).toEqual({
@ -909,7 +907,7 @@ describe('nest app test', () => {
it('create post with long title', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/posts`,
url: '/posts',
body: {
title: 'A'.repeat(256),
body: 'Post content',
@ -925,7 +923,7 @@ describe('nest app test', () => {
it('create post with long summary', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/posts`,
url: '/posts',
body: {
title: 'New Post',
body: 'Content',
@ -942,7 +940,7 @@ describe('nest app test', () => {
it('create post with invalid category', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/posts`,
url: '/posts',
body: {
title: 'New Post',
body: 'Content',
@ -959,7 +957,7 @@ describe('nest app test', () => {
it('create post with non-existent category', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/posts`,
url: '/posts',
body: {
title: 'New Post',
body: 'Content',
@ -976,7 +974,7 @@ describe('nest app test', () => {
it('create post with invalid tag format', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/posts`,
url: '/posts',
body: {
title: 'New Post',
body: 'Content',
@ -993,7 +991,7 @@ describe('nest app test', () => {
it('create post with non-existent tag', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/posts`,
url: '/posts',
body: {
title: 'New Post',
body: 'Content',
@ -1010,7 +1008,7 @@ describe('nest app test', () => {
it('create post with long keyword', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/posts`,
url: '/posts',
body: {
title: 'New Post',
body: 'Content',
@ -1027,7 +1025,7 @@ describe('nest app test', () => {
it('create post with negative custom order', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/posts`,
url: '/posts',
body: {
title: 'New Post',
body: 'Content',
@ -1045,7 +1043,7 @@ describe('nest app test', () => {
it('update post without id', async () => {
const result = await app.inject({
method: 'PATCH',
url: `${URL_PREFIX}/posts`,
url: '/posts',
body: { title: 'Updated Post' },
});
expect(result.json()).toEqual({
@ -1061,7 +1059,7 @@ describe('nest app test', () => {
it('update post with invalid id format', async () => {
const result = await app.inject({
method: 'PATCH',
url: `${URL_PREFIX}/posts`,
url: '/posts',
body: {
id: 'invalid-uuid',
title: 'Updated Post',
@ -1080,7 +1078,7 @@ describe('nest app test', () => {
it('update post with non-existent id', async () => {
const result = await app.inject({
method: 'PATCH',
url: `${URL_PREFIX}/posts`,
url: '/posts',
body: {
id: '74e655b3-b69a-42ae-a101-41c224386e74',
title: 'Updated Post non-existent id',
@ -1097,7 +1095,7 @@ describe('nest app test', () => {
const post = posts[0];
const result = await app.inject({
method: 'PATCH',
url: `${URL_PREFIX}/posts`,
url: '/posts',
body: {
id: post.id,
title: 'A'.repeat(256),
@ -1124,7 +1122,7 @@ describe('nest app test', () => {
const post = posts[0];
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/comment`,
url: '/comment',
body: { post: post.id },
});
expect(result.json()).toEqual({
@ -1140,7 +1138,7 @@ describe('nest app test', () => {
it('create comment without post', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/comment`,
url: '/comment',
body: { body: 'Test comment' },
});
expect(result.json()).toEqual({
@ -1154,7 +1152,7 @@ describe('nest app test', () => {
const post = posts[0];
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/comment`,
url: '/comment',
body: {
body: 'A'.repeat(1001),
post: post.id,
@ -1170,7 +1168,7 @@ describe('nest app test', () => {
it('create comment with invalid post format', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/comment`,
url: '/comment',
body: {
body: 'Test comment',
post: 'invalid-uuid',
@ -1186,7 +1184,7 @@ describe('nest app test', () => {
it('create comment with non-existent post', async () => {
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/comment`,
url: '/comment',
body: {
body: 'Test comment',
post: '74e655b3-b69a-42ae-a101-41c224386e74',
@ -1203,7 +1201,7 @@ describe('nest app test', () => {
const post = posts[0];
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/comment`,
url: '/comment',
body: {
body: 'Test comment',
post: post.id,
@ -1221,7 +1219,7 @@ describe('nest app test', () => {
const post = posts[0];
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/comment`,
url: '/comment',
body: {
body: 'Test comment',
post: post.id,
@ -1253,7 +1251,7 @@ async function addCategory(
const item = data[index];
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/category`,
url: '/category',
body: { ...pick(item, ['name', 'customOrder']), parent: parentId },
});
const addedItem: CategoryEntity = result.json();
@ -1271,7 +1269,7 @@ async function addTag(app: NestFastifyApplication, data: RecordAny[]): Promise<T
const item = data[index];
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/tag`,
url: '/tag',
body: item,
});
const addedItem: TagEntity = result.json();
@ -1295,7 +1293,7 @@ async function addPost(
item.tags = generateUniqueRandomNumbers(0, tags.length - 1, 3).map((idx) => tags[idx]);
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/posts`,
url: '/posts',
body: item,
});
const addedItem: PostEntity = result.json();
@ -1325,7 +1323,7 @@ async function addComment(
: undefined;
const result = await app.inject({
method: 'POST',
url: `${URL_PREFIX}/comment`,
url: '/comment',
body: item,
});
const addedItem = result.json();

View File

@ -31,7 +31,6 @@
"lib": ["esnext", "DOM", "ScriptHost", "WebWorker"],
"baseUrl": ".",
"outDir": "./dist",
"types": ["bun-types", "@types/jest"],
"paths": {
"@/*": ["./src/*"]
}