npm 核心包源码

This commit is contained in:
啊平 2023-04-04 09:15:48 +08:00
parent 68f91fa9f8
commit 600e2396c4
186 changed files with 8826 additions and 0 deletions

View File

@ -0,0 +1,11 @@
# 🎨 editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

View File

@ -0,0 +1,20 @@
{
"extends": "./node_modules/mwts/",
"ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings"],
"env": {
"jest": true
},
"rules": {
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/ban-ts-comment": "off",
"node/no-extraneous-import": "off",
"no-empty": "off",
"node/no-extraneous-require": "off",
"eqeqeq": "off",
"node/no-unsupported-features/node-builtins": "off",
"@typescript-eslint/ban-types": "off",
"no-control-regex": "off",
"prefer-const": "off"
}
}

15
packages/cloud/.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
logs/
npm-debug.log
yarn-error.log
node_modules/
package-lock.json
yarn.lock
coverage/
dist/
.idea/
run/
.DS_Store
*.sw*
*.un~
.tsbuildinfo
.tsbuildinfo.*

View File

@ -0,0 +1,3 @@
module.exports = {
...require('mwts/.prettierrc.json')
}

21
packages/cloud/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2013 - Now midwayjs
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.

10
packages/cloud/index.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
export * from './dist/index';
declare module '@midwayjs/core/dist/interface' {
interface MidwayConfig {
book?: PowerPartial<{
a: number;
b: string;
}>;
}
}

View File

@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['<rootDir>/test/fixtures'],
coveragePathIgnorePatterns: ['<rootDir>/test/'],
setupFilesAfterEnv: ['./jest.setup.js']
};

View File

@ -0,0 +1 @@
jest.setTimeout(30000);

View File

@ -0,0 +1,47 @@
{
"name": "@cool-midway/cloud",
"version": "6.0.0",
"description": "",
"main": "dist/index.js",
"typings": "index.d.ts",
"scripts": {
"build": "cross-env midway-bin build -c",
"test": "cross-env midway-bin test --ts",
"cov": "cross-env midway-bin cov --ts",
"lint": "mwts check",
"lint:fix": "mwts fix"
},
"keywords": [
"cool",
"cool-admin",
"cooljs"
],
"author": "COOL",
"files": [
"dist/**/*.js",
"dist/**/*.d.ts",
"index.d.ts"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://cool-js.com"
},
"devDependencies": {
"@cool-midway/core": "^6.0.0",
"@midwayjs/cli": "^2.0.0",
"@midwayjs/core": "^3.9.0",
"@midwayjs/decorator": "^3.9.0",
"@midwayjs/mock": "^3.9.0",
"@midwayjs/typeorm": "^3.9.0",
"@types/jest": "^29.2.4",
"@types/node": "^18.11.15",
"cross-env": "^7.0.3",
"jest": "^29.3.1",
"lodash": "^4.17.21",
"mwts": "^1.3.0",
"ts-jest": "^29.0.3",
"typeorm": "^0.3.11",
"typescript": "^4.9.4"
}
}

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2013 - Now midwayjs
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.

View File

@ -0,0 +1,4 @@
/**
* cool的配置
*/
export default {};

View File

@ -0,0 +1,22 @@
import { ILifeCycle, ILogger, IMidwayContainer, Logger } from '@midwayjs/core';
import { Configuration } from '@midwayjs/decorator';
import * as DefaultConfig from './config/config.default';
import { CoolCloudDb } from './db';
@Configuration({
namespace: 'cloud',
importConfigs: [
{
default: DefaultConfig,
},
],
})
export class CoolCloudConfiguration implements ILifeCycle {
@Logger()
coreLogger: ILogger;
async onReady(container: IMidwayContainer) {
await container.getAsync(CoolCloudDb);
this.coreLogger.info('\x1B[36m [cool:cloud] ready \x1B[0m');
}
}

View File

@ -0,0 +1,123 @@
import { CoolCommException } from '@cool-midway/core';
import { CoolDataSource } from './source';
import {
ALL,
Config,
ILogger,
Init,
Logger,
Provide,
Scope,
ScopeEnum,
} from '@midwayjs/core';
import { Repository } from 'typeorm';
import * as ts from 'typescript';
import * as _ from 'lodash';
/**
* 数据库
*/
@Provide()
@Scope(ScopeEnum.Singleton)
export class CoolCloudDb {
@Logger()
coreLogger: ILogger;
coolDataSource: CoolDataSource;
@Config(ALL)
config;
@Init()
async init() {
const config = this.config.typeorm.dataSource.default;
if (!config) {
throw new CoolCommException('未配置数据库default信息');
}
this.coolDataSource = new CoolDataSource({
...this.config.typeorm.dataSource.default,
entities: [],
});
// 连接数据库
await this.coolDataSource.initialize();
}
/**
* 获得数据库操作实例
* @param tableClass 表类
* @param appId 应用ID
* @returns
*/
getRepository(tableClass: string, appId = 'CLOUD'): Repository<any> {
return this.coolDataSource.getRepository(`${tableClass}${appId}`);
}
/**
* 创建表
* @param table 表结构,元函数,字符串
* @param appId 应用ID,确保每个应用的数据隔离
* @param synchronize 是否同步表结构
*/
async createTable(table: string, synchronize = false, appId = 'CLOUD') {
if (!table || !appId) {
throw new CoolCommException('table、appId不能为空');
}
const { newCode, className } = this.parseCode(table, appId);
const entities = this.coolDataSource.options.entities;
// @ts-ignore
this.coolDataSource.options.entities = _.dropWhile(entities, {
name: className,
});
const code = ts.transpile(
`${newCode}
this.coolDataSource.options.entities.push(${className})
this.coolDataSource.buildMetadatas().then(() => {
if(synchronize){
this.coolDataSource.synchronize();
}
});
`,
{
emitDecoratorMetadata: true,
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2018,
removeComments: true,
}
);
eval(code);
}
/**
* 根据字符串查找并生成一个跟appId相关的类名
* @param code 代码
* @param appId
*/
parseCode(code: string, appId = 'CLOUD') {
try {
const oldClassName = code
.match('class(.*)extends')[1]
.replace(/\s*/g, '');
const oldTableStart = code.indexOf('@Entity(');
const oldTableEnd = code.indexOf(')');
const oldTableName = code
.substring(oldTableStart + 9, oldTableEnd - 1)
.replace(/\s*/g, '')
// eslint-disable-next-line no-useless-escape
.replace(/\"/g, '')
// eslint-disable-next-line no-useless-escape
.replace(/\'/g, '');
const className = `${oldClassName}${appId}`;
return {
newCode: code
.replace(oldClassName, className)
.replace(oldTableName, `func_${oldTableName}`),
className,
tableName: `func_${oldTableName}`,
};
} catch (err) {
this.coreLogger.error(err);
throw new CoolCommException('代码结构不正确,请检查');
}
}
}

View File

@ -0,0 +1,10 @@
import { DataSource } from 'typeorm';
export class CoolDataSource extends DataSource {
/**
* 重新构造元数据
*/
async buildMetadatas() {
await super.buildMetadatas();
}
}

View File

@ -0,0 +1,525 @@
import { CloudReq } from './../interface';
import { IMidwayApplication } from '@midwayjs/core';
import {
CoolConfig,
CoolEventManager,
CoolValidateException,
CurdOption,
ERRINFO,
EVENT,
} from '@cool-midway/core';
import { Brackets, In, Repository, SelectQueryBuilder } from 'typeorm';
import { CoolCloudDb } from '../db';
import * as _ from 'lodash';
import * as SqlString from 'sqlstring';
export abstract class CloudCrud {
ctx;
curdOption: CurdOption;
coolCloudDb: CoolCloudDb;
coolConfig: CoolConfig;
entity: Repository<any>;
app: IMidwayApplication;
req: CloudReq;
coolEventManager: CoolEventManager;
protected sqlParams;
setCurdOption(curdOption: CurdOption) {
this.curdOption = curdOption;
}
/**
* 设置实体
* @param entityModel
*/
async setEntity() {
this.entity = this.coolCloudDb.getRepository(
this.curdOption.entity,
'CLOUD'
);
}
abstract main(req: CloudReq): Promise<void>;
async init(req: CloudReq) {
this.sqlParams = [];
// 执行主函数
await this.main(req);
// 操作之前
await this.before();
// // 设置实体
await this.setEntity();
}
/**
* 参数安全性检查
* @param params
*/
async paramSafetyCheck(params) {
const lp = params.toLowerCase();
return !(
lp.indexOf('update ') > -1 ||
lp.indexOf('select ') > -1 ||
lp.indexOf('delete ') > -1 ||
lp.indexOf('insert ') > -1
);
}
/**
* 非分页查询
* @param query 查询条件
* @param option 查询配置
*/
async list(query): Promise<any> {
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
const sql = await this.getOptionFind(query, this.curdOption.listQueryOp);
return this.nativeQuery(sql, []);
}
/**
* 执行SQL并获得分页数据
* @param sql 执行的sql语句
* @param query 分页查询条件
* @param autoSort 是否自动排序
*/
async sqlRenderPage(sql, query, autoSort = true) {
const {
size = this.coolConfig.crud.pageSize,
page = 1,
order = 'createTime',
sort = 'desc',
isExport = false,
maxExportLimit,
} = query;
if (order && sort && autoSort) {
if (!(await this.paramSafetyCheck(order + sort))) {
throw new CoolValidateException('非法传参~');
}
sql += ` ORDER BY ${SqlString.escapeId(order)} ${this.checkSort(sort)}`;
}
if (isExport && maxExportLimit > 0) {
this.sqlParams.push(parseInt(maxExportLimit));
sql += ' LIMIT ? ';
}
if (!isExport) {
this.sqlParams.push((page - 1) * size);
this.sqlParams.push(parseInt(size));
sql += ' LIMIT ?,? ';
}
let params = [];
params = params.concat(this.sqlParams);
const result = await this.nativeQuery(sql, params);
const countResult = await this.nativeQuery(this.getCountSql(sql), params);
return {
list: result,
pagination: {
page: parseInt(page),
size: parseInt(size),
total: parseInt(countResult[0] ? countResult[0].count : 0),
},
};
}
/**
* 分页查询
* @param connectionName 连接名
*/
async page(query) {
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
const sql = await this.getOptionFind(query, this.curdOption.pageQueryOp);
return this.sqlRenderPage(sql, query, false);
}
/**
* 获得查询个数的SQL
* @param sql
*/
getCountSql(sql) {
sql = sql
.replace(new RegExp('LIMIT', 'gm'), 'limit ')
.replace(new RegExp('\n', 'gm'), ' ');
if (sql.includes('limit')) {
const sqlArr = sql.split('limit ');
sqlArr.pop();
sql = sqlArr.join('limit ');
}
return `select count(*) as count from (${sql}) a`;
}
/**
* 操作entity获得分页数据,不用写sql
* @param find QueryBuilder
* @param query
* @param autoSort
* @param connectionName
*/
async entityRenderPage(
find: SelectQueryBuilder<any>,
query,
autoSort = true
) {
const {
size = this.coolConfig.crud.pageSize,
page = 1,
order = 'createTime',
sort = 'desc',
isExport = false,
maxExportLimit,
} = query;
const count = await find.getCount();
let dataFind: SelectQueryBuilder<any>;
if (isExport && maxExportLimit > 0) {
dataFind = find.limit(maxExportLimit);
} else {
dataFind = find.offset((page - 1) * size).limit(size);
}
if (autoSort) {
find.addOrderBy(order, sort.toUpperCase());
}
return {
list: await dataFind.getRawMany(),
pagination: {
page: parseInt(page),
size: parseInt(size),
total: count,
},
};
}
/**
* 检查排序
* @param sort 排序
* @returns
*/
private checkSort(sort) {
if (!['desc', 'asc'].includes(sort.toLowerCase())) {
throw new CoolValidateException('sort 非法传参~');
}
return sort;
}
/**
* 原生查询
* @param sql
* @param params
*/
async nativeQuery(sql, params?) {
if (_.isEmpty(params)) {
params = this.sqlParams;
}
let newParams = [];
newParams = newParams.concat(params);
this.sqlParams = [];
for (const param of newParams) {
SqlString.escape(param);
}
return await this.getOrmManager().query(sql, newParams || []);
}
/**
* 获得ORM管理
* @param connectionName 连接名称
*/
getOrmManager() {
return this.coolCloudDb.coolDataSource;
}
private async before() {
if (!this.curdOption?.before) {
return;
}
await this.curdOption.before(this.ctx, this.app);
}
/**
* 插入参数值
* @param curdOption 配置
*/
private async insertParam(param) {
if (!this.curdOption?.insertParam) {
return param;
}
return {
...param,
...(await this.curdOption.insertParam(this.ctx, this.app)),
};
}
/**
* 新增|修改|删除 之后的操作
* @param data 对应数据
*/
async modifyAfter(
data: any,
type: 'delete' | 'update' | 'add'
): Promise<void> {}
/**
* 新增|修改|删除 之前的操作
* @param data 对应数据
*/
async modifyBefore(
data: any,
type: 'delete' | 'update' | 'add'
): Promise<void> {}
/**
* 新增
* @param param
* @returns
*/
async add(param) {
param = await this.insertParam(param);
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
await this.modifyBefore(param, 'add');
await this.addOrUpdate(param);
await this.modifyAfter(param, 'add');
return {
id:
param instanceof Array
? param.map(e => {
return e.id ? e.id : e._id;
})
: param.id
? param.id
: param._id,
};
}
/**
* 新增|修改
* @param param 数据
*/
async addOrUpdate(param: any | any[]) {
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
delete param.createTime;
if (param.id) {
param.updateTime = new Date();
await this.entity.update(param.id, param);
} else {
param.createTime = new Date();
param.updateTime = new Date();
await this.entity.insert(param);
}
}
/**
* 删除
* @param ids 删除的ID集合 如:[1,2,3] 或者 1,2,3
*/
async delete(ids: any) {
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
await this.modifyBefore(ids, 'delete');
if (ids instanceof String) {
ids = ids.split(',');
}
if (this.coolConfig.crud?.softDelete) {
this.softDelete(ids);
}
await this.entity.delete(ids);
await this.modifyAfter(ids, 'delete');
}
/**
* 软删除
* @param ids 删除的ID数组
* @param entity 实体
*/
async softDelete(ids: string[], entity?: Repository<any>, userId?: string) {
const data = await this.entity.find({
where: {
id: In(ids),
},
});
if (_.isEmpty(data)) return;
const _entity = entity ? entity : this.entity;
const params = {
data,
ctx: this.ctx,
entity: _entity,
};
if (data.length > 0) {
this.coolEventManager.emit(EVENT.SOFT_DELETE, params);
}
}
/**
* 修改
* @param param 数据
*/
async update(param: any) {
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
await this.modifyBefore(param, 'update');
if (!param.id && !(param instanceof Array))
throw new CoolValidateException(ERRINFO.NOID);
await this.addOrUpdate(param);
await this.modifyAfter(param, 'update');
}
/**
* 获得单个ID
* @param id ID
*/
async info(id: any): Promise<any> {
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
if (!id) {
throw new CoolValidateException(ERRINFO.NOID);
}
const info = await this.entity.findBy({ id });
if (info && this.curdOption?.infoIgnoreProperty) {
for (const property of this.curdOption?.infoIgnoreProperty) {
delete info[property];
}
}
return info;
}
/**
* 构建查询配置
* @param query 前端查询
* @param option
*/
private async getOptionFind(query, option) {
let { order = 'createTime', sort = 'desc', keyWord = '' } = query;
const sqlArr = ['SELECT'];
const selects = ['a.*'];
const find = this.entity.createQueryBuilder('a');
if (option) {
if (typeof option == 'function') {
// @ts-ignore
option = await option(this.baseCtx, this.baseApp);
}
// 判断是否有关联查询,有的话取个别名
if (!_.isEmpty(option.join)) {
for (const item of option.join) {
selects.push(`${item.alias}.*`);
find[item.type || 'leftJoin'](
item.entity,
item.alias,
item.condition
);
}
}
// 默认条件
if (option.where) {
const wheres =
typeof option.where == 'function'
? await option.where(this.ctx, this.app)
: option.where;
if (!_.isEmpty(wheres)) {
for (const item of wheres) {
if (
item.length == 2 ||
(item.length == 3 &&
(item[2] || (item[2] === 0 && item[2] != '')))
) {
for (const key in item[1]) {
this.sqlParams.push(item[1][key]);
}
find.andWhere(item[0], item[1]);
}
}
}
}
// 附加排序
if (!_.isEmpty(option.addOrderBy)) {
for (const key in option.addOrderBy) {
if (order && order == key) {
sort = option.addOrderBy[key].toUpperCase();
}
find.addOrderBy(
SqlString.escapeId(key),
this.checkSort(option.addOrderBy[key].toUpperCase())
);
}
}
// 关键字模糊搜索
if (keyWord || (keyWord == 0 && keyWord != '')) {
keyWord = `%${keyWord}%`;
find.andWhere(
new Brackets(qb => {
const keyWordLikeFields = option.keyWordLikeFields;
for (let i = 0; i < option.keyWordLikeFields?.length || 0; i++) {
qb.orWhere(`${keyWordLikeFields[i]} like :keyWord`, {
keyWord,
});
this.sqlParams.push(keyWord);
}
})
);
}
// 筛选字段
if (!_.isEmpty(option.select)) {
sqlArr.push(option.select.join(','));
find.select(option.select);
} else {
sqlArr.push(selects.join(','));
}
// 字段全匹配
if (!_.isEmpty(option.fieldEq)) {
for (const key of option.fieldEq) {
const c = {};
// 单表字段无别名的情况下操作
if (typeof key === 'string') {
if (query[key] || (query[key] == 0 && query[key] == '')) {
c[key] = query[key];
const eq = query[key] instanceof Array ? 'in' : '=';
if (eq === 'in') {
find.andWhere(`${key} ${eq} (:${key})`, c);
} else {
find.andWhere(`${key} ${eq} :${key}`, c);
}
this.sqlParams.push(query[key]);
}
} else {
if (
query[key.requestParam] ||
(query[key.requestParam] == 0 && query[key.requestParam] !== '')
) {
c[key.column] = query[key.requestParam];
const eq = query[key.requestParam] instanceof Array ? 'in' : '=';
if (eq === 'in') {
find.andWhere(`${key.column} ${eq} (:${key.column})`, c);
} else {
find.andWhere(`${key.column} ${eq} :${key.column}`, c);
}
this.sqlParams.push(query[key.requestParam]);
}
}
}
}
} else {
sqlArr.push(selects.join(','));
}
// 接口请求的排序
if (sort && order) {
const sorts = sort.toUpperCase().split(',');
const orders = order.split(',');
if (sorts.length != orders.length) {
throw new CoolValidateException(ERRINFO.SORTFIELD);
}
for (const i in sorts) {
find.addOrderBy(
SqlString.escapeId(orders[i]),
this.checkSort(sorts[i])
);
}
}
if (option?.extend) {
await option?.extend(find, this.ctx, this.app);
}
const sqls = find.getSql().split('FROM');
sqlArr.push('FROM');
sqlArr.push(sqls[1]);
return sqlArr.join(' ');
}
}

View File

@ -0,0 +1,17 @@
import { Provide, Scope, ScopeEnum } from '@midwayjs/core';
/**
* 云函数
*/
@Provide()
@Scope(ScopeEnum.Singleton)
export class CoolCloudFunc {
/**
* 获得类名
* @param code
* @returns
*/
getClassName(code: string) {
return code.match('class(.*)extends')[1].replace(/\s*/g, '');
}
}

View File

@ -0,0 +1,10 @@
export { CoolCloudConfiguration as Configuration } from './configuration';
export * from './interface';
// 云数据库
export * from './db/index';
// 云函数
export * from './func/index';
export * from './func/crud';

View File

@ -0,0 +1,11 @@
/**
* 云函数请求
*/
export interface CloudReq {
// 云函数名称
name: string;
// 请求参数
params: any;
// 调用方法
method: string;
}

View File

@ -0,0 +1,42 @@
{
"name": "@cool-midway/cloud",
"version": "6.0.0",
"description": "",
"main": "index.js",
"typings": "index.d.ts",
"scripts": {
"build": "cross-env midway-bin build -c",
"test": "cross-env midway-bin test --ts",
"cov": "cross-env midway-bin cov --ts",
"lint": "mwts check",
"lint:fix": "mwts fix"
},
"keywords": ["cool","cool-admin","cooljs"],
"author": "COOL",
"files": [
"**/*.js",
"**/*.d.ts",
"index.d.ts"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://cool-js.com"
},
"devDependencies": {
"@cool-midway/core": "^6.0.0",
"@midwayjs/cli": "^2.0.0",
"@midwayjs/core": "^3.0.0",
"@midwayjs/decorator": "^3.0.0",
"@midwayjs/mock": "^3.0.0",
"@midwayjs/typeorm": "^3.8.3",
"@types/jest": "^29.2.0",
"@types/node": "^16.11.22",
"cross-env": "^6.0.0",
"jest": "^29.2.2",
"mwts": "^1.0.5",
"ts-jest": "^29.0.3",
"typeorm": "^0.3.11",
"typescript": "^4.9.4"
}
}

View File

@ -0,0 +1 @@
export class CoolCloudUtil {}

View File

@ -0,0 +1,14 @@
import { createLightApp } from '@midwayjs/mock';
import * as custom from '../src';
describe('/test/index.test.ts', () => {
it('test component', async () => {
const app = await createLightApp('', {
imports: [
custom
]
});
const bookService = await app.getApplicationContext().getAsync(custom.BookService);
expect(await bookService.getBookById()).toEqual('hello world');
});
});

View File

@ -0,0 +1,25 @@
{
"compileOnSave": true,
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"moduleResolution": "node",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"inlineSourceMap":false,
"noImplicitThis": true,
"noUnusedLocals": true,
"stripInternal": true,
"skipLibCheck": true,
"noImplicitReturns": false,
"pretty": true,
"declaration": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist"
},
"exclude": [
"dist",
"node_modules",
"test"
]
}

21
packages/core/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2013 - Now midwayjs
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.

3
packages/core/README.md Normal file
View File

@ -0,0 +1,3 @@
# cool-admin
https://cool-js.com

View File

@ -0,0 +1,11 @@
# 🎨 editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

View File

@ -0,0 +1,7 @@
{
"extends": "./node_modules/mwts/",
"ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings"],
"env": {
"jest": true
}
}

