From ed9977cab89851479a13b91dfba749670046fa01 Mon Sep 17 00:00:00 2001 From: cebgcontract <99630598+cebgcontract@users.noreply.github.com> Date: Tue, 18 Oct 2022 18:55:25 +0800 Subject: [PATCH] add some code --- src/modules/Base.ts | 201 ++++++++++++++++++++++++++++++++++++++ src/utils/net.util.ts | 116 ++++++++++++++++++++++ src/utils/promise.util.ts | 47 +++++++++ src/utils/string.util.ts | 106 ++++++++++++++++++++ yarn.lock | 58 ++++++++++- 5 files changed, 527 insertions(+), 1 deletion(-) create mode 100644 src/modules/Base.ts create mode 100644 src/utils/net.util.ts create mode 100644 src/utils/promise.util.ts create mode 100644 src/utils/string.util.ts diff --git a/src/modules/Base.ts b/src/modules/Base.ts new file mode 100644 index 0000000..71aee4d --- /dev/null +++ b/src/modules/Base.ts @@ -0,0 +1,201 @@ +import { FindOrCreate } from '@typegoose/typegoose/lib/defaultClasses' +import { checkJson } from '../decorators/nojson' +import { plugin, ReturnModelType } from '@typegoose/typegoose' + +// @ts-ignore +import findOrCreate from 'mongoose-findorcreate' +import { Connection } from 'mongoose' +import { ObjectId } from 'bson' +import { isTrue } from '../utils/string.util' +import { AnyParamConstructor } from '@typegoose/typegoose/lib/types' + +const jsonExcludeKeys = ['updatedAt', '__v'] +const saveExcludeKeys = ['createdAt', 'updatedAt', '__v', '_id'] + +@plugin(findOrCreate) +export abstract class BaseModule extends FindOrCreate { + static db: Connection + + public updateFromReq(data: any) { + for (let key in data) { + if (saveExcludeKeys.indexOf(key) == -1) { + this[key] = data[key] + } + } + } + + /** + * 插入或更新 + * @param condition + * @param data + */ + public static insertOrUpdate( + this: ReturnModelType>, + condition: any, + data: any, + ) { + return this.findOneAndUpdate(condition, data, { upsert: true, new: true, setDefaultsOnInsert: true }) + } + + /** + * 虚拟删除 + * @param {string[]} ids + */ + public static deleteVirtual(this: ReturnModelType>, ids: string[]) { + return this.updateMany( + // @ts-ignore + { + _id: { $in: ids }, + }, + { + $set: { + deleted: true, + deleteTime: new Date(), + }, + }, + ) + } + + /** + * 自定义分页查询 + * @param data + * @param {boolean} json + */ + public static async pageQuery( + this: ReturnModelType>, + data: any, + json: boolean = false, + ) { + let { start, limit, page } = data + limit = +limit || 10 + start = +start || (+page - 1) * limit || 0 + // @ts-ignore + let { opt, sort } = this.parseQueryParam(data) + let records = await this.find(opt).sort(sort).skip(start).limit(limit) + let total = await this.countDocuments(opt) + if (json) { + records.map((o: T) => o.toJson()) + } + return { records, total, start, limit } + } + + public toJson() { + let result: any = {} + // @ts-ignore + for (let key in this._doc) { + if (checkJson(this, key + '') && jsonExcludeKeys.indexOf(key) == -1) { + result[key] = this[key] + } + } + return result + } + + /** + * 通用的查询条件拼接方法 + * @param {{}} params req.params + * @param options + * sort: 排序 比如: {createdAt: 1} 默认是 {_id: 1} + * opt: 设置一些特殊的过滤条件, 比如{deleted: 0} + * timeKey: 如果需要查询创建时间, 而且创建时间不为 createdAt, 可以用此字段设置 + * matchKey: 指定关键字查询的匹配字段, 可为string或[string] + * + * @return {{opt: any, sort: {_id: number}}} + */ + public static parseQueryParam(params: {}, options?: any) { + const opt: any = { deleted: false } + // @ts-ignore + let obj = this.schema.paths + for (let key in params) { + if (key !== 'sort' && obj.hasOwnProperty(key)) { + switch (obj[key].instance) { + case 'String': + opt[key] = { $regex: params[key], $options: 'i' } + break + case 'Number': + opt[key] = params[key] + break + case 'Array': + if (Array.isArray(params[key])) { + opt[key] = { $in: params[key] } + } else { + opt[key] = params[key] + } + break + case 'Date': + // TODO: + break + case 'Boolean': + opt[key] = isTrue(params[key]) + break + case 'ObjectID': + if (/^[0-9a-fA-F]{24}$/.test(params[key])) { + opt[key] = new ObjectId(params[key]) + } + break + } + } + } + if (params.hasOwnProperty('key') && params['key']) { + let orArr = [] + if (options?.matchKey) { + if (Array.isArray(options?.matchKey)) { + for (let key in options?.matchKey) { + let _tmp = {} + _tmp[key] = { $regex: params['key'], $options: 'i' } + orArr.push(_tmp) + } + } else { + let _tmp = {} + _tmp[options.matchKey] = { $regex: params['key'], $options: 'i' } + orArr.push(_tmp) + } + } else { + for (let key in obj) { + if (obj[key].instance === 'String') { + let _tmp = {} + _tmp[key] = { $regex: params['key'], $options: 'i' } + orArr.push(_tmp) + } + } + } + + Object.assign(opt, { $or: orArr }) + } + let timeKey = options?.timeKey ? options.timeKey : 'createdAt' + if (params.hasOwnProperty('timeBegin') && !params.hasOwnProperty('timeEnd')) { + let timeBegin = params['timeBegin'] + if (!(timeBegin instanceof Date)) { + timeBegin = new Date(parseInt(timeBegin)) + } + opt[timeKey] = { $gte: timeBegin } + } else if (params.hasOwnProperty('timeBegin') && params.hasOwnProperty('timeEnd')) { + let timeBegin = params['timeBegin'] + if (!(timeBegin instanceof Date)) { + timeBegin = new Date(parseInt(timeBegin)) + } + let timeEnd = params['timeEnd'] + if (!(timeEnd instanceof Date)) { + timeEnd = new Date(parseInt(timeEnd)) + } + let tmpB = {} + tmpB[timeKey] = { $gte: timeBegin } + let tmpE = {} + tmpE[timeKey] = { $lte: timeEnd } + opt['$and'] = [tmpB, tmpE] + } else if (!params.hasOwnProperty('timeBegin') && params.hasOwnProperty('timeEnd')) { + let timeEnd = params['timeEnd'] + if (!(timeEnd instanceof Date)) { + timeEnd = new Date(parseInt(timeEnd)) + } + opt[timeKey] = { $lte: timeEnd } + } + if (options?.opt) { + Object.assign(opt, options.opt) + } + let sort = { _id: 1 } + if (params.hasOwnProperty('sort')) { + sort = params['sort'] + } + return { opt, sort } + } +} diff --git a/src/utils/net.util.ts b/src/utils/net.util.ts new file mode 100644 index 0000000..44d556c --- /dev/null +++ b/src/utils/net.util.ts @@ -0,0 +1,116 @@ +const TIMEOUT_ERROR = new Error('timeout') + +const hexRe = /^[0-9A-Fa-f]+$/gu + +/** + * Execute fetch and verify that the response was successful. + * + * @param request - Request information. + * @param options - Fetch options. + * @returns The fetch response. + */ +export async function successfulFetch(request: string, options?: RequestInit) { + const response = await fetch(request, options) + if (!response.ok) { + throw new Error(`Fetch failed with status '${response.status}' for request '${request}'`) + } + return response +} + +/** + * Execute fetch and return object response. + * + * @param request - The request information. + * @param options - The fetch options. + * @returns The fetch response JSON data. + */ +export async function handleFetch(request: string, options?: RequestInit) { + const response = await successfulFetch(request, options) + const object = await response.json() + return object +} + +/** + * Execute fetch and return object response, log if known error thrown, otherwise rethrow error. + * + * @param request - the request options object + * @param request.url - The request url to query. + * @param request.options - The fetch options. + * @param request.timeout - Timeout to fail request + * @param request.errorCodesToCatch - array of error codes for errors we want to catch in a particular context + * @returns The fetch response JSON data or undefined (if error occurs). + */ +export async function fetchWithErrorHandling({ + url, + options, + timeout, + errorCodesToCatch, +}: { + url: string + options?: RequestInit + timeout?: number + errorCodesToCatch?: number[] +}) { + let result + try { + if (timeout) { + result = Promise.race([ + await handleFetch(url, options), + new Promise((_, reject) => + setTimeout(() => { + reject(TIMEOUT_ERROR) + }, timeout), + ), + ]) + } else { + result = await handleFetch(url, options) + } + } catch (e) { + logOrRethrowError(e, errorCodesToCatch) + } + return result +} + +/** + * Fetch that fails after timeout. + * + * @param url - Url to fetch. + * @param options - Options to send with the request. + * @param timeout - Timeout to fail request. + * @returns Promise resolving the request. + */ +export async function timeoutFetch(url: string, options?: RequestInit, timeout = 500): Promise { + return Promise.race([ + successfulFetch(url, options), + new Promise((_, reject) => + setTimeout(() => { + reject(TIMEOUT_ERROR) + }, timeout), + ), + ]) +} + +/** + * Utility method to log if error is a common fetch error and otherwise rethrow it. + * + * @param error - Caught error that we should either rethrow or log to console + * @param codesToCatch - array of error codes for errors we want to catch and log in a particular context + */ +function logOrRethrowError(error: any, codesToCatch: number[] = []) { + if (!error) { + return + } + + const includesErrorCodeToCatch = codesToCatch.some(code => + error.message.includes(`Fetch failed with status '${code}'`), + ) + + if ( + error instanceof Error && + (includesErrorCodeToCatch || error.message.includes('Failed to fetch') || error === TIMEOUT_ERROR) + ) { + console.error(error) + } else { + throw error + } +} diff --git a/src/utils/promise.util.ts b/src/utils/promise.util.ts new file mode 100644 index 0000000..1cb6ebe --- /dev/null +++ b/src/utils/promise.util.ts @@ -0,0 +1,47 @@ +/** + * + * @param {Function} cb + * @param {number} maxRetries + * @param {any[]} errorWhiteList + * @param {number} retries + * @return {Promise} + */ +export function retry(cb: Function, maxRetries: number = 3, errorWhiteList: any[] = [], retries: number = 0) { + return new Promise((resolve, reject) => { + cb() + .then(resolve) + .catch(e => { + if (errorWhiteList.indexOf(e.constructor) !== -1 && retries++ < maxRetries) { + setTimeout(() => { + retry(cb, maxRetries, errorWhiteList, retries) + .then(resolve) + .catch(e2 => reject(e2)) + }, Math.floor(Math.random() * Math.pow(2, retries) * 400)) + } else { + reject(e) + } + }) + }) +} + +export class Deferred { + public promise: Promise + + public resolve: Function + public reject: Function + + constructor() { + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve + this.reject = reject + }) + } + + public then(func: (value: T) => any) { + return this.promise.then.apply(this.promise, arguments) + } + + public catch(func: (value: any) => any) { + return this.promise.catch(func) + } +} diff --git a/src/utils/string.util.ts b/src/utils/string.util.ts new file mode 100644 index 0000000..62dba1b --- /dev/null +++ b/src/utils/string.util.ts @@ -0,0 +1,106 @@ +/** + * 根据key升序生成 key1=val1&key2=val2的字符串 + * @param {object} data 需要处理的对象 + * @param {boolean} ignoreNull 是否过滤空值(空格或者null值不参与拼接) + * @param splitChar 连接的字符, 默认是& + * @param equalChar = + */ +export function generateKeyValStr(data: {}, ignoreNull = true, splitChar: string = '&', equalChar = '=') { + const keys = Object.keys(data) + keys.sort() + let result = '' + let i = 0 + for (let key of keys) { + if (ignoreNull && !data[key]) { + return + } + if (i++ > 0) result += splitChar + result += `${key}${equalChar}${data[key]}` + } + return result +} + +/** + * 将key1=val&key2=val的字符串组装成对象 + * @param str key1=val&key2=val的字符串 + * @param splitChar 连接的字符, 默认是& + * @param equalChar = + */ +export function keyValToObject(str: string, splitChar: string = '&', equalChar = '='): {} { + let result = {} + if (!str) { + return result + } + let arrs = str.split(splitChar) + for (let sub of arrs) { + let subArr = sub.split(equalChar) + result[subArr[0]] = subArr[1] + } + return result +} + +/** + * 判断传入的值是否为true + * @param {Object} obj 传入值为'true','TRUE',1,'1','on','ON','YES','yes'时,返回true,其他值均返回false + * @return {boolean} + */ +export function isTrue(obj) { + return ( + obj === 'true' || + obj === 'TRUE' || + obj === 'True' || + obj === 'on' || + obj === 'ON' || + obj === true || + obj === 1 || + obj === '1' || + obj === 'YES' || + obj === 'yes' + ) +} + +/** + * 验证ObjectId格式是否正确 + * @param {string} id + * @return {boolean} + */ +export function isObjectId(id: string): boolean { + //mongoose.Types.ObjectId.isValid(id) + return /^[a-fA-F0-9]{24}$/.test(id) +} + +/** + * 10进制 -> 62进制 + * @param {string | number} number + * @return {string} + */ +export function string10to62(number: string | number) { + const chars = '0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ'.split('') + const radix = chars.length + let qutient = +number + const arr = [] + do { + const mod = qutient % radix + qutient = (qutient - mod) / radix + arr.unshift(chars[mod]) + } while (qutient) + return arr.join('') +} + +/** + * 62进制 -> 10 进制 + * @param {string} numberCode + * @return {number} + */ +export function string62to10(numberCode: string) { + const chars = '0123456789abcdefghigklmnopqrstuvwxyzABCDEFGHIGKLMNOPQRSTUVWXYZ' + const radix = chars.length + numberCode = numberCode + '' + const len = numberCode.length + let i = 0 + let originNumber = 0 + while (i < len) { + originNumber += Math.pow(radix, i++) * (chars.indexOf(numberCode.charAt(len - i)) || 0) + } + return originNumber +} diff --git a/yarn.lock b/yarn.lock index fe4daf6..a065259 100644 --- a/yarn.lock +++ b/yarn.lock @@ -148,6 +148,40 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@redis/bloom@1.0.2": + version "1.0.2" + resolved "https://registry.npmmirror.com/@redis/bloom/-/bloom-1.0.2.tgz#42b82ec399a92db05e29fffcdfd9235a5fc15cdf" + integrity sha512-EBw7Ag1hPgFzdznK2PBblc1kdlj5B5Cw3XwI9/oG7tSn85/HKy3X9xHy/8tm/eNXJYHLXHJL/pkwBpFMVVefkw== + +"@redis/client@1.3.0": + version "1.3.0" + resolved "https://registry.npmmirror.com/@redis/client/-/client-1.3.0.tgz#c62ccd707f16370a2dc2f9e158a28b7da049fa77" + integrity sha512-XCFV60nloXAefDsPnYMjHGtvbtHR8fV5Om8cQ0JYqTNbWcQo/4AryzJ2luRj4blveWazRK/j40gES8M7Cp6cfQ== + dependencies: + cluster-key-slot "1.1.0" + generic-pool "3.8.2" + yallist "4.0.0" + +"@redis/graph@1.0.1": + version "1.0.1" + resolved "https://registry.npmmirror.com/@redis/graph/-/graph-1.0.1.tgz#eabc58ba99cd70d0c907169c02b55497e4ec8a99" + integrity sha512-oDE4myMCJOCVKYMygEMWuriBgqlS5FqdWerikMoJxzmmTUErnTRRgmIDa2VcgytACZMFqpAOWDzops4DOlnkfQ== + +"@redis/json@1.0.4": + version "1.0.4" + resolved "https://registry.npmmirror.com/@redis/json/-/json-1.0.4.tgz#f372b5f93324e6ffb7f16aadcbcb4e5c3d39bda1" + integrity sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw== + +"@redis/search@1.1.0": + version "1.1.0" + resolved "https://registry.npmmirror.com/@redis/search/-/search-1.1.0.tgz#7abb18d431f27ceafe6bcb4dd83a3fa67e9ab4df" + integrity sha512-NyFZEVnxIJEybpy+YskjgOJRNsfTYqaPbK/Buv6W2kmFNaRk85JiqjJZA5QkRmWvGbyQYwoO5QfDi2wHskKrQQ== + +"@redis/time-series@1.0.3": + version "1.0.3" + resolved "https://registry.npmmirror.com/@redis/time-series/-/time-series-1.0.3.tgz#4cfca8e564228c0bddcdf4418cba60c20b224ac4" + integrity sha512-OFp0q4SGrTH0Mruf6oFsHGea58u8vS/iI5+NpYdicaM+7BgqBZH8FFvNZ8rYYLrUO/QRqMq72NpXmxLVNcdmjA== + "@tsconfig/node10@^1.0.7": version "1.0.9" resolved "https://registry.npmmirror.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2" @@ -529,6 +563,11 @@ chokidar@^3.5.1: optionalDependencies: fsevents "~2.3.2" +cluster-key-slot@1.1.0: + version "1.1.0" + resolved "https://registry.npmmirror.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d" + integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw== + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -991,6 +1030,11 @@ function-bind@^1.1.1: resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== +generic-pool@3.8.2: + version "3.8.2" + resolved "https://registry.npmmirror.com/generic-pool/-/generic-pool-3.8.2.tgz#aab4f280adb522fdfbdc5e5b64d718d3683f04e9" + integrity sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg== + glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -1551,6 +1595,18 @@ real-require@^0.2.0: resolved "https://registry.npmmirror.com/real-require/-/real-require-0.2.0.tgz#209632dea1810be2ae063a6ac084fee7e33fba78" integrity sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg== +redis@^4.3.1: + version "4.3.1" + resolved "https://registry.npmmirror.com/redis/-/redis-4.3.1.tgz#290532a0c22221e05e991162ac4dca1e1b2ff6da" + integrity sha512-cM7yFU5CA6zyCF7N/+SSTcSJQSRMEKN0k0Whhu6J7n9mmXRoXugfWDBo5iOzGwABmsWKSwGPTU5J4Bxbl+0mrA== + dependencies: + "@redis/bloom" "1.0.2" + "@redis/client" "1.3.0" + "@redis/graph" "1.0.1" + "@redis/json" "1.0.4" + "@redis/search" "1.1.0" + "@redis/time-series" "1.0.3" + reflect-metadata@^0.1.13: version "0.1.13" resolved "https://registry.npmmirror.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" @@ -1960,7 +2016,7 @@ xtend@^4.0.0, xtend@^4.0.2: resolved "https://registry.npmmirror.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== -yallist@^4.0.0: +yallist@4.0.0, yallist@^4.0.0: version "4.0.0" resolved "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==