add base code

This commit is contained in:
CounterFire2023 2024-01-19 13:54:32 +08:00
parent 375eba0718
commit 29a9fc2264
17 changed files with 5976 additions and 7 deletions

View File

@ -11,6 +11,11 @@ yarn add file:packages/mongobase
# or
yarn add link:packages/mongobase
```
db的配置, 请在环境变量中配置
```bash
process.env.DB_DBNAME=xxx
```
```typescript

90
dist/index.d.ts vendored Normal file
View File

@ -0,0 +1,90 @@
import * as tracer from 'tracer';
import * as _typegoose_typegoose from '@typegoose/typegoose';
import { ReturnModelType } from '@typegoose/typegoose';
import * as _typegoose_typegoose_lib_types from '@typegoose/typegoose/lib/types';
import { AnyParamConstructor } from '@typegoose/typegoose/lib/types';
import * as mongoose from 'mongoose';
import { Connection } from 'mongoose';
/**
* model指定数据库连接
* @param {string} name config中必须要有对应的配置 main db_main
* */
declare function dbconn(name?: string): (target: any) => void;
declare class NoJsonClass {
private noJsonPropSet;
addKey(className: string, propertyKey: string): void;
checkExist(className: string, propertyKey: string): boolean;
}
/**
* toJson方法输出的字段上加上 @noJson
* @return {{(target: Function): void, (target: Object, propertyKey: (string | symbol)): void}}
*/
declare function noJson(): (target: Object, propertyKey: string) => void;
declare function checkJson(target: any, propertyKey: string): boolean;
declare const logger: tracer.Tracer.Logger<string>;
declare abstract class BaseModule {
static db: Connection;
updateFromReq(data: any): void;
/**
*
* @param condition
* @param data
*/
static insertOrUpdate<T extends BaseModule>(this: ReturnModelType<AnyParamConstructor<T>>, condition: any, data: any): mongoose.QueryWithHelpers<mongoose.IfAny<T, any, mongoose.Document<unknown, _typegoose_typegoose_lib_types.BeAnObject, T> & Omit<mongoose.Require_id<T>, "typegooseName"> & _typegoose_typegoose_lib_types.IObjectWithTypegooseFunction>, mongoose.IfAny<T, any, mongoose.Document<unknown, _typegoose_typegoose_lib_types.BeAnObject, T> & Omit<mongoose.Require_id<T>, "typegooseName"> & _typegoose_typegoose_lib_types.IObjectWithTypegooseFunction>, _typegoose_typegoose_lib_types.BeAnObject, T, "findOneAndUpdate">;
/**
*
* @param {string[]} ids
*/
static deleteVirtual<T extends BaseModule>(this: ReturnModelType<AnyParamConstructor<T>>, ids: string[]): mongoose.QueryWithHelpers<mongoose.UpdateWriteOpResult, mongoose.IfAny<T, any, mongoose.Document<unknown, _typegoose_typegoose_lib_types.BeAnObject, T> & Omit<mongoose.Require_id<T>, "typegooseName"> & _typegoose_typegoose_lib_types.IObjectWithTypegooseFunction>, _typegoose_typegoose_lib_types.BeAnObject, T, "updateMany">;
/**
*
* @param data
* @param {boolean} json
*/
static pageQuery<T extends BaseModule>(this: ReturnModelType<AnyParamConstructor<T>>, data: any, json?: boolean): Promise<{
records: mongoose.IfAny<T, any, mongoose.Document<unknown, _typegoose_typegoose_lib_types.BeAnObject, T> & Omit<mongoose.Require_id<T>, "typegooseName"> & _typegoose_typegoose_lib_types.IObjectWithTypegooseFunction>[];
total: number;
start: any;
limit: any;
}>;
toJson(): any;
/**
*
* @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}}}
*/
static parseQueryParam(params: {}, options?: any): {
opt: any;
sort: {
_id: number;
};
};
getTimestampOfID(): any;
}
/**
*
*/
declare class UserLogClass extends BaseModule {
user: string;
name: string;
method: string;
path: string;
referer: string;
user_agent: string;
ip: string;
params: any;
}
declare const UserLog: _typegoose_typegoose.ReturnModelType<typeof UserLogClass, _typegoose_typegoose_lib_types.BeAnObject>;
export { BaseModule, NoJsonClass, UserLog, checkJson, dbconn, logger, noJson };