15
packages/core/_.gitignore Normal file
View File

@ -0,0 +1,15 @@
logs/
npm-debug.log
yarn-error.log
node_modules/
package-lock.json
yarn.lock
coverage/
dist/
.idea/
run/
.DS_Store
*.sw*
*.un~
.tsbuildinfo
.tsbuildinfo.*

View File

@ -0,0 +1,3 @@
module.exports = {
...require('mwts/.prettierrc.json')
}

10
packages/core/index.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
export * from './dist/index';
declare module '@midwayjs/core/dist/interface' {
interface MidwayConfig {
book?: PowerPartial<{
a: number;
b: string;
}>;
}
}

View File

@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['<rootDir>/test/fixtures'],
coveragePathIgnorePatterns: ['<rootDir>/test/'],
setupFilesAfterEnv: ['./jest.setup.js']
};

View File

@ -0,0 +1 @@
jest.setTimeout(30000);

View File

@ -0,0 +1,56 @@
{
"name": "@cool-midway/core",
"version": "6.0.2",
"description": "",
"main": "dist/index.js",
"typings": "index.d.ts",
"scripts": {
"build": "cross-env midway-bin build -c",
"test": "cross-env midway-bin test --ts",
"cov": "cross-env midway-bin cov --ts",
"lint": "mwts check",
"lint:fix": "mwts fix"
},
"keywords": [
"cool",
"cool-admin",
"cooljs"
],
"author": "COOL",
"files": [
"dist/**/*.js",
"dist/**/*.d.ts",
"index.d.ts"
],
"readme": "README.md",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://cool-js.com"
},
"devDependencies": {
"@midwayjs/cli": "1.3.21",
"@midwayjs/core": "^3.9.0",
"@midwayjs/decorator": "^3.9.0",
"@midwayjs/koa": "^3.9.0",
"@midwayjs/mock": "^3.9.0",
"@midwayjs/typeorm": "^3.9.0",
"@types/jest": "^29.2.4",
"@types/node": "^18.11.15",
"aedes": "^0.48.1",
"cross-env": "^7.0.3",
"jest": "^29.3.1",
"mwts": "^1.3.0",
"ts-jest": "^29.0.3",
"typeorm": "^0.3.11",
"typescript": "~4.9.4"
},
"dependencies": {
"@midwayjs/cache": "^3.9.0",
"lodash": "^4.17.21",
"md5": "^2.3.0",
"moment": "^2.29.4",
"mysql2-import": "^5.0.22",
"sqlstring": "^2.3.3"
}
}

21
packages/core/src/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2013 - Now midwayjs
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.

View File

@ -0,0 +1,18 @@
import { CoolConfig } from "../interface";
/**
* cool的配置
*/
export default {
cool: {
// 是否自动导入数据库
initDB: false,
// crud配置
crud: {
// 软删除
softDelete: true,
// 分页查询每页条数
pageSize: 15,
},
} as CoolConfig,
};

View File

@ -0,0 +1,82 @@
import {
App,
Context,
ILifeCycle,
ILogger,
IMidwayBaseApplication,
IMidwayContainer,
Inject,
Logger,
} from "@midwayjs/core";
import { Configuration } from "@midwayjs/decorator";
import * as DefaultConfig from "./config/config.default";
import { CoolExceptionFilter } from "./exception/filter";
import { FuncUtil } from "./util/func";
import location from "./util/location";
import * as koa from "@midwayjs/koa";
import { CoolModuleConfig } from "./module/config";
import { CoolModuleImport } from "./module/import";
import { CoolEventManager } from "./event";
import { CoolEps } from "./rest/eps";
import { CacheManager } from "@midwayjs/cache";
import * as cache from "@midwayjs/cache";
import { CoolDecorator } from "./decorator";
@Configuration({
namespace: "cool",
imports: [cache],
importConfigs: [
{
default: DefaultConfig,
},
],
})
export class CoolConfiguration implements ILifeCycle {
@Logger()
coreLogger: ILogger;
@App()
app: koa.Application;
@Inject()
coolEventManager: CoolEventManager;
async onReady(container: IMidwayContainer) {
this.coolEventManager.emit("onReady");
// 处理模块配置
await container.getAsync(CoolModuleConfig);
// 导入模块数据
await container.getAsync(CoolModuleImport);
// 常用函数处理
await container.getAsync(FuncUtil);
// 事件
await container.getAsync(CoolEventManager);
// 异常处理
this.app.useFilter([CoolExceptionFilter]);
// 装饰器
await container.getAsync(CoolDecorator);
if (this.app.getEnv() == "local") {
// 实体与路径
const eps: CoolEps = await container.getAsync(CoolEps);
eps.init();
}
// 缓存设置为全局
global["COOL-CACHE"] = await container.getAsync(CacheManager);
// 清除 location
setTimeout(() => {
location.clean();
this.coreLogger.info("\x1B[36m [cool:core] location clean \x1B[0m");
}, 10000);
}
async onConfigLoad(
container: IMidwayContainer,
mainApp?: IMidwayBaseApplication<Context>
) {}
async onServerReady() {
this.coolEventManager.emit("onServerReady");
location.clean();
}
}

View File

@ -0,0 +1,50 @@
/**
* 返回码
*/
export enum RESCODE {
// 成功
SUCCESS = 1000,
// 失败
COMMFAIL = 1001,
// 参数验证失败
VALIDATEFAIL = 1002,
// 参数验证失败
COREFAIL = 1003,
}
/**
* 返回信息
*/
export enum RESMESSAGE {
// 成功
SUCCESS = "success",
// 失败
COMMFAIL = "comm fail",
// 参数验证失败
VALIDATEFAIL = "validate fail",
// 核心异常
COREFAIL = "core fail",
}
/**
* 错误提示
*/
export enum ERRINFO {
NOENTITY = "未设置操作实体",
NOID = "查询参数[id]不存在",
SORTFIELD = "排序参数不正确",
}
/**
* 事件
*/
export enum EVENT {
// 软删除
SOFT_DELETE = "onSoftDelete",
// 服务成功启动
SERVER_READY = "onServerReady",
// 服务就绪
READY = "onReady",
// ES 数据改变
ES_DATA_CHANGE = "esDataChange",
}

View File

@ -0,0 +1,201 @@
import {
App,
CONTROLLER_KEY,
getClassMetadata,
Init,
Inject,
Provide,
} from "@midwayjs/decorator";
import { RESCODE, RESMESSAGE } from "../constant/global";
import { ControllerOption, CurdOption } from "../decorator/controller";
import { BaseService } from "../service/base";
import { IMidwayApplication } from "@midwayjs/core";
import { Context } from "@midwayjs/koa";
import { TypeORMDataSourceManager } from "@midwayjs/typeorm";
/**
* 控制器基类
*/
@Provide()
export abstract class BaseController {
@Inject("ctx")
baseCtx: Context;
@Inject()
service: BaseService;
@App()
baseApp: IMidwayApplication;
curdOption: CurdOption;
@Inject()
typeORMDataSourceManager: TypeORMDataSourceManager;
connectionName;
@Init()
async init() {
const option: ControllerOption = getClassMetadata(CONTROLLER_KEY, this);
const curdOption: CurdOption = option.curdOption;
this.curdOption = curdOption;
if (!this.curdOption) {
return;
}
// 操作之前
await this.before(curdOption);
// 设置service
await this.setService(curdOption);
// 设置实体
await this.setEntity(curdOption);
}
private async before(curdOption: CurdOption) {
if (!curdOption?.before) {
return;
}
await curdOption.before(this.baseCtx, this.baseApp);
}
/**
* 插入参数值
* @param curdOption 配置
*/
private async insertParam(curdOption: CurdOption) {
if (!curdOption?.insertParam) {
return;
}
this.baseCtx.request.body = {
...this.baseCtx.request.body,
...(await curdOption.insertParam(this.baseCtx, this.baseApp)),
};
}
/**
* 设置实体
* @param curdOption 配置
*/
private async setEntity(curdOption: CurdOption) {
const entity = curdOption?.entity;
if (entity) {
const dataSourceName =
this.typeORMDataSourceManager.getDataSourceNameByModel(entity);
let entityModel = this.typeORMDataSourceManager
.getDataSource(dataSourceName)
.getRepository(entity);
this.service.setEntity(entityModel);
}
}
/**
* 设置service
* @param curdOption
*/
private async setService(curdOption: CurdOption) {
if (curdOption.service) {
this.service = await this.baseCtx.requestContext.getAsync(
curdOption.service
);
}
}
/**
* 新增
* @returns
*/
async add() {
// 插入参数
await this.insertParam(this.curdOption);
const { body } = this.baseCtx.request;
return this.ok(await this.service.add(body));
}
/**
* 删除
* @returns
*/
async delete() {
const { ids } = this.baseCtx.request.body;
return this.ok(await this.service.delete(ids));
}
/**
* 更新
* @returns
*/
async update() {
const { body } = this.baseCtx.request;
return this.ok(await this.service.update(body));
}
/**
* 分页查询
* @returns
*/
async page() {
const { body } = this.baseCtx.request;
return this.ok(
await this.service.page(
body,
this.curdOption.pageQueryOp,
this.connectionName
)
);
}
/**
* 列表查询
* @returns
*/
async list() {
const { body } = this.baseCtx.request;
return this.ok(
await this.service.list(
body,
this.curdOption.listQueryOp,
this.connectionName
)
);
}
/**
* 根据ID查询信息
* @returns
*/
async info() {
const { id } = this.baseCtx.query;
return this.ok(
await this.service.info(id, this.curdOption.infoIgnoreProperty)
);
}
/**
* 成功返回
* @param data 返回数据
*/
ok(data?: any) {
const res = {
code: RESCODE.SUCCESS,
message: RESMESSAGE.SUCCESS,
};
if (data || data == 0) {
res["data"] = data;
}
return res;
}
/**
* 失败返回
* @param message
*/
fail(message?: string, code?: RESCODE) {
return {
code: code ? code : RESCODE.COMMFAIL,
message: message
? message
: code == RESCODE.VALIDATEFAIL
? RESMESSAGE.VALIDATEFAIL
: RESMESSAGE.COMMFAIL,
};
}
}

