
Security News
Axios Maintainer Confirms Social Engineering Attack Behind npm Compromise
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.
@meta-1/nest-common
Advanced tools
Common utilities and decorators for NestJS applications including caching, distributed lock, i18n, error handling, and more
NestJS 应用的通用工具库,提供缓存、分布式锁、国际化、错误处理、事务管理等功能。
@Cacheable 和 @CacheEvict 装饰器,支持 Redis,CacheEvict 支持 keys 和 #{result} 占位符@WithLock 装饰器,基于 Redis 实现分布式锁@SnowflakeId 装饰器,自动生成分布式唯一ID@Transactional 装饰器,自动管理数据库事务@I18n 装饰器和 I18nContext,支持命名空间和自动采集createI18nZodDto 自动采集验证错误消息syncLocales 同步翻译文件到 i18n 目录runMigration 可配置的 TypeORM 多模块迁移npm install @meta-1/nest-common
# 或
pnpm add @meta-1/nest-common
# 或
yarn add @meta-1/nest-common
npm install @nestjs/common @nestjs/platform-express @nestjs-modules/ioredis nestjs-i18n nestjs-zod ioredis lodash nacos yaml zod
说明: 缓存和分布式锁依赖 @nestjs-modules/ioredis,需先配置 RedisModule。
import { CommonModule } from '@meta-1/nest-common';
@Module({
imports: [CommonModule],
})
export class AppModule {}
CommonModule 会自动注册以下全局功能:
ResponseInterceptor - 统一响应格式ErrorsFilter - 全局错误处理ZodValidationPipe - Zod 验证管道HttpService - HTTP 客户端服务缓存功能依赖 @nestjs-modules/ioredis,需在应用模块中导入 RedisModule:
import { RedisModule } from '@nestjs-modules/ioredis';
@Module({
imports: [
RedisModule.forRoot({
type: 'single',
url: 'redis://localhost:6379',
}),
CommonModule,
],
})
export class AppModule {}
CommonModule 内置 CacheableInitializer,会自动发现所有使用 @CacheableService() 的类并注入 Redis。
import { CacheableService, Cacheable, CacheEvict } from '@meta-1/nest-common';
@CacheableService()
@Injectable()
export class UserService {
// 缓存结果,默认 TTL 300 秒
@Cacheable({ key: 'user:#{0}', ttl: 300 })
async getUserById(id: string) {
return await this.userRepository.findOne({ where: { id } });
}
// 使用对象属性作为缓存键
@Cacheable({ key: 'user:#{user.id}:profile', ttl: 600 })
async getUserProfile(user: { id: string }) {
return await this.userRepository.findProfile(user.id);
}
// 清除单个缓存
@CacheEvict({ key: 'user:#{0}' })
async updateUser(id: string, data: UpdateUserDto) {
return await this.userRepository.update(id, data);
}
// 清除多个缓存(支持从返回值读取)
@CacheEvict({
keys: ['user:#{0}', 'user:#{result.id}:profile', 'user:#{result.channel}:#{result.username}'],
})
async updateUserWithEvict(id: string, data: UpdateUserDto) {
const user = await this.userRepository.update(id, data);
return user; // #{result.id} 等从返回值读取
}
}
缓存键占位符:
#{0}, #{1}, #{2} - 使用参数位置索引#{user.id}, #{name} - 使用第一个参数的属性(等同于 #{0.user.id})#{1.book.title} - 使用指定参数的路径属性#{result}, #{result.id} - 从方法返回值读取(仅 @CacheEvict 的 keys 选项)缓存键前缀: 键名会自动添加 cache: 前缀(若已以 cache: 开头则不会重复添加)
分布式锁同样依赖 RedisModule,与缓存共用同一 Redis 实例。确保已按「缓存装饰器」章节配置 RedisModule 即可,LockInitializer 会自动发现并注入。
锁键前缀: 键名会自动添加 lock: 前缀(若已以 lock: 开头则不会重复添加)
import { WithLock } from '@meta-1/nest-common';
@Injectable()
export class OrderService {
// 防止同一用户重复创建订单
@WithLock({
key: 'order:create:#{0}',
ttl: 10000, // 锁过期时间:10秒
waitTimeout: 3000, // 等待锁的超时时间:3秒
})
async createOrder(userId: string, items: OrderItem[]) {
// 同一用户的订单创建操作会被加锁
return await this.orderRepository.save({ userId, items });
}
// 防止重复支付
@WithLock({
key: 'payment:#{0}',
ttl: 30000,
waitTimeout: 0, // 不等待,立即失败
errorMessage: '订单正在支付中,请勿重复提交',
})
async processPayment(orderId: string) {
// 支付逻辑
}
// 使用对象属性作为锁键
@WithLock({
key: 'inventory:#{product.id}',
ttl: 5000,
})
async reduceInventory(product: { id: string; quantity: number }) {
// 库存扣减逻辑
}
}
配置选项:
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
key | string | 必填 | 锁的键名,支持占位符 |
ttl | number | 30000 | 锁的过期时间(毫秒) |
waitTimeout | number | 5000 | 等待锁的超时时间(毫秒),0 表示不等待 |
retryInterval | number | 100 | 重试获取锁的间隔(毫秒) |
errorMessage | string | '操作正在处理中,请稍后重试' | 获取锁失败时的错误提示 |
import { SnowflakeId } from '@meta-1/nest-common';
import { Entity, Column } from 'typeorm';
@Entity()
export class User {
@SnowflakeId()
id: string; // 自动生成分布式唯一ID
@Column()
name: string;
}
// 使用时无需手动设置 ID
const user = new User();
user.name = 'Alice';
await repository.save(user); // ID 自动生成
环境变量配置:
SNOWFLAKE_WORKER_ID=0 # 0-31
SNOWFLAKE_DATACENTER_ID=0 # 0-31
特性:
import { Transactional } from '@meta-1/nest-common';
@Injectable()
export class OrderService {
constructor(
@InjectRepository(Order) private orderRepo: Repository<Order>,
@InjectRepository(OrderItem) private itemRepo: Repository<OrderItem>,
) {}
@Transactional()
async createOrder(orderData: CreateOrderDto) {
// 所有数据库操作都在同一个事务中
const order = await this.orderRepo.save(orderData);
for (const item of orderData.items) {
await this.itemRepo.save({ ...item, orderId: order.id });
}
// 如果任何操作失败,整个事务会自动回滚
return order;
}
}
注意事项:
import { I18nModule } from 'nestjs-i18n';
import * as path from 'path';
@Module({
imports: [
I18nModule.forRoot({
fallbackLanguage: 'en',
loaderOptions: {
path: path.join(__dirname, '/i18n/'),
watch: true,
},
}),
],
})
export class AppModule {}
import { I18n, I18nContext } from '@meta-1/nest-common';
@Controller('users')
export class UserController {
@Get()
async getUsers(@I18n() i18n: I18nContext) {
const users = await this.userService.findAll();
return {
message: i18n.t('users.list.success'), // 自动添加 'common.' 前缀
data: users,
};
}
@Post()
async createUser(
@Body() dto: CreateUserDto,
@I18n() i18n: I18nContext,
) {
const user = await this.userService.create(dto);
return {
message: i18n.t('users.create.success', {
args: { name: user.name },
}),
data: user,
};
}
}
自定义命名空间:
import { createI18nContext, type RawI18nContext } from '@meta-1/nest-common';
import { I18n as NestI18n } from 'nestjs-i18n';
@Controller('products')
export class ProductController {
@Get()
async getProducts(@NestI18n() rawI18n: RawI18nContext) {
const i18n = createI18nContext(rawI18n, 'products');
return {
message: i18n.t('list.success'), // 翻译为 'products.list.success'
data: await this.productService.findAll(),
};
}
}
import { defineErrorCode, AppError } from '@meta-1/nest-common';
// 定义模块错误码
export const UserErrorCode = defineErrorCode({
USER_NOT_FOUND: { code: 2000, message: 'User not found' },
USER_ALREADY_EXISTS: { code: 2001, message: 'User already exists' },
});
// 使用
@Injectable()
export class UserService {
async getUserById(id: string) {
const user = await this.userRepository.findOne({ where: { id } });
if (!user) {
throw new AppError(UserErrorCode.USER_NOT_FOUND, { userId: id });
}
return user;
}
}
错误码范围约定:
0-999: 通用错误(@meta-1/nest-common)1000-1999: Message 模块错误2000-2999: User 模块错误3000-3999: Auth 模块错误100-199: 分布式锁错误错误响应格式:
{
"code": 2000,
"success": false,
"message": "User not found",
"data": { "userId": "123" },
"timestamp": "2024-01-01T00:00:00.000Z",
"path": "/api/users/123"
}
import { PageRequestDto, PageDataDto } from '@meta-1/nest-common';
import { ApiOkResponse } from '@nestjs/swagger';
import { createPageSchema, createPageModels } from '@meta-1/nest-common';
@Controller('users')
export class UserController {
@Get()
@ApiOkResponse({
schema: createPageSchema(UserDto),
})
@ApiExtraModels(...createPageModels(UserDto))
async getUsers(@Query() query: PageRequestDto) {
const [data, total] = await this.userService.findAndCount(query);
return PageDataDto.of(total, data);
}
}
import { HttpService } from '@meta-1/nest-common';
@Injectable()
export class ExternalApiService {
constructor(private readonly httpService: HttpService) {}
async fetchData() {
// GET 请求
const response = await this.httpService.get<Data>('https://api.example.com/data');
return response.data;
}
async postData(data: any) {
// POST 请求(支持重试)
const response = await this.httpService.post<Result>('https://api.example.com/data', data, {
retries: 3,
retryDelay: 1000,
});
return response.data;
}
async downloadFile(url: string, filePath: string) {
// 下载文件(支持进度回调)
await this.httpService.download({
url,
filePath,
onProgress: (progress) => {
console.log(`下载进度: ${progress}%`);
},
});
}
}
import { ConfigLoader, ConfigSourceType } from '@meta-1/nest-common';
// 从本地 YAML 文件加载
const loader = new ConfigLoader<AppConfig>({
type: ConfigSourceType.LOCAL_YAML,
filePath: './config/app.yaml',
});
const config = await loader.load();
// 从 Nacos 加载
const nacosLoader = new ConfigLoader<AppConfig>({
type: ConfigSourceType.NACOS,
server: '127.0.0.1:8848',
dataId: 'app-config',
group: 'DEFAULT_GROUP',
namespace: 'public',
username: 'nacos',
password: 'nacos',
});
const nacosConfig = await nacosLoader.load();
配置特性:
import { createI18nZodDto } from '@meta-1/nest-common';
import { z } from 'zod';
const CreateUserSchema = z.object({
email: z.string().email('Invalid email format'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
export class CreateUserDto extends createI18nZodDto(CreateUserSchema) {}
特性:
import { createPageSchema, createPageModels } from '@meta-1/nest-common';
import { ApiOkResponse, ApiExtraModels } from '@nestjs/swagger';
@Controller('users')
export class UserController {
@Get()
@ApiOkResponse({
schema: createPageSchema(UserDto),
})
@ApiExtraModels(...createPageModels(UserDto))
async getUsers() {
// ...
}
}
import { generateKey, md5, PlainTextLogger } from '@meta-1/nest-common';
// 生成动态键名(参数)
const key = generateKey('user:#{0}:profile:#{1.name}', ['123', { name: 'Alice' }]);
// 结果: 'user:123:profile:Alice'
// 从返回值生成键名(用于 CacheEvict 的 keys)
const evictKey = generateKey('user:#{result.id}', [], { id: '456' });
// 结果: 'user:456'
// MD5 哈希
const hash = md5('hello world');
// TypeORM 纯文本日志输出器
const logger = new PlainTextLogger();
开发环境下自动收集缺失的翻译键并写入 locales 文件:
import * as path from 'path';
import { initI18nCollector, getI18nCollector } from '@meta-1/nest-common';
import { I18nModule } from 'nestjs-i18n';
// 1. 在应用启动时先初始化收集器(仅开发环境)
if (process.env.NODE_ENV === 'development') {
const localesDir = path.join(process.cwd(), 'locales');
initI18nCollector(localesDir);
}
// 2. 配置 I18nModule 时传入 missingKeyHandler
I18nModule.forRoot({
// ... 其他配置
missingKeyHandler: (key: string) => {
const collector = getI18nCollector();
if (collector) {
const actualKey = key.includes('.') ? key.split('.').slice(1).join('.') : key;
collector.add(actualKey);
}
},
});
将 locales/*.json 同步到 i18n 目标目录:
import { syncLocales } from '@meta-1/nest-common';
// 一次性同步
syncLocales({
sourceDir: './locales',
targetDir: './dist/apps/server/i18n',
});
// 监听模式(开发时)
syncLocales({
sourceDir: './locales',
targetDir: './dist/apps/server/i18n',
watch: true,
});
可配置的迁移脚本,支持多模块:
import { runMigration } from '@meta-1/nest-common';
runMigration({
libsPath: path.join(process.cwd(), 'libs'),
dataSourcePath: path.join(process.cwd(), 'apps/server/src/data-source.ts'),
});
用法:
migration:generate <模块名> <迁移名称> - 生成迁移migration:create <模块名> <迁移名称> - 创建空迁移migration:run - 执行所有待运行迁移migration:revert - 回滚最后一次迁移migration:show - 显示迁移状态@CacheableService() - 标记服务类支持缓存@Cacheable(options) - 缓存方法结果@CacheEvict(options) - 清除缓存@WithLock(options) - 分布式锁@SnowflakeId() - 自动生成雪花ID@Transactional() - 自动事务管理@I18n() - 注入 I18nContextCommonModule - 通用模块AppError - 自定义错误类I18nContext - 国际化上下文ErrorsFilter - 全局异常过滤器ResponseInterceptor - 响应拦截器HttpService - HTTP 客户端服务PageRequestDto - 分页请求 DTOPageDataDto<T> - 分页响应 DTOConfigLoader<T> - 配置加载器PlainTextLogger - TypeORM 纯文本日志输出器defineErrorCode(definition) - 定义错误码createI18nZodDto(schema) - 创建支持 i18n 的 Zod DTOcreateI18nContext(context, namespace) - 创建自定义命名空间上下文createPageSchema(itemDto) - 创建分页 Swagger SchemacreatePageModels(itemDto) - 创建分页 Swagger ModelsgenerateKey(pattern, args, result?) - 生成动态键名,result 用于 #{result} 占位符md5(text) - MD5 哈希initI18nCollector(localesDir) - 初始化 I18n 缺失键收集器(仅开发环境)getI18nCollector() - 获取 I18n 收集器实例syncLocales(options) - 同步 locales 文件到 i18n 目录runMigration(config) - 执行 TypeORM 迁移 CLI通用错误码 (0-999):
SERVER_ERROR (500) - 服务器错误VALIDATION_FAILED (400) - 验证失败UNAUTHORIZED (401) - 未授权FORBIDDEN (403) - 禁止访问NOT_FOUND (404) - 未找到I18N_CONTEXT_NOT_FOUND (500) - I18n 上下文未找到CONFIG_NOT_FOUND (3000) - 配置未找到CONFIG_INVALID (3001) - 配置无效分布式锁错误码 (100-199):
REDIS_NOT_INJECTED (100) - Redis 未注入LOCK_ACQUIRE_FAILED (110) - 获取锁失败LOCK_ACQUIRE_ERROR (111) - 获取锁时发生错误LOCK_RELEASE_ERROR (112) - 释放锁时发生错误MIT
FAQs
Common utilities and decorators for NestJS applications including caching, distributed lock, i18n, error handling, and more
We found that @meta-1/nest-common demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.

Security News
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.