4040
dist/index.js vendored Normal file

File diff suppressed because it is too large Load Diff

1
dist/index.js.map vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -2,13 +2,28 @@
"name": "mongobase",
"version": "1.0.0",
"description": "",
"main": "index.js",
"main": "./dist/index.js",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"build": "tsup",
"lint": "eslint --ext .ts src/**",
"format": "eslint --ext .ts src/** --fix"
},
"author": "zhl",
"license": "ISC",
"dependencies": {
"@typegoose/typegoose": "^12.1.0",
"mongoose": "^8.1.0",
"tracer": "^1.3.0"
},
"devDependencies": {
"@types/node": "16",
"@typescript-eslint/eslint-plugin": "^6.19.0",
"@typescript-eslint/parser": "^6.19.0",
"eslint-config-prettier": "^9.1.0",

107
src/common/AsyncQueue.ts Normal file
View File

@ -0,0 +1,107 @@
type Callback<T> = () => Promise<T>
export type AsyncQueue<T = void> = {
push: (task: Callback<T>) => Promise<T>
flush: () => Promise<void>
size: number
}
/**
* Ensures that each callback pushed onto the queue is executed in series.
* Such a quetie 😻
* @param opts.dedupeConcurrent If dedupeConcurrent is `true` it ensures that if multiple
* tasks are pushed onto the queue while there is an active task, only the
* last one will be executed, once the active task has completed.
* e.g. in the below example, only 0 and 3 will be executed.
* ```
* const queue = createAsyncQueue({ dedupeConcurrent: true })
* queue.push(async () => console.log(0)) // returns 0
* queue.push(async () => console.log(1)) // returns 3
* queue.push(async () => console.log(2)) // returns 3
* queue.push(async () => console.log(3)) // returns 3
* ```
* */
export function createAsyncQueue<T = void>(opts = { dedupeConcurrent: false }): AsyncQueue<T> {
const { dedupeConcurrent } = opts
let queue: Callback<T>[] = []
let running: Promise<void> | undefined
let nextPromise = new DeferredPromise<T>()
const push = (task: Callback<T>) => {
let taskPromise = new DeferredPromise<T>()
if (dedupeConcurrent) {
queue = []
if (nextPromise.started) nextPromise = new DeferredPromise<T>()
taskPromise = nextPromise
}
queue.push(() => {
taskPromise.started = true
task().then(taskPromise.resolve).catch(taskPromise.reject)
return taskPromise.promise
})
if (!running) running = start()
return taskPromise.promise
}
const start = async () => {
while (queue.length) {
const task = queue.shift()!
await task().catch(() => {})
}
running = undefined
}
return {
push,
flush: () => running || Promise.resolve(),
get size() {
return queue.length
},
}
}
export const createAsyncQueues = <T = void>(opts = { dedupeConcurrent: false }) => {
const queues: { [queueId: string]: AsyncQueue<T> } = {}
const push = (queueId: string, task: Callback<T>) => {
if (!queues[queueId]) queues[queueId] = createAsyncQueue<T>(opts)
return queues[queueId].push(task)
}
const flush = (queueId: string) => {
if (!queues[queueId]) queues[queueId] = createAsyncQueue<T>(opts)
return queues[queueId].flush()
}
return { push, flush }
}
class DeferredPromise<T = void, E = any> {
started = false
resolve: (x: T | PromiseLike<T>) => void = () => {}
reject: (x: E) => void = () => {}
promise: Promise<T>
constructor() {
this.promise = new Promise<T>((res, rej) => {
this.resolve = res
this.reject = rej
})
}
}
// function main() {
// const queue = createAsyncQueue()
// queue.push(async () => {
// console.log(0)
// }) // returns 0
// queue.push(async () => {
// console.log(1)
// return new Promise((resolve, reject) => {
// setTimeout(() => {
// console.log('12')
// resolve()
// }, 1000)
// })
// }) // returns 3
// queue.push(async () => console.log(2)) // returns 3
// queue.push(async () => console.log(3)) // returns 3
// console.log('hi')
// }
// main()

14
src/decorators/dbconn.ts Normal file
View File

@ -0,0 +1,14 @@
import { mongoose } from '@typegoose/typegoose'
/**
* model指定数据库连接
* @param {string} name config中必须要有对应的配置 main db_main
* */
export function dbconn(name?: string) {
return target => {
name = name || 'main'
const dbName = ('db_' + name).toUpperCase()
const url = process.env[dbName]
target['db'] = mongoose.createConnection(url, {})
}
}

30
src/decorators/nojson.ts Normal file
View File

@ -0,0 +1,30 @@
import 'reflect-metadata'
import { singleton } from './singleton'
@singleton
export class NoJsonClass {
private noJsonPropSet: Set<string> = new Set()
public addKey(className: string, propertyKey: string) {
this.noJsonPropSet.add(className + '_' + propertyKey)
}
public checkExist(className: string, propertyKey: string) {
return this.noJsonPropSet.has(className + '_' + propertyKey)
}
}
/**
* toJson方法输出的字段上加上 @noJson
* @return {{(target: Function): void, (target: Object, propertyKey: (string | symbol)): void}}
*/
export function noJson() {
// return Reflect.metadata(noJsonMetadataKey, !0)
return function (target: Object, propertyKey: string) {
new NoJsonClass().addKey(target.constructor.name, propertyKey)
}
}
export function checkJson(target: any, propertyKey: string) {
return !new NoJsonClass().checkExist(target.constructor.modelName, propertyKey)
}

View File

@ -0,0 +1,29 @@
/**
* class
* 使:
* @singleton
* class Test {}
* new Test() === new Test() // returns `true`
* 使 decorator
* const TestSingleton = singleton(Test)
* new TestSingleton() === new TestSingleton() //returns 'true'
*/
export const SINGLETON_KEY = Symbol()
export type Singleton<T extends new (...args: any[]) => any> = T & {
[SINGLETON_KEY]: T extends new (...args: any[]) => infer I ? I : never
}
export const singleton = <T extends new (...args: any[]) => any>(classTarget: T) =>
new Proxy(classTarget, {
construct(target: Singleton<T>, argumentsList, newTarget) {
// Skip proxy for children
if (target.prototype !== newTarget.prototype) {
return Reflect.construct(target, argumentsList, newTarget)
}
if (!target[SINGLETON_KEY]) {
target[SINGLETON_KEY] = Reflect.construct(target, argumentsList, newTarget)
}
return target[SINGLETON_KEY]
},
})

5
src/index.ts Normal file
View File

@ -0,0 +1,5 @@
export * from './decorators/dbconn'
export * from './decorators/nojson'
export { logger } from './logger/logger'
export { UserLog } from './models/UserLog'
export { BaseModule } from './models/Base'

19
src/logger/logger.ts Normal file
View File

@ -0,0 +1,19 @@
import { LoggerQueue } from 'queue/logger.queue'
import { colorConsole } from 'tracer'
declare module Tracer {
interface Logger<M extends string> {
db: (name: string, req: any, logObj?: any) => void
}
}
const level = process.env.NODE_ENV === 'production' ? 'info' : 'log'
const _logger = colorConsole({ dateformat: 'yyyy-mm-dd HH:MM:ss.L', level })
Object.assign(_logger, {
db: (name: string, req: any, logObj?: any) => {
logObj = logObj || {}
new LoggerQueue().addLog(name, req, logObj)
}
})
export const logger = _logger

216
src/models/Base.ts Normal file
View File

@ -0,0 +1,216 @@
import { checkJson } from '../decorators/nojson'
import { ReturnModelType } from '@typegoose/typegoose'
import { Connection } from 'mongoose'
import { ObjectId } from 'bson'
import { AnyParamConstructor } from '@typegoose/typegoose/lib/types'
const jsonExcludeKeys = ['updatedAt', '__v']
const saveExcludeKeys = ['createdAt', 'updatedAt', '__v', '_id']
const isTrue = (obj: any): boolean =>{
return (
obj === 'true' ||
obj === 'TRUE' ||
obj === 'True' ||
obj === 'on' ||
obj === 'ON' ||
obj === true ||
obj === 1 ||
obj === '1' ||
obj === 'YES' ||
obj === 'yes'
)
}
export abstract class BaseModule {
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<T extends BaseModule>(
this: ReturnModelType<AnyParamConstructor<T>>,
condition: any,
data: any,
) {
return this.findOneAndUpdate(condition, data, { upsert: true, new: true, setDefaultsOnInsert: true })
}
/**
*
* @param {string[]} ids
*/
public static deleteVirtual<T extends BaseModule>(this: ReturnModelType<AnyParamConstructor<T>>, 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<T extends BaseModule>(
this: ReturnModelType<AnyParamConstructor<T>>,
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 => 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: { $ne: true } }
// @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 }
}
public getTimestampOfID() {
// Extract the timestamp from the ObjectId
// @ts-ignore
return this._id.getTimestamp()
}
}

31
src/models/UserLog.ts Normal file
View File

@ -0,0 +1,31 @@
import { dbconn } from 'decorators/dbconn'
import { getModelForClass, index, modelOptions, mongoose, prop } from '@typegoose/typegoose'
import { Severity } from '@typegoose/typegoose/lib/internal/constants'
import { BaseModule } from './Base'
/**
*
*/
@dbconn()
@index({ user: 1 }, { unique: false })
@index({ name: 1 }, { unique: false })
@modelOptions({ schemaOptions: { collection: 'user_log', timestamps: true }, options: { allowMixed: Severity.ALLOW } })
class UserLogClass extends BaseModule {
@prop()
public user: string
@prop()
public name: string
@prop()
public method: string
@prop()
public path: string
@prop()
public referer: string
@prop()
public user_agent: string
@prop()
public ip: string
@prop({ type: mongoose.Schema.Types.Mixed })
public params: any
}
export const UserLog = getModelForClass(UserLogClass, { existingConnection: UserLogClass['db'] })

39
src/queue/logger.queue.ts Normal file
View File

@ -0,0 +1,39 @@
import { AsyncQueue, createAsyncQueue } from 'common/AsyncQueue'
import { singleton } from 'decorators/singleton'
import { logger } from 'logger/logger'
import { UserLog } from 'models/UserLog'
@singleton
export class LoggerQueue {
private queue: AsyncQueue
constructor() {
this.queue = createAsyncQueue()
}
public addLog(name: string, req: any, logObj: any = {}) {
this.queue.push(async () => {
const user = req.user
const ip = req.headers['x-forwarded-for'] || req.ip
const path = req.url
const params = req.method === 'GET' ? req.query : req.body
const dataObj = JSON.stringify(logObj) === '{}' ? params : logObj
try {
const history = new UserLog({
user: user ? user.id : '',
path: path,
method: req.method,
params: dataObj,
referer: req.headers['referer'],
user_agent: req.headers['user-agent'],
ip,
name,
})
await history.save()
} catch (err) {
logger.error('error add user log: ')
logger.error(err)
}
})
}
}

View File

@ -5,17 +5,17 @@
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"module": "commonjs",
"module": "esnext",
"esModuleInterop": true,
"resolveJsonModule": true,
"target": "es2020",
"target": "es2022",
"moduleResolution": "node",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./src",
"rootDir": "./src"
},
"lib": ["es2020"],
"lib": ["es2021"],
"include": [
"src/**/*.ts",
"typings/extend.d.ts"

10
tsup.config.ts Normal file
View File

@ -0,0 +1,10 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm"], // Build for and ESmodules
dts: true, // Generate declaration file (.d.ts)
splitting: false,
sourcemap: true,
clean: true,
});

1322
yarn.lock

File diff suppressed because it is too large Load Diff