View File

@ -0,0 +1,8 @@
import { createCustomMethodDecorator } from "@midwayjs/decorator";
// 装饰器内部的唯一 id
export const COOL_CACHE = "decorator:cool_cache";
export function CoolCache(ttl?: number): MethodDecorator {
return createCustomMethodDecorator(COOL_CACHE, ttl);
}

View File

@ -0,0 +1,208 @@
import { ModuleConfig } from "./../interface";
import {
Scope,
ScopeEnum,
saveClassMetadata,
saveModule,
CONTROLLER_KEY,
MiddlewareParamArray,
WEB_ROUTER_KEY,
attachClassMetadata,
} from "@midwayjs/decorator";
import * as fs from "fs";
import * as _ from "lodash";
import * as os from "os";
import location from "../util/location";
export type ApiTypes = "add" | "delete" | "update" | "page" | "info" | "list";
// Crud配置
export interface CurdOption {
// 路由前缀,不配置默认是按Controller下的文件夹路径
prefix?: string;
// curd api接口
api: ApiTypes[];
// 分页查询配置
pageQueryOp?: QueryOp | Function;
// 非分页查询配置
listQueryOp?: QueryOp | Function;
// 插入参数
insertParam?: Function;
// 操作之前
before?: Function;
// info 忽略返回属性
infoIgnoreProperty?: string[];
// 实体
entity: any;
// 服务
service?: any;
// api标签
urlTag?: {
name: "ignoreToken" | string;
url: ApiTypes[];
};
}
export interface JoinOp {
// 实体
entity: any;
// 别名
alias: string;
// 关联条件
condition: string;
// 关联类型
type?: "innerJoin" | "leftJoin";
}
// 字段匹配
export interface FieldEq {
// 字段
column: string;
// 请求参数
requestParam: string;
}
// 查询配置
export interface QueryOp {
// 需要模糊查询的字段
keyWordLikeFields?: string[];
// 查询条件
where?: Function;
// 查询字段
select?: string[];
// 字段相等
fieldEq?: string[] | FieldEq[];
// 添加排序条件
addOrderBy?: {};
// 关联配置
join?: JoinOp[];
// 其他条件
extend?: Function;
}
// Controller 配置
export interface ControllerOption {
// crud配置 如果是字符串则为路由前缀,不配置默认是按Controller下的文件夹路径
curdOption?: CurdOption & string;
// 路由配置
routerOptions?: {
// 是否敏感
sensitive?: boolean;
// 路由中间件
middleware?: MiddlewareParamArray;
// 别名
alias?: string[];
// 描述
description?: string;
// 标签名称
tagName?: string;
};
}
// COOL的装饰器
export function CoolController(
curdOption?: CurdOption | string,
routerOptions: {
sensitive?: boolean;
middleware?: MiddlewareParamArray;
description?: string;
tagName?: string;
ignoreGlobalPrefix?: boolean;
} = { middleware: [], sensitive: true }
): ClassDecorator {
return (target: any) => {
// 将装饰的类,绑定到该装饰器,用于后续能获取到 class
saveModule(CONTROLLER_KEY, target);
let prefix;
if (typeof curdOption === "string") {
prefix = curdOption;
} else {
prefix = curdOption?.prefix || "";
}
// 如果不存在路由前缀,那么自动根据当前文件夹路径
location.scriptPath(target).then(async (res: any) => {
const pathSps = res.path.split(".");
const paths = pathSps[pathSps.length - 2].split("/");
const pathArr = [];
let module = null;
for (const path of paths.reverse()) {
if (path != "controller" && !module) {
pathArr.push(path);
}
if (path == "controller" && !paths.includes("modules")) {
break;
}
if (path == "controller" && paths.includes("modules")) {
module = "ready";
}
if (module && path != "controller") {
module = `${path}`;
break;
}
}
if (module) {
pathArr.reverse();
pathArr.splice(1, 0, module);
// 追加模块中间件
let path = `${
res.path.split(`modules/${module}`)[0]
}modules/${module}/config.${_.endsWith(res.path, "ts") ? "ts" : "js"}`;
if (os.type() == "Windows_NT") {
path = path.substr(1);
}
if (fs.existsSync(path)) {
const config: ModuleConfig = require(path).default();
routerOptions.middleware = (config.middlewares || []).concat(
routerOptions.middleware || []
);
}
}
if (!prefix) {
prefix = `/${pathArr.join("/")}`;
}
saveMetadata(prefix, routerOptions, target, curdOption, module);
});
};
}
export const apiDesc = {
add: "新增",
delete: "删除",
update: "修改",
page: "分页查询",
list: "列表查询",
info: "单个信息",
};
// 保存一些元数据信息,任意你希望存的东西
function saveMetadata(prefix, routerOptions, target, curdOption, module) {
if (module && !routerOptions.tagName) {
routerOptions = routerOptions || {};
routerOptions.tagName = module;
}
saveClassMetadata(
CONTROLLER_KEY,
{
prefix,
routerOptions,
curdOption,
module,
} as ControllerOption,
target
);
// 追加CRUD路由
if (!_.isEmpty(curdOption?.api)) {
curdOption?.api.forEach((path) => {
attachClassMetadata(
WEB_ROUTER_KEY,
{
path: `/${path}`,
requestMethod: path == "info" ? "get" : "post",
method: path,
summary: apiDesc[path],
description: "",
},
target
);
});
Scope(ScopeEnum.Request)(target);
}
}

View File

@ -0,0 +1,42 @@
import {
Scope,
ScopeEnum,
saveClassMetadata,
saveModule,
attachClassMetadata,
} from "@midwayjs/decorator";
export const COOL_CLS_EVENT_KEY = "decorator:cool:cls:event";
export function CoolEvent(): ClassDecorator {
return (target: any) => {
// 将装饰的类,绑定到该装饰器,用于后续能获取到 class
saveModule(COOL_CLS_EVENT_KEY, target);
// 保存一些元数据信息,任意你希望存的东西
saveClassMetadata(COOL_CLS_EVENT_KEY, {}, target);
// 指定 IoC 容器创建实例的作用域,这里注册为请求作用域,这样能取到 ctx
Scope(ScopeEnum.Singleton)(target);
};
}
export const COOL_EVENT_KEY = "decorator:cool:event";
/**
* 事件
* @param eventName
* @returns
*/
export function Event(eventName?: string): MethodDecorator {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
// 将装饰的类,绑定到该装饰器,用于后续能获取到 class
attachClassMetadata(
COOL_EVENT_KEY,
{
eventName,
propertyKey,
descriptor,
},
target
);
};
}

View File

@ -0,0 +1,103 @@
import { COOL_CACHE } from "./cache";
import { CacheManager } from "@midwayjs/cache";
import {
Init,
Inject,
JoinPoint,
MidwayDecoratorService,
Provide,
Scope,
ScopeEnum,
} from "@midwayjs/core";
import { TypeORMDataSourceManager } from "@midwayjs/typeorm";
import { CoolCommException } from "../exception/comm";
import { COOL_TRANSACTION, TransactionOptions } from "./transaction";
import * as md5 from "md5";
/**
* 装饰器
*/
@Provide()
@Scope(ScopeEnum.Singleton)
export class CoolDecorator {
@Inject()
typeORMDataSourceManager: TypeORMDataSourceManager;
@Inject()
decoratorService: MidwayDecoratorService;
@Inject()
cacheManager: CacheManager;
@Init()
async init() {
// 事务
await this.transaction();
// 缓存
await this.cache();
}
/**
* 缓存
*/
async cache() {
this.decoratorService.registerMethodHandler(COOL_CACHE, (options) => {
return {
around: async (joinPoint: JoinPoint) => {
const key = md5(
joinPoint.target.constructor.name +
joinPoint.methodName +
JSON.stringify(joinPoint.args)
);
// 缓存有数据就返回
let data: any = await this.cacheManager.get(key);
if (data) {
return JSON.parse(data);
} else {
// 执行原始方法
data = await joinPoint.proceed(...joinPoint.args);
await this.cacheManager.set(key, JSON.stringify(data), {
ttl: options.metadata.ttl,
});
}
return data;
},
};
});
}
/**
* 事务
*/
async transaction() {
this.decoratorService.registerMethodHandler(COOL_TRANSACTION, (options) => {
return {
around: async (joinPoint: JoinPoint) => {
const option: TransactionOptions = options.metadata;
const dataSource = this.typeORMDataSourceManager.getDataSource(
option?.connectionName || "default"
);
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
if (option && option.isolation) {
await queryRunner.startTransaction(option.isolation);
} else {
await queryRunner.startTransaction();
}
let data;
try {
joinPoint.args.push(queryRunner);
data = await joinPoint.proceed(...joinPoint.args);
await queryRunner.commitTransaction();
} catch (error) {
await queryRunner.rollbackTransaction();
throw new CoolCommException(error.message);
} finally {
await queryRunner.release();
}
return data;
},
};
});
}
}

View File

@ -0,0 +1,27 @@
import { saveClassMetadata, saveModule } from "@midwayjs/decorator";
export const COOL_URL_TAG_KEY = "decorator:cool:url:tag";
export enum TagTypes {
IGNORE_TOKEN = "ignoreToken",
IGNORE_SIGN = "ignoreSign",
}
export interface CoolUrlTagConfig {
key: TagTypes | string;
value: string[];
}
/**
* 打标记
* @param data
* @returns
*/
export function CoolUrlTag(data: CoolUrlTagConfig): ClassDecorator {
return (target: any) => {
// 将装饰的类,绑定到该装饰器,用于后续能获取到 class
saveModule(COOL_URL_TAG_KEY, target);
// 保存一些元数据信息,任意你希望存的东西
saveClassMetadata(COOL_URL_TAG_KEY, data, target);
};
}

View File

@ -0,0 +1,19 @@
import { createCustomMethodDecorator } from "@midwayjs/decorator";
type IsolationLevel =
| "READ UNCOMMITTED"
| "READ COMMITTED"
| "REPEATABLE READ"
| "SERIALIZABLE";
export interface TransactionOptions {
connectionName?: string;
isolation?: IsolationLevel;
}
// 装饰器内部的唯一 id
export const COOL_TRANSACTION = "decorator:cool_transaction";
export function CoolTransaction(option?: TransactionOptions): MethodDecorator {
return createCustomMethodDecorator(COOL_TRANSACTION, option);
}

View File

@ -0,0 +1,27 @@
import {
Index,
UpdateDateColumn,
CreateDateColumn,
PrimaryGeneratedColumn,
} from "typeorm";
import { CoolBaseEntity } from "./typeorm";
/**
* 模型基类
*/
export abstract class BaseEntity extends CoolBaseEntity {
// 默认自增
@PrimaryGeneratedColumn("increment", {
comment: "ID",
// type: "bigint",
})
id: number;
@Index()
@CreateDateColumn({ comment: "创建时间" })
createTime: Date;
@Index()
@UpdateDateColumn({ comment: "更新时间" })
updateTime: Date;
}

View File

@ -0,0 +1,24 @@
import {
Index,
UpdateDateColumn,
CreateDateColumn,
ObjectID,
ObjectIdColumn,
} from "typeorm";
import { CoolBaseEntity } from "./typeorm";
/**
* 模型基类
*/
export abstract class BaseMongoEntity extends CoolBaseEntity {
@ObjectIdColumn({ comment: "id" })
id: ObjectID;
@Index()
@CreateDateColumn({ comment: "创建时间" })
createTime: Date;
@Index()
@UpdateDateColumn({ comment: "更新时间" })
updateTime: Date;
}

View File

@ -0,0 +1,3 @@
import { BaseEntity } from "typeorm";
export abstract class CoolBaseEntity extends BaseEntity {}

View File

@ -0,0 +1,43 @@
import {
App,
getClassMetadata,
Init,
listModule,
Provide,
Scope,
ScopeEnum,
} from "@midwayjs/decorator";
import * as Events from "events";
import { IMidwayApplication } from "@midwayjs/core";
import { COOL_CLS_EVENT_KEY, COOL_EVENT_KEY } from "../decorator/event";
/**
* 事件
*/
@Provide()
@Scope(ScopeEnum.Singleton)
export class CoolEventManager extends Events {
@App()
app: IMidwayApplication;
@Init()
async init() {
const eventModules = listModule(COOL_CLS_EVENT_KEY);
for (const module of eventModules) {
this.handlerEvent(module);
}
}
async handlerEvent(module) {
const events = getClassMetadata(COOL_EVENT_KEY, module);
for (const event of events) {
const method = event.eventName ? event.eventName : event.propertyKey;
this.on(method, async (...args) => {
const moduleInstance = await this.app
.getApplicationContext()
.getAsync(module);
moduleInstance[event.propertyKey](...args);
});
}
}
}

View File

@ -0,0 +1,13 @@
/**
* 异常基类
*/
export class BaseException extends Error {
status: number;
constructor(name: string, code: number, message: string) {
super(message);
this.name = name;
this.status = code;
}
}

View File

@ -0,0 +1,15 @@
import { RESCODE, RESMESSAGE } from '../constant/global';
import { BaseException } from './base';
/**
* 通用异常
*/
export class CoolCommException extends BaseException {
constructor(message: string) {
super(
'CoolCommException',
RESCODE.COMMFAIL,
message ? message : RESMESSAGE.COMMFAIL
);
}
}

View File

@ -0,0 +1,15 @@
import { RESCODE, RESMESSAGE } from '../constant/global';
import { BaseException } from './base';
/**
* 核心异常
*/
export class CoolCoreException extends BaseException {
constructor(message: string) {
super(
'CoolCoreException',
RESCODE.COREFAIL,
message ? message : RESMESSAGE.COREFAIL
);
}
}

View File

@ -0,0 +1,20 @@
import { RESCODE } from './../constant/global';
import { ILogger } from '@midwayjs/core';
import { Catch, Logger } from '@midwayjs/decorator';
/**
* 全局异常处理
*/
@Catch()
export class CoolExceptionFilter {
@Logger()
coreLogger: ILogger;
async catch(err) {
this.coreLogger.error(err);
return {
code: err.status || RESCODE.COMMFAIL,
message: err.message,
};
}
}

View File

@ -0,0 +1,15 @@
import { RESCODE, RESMESSAGE } from '../constant/global';
import { BaseException } from './base';
/**
* 校验异常
*/
export class CoolValidateException extends BaseException {
constructor(message: string) {
super(
'CoolValidateException',
RESCODE.VALIDATEFAIL,
message ? message : RESMESSAGE.VALIDATEFAIL
);
}
}

View File

@ -0,0 +1,45 @@
export { CoolConfiguration as Configuration } from "./configuration";
// 异常处理
export * from "./exception/filter";
export * from "./exception/core";
export * from "./exception/base";
export * from "./exception/comm";
export * from "./exception/validate";
// entity
export * from "./entity/base";
export * from "./entity/typeorm";
export * from "./entity/mongo";
// service
export * from "./service/base";
// controller
export * from "./controller/base";
// 事件
export * from "./event/index";
// 装饰器
export * from "./decorator/controller";
export * from "./decorator/cache";
export * from "./decorator/event";
export * from "./decorator/transaction";
export * from "./decorator/tag";
export * from "./decorator/index";
// rest
export * from "./rest/eps";
// tag
export * from "./tag/data";
// 模块
export * from "./module/config";
export * from "./module/import";
// 其他
export * from "./interface";
export * from "./util/func";
export * from "./constant/global";

View File

@ -0,0 +1,300 @@
import { MiddlewareParamArray } from "@midwayjs/core";
import { AedesOptions } from "aedes";
import { PublishPacket } from "packet";
/**
* 模块配置
*/
export interface ModuleConfig {
/** 名称 */
name: string;
/** 描述 */
description: string;
/** 模块中间件 */
middlewares?: MiddlewareParamArray;
/** 全局中间件 */
globalMiddlewares?: MiddlewareParamArray;
/** 模块加载顺序,默认为0,值越大越优先加载 */
order?: number;
}
export interface CoolConfig {
/** 是否自动导入数据库 */
initDB?: boolean;
// 实体配置
// entity?: {
// primaryType: "uuid" | "increment" | "rowid" | "identity";
// };
/** crud配置 */
crud?: {
/** 软删除 */
softDelete: boolean;
/** 分页查询每页条数 */
pageSize: number;
// 多租户
// tenant: boolean;
};
/** elasticsearch配置 */
es?: {
nodes: string[];
};
/** pay */
pay?: {
/** 微信支付 */
wx?: CoolWxPayConfig;
/** 支付宝支付 */
ali?: CoolAliPayConfig;
};
/** rpc */
rpc?: CoolRpcConfig;
/** redis */
redis?: RedisConfig | RedisConfig[];
/** 文件上传 */
file?: {
/** 上传模式 */
mode: MODETYPE;
/** 本地上传 文件地址前缀 */
domain?: string;
/** oss */
oss?: OSSConfig;
/** cos */
cos?: COSConfig;
/** qiniu */
qiniu?: QINIUConfig;
};
/** IOT 配置 */
iot: CoolIotConfig;
}
export interface CoolRpcConfig {
/** 服务名称 */
name: string;
/** redis */
redis: RedisConfig & RedisConfig[] & unknown;
}
export interface RedisConfig {
/** host */
host: string;
/** password */
password: string;
/** port */
port: number;
/** db */
db: number;
}
// 模式
export enum MODETYPE {
/** 本地 */
LOCAL = "local",
/** 云存储 */
CLOUD = "cloud",
/** 其他 */
OTHER = "other",
}
export enum CLOUDTYPE {
/** 阿里云存储 */
OSS = "oss",
/** 腾讯云存储 */
COS = "cos",
/** 七牛云存储 */
QINIU = "qiniu",
}
/**
* 上传模式
*/
export interface Mode {
/** 模式 */
mode: MODETYPE;
/** 类型 */
type: string;
}
/**
* 模块配置
*/
export interface CoolFileConfig {
/** 上传模式 */
mode: MODETYPE;
/** 阿里云oss 配置 */
oss: OSSConfig;
/** 腾讯云 cos配置 */
cos: COSConfig;
/** 七牛云 配置 */
qiniu: QINIUConfig;
/** 文件前缀 */
domain: string;
}
/**
* OSS 配置
*/
export interface OSSConfig {
/** 阿里云accessKeyId */
accessKeyId: string;
/** 阿里云accessKeySecret */
accessKeySecret: string;
/** 阿里云oss的bucket */
bucket: string;
/** 阿里云oss的endpoint */
endpoint: string;
/** 阿里云oss的timeout */
timeout: string;
/** 签名失效时间,毫秒 */
expAfter?: number;
/** 文件最大的 size */
maxSize?: number;
}
/**
* COS 配置
*/
export interface COSConfig {
/** 腾讯云accessKeyId */
accessKeyId: string;
/** 腾讯云accessKeySecret */
accessKeySecret: string;
/** 腾讯云cos的bucket */
bucket: string;
/** 腾讯云cos的区域 */
region: string;
/** 腾讯云cos的公网访问地址 */
publicDomain: string;
/** 上传持续时间 */
durationSeconds?: number;
/** 允许操作(上传)的对象前缀 */
allowPrefix?: string;
/** 密钥的权限列表 */
allowActions?: string[];
}
export interface QINIUConfig {
/** 七牛云accessKeyId */
accessKeyId: string;
/** 七牛云accessKeySecret */
accessKeySecret: string;
/** 七牛云cos的bucket */
bucket: string;
/** 七牛云cos的区域 */
region: string;
/** 七牛云cos的公网访问地址 */
publicDomain: string;
/** 上传地址 */
uploadUrl?: string;
/** 上传fileKey */
fileKey?: string;
}
/**
* 微信支付配置
*/
export interface CoolWxPayConfig {
/** 直连商户申请的公众号或移动应用appid。 */
appid: string;
/** 商户号 */
mchid: string;
/** 可选参数 证书序列号 */
serial_no?: string;
/** 回调链接 */
notify_url: string;
/** 公钥 */
publicKey: Buffer;
/** 私钥 */
privateKey: Buffer;
/** 可选参数 认证类型,目前为WECHATPAY2-SHA256-RSA2048 */
authType?: string;
/** 可选参数 User-Agent */
userAgent?: string;
/** 可选参数 APIv3密钥 */
key?: string;
}
/**
* 支付宝支付配置
*/
export interface CoolAliPayConfig {
/** 支付回调地址 */
notifyUrl: string;
/** 应用ID */
appId: string;
/**
* 应用私钥字符串
* RSA签名验签工具:https://docs.open.alipay.com/291/106097)
* 密钥格式一栏请选择 “PKCS1(非JAVA适用)”
*/
privateKey: string;
/** 签名类型 */
signType?: "RSA2" | "RSA";
/** 支付宝公钥(需要对返回值做验签时候必填) */
alipayPublicKey?: string;
/** 网关 */
gateway?: string;
/** 网关超时时间(单位毫秒,默认 5s) */
timeout?: number;
/** 是否把网关返回的下划线 key 转换为驼峰写法 */
camelcase?: boolean;
/** 编码(只支持 utf-8) */
charset?: "utf-8";
/** api版本 */
version?: "1.0";
urllib?: any;
/** 指定private key类型, 默认: PKCS1, PKCS8: PRIVATE KEY, PKCS1: RSA PRIVATE KEY */
keyType?: "PKCS1" | "PKCS8";
/** 应用公钥证书文件路径 */
appCertPath?: string;
/** 应用公钥证书文件内容 */
appCertContent?: string | Buffer;
/** 应用公钥证书sn */
appCertSn?: string;
/** 支付宝根证书文件路径 */
alipayRootCertPath?: string;
/** 支付宝根证书文件内容 */
alipayRootCertContent?: string | Buffer;
/** 支付宝根证书sn */
alipayRootCertSn?: string;
/** 支付宝公钥证书文件路径 */
alipayPublicCertPath?: string;
/** 支付宝公钥证书文件内容 */
alipayPublicCertContent?: string | Buffer;
/** 支付宝公钥证书sn */
alipayCertSn?: string;
/** AES密钥,调用AES加解密相关接口时需要 */
encryptKey?: string;
/** 服务器地址 */
wsServiceUrl?: string;
}
/**
* IOT配置
*/
export interface CoolIotConfig {
/** MQTT服务端口 */
port: number;
/** MQTT Websocket服务端口 */
wsPort: number;
/** redis 配置 mqtt cluster下必须要配置 */
redis?: {
/** host */
host: string;
/** port */
port: number;
/** password */
password: string;
/** db */
db: number;
};
/** 发布消息配置 */
publish?: PublishPacket;
/** 认证 */
auth?: {
/** 用户 */
username: string;
/** 密码 */
password: string;
};
/** 服务配置 */
serve?: AedesOptions;
}

View File

@ -0,0 +1,100 @@
import { IMidwayApplication } from '@midwayjs/core';
import {
ALL,
App,
Config,
Init,
Provide,
Scope,
ScopeEnum,
} from '@midwayjs/decorator';
import * as fs from 'fs';
import { CoolCoreException } from '../exception/core';
import { ModuleConfig } from '../interface';
import * as _ from 'lodash';
/**
* 模块配置
*/
@Provide()
@Scope(ScopeEnum.Singleton)
export class CoolModuleConfig {
@App()
app: IMidwayApplication;
@Config(ALL)
allConfig;
modules;
@Init()
async init() {
let modules = [];
// 模块路径
const moduleBasePath = `${this.app.getBaseDir()}/modules/`;
if (!fs.existsSync(moduleBasePath)) {
return;
}
if (!this.allConfig['module']) {
this.allConfig['module'] = {};
}
// 全局中间件
const globalMiddlewareArr = [];
for (const module of fs.readdirSync(moduleBasePath)) {
const modulePath = `${moduleBasePath}/${module}`;
const dirStats = fs.statSync(modulePath);
if (dirStats.isDirectory()) {
const configPath = `${modulePath}/config.${
this.app.getEnv() == 'local' ? 'ts' : 'js'
}`;
if (fs.existsSync(configPath)) {
const moduleConfig: ModuleConfig = require(configPath).default({
app: this.app,
env: this.app.getEnv(),
});
modules.push({
order: moduleConfig.order || 0,
module: module,
});
await this.moduleConfig(module, moduleConfig);
// 处理全局中间件
if (!_.isEmpty(moduleConfig.globalMiddlewares)) {
globalMiddlewareArr.push({
order: moduleConfig.order || 0,
data: moduleConfig.globalMiddlewares,
});
}
} else {
throw new CoolCoreException(`模块【${module}】缺少config.ts配置文件`);
}
}
}
this.modules = _.orderBy(modules, ['order'], ['desc']).map(e => {
return e.module;
});
await this.globalMiddlewareArr(globalMiddlewareArr);
}
/**
* 模块配置
* @param module 模块
* @param config 配置
*/
async moduleConfig(module, config) {
// 追加配置
this.allConfig['module'][module] = config;
}
/**
* 全局中间件
* @param middleware 中间件
*/
async globalMiddlewareArr(middlewares: any[]) {
middlewares = _.orderBy(middlewares, ['order'], ['desc']);
for (const middleware of middlewares) {
for (const item of middleware.data) {
this.app.getMiddleware().insertLast(item);
}
}
}
}

View File

@ -0,0 +1,150 @@
import { ILogger, IMidwayApplication } from "@midwayjs/core";
import {
App,
Config,
Init,
Inject,
Logger,
Provide,
Scope,
ScopeEnum,
} from "@midwayjs/decorator";
import { CoolCoreException } from "../exception/core";
import * as Importer from "mysql2-import";
import * as fs from "fs";
import { CoolModuleConfig } from "./config";
import * as path from "path";
import { InjectDataSource, TypeORMDataSourceManager } from "@midwayjs/typeorm";
import { DataSource } from "typeorm";
import { CoolEventManager } from "../event";
/**
* 模块sql
*/
@Provide()
@Scope(ScopeEnum.Singleton)
export class CoolModuleImport {
@Config("typeorm.dataSource")
ormConfig;
@InjectDataSource("default")
defaultDataSource: DataSource;
@Inject()
typeORMDataSourceManager: TypeORMDataSourceManager;
@Config("cool")
coolConfig;
@Logger()
coreLogger: ILogger;
@Inject()
coolModuleConfig: CoolModuleConfig;
@Inject()
coolEventManager: CoolEventManager;
@App()
app: IMidwayApplication;
@Init()
async init() {
// 是否需要导入
if (this.coolConfig.initDB) {
await this.checkDbVersion();
const modules = this.coolModuleConfig.modules;
const importLockPath = path.join(
`${this.app.getBaseDir()}`,
"..",
"lock"
);
if (!fs.existsSync(importLockPath)) {
fs.mkdirSync(importLockPath);
}
setTimeout(async () => {
for (const module of modules) {
const lockPath = path.join(importLockPath, module + ".sql.lock");
if (!fs.existsSync(lockPath)) {
await this.initDataBase(module, lockPath);
}
}
this.coolEventManager.emit("onDBInit", {});
}, 2000);
}
}
/**
* 导入数据库
* @param module
* @param lockPath 锁定导入
*/
async initDataBase(module: string, lockPath: string) {
// 模块路径
const modulePath = `${this.app.getBaseDir()}/modules/${module}`;
// sql 路径
const sqlPath = `${modulePath}/init.sql`;
// 延迟2秒再导入数据库
if (fs.existsSync(sqlPath)) {
let second = 0;
const t = setInterval(() => {
this.coreLogger.info(
"\x1B[36m [cool:core] midwayjs cool core init " +
module +
" database... \x1B[0m"
);
second++;
}, 1000);
const { host, username, password, database, charset, port } = this
.ormConfig?.default
? this.ormConfig.default
: this.ormConfig;
const importer = new Importer({
host,
password,
database,
charset,
port,
user: username,
});
await importer
.import(sqlPath)
.then(async () => {
clearInterval(t);
this.coreLogger.info(
"\x1B[36m [cool:core] midwayjs cool core init " +
module +
" database complete \x1B[0m"
);
fs.writeFileSync(lockPath, `time consuming:${second}s`);
})
.catch((err) => {
clearTimeout(t);
this.coreLogger.error(
"\x1B[36m [cool:core] midwayjs cool core init " +
module +
" database err please manual import \x1B[0m"
);
fs.writeFileSync(lockPath, `time consuming:${second}s`);
this.coreLogger.error(err);
this.coreLogger.error(
`自动初始化模块[${module}]数据库失败,尝试手动导入数据库`
);
});
}
}
/**
* 检查数据库版本
*/
async checkDbVersion() {
const versions = (
await this.defaultDataSource.query("SELECT VERSION() AS version")
)[0].version.split(".");
if ((versions[0] == 5 && versions[1] < 7) || versions[0] < 5) {
throw new CoolCoreException(
"数据库不满足要求:mysql>=5.7,请升级数据库版本"
);
}
}
}

View File

@ -0,0 +1,55 @@
{
"name": "@cool-midway/core",
"version": "6.0.2",
"description": "",
"main": "index.js",
"typings": "index.d.ts",
"scripts": {
"build": "cross-env midway-bin build -c",
"test": "cross-env midway-bin test --ts",
"cov": "cross-env midway-bin cov --ts",
"lint": "mwts check",
"lint:fix": "mwts fix"
},
"keywords": [
"cool",
"cool-admin",
"cooljs"
],
"readme": "README.md",
"author": "COOL",
"files": [
"**/*.js",
"**/*.d.ts",
"index.d.ts"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://cool-js.com"
},
"devDependencies": {
"@midwayjs/cli": "1.3.21",
"@midwayjs/core": "^3.9.0",
"@midwayjs/decorator": "^3.9.0",
"@midwayjs/koa": "^3.9.0",
"@midwayjs/mock": "^3.9.0",
"@midwayjs/typeorm": "^3.9.0",
"@types/jest": "^29.2.4",
"@types/node": "^18.11.15",
"cross-env": "^7.0.3",
"jest": "^29.3.1",
"mwts": "^1.3.0",
"ts-jest": "^29.0.3",
"typeorm": "^0.3.11",
"typescript": "~4.9.4"
},
"dependencies": {
"@midwayjs/cache": "^3.9.0",
"lodash": "^4.17.21",
"md5": "^2.3.0",
"moment": "^2.29.4",
"mysql2-import": "^5.0.22",
"sqlstring": "^2.3.3"
}
}

View File

@ -0,0 +1,124 @@
import {
CONTROLLER_KEY,
getClassMetadata,
listModule,
Provide,
Scope,
ScopeEnum,
} from "@midwayjs/decorator";
import * as _ from "lodash";
import { Inject, MidwayWebRouterService } from "@midwayjs/core";
import { TypeORMDataSourceManager } from "@midwayjs/typeorm";
/**
* 实体路径
*/
@Provide()
@Scope(ScopeEnum.Singleton)
export class CoolEps {
admin = {};
app = {};
@Inject()
midwayWebRouterService: MidwayWebRouterService;
@Inject()
typeORMDataSourceManager: TypeORMDataSourceManager;
// @Init()
async init() {
const entitys = await this.entity();
const controllers = await this.controller();
const routers = await this.router();
const adminArr = [];
const appArr = [];
for (const controller of controllers) {
const { prefix, module, curdOption } = controller;
const name = curdOption?.entity?.name;
(_.startsWith(prefix, "/admin/") ? adminArr : appArr).push({
module,
api: routers[prefix],
name,
columns: entitys[name] || [],
prefix,
});
}
this.admin = _.groupBy(adminArr, "module");
this.app = _.groupBy(appArr, "module");
}
/**
* 所有controller
* @returns
*/
async controller() {
const result = [];
const controllers = listModule(CONTROLLER_KEY);
for (const controller of controllers) {
result.push(getClassMetadata(CONTROLLER_KEY, controller));
}
return result;
}
/**
* 所有路由
* @returns
*/
async router() {
return _.groupBy(
(await await this.midwayWebRouterService.getFlattenRouterTable()).map(
(item) => {
return {
method: item.requestMethod,
path: item.url,
summary: item.summary,
dts: {},
tag: "",
prefix: item.prefix,
};
}
),
"prefix"
);
}
/**
* 所有实体
* @returns
*/
async entity() {
const result = {};
const dataSourceNames = this.typeORMDataSourceManager.getDataSourceNames();
for (const dataSourceName of dataSourceNames) {
const entityMetadatas = await this.typeORMDataSourceManager.getDataSource(
dataSourceName
).entityMetadatas;
for (const entityMetadata of entityMetadatas) {
const commColums = [];
let columns = entityMetadata.columns;
columns = _.filter(
columns.map((e) => {
return {
propertyName: e.propertyName,
type:
typeof e.type == "string" ? e.type : e.type.name.toLowerCase(),
length: e.length,
comment: e.comment,
nullable: e.isNullable,
};
}),
(o) => {
if (["createTime", "updateTime"].includes(o.propertyName)) {
commColums.push(o);
}
return o && !["createTime", "updateTime"].includes(o.propertyName);
}
).concat(commColums);
result[entityMetadata.name] = columns;
}
}
return result;
}
}

View File

@ -0,0 +1,526 @@
import { Init, Provide, Inject, App, Config } from "@midwayjs/decorator";
import { CoolValidateException } from "../exception/validate";
import { ERRINFO, EVENT } from "../constant/global";
import { Application, Context } from "@midwayjs/koa";
import * as SqlString from "sqlstring";
import { CoolConfig } from "../interface";
import { TypeORMDataSourceManager } from "@midwayjs/typeorm";
import { Brackets, In, Repository, SelectQueryBuilder } from "typeorm";
import { QueryOp } from "../decorator/controller";
import * as _ from "lodash";
import { CoolEventManager } from "../event";
/**
* 服务基类
*/
@Provide()
export abstract class BaseService {
// 分页配置
@Config("cool")
private _coolConfig: CoolConfig;
// 模型
protected entity: Repository<any>;
protected sqlParams;
@Inject()
typeORMDataSourceManager: TypeORMDataSourceManager;
@Inject()
coolEventManager: CoolEventManager;
// 设置模型
setEntity(entity: any) {
this.entity = entity;
}
// 设置请求上下文
setCtx(ctx: Context) {
this.baseCtx = ctx;
}
@App()
baseApp: Application;
// 设置应用对象
setApp(app: Application) {
this.baseApp = app;
}
@Inject("ctx")
baseCtx: Context;
// 初始化
@Init()
init() {
this.sqlParams = [];
}
/**
* 设置sql
* @param condition 条件是否成立
* @param sql sql语句
* @param params 参数
*/
setSql(condition, sql, params) {
let rSql = false;
if (condition || (condition === 0 && condition !== "")) {
rSql = true;
this.sqlParams = this.sqlParams.concat(params);
}
return rSql ? sql : "";
}
/**
* 获得查询个数的SQL
* @param sql
*/
getCountSql(sql) {
sql = sql
.replace(new RegExp("LIMIT", "gm"), "limit ")
.replace(new RegExp("\n", "gm"), " ");
if (sql.includes("limit")) {
const sqlArr = sql.split("limit ");
sqlArr.pop();
sql = sqlArr.join("limit ");
}
return `select count(*) as count from (${sql}) a`;
}
/**
* 参数安全性检查
* @param params
*/
async paramSafetyCheck(params) {
const lp = params.toLowerCase();
return !(
lp.indexOf("update ") > -1 ||
lp.indexOf("select ") > -1 ||
lp.indexOf("delete ") > -1 ||
lp.indexOf("insert ") > -1
);
}
/**
* 原生查询
* @param sql
* @param params
* @param connectionName
*/
async nativeQuery(sql, params?, connectionName?) {
if (_.isEmpty(params)) {
params = this.sqlParams;
}
let newParams = [];
newParams = newParams.concat(params);
this.sqlParams = [];
for (const param of newParams) {
SqlString.escape(param);
}
return await this.getOrmManager(connectionName).query(sql, newParams || []);
}
/**
* 获得ORM管理
* @param connectionName 连接名称
*/
getOrmManager(connectionName = "default") {
return this.typeORMDataSourceManager.getDataSource(connectionName);
}
/**
* 操作entity获得分页数据,不用写sql
* @param find QueryBuilder
* @param query
* @param autoSort
* @param connectionName
*/
async entityRenderPage(
find: SelectQueryBuilder<any>,
query,
autoSort = true
) {
const {
size = this._coolConfig.crud.pageSize,
page = 1,
order = "createTime",
sort = "desc",
isExport = false,
maxExportLimit,
} = query;
const count = await find.getCount();
let dataFind: SelectQueryBuilder<any>;
if (isExport && maxExportLimit > 0) {
dataFind = find.limit(maxExportLimit);
} else {
dataFind = find.offset((page - 1) * size).limit(size);
}
if (autoSort) {
find.addOrderBy(order, sort.toUpperCase());
}
return {
list: await dataFind.getMany(),
pagination: {
page: parseInt(page),
size: parseInt(size),
total: count,
},
};
}
/**
* 执行SQL并获得分页数据
* @param sql 执行的sql语句
* @param query 分页查询条件
* @param autoSort 是否自动排序
* @param connectionName 连接名称
*/
async sqlRenderPage(sql, query, autoSort = true, connectionName?) {
const {
size = this._coolConfig.crud.pageSize,
page = 1,
order = "createTime",
sort = "desc",
isExport = false,
maxExportLimit,
} = query;
if (order && sort && autoSort) {
if (!(await this.paramSafetyCheck(order + sort))) {
throw new CoolValidateException("非法传参~");
}
sql += ` ORDER BY ${SqlString.escapeId(order)} ${this.checkSort(sort)}`;
}
if (isExport && maxExportLimit > 0) {
this.sqlParams.push(parseInt(maxExportLimit));
sql += " LIMIT ? ";
}
if (!isExport) {
this.sqlParams.push((page - 1) * size);
this.sqlParams.push(parseInt(size));
sql += " LIMIT ?,? ";
}
let params = [];
params = params.concat(this.sqlParams);
const result = await this.nativeQuery(sql, params, connectionName);
const countResult = await this.nativeQuery(
this.getCountSql(sql),
params,
connectionName
);
return {
list: result,
pagination: {
page: parseInt(page),
size: parseInt(size),
total: parseInt(countResult[0] ? countResult[0].count : 0),
},
};
}
/**
* 检查排序
* @param sort 排序
* @returns
*/
private checkSort(sort) {
if (!["desc", "asc"].includes(sort.toLowerCase())) {
throw new CoolValidateException("sort 非法传参~");
}
return sort;
}
/**
* 获得单个ID
* @param id ID
* @param infoIgnoreProperty 忽略返回属性
*/
async info(id: any, infoIgnoreProperty?: string[]) {
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
if (!id) {
throw new CoolValidateException(ERRINFO.NOID);
}
const info = await this.entity.findOneBy({ id });
if (info && infoIgnoreProperty) {
for (const property of infoIgnoreProperty) {
delete info[property];
}
}
return info;
}
/**
* 删除
* @param ids 删除的ID集合 如:[1,2,3] 或者 1,2,3
*/
async delete(ids: any) {
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
await this.modifyBefore(ids, "delete");
if (ids instanceof String) {
ids = ids.split(",");
}
// 启动软删除发送事件
if (this._coolConfig.crud?.softDelete) {
this.softDelete(ids);
}
await this.entity.delete(ids);
await this.modifyAfter(ids, "delete");
}
/**
* 软删除
* @param ids 删除的ID数组
* @param entity 实体
*/
async softDelete(ids: number[], entity?: Repository<any>) {
const data = await this.entity.find({
where: {
id: In(ids),
},
});
if (_.isEmpty(data)) return;
const _entity = entity ? entity : this.entity;
const params = {
data,
ctx: this.baseCtx,
entity: _entity,
};
if (data.length > 0) {
this.coolEventManager.emit(EVENT.SOFT_DELETE, params);
}
}
/**
* 修改
* @param param 数据
*/
async update(param: any) {
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
await this.modifyBefore(param, "update");
if (!param.id && !(param instanceof Array))
throw new CoolValidateException(ERRINFO.NOID);
await this.addOrUpdate(param);
await this.modifyAfter(param, "update");
}
/**
* 新增
* @param param 数据
*/
async add(param: any | any[]): Promise<Object> {
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
await this.modifyBefore(param, "add");
await this.addOrUpdate(param);
await this.modifyAfter(param, "add");
return {
id:
param instanceof Array
? param.map((e) => {
return e.id ? e.id : e._id;
})
: param.id
? param.id
: param._id,
};
}
/**
* 新增|修改
* @param param 数据
*/
async addOrUpdate(param: any | any[]) {
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
delete param.createTime;
if (param.id) {
param.updateTime = new Date();
await this.entity.update(param.id, param);
} else {
param.createTime = new Date();
param.updateTime = new Date();
await this.entity.insert(param);
}
}
/**
* 非分页查询
* @param query 查询条件
* @param option 查询配置
* @param connectionName 连接名
*/
async list(query, option, connectionName?): Promise<any> {
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
const sql = await this.getOptionFind(query, option);
return this.nativeQuery(sql, [], connectionName);
}
/**
* 分页查询
* @param query 查询条件
* @param option 查询配置
* @param connectionName 连接名
*/
async page(query, option, connectionName?) {
if (!this.entity) throw new CoolValidateException(ERRINFO.NOENTITY);
const sql = await this.getOptionFind(query, option);
return this.sqlRenderPage(sql, query, false, connectionName);
}
/**
* 构建查询配置
* @param query 前端查询
* @param option
*/
private async getOptionFind(query, option: QueryOp) {
let { order = "createTime", sort = "desc", keyWord = "" } = query;
const sqlArr = ["SELECT"];
const selects = ["a.*"];
const find = this.entity.createQueryBuilder("a");
if (option) {
if (typeof option == "function") {
// @ts-ignore
option = await option(this.baseCtx, this.baseApp);
}
// 判断是否有关联查询,有的话取个别名
if (!_.isEmpty(option.join)) {
for (const item of option.join) {
selects.push(`${item.alias}.*`);
find[item.type || "leftJoin"](
item.entity,
item.alias,
item.condition
);
}
}
// 默认条件
if (option.where) {
const wheres =
typeof option.where == "function"
? await option.where(this.baseCtx, this.baseApp)
: option.where;
if (!_.isEmpty(wheres)) {
for (const item of wheres) {
if (
item.length == 2 ||
(item.length == 3 &&
(item[2] || (item[2] === 0 && item[2] != "")))
) {
for (const key in item[1]) {
this.sqlParams.push(item[1][key]);
}
find.andWhere(item[0], item[1]);
}
}
}
}
// 附加排序
if (!_.isEmpty(option.addOrderBy)) {
for (const key in option.addOrderBy) {
if (order && order == key) {
sort = option.addOrderBy[key].toUpperCase();
}
find.addOrderBy(
SqlString.escapeId(key),
this.checkSort(option.addOrderBy[key].toUpperCase())
);
}
}
// 关键字模糊搜索
if (keyWord || (keyWord == 0 && keyWord != "")) {
keyWord = `%${keyWord}%`;
find.andWhere(
new Brackets((qb) => {
const keyWordLikeFields = option.keyWordLikeFields;
for (let i = 0; i < option.keyWordLikeFields?.length || 0; i++) {
qb.orWhere(`${keyWordLikeFields[i]} like :keyWord`, {
keyWord,
});
this.sqlParams.push(keyWord);
}
})
);
}
// 筛选字段
if (!_.isEmpty(option.select)) {
sqlArr.push(option.select.join(","));
find.select(option.select);
} else {
sqlArr.push(selects.join(","));
}
// 字段全匹配
if (!_.isEmpty(option.fieldEq)) {
for (const key of option.fieldEq) {
const c = {};
// 单表字段无别名的情况下操作
if (typeof key === "string") {
if (query[key] || (query[key] == 0 && query[key] == "")) {
c[key] = query[key];
const eq = query[key] instanceof Array ? "in" : "=";
if (eq === "in") {
find.andWhere(`${key} ${eq} (:${key})`, c);
} else {
find.andWhere(`${key} ${eq} :${key}`, c);
}
this.sqlParams.push(query[key]);
}
} else {
if (
query[key.requestParam] ||
(query[key.requestParam] == 0 && query[key.requestParam] !== "")
) {
c[key.column] = query[key.requestParam];
const eq = query[key.requestParam] instanceof Array ? "in" : "=";
if (eq === "in") {
find.andWhere(`${key.column} ${eq} (:${key.column})`, c);
} else {
find.andWhere(`${key.column} ${eq} :${key.column}`, c);
}
this.sqlParams.push(query[key.requestParam]);
}
}
}
}
} else {
sqlArr.push(selects.join(","));
}
// 接口请求的排序
if (sort && order) {
const sorts = sort.toUpperCase().split(",");
const orders = order.split(",");
if (sorts.length != orders.length) {
throw new CoolValidateException(ERRINFO.SORTFIELD);
}
for (const i in sorts) {
find.addOrderBy(
SqlString.escapeId(orders[i]),
this.checkSort(sorts[i])
);
}
}
if (option?.extend) {
await option?.extend(find, this.baseCtx, this.baseApp);
}
const sqls = find.getSql().split("FROM");
sqlArr.push("FROM");
sqlArr.push(sqls[1]);
return sqlArr.join(" ");
}
/**
* 新增|修改|删除 之后的操作
* @param data 对应数据
*/
async modifyAfter(
data: any,
type: "delete" | "update" | "add"
): Promise<void> {}
/**
* 新增|修改|删除 之前的操作
* @param data 对应数据
*/
async modifyBefore(
data: any,
type: "delete" | "update" | "add"
): Promise<void> {}
}

View File

@ -0,0 +1,46 @@
import { CoolUrlTagConfig } from './../decorator/tag';
import {
CONTROLLER_KEY,
getClassMetadata,
Init,
listModule,
Provide,
Scope,
ScopeEnum,
} from '@midwayjs/decorator';
import { COOL_URL_TAG_KEY } from '../decorator/tag';
/**
* URL标签
*/
@Provide()
@Scope(ScopeEnum.Singleton)
export class CoolUrlTagData {
data = {};
@Init()
async init() {
const tags = listModule(COOL_URL_TAG_KEY);
for (const controller of tags) {
const controllerOption = getClassMetadata(CONTROLLER_KEY, controller);
const tagOption: CoolUrlTagConfig = getClassMetadata(
COOL_URL_TAG_KEY,
controller
);
const data: string[] = this.data[tagOption.key] || [];
this.data[tagOption.key] = data.concat(
tagOption.value.map(e => {
return controllerOption.prefix + '/' + e;
})
);
}
}
/**
* 根据键获得
* @param key
* @returns
*/
byKey(key: string): string[] {
return this.data[key];
}
}

View File

@ -0,0 +1,27 @@
import { ILogger } from '@midwayjs/core';
import { Init, Logger, Provide, Scope, ScopeEnum } from '@midwayjs/decorator';
import * as moment from 'moment';
/**
* 常用函数处理
*/
@Provide()
@Scope(ScopeEnum.Singleton)
export class FuncUtil {
@Logger()
coreLogger: ILogger;
@Init()
async init() {
Date.prototype.toJSON = function () {
return moment(this).format('YYYY-MM-DD HH:mm:ss');
};
// 新增String支持replaceAll方法
String.prototype['replaceAll'] = function (s1, s2) {
return this.replace(new RegExp(s1, 'gm'), s2);
};
this.coreLogger.info(
'\x1B[36m [cool:core] midwayjs cool core func handler \x1B[0m'
);
}
}

View File

@ -0,0 +1,95 @@
import { Session } from 'inspector';
import { v1 as uuid } from 'uuid';
import * as util from 'util';
/**
* Location 工具类
*/
class LocationUtil {
static instance = null;
session: Session;
PREFIX = '__functionLocation__';
scripts = {};
post$ = null;
constructor() {
if (!LocationUtil.instance) {
this.init();
LocationUtil.instance = this;
}
return LocationUtil.instance;
}
init() {
if (!global[this.PREFIX]) {
global[this.PREFIX] = {};
}
if (this.session) {
return;
}
this.session = new Session();
this.session.connect();
this.post$ = util.promisify(this.session.post).bind(this.session);
this.session.on('Debugger.scriptParsed', res => {
this.scripts[res.params.scriptId] = res.params;
LocationUtil.instance = this;
});
this.post$('Debugger.enable');
LocationUtil.instance = this;
}
/**
* 获得脚本位置
* @param target
*/
async scriptPath(target: any) {
const id = uuid();
global[this.PREFIX][id] = target;
const evaluated = await this.post$('Runtime.evaluate', {
expression: `global['${this.PREFIX}']['${id}']`,
objectGroup: this.PREFIX,
});
const properties = await this.post$('Runtime.getProperties', {
objectId: evaluated.result.objectId,
});
const location = properties.internalProperties.find(
prop => prop.name === '[[FunctionLocation]]'
);
const script = this.scripts[location.value.value.scriptId];
delete global[this.PREFIX][id];
let source = decodeURI(script.url);
if (!source.startsWith('file://')) {
source = `file://${source}`;
}
return {
column: location.value.value.columnNumber + 1,
line: location.value.value.lineNumber + 1,
path: source.substr(7),
source,
};
}
/**
* 清除
*/
async clean() {
if (this.session) {
await this.post$('Runtime.releaseObjectGroup', {
objectGroup: this.PREFIX,
});
this.session.disconnect();
}
this.session = null;
this.post$ = null;
this.scripts = null;
delete global[this.PREFIX];
LocationUtil.instance = null;
}
}
export default new LocationUtil();

View File

@ -0,0 +1,14 @@
import { createLightApp } from '@midwayjs/mock';
import * as custom from '../src';
describe('/test/index.test.ts', () => {
it('test component', async () => {
const app = await createLightApp('', {
imports: [
custom
]
});
const bookService = await app.getApplicationContext().getAsync(custom.BookService);
expect(await bookService.getBookById()).toEqual('hello world');
});
});

View File

@ -0,0 +1,25 @@
{
"compileOnSave": true,
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"moduleResolution": "node",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"inlineSourceMap":false,
"noImplicitThis": true,
"noUnusedLocals": true,
"stripInternal": true,
"skipLibCheck": true,
"noImplicitReturns": false,
"pretty": true,
"declaration": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist"
},
"exclude": [
"dist",
"node_modules",
"test"
]
}

11
packages/es/.editorconfig Normal file
View File

@ -0,0 +1,11 @@
# 🎨 editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

View File

@ -0,0 +1,28 @@
{
"extends": "./node_modules/mwts/",
"ignorePatterns": [
"node_modules",
"dist",
"test",
"jest.config.js",
"typings",
"public/**/**",
"view/**/**"
],
"env": {
"jest": true
},
"rules": {
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/ban-ts-comment": "off",
"node/no-extraneous-import": "off",
"no-empty": "off",
"node/no-extraneous-require": "off",
"eqeqeq": "off",
"node/no-unsupported-features/node-builtins": "off",
"@typescript-eslint/ban-types": "off",
"no-control-regex": "off",
"prefer-const": "off"
}
}

15
packages/es/.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
logs/
npm-debug.log
yarn-error.log
node_modules/
package-lock.json
yarn.lock
coverage/
dist/
.idea/
run/
.DS_Store
*.sw*
*.un~
.tsbuildinfo
.tsbuildinfo.*

View File

@ -0,0 +1,3 @@
module.exports = {
...require('mwts/.prettierrc.json')
}

3
packages/es/README.md Normal file
View File

@ -0,0 +1,3 @@
# cool-admin
https://cool-js.com

10
packages/es/index.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
export * from './dist/index';
declare module '@midwayjs/core/dist/interface' {
interface MidwayConfig {
book?: PowerPartial<{
a: number;
b: string;
}>;
}
}

View File

@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['<rootDir>/test/fixtures'],
coveragePathIgnorePatterns: ['<rootDir>/test/'],
setupFilesAfterEnv: ['./jest.setup.js']
};

View File

@ -0,0 +1 @@
jest.setTimeout(30000);

44
packages/es/package.json Normal file
View File

@ -0,0 +1,44 @@
{
"name": "@cool-midway/es",
"version": "6.0.0",
"description": "cool-js.com elasticsearch",
"main": "dist/index.js",
"typings": "index.d.ts",
"scripts": {
"build": "cross-env midway-bin build -c",
"test": "cross-env midway-bin test --ts",
"cov": "cross-env midway-bin cov --ts",
"lint": "mwts check",
"lint:fix": "mwts fix"
},
"keywords": [
"cool",
"cool-admin",
"cooljs"
],
"author": "COOL",
"readme": "README.md",
"files": [
"dist/**/*.js",
"dist/**/*.d.ts",
"index.d.ts"
],
"license": "MIT",
"devDependencies": {
"@cool-midway/core": "^6.0.0",
"@midwayjs/cli": "^2.0.9",
"@midwayjs/core": "^3.9.0",
"@midwayjs/decorator": "^3.9.0",
"@midwayjs/mock": "^3.9.0",
"@types/jest": "^29.2.5",
"@types/node": "^18.11.18",
"cross-env": "^7.0.3",
"jest": "^29.3.1",
"mwts": "^1.3.0",
"ts-jest": "^29.0.3",
"typescript": "^4.9.4"
},
"dependencies": {
"@elastic/elasticsearch": "^8.5.0"
}
}

569
packages/es/src/base.ts Normal file
View File

@ -0,0 +1,569 @@
import { CoolEventManager } from '@cool-midway/core';
import { Client } from '@elastic/elasticsearch';
import { WaitForActiveShards } from '@elastic/elasticsearch/lib/api/types';
import { Inject, Logger } from '@midwayjs/decorator';
import { ILogger } from '@midwayjs/logger';
import { EsConfig } from '.';
/**
* Es索引基类
*/
export class BaseEsIndex {
// 索引
public index: string;
// es客户端
public client: Client;
// 日志
@Logger()
coreLogger: ILogger;
// 事件
@Inject('cool:coolEventManager')
coolEventManager: CoolEventManager;
/**
* 设置索引
* @param index
*/
setIndex(index: string) {
this.index = index;
}
/**
* 处理es数据变更事件,主要用于同步数据
* @param method
* @param data
*/
async handleDataChange(index, method, data) {
this.index = index;
const {
id,
ids,
bodys,
body,
type,
refresh,
waitForActiveShards,
properties,
config,
} = data;
switch (method) {
case 'upsert':
await this.upsert(body, refresh, waitForActiveShards);
break;
case 'batchIndex':
await this.batchIndex(bodys, type, refresh, waitForActiveShards);
break;
case 'deleteById':
await this.deleteById(id, refresh, waitForActiveShards);
break;
case 'deleteByIds':
await this.deleteByIds(ids, refresh, waitForActiveShards);
break;
case 'deleteByQuery':
await this.deleteByQuery(body, refresh, waitForActiveShards);
break;
case 'updateById':
await this.updateById(body, refresh, waitForActiveShards);
break;
case 'updateByQuery':
await this.updateByQuery(body, refresh, waitForActiveShards);
break;
case 'createIndex':
await this.updateByQuery(properties, config);
break;
}
}
/**
* 数据更新事件
* @param method
* @param data
*/
async esDataChange(method, data) {
this.coolEventManager?.emit('esDataChange', this.index, method, data);
}
/**
*
* @param client
*/
setClient(client: Client) {
this.client = client;
}
/**
* 查询
* @param body
*/
async find(body?: any, size?: number) {
if (!body) {
body = {};
}
body.size = size ? size : 10000;
return this.client
.search({
index: this.index,
body,
})
.then(res => {
return (
res.hits.hits.map(e => {
e._source['id'] = e._id;
const _source: any = e._source;
['_id', '_index', '_score', '_source'].forEach(key => {
delete e[key];
});
return {
..._source,
...e,
};
}) || []
);
});
}
/**
* 分页查询
* @param body
* @param page
* @param size
*/
async findPage(body?: any, page?: number, size?: number) {
if (!page) {
page = 1;
}
if (!size) {
size = 20;
}
if (!body) {
body = {};
}
const total = await this.findCount(body);
body.from = (page - 1) * size;
body.size = size;
return this.client.search({ index: this.index, body }).then(res => {
const result =
res.hits.hits.map(e => {
e._source['id'] = e._id;
const _source: any = e._source;
['_id', '_index', '_score', '_source'].forEach(key => {
delete e[key];
});
return {
..._source,
...e,
};
}) || [];
return {
list: result,
pagination: {
page,
size,
total,
},
};
});
}
/**
* 根据ID查询
* @param id
* @returns
*/
async findById(id) {
return this.client
.get({
index: this.index,
id,
})
.then(res => {
res._source['id'] = res._id;
return res._source || undefined;
})
.catch(e => {
return undefined;
});
}
/**
* 根据多个ID查询
* @param ids
* @returns
*/
async findByIds(ids: string[]) {
return this.client
.mget({ index: this.index, body: { ids } })
.then(res => {
const result = res.docs.map((e: any) => {
e._source.id = e._id;
return e._source || 'undefined';
});
return result.filter(e => {
return e !== 'undefined';
});
})
.catch(e => {
return undefined;
});
}
/**
* 插入与更新
* @param body
* @param refresh
* @param waitForActiveShards
* @returns
*/
async upsert(
body: any,
refresh?: boolean | 'wait_for',
waitForActiveShards?: WaitForActiveShards
) {
if (refresh == undefined) {
refresh = true;
}
if (body.id) {
this.esDataChange('upsert', {
body,
refresh,
waitForActiveShards,
});
const id = body.id;
delete body.id;
return this.client.index({
id,
index: this.index,
wait_for_active_shards: waitForActiveShards,
refresh,
body,
});
} else {
return this.client
.index({
index: this.index,
wait_for_active_shards: waitForActiveShards,
refresh,
body,
})
.then(res => {
this.esDataChange('upsert', {
body: {
...body,
id: res._id,
},
refresh,
waitForActiveShards,
});
return res;
});
}
}
/**
* 批量插入更新
* @param bodys
* @param type
* @param refresh
* @param waitForActiveShards
* @returns
*/
async batchIndex(
bodys: any[],
type: 'index' | 'create' | 'delete' | 'update',
refresh?: boolean | 'wait_for',
waitForActiveShards?: WaitForActiveShards
) {
this.esDataChange('batchIndex', {
bodys,
type,
refresh,
waitForActiveShards,
});
if (refresh == undefined) {
refresh = true;
}
const list = [];
for (const body of bodys) {
const typeO = {};
typeO[type] = { _index: this.index, _id: body.id };
if (body.id) {
delete body.id;
}
list.push(typeO);
if (type !== 'delete') {
if (type == 'update') {
list.push({ doc: body });
} else {
list.push(body);
}
}
}
return this.client.bulk({
wait_for_active_shards: waitForActiveShards,
index: this.index,
refresh,
body: list,
});
}
/**
* 删除索引
* @param id
* @param refresh
* @param waitForActiveShards
* @returns
*/
async deleteById(
id,
refresh?: boolean | 'wait_for',
waitForActiveShards?: WaitForActiveShards
) {
this.esDataChange('deleteById', {
id,
refresh,
waitForActiveShards,
});
if (refresh == undefined) {
refresh = true;
}
try {
return this.client.delete({
index: this.index,
refresh,
wait_for_active_shards: waitForActiveShards,
id,
});
} catch {}
}
/**
* 删除文档
* @param ids
* @param refresh
* @param waitForActiveShards
* @returns
*/
async deleteByIds(
ids: string[],
refresh?: boolean,
waitForActiveShards?: WaitForActiveShards
) {
this.esDataChange('deleteByIds', {
ids,
refresh,
waitForActiveShards,
});
if (refresh == undefined) {
refresh = true;
}
const body = {
query: {
bool: {
must: [
{
terms: {
_id: ids,
},
},
],
},
},
};
return this.client.deleteByQuery({
index: this.index,
refresh,
wait_for_active_shards: waitForActiveShards,
body,
});
}
/**
* 根据条件批量删除
* @param body
* @param refresh
* @param waitForActiveShards
* @returns
*/
async deleteByQuery(
body,
refresh?: boolean,
waitForActiveShards?: WaitForActiveShards
) {
this.esDataChange('deleteByQuery', {
body,
refresh,
waitForActiveShards,
});
if (refresh == undefined) {
refresh = true;
}
return this.client.deleteByQuery({
index: this.index,
refresh,
wait_for_active_shards: waitForActiveShards,
body,
});
}
/**
* 更新索引
* @param body
* @param refresh
* @param waitForActiveShards
* @returns
*/
async updateById(
body,
refresh?: boolean | 'wait_for',
waitForActiveShards?: WaitForActiveShards
) {
this.esDataChange('updateById', {
body,
refresh,
waitForActiveShards,
});
if (refresh == undefined) {
refresh = true;
}
const id = body.id;
delete body.id;
return this.client.update({
wait_for_active_shards: waitForActiveShards,
index: this.index,
id: id,
refresh,
body: {
doc: body,
},
});
}
/**
* 根据条件更新
* @param body
* @param refresh
* @param waitForActiveShards
*/
async updateByQuery(
body,
refresh?: boolean,
waitForActiveShards?: WaitForActiveShards
) {
this.esDataChange('updateByQuery', {
body,
refresh,
waitForActiveShards,
});
if (refresh == undefined) {
refresh = true;
}
return this.client.updateByQuery({
index: this.index,
refresh,
wait_for_active_shards: waitForActiveShards,
body,
});
}
/**
* 查询条数
* @param body
*/
async findCount(body?: any) {
let _body = Object.assign({}, body || {});
delete _body.from;
delete _body.size;
delete _body.sort;
return this.client
.count({
index: this.index,
body: _body,
})
.then(res => {
return res.count;
})
.catch(e => {
return undefined;
});
}
/**
* 创建更新索引
* @param config 配置
*/
async createIndex(
properties: {},
config: EsConfig = {
name: '',
replicas: 1,
shards: 8,
analyzers: [],
}
) {
this.esDataChange('createIndex', {
properties,
config,
});
const body = {
settings: {
number_of_shards: config.shards,
number_of_replicas: config.replicas,
analysis: {
analyzer: {
comma: { type: 'pattern', pattern: ',' },
blank: { type: 'pattern', pattern: ' ' },
},
},
mapping: {
nested_fields: {
limit: 100,
},
},
},
mappings: {
properties: {},
},
};
if (config.analyzers) {
for (const analyzer of config.analyzers) {
for (const key in analyzer) {
body.settings.analysis.analyzer[key] = analyzer[key];
}
}
}
const param = {
index: this.index,
body,
};
param.body = body;
param.body.mappings.properties = properties;
this.client.indices.exists({ index: this.index }).then(async res => {
if (!res) {
await this.client.indices.create(param).then(res => {
if (res.acknowledged) {
console.info(
'\x1B[36m [cool:core] midwayjs cool elasticsearch ES索引创建成功: ' +
this.index +
' \x1B[0m'
);
}
});
} else {
const updateParam = {
index: this.index,
body: param.body.mappings,
};
await this.client.indices.putMapping(updateParam).then(res => {
if (res.acknowledged) {
console.info(
'\x1B[36m [cool:core] midwayjs cool elasticsearch ES索引更新成功: ' +
this.index +
' \x1B[0m'
);
}
});
}
});
}
}

View File

@ -0,0 +1,4 @@
export const customKey = {
a: 1,
b: 'hello',
};

View File

@ -0,0 +1,19 @@
import { Configuration } from '@midwayjs/decorator';
import * as DefaultConfig from './config/config.default';
import { IMidwayContainer } from '@midwayjs/core';
import { CoolElasticSearch } from './elasticsearch';
@Configuration({
namespace: 'cool:es',
importConfigs: [
{
default: DefaultConfig,
},
],
})
export class CoolEsConfiguration {
async onReady(container: IMidwayContainer) {
await container.getAsync(CoolElasticSearch);
// TODO something
}
}

View File

@ -0,0 +1,41 @@
import {
Scope,
ScopeEnum,
saveClassMetadata,
saveModule,
} from '@midwayjs/decorator';
export const COOL_ES_KEY = 'decorator:cool:es';
export interface EsConfig {
shards?: number;
name: string;
replicas?: number;
analyzers?: any[];
}
/**
* 索引
* @param config
* @returns
*/
export function CoolEsIndex(
config: EsConfig | string = {
name: '',
replicas: 1,
shards: 8,
analyzers: [],
}
): ClassDecorator {
if (typeof config == 'string') {
config = { name: config, replicas: 1, shards: 8, analyzers: [] };
}
return (target: any) => {
// 将装饰的类,绑定到该装饰器,用于后续能获取到 class
saveModule(COOL_ES_KEY, target);
// 保存一些元数据信息,任意你希望存的东西
saveClassMetadata(COOL_ES_KEY, config, target);
// 指定 IoC 容器创建实例的作用域,这里注册为请求作用域,这样能取到 ctx
Scope(ScopeEnum.Singleton)(target);
};
}

View File

@ -0,0 +1,154 @@
import {
Provide,
getClassMetadata,
App,
Logger,
Inject,
Init,
Scope,
ScopeEnum,
Config,
} from '@midwayjs/decorator';
import { COOL_ES_KEY, EsConfig } from './decorator/elasticsearch';
import { listModule } from '@midwayjs/decorator';
import { IMidwayApplication } from '@midwayjs/core';
import { CoolCoreException, CoolEventManager } from '@cool-midway/core';
import { ILogger } from '@midwayjs/logger';
import { Client } from '@elastic/elasticsearch';
import * as _ from 'lodash';
import { CoolEsConfig, ICoolEs } from '.';
/**
* 搜索引擎
*/
@Provide()
@Scope(ScopeEnum.Singleton)
export class CoolElasticSearch {
@App()
app: IMidwayApplication;
@Logger()
coreLogger: ILogger;
@Config('cool.es')
esConfig: CoolEsConfig;
client: Client;
@Inject('cool:coolEventManager')
coolEventManager: CoolEventManager;
@Init()
async init() {
if (!this.esConfig?.nodes) {
throw new CoolCoreException('es.nodes config is require');
}
if (this.esConfig.nodes.length == 1) {
this.client = new Client({ node: this.esConfig.nodes[0] });
} else {
this.client = new Client({ nodes: this.esConfig.nodes });
}
this.client.ping({}, { requestTimeout: 30000 }).then(res => {
if (res) {
this.coolEventManager.emit('esReady', this.client);
this.scan();
}
});
}
async scan() {
const modules = listModule(COOL_ES_KEY);
for (let module of modules) {
const cls: ICoolEs = await this.app
.getApplicationContext()
.getAsync(module);
const data = getClassMetadata(COOL_ES_KEY, module);
this.createIndex(cls, data);
}
}
/**
* 数据更新事件
* @param method
* @param data
*/
async esDataChange(method, data) {
//this.coolEventManager.emit('esDataChange', { method, data });
}
/**
* 创建索引
* @param cls
* @param config
*/
async createIndex(cls, config: EsConfig) {
cls.index = config.name;
cls.client = this.client;
const body = {
settings: {
number_of_shards: config.shards,
number_of_replicas: config.replicas,
analysis: {
analyzer: {
comma: { type: 'pattern', pattern: ',' },
blank: { type: 'pattern', pattern: ' ' },
},
},
mapping: {
nested_fields: {
limit: 100,
},
},
},
mappings: {
properties: {},
},
};
if (config.analyzers) {
for (const analyzer of config.analyzers) {
for (const key in analyzer) {
body.settings.analysis.analyzer[key] = analyzer[key];
}
}
}
const param = {
index: config.name,
body,
};
param.body = body;
param.body.mappings.properties = cls.indexInfo();
this.esDataChange('createIndex', {
properties: param.body.mappings.properties,
config,
});
this.client.indices.exists({ index: config.name }).then(async res => {
if (!res) {
await this.client.indices.create(param).then(res => {
if (res.acknowledged) {
this.coreLogger.info(
'\x1B[36m [cool:core] midwayjs cool elasticsearch ES索引创建成功: ' +
config.name +
' \x1B[0m'
);
}
});
} else {
const updateParam = {
index: config.name,
body: param.body.mappings,
};
await this.client.indices.putMapping(updateParam).then(res => {
if (res.acknowledged) {
this.coreLogger.info(
'\x1B[36m [cool:core] midwayjs cool elasticsearch ES索引更新成功: ' +
config.name +
' \x1B[0m'
);
}
});
}
});
}
}

15
packages/es/src/index.ts Normal file
View File

@ -0,0 +1,15 @@
export { CoolEsConfiguration as Configuration } from './configuration';
export * from './elasticsearch';
export * from './decorator/elasticsearch';
export * from './base';
export interface ICoolEs {
indexInfo(): Object;
}
export interface CoolEsConfig {
nodes: string[];
}

View File

@ -0,0 +1,42 @@
{
"name": "@cool-midway/es",
"version": "6.0.0",
"description": "cool-js.com elasticsearch",
"main": "index.js",
"typings": "index.d.ts",
"scripts": {
"build": "cross-env midway-bin build -c",
"test": "cross-env midway-bin test --ts",
"cov": "cross-env midway-bin cov --ts",
"lint": "mwts check",
"lint:fix": "mwts fix"
},
"keywords": [],
"author": "",
"files": [
"**/*.js",
"**/*.d.ts"
],
"repository": {
"type": "git",
"url": "https://cool-js.com"
},
"license": "MIT",
"devDependencies": {
"@cool-midway/core": "^6.0.0",
"@midwayjs/cli": "^1.2.38",
"@midwayjs/core": "^3.0.0",
"@midwayjs/decorator": "^3.0.0",
"@midwayjs/mock": "^3.0.0",
"@types/jest": "^27.4.0",
"@types/node": "^16.11.22",
"cross-env": "^6.0.0",
"jest": "^27.5.1",
"mwts": "^1.0.5",
"ts-jest": "^27.1.3",
"typescript": "^4.0.0"
},
"dependencies": {
"@elastic/elasticsearch": "^8.1.0"
}
}

View File

@ -0,0 +1,14 @@
import { createLightApp } from '@midwayjs/mock';
import * as custom from '../src';
describe('/test/index.test.ts', () => {
it('test component', async () => {
const app = await createLightApp('', {
imports: [
custom
]
});
const bookService = await app.getApplicationContext().getAsync(custom.BookService);
expect(await bookService.getBookById()).toEqual('hello world');
});
});

24
packages/es/tsconfig.json Normal file
View File

@ -0,0 +1,24 @@
{
"compileOnSave": true,
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"moduleResolution": "node",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"inlineSourceMap":false,
"noImplicitThis": true,
"noUnusedLocals": true,
"stripInternal": true,
"skipLibCheck": true,
"noImplicitReturns": false,
"pretty": true,
"declaration": true,
"outDir": "dist"
},
"exclude": [
"dist",
"node_modules",
"test"
]
}

View File

@ -0,0 +1,11 @@
# 🎨 editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

View File

@ -0,0 +1,20 @@
{
"extends": "./node_modules/mwts/",
"ignorePatterns": ["node_modules", "dist", "test", "jest.config.js", "typings"],
"env": {
"jest": true
},
"rules": {
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/ban-ts-comment": "off",
"node/no-extraneous-import": "off",
"no-empty": "off",
"node/no-extraneous-require": "off",
"eqeqeq": "off",
"node/no-unsupported-features/node-builtins": "off",
"@typescript-eslint/ban-types": "off",
"no-control-regex": "off",
"prefer-const": "off"
}
}

15
packages/file/.gitignore vendored Normal file
View File

@ -0,0 +1,15 @@
logs/
npm-debug.log
yarn-error.log
node_modules/
package-lock.json
yarn.lock
coverage/
dist/
.idea/
run/
.DS_Store
*.sw*
*.un~
.tsbuildinfo
.tsbuildinfo.*

View File

@ -0,0 +1,3 @@
module.exports = {
...require('mwts/.prettierrc.json')
}

21
packages/file/LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2013 - Now midwayjs
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.

10
packages/file/index.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
export * from './dist/index';
declare module '@midwayjs/core/dist/interface' {
interface MidwayConfig {
book?: PowerPartial<{
a: number;
b: string;
}>;
}
}

View File

@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testPathIgnorePatterns: ['<rootDir>/test/fixtures'],
coveragePathIgnorePatterns: ['<rootDir>/test/'],
setupFilesAfterEnv: ['./jest.setup.js']
};

View File

@ -0,0 +1 @@
jest.setTimeout(30000);

View File

@ -0,0 +1,53 @@
{
"name": "@cool-midway/file",
"version": "6.0.1",
"description": "",
"main": "dist/index.js",
"typings": "index.d.ts",
"scripts": {
"build": "cross-env midway-bin build -c",
"test": "cross-env midway-bin test --ts",
"cov": "cross-env midway-bin cov --ts",
"lint": "mwts check",
"lint:fix": "mwts fix"
},
"keywords": [
"cool",
"cool-admin",
"cooljs"
],
"author": "COOL",
"files": [
"dist/**/*.js",
"dist/**/*.d.ts",
"index.d.ts"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://cool-js.com"
},
"devDependencies": {
"@cool-midway/core": "^6.0.0",
"@midwayjs/cli": "^2.0.9",
"@midwayjs/core": "^3.9.0",
"@midwayjs/decorator": "^3.9.0",
"@midwayjs/mock": "^3.9.0",
"@types/jest": "^29.2.5",
"@types/node": "^18.11.18",
"cross-env": "^7.0.3",
"jest": "^29.3.1",
"lodash": "^4.17.21",
"mwts": "^1.3.0",
"ts-jest": "^29.0.3",
"typescript": "^4.9.4"
},
"dependencies": {
"@midwayjs/upload": "^3.9.9",
"ali-oss": "^6.17.1",
"cos-nodejs-sdk-v5": "^2.11.19",
"download": "^8.0.0",
"qcloud-cos-sts": "^3.1.0",
"qiniu": "^7.8.0"
}
}

View File

@ -0,0 +1,16 @@
import { CoolFileConfig } from './../interface';
import { MODETYPE } from '../interface';
/**
* cool的配置
*/
export default {
cool: {
file: {
// 上传模式
mode: MODETYPE.LOCAL,
// 文件路径前缀 本地上传模式下 有效
domain: 'http://127.0.0.1:8001',
} as CoolFileConfig,
},
};

View File

@ -0,0 +1,21 @@
import { Configuration } from '@midwayjs/decorator';
import * as DefaultConfig from './config/config.default';
import * as upload from '@midwayjs/upload';
import { IMidwayContainer } from '@midwayjs/core';
import { CoolFile } from './file';
@Configuration({
namespace: 'cool:file',
importConfigs: [
{
default: DefaultConfig,
},
],
imports: [upload],
})
export class CoolFileConfiguration {
async onReady(container: IMidwayContainer) {
await container.getAsync(CoolFile);
// TODO something
}
}

475
packages/file/src/file.ts Normal file
View File

@ -0,0 +1,475 @@
import {
App,
Config,
Init,
Logger,
Provide,
Scope,
ScopeEnum,
} from '@midwayjs/decorator';
import { Mode, CoolFileConfig, MODETYPE, CLOUDTYPE } from './interface';
import { CoolCommException } from '@cool-midway/core';
import * as moment from 'moment';
import { v1 as uuid } from 'uuid';
import * as path from 'path';
import * as fs from 'fs';
import { ILogger, IMidwayApplication } from '@midwayjs/core';
import * as _ from 'lodash';
import * as OSS from 'ali-oss';
import * as crypto from 'crypto';
import * as STS from 'qcloud-cos-sts';
import * as download from 'download';
import * as COS from 'cos-nodejs-sdk-v5';
import * as QINIU from 'qiniu';
/**
* 文件上传
*/
@Provide()
@Scope(ScopeEnum.Singleton)
export class CoolFile {
@Config('cool.file')
config: CoolFileConfig;
@Logger()
coreLogger: ILogger;
client: OSS & COS & QINIU.auth.digest.Mac;
@App()
app: IMidwayApplication;
@Init()
async init(config: CoolFileConfig) {
const filePath = path.join(this.app.getBaseDir(), '..', 'public');
const uploadsPath = path.join(filePath, 'uploads');
const tempPath = path.join(filePath, 'temp');
if (!fs.existsSync(uploadsPath)) {
fs.mkdirSync(uploadsPath);
}
if (!fs.existsSync(tempPath)) {
fs.mkdirSync(tempPath);
}
if (config) {
this.config = config;
}
const { mode, oss, cos, qiniu } = this.config;
if (mode == MODETYPE.CLOUD) {
if (oss) {
const { accessKeyId, accessKeySecret, bucket, endpoint } = oss;
this.client = new OSS({
region: endpoint.split('.')[0],
accessKeyId,
accessKeySecret,
bucket,
});
}
if (cos) {
const { accessKeyId, accessKeySecret } = cos;
this.client = new COS({
SecretId: accessKeyId,
SecretKey: accessKeySecret,
});
}
if (qiniu) {
const { accessKeyId, accessKeySecret } = qiniu;
this.client = new QINIU.auth.digest.Mac(accessKeyId, accessKeySecret);
}
}
}
/**
* 上传模式
* @returns 上传模式
*/
async getMode(): Promise<Mode> {
const { mode, oss, cos, qiniu } = this.config;
if (mode == MODETYPE.LOCAL) {
return {
mode: MODETYPE.LOCAL,
type: MODETYPE.LOCAL,
};
}
if (oss) {
return {
mode: MODETYPE.CLOUD,
type: CLOUDTYPE.OSS,
};
}
if (cos) {
return {
mode: MODETYPE.CLOUD,
type: CLOUDTYPE.COS,
};
}
if (qiniu) {
return {
mode: MODETYPE.CLOUD,
type: CLOUDTYPE.QINIU,
};
}
}
/**
* 获得原始操作对象
* @returns
*/
getMetaFileObj(): OSS & COS & QINIU.auth.digest.Mac {
return this.client;
}
/**
* 下载并上传
* @param url
* @param fileName 文件名
*/
async downAndUpload(url: string, fileName?: string) {
const { mode, oss, cos, qiniu, domain } = this.config;
let extend = '';
if (url.includes('.')) {
const urlArr = url.split('.');
extend = '.' + urlArr[urlArr.length - 1].split('?')[0];
}
const data = url.includes('http')
? await download(url)
: fs.readFileSync(url);
const isCloud = mode == MODETYPE.CLOUD;
// 创建文件夹
const dirPath = path.join(
this.app.getBaseDir(),
'..',
`public/${isCloud ? 'temp' : 'uploads'}/${moment().format('YYYYMMDD')}`
);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath);
}
const uuidStr = uuid();
const name = `uploads/${moment().format('YYYYMMDD')}/${
fileName ? fileName : uuidStr + extend
}`;
if (isCloud) {
if (oss) {
const ossClient: OSS = this.getMetaFileObj();
return (await ossClient.put(name, data)).url;
}
if (cos) {
const cosClient: COS = this.getMetaFileObj();
await cosClient.putObject({
Bucket: cos.bucket,
Region: cos.region,
Key: name,
Body: data,
});
return cos.publicDomain + '/' + name;
}
if (qiniu) {
let uploadToken = (await this.qiniu())['token'];
const formUploader = new QINIU.form_up.FormUploader();
const putExtra = new QINIU.form_up.PutExtra();
return new Promise((resolve, reject) => {
formUploader.put(
uploadToken,
name,
data,
putExtra,
(respErr, respBody, respInfo) => {
if (respErr) {
throw respErr;
}
if (respInfo.statusCode == 200) {
resolve(qiniu.publicDomain + '/' + name);
}
}
);
});
}
} else {
fs.writeFileSync(
`${dirPath}/${fileName ? fileName : uuidStr + extend}`,
data
);
return `${domain}/public/${name}`;
}
}
/**
* 指定Key(路径)上传
* @param file
* @param key 路径一致会覆盖源文件
*/
async uploadWithKey(file, key) {
const { mode, oss, cos, qiniu } = this.config;
const data = fs.readFileSync(file.data);
if (mode == MODETYPE.LOCAL) {
fs.writeFileSync(path.join(this.app.getBaseDir(), '..', key), data);
return this.config.domain + key;
}
if (mode == MODETYPE.CLOUD) {
if (oss) {
const ossClient: OSS = this.getMetaFileObj();
return (await ossClient.put(key, data)).url;
}
if (cos) {
const cosClient: COS = this.getMetaFileObj();
await cosClient.putObject({
Bucket: cos.bucket,
Region: cos.region,
Key: key,
Body: data,
});
return cos.publicDomain + '/' + key;
}
if (qiniu) {
let uploadToken = (await this.qiniu())['token'];
const formUploader = new QINIU.form_up.FormUploader();
const putExtra = new QINIU.form_up.PutExtra();
return new Promise((resolve, reject) => {
formUploader.put(
uploadToken,
key,
data,
putExtra,
(respErr, respBody, respInfo) => {
if (respErr) {
throw respErr;
}
if (respInfo.statusCode == 200) {
resolve(qiniu.publicDomain + '/' + name);
}
}
);
});
}
}
}
/**
* 上传文件
* @param ctx
* @param key 文件路径
*/
async upload(ctx) {
const { mode, oss, cos, qiniu } = this.config;
if (mode == MODETYPE.LOCAL) {
return await this.local(ctx);
}
if (mode == MODETYPE.CLOUD) {
if (oss) {
return await this.oss(ctx);
}
if (cos) {
return await this.cos(ctx);
}
if (qiniu) {
return await this.qiniu(ctx);
}
}
}
/**
* 七牛上传
* @param ctx
* @returns
*/
private async qiniu(ctx?) {
const {
bucket,
publicDomain,
region,
uploadUrl = `https://upload-${region}.qiniup.com/`,
fileKey = 'file',
} = this.config.qiniu;
let options = {
scope: bucket,
};
const putPolicy = new QINIU.rs.PutPolicy(options);
const uploadToken = putPolicy.uploadToken(this.client);
return new Promise((resolve, reject) => {
resolve({
uploadUrl,
publicDomain,
token: uploadToken,
fileKey,
});
});
}
/**
* OSS 文件上传
* @param ctx
*/
private async oss(ctx) {
const {
accessKeyId,
accessKeySecret,
bucket,
endpoint,
expAfter = 300000,
maxSize = 200 * 1024 * 1024,
} = this.config.oss;
const oss = {
bucket,
region: endpoint.split('.')[0], // 我的是 hangzhou
accessKeyId,
accessKeySecret,
expAfter, // 签名失效时间,毫秒
maxSize, // 文件最大的 size
};
const host = `https://${bucket}.${endpoint}`;
const expireTime = new Date().getTime() + oss.expAfter;
const expiration = new Date(expireTime).toISOString();
const policyString = JSON.stringify({
expiration,
conditions: [
['content-length-range', 0, oss.maxSize], // 设置上传文件的大小限制,200mb
],
});
const policy = Buffer.from(policyString).toString('base64');
const signature = crypto
.createHmac('sha1', oss.accessKeySecret)
.update(policy)
.digest('base64');
return {
signature,
policy,
host,
OSSAccessKeyId: accessKeyId,
success_action_status: 200,
};
}
/**
* COS 文件上传
* @param ctx
*/
private async cos(ctx) {
const {
accessKeyId,
accessKeySecret,
bucket,
region,
publicDomain,
durationSeconds = 1800,
allowPrefix = '_ALLOW_DIR_/*',
allowActions = [
// 所有 action 请看文档 https://cloud.tencent.com/document/product/436/31923
// 简单上传
'name/cos:PutObject',
'name/cos:PostObject',
// 分片上传
'name/cos:InitiateMultipartUpload',
'name/cos:ListMultipartUploads',
'name/cos:ListParts',
'name/cos:UploadPart',
'name/cos:CompleteMultipartUpload',
],
} = this.config.cos;
// 配置参数
let config = {
secretId: accessKeyId,
secretKey: accessKeySecret,
durationSeconds,
bucket: bucket,
region: region,
// 允许操作(上传)的对象前缀,可以根据自己网站的用户登录态判断允许上传的目录,例子: user1/* 或者 * 或者a.jpg
// 请注意当使用 * 时,可能存在安全风险,详情请参阅:https://cloud.tencent.com/document/product/436/40265
allowPrefix,
// 密钥的权限列表
allowActions,
};
// 获取临时密钥
let LongBucketName = config.bucket;
let ShortBucketName = LongBucketName.substring(
0,
LongBucketName.lastIndexOf('-')
);
let AppId = LongBucketName.substring(LongBucketName.lastIndexOf('-') + 1);
let policy = {
version: '2.0',
statement: [
{
action: config.allowActions,
effect: 'allow',
resource: [
'qcs::cos:' +
config.region +
':uid/' +
AppId +
':prefix//' +
AppId +
'/' +
ShortBucketName +
'/' +
config.allowPrefix,
],
},
],
};
return new Promise((resolve, reject) => {
STS.getCredential(
{
secretId: config.secretId,
secretKey: config.secretKey,
durationSeconds: config.durationSeconds,
policy: policy,
},
(err, tempKeys) => {
if (err) {
reject(err);
}
if (tempKeys) {
tempKeys.startTime = Math.round(Date.now() / 1000);
}
resolve({
...tempKeys,
url: publicDomain,
});
}
);
});
}
/**
* 本地上传
* @param ctx
* @returns
*/
private async local(ctx) {
try {
const { key } = ctx.fields;
if (_.isEmpty(ctx.files)) {
throw new CoolCommException('上传文件为空');
}
const file = ctx.files[0];
const extension = file.filename.split('.').pop();
const name =
moment().format('YYYYMMDD') + '/' + (key || `${uuid()}.${extension}`);
const target = path.join(
this.app.getBaseDir(),
'..',
`public/uploads/${name}`
);
const dirPath = path.join(
this.app.getBaseDir(),
'..',
`public/uploads/${moment().format('YYYYMMDD')}`
);
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath);
}
const data = fs.readFileSync(file.data);
fs.writeFileSync(target, data);
return this.config.domain + '/public/uploads/' + name;
} catch (err) {
this.coreLogger.error(err);
throw new CoolCommException('上传失败');
}
}
}

View File

@ -0,0 +1,5 @@
export { CoolFileConfiguration as Configuration } from './configuration';
export * from './interface';
export * from './file';

View File

@ -0,0 +1,103 @@
// 模式
export enum MODETYPE {
// 本地
LOCAL = 'local',
// 云存储
CLOUD = 'cloud',
// 其他
OTHER = 'other',
}
export enum CLOUDTYPE {
// 阿里云存储
OSS = 'oss',
// 腾讯云存储
COS = 'cos',
// 七牛云存储
QINIU = 'qiniu',
}
/**
* 上传模式
*/
export interface Mode {
// 模式
mode: MODETYPE;
// 类型
type: string;
}
/**
* 模块配置
*/
export interface CoolFileConfig {
// 上传模式
mode: MODETYPE;
// 阿里云oss 配置
oss: OSSConfig;
// 腾讯云 cos配置
cos: COSConfig;
// 七牛云 配置
qiniu: QINIUConfig;
// 文件前缀
domain: string;
}
/**
* OSS 配置
*/
export interface OSSConfig {
// 阿里云accessKeyId
accessKeyId: string;
// 阿里云accessKeySecret
accessKeySecret: string;
// 阿里云oss的bucket
bucket: string;
// 阿里云oss的endpoint
endpoint: string;
// 阿里云oss的timeout
timeout: string;
// 签名失效时间,毫秒
expAfter?: number;
// 文件最大的 size
maxSize?: number;
}
/**
* COS 配置
*/
export interface COSConfig {
// 腾讯云accessKeyId
accessKeyId: string;
// 腾讯云accessKeySecret
accessKeySecret: string;
// 腾讯云cos的bucket
bucket: string;
// 腾讯云cos的区域
region: string;
// 腾讯云cos的公网访问地址
publicDomain: string;
// 上传持续时间
durationSeconds?: number;
// 允许操作(上传)的对象前缀
allowPrefix?: string;
// 密钥的权限列表
allowActions?: string[];
}
export interface QINIUConfig {
// 七牛云accessKeyId
accessKeyId: string;
// 七牛云accessKeySecret
accessKeySecret: string;
// 七牛云cos的bucket
bucket: string;
// 七牛云cos的区域
region: string;
// 七牛云cos的公网访问地址
publicDomain: string;
// 上传地址
uploadUrl?: string;
// 上传fileKey
fileKey?: string;
}

View File

@ -0,0 +1,53 @@
{
"name": "@cool-midway/file",
"version": "6.0.1",
"description": "",
"main": "index.js",
"typings": "index.d.ts",
"scripts": {
"build": "cross-env midway-bin build -c",
"test": "cross-env midway-bin test --ts",
"cov": "cross-env midway-bin cov --ts",
"lint": "mwts check",
"lint:fix": "mwts fix"
},
"keywords": [
"cool",
"cool-admin",
"cooljs"
],
"author": "COOL",
"files": [
"**/*.js",
"**/*.d.ts",
"index.d.ts"
],
"license": "MIT",
"repository": {
"type": "git",
"url": "https://cool-js.com"
},
"devDependencies": {
"@cool-midway/core": "^6.0.0",
"@midwayjs/cli": "^2.0.9",
"@midwayjs/core": "^3.9.0",
"@midwayjs/decorator": "^3.9.0",
"@midwayjs/mock": "^3.9.0",
"@types/jest": "^29.2.5",
"@types/node": "^18.11.18",
"cross-env": "^7.0.3",
"jest": "^29.3.1",
"lodash": "^4.17.21",
"mwts": "^1.3.0",
"ts-jest": "^29.0.3",
"typescript": "^4.9.4"
},
"dependencies": {
"@midwayjs/upload": "^3.9.1",
"ali-oss": "^6.17.1",
"cos-nodejs-sdk-v5": "^2.11.19",
"download": "^8.0.0",
"qcloud-cos-sts": "^3.1.0",
"qiniu": "^7.8.0"
}
}

View File

@ -0,0 +1,14 @@
import { createLightApp } from '@midwayjs/mock';
import * as custom from '../src';
describe('/test/index.test.ts', () => {
it('test component', async () => {
const app = await createLightApp('', {
imports: [
custom
]
});
const bookService = await app.getApplicationContext().getAsync(custom.BookService);
expect(await bookService.getBookById()).toEqual('hello world');
});
});

View File

@ -0,0 +1,25 @@
{
"compileOnSave": true,
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"moduleResolution": "node",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"inlineSourceMap":false,
"noImplicitThis": true,
"noUnusedLocals": true,
"stripInternal": true,
"skipLibCheck": true,
"noImplicitReturns": false,
"pretty": true,
"declaration": true,
"forceConsistentCasingInFileNames": true,
"outDir": "dist"
},
"exclude": [
"dist",
"node_modules",
"test"
]
}

View File

@ -0,0 +1,11 @@
# 🎨 editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true

Some files were not shown because too many files have changed in this diff Show More