diff --git a/.travis.yml b/.travis.yml index f81505b..cc332d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ install: - "npm install" script: - - "npm test" + - "npm run tsc && npm test" after_success: - npm install coveralls@~3.0.0 --global diff --git a/Dockerfile b/Dockerfile index 9e5abd4..a22e20c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,8 +14,10 @@ RUN npm run tsc && \ rm -rf node_modules && \ npm install --production && \ npm install cross-env tsconfig-paths --save-dev && \ + npm install pm2 --global && \ npm cache clean -f -CMD [ "npm", "run", "start:prod" ] +CMD pm2 start index.js --node-args="-r tsconfig-paths/register" -i 0 --no-daemon +# CMD [ "npm", "run", "start:prod" ] EXPOSE 9000 diff --git a/README.md b/README.md index b28cae3..ff1068c 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,10 @@ Store PandoraBox Firmware Images and its packages -[![Travis](https://img.shields.io/travis/Arylo/StoreBox.svg?style=flat-square)](https://travis-ci.org/Arylo/StoreBox) -[![Coveralls](https://img.shields.io/coveralls/github/Arylo/StoreBox.svg?style=flat-square)](https://coveralls.io/github/Arylo/StoreBox) -[![license](https://img.shields.io/github/license/Arylo/StoreBox.svg?style=flat-square)](https://github.com/Arylo/storebox) +[![Travis](https://img.shields.io/travis/BoxSystem/StoreBox-Api.svg?style=flat-square)](https://travis-ci.org/BoxSystem/StoreBox-Api) +[![Coveralls](https://img.shields.io/coveralls/github/BoxSystem/StoreBox-Api.svg?style=flat-square)](https://coveralls.io/github/BoxSystem/StoreBox-Api) +[![Known Vulnerabilities](https://snyk.io/test/github/BoxSystem/StoreBox-Api/badge.svg?style=flat-square)](https://snyk.io/test/github/BoxSystem/StoreBox-Api) +[![license](https://img.shields.io/github/license/BoxSystem/StoreBox-Api.svg?style=flat-square)](https://github.com/BoxSystem/StoreBox-Api) # Usage diff --git a/TODOLIST.md b/TODOLIST.md new file mode 100644 index 0000000..6260308 --- /dev/null +++ b/TODOLIST.md @@ -0,0 +1,16 @@ +# TODO LIST + +- [x] 用户组 + - [ ] 权限 + - [x] 默认用户组 +- [x] 上传到指定Categroy + - [x] 上传时追加Catogroy +- [x] 整顿collectin info的goods 列表 +- [ ] Token 使用日志显示 +- [ ] Good 下载次数统计 +- [ ] 接入统计 +- [x] 配置文件写入初始化用户账号密码 + - [ ] 接入AuthBox +- [x] Redis 接入 + - [x] 支持Session + - [x] 缓存整理 \ No newline at end of file diff --git a/config/app.default.yaml b/config/app.default.yaml index 7946bfa..d94193e 100644 --- a/config/app.default.yaml +++ b/config/app.default.yaml @@ -1,5 +1,9 @@ server: port: 9000 +redis: + host: 127.0.0.1 + port: 6379 + url: db: type: mongo host: 0.0.0.0 @@ -10,4 +14,10 @@ paths: upload: upload log: log resource: resource - backup: backup \ No newline at end of file + backup: backup +defaults: + user: + name: root + pass: admin + group: + name: admin \ No newline at end of file diff --git a/docker/app.yaml b/docker/app.yaml index 86bad22..73dad1c 100644 --- a/docker/app.yaml +++ b/docker/app.yaml @@ -1,4 +1,6 @@ db: host: "db" +redis: + host: "cache" path: tmp: /tmp \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 83516fe..8782e58 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -5,6 +5,11 @@ db: container_name: storebox_db restart: always +cache: + image: redis + container_name: storebox_cache + restart: always + svr: build: .. ports: @@ -16,5 +21,6 @@ svr: - ./app.yaml:/usr/src/app/config/app.yaml:ro links: - db + - cache container_name: storebox_svr restart: always \ No newline at end of file diff --git a/package.json b/package.json index ebc6842..dfa916c 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,18 @@ { - "name": "storebox", - "version": "1.2.0", + "name": "storebox-api", + "version": "1.3.0", "description": "", "main": "index.js", "scripts": { "clean": "rimraf dist tmp", - "tsc": "tsc -P .", + "tsc": "tsc --locale zh-cn --pretty -P .", "build": "npm run clean && npm run tsc", "prestart": "npm run build", "start-svr": "node -r tsconfig-paths/register index.js", "start:dev": "npm run prestart && cross-env NODE_ENV=development npm run start-svr", "start:prod": "cross-env NODE_ENV=production npm run start-svr", "start": "npm run start:prod", - "test-spec": "cross-env NODE_ENV=test mocha", + "test-spec": "cross-env NODE_ENV=test TS_NODE_PROJECT=./test/tsconfig.json mocha", "test-cov": "nyc npm run test-spec && nyc report", "test": "npm run test-cov", "report-coverage": "cat ./coverage/lcov.info | coveralls" @@ -27,21 +27,25 @@ "author": "AryloYeung ", "license": "MIT", "devDependencies": { - "@nestjs/testing": "^4.5.1", + "@nestjs/testing": "4.6.1", "@types/basic-auth": "^1.1.2", "@types/bunyan": "^1.8.4", + "@types/connect-redis": "0.0.7", "@types/cors": "^2.8.3", "@types/express": "^4.0.39", "@types/express-session": "^1.15.6", "@types/faker": "^4.1.2", "@types/fs-extra": "^4.0.4", "@types/hasha": "^3.0.0", + "@types/helmet": "0.0.37", + "@types/is-promise": "^2.1.0", "@types/lodash": "^4.14.85", "@types/md5": "^2.1.32", "@types/mocha": "^2.2.44", "@types/mocha-steps": "^1.1.0", "@types/mongoose": "^4.7.26", "@types/multer": "^1.3.6", + "@types/node": "^9.3.0", "@types/path-exists": "^3.0.0", "@types/reflect-metadata": "0.0.5", "@types/should": "^11.2.0", @@ -64,20 +68,24 @@ "typescript": "^2.6.1" }, "dependencies": { - "@nestjs/common": "^4.5.1", - "@nestjs/core": "^4.5.1", - "@nestjs/swagger": "^1.1.3", - "@types/node": "^9.3.0", + "@keyv/redis": "^1.3.8", + "@nestjs/common": "4.6.5", + "@nestjs/core": "4.6.5", + "@nestjs/swagger": "1.1.4", "basic-auth": "^2.0.0", "body-parser": "^1.18.2", "bunyan": "^1.8.12", "class-validator": "^0.7.3", + "connect-redis": "^3.3.3", "cookie-parser": "^1.4.3", "cors": "^2.8.4", "express": "^4.16.2", "express-session": "^1.15.6", "fs-extra": "^4.0.2", "hasha": "^3.0.0", + "helmet": "^3.11.0", + "is-promise": "^2.1.0", + "keyv": "^3.0.0", "lodash": "^4.17.4", "md5": "^2.2.1", "md5-file": "^3.2.3", @@ -86,7 +94,6 @@ "path-exists": "^3.0.0", "reflect-metadata": "^0.1.10", "rxjs": "^5.5.2", - "schedule-cache": "^1.0.0", "useragent": "^2.2.1", "uuid": "^3.1.0", "y-config": "^1.1.5" diff --git a/src/express.ts b/src/express.ts index 7800301..3fd5e1d 100644 --- a/src/express.ts +++ b/src/express.ts @@ -1,8 +1,15 @@ import * as express from "express"; -import * as session from "express-session"; +import session = require("express-session"); import * as bodyParser from "body-parser"; import * as cookieParser from "cookie-parser"; +import connectRedis = require("connect-redis"); +import { config } from "@utils/config"; +import helmet = require("helmet"); + import { error } from "./modules/common/middlewares/logger.middleware"; +import { isTest } from "@utils/env"; + +const RedisStore = connectRedis(session); let server: express.Express; @@ -16,15 +23,24 @@ export const initExpress = () => { mServer.enable("trust proxy"); + mServer.use(helmet()); mServer.use(bodyParser.json()); mServer.use(bodyParser.urlencoded()); mServer.use(cookieParser("storebox")); - mServer.use(session({ + const sessionOpts = { + store: undefined, secret: "storebox", resave: false, saveUninitialized: true, cookie: { secure: false, maxAge: 7200 * 1000 } - })); + }; + /* istanbul ignore if */ + if (!isTest) { + sessionOpts.store = new RedisStore({ + url: config.redis.url + }); + } + mServer.use(session(sessionOpts)); mServer.use(error); server = mServer; diff --git a/src/index.ts b/src/index.ts index 54dac33..1c16643 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,13 +4,15 @@ import { config } from "@utils/config"; import { initExpress } from "./express"; import { ApplicationModule } from "./modules/app.module"; import { ValidationPipe } from "@pipes/validation"; -import { systemLogger } from "./modules/common/helper/log"; -import { isDevelopment } from "./modules/common/helper/env"; +import { systemLogger } from "@utils/log"; +import { isDevelopment } from "@utils/env"; const bootstrap = async () => { const server = initExpress(); - const app = await NestFactory.create(ApplicationModule, server); + const app = await NestFactory.create(ApplicationModule, server, { + cors: true + }); app.useGlobalPipes(new ValidationPipe()); if (isDevelopment) { diff --git a/src/models/Categroy.ts b/src/models/Categroy.ts index aeef6b4..7ff92f0 100644 --- a/src/models/Categroy.ts +++ b/src/models/Categroy.ts @@ -5,11 +5,12 @@ import { DEF_PER_COUNT } from "@dtos/page"; import { isArray } from "util"; import { reduce, includes, difference } from "lodash"; import { MongoError } from "mongodb"; -import Cache = require("schedule-cache"); - -const cache = Cache.create(`${Date.now()}${Math.random()}`); +import newCache = require("@utils/newCache"); export const FLAG = "categories"; + +export const cache = newCache(FLAG); + export type CategoryDoc = IDoc; const Definition: SchemaDefinition = { @@ -39,28 +40,6 @@ export interface ICategoryRaw extends ICategory { const CategorySchema = new Base(Definition).createSchema(); -CategorySchema.static("countCategories", async (perNum = 1) => { - const FLAG = `page_count_${perNum}`; - if (cache.get(FLAG)) { - return cache.get(FLAG); - } - const count = Math.ceil((await Model.count({ }).exec()) / perNum); - cache.put(FLAG, count); - return cache.get(FLAG); -}); - -CategorySchema.static("list", (perNum = DEF_PER_COUNT, page = 1) => { - const FLAG_LIST = `list_${perNum}_${page}`; - if (cache.get(FLAG_LIST)) { - return cache.get(FLAG_LIST); - } - const p = Model.find({ }) - .skip((page - 1) * perNum).limit(perNum) - .exec(); - cache.put(FLAG_LIST, p); - return cache.get(FLAG_LIST); -}); - const getIdGroups = (obj): string[] => { const selfIdArr = [ obj._id.toString() ]; if (obj.pid) { @@ -101,52 +80,8 @@ CategorySchema.static("moveCategory", async (id: ObjectId, pid: ObjectId) => { }).exec(); }); -CategorySchema.static("getCategories", async (tags: string | string[] = [ ]) => { - if (!isArray(tags)) { - tags = [ tags ]; - } - if (tags.length === 0) { - return Promise.resolve([ ]); - } - const conditions = tags.length === 1 ? { - tags: { $in: tags } - } : { - $or: reduce(tags, (arr, tag) => { - arr.push({ tags: { $in: [ tag ] } }); - return arr; - }, []) - }; - const p = (await Model.find(conditions) - .populate({ path: "pid", populate: { path: "pid" } }) - .populate("attributes") - .exec()) - .map((item) => item.toObject()) - .map((item) => { - item.tags = Array.from(new Set(getTags(item))); - delete item.pid; - return item; - }) - .filter((item) => { - const diffLength = difference(item.tags, tags).length; - return diffLength + tags.length === item.tags.length ; - }); - return p; -}); - export interface ICategoryModel extends M { moveCategory(id: ObjectId, pid: ObjectId): Promise; - getCategories(tags: string | string[]): Promise; - /** - * Category 列表 - * @param perNum {number} 每页数量 - * @param page {number} 页数 - * @return {Promise} - */ - list(perNum?: number, page?: number): Promise; - /** - * 返回总页数 - */ - countCategories(perNum?: number): Promise; } for (const method of MODIFY_MOTHODS) { @@ -156,12 +91,3 @@ for (const method of MODIFY_MOTHODS) { } export const Model = model(FLAG, CategorySchema) as ICategoryModel; - -const getTags = (obj: ICategoryRaw | ICategory) => { - const tags = obj.tags; - const pid = obj.pid as ICategoryRaw | void; - if (pid && pid.tags) { - return tags.concat(getTags(pid)); - } - return tags; -}; diff --git a/src/models/Collection.ts b/src/models/Collection.ts index c289a35..815aeac 100644 --- a/src/models/Collection.ts +++ b/src/models/Collection.ts @@ -1,10 +1,15 @@ import { model, SchemaDefinition, Model as M, SchemaTypes } from "mongoose"; -import { Base, IDoc, IDocRaw, ObjectId, MODIFY_MOTHODS } from "@models/common"; +import { + Base, IDoc, IDocRaw, ObjectId, MODIFY_MOTHODS, existsValidator +} from "@models/common"; import { IGoods, FLAG as GoodFlag, Model as GoodsModels } from "@models/Good"; import { IUser, FLAG as UserFlag } from "@models/User"; +import newCache = require("@utils/newCache"); export const FLAG = "collections"; +export const cache = newCache(FLAG); + const Definition: SchemaDefinition = { name: { type: String, @@ -43,15 +48,7 @@ const CollectionsSchema = new Base(Definition).createSchema(); CollectionsSchema.path("name").validate({ isAsync: true, validator: async function nameValidator(val, respond) { - if (!this.isNew) { - const id = this.getQuery()._id; - const col = await Model.findById(id).exec(); - if (col.toObject().name === val) { - return respond(true); - } - } - const result = await Model.findOne({ name: val }).exec(); - respond(result ? false : true); + respond(await existsValidator.bind(this)(Model, "name", val)); }, message: "The name is existed" }); @@ -79,4 +76,10 @@ CollectionsSchema.path("goods").validate({ }); // endregion validators +for (const method of MODIFY_MOTHODS) { + CollectionsSchema.post(method, () => { + cache.clear(); + }); +} + export const Model = model(FLAG, CollectionsSchema) as M; diff --git a/src/models/Good.ts b/src/models/Good.ts index 0ba2214..acabad6 100644 --- a/src/models/Good.ts +++ b/src/models/Good.ts @@ -1,16 +1,16 @@ import { model, SchemaDefinition, Model as M, SchemaTypes } from "mongoose"; -import { Base, IDoc, IDocRaw, ObjectId, MODIFY_MOTHODS } from "@models/common"; +import { + Base, IDoc, IDocRaw, ObjectId, MODIFY_MOTHODS, existsValidator +} from "@models/common"; import { IValues, Flag as ValueFlag } from "@models/Value"; import { IUser, FLAG as UserFlag } from "@models/User"; import { ICategory, FLAG as CategoryFlag } from "@models/Categroy"; -import { DEF_PER_COUNT } from "@dtos/page"; -import Cache = require("schedule-cache"); -import { isArray } from "util"; -import { reduce } from "lodash"; - -const cache = Cache.create(`${Date.now()}${Math.random()}`); +import newCache = require("@utils/newCache"); export const FLAG = "goods"; + +export const cache = newCache(FLAG); + export type GoodDoc = IDoc; const Definition: SchemaDefinition = { @@ -64,152 +64,29 @@ const GoodsSchema = new Base(Definition).createSchema(); // region validators GoodsSchema.path("md5sum").validate({ isAsync: true, - validator: (val, respond) => { - Model.findOne({ md5sum: val }).exec().then((result) => { - respond(result ? false : true); - }); + validator: async function md5ExistsValidator(val, respond) { + respond(await existsValidator.bind(this)(Model, "md5sum", val, { + update: false + })); }, message: "The file is existed" }); GoodsSchema.path("sha256sum").validate({ isAsync: true, - validator: (val, respond) => { - Model.findOne({ sha256sum: val }).exec().then((result) => { - respond(result ? false : true); - }); + validator: async function sha256ExistsValidator(val, respond) { + respond(await existsValidator.bind(this)(Model, "sha256sum", val, { + update: false + })); }, message: "The file is existed" }); // endregion validators -const getConditionsByUids = (uids: ObjectId[]) => { - let conditions; - switch (uids.length) { - case 0: - conditions = { }; - break; - case 1: - conditions = { - uploader: uids[0] - }; - break; - default: - conditions = { - $or: reduce(uids, (arr, uid) => { - arr.push({ uploader: uid }); - return arr; - }, []) - }; - break; - } - return conditions; -}; - -GoodsSchema.static( - "getGoodsByUids", - (uids: ObjectId | ObjectId[], perNum = DEF_PER_COUNT, page = 1) => { - if (!isArray(uids)) { - uids = [ uids ]; - } - const conditions = getConditionsByUids(uids); - return Model.find(conditions) - .skip((page - 1) * perNum).limit(perNum) - .select("-uploader") - .populate("attributes") - .sort({ updatedAt: -1 }) - .exec(); - } -); - -GoodsSchema.static( - "countGoodsByUids", - async (uids: ObjectId | ObjectId[], perNum = 1) => { - if (!isArray(uids)) { - uids = [ uids ]; - } - const flag = `count_uids_${uids.join("_")}_${perNum}`; - const count = cache.get(flag); - if (count) { - return count; - } - const conditions = getConditionsByUids(uids); - const total = await Model.count(conditions).exec(); - cache.put(flag, Math.ceil(total / perNum)); - return cache.get(flag); - } -); - -const getConditionsByCids = (cids: ObjectId[]) => { - return cids.length === 1 ? { - category: cids[0], - active: true - } : { - $or: reduce(cids, (arr, cid) => { - arr.push({ category: { $in: [ cid ] } }); - return arr; - }, []), - active: true - }; -}; - -GoodsSchema.static( - "getGoodsByCids", - (cids: ObjectId | ObjectId[], perNum = DEF_PER_COUNT, page = 1) => { - if (!isArray(cids)) { - cids = [ cids ]; - } - const conditions = getConditionsByCids(cids); - return Model.find(conditions) - .skip((page - 1) * perNum).limit(perNum) - .populate("uploader attributes") - .sort({ updatedAt: -1 }) - .exec(); - } -); - -GoodsSchema.static( - "countGoodsByCids", - async (cids: ObjectId | ObjectId[], perNum = 1) => { - if (!isArray(cids)) { - cids = [ cids ]; - } - if (cids.length === 0) { - return [ ]; - } - const flag = `count_cids_${cids.join("_")}_${perNum}`; - const count = cache.get(flag); - if (count) { - return count; - } - const conditions = getConditionsByCids(cids); - const total = await Model.count(conditions).exec(); - cache.put(flag, Math.ceil(total / perNum)); - return cache.get(flag); - } -); - -export interface IGoodModel extends M { - // By UID - getGoodsByUids( - uids: ObjectId | ObjectId[], perNum?: number, page?: number - ): Promise; - countGoodsByUids( - uids: ObjectId | ObjectId[], perNum?: number - ): Promise; - // By CIDs - getGoodsByCids( - cids: ObjectId | ObjectId[], perNum?: number, page?: number - ): Promise; - countGoodsByCids( - cids: ObjectId | ObjectId[], perNum?: number - ): Promise; -} - for (const method of MODIFY_MOTHODS) { GoodsSchema.post(method, () => { cache.clear(); }); } -export const Model = model(FLAG, GoodsSchema) as IGoodModel; +export const Model = model(FLAG, GoodsSchema) as M; diff --git a/src/models/Regexp.ts b/src/models/Regexp.ts index 43eb997..5603d41 100644 --- a/src/models/Regexp.ts +++ b/src/models/Regexp.ts @@ -1,47 +1,51 @@ import { model, SchemaDefinition, Model as M, SchemaTypes } from "mongoose"; -import { Base, IDoc, IDocRaw, ObjectId, MODIFY_MOTHODS } from "@models/common"; +import { + Base, IDoc, IDocRaw, ObjectId, MODIFY_MOTHODS, existsValidator +} from "@models/common"; import { ICategory, FLAG as CF, Model as CM } from "@models/Categroy"; import { DEF_PER_COUNT } from "@dtos/page"; -import Cache = require("schedule-cache"); import isRegExp = require("@utils/isRegExp"); +import newCache = require("@utils/newCache"); import { INewRegexp } from "../modules/regexps/regexps.dto"; -export const cache = Cache.create(`${Date.now()}${Math.random()}`); +export const FLAG = "regexps"; + +export const cache = newCache(FLAG); const Definition: SchemaDefinition = { name: { type: String, required: true, unique: true }, - value: { type: String, required: true, unique: true }, + value: { type: String, required: true }, link: { type: SchemaTypes.ObjectId, ref: CF - } + }, + hidden: { type: Boolean, default: false } }; export interface IRegexp extends IDocRaw { name: string; value: string; link: ObjectId | ICategory; + hidden: boolean; } export interface IRegexpsRaw extends IRegexp { link: ICategory; } +export interface IRegexpDoc { + name: string; + value: string; + link?: ObjectId; + hidden?: boolean; +} + export type RegexpDoc = IDoc; const RegexpSchema = new Base(Definition).createSchema(); // region static methods -RegexpSchema.static("countRegexps", async (perNum = 1) => { - const FLAG = `page_count_${perNum}`; - if (cache.get(FLAG)) { - return cache.get(FLAG); - } - cache.put(FLAG, Math.ceil((await Model.count({ }).exec()) / perNum)); - return cache.get(FLAG); -}); - RegexpSchema.static( "addRegexp", (name: string, value: string, link?: ObjectId) => { @@ -52,10 +56,7 @@ RegexpSchema.static( if (link) { obj.link = link; } - return Model.create(obj).then((result) => { - cache.clear(); - return result; - }); + return Model.create(obj); } ); @@ -63,59 +64,8 @@ RegexpSchema.static("removeRegexp", (id: ObjectId) => { return Model.findByIdAndRemove(id).exec(); }); -RegexpSchema.static("link", (id: ObjectId, linkId: ObjectId | false) => { - if (!linkId) { - return Model.findByIdAndUpdate(id, { - "$unset": { link: 0 } - }).exec(); - } else { - return Model.findByIdAndUpdate( - id, { link: linkId }, { runValidators: true } - ).exec(); - } -}); - -RegexpSchema.static("list", (perNum = DEF_PER_COUNT, page = 1) => { - const FLAG_LIST = `list_${perNum}_${page}`; - if (cache.get(FLAG_LIST)) { - return cache.get(FLAG_LIST); - } - cache.put( - FLAG_LIST, - Model.find({ }) - .skip((page - 1) * perNum).limit(perNum) - .populate("link").exec() - ); - return cache.get(FLAG_LIST); -}); - -RegexpSchema.static("discern", (name: string) => { - const FLAG_DISCER_LIST = "discern"; - let p: Promise; - if (cache.get(FLAG_DISCER_LIST)) { - p = cache.get(FLAG_DISCER_LIST); - } else { - p = Model.find({ link: { $exists: true } }) - .populate("link") - .exec(); - cache.put(FLAG_DISCER_LIST, p); - } - return p.then((result) => { - const list = [ ]; - result.forEach((item) => { - const obj = item.toObject(); - const reg = new RegExp(obj.value); - if (reg.test(name)) { - list.push(obj.link); - } - }); - return list; - }); -}); // endregion static methods -export const FLAG = "regexps"; - interface IRegexpModel extends M { /** * 创建新规则 @@ -127,35 +77,15 @@ interface IRegexpModel extends M { * @return {Promise} */ removeRegexp(id: ObjectId): Promise; - /** - * 规则关联 - * @return {Promise} - */ - link(id: ObjectId, linkId: ObjectId | false): Promise; - /** - * 规则列表 - * @param perNum {number} 每页数量 - * @param page {number} 页数 - * @return {Promise} - */ - list(perNum?: number, page?: number): Promise; - /** - * 根据规则进行识别 - * @return {Promise} - */ - discern(filename: string): Promise; - /** - * 返回总页数 - */ - countRegexps(perNum?: number): Promise; } // region Validators RegexpSchema.path("name").validate({ isAsync: true, - validator: async (value, respond) => { - const result = await Model.findOne({ name: value }).exec(); - return !result; + validator: async function nameExistValidator(value, respond) { + return respond(await existsValidator.bind(this)( + Model, "name", value + )); }, message: "The name is exist" }); @@ -163,16 +93,20 @@ RegexpSchema.path("name").validate({ RegexpSchema.path("value").validate({ isAsync: true, validator: (value, respond) => { - return isRegExp(value); + return respond(isRegExp(value)); }, message: "The value isnt Regexp" }); RegexpSchema.path("value").validate({ isAsync: true, - validator: async (value, respond) => { - const result = await Model.findOne({ value: value }).exec(); - return !result; + validator: async function valueExistValidator(value, respond) { + if (this && this.hidden) { + return respond(true); + } + return respond(await existsValidator.bind(this)( + Model, "value", value, { extraCond: { hidden: false } } + )); }, message: "The value is exist" }); @@ -181,10 +115,23 @@ RegexpSchema.path("link").validate({ isAsync: true, validator: async (value, respond) => { const result = await CM.findById(value).exec(); - return !!result; + return respond(!!result); }, message: "The Category ID is not exist" }); + +RegexpSchema.path("hidden").validate({ + isAsync: true, + validator: async function hiddenExistValidator(value, respond) { + if (!value) { // hidden === false + return respond(true); + } + respond(await existsValidator.bind(this)( + Model, "hidden", value, { extraCond: { value: this.value } } + )); + }, + message: "Only one active item with every value" +}); // endregion Validators for (const method of MODIFY_MOTHODS) { diff --git a/src/models/System.ts b/src/models/System.ts new file mode 100644 index 0000000..472f53d --- /dev/null +++ b/src/models/System.ts @@ -0,0 +1,29 @@ +import { model, SchemaDefinition, Model as M } from "mongoose"; +import { Base, IDoc, IDocRaw, MODIFY_MOTHODS } from "./common"; +import newCache = require("@utils/newCache"); + +export const FLAG = "system"; + +export const cache = newCache(FLAG); + +const Definition: SchemaDefinition = { + key: { type: String, required: true }, + value: { type: String, required: true } +}; + +export interface ISystem extends IDocRaw { + key: string; + value: string; +} + +const SystemSchema = new Base(Definition).createSchema(); + +export type SystemDoc = IDoc; + +for (const method of MODIFY_MOTHODS) { + SystemSchema.post(method, () => { + cache.clear(); + }); +} + +export const Model: M = model(FLAG, SystemSchema); diff --git a/src/models/Token.ts b/src/models/Token.ts index 3e3ed8e..0c623ed 100644 --- a/src/models/Token.ts +++ b/src/models/Token.ts @@ -1,12 +1,13 @@ import { model, SchemaDefinition, Model as M, SchemaTypes } from "mongoose"; -import { Base, IDoc, IDocRaw, ObjectId, MODIFY_MOTHODS } from "@models/common"; +import { + Base, IDoc, IDocRaw, ObjectId, MODIFY_MOTHODS, existsValidator +} from "@models/common"; import { IUser, FLAG as UserFlag } from "@models/User"; +import newCache = require("@utils/newCache"); -import Cache = require("schedule-cache"); +export const FLAG = "tokens"; -export const cache = Cache.create(`${Date.now()}${Math.random()}`); - -export const Flag = "tokens"; +export const cache = newCache(FLAG); const Definition: SchemaDefinition = { token: { type: String, unique: true, index: true }, @@ -33,10 +34,10 @@ const TokensSchema = new Base(Definition).createSchema(); // region validators TokensSchema.path("token").validate({ isAsync: true, - validator: (val, respond) => { - Model.findOne({ token: val }).exec().then((result) => { - respond(result ? false : true); - }); + validator: async (val, respond) => { + respond(await existsValidator.bind(this)(Model, "token", val, { + update: false + })); }, message: "The token is existed" }); @@ -57,13 +58,8 @@ for (const method of MODIFY_MOTHODS) { }); } -export const Model = model(Flag, TokensSchema) as M; +export const Model = model(FLAG, TokensSchema) as M; -const getCount = async (userId: ObjectId): Promise => { - if (cache.get(userId.toString())) { - cache.get(userId.toString()); - } - const count = await Model.count({ user: userId }).exec(); - cache.put(userId.toString(), count); - return cache.get(userId.toString()); +const getCount = (userId: ObjectId): Promise => { + return Model.count({ user: userId }).exec(); }; diff --git a/src/models/User-Usergroup.ts b/src/models/User-Usergroup.ts new file mode 100644 index 0000000..ae38d5d --- /dev/null +++ b/src/models/User-Usergroup.ts @@ -0,0 +1,50 @@ +import { model, SchemaDefinition, Model as M, SchemaTypes } from "mongoose"; +import { FLAG as UF, IUser } from "@models/User"; +import { FLAG as GF, IUsergroups } from "@models/Usergroup"; +import { ObjectId } from "@models/common"; +import { Base, IDoc, IDocRaw } from "./common"; + +const Definition: SchemaDefinition = { + user: { + type: SchemaTypes.ObjectId, + ref: UF, + index: true, + required: true + }, + usergroup: { + type: SchemaTypes.ObjectId, + ref: GF, + index: true, + required: true + } +}; + +/** + * User-Usergroup Model FLAG + */ +export const FLAG = "user-usergroups"; + +/** + * User-Usergroup Doc Interface + */ +export interface IUserUsergroups extends IDocRaw { + user: ObjectId | IUser; + usergroup: ObjectId | IUsergroups; +} + +/** + * User-Usergroup Raw Doc Interface + */ +export interface IUserUsergroupsRaw extends IUserUsergroups { + user: IUser; + usergroup: IUsergroups; +} + +export type UserUsergroupDoc = IDoc; + +const UserUsergroupsSchema = new Base(Definition).createSchema(); + +/** + * User-Usergroup Model + */ +export const Model: M = model(FLAG, UserUsergroupsSchema); diff --git a/src/models/User.ts b/src/models/User.ts index 1e8a4bc..6cf739f 100644 --- a/src/models/User.ts +++ b/src/models/User.ts @@ -1,15 +1,15 @@ import { model, SchemaDefinition, Model as M } from "mongoose"; import * as md5 from "md5"; import { config } from "@utils/config"; -import { ObjectId } from "@models/common"; +import { ObjectId, existsValidator } from "@models/common"; import { DEF_PER_COUNT } from "@dtos/page"; -import Cache = require("schedule-cache"); import { Base, IDoc, IDocRaw, MODIFY_MOTHODS } from "./common"; - -const cache = Cache.create(`${Date.now()}${Math.random()}`); +import newCache = require("@utils/newCache"); export const FLAG = "users"; +export const cache = newCache(FLAG); + const Definition: SchemaDefinition = { username: { type: String, required: true, trim: true }, password: { type: String, required: true }, @@ -32,7 +32,7 @@ const UsersSchema = new Base(Definition).createSchema(); UsersSchema.path("username").validate({ isAsync: true, validator: async function usernameModifyValidator(val, respond) { - if (!this.isNew) { + if (this && !this.isNew) { const id = this.getQuery()._id; const col = await Model.findById(id).exec(); return respond(col.toObject().username === val); @@ -45,11 +45,12 @@ UsersSchema.path("username").validate({ UsersSchema.path("username").validate({ isAsync: true, validator: async function usernameExistValidator(val, respond) { - if (!this.isNew) { + if (this && !this.isNew) { return respond(true); } - const result = await Model.findOne({ username: val }).exec(); - respond(result ? false : true); + respond(await existsValidator.bind(this)( + Model, "username", val, { update: false } + )); }, message: "The username is existed" }); @@ -61,14 +62,6 @@ const encryptStr = (pwd: string) => { }; // region static methods -UsersSchema.static("countUsers", async (perNum = 1) => { - const FLAG = `page_count_${perNum}`; - if (cache.get(FLAG)) { - return cache.get(FLAG); - } - cache.put(FLAG, Math.ceil((await Model.count({ }).exec()) / perNum)); - return cache.get(FLAG); -}); UsersSchema.static("addUser", (username: string, password: string) => { const newObj = { @@ -88,12 +81,6 @@ UsersSchema.static("removeUser", (id: ObjectId) => { }); }); -UsersSchema.static("list", (perNum = DEF_PER_COUNT, page = 1) => { - return Model.find().select("-password") - .skip((page - 1) * perNum).limit(perNum) - .exec(); -}); - UsersSchema.static("passwd", (id: ObjectId, oldP: string, newP: string) => { return Model.findById(id).exec() .then((result) => { @@ -152,14 +139,6 @@ interface IUserModel extends M { * @return {Promise} */ removeUser(id: ObjectId): Promise; - /** - * 获取用户列表 - * - * @param perNum {number} 每页数量 - * @param page {number} 页数 - * @return {Promise} - */ - list(perNum?: number, page?: number): Promise; /** * 修改用户密码 * @@ -177,10 +156,6 @@ interface IUserModel extends M { * @return {Promise} */ isVaild(username: string, password: string): Promise; - /** - * 返回总页数 - */ - countUsers(perNum?: number): Promise; } // region Validators diff --git a/src/models/Usergroup.ts b/src/models/Usergroup.ts new file mode 100644 index 0000000..2a9bcef --- /dev/null +++ b/src/models/Usergroup.ts @@ -0,0 +1,31 @@ +import { model, SchemaDefinition, Model as M, SchemaTypes } from "mongoose"; +import { FLAG as UF, IUser } from "@models/User"; +import { ObjectId, existsValidator } from "@models/common"; +import { Base, IDoc, IDocRaw } from "./common"; +import newCache = require("@utils/newCache"); + +const Definition: SchemaDefinition = { + name: { type: String, required: true } +}; + +export const FLAG = "usergroups"; + +export const cache = newCache(FLAG); + +export interface IUsergroups extends IDocRaw { + name: string; +} + +export type UsergroupDoc = IDoc; + +const UsergroupsSchema = new Base(Definition).createSchema(); + +UsergroupsSchema.path("name").validate({ + isAsync: true, + validator: async function nameExistValidator(val, respond) { + return respond(await existsValidator.bind(this)(Model, "name", val)); + }, + message: "The Name is exist" +}); + +export const Model: M = model(FLAG, UsergroupsSchema); diff --git a/src/models/common.ts b/src/models/common.ts index fbeaaa2..8604b52 100644 --- a/src/models/common.ts +++ b/src/models/common.ts @@ -1,5 +1,5 @@ import * as lodash from "lodash"; -import { Schema, Document as Doc } from "mongoose"; +import { Schema, Document as Doc, Model as M } from "mongoose"; export type ObjectId = Schema.Types.ObjectId | string; @@ -51,3 +51,29 @@ export const MODIFY_MOTHODS = [ "findOneAndRemove", "findOneAndUpdate", "insertMany" ]; + +interface IExistsValidatorOptions { + update?: boolean; + extraCond?: object; +} + +const existsValidatorOptions: IExistsValidatorOptions = { + update: true +}; + +export const existsValidator = async function ExistsValidatorFn( + model: M, field: string, value, opts?: IExistsValidatorOptions +) { + const options = Object.assign({ }, existsValidatorOptions, opts); + if (options.update && this && !this.isNew) { + const id = this.getQuery()._id; + const col = await model.findById(id).exec(); + if (col.toObject()[field] === value) { + return true; + } + } + const cond = + Object.assign({ }, (options.extraCond || { }), { [field]: value }); + const result = await model.findOne(cond).exec(); + return !result; +}; diff --git a/src/modules/app.module.ts b/src/modules/app.module.ts index 227b62c..5a4e6e7 100644 --- a/src/modules/app.module.ts +++ b/src/modules/app.module.ts @@ -2,7 +2,7 @@ import { Module, MiddlewaresConsumer } from "@nestjs/common"; // Modules import { DatabaseModule } from "./database/database.module"; import {controllers, ControllersModule} from "./controllers.module"; -import { Clear304Middleware } from "./common/middlewares/clear304.middleware"; +import { NoCacheMiddleware } from "./common/middlewares/noCache.middleware"; @Module({ modules: [ DatabaseModule, ControllersModule ] @@ -10,7 +10,7 @@ import { Clear304Middleware } from "./common/middlewares/clear304.middleware"; export class ApplicationModule { public configure(consumer: MiddlewaresConsumer) { consumer - .apply(Clear304Middleware) + .apply(NoCacheMiddleware) .forRoutes(...controllers); } } diff --git a/src/modules/categroies/categroies.controller.ts b/src/modules/categroies/categroies.controller.ts index 9bebac0..c95e34a 100644 --- a/src/modules/categroies/categroies.controller.ts +++ b/src/modules/categroies/categroies.controller.ts @@ -1,20 +1,20 @@ import { Controller, Post, Res, Body, Get, HttpStatus, HttpCode, Param, - BadRequestException, UseGuards, Delete, Query + BadRequestException, UseGuards, Delete, Query, BadGatewayException } from "@nestjs/common"; import { ApiBearerAuth, ApiUseTags, ApiResponse, ApiImplicitParam, ApiOperation } from "@nestjs/swagger"; -import { - Model as CategoriesModel, CategoryDoc, ICategory -} from "@models/Categroy"; import { Model as ValuesModel, ValueDoc, IValues } from "@models/Value"; -import { Model as GoodsModels } from "@models/Good"; import { Roles } from "@decorators/roles"; import { RolesGuard } from "@guards/roles"; import { ParseIntPipe } from "@pipes/parse-int"; import { PerPageDto, ListResponse } from "@dtos/page"; import { CidDto } from "@dtos/ids"; +import { DefResDto } from "@dtos/res"; +import { CategoriesService } from "@services/categories"; +import { GoodsService } from "@services/goods"; +import { UtilService } from "@services/util"; import md5 = require("md5"); import { @@ -30,6 +30,11 @@ import { CreateValueDto, EditValueDto } from "../values/values.dto"; // endregion Swagger Docs export class CategoriesAdminController { + constructor( + private readonly categoriesSvr: CategoriesService, + private readonly goodsSvr: GoodsService + ) { } + @Roles("admin") @Get() // region Swagger Docs @@ -41,18 +46,10 @@ export class CategoriesAdminController { }) // endregion Swagger Docs public async list(@Query(new ParseIntPipe()) query: PerPageDto) { - const curPage = query.page || 1; - const totalPages = await CategoriesModel.countCategories(query.perNum); - const totalCount = await CategoriesModel.countCategories(); - - const data = new ListResponse(); - data.current = curPage; - data.totalPages = totalPages; - data.total = totalCount; - if (totalPages >= curPage) { - data.data = await CategoriesModel.list(query.perNum, query.page); - } - return data; + const arr = await this.categoriesSvr.list(query); + return UtilService.toListRespone(arr, Object.assign({ + total: await this.categoriesSvr.count() + }, query)); } @Roles("admin") @@ -62,7 +59,7 @@ export class CategoriesAdminController { @ApiOperation({ title: "Add Category" }) // endregion Swagger Docs public async add(@Body() ctx: NewCategoryDto) { - if (ctx.pid && !(await CategoriesModel.findById(ctx.pid).exec())) { + if (ctx.pid && !(await this.categoriesSvr.getById(ctx.pid))) { throw new BadRequestException("The Parent Category isnt exist!"); } let attrsIds = [ ]; @@ -88,20 +85,12 @@ export class CategoriesAdminController { attrs.map((item) => item.toObject()._id) ); } - let result; - try { - result = await CategoriesModel.create({ - name: ctx.name, - tags: ctx.tags, - attributes: attrsIds, - pid: ctx.pid - }); - } catch (error) { - attrsIds.forEach((id) => { - ValuesModel.findByIdAndRemove(id).exec(); - }); - throw new BadRequestException(error.toString()); - } + const result = await this.categoriesSvr.add({ + name: ctx.name, + tags: ctx.tags, + attributes: attrsIds, + pid: ctx.pid + }); return result; } @@ -111,28 +100,31 @@ export class CategoriesAdminController { @HttpCode(HttpStatus.OK) @ApiOperation({ title: "Get Category Info" }) // endregion Swagger Docs - public async get(@Param() param: CidDto) { - let obj: ICategory; + public async get( + @Param() param: CidDto, @Query(new ParseIntPipe()) query: PerPageDto + ) { + let obj; try { - const doc = await CategoriesModel.findById(param.cid) - .populate("attributes") - .populate({ - path: "pid", populate: { path: "pid" } - }) - .exec(); + const doc = await this.categoriesSvr.getById(param.cid, { + populate: [ + "attributes", + { + path: "pid", populate: { path: "pid" } + } + ] + }); + if (!obj) { + return doc; + } obj = doc.toObject(); } catch (error) { throw new BadRequestException(error.toString()); } - obj.goods = ( - await GoodsModels.find({ category: obj._id }) - .populate("uploader") - .populate("attributes") - .select("-category") - .exec() - ).map((doc) => { - return doc.toObject(); - }); + const arr = (await this.goodsSvr.listByCategoryId(param.cid)) + .map((doc) => { + return doc.toObject(); + }); + obj.goods = UtilService.toListRespone(arr, query); return obj; } @@ -145,9 +137,14 @@ export class CategoriesAdminController { public async addAttr( @Param() param: CidDto, @Body() ctx: CreateValueDto ) { - const curCategory = ( - await CategoriesModel.findById(param.cid).populate("attributes").exec() - ).toObject(); + const category = + await this.categoriesSvr.getById(param.cid, { + populate: [ "attributes" ] + }); + if (!category) { + throw new BadGatewayException("Non Exist Category"); + } + const curCategory = category.toObject(); const attributes = curCategory.attributes as IValues[]; if (attributes.length !== 0) { const attrSet = new Set(); @@ -163,9 +160,9 @@ export class CategoriesAdminController { } } const newAttr = await ValuesModel.create(ctx); - await CategoriesModel.findByIdAndUpdate( + await this.categoriesSvr.editById( param.cid, { $push: { attributes: newAttr._id } } - ).exec(); + ); return newAttr; } @@ -183,7 +180,7 @@ export class CategoriesAdminController { } catch (error) { throw new BadRequestException(error.toString()); } - return { statusCode: HttpStatus.OK }; + return new DefResDto(); } @Roles("admin") @@ -209,19 +206,15 @@ export class CategoriesAdminController { }) // endregion Swagger Docs public async deleteAttrByGet(@Param() param: CategoryAttributeParamDto) { - try { - await CategoriesModel.findByIdAndUpdate(param.cid, { - $pull: { attributes: param.aid} - }).exec(); - } catch (error) { - throw new BadRequestException(error.toString()); - } + await this.categoriesSvr.editById(param.cid, { + $pull: { attributes: param.aid} + }); try { await ValuesModel.findByIdAndRemove(param.aid).exec(); } catch (error) { - await CategoriesModel.findByIdAndUpdate( + await this.categoriesSvr.editById( param.cid, { $push: { attributes: param.aid } } - ).exec(); + ); throw new BadRequestException(error.toString()); } return { status: HttpStatus.OK }; @@ -236,32 +229,36 @@ export class CategoriesAdminController { public async edit( @Param() param: CidDto, @Body() ctx: EditCategoryDto ) { - const curCategory = ( - await CategoriesModel.findById(param.cid) - .populate("attributes") - .populate({ - path: "pid", populate: { path: "pid" } - }).exec() - ).toObject(); + const curCategory = + await this.categoriesSvr.getById(param.cid, { + populate: [ + "attributes", + { + path: "pid", populate: { path: "pid" } + } + ] + }); + if (!curCategory) { + throw new BadGatewayException("Non Exist Category"); + } let parentCategory; if (ctx.pid) { - parentCategory = await CategoriesModel.findById(ctx.pid) - .populate("attributes") - .populate({ - path: "pid", populate: { path: "pid" } - }).exec(); + parentCategory = await this.categoriesSvr.getById(ctx.pid, { + populate: [ + "attributes", + { + path: "pid", populate: { path: "pid" } + } + ] + }); if (!parentCategory) { throw new BadRequestException( "The Parent Category isnt exist!" ); } } - try { - await CategoriesModel.findByIdAndUpdate(param.cid, ctx).exec(); - } catch (error) { - throw new BadRequestException(error.toString()); - } - return { status: HttpStatus.OK }; + await this.categoriesSvr.editById(param.cid, ctx); + return new DefResDto(); } @Roles("admin") @@ -287,11 +284,7 @@ export class CategoriesAdminController { }) // endregion Swagger Docs public async deleteByGet(@Param() param: CidDto) { - try { - await CategoriesModel.findByIdAndRemove(param.cid).exec(); - } catch (error) { - throw new BadRequestException(error.toString()); - } - return { status: HttpStatus.OK }; + await this.categoriesSvr.removeById(param.cid); + return new DefResDto(); } } diff --git a/src/modules/collections/collections.admin.controller.ts b/src/modules/collections/collections.admin.controller.ts index 438a53d..ba0bfbd 100644 --- a/src/modules/collections/collections.admin.controller.ts +++ b/src/modules/collections/collections.admin.controller.ts @@ -3,16 +3,18 @@ import { } from "@nestjs/swagger"; import { Controller, UseGuards, Get, HttpCode, HttpStatus, Session, Query, Post, - Body, Param, Delete + Body, Param, Delete, BadRequestException } from "@nestjs/common"; import { CollectionDoc } from "@models/Collection"; import { ObjectId } from "@models/common"; import { RolesGuard } from "@guards/roles"; import { CollectionsService } from "@services/collections"; +import { UtilService } from "@services/util"; import { Roles } from "@decorators/roles"; import { ParseIntPipe } from "@pipes/parse-int"; import { PerPageDto, DEF_PER_COUNT, ListResponse } from "@dtos/page"; import { CCidDto } from "@dtos/ids"; +import { DefResDto } from "@dtos/res"; import { CreateCollectionDto, EditCollectionDto, ICollection, IEditCollection } from "./collections.dto"; @@ -28,22 +30,13 @@ export class CollectionsAdminController { constructor(private readonly collectionsSvr: CollectionsService) { } private async getCollectionsRes(uid: ObjectId, query: PerPageDto) { - const curPage = query.page || 1; - const perNum = query.perNum || DEF_PER_COUNT; - const totalPages = - await this.collectionsSvr.countPage(uid, query.perNum); - const totalCount = await this.collectionsSvr.count(uid); - - const resData = new ListResponse(); - resData.current = curPage; - resData.totalPages = totalPages; - resData.total = totalCount; - if (totalPages >= curPage) { - resData.data = await this.collectionsSvr.list(uid, { - page: curPage, perNum - }); - } - return resData; + const arr = await this.collectionsSvr.list(uid, { + page: query.page, perNum: query.perNum + }); + const opts = Object.assign({ + total: await this.collectionsSvr.count(uid) + }, query); + return UtilService.toListRespone(arr, opts); } @Roles("admin") @@ -88,7 +81,7 @@ export class CollectionsAdminController { @HttpCode(HttpStatus.OK) @ApiOperation({ title: "Edit Collection" }) // endregion Swagger Docs - public editCollection( + public async editCollection( @Param() param: CCidDto, @Body() body: EditCollectionDto ) { const obj: IEditCollection = { }; @@ -98,7 +91,8 @@ export class CollectionsAdminController { if (body.goods) { obj.goods = body.goods; } - return this.collectionsSvr.edit(param.cid, obj); + await this.collectionsSvr.edit(param.cid, obj); + return new DefResDto(); } @Roles("admin") @@ -107,8 +101,15 @@ export class CollectionsAdminController { @HttpCode(HttpStatus.OK) @ApiOperation({ title: "Get One Collection's Info" }) // endregion Swagger Docs - public getCollection(@Param() param: CCidDto) { - return this.collectionsSvr.getById(param.cid); + public async getCollection(@Param() param: CCidDto) { + const obj = await this.collectionsSvr.getById(param.cid); + if (!obj) { + return null; + } + obj.goods = UtilService.toListRespone(obj.goods as any[], { + perNum: obj.goods.length + }) as any; + return obj; } ///////////////////////// diff --git a/src/modules/collections/collections.controller.ts b/src/modules/collections/collections.controller.ts index 6b1097d..bb3fea1 100644 --- a/src/modules/collections/collections.controller.ts +++ b/src/modules/collections/collections.controller.ts @@ -1,11 +1,14 @@ import { - UseGuards, Controller, Get, HttpCode, HttpStatus, Param + UseGuards, Controller, Get, HttpCode, HttpStatus, Param, Query } from "@nestjs/common"; import { ApiUseTags, ApiOperation } from "@nestjs/swagger"; import { CollectionsService } from "@services/collections"; +import { UtilService } from "@services/util"; import { RolesGuard } from "@guards/roles"; import { Roles } from "@decorators/roles"; import { GetCollectionNameDto } from "./collections.dto"; +import { PerPageDto } from "@dtos/page"; +import { ParseIntPipe } from "@pipes/parse-int"; @UseGuards(RolesGuard) @Controller("/collections") @@ -22,12 +25,18 @@ export class CollectionsController { @HttpCode(HttpStatus.OK) @ApiOperation({ title: "Get Collection Info" }) // endregion Swagger Docs - public async getCollection(@Param() query: GetCollectionNameDto) { - const colDoc = await this.collectionsSvr.getByName(query.name); - let col; - col = colDoc.toObject(); - col.creator = col.creator.nickname; - col.goods.map((good) => { + public async getCollection( + @Param() param: GetCollectionNameDto, + @Query(new ParseIntPipe()) query: PerPageDto + ) { + const doc = await this.collectionsSvr.getByName(param.name); + if (!doc) { + return null; + } + let obj; + obj = doc.toObject(); + obj.creator = obj.creator.nickname; + obj.goods.map((good) => { const keys = [ "__v", "uploader", "hidden" ]; @@ -36,7 +45,10 @@ export class CollectionsController { } return good; }); - return col; + obj.goods = UtilService.toListRespone(obj.goods, { + perNum: obj.goods.length + }); + return obj; } } diff --git a/src/modules/common/decorators/route.decorator.ts b/src/modules/common/decorators/route.decorator.ts index 33efe67..bed69fc 100644 --- a/src/modules/common/decorators/route.decorator.ts +++ b/src/modules/common/decorators/route.decorator.ts @@ -3,11 +3,3 @@ import { createRouteParamDecorator } from "@nestjs/common"; export const User = createRouteParamDecorator((data, req) => { return req.user; }); - -export const File = createRouteParamDecorator((data, req) => { - return req.file; -}); - -export const Files = createRouteParamDecorator((data, req) => { - return req.files; -}); diff --git a/src/modules/common/dtos/ids.dto.ts b/src/modules/common/dtos/ids.dto.ts index ce69680..02ee5ff 100644 --- a/src/modules/common/dtos/ids.dto.ts +++ b/src/modules/common/dtos/ids.dto.ts @@ -79,6 +79,12 @@ export class GidDto implements IGidDto { public readonly gid: ObjectId; } +export class UGidDto implements IGidDto { + @ApiModelProperty({ type: String, description: "Usergroup ID" }) + @IsMongoId() + public readonly gid: ObjectId; +} + export interface IUidDto { /** * User MongoID diff --git a/src/modules/common/dtos/page.dto.ts b/src/modules/common/dtos/page.dto.ts index 2fc97b6..86ebf66 100644 --- a/src/modules/common/dtos/page.dto.ts +++ b/src/modules/common/dtos/page.dto.ts @@ -23,8 +23,8 @@ export class ListResponse { } export interface IPerPage { - readonly perNum: number; - readonly page: number; + readonly perNum?: number; + readonly page?: number; } export class PerPageDto implements IPerPage { diff --git a/src/modules/common/interceptors/regexp-count-check.interceptor.ts b/src/modules/common/interceptors/regexp-count-check.interceptor.ts new file mode 100644 index 0000000..36df5c4 --- /dev/null +++ b/src/modules/common/interceptors/regexp-count-check.interceptor.ts @@ -0,0 +1,25 @@ +import { Interceptor, NestInterceptor, BadRequestException } from "@nestjs/common"; +import { Observable } from "rxjs/Observable"; +import { RegexpsService } from "@services/regexps"; +import * as fs from "fs-extra"; + +@Interceptor() +export class RegexpCountCheckInterceptor implements NestInterceptor { + + constructor(private readonly regexpsSvr: RegexpsService) { } + + public async intercept( + dataOrRequest, context, stream$: Observable + ) { + const regexpCount = await this.regexpsSvr.count(); + if (regexpCount !== 0) { + return stream$; + } + const files = dataOrRequest.file ? + [ dataOrRequest.file ] : dataOrRequest.files; + for (const val of files) { + fs.remove(val.path); + } + throw new BadRequestException("Lost The Good Role"); + } +} diff --git a/src/modules/common/middlewares/clear304.middleware.ts b/src/modules/common/middlewares/clear304.middleware.ts deleted file mode 100644 index 1f1f2df..0000000 --- a/src/modules/common/middlewares/clear304.middleware.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ExpressMiddleware, Middleware, NestMiddleware } from "@nestjs/common"; -import { Request, Response, NextFunction } from "express"; - -@Middleware() -export class Clear304Middleware implements NestMiddleware { - public resolve(): ExpressMiddleware { - return (req: Request, res: Response, next: NextFunction) => { - const header = { - // HTTP 1.1. - "Cache-Control": "no-cache, no-store, must-revalidate", - // HTTP 1.0. - "Pragma": "no-cache", - // Proxies. - "Expires": "0" - }; - for (const key of Object.keys(header)) { - res.setHeader(key, header[key]); - } - next(); - }; - } -} diff --git a/src/modules/common/middlewares/logger.middleware.ts b/src/modules/common/middlewares/logger.middleware.ts index 00da8fc..053a1b4 100644 --- a/src/modules/common/middlewares/logger.middleware.ts +++ b/src/modules/common/middlewares/logger.middleware.ts @@ -4,7 +4,7 @@ import { } from "@nestjs/common"; import { getMeta, downloadLogger, apiLogger, accessLogger, systemLogger -} from "../helper/log"; +} from "@utils/log"; const access: RequestHandler = (req, res, next) => { // const logger = accessLogger; diff --git a/src/modules/common/middlewares/noCache.middleware.ts b/src/modules/common/middlewares/noCache.middleware.ts new file mode 100644 index 0000000..ce993f0 --- /dev/null +++ b/src/modules/common/middlewares/noCache.middleware.ts @@ -0,0 +1,9 @@ +import { ExpressMiddleware, Middleware, NestMiddleware } from "@nestjs/common"; +import helmet = require("helmet"); + +@Middleware() +export class NoCacheMiddleware implements NestMiddleware { + public resolve(): ExpressMiddleware { + return helmet.noCache(); + } +} diff --git a/src/modules/common/pipes/regexp-count-check.pipe.ts b/src/modules/common/pipes/regexp-count-check.pipe.ts deleted file mode 100644 index dfe2a03..0000000 --- a/src/modules/common/pipes/regexp-count-check.pipe.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { - Pipe, PipeTransform, ArgumentMetadata, BadRequestException -} from "@nestjs/common"; -import { Model as RegexpModel } from "@models/Regexp"; -import { isArray } from "util"; -import fs = require("fs-extra"); - -type File = Express.Multer.File; - -@Pipe() -export class RegexpCountCheckPipe implements PipeTransform { - public async transform(value: File | File[], metadata: ArgumentMetadata) { - const regexpCount = (await RegexpModel.list()).length; - if (regexpCount !== 0) { - return value; - } - if (!value) { - return value; - } - if (isArray(value)) { - for (const val of value) { - fs.remove(val.path); - } - } else { - fs.remove(value.path); - } - throw new BadRequestException("Lost The Good Role"); - } -} diff --git a/src/modules/common/pipes/to-array.pipe.ts b/src/modules/common/pipes/to-array.pipe.ts new file mode 100644 index 0000000..6007559 --- /dev/null +++ b/src/modules/common/pipes/to-array.pipe.ts @@ -0,0 +1,37 @@ +import { Pipe, PipeTransform, ArgumentMetadata } from "@nestjs/common"; +import { isObject, isArray } from "util"; + +@Pipe() +export class ToArrayPipe implements PipeTransform { + + private readonly properties: string[]; + constructor(...properties: string[]) { + this.properties = properties; + } + + public transform(value: any, metadata: ArgumentMetadata) { + if (!value || isArray(value)) { + return value; + } + if (!isObject(value)) { + return [ value ]; + } + if (this.properties.length === 0) { + for (const key of Object.keys(value)) { + if (isArray(value[key])) { + continue; + } + value[key] = [ value[key] ]; + } + } else { + for (const property of this.properties) { + const val = value[property]; + if (!val || isArray(val)) { + continue; + } + value[property] = [ val ]; + } + } + return value; + } +} diff --git a/src/modules/common/services/collections.service.ts b/src/modules/common/services/collections.service.ts deleted file mode 100644 index d66ce7f..0000000 --- a/src/modules/common/services/collections.service.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Component, Param, BadRequestException } from "@nestjs/common"; -import { UidDto } from "@dtos/ids"; -import { Model as CollectionsModel } from "@models/Collection"; -import { ObjectId } from "@models/common"; -import { IPerPage, DEF_PER_COUNT } from "@dtos/page"; -import { IEditCollection } from "../../../modules/collections/collections.dto"; - -@Component() -export class CollectionsService { - - private DEF_PER_OBJ: IPerPage = { - perNum: DEF_PER_COUNT, - page: 1 - }; - - public async create(obj) { - try { - return await CollectionsModel.create(obj); - } catch (error) { - throw new BadRequestException(error.toString()); - } - } - - public async edit(cid: ObjectId, ctx: IEditCollection) { - try { - return await CollectionsModel.update({ _id: cid }, ctx, { - runValidators: true, context: "query" - }).exec(); - } catch (error) { - throw new BadRequestException(error.toString()); - } - } - - public list(uid: ObjectId, pageObj: IPerPage = this.DEF_PER_OBJ) { - const perNum = pageObj.perNum; - const page = pageObj.page; - return CollectionsModel.find({ creator: uid }) - .skip((page - 1) * perNum).limit(perNum) - .sort({ updatedAt: -1 }) - .populate("creator") - .populate("goods") - .exec(); - } - - public count(uid: ObjectId) { - return CollectionsModel.count({ creator: uid }).exec(); - } - - public async countPage(uid: ObjectId, perNum = DEF_PER_COUNT) { - const total = await this.count(uid); - return Math.ceil(total / perNum); - } - - public getByName(name: string) { - return CollectionsModel.findOne({ name }) - .populate("creator") - .populate("goods") - .exec(); - } - - public getById(cid: ObjectId) { - return CollectionsModel.findById(cid) - .populate("creator") - .populate("goods") - .exec(); - } - - public async remove(cid: ObjectId) { - try { - return await CollectionsModel.findByIdAndRemove(cid).exec(); - } catch (error) { - throw new BadRequestException(error.toString()); - } - } - -} diff --git a/src/modules/common/services/users.service.ts b/src/modules/common/services/users.service.ts deleted file mode 100644 index d9d1c1d..0000000 --- a/src/modules/common/services/users.service.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Component, BadRequestException } from "@nestjs/common"; -import { ObjectId } from "@models/common"; -import { Model as UsersModel, UserDoc } from "@models/User"; - -@Component() -export class UsersService { - - /** - * 修改`User`属性, 除了`username` - * @param id User ID - * @param content Content - */ - public async modify(id: ObjectId, content): Promise { - if (content && content.username) { - delete content.username; - } - if (Object.keys(content).length === 0) { - throw new BadRequestException("Empty Content"); - } - try { - return await UsersModel.update({ _id: id }, content, { - runValidators: true, context: "query" - }).exec(); - } catch (error) { - throw new BadRequestException(error.toString()); - } - } -} diff --git a/src/modules/controllers.module.ts b/src/modules/controllers.module.ts index fd4cf44..cb817d1 100644 --- a/src/modules/controllers.module.ts +++ b/src/modules/controllers.module.ts @@ -14,6 +14,8 @@ import { CollectionsAdminController } from "./collections/collections.admin.controller"; import { TokensAdminController } from "./tokens/tokens.controller"; +import { UsergroupsAdminController } from "./usergroups/usergroups.controller"; +import { SystemController } from "./system/system.controller"; // endregion Controllers // region Middlewares @@ -30,22 +32,36 @@ import { // endregion Middlewares // region Services +import { RegexpsService } from "@services/regexps"; import { CollectionsService } from "@services/collections"; import { UsersService } from "@services/users"; import { TokensService } from "@services/tokens"; +import { UsergroupsService } from "@services/usergroups"; +import { SystemService } from "@services/system"; +import { CategoriesService } from "@services/categories"; +import { GoodsService } from "@services/goods"; // endregion Services export const controllers = [ FilesController, GoodsController, - UsersAdminController, AuthAdminController, RegexpsAdminController, + UsersAdminController, AuthAdminController, + UsergroupsAdminController, + RegexpsAdminController, CategoriesAdminController, GoodsAdminController, TokensAdminController, - CollectionsController, CollectionsAdminController + CollectionsController, CollectionsAdminController, + SystemController +]; + +export const services = [ + RegexpsService, CategoriesService, GoodsService, + CollectionsService, TokensService, UsersService, UsergroupsService, + SystemService ]; @Module({ controllers, - components: [ CollectionsService, TokensService, UsersService ] + components: [ ...services ] }) export class ControllersModule { private uploadFileMethod = { diff --git a/src/modules/database/database.providers.ts b/src/modules/database/database.providers.ts index d495479..db4af71 100644 --- a/src/modules/database/database.providers.ts +++ b/src/modules/database/database.providers.ts @@ -2,7 +2,10 @@ import * as mongoose from "mongoose"; import { isArray } from "util"; import { config } from "@utils/config"; import { Model as UsersModel } from "@models/User"; -import { systemLogger } from "../common/helper/log"; +import { Model as UsergroupsModel } from "@models/Usergroup"; +import { Model as UserUsergroupsModel } from "@models/User-Usergroup"; +import { SystemService } from "@services/system"; +import { systemLogger } from "@utils/log"; const getDatabaseUrl = () => { let host: string[] = [ ]; @@ -21,21 +24,41 @@ export const connectDatabase = () => { return new Promise((resolve, reject) => { const connection = mongoose.connect(getDatabaseUrl(), { useMongoClient: true, - }, (err) => { + }, async (err) => { if (err) { return reject(err); } systemLogger.info("Connected Database."); - UsersModel.count({ }).exec().then((num) => { - if (num === 0) { - return UsersModel.addUser("root", "admin"); - } - }); + await injectData(); return resolve(connection); }); }); }; +export const injectData = async () => { + let num = await UsersModel.count({ }).exec(); + if (num === 0) { + return UsersModel.addUser( + config.defaults.user.name, config.defaults.user.pass + ); + } + num = await UsergroupsModel.count({ }).exec(); + if (num === 0) { + const group = await UsergroupsModel.create({ + name: config.defaults.group.name + }); + const conditions = (await UsersModel.find({ }).exec()) + .map((item) => { + return { + user: item._id, + usergroup: group._id + }; + }); + await new SystemService().setDefaultUsergroup(group._id); + await UserUsergroupsModel.create(conditions); + } +}; + export const databaseProviders = [ { provide: "DbConnectionToken", diff --git a/src/modules/files/files.controller.ts b/src/modules/files/files.controller.ts index 13805d9..fce6fb6 100644 --- a/src/modules/files/files.controller.ts +++ b/src/modules/files/files.controller.ts @@ -3,11 +3,12 @@ import { NotFoundException, UseGuards, Query, HttpStatus, HttpCode } from "@nestjs/common"; import { ApiUseTags, ApiImplicitParam, ApiOperation } from "@nestjs/swagger"; -import { Model as GoodsModels, GoodDoc } from "@models/Good"; +import { GoodDoc } from "@models/Good"; import { config } from "@utils/config"; import { Roles } from "@decorators/roles"; import { RolesGuard } from "@guards/roles"; import { ParseIntPipe } from "@pipes/parse-int"; +import { GoodsService } from "@services/goods"; import { Response } from "express"; import pathExists = require("path-exists"); @@ -25,6 +26,8 @@ import { DownlaodDto } from "./files.dto"; @ApiUseTags("Good Download") export class FilesController { + constructor(private readonly goodsSvr: GoodsService) { } + @Roles("guest") @Get("/categories/:cid/goods/:id") // region Swagger Docs @@ -35,26 +38,22 @@ export class FilesController { public async downloadFile( @Req() req, @Res() res: Response, @Param() params: DownlaodDto ) { - let obj: GoodDoc; - try { - obj = await GoodsModels - .findOne({_id: params.id, category: params.cid}) - .exec(); - } catch (error) { - throw new BadRequestException(error.toString()); - } + const obj = (await this.goodsSvr.get({ + _id: params.id, category: params.cid + }))[0]; + if (!obj) { throw new NotFoundException(); } const good = obj.toObject(); - const filepath = - `${config.paths.upload}/${params.cid}/${good.filename}`; + const filepath = this.goodsSvr.getFilepath(good); if (!good.active) { throw new BadRequestException("Disallow download the File"); } res.download(filepath, good.originname, (err) => { + /* istanbul ignore if */ if (err) { // Recode Error } diff --git a/src/modules/files/goods.controller.ts b/src/modules/files/goods.controller.ts index 9f19ea0..67b3914 100644 --- a/src/modules/files/goods.controller.ts +++ b/src/modules/files/goods.controller.ts @@ -1,12 +1,13 @@ import { UseGuards, Controller, Get, HttpCode, HttpStatus, Query } from "@nestjs/common"; -import { ApiUseTags, ApiOperation } from "@nestjs/swagger"; -import { Model as CategoriesModel } from "@models/Categroy"; -import { Model as GoodsModels } from "@models/Good"; +import { ApiUseTags, ApiOperation, ApiResponse } from "@nestjs/swagger"; +import { UtilService } from "@services/util"; import { IUser } from "@models/User"; import { IGoodsRaw } from "@models/Good"; import { RolesGuard } from "@guards/roles"; +import { GoodsService } from "@services/goods"; +import { CategoriesService } from "@services/categories"; import { Roles } from "@decorators/roles"; import { ParseIntPipe } from "@pipes/parse-int"; import { ListResponse, DEF_PER_COUNT } from "@dtos/page"; @@ -19,31 +20,36 @@ import { GoodsQueryDto } from "./goods.dto"; @ApiUseTags("Good Download") export class GoodsController { + constructor( + private readonly goodsSvr: GoodsService, + private readonly categoriesSvr: CategoriesService + ) { } + @Roles("guest") @Get() // region Swagger Docs @HttpCode(HttpStatus.OK) @ApiOperation({ title: "Get Good List" }) + @ApiResponse({ status: HttpStatus.OK, type: ListResponse }) // endregion Swagger Docs public async getList(@Query(new ParseIntPipe()) query: GoodsQueryDto) { - const data = new ListResponse(); - const categoryModels = await CategoriesModel.getCategories(query.tags); + const categoryModels = await this.categoriesSvr.getByTags(query.tags); const categories = reduce(categoryModels, (obj, cate) => { obj[cate._id.toString()] = cate; return obj; }, { }); if (Object.keys(categories).length === 0) { - return data; + return UtilService.toListRespone([ ]); } const perNum = query.perNum || DEF_PER_COUNT; const cids = Object.keys(categories); const goods = - (await GoodsModels.getGoodsByCids(cids, perNum, query.page)) + (await this.goodsSvr.getByCids(cids, query)) .map((doc) => { const good = doc.toObject() as IGoodsRaw; const category = categories[good.category.toString()]; - delete good.category; + // delete good.category; good.uploader = good.uploader.nickname as any; good.tags = Array.from(new Set(good.tags.concat(category.tags))); @@ -52,9 +58,8 @@ export class GoodsController { )) as any; return good; }); - data.data = goods; - data.totalPages = await GoodsModels.countGoodsByCids(cids, perNum); - data.total = await GoodsModels.countGoodsByCids(cids); - return data; + return UtilService.toListRespone(goods, Object.assign({ + total: await this.goodsSvr.countByCids(cids) + }, query)); } } diff --git a/src/modules/files/goods.dto.ts b/src/modules/files/goods.dto.ts index 7291581..3792155 100644 --- a/src/modules/files/goods.dto.ts +++ b/src/modules/files/goods.dto.ts @@ -3,7 +3,7 @@ import { ApiModelPropertyOptional, ApiModelProperty } from "@nestjs/swagger"; import { PerPageDto } from "@dtos/page"; export class GoodsQueryDto extends PerPageDto { - @ApiModelPropertyOptional({ type: String }) + @ApiModelPropertyOptional({ type: String, isArray: true }) @IsString({ each: true }) diff --git a/src/modules/goods/goods.controller.ts b/src/modules/goods/goods.controller.ts index f72dc0b..b400240 100644 --- a/src/modules/goods/goods.controller.ts +++ b/src/modules/goods/goods.controller.ts @@ -1,35 +1,39 @@ import { Controller, Req, Res, Body, Get, Post, Param, Session, - HttpStatus, BadRequestException, UseGuards, Delete, HttpCode, Query + HttpStatus, BadRequestException, UseGuards, Delete, HttpCode, Query, Put, + UploadedFile, UploadedFiles, UsePipes, UseInterceptors } from "@nestjs/common"; import { ApiBearerAuth, ApiUseTags, ApiResponse, ApiOperation, ApiImplicitParam, ApiImplicitBody, ApiConsumes } from "@nestjs/swagger"; import { IValues, Model as ValuesModel } from "@models/Value"; -import { Model as GoodsModels, IGoods } from "@models/Good"; -import { Model as RegexpModel } from "@models/Regexp"; -import { Model as TokensModel } from "@models/Token"; -import { Model as CollectionsModel } from "@models/Collection"; +import { IGoods } from "@models/Good"; import { ObjectId } from "@models/common"; import { config } from "@utils/config"; import { RolesGuard } from "@guards/roles"; import { Roles } from "@decorators/roles"; -import { File, Files, User } from "@decorators/route"; +import { User } from "@decorators/route"; import { GidDto } from "@dtos/ids"; import { IReqUser } from "@dtos/req"; import { PerPageDto, ListResponse } from "@dtos/page"; -import { RegexpCountCheckPipe } from "@pipes/regexp-count-check"; +import { DefResDto } from "@dtos/res"; import { ParseIntPipe } from "@pipes/parse-int"; +import { ToArrayPipe } from "@pipes/to-array"; import { TokensService } from "@services/tokens"; import { CollectionsService } from "@services/collections"; +import { IGetRegexpsOptions, RegexpsService } from "@services/regexps"; +import { CategoriesService } from "@services/categories"; +import { GoodsService } from "@services/goods"; +import { UtilService } from "@services/util"; import * as hasha from "hasha"; import fs = require("fs-extra"); import multer = require("multer"); import { isArray } from "util"; import { CreateValueDto, EditValueDto } from "../values/values.dto"; -import { GoodAttributeParamDto } from "./goods.dto"; +import { GoodAttributeParamDto, UploadQueryDto, EditBodyDto } from "./goods.dto"; +import { RegexpCountCheckInterceptor } from "@interceptors/regexp-count-check"; @UseGuards(RolesGuard) @Controller("api/v1/goods") @@ -40,7 +44,10 @@ export class GoodsAdminController { constructor( private readonly tokensSvr: TokensService, - private readonly collectionsSvr: CollectionsService + private readonly collectionsSvr: CollectionsService, + private readonly regexpSvr: RegexpsService, + private readonly categoriesSvr: CategoriesService, + private readonly goodsSvr: GoodsService ) { } private toMd5sum(filepath: string) { @@ -62,30 +69,52 @@ export class GoodsAdminController { }) // endregion Swagger Docs public async getGoods(@Query(new ParseIntPipe()) query: PerPageDto) { - const curPage = query.page || 1; - const totalPages = - await GoodsModels.countGoodsByUids([ ], query.perNum); - const totalCount = await GoodsModels.countGoodsByUids([ ]); + const arr = await this.goodsSvr.getByUids( + [ ], query + ); + return UtilService.toListRespone(arr, Object.assign({ + total: await this.goodsSvr.countByUids([ ]) + }, query)); + } - const resData = new ListResponse(); - resData.current = curPage; - resData.totalPages = totalPages; - resData.total = totalCount; - if (totalPages >= curPage) { - resData.data = await GoodsModels.getGoodsByUids( - [ ], query.perNum, query.page - ); + private async getCategoriesIds(names: string[]) { + if (names.length === 0) { + return [ ]; + } + const conditions = { + $or: names.reduce((arr: any[], item) => { + arr.push({ name: item }); + return arr; + }, [ ]) + }; + const categories = await this.categoriesSvr.get(conditions); + const idSet = new Set(); + for (const category of categories) { + const id = category._id.toString(); + idSet.add(id); + const ids = await this.categoriesSvr.getChildrenIds(id); + for (const id of ids) { + idSet.add(id.toString()); + } } - return resData; + return Array.from(idSet); } + /** + * 文件处理 + */ private async fileProcess( - file: Express.Multer.File, uploader: string, + obj: { + file: Express.Multer.File, uploader: string, + opt?: IGetRegexpsOptions + }, cb?: (type: "Categories" | "Good", error) => void ) { - const categories = await RegexpModel.discern(file.originalname); + const categories = await this.regexpSvr.discern( + obj.file.originalname, obj.opt + ); if (categories.length !== 1) { - fs.remove(file.path); + fs.remove(obj.file.path); if (cb) { cb("Categories", categories.length); } @@ -93,29 +122,31 @@ export class GoodsAdminController { } let goodObj: IGoods; try { - const md5sum = this.toMd5sum(file.path); - const sha256sum = this.toSha256sum(file.path); - goodObj = (await GoodsModels.create({ - filename: file.filename, - originname: file.originalname, + const md5sum = this.toMd5sum(obj.file.path); + const sha256sum = this.toSha256sum(obj.file.path); + goodObj = (await this.goodsSvr.add({ + filename: obj.file.filename, + originname: obj.file.originalname, category: categories[0]._id, - uploader, md5sum, sha256sum, + uploader: obj.uploader, md5sum, sha256sum, active: true })).toObject(); } catch (error) { if (cb) { cb("Good", error); + } else { + throw error; } - return; } const newFilePath = - `${config.paths.upload}/${categories[0]._id}/${file.filename}`; - fs.move(file.path, newFilePath); + `${config.paths.upload}/${categories[0]._id}/${obj.file.filename}`; + fs.move(obj.file.path, newFilePath); return goodObj; } @Roles("admin", "token") @Post() + @UseInterceptors(RegexpCountCheckInterceptor) // region Swagger Docs @HttpCode(HttpStatus.CREATED) @ApiOperation({ title: "上传单个文件" }) @@ -125,28 +156,38 @@ export class GoodsAdminController { }) // endregion Swagger Docs public async addGood( - @File(new RegexpCountCheckPipe()) file: Express.Multer.File, - @User() user: IReqUser, @Session() session + @UploadedFile() file: Express.Multer.File, + @User() user: IReqUser, @Session() session, + @Query(new ToArrayPipe()) query: UploadQueryDto ) { const uploaderId = session.loginUserId || await this.tokensSvr.getIdByToken(user.token); + const fileProcessOpts = { + categroies: + await this.getCategoriesIds(query.category || []), + appends: await this.getCategoriesIds(query.append || []) + }; - return await this.fileProcess(file, uploaderId, (type, error) => { - if (type === "Categories") { - if (error === 0) { - throw new BadRequestException("Lost Role for the file"); - } else { - throw new BadRequestException("Much Role for the file"); + return await this.fileProcess( + { file, uploader: uploaderId, opt: fileProcessOpts }, + (type, error) => { + if (type === "Categories") { + if (error === 0) { + throw new BadRequestException("Lost Role for the file"); + } else { + throw new BadRequestException("Much Role for the file"); + } + } + if (type === "Good") { + throw new BadRequestException(error.toString()); } } - if (type === "Good") { - throw new BadRequestException(error.toString()); - } - }); + ); } @Roles("admin", "token") @Post("/collections") + @UseInterceptors(RegexpCountCheckInterceptor) // region Swagger Docs @HttpCode(HttpStatus.CREATED) @ApiOperation({ title: "上传多个文件并形成文件集" }) @@ -156,15 +197,23 @@ export class GoodsAdminController { }) // endregion Swagger Docs public async addGoods( - @Files(new RegexpCountCheckPipe()) files: Express.Multer.File[], - @User() user: IReqUser, @Session() session + @UploadedFiles() files: Express.Multer.File[], + @User() user: IReqUser, @Session() session, + @Query(new ToArrayPipe()) query: UploadQueryDto ) { const uploaderId = session.loginUserId || await this.tokensSvr.getIdByToken(user.token); + const fileProcessOpts = { + categroies: + await this.getCategoriesIds(query.category || []), + appends: await this.getCategoriesIds(query.append || []) + }; const goods: IGoods[] = [ ]; for (const file of files) { - const goodObj = await this.fileProcess(file, uploaderId); + const goodObj = await this.fileProcess({ + file, uploader: uploaderId, opt: fileProcessOpts + }); if (!goodObj) { fs.remove(file.path); continue; @@ -174,23 +223,18 @@ export class GoodsAdminController { if (goods.length === 0) { throw new BadRequestException("The Collection no good"); } else { - try { - const collections = await this.collectionsSvr.create({ - goods: goods.reduce((arr, good) => { - arr.push(good._id); - return arr; - }, []), - creator: uploaderId - }); - const collection = - isArray(collections) ? collections[0] : collections; - return CollectionsModel - .findById(collection._id) - .populate("goods") - .exec(); - } catch (error) { - throw new BadRequestException(error.toString()); - } + const collections = await this.collectionsSvr.create({ + goods: goods.reduce((arr, good) => { + arr.push(good._id); + return arr; + }, []), + creator: uploaderId + }); + const collection = + isArray(collections) ? collections[0] : collections; + return this.collectionsSvr.getById(collection._id, { + populate: [ "goods" ] + }); } } @@ -203,11 +247,13 @@ export class GoodsAdminController { public async get(@Param() param: GidDto) { let obj; try { - obj = await GoodsModels.findById(param.gid) - .populate("uploader", "nickname") - .populate("attributes") - .populate("category", "name attributes tags") - .exec(); + obj = await this.goodsSvr.getById(param.gid, { + populate: [ + "attributes", + { path: "uploader", select: "nickname" }, + { path: "category", select: "name attributes tags" } + ] + }); } catch (error) { throw new BadRequestException(error.toString()); } @@ -223,11 +269,11 @@ export class GoodsAdminController { public async addAttr( @Param() param: GidDto, @Body() ctx: CreateValueDto ) { - const obj = await GoodsModels.findById(param.gid) - .populate("attributes") - .exec(); + const obj = await this.goodsSvr.getById(param.gid, { + populate: [ "attributes" ] + }); if (!obj) { - // TODO throw + throw new BadRequestException("Non Exist Good"); } const attributes = obj.toObject().attributes as IValues[]; if (attributes.length !== 0) { @@ -241,9 +287,9 @@ export class GoodsAdminController { } } const newAttr = await ValuesModel.create(ctx); - await GoodsModels.findByIdAndUpdate( + await this.goodsSvr.editById( param.gid, { $push: { attributes: newAttr._id } } - ).exec(); + ); return { statusCode: HttpStatus.CREATED }; } @@ -261,7 +307,7 @@ export class GoodsAdminController { } catch (error) { throw new BadRequestException(error.toString()); } - return { statusCode: HttpStatus.OK }; + return new DefResDto(); } @Roles("admin") @@ -287,22 +333,47 @@ export class GoodsAdminController { }) // endregion Swagger Docs public async deleteAttrByGet(@Param() param: GoodAttributeParamDto) { - try { - await GoodsModels.findByIdAndUpdate(param.gid, { - $pull: { attributes: param.aid} - }).exec(); - } catch (error) { - throw new BadRequestException(error.toString()); - } + await this.goodsSvr.editById(param.gid, { + $pull: { attributes: param.aid} + }); try { await ValuesModel.findByIdAndRemove(param.aid).exec(); } catch (error) { - await GoodsModels.findByIdAndUpdate( + await this.goodsSvr.editById( param.gid, { $push: { attributes: param.aid } } - ).exec(); + ); throw new BadRequestException(error.toString()); } - return { statusCode: HttpStatus.OK }; + return new DefResDto(); + } + + @Roles("admin") + @Put("/:gid") + @Delete("/:gid") + // region Swagger Docs + @HttpCode(HttpStatus.OK) + @ApiOperation({ title: "Modify Good" }) + @ApiResponse({ + status: HttpStatus.OK, description: "Modify Success" + }) + // endregion Swagger Docs + public async edit(@Param() param: GidDto, @Body() body: EditBodyDto) { + await this.goodsSvr.editById(param.gid, body); + return new DefResDto(); + } + + @Roles("admin") + @Delete("/:gid") + // region Swagger Docs + @HttpCode(HttpStatus.OK) + @ApiOperation({ title: "Delete Good" }) + @ApiResponse({ + status: HttpStatus.OK, description: "Delete Success" + }) + // endregion Swagger Docs + public async delete(@Param() param: GidDto) { + await this.goodsSvr.remove(param.gid); + return new DefResDto(); } } diff --git a/src/modules/goods/goods.dto.ts b/src/modules/goods/goods.dto.ts index a6ea7b1..6d8b18d 100644 --- a/src/modules/goods/goods.dto.ts +++ b/src/modules/goods/goods.dto.ts @@ -1,6 +1,6 @@ import { IGidDto, IAidDto } from "@dtos/ids"; -import { ApiModelProperty } from "@nestjs/swagger"; -import { IsMongoId } from "class-validator"; +import { ApiModelProperty, ApiModelPropertyOptional } from "@nestjs/swagger"; +import { IsMongoId, IsString, IsOptional } from "class-validator"; import { ObjectId } from "@models/common"; import { IGoods } from "@models/Good"; @@ -19,3 +19,35 @@ export class GoodsDto { @ApiModelProperty({ type: Object, description: "Goods", isArray: true }) public readonly goods: IGoods[]; } + +export class UploadQueryDto { + @ApiModelProperty({ + type: String, description: "Category Name", isArray: true + }) + @IsString({ each: true }) + @IsOptional() + public readonly category?: string[]; + @ApiModelProperty({ + type: String, description: "Append Category Name", isArray: true + }) + @IsString({ each: true }) + @IsOptional() + public readonly append?: string[]; +} + +export class EditBodyDto { + @ApiModelPropertyOptional({ type: String, description: "Good Name" }) + @IsOptional() + public readonly name?: string; + @ApiModelPropertyOptional({ type: Boolean }) + @IsOptional() + public readonly hidden?: boolean; + @ApiModelPropertyOptional({ type: String, description: "Category ID" }) + @IsOptional() + public readonly category?: ObjectId; + @ApiModelPropertyOptional({ + type: String, description: "Filename when download" + }) + @IsOptional() + public readonly originname?: string; +} diff --git a/src/modules/regexps/regexps.controller.ts b/src/modules/regexps/regexps.controller.ts index 7307892..84db6c6 100644 --- a/src/modules/regexps/regexps.controller.ts +++ b/src/modules/regexps/regexps.controller.ts @@ -5,7 +5,7 @@ import { import { ApiBearerAuth, ApiUseTags, ApiResponse, ApiOperation, ApiImplicitParam } from "@nestjs/swagger"; -import { Model as RegexpsModel, IRegexp, RegexpDoc } from "@models/Regexp"; +import { RegexpDoc } from "@models/Regexp"; import { NewRegexp, EditRegexpDot, EditRegexpRawDot } from "./regexps.dto"; @@ -14,6 +14,9 @@ import { RolesGuard } from "@guards/roles"; import { PerPageDto, ListResponse } from "@dtos/page"; import { RidDto } from "@dtos/ids"; import { ParseIntPipe } from "@pipes/parse-int"; +import { RegexpsService } from "@services/regexps"; +import { UtilService } from "@services/util"; +import { DefResDto } from "@dtos/res"; @UseGuards(RolesGuard) @Controller("api/v1/regexps") @@ -23,6 +26,10 @@ import { ParseIntPipe } from "@pipes/parse-int"; // endregion Swagger Docs export class RegexpsAdminController { + constructor( + private readonly regexpsSvr: RegexpsService + ) { } + @Roles("admin") @Get() // region Swagger Docs @@ -34,33 +41,18 @@ export class RegexpsAdminController { }) // endregion Swagger Docs public async list(@Query(new ParseIntPipe()) query: PerPageDto) { - const curPage = query.page || 1; - const totalPages = await RegexpsModel.countRegexps(query.perNum); - const totalCount = await RegexpsModel.countRegexps(); - - const data = new ListResponse(); - data.current = curPage; - data.totalPages = totalPages; - data.total = totalCount; - if (totalPages >= curPage) { - data.data = await RegexpsModel.list(query.perNum, query.page); - } - return data; + const arr = await this.regexpsSvr.list(query.perNum, query.page); + return UtilService.toListRespone(arr, Object.assign({ + total: await this.regexpsSvr.count() + }, query)); } @Roles("admin") @Post() @HttpCode(HttpStatus.CREATED) @ApiOperation({ title: "Add RegExp" }) - public async add(@Body() ctx: NewRegexp) { - let regexp; - try { - regexp = - await RegexpsModel.addRegexp(ctx.name, ctx.value, ctx.link); - } catch (error) { - throw new BadRequestException(error.toString()); - } - return regexp; + public add(@Body() ctx: NewRegexp) { + return this.regexpsSvr.create(ctx); } @Roles("admin") @@ -70,9 +62,7 @@ export class RegexpsAdminController { @ApiOperation({ title: "Get RegExp Info" }) // endregion Swagger Docs public getRegexp(@Param() param: RidDto) { - return RegexpsModel.findById(param.rid) - .populate({ path: "link", populate: { path: "pid" } }) - .exec(); + return this.regexpsSvr.getById(param.rid); } @Roles("admin") @@ -82,24 +72,11 @@ export class RegexpsAdminController { @ApiOperation({ title: "Edit RegExp" }) // endregion Swagger Docs public async edit(@Body() ctx: EditRegexpDot, @Param() param: RidDto) { - const data: EditRegexpRawDot = { }; - if (ctx.name) { data.name = ctx.name; } - if (ctx.value) { data.value = ctx.value; } - if (ctx.link) { data.link = ctx.link; } - if (Object.keys(data).length === 0) { - throw new BadRequestException("No Params"); - } - try { - const regexp = await RegexpsModel.findByIdAndUpdate( - param.rid, data, { runValidators: true } - ).exec(); - if (!regexp) { - throw new BadRequestException("NonExist RegExp"); - } - } catch (error) { - throw new BadRequestException(error.toString()); + const regexp = await this.regexpsSvr.editById(param.rid, ctx); + if (!regexp) { + throw new BadRequestException("Non Exist RegExp"); } - return { statusCode: HttpStatus.OK }; + return new DefResDto(); } @Roles("admin") @@ -108,7 +85,8 @@ export class RegexpsAdminController { @HttpCode(HttpStatus.OK) @ApiOperation({ title: "Delete RegExp" }) @ApiResponse({ - status: HttpStatus.OK, description: "Delete RegExp Success" + status: HttpStatus.OK, description: "Delete RegExp Success", + type: DefResDto }) // endregion Swagger Docs public deleteByDelete(@Param() param: RidDto) { @@ -121,16 +99,13 @@ export class RegexpsAdminController { @HttpCode(HttpStatus.OK) @ApiOperation({ title: "Delete RegExp" }) @ApiResponse({ - status: HttpStatus.OK, description: "Delete RegExp Success" + status: HttpStatus.OK, description: "Delete RegExp Success", + type: DefResDto }) // endregion Swagger Docs public async deleteByGet(@Param() param: RidDto) { - try { - await RegexpsModel.removeRegexp(param.rid); - } catch (error) { - throw new BadRequestException(error.toString()); - } - return { statusCode: HttpStatus.OK }; + await this.regexpsSvr.remove(param.rid); + return new DefResDto(); } } diff --git a/src/modules/regexps/regexps.dto.ts b/src/modules/regexps/regexps.dto.ts index 6078aaa..d758eeb 100644 --- a/src/modules/regexps/regexps.dto.ts +++ b/src/modules/regexps/regexps.dto.ts @@ -1,4 +1,4 @@ -import { IsString, IsMongoId, IsOptional } from "class-validator"; +import { IsString, IsMongoId, IsOptional, IsBoolean } from "class-validator"; import { ObjectId } from "@models/common"; import { ApiModelProperty, ApiModelPropertyOptional } from "@nestjs/swagger"; @@ -17,6 +17,7 @@ export class EditRegexpRawDot implements IRegexp { public name?: string; public value?: string; public link?: ObjectId; + public hidden?: boolean; } export class NewRegexp implements INewRegexp { @@ -45,4 +46,8 @@ export class EditRegexpDot implements IRegexp { @IsOptional() @IsMongoId() public readonly link: ObjectId; + @ApiModelPropertyOptional({ type: Boolean }) + @IsOptional() + @IsBoolean() + public readonly hidden: boolean; } diff --git a/src/modules/system/system.controller.ts b/src/modules/system/system.controller.ts new file mode 100644 index 0000000..47bbe86 --- /dev/null +++ b/src/modules/system/system.controller.ts @@ -0,0 +1,38 @@ +import { DefResDto } from "@dtos/res"; +import { Controller, Get, UseGuards, Put, Body } from "@nestjs/common"; +import { SystemService } from "@services/system"; +import { RolesGuard } from "@guards/roles"; +import { ApiUseTags, ApiBearerAuth } from "@nestjs/swagger"; +import { Roles } from "@decorators/roles"; +import { EditSystemVarDto } from "./system.dto"; + +@UseGuards(RolesGuard) +@Controller("api/v1/system") +// region Swagger Docs +@ApiUseTags("System") +@ApiBearerAuth() +// endregion Swagger Docs +export class SystemController { + + constructor(private readonly systemSvr: SystemService) { } + + @Roles("admin", "token") + @Get("/vars") + public getVars() { + return this.systemSvr.get(); + } + + @Roles("admin") + @Put("/vars") + public async setVars(@Body() body: EditSystemVarDto) { + await this.systemSvr.set(body.key, body.value); + return new DefResDto(); + } + + @Roles("admin") + @Get("/info") + public info() { + return this.systemSvr.info(); + } + +} diff --git a/src/modules/system/system.dto.ts b/src/modules/system/system.dto.ts new file mode 100644 index 0000000..ef3898c --- /dev/null +++ b/src/modules/system/system.dto.ts @@ -0,0 +1,12 @@ +import { ApiModelProperty } from "@nestjs/swagger"; +import { IsString } from "class-validator"; +import { DEFAULTS } from "@services/system"; + +export class EditSystemVarDto { + @ApiModelProperty({ type: String }) + @IsString() + public readonly key: DEFAULTS; + @ApiModelProperty({ type: String }) + @IsString() + public readonly value: string; +} diff --git a/src/modules/tokens/tokens.controller.ts b/src/modules/tokens/tokens.controller.ts index deae07f..b0cf67a 100644 --- a/src/modules/tokens/tokens.controller.ts +++ b/src/modules/tokens/tokens.controller.ts @@ -3,10 +3,10 @@ import { BadRequestException, ForbiddenException } from "@nestjs/common"; import { ApiUseTags, ApiResponse, ApiOperation } from "@nestjs/swagger"; -import { Model as TokensModel } from "@models/Token"; import { Roles } from "@decorators/roles"; import { RolesGuard } from "@guards/roles"; import { TokensService } from "@services/tokens"; +import { UtilService } from "@services/util"; import { DefResDto } from "@dtos/res"; import { ListResponse } from "@dtos/page"; import { TokenParamDto } from "./tokens.dto"; @@ -29,11 +29,8 @@ export class TokensAdminController { }) // endregion Swagger Docs public async getTokens(@Session() session) { - const data = new ListResponse(); - data.current = data.totalPages = 1; - data.data = await this.tokensSvr.getTokens(session.loginUserId); - data.total = data.data.length; - return data; + const arr = await this.tokensSvr.getTokens(session.loginUserId); + return UtilService.toListRespone(arr); } @Roles("admin") @@ -68,16 +65,12 @@ export class TokensAdminController { public async deleteTokenByGet( @Param() param: TokenParamDto, @Session() session ) { - try { - const token = await this.tokensSvr.remove({ - _id: param.tid, - user: session.loginUserId - }); - if (!token) { - throw new BadRequestException("The Tokens isth exist"); - } - } catch (error) { - throw new BadRequestException(error.toString()); + const token = await this.tokensSvr.remove({ + _id: param.tid, + user: session.loginUserId + }); + if (!token) { + throw new BadRequestException("The Tokens isth exist"); } return new DefResDto(); } diff --git a/src/modules/usergroups/usergroups.controller.ts b/src/modules/usergroups/usergroups.controller.ts new file mode 100644 index 0000000..4d8b2f6 --- /dev/null +++ b/src/modules/usergroups/usergroups.controller.ts @@ -0,0 +1,146 @@ +import { + UseGuards, Controller, Get, Query, HttpStatus, HttpCode, Post, Body, Param, + Delete +} from "@nestjs/common"; +import { ApiUseTags, ApiOperation, ApiResponse } from "@nestjs/swagger"; +import { RolesGuard } from "@guards/roles"; +import { UsergroupsService } from "@services/usergroups"; +import { Roles } from "@decorators/roles"; +import { ParseIntPipe } from "@pipes/parse-int"; +import { PerPageDto, ListResponse, DEF_PER_COUNT } from "@dtos/page"; +import { UGidDto } from "@dtos/ids"; +import { UtilService } from "@services/util"; + +import { + AddUsergroupDto, EditUsergroupDto, UserUsergroupDto +} from "./usergroups.dto"; + +@UseGuards(RolesGuard) +@ApiUseTags("User Groups") +@Controller("api/v1/usergroups") +export class UsergroupsAdminController { + + constructor(private readonly ugSvr: UsergroupsService) { } + + @Roles("admin") + @Get() + // region Swagger Docs + @HttpCode(HttpStatus.OK) + @ApiOperation({ title: "Get Usergroup List" }) + @ApiResponse({ + status: HttpStatus.OK, description: "Usergroup List", + type: ListResponse + }) + // endregion Swagger Docs + public async getList(@Query(new ParseIntPipe()) query: PerPageDto) { + const arr = await this.ugSvr.list(query); + return UtilService.toListRespone(arr, Object.assign({ + total: await this.ugSvr.count() + }, query)); + } + + @Roles("admin") + @Post() + // region Swagger Docs + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ title: "New Usergroup" }) + @ApiResponse({ + status: HttpStatus.CREATED, description: "Success" + }) + // endregion Swagger Docs + public addUsergroup(@Body() body: AddUsergroupDto) { + return this.ugSvr.add(body); + } + + @Roles("admin") + @Get("/:gid/delete") + // region Swagger Docs + @HttpCode(HttpStatus.OK) + @ApiOperation({ title: "Delete Usergroup" }) + @ApiResponse({ + status: HttpStatus.OK, description: "Delete Success" + }) + // endregion Swagger Docs + public async removeUsergroupByGet(@Param() param: UGidDto) { + return this.ugSvr.remove(param.gid); + } + + @Roles("admin") + @Delete("/:gid") + // region Swagger Docs + @HttpCode(HttpStatus.OK) + @ApiOperation({ title: "Delete Usergroup" }) + @ApiResponse({ + status: HttpStatus.OK, description: "Delete Success" + }) + // endregion Swagger Docs + public async removeUsergroupByDelete(@Param() param: UGidDto) { + return this.ugSvr.remove(param.gid); + } + + @Roles("admin") + @Post("/:gid") + // region Swagger Docs + @HttpCode(HttpStatus.OK) + @ApiOperation({ title: "Modify Usergroup Info" }) + @ApiResponse({ + status: HttpStatus.OK, description: "Modify Success" + }) + // endregion Swagger Docs + public async editUsergroup( + @Param() param: UGidDto, @Body() body: EditUsergroupDto + ) { + return this.ugSvr.edit(param.gid, body); + } + + @Roles("admin") + @Get("/:gid") + // region Swagger Docs + @HttpCode(HttpStatus.OK) + @ApiOperation({ title: "Get Usergroup Info" }) + @ApiResponse({ + status: HttpStatus.OK, description: "Usergroup Info" + }) + // endregion Swagger Docs + public async getUsergroup( + @Param() param: UGidDto, @Query(new ParseIntPipe()) query: PerPageDto + ) { + let group: any = await this.ugSvr.getGroup(param.gid); + if (!group) { + return group; + } + group = group.toObject(); + const arr = await this.ugSvr.getGroupUsers(param.gid, query); + group.users = UtilService.toListRespone(arr, Object.assign({ + total: await this.ugSvr.usersCount(param.gid) + }, query)); + return group; + } + + @Roles("admin") + @Get("/:gid/add/:uid") + // region Swagger Docs + @HttpCode(HttpStatus.CREATED) + @ApiOperation({ title: "Add User to Usergroup" }) + @ApiResponse({ + status: HttpStatus.CREATED, description: "Add Success" + }) + // endregion Swagger Docs + public addUser(@Param() param: UserUsergroupDto) { + return this.ugSvr.addUserToGroup(param.gid, param.uid); + } + + @Roles("admin") + @Get("/:gid/remove/:uid") + // region Swagger Docs + @HttpCode(HttpStatus.OK) + @ApiOperation({ title: "Remove User from Usergroup" }) + @ApiResponse({ + status: HttpStatus.OK, description: "Remove Success" + }) + // endregion Swagger Docs + public removeUser(@Param() param: UserUsergroupDto) { + return this.ugSvr.removeUserFromGroup(param.gid, param.uid); + } + +} diff --git a/src/modules/usergroups/usergroups.dto.ts b/src/modules/usergroups/usergroups.dto.ts new file mode 100644 index 0000000..fcacae5 --- /dev/null +++ b/src/modules/usergroups/usergroups.dto.ts @@ -0,0 +1,25 @@ +import { ApiModelProperty } from "@nestjs/swagger"; +import { IsString, IsMongoId } from "class-validator"; +import { ObjectId } from "@models/common"; +import { IGidDto, IUidDto } from "@dtos/ids"; + +export class AddUsergroupDto { + @ApiModelProperty({ type: String, description: "Usergroup Name" }) + @IsString() + public readonly name: string; +} + +export class EditUsergroupDto { + @ApiModelProperty({ type: String, description: "Usergroup Name" }) + @IsString() + public readonly name: string; +} + +export class UserUsergroupDto implements IGidDto, IUidDto { + @ApiModelProperty({ type: String, description: "User ID" }) + @IsMongoId() + public readonly uid: ObjectId; + @ApiModelProperty({ type: String, description: "Usergroup ID" }) + @IsMongoId() + public readonly gid: ObjectId; +} diff --git a/src/modules/users/auth.controller.ts b/src/modules/users/auth.controller.ts index 0552ca7..629e946 100644 --- a/src/modules/users/auth.controller.ts +++ b/src/modules/users/auth.controller.ts @@ -7,10 +7,9 @@ import { } from "@nestjs/swagger"; import uuid = require("uuid"); import basicAuth = require("basic-auth"); -import { Model as UserModel, UserDoc } from "@models/User"; -import { Model as TokensModel } from "@models/Token"; import { RolesGuard } from "@guards/roles"; import { TokensService } from "@services/tokens"; +import { UsersService } from "@services/users"; import { LoginBodyDto, LoginQueryDto, LoginRespone } from "./auth.dto"; @UseGuards(RolesGuard) @@ -18,7 +17,10 @@ import { LoginBodyDto, LoginQueryDto, LoginRespone } from "./auth.dto"; @Controller("api/v1/auth") export class AuthAdminController { - constructor(private readonly tokensSvr: TokensService) { } + constructor( + private readonly usersSvr: UsersService, + private readonly tokensSvr: TokensService + ) { } @Post("login") @ApiOperation({ title: "Login System" }) @@ -31,12 +33,8 @@ export class AuthAdminController { @Session() session, @Body() ctx: LoginBodyDto, @Query() query: LoginQueryDto ) { - let user: UserDoc = null; - try { - user = await UserModel.isVaild(ctx.username, ctx.password); - } catch (err) { - throw new BadRequestException(err.toString()); - } + const user = + await this.usersSvr.isVaild(ctx.username, ctx.password); session.loginUser = user.toObject().username; session.loginUserId = user.toObject()._id; const obj = new LoginRespone(); @@ -45,13 +43,9 @@ export class AuthAdminController { obj.id = user.toObject()._id; if (query.token) { const token = uuid(); - try { - await this.tokensSvr.create({ - token, user: session.loginUserId - }); - } catch (error) { - throw new BadRequestException(error.toString()); - } + await this.tokensSvr.create({ + token, user: session.loginUserId + }); obj.token = token; } obj.expires = session.cookie.maxAge || session.cookie.originalMaxAge; diff --git a/src/modules/users/users.controller.ts b/src/modules/users/users.controller.ts index ecbd06a..ee54c37 100644 --- a/src/modules/users/users.controller.ts +++ b/src/modules/users/users.controller.ts @@ -5,23 +5,23 @@ import { import { ApiBearerAuth, ApiUseTags, ApiResponse, ApiOperation, ApiImplicitParam } from "@nestjs/swagger"; -import { Model as UserModel, IUser, UserDoc } from "@models/User"; -import { Model as TokensModel } from "@models/Token"; -import { Model as GoodsModels } from "@models/Good"; -import { CollectionDoc } from "@models/Collection"; import { ObjectId } from "@models/common"; import { Roles } from "@decorators/roles"; import { RolesGuard } from "@guards/roles"; import { ParseIntPipe } from "@pipes/parse-int"; import { TokensService } from "@services/tokens"; +import { UsergroupsService } from "@services/usergroups"; import { CollectionsService } from "@services/collections"; import { UsersService } from "@services/users"; +import { SystemService } from "@services/system"; +import { UtilService } from "@services/util"; +import { GoodsService } from "@services/goods"; import { PerPageDto, ListResponse, DEF_PER_COUNT } from "@dtos/page"; import { UidDto } from "@dtos/ids"; import { DefResDto } from "@dtos/res"; import { - CreateUserDto, ModifyPasswordDto, EditUserDto + CreateUserDto, ModifyPasswordDto, EditUserDto, UsergroupBodyDto } from "./users.dto"; @UseGuards(RolesGuard) @@ -35,7 +35,10 @@ export class UsersAdminController { constructor( private readonly tokensSvr: TokensService, private readonly collectionsSvr: CollectionsService, - private readonly usersSvr: UsersService + private readonly usersSvr: UsersService, + private readonly ugSvr: UsergroupsService, + private readonly sysSvr: SystemService, + private readonly goodsSvr: GoodsService ) { } @Roles("admin") @@ -49,18 +52,10 @@ export class UsersAdminController { }) // endregion Swagger Docs public async findAll(@Query(new ParseIntPipe()) query: PerPageDto) { - const curPage = query.page || 1; - const totalPages = await UserModel.countUsers(query.perNum); - const totalCount = await UserModel.countUsers(); - - const data = new ListResponse(); - data.current = curPage; - data.totalPages = totalPages; - data.total = totalCount; - if (totalPages >= curPage) { - data.data = await UserModel.list(query.perNum, query.page); - } - return data; + const arr = await this.usersSvr.list(query); + return UtilService.toListRespone(arr, Object.assign({ + total: await this.usersSvr.conut() + }, query)); } @Roles("admin") @@ -75,14 +70,8 @@ export class UsersAdminController { status: HttpStatus.BAD_REQUEST, description: "Add User Fail" }) // endregion Swagger Docs - public async addUser(@Body() user: CreateUserDto) { - let obj; - try { - obj = await UserModel.addUser(user.username, user.password); - } catch (error) { - throw new BadRequestException(error.toString()); - } - return obj; + public addUser(@Body() user: CreateUserDto) { + return this.usersSvr.addUser(user); } @Roles("admin") @@ -118,11 +107,9 @@ export class UsersAdminController { public async password( @Body() user: ModifyPasswordDto, @Param() param: UidDto ) { - try { - await UserModel.passwd(param.uid, user.oldPassword, user.newPassword); - } catch (error) { - throw new BadRequestException(error.toString()); - } + await this.usersSvr.passwd( + param.uid, user.oldPassword, user.newPassword + ); return new DefResDto(); } @@ -153,12 +140,8 @@ export class UsersAdminController { status: HttpStatus.BAD_REQUEST, description: "Delete User Fail" }) // endregion Swagger Docs - public async delete(@Param() user: UidDto) { - try { - await UserModel.removeUser(user.uid); - } catch (error) { - throw new BadRequestException(error.toString()); - } + public delete(@Param() user: UidDto) { + this.usersSvr.removeUser(user.uid); return new DefResDto(); } @@ -213,11 +196,8 @@ export class UsersAdminController { }) // endregion Swagger Docs public async getSelfTokens(@Session() session) { - const data = new ListResponse(); - data.current = data.totalPages = 1; - data.data = await this.tokensSvr.getTokens(session.loginUserId); - data.total = data.data.length; - return data; + const arr = await this.tokensSvr.getTokens(session.loginUserId); + return UtilService.toListRespone(arr); } @Roles("admin") @@ -231,11 +211,8 @@ export class UsersAdminController { }) // endregion Swagger Docs public async getTokens(@Param() param: UidDto, @Session() session) { - const data = new ListResponse(); - data.current = data.totalPages = 1; - data.data = await this.tokensSvr.getTokens(param.uid); - data.total = data.data.length; - return data; + const arr = await this.tokensSvr.getTokens(param.uid); + return UtilService.toListRespone(arr); } //////////////////////////////////////// @@ -247,22 +224,10 @@ export class UsersAdminController { //////////////////////////////////////// private async getGoodsRes(uid: ObjectId, query: PerPageDto) { - const curPage = query.page || 1; - const perNum = query.perNum || DEF_PER_COUNT; - const totalPages = - await GoodsModels.countGoodsByUids(uid, query.perNum); - const totalCount = await GoodsModels.countGoodsByUids(uid); - - const resData = new ListResponse(); - resData.current = curPage; - resData.totalPages = totalPages; - resData.total = totalCount; - if (totalPages >= curPage) { - resData.data = await GoodsModels.getGoodsByUids( - uid, query.perNum, query.page - ); - } - return resData; + const arr = await this.goodsSvr.getByUids(uid, query); + return UtilService.toListRespone(arr, Object.assign({ + total: await this.goodsSvr.countByUids(uid) + }, query)); } @Roles("admin", "token") @@ -308,20 +273,10 @@ export class UsersAdminController { //////////////////////////////////////// private async getCollectionsRes(uid: ObjectId, query: PerPageDto) { - const curPage = query.page || 1; - const perNum = query.perNum || DEF_PER_COUNT; - const totalPages = - await this.collectionsSvr.countPage(uid, query.perNum); - const totalCount = await this.collectionsSvr.count(uid); - - const resData = new ListResponse(); - resData.current = curPage; - resData.totalPages = totalPages; - resData.total = totalCount; - if (totalPages >= curPage) { - resData.data = await this.collectionsSvr.list(uid, query); - } - return resData; + const arr = await this.collectionsSvr.list(uid, query); + return UtilService.toListRespone(arr, { + total: await this.collectionsSvr.count(uid) + }); } @Roles("admin", "token") @@ -362,6 +317,32 @@ export class UsersAdminController { // endregion Collection Methods //////////////////////////////////////// + //////////////////////////////////////// + // region Usergroup Methods + //////////////////////////////////////// + + @Roles("admin") + @Get("/:uid/usergroups") + // region Swagger Docs + @HttpCode(HttpStatus.OK) + @ApiOperation({ title: "Get Usergroup" }) + @ApiResponse({ + status: HttpStatus.OK, description: "Usergroup List", type: ListResponse + }) + // endregion Swagger Docs + public async getUsergroups( + @Param() param: UidDto, @Query(new ParseIntPipe()) query: PerPageDto + ) { + const arr = await this.usersSvr.getUsergroups(param.uid, query); + return UtilService.toListRespone(arr, Object.assign({ + total: await this.usersSvr.countUsergroups(param.uid) + }, query)); + } + + //////////////////////////////////////// + // endregion Usergroup Methods + //////////////////////////////////////// + @Roles("admin") @Get("/:uid") // region Swagger Docs @@ -370,7 +351,7 @@ export class UsersAdminController { @ApiResponse({ status: HttpStatus.OK, description: "Get User Info" }) // endregion Swagger Docs public get(@Param() param: UidDto) { - return UserModel.findById(param.uid).exec(); + return this.usersSvr.getById(param.uid); } } diff --git a/src/modules/users/users.dto.ts b/src/modules/users/users.dto.ts index df44a52..49b6c2d 100644 --- a/src/modules/users/users.dto.ts +++ b/src/modules/users/users.dto.ts @@ -1,6 +1,7 @@ import { IsString, IsMongoId } from "class-validator"; import { ObjectId } from "@models/common"; import { ApiUseTags, ApiModelProperty } from "@nestjs/swagger"; +import { IUidDto, IGidDto } from "@dtos/ids"; export class CreateUserDto { @ApiModelProperty({ type: String, description: "Username" }) @@ -25,3 +26,12 @@ export class ModifyPasswordDto { @IsString() public readonly newPassword: string; } + +export class UsergroupBodyDto { + @ApiModelProperty({ type: String, description: "Action" }) + @IsString() + public readonly type: "add" | "remove"; + @ApiModelProperty({ type: String, description: "Usergroup ID" }) + @IsMongoId({ each: true }) + public readonly gids: ObjectId; +} diff --git a/src/services/base.service.ts b/src/services/base.service.ts new file mode 100644 index 0000000..5f988ce --- /dev/null +++ b/src/services/base.service.ts @@ -0,0 +1,234 @@ +import { BadGatewayException, BadRequestException } from "@nestjs/common"; +import { isString } from "util"; +import { DEF_PER_COUNT } from "@dtos/page"; +import { UtilService } from "@services/util"; +import { + DocumentQuery, ModelPopulateOptions, Model, ModelUpdateOptions +} from "mongoose"; +import keyv = require("keyv"); +import isPromise = require("is-promise"); +import { IDocRaw, IDoc, ObjectId } from "@models/common"; + +export interface IGetOptions { + populate?: string | Array; + select?: string | string[]; + perNum?: number; + page?: number; + sort?: string | object; +} + +abstract class ModelService { + + protected readonly DEF_UPDATE_OPTIONS: ModelUpdateOptions = { + runValidators: true, context: "query" + }; + private model: Model>; + + protected setModel>>(model: M) { + this.model = model; + } + + private checkModel() { + /* istanbul ignore if */ + if (!this.model) { + throw new BadGatewayException("Lost Model"); + } + } + + protected runBeforeAll() { + return Promise.resolve(this.beforeAll()); + } + + protected beforeAll(): Promise { + return Promise.resolve({ }); + } + + private runBeforeEach() { + return Promise.resolve(this.beforeEach()); + } + + protected beforeEach(): Promise { + return Promise.resolve({ }); + } + + public async create(obj: object) { + this.checkModel(); + await this.runBeforeAll(); + await this.runBeforeEach(); + try { + return await this.model.create(obj); + } catch (error) { + /* istanbul ignore next */ + throw new BadRequestException(error.toString()); + } + } + + public async delete(cond: object) { + this.checkModel(); + await this.runBeforeAll(); + await this.runBeforeEach(); + try { + return await this.model.findOneAndRemove(cond).exec(); + } catch (error) { + /* istanbul ignore next */ + throw new BadRequestException(error.toString()); + } + } + + public async deleteById(id: ObjectId) { + this.checkModel(); + await this.runBeforeAll(); + await this.runBeforeEach(); + try { + return await this.model.findByIdAndRemove(id).exec(); + } catch (error) { + /* istanbul ignore next */ + throw new BadRequestException(error.toString()); + } + } + + public async modifyById( + id: ObjectId, ctx: object, opts = this.DEF_UPDATE_OPTIONS + ) { + this.checkModel(); + await this.runBeforeAll(); + await this.runBeforeEach(); + const options = Object.assign({ }, this.DEF_UPDATE_OPTIONS, opts); + try { + return await this.model.update({ _id: id }, ctx, options).exec(); + } catch (error) { + /* istanbul ignore next */ + throw new BadRequestException(error.toString()); + } + } + + protected async find(cond: object, opts?: IGetOptions) { + this.checkModel(); + await this.runBeforeAll(); + await this.runBeforeEach(); + const p = this.model.find(cond); + return this.documentQueryProcess(p, opts).exec(); + } + + protected findObjects(cond: object, opts?: IGetOptions) { + return this.find(cond, opts).then((arr) => { + return arr.map((item) => item.toObject()); + }); + } + + protected async findOne(cond: object, opts?: IGetOptions) { + this.checkModel(); + await this.runBeforeAll(); + await this.runBeforeEach(); + const p = this.model.findOne(cond); + return this.documentQueryProcess(p, opts).exec(); + } + + protected findObject(cond: object, opts?: IGetOptions) { + return this.findOne(cond, opts).then((item) => { + return !item ? null : item.toObject(); + }); + } + + protected async findById(id: ObjectId, opts?: IGetOptions) { + this.checkModel(); + await this.runBeforeAll(); + await this.runBeforeEach(); + const p = this.model.findById(id); + return this.documentQueryProcess(p, opts).exec(); + } + + protected findObjectById(id: ObjectId, opts?: IGetOptions) { + return this.findById(id, opts).then((item) => { + return !item ? null : item.toObject(); + }); + } + + protected async total(cond: object = { }) { + this.checkModel(); + await this.runBeforeAll(); + await this.runBeforeEach(); + return this.model.count(cond).exec(); + } + + private documentQueryProcess>( + query: T, opts: IGetOptions = { } + ) { + if (opts.sort) { + query = query.sort(opts.sort); + } + if (isString(opts.populate)) { + opts.populate = [ opts.populate ]; + } + for (const p of (opts.populate || [ ])) { + query = query.populate(p); + } + if (isString(opts.select)) { + opts.select = [ opts.select ]; + } + for (const s of (opts.select || [ ])) { + query = query.select(s); + } + if (opts.perNum && opts.page) { + query = query + .skip((opts.page - 1) * opts.perNum) + .limit(opts.perNum); + } + return query; + } +} + +export abstract class BaseService extends ModelService { + + private cache: keyv; + + protected setCache(cache: keyv) { + this.cache = cache; + } + + protected async loadAndCache( + FLAG: string, value: () => Promise, time?: number + ): Promise; + protected async loadAndCache( + FLAG: string, value: () => T, time = 1000 * 60 * 5 // 5 min + ): Promise { + if (!this.cache) { + return value(); + } + const c: T = await this.cache.get(FLAG); + if (c) { + return c; + } + let val = value(); + if (isPromise(val)) { + val = await val; + } + await this.cache.set(FLAG, val, time); + return val; + } + + protected runBeforeAll() { + return this.loadAndCache("_RunBeforeAll_", () => { + return super.runBeforeAll(); + }); + } + + protected DEF_PER_OBJ = UtilService.DEF_PER_OBJ; + + /** + * 计算页数 + * @param total 总数 + * @param perNum 每页显示数 + */ + public calPageCount = UtilService.calPageCount; + + protected total(cond: object = { }) { + const flag = Object.keys(cond).sort().reduce((f, key) => { + return `${f}_${key}_${cond[key].toString()}`; + }, "total"); + return this.loadAndCache( + flag, () => super.total(cond) + ); + } + +} diff --git a/src/services/categories.service.ts b/src/services/categories.service.ts new file mode 100644 index 0000000..32f285a --- /dev/null +++ b/src/services/categories.service.ts @@ -0,0 +1,144 @@ +import { Component, BadRequestException } from "@nestjs/common"; +import { ObjectId } from "@models/common"; +import { + Model as CategoriesModel, cache, ICategoryRaw, ICategory +} from "@models/Categroy"; +import { isFunction, isArray } from "util"; +import { BaseService, IGetOptions } from "@services/base"; +import { difference } from "lodash"; +import { GoodsService } from "@services/goods"; + +interface IIdMap { + [parentId: string]: ObjectId[]; +} + +@Component() +export class CategoriesService extends BaseService { + + constructor(private readonly goodsSvr: GoodsService) { + super(); + super.setCache(cache); + super.setModel(CategoriesModel); + } + + private getIdMap() { + return this.loadAndCache("IdMap", async () => { + // { parentId: childrenIds } + const map: IIdMap = { }; + const categories = + await this.findObjects({ }, { select: "_id pid" }); + categories.forEach((category) => { + let index; + if (!category.pid) { + index = "*"; + } else { + index = category.pid.toString(); + } + if (!map[index]) { + map[index] = [ ]; + } + map[index].push(category._id.toString()); + }); + return map; + }); + } + + public async getChildrenIds(pid: ObjectId) { + const map = await this.getIdMap(); + const ids: ObjectId[] = [ ]; + const childrenIds = map[pid.toString()]; + if (childrenIds) { + ids.push(...childrenIds); + for (const id of childrenIds) { + ids.push(...(await this.getChildrenIds(id))); + } + } + return ids; + } + + private getTags(obj: ICategoryRaw | ICategory) { + const tags = obj.tags; + const pid = obj.pid as ICategoryRaw | void; + if (pid && pid.tags) { + return tags.concat(this.getTags(pid)); + } + return tags; + } + + public async getByTags(tags: string | string[]) { + if (!isArray(tags)) { + tags = [ tags ]; + } + if (tags.length === 0) { + return Promise.resolve([ ]); + } + const conditions = tags.length === 1 ? { + tags: { $in: tags } + } : { + $or: tags.reduce((arr, tag) => { + arr.push({ tags: { $in: [ tag ] } }); + return arr; + }, []) + }; + const p = (await this.findObjects(conditions, { + populate: [ + "attributes", + { path: "pid", populate: { path: "pid" } } + ] + })) + .map((item) => { + item.tags = Array.from(new Set(this.getTags(item))); + delete item.pid; + return item; + }) + .filter((item) => { + const diffLength = difference(item.tags, tags).length; + return diffLength + tags.length === item.tags.length ; + }); + return p; + } + + /** + * Category 列表 + * @param opts.perNum {number} 每页数量 + * @param opts.page {number} 页数 + * @return {Promise} + */ + public async list(opts = this.DEF_PER_OBJ) { + return this.find({ }, opts); + } + + public count() { + return this.total({ }); + } + + public add(ctx: object) { + return this.create(ctx); + } + + public get(cond: object, opts?: IGetOptions) { + return this.find(cond, opts); + } + + public async getById(id: ObjectId, opts?: IGetOptions) { + return this.findById(id, opts); + } + + public editById(id: ObjectId, ctx: object) { + return this.modifyById(id, ctx); + } + + public async removeById(id: ObjectId) { + const goods = await this.goodsSvr.getByCids(id); + if (goods.length > 0) { + throw new BadRequestException("Some Good in the category"); + } + if ((await this.getChildrenIds(id)).length > 0) { + throw new BadRequestException( + "The Category have some child categories" + ); + } + return this.deleteById(id); + } + +} diff --git a/src/services/collections.service.ts b/src/services/collections.service.ts new file mode 100644 index 0000000..35b93d4 --- /dev/null +++ b/src/services/collections.service.ts @@ -0,0 +1,78 @@ +import { Component, Param, BadRequestException } from "@nestjs/common"; +import { UidDto } from "@dtos/ids"; +import { + Model as CollectionsModel, cache, ICollections +} from "@models/Collection"; +import { ObjectId } from "@models/common"; +import { DEF_PER_COUNT } from "@dtos/page"; +import { IEditCollection } from "../modules/collections/collections.dto"; +import { BaseService, IGetOptions } from "@services/base"; + +@Component() +export class CollectionsService extends BaseService { + + constructor() { + super(); + this.setCache(cache); + this.setModel(CollectionsModel); + } + + public create(obj: object) { + return super.create(obj); + } + + public edit(cid: ObjectId, ctx: IEditCollection) { + return this.modifyById(cid, ctx); + } + + public list(uid: ObjectId, pageObj = this.DEF_PER_OBJ) { + const perNum = pageObj.perNum || this.DEF_PER_OBJ.perNum; + const page = pageObj.page || this.DEF_PER_OBJ.page; + return this.loadAndCache( + `list_${uid.toString()}_${perNum}_${page}`, + () => this.findObjects({ creator: uid }, { + sort: { updatedAt: -1 }, + page, perNum, + populate: [ "creator", "goods" ] + }), + 1000 + ); + } + + public count(uid: ObjectId) { + return this.total({ creator: uid }); + } + + private readonly GET_OPTIONS: IGetOptions = { + populate: [ "creator", "goods" ] + }; + + /** + * Get By Collection Name + * @param name Collection Name + */ + public getByName(name: string, opts = this.GET_OPTIONS) { + return this.loadAndCache( + `getByName_${name}`, + () => this.findObject({ name }, opts), + 1000 + ); + } + + /** + * Get By Collection ID + * @param id Collection ID + */ + public getById(id: ObjectId, opts = this.GET_OPTIONS) { + return this.loadAndCache( + `getById_${id.toString()}`, + () => this.findObjectById(id, opts), + 1000 + ); + } + + public remove(cid: ObjectId) { + return this.deleteById(cid); + } + +} diff --git a/src/services/goods.service.ts b/src/services/goods.service.ts new file mode 100644 index 0000000..98f002e --- /dev/null +++ b/src/services/goods.service.ts @@ -0,0 +1,172 @@ +import { Component, BadRequestException } from "@nestjs/common"; +import { ObjectId } from "@models/common"; +import { Model as GoodsModels, IGoods, IGoodsRaw } from "@models/Good"; +import { ICategory } from "@models/Categroy"; +import { Model as ValuesModel } from "@models/Value"; +import { BaseService, IGetOptions } from "@services/base"; +import { isArray, isObject, isString } from "util"; +import { config } from "@utils/config"; +import fs = require("fs-extra"); + +@Component() +export class GoodsService extends BaseService { + + constructor() { + super(); + this.setModel(GoodsModels); + } + + private getConditionsByCids(cids: ObjectId[]) { + return cids.length === 1 ? { + category: cids[0], + active: true + } : { + $or: cids.reduce((arr, cid) => { + arr.push({ category: { $in: [ cid ] } }); + return arr; + }, [ ]), + active: true + }; + } + + public get(cond: object, opts?: IGetOptions) { + return this.find(cond, opts); + } + + public listByCategoryId(cid: ObjectId, pageObj = this.DEF_PER_OBJ) { + return this.loadAndCache( + `list_category_${cid.toString()}`, + () => this.findObjects({ category: cid }, Object.assign({ + populate: [ "uploader", "attributes" ], + select: "-category" + }, pageObj)), + 1000 + ); + } + + public countByCids(cids: ObjectId | ObjectId[]) { + if (!isArray(cids)) { + cids = [ cids ]; + } + if (cids.length === 0) { + return [ ]; + } + const conditions = this.getConditionsByCids(cids); + return this.total(conditions); + } + + public countByUids(uids: ObjectId | ObjectId[]) { + if (!isArray(uids)) { + uids = [ uids ]; + } + const conditions = this.getConditionsByUids(uids); + return this.total(conditions); + } + + /** + * Get Goods by Category ID(s) + * @param cids Category ID(s) + */ + public getByCids( + cids: ObjectId | ObjectId[], opts = this.DEF_PER_OBJ + ) { + if (!isArray(cids)) { + cids = [ cids ]; + } + const conditions = this.getConditionsByCids(cids); + return this.find(conditions, Object.assign({ + populate: "uploader attributes", + sort: { updatedAt: -1 } + }, opts)); + } + + private getConditionsByUids(uids: ObjectId[]) { + switch (uids.length) { + case 0: + return { }; + case 1: + return { + uploader: uids[0] + }; + default: + return { + $or: uids.reduce((arr, uid) => { + arr.push({ uploader: uid }); + return arr; + }, [ ]) + }; + } + } + + /** + * Get Goods by User ID(s) + * @param uids User ID(s) + */ + public getByUids( + uids: ObjectId | ObjectId[], opts = this.DEF_PER_OBJ + ) { + if (!isArray(uids)) { + uids = [ uids ]; + } + const conditions = this.getConditionsByUids(uids); + return this.find(conditions, Object.assign({ + select: "-uploader", + populate: "attributes", + sort: { updatedAt: -1 } + }, opts)); + } + + /** + * Get Good by Good ID + * @param id Good ID + */ + public getById(id: ObjectId, opts?: IGetOptions) { + return super.findById(id, opts); + } + + /** + * Edit Good by Good ID + * @param id Good ID + */ + public editById(id: ObjectId, ctx: object) { + return this.modifyById(id, ctx); + } + + public add(ctx: object) { + return this.create(ctx); + } + + public async remove(id: ObjectId) { + const good = await this.findObjectById(id); + if (!good) { + throw new BadRequestException("Non Exist Good ID"); + } + this.deleteById(id); + try { + if (good.attributes.length > 0) { + const cond = (good.attributes as ObjectId[]) + .reduce((obj, item) => { + obj.$or.push({ _id: item }); + return obj; + }, { $or: [ ] }); + await ValuesModel.remove(cond).exec(); + } + } catch (error) { + throw new BadRequestException(error.toString()); + } + await fs.remove(this.getFilepath(good)); + } + + public getFilepath(good: IGoods) { + const filename = good.filename; + const cid = + isObject(good.category) && (good.category as ICategory)._id ? + (good.category as ICategory)._id : + good.category; + if (!cid) { + throw new BadRequestException(); + } + return `${config.paths.upload}/${cid}/${filename}`; + } + +} diff --git a/src/services/regexps.service.ts b/src/services/regexps.service.ts new file mode 100644 index 0000000..6d546e9 --- /dev/null +++ b/src/services/regexps.service.ts @@ -0,0 +1,194 @@ +import { Component, BadRequestException } from "@nestjs/common"; +import { ObjectId } from "@models/common"; +import { + Model as RegexpsModel, cache, RegexpDoc, IRegexpDoc, IRegexp +} from "@models/Regexp"; +import { Model as CategroiesModel, ICategory } from "@models/Categroy"; +import { DEF_PER_COUNT } from "@dtos/page"; +import { isArray } from "util"; +import { BaseService, IGetOptions } from "@services/base"; + +export interface IGetRegexpsOptions { + categroies?: ObjectId[]; + appends?: ObjectId[]; +} + +@Component() +export class RegexpsService extends BaseService { + + constructor() { + super(); + this.setCache(cache); + this.setModel(RegexpsModel); + } + + protected async beforeAll() { + // Update + // Add Hidden Label + await RegexpsModel.update( + { hidden: { $exists: false } }, { hidden: false }, + { multi: true } + ).exec(); + } + + /** + * 新增规则 + */ + public async create(obj: IRegexpDoc) { + return super.create(obj); + } + + /** + * 修改规则 + * @param id Regexp ID + */ + public async editById(id: ObjectId, obj: object) { + try { + return await RegexpsModel + .update({ _id: id }, obj, this.DEF_UPDATE_OPTIONS) + .exec(); + } catch (error) { + throw new BadRequestException(error.toString()); + } + } + + /** + * 删除规则 + */ + public async remove(id: ObjectId) { + try { + return await RegexpsModel.findByIdAndRemove(id).exec(); + } catch (error) { + throw new BadRequestException(error.toSrting()); + } + } + + /** + * 规则关联 + * @return {Promise} + */ + public async link(id: ObjectId, linkId?: ObjectId) { + if (!linkId) { + try { + return await RegexpsModel.findByIdAndUpdate(id, { + "$unset": { link: 0 } + }).exec(); + } catch (error) { + throw new BadRequestException(error.toSrting()); + } + } + if (!(await CategroiesModel.findById(linkId).exec())) { + throw new BadRequestException("Nonexist Categroy ID"); + } + try { + return await RegexpsModel + .update({ _id: id }, { link: linkId }, this.DEF_UPDATE_OPTIONS) + .exec(); + } catch (error) { + throw new BadRequestException(error.toSrting()); + } + } + + public count() { + const FLAG = "total"; + return this.loadAndCache( + FLAG, + () => RegexpsModel.count({ }).exec() + ); + } + + public get(conditions: object, opts?: IGetOptions) { + return this.find(conditions, opts); + } + + public async getById(id: ObjectId, opts?: IGetOptions) { + const extraPopulate = { path: "link", populate: { path: "pid" } }; + if (!opts) { + opts = { }; + } + if (opts.populate && isArray(opts.populate)) { + opts.populate.push(extraPopulate); + } else { + opts.populate = [ extraPopulate ]; + } + return (await this.get({ _id: id }, opts))[0]; + } + + /** + * 规则列表 + * @param perNum {number} 每页数量 + * @param page {number} 页数 + * @return {Promise} + */ + public list( + perNum = this.DEF_PER_OBJ.perNum, page = this.DEF_PER_OBJ.page + ) { + const FLAG = `list_${perNum}_${page}`; + return this.loadAndCache( + FLAG, + () => this.findObjects({ }, { + perNum, page, + populate: "link" + }), + 1000 + ); + } + + private getRegexps(opts: IGetRegexpsOptions = { }) { + const DEF_CONDITIONS = { + link: { $exists: true }, hidden: false + }; + const DEF_OPTIONS = { + populate: [ "link" ] + }; + + if (opts.categroies && opts.categroies.length > 0) { + // 指定Categroy + const FLAG = `categroies_scan_regexps_${opts.categroies.join("_")}`; + const conditions = { + $or: opts.categroies.reduce((arr, item) => { + arr.push({ link: item }); + return arr; + }, [ ]) + }; + return this.loadAndCache( + FLAG, () => this.findObjects(conditions, DEF_OPTIONS), 1000 + ); + } else if (opts.appends && opts.appends.length > 0) { + // 追加Categroy + const FLAG = `appends_scan_regexps_${opts.appends.join("_")}`; + const conditions = { + $or: opts.appends.reduce((arr: any, item) => { + arr.push({ link: item }); + return arr; + }, [ DEF_CONDITIONS ]) + }; + return this.loadAndCache( + FLAG, () => this.findObjects(conditions, DEF_OPTIONS), 1000 + ); + } else { + const FLAG = "default_scan_regexps"; + return this.loadAndCache( + FLAG, () => this.findObjects(DEF_CONDITIONS, DEF_OPTIONS), 1000 + ); + } + } + + /** + * 根据规则进行识别 + * @return {Promise} + */ + public async discern(name: string, opts?: IGetRegexpsOptions) { + const result = await this.getRegexps(opts); + const list = [ ]; + result.forEach((item) => { + const obj = item; + const reg = new RegExp(obj.value); + if (reg.test(name)) { + list.push(obj.link); + } + }); + return list as ICategory[]; + } + +} diff --git a/src/services/system.service.ts b/src/services/system.service.ts new file mode 100644 index 0000000..eb18317 --- /dev/null +++ b/src/services/system.service.ts @@ -0,0 +1,185 @@ +import { Component, BadRequestException } from "@nestjs/common"; +import { ObjectId } from "@models/common"; +import { Model as SystemModel, cache, ISystem } from "@models/System"; +import { Model as UsergroupsModel } from "@models/Usergroup"; +import { BaseService, IGetOptions } from "@services/base"; +import { isURL } from "validator"; +import * as typescript from "typescript"; + +import { systemLogger } from "@utils/log"; + +export enum DEFAULTS { + USERGROUP_FLAG = "DEFAULT_USERGROUP", + GOOD_URL_FLAG = "DEFAULT_GOOD_URL", + COLLECTION_URL_FLAG = "DEFAULT_COLLECTION_URL" +} + +@Component() +export class SystemService extends BaseService { + + constructor() { + super(); + super.setCache(cache); + super.setModel(SystemModel); + } + + private readonly DEFAULTS_MAPS = { + [DEFAULTS.USERGROUP_FLAG]: { + get: this.getDefaultUsergroup.name, + set: this.setDefaultUsergroup.name + }, + [DEFAULTS.GOOD_URL_FLAG]: { + get: this.getDefaultGoodUrl.name, + set: this.setDefaultGoodUrl.name + }, + [DEFAULTS.COLLECTION_URL_FLAG]: { + get: this.getDefaultCollectionUrl.name, + set: this.setDefaultCollectionUrl.name + } + }; + + private async checkUsergroupId(gid: ObjectId) { + const doc = await UsergroupsModel.findById(gid).exec(); + /* istanbul ignore if */ + if (!doc) { + throw new BadRequestException("The ID isnt a Usergroup ID"); + } + return true; + } + + private async checkUrl(url: string) { + if (!isURL(url)) { + throw new BadRequestException("URL Parse Fail"); + } + return true; + } + + private setValue(key: string, value: string) { + try { + return SystemModel.findOneAndUpdate( + { key: key }, { value: value }, { upsert: true } + ).exec(); + } catch (error) { + throw new BadRequestException(error.toString()); + } + } + + // region Default Usergroup ID + /** + * Get Default Usergroup ID + * @returns Usergroup ID + */ + public getDefaultUsergroup() { + const FLAG = DEFAULTS.USERGROUP_FLAG; + return this.loadAndCache(FLAG, async () => { + const obj = await this.findObject({ key: FLAG }); + let gid = obj.value; + /* istanbul ignore if */ + if (!gid) { + systemLogger.warn(`Miss ${FLAG}`); + gid = (await UsergroupsModel.findOne().exec())._id; + this.setDefaultUsergroup(gid); + } + return gid; + }); + } + + /** + * Set Default Usergroup ID + * @param gid Usergroup ID + */ + public async setDefaultUsergroup(gid: ObjectId) { + await this.checkUsergroupId(gid); + return await this.setValue(DEFAULTS.USERGROUP_FLAG, gid.toString()); + } + // endregion Default Usergroup ID + + // region Default Urls + public getDefaultGoodUrl() { + const FLAG = DEFAULTS.GOOD_URL_FLAG; + return this.loadAndCache(FLAG, async () => { + const obj = await this.findObject({ key: FLAG }); + return obj ? obj.value : ""; + }); + } + + public setDefaultGoodUrl(url: string) { + if (url.length !== 0) { + if (!url.includes("{{gid}}")) { + throw new BadRequestException("Url must include `{{gid}}`"); + } + this.checkUrl(url); + } + return this.setValue(DEFAULTS.GOOD_URL_FLAG, url); + } + + public getDefaultCollectionUrl() { + const FLAG = DEFAULTS.COLLECTION_URL_FLAG; + return this.loadAndCache(FLAG, async () => { + const obj = await this.findObject({ key: FLAG }); + return obj ? obj.value : ""; + }); + } + + public setDefaultCollectionUrl(url: string) { + if (url.length !== 0) { + if (!url.includes("{{cid}}")) { + throw new BadRequestException("Url must include `{{cid}}`"); + } + this.checkUrl(url); + } + return this.setValue(DEFAULTS.COLLECTION_URL_FLAG, url); + } + // endregion Default Urls + + public get() { + return this.loadAndCache("get", async () => { + const objs: Array<{ key: string, value: string }> = + await this.findObjects({ }, { select: "key value -_id" }); + const keys = objs.reduce((arr, item) => { + arr.push(item.key); + return arr; + }, [ ]); + if (objs.length === Object.keys(DEFAULTS).length) { + return objs; + } + for (const k of Object.keys(DEFAULTS)) { + if (!!~keys.indexOf(DEFAULTS[k])) { + continue; + } + const key = DEFAULTS[k]; + objs.push({ + key, value: await this[this.DEFAULTS_MAPS[key].get]() + }); + } + return objs; + }); + } + + public set(key: DEFAULTS, value: any) { + switch (key) { + case DEFAULTS.USERGROUP_FLAG: + return this[this.DEFAULTS_MAPS[key].set](value); + case DEFAULTS.GOOD_URL_FLAG: + return this[this.DEFAULTS_MAPS[key].set](value); + case DEFAULTS.COLLECTION_URL_FLAG: + return this[this.DEFAULTS_MAPS[key].set](value); + } + throw new BadRequestException("What do you want to set up"); + } + + public async info() { + return { + version: { + typescript: typescript.version, + api: require("../../package.json").version, + node: process.versions + }, + env: { + system: process.env, + service: await this.get() + } + }; + } + +} diff --git a/src/modules/common/services/tokens.service.ts b/src/services/tokens.service.ts similarity index 56% rename from src/modules/common/services/tokens.service.ts rename to src/services/tokens.service.ts index 913a29a..63c5ff2 100644 --- a/src/modules/common/services/tokens.service.ts +++ b/src/services/tokens.service.ts @@ -1,21 +1,21 @@ +import { BaseService } from "@services/base"; import { Component, BadRequestException } from "@nestjs/common"; import { ObjectId } from "@models/common"; -import { Model as TokensModel } from "@models/Token"; +import { Model as TokensModel, cache } from "@models/Token"; import { IUser } from "@models/User"; import { IUidDto } from "@dtos/ids"; @Component() -export class TokensService { +export class TokensService extends BaseService { - public create(obj) { - return TokensModel.create(obj); + constructor() { + super(); + this.setCache(cache); + this.setModel(TokensModel); } public getRawTokens(uid: ObjectId) { - return TokensModel - .find({ user: uid }) - .select("-user") - .exec(); + return this.find({ user: uid }, { select: "-uesr" }); } public async getTokens(uid: ObjectId) { @@ -28,16 +28,15 @@ export class TokensService { } public async getIdByToken(token: string) { - const t = await TokensModel.findOne({ token: token }); + const t = await this.findOne({ token }); if (t) { return t._id as ObjectId; - } else { - throw new BadRequestException("The token isnt exist"); } + throw new BadRequestException("The token isnt exist"); } - public remove(obj) { - return TokensModel.findOneAndRemove(obj).exec(); + public remove(obj: object) { + return this.delete(obj); } /** @@ -46,9 +45,11 @@ export class TokensService { * @param token */ public async isVaild(username: string, token: string) { - const t = await TokensModel.findOne({ token }) - .populate("user").exec(); - const tokenOwn = t.toObject().user as IUser; + const obj = await this.findObject({ token }, { populate: "user" }); + if (!obj) { + return false; + } + const tokenOwn = obj.user as IUser; return tokenOwn.username === username && tokenOwn.active; } diff --git a/src/services/usergroups.service.ts b/src/services/usergroups.service.ts new file mode 100644 index 0000000..184f0dd --- /dev/null +++ b/src/services/usergroups.service.ts @@ -0,0 +1,119 @@ +import { Component, BadRequestException } from "@nestjs/common"; +import { Model as UsersModel } from "@models/User"; +import { + Model as UsergroupsModel, IUsergroups, cache +} from "@models/Usergroup"; +import { Model as UserUsergroupsModel } from "@models/User-Usergroup"; +import { ObjectId } from "@models/common"; +import { BaseService } from "@services/base"; + +@Component() +export class UsergroupsService extends BaseService { + + constructor() { + super(); + this.setCache(cache); + this.setModel(UsergroupsModel); + } + + public add(obj: object) { + return this.create(obj); + } + + public async edit(id: ObjectId, obj: object) { + try { + return await UsergroupsModel + .update({ _id: id }, obj, this.DEF_UPDATE_OPTIONS) + .exec(); + } catch (error) { + throw new BadRequestException(error.toString()); + } + } + + public usersCount(gid: ObjectId) { + return UserUsergroupsModel.count({ usergroup: gid }).exec(); + } + + public getGroup(gid: ObjectId) { + return this.findById(gid); + } + + public async getGroupUsers( + gid: ObjectId, pageObj = this.DEF_PER_OBJ + ) { + const perNum = pageObj.perNum; + const page = pageObj.page; + return (await UserUsergroupsModel.find({ usergroup: gid }) + .skip((page - 1) * perNum).limit(perNum) + .populate("user").exec() + ).map((item) => { + return item.toObject().user; + }); + } + + public count() { + return this.total({ }); + } + + public list(pageObj = this.DEF_PER_OBJ) { + const perNum = pageObj.perNum || this.DEF_PER_OBJ.perNum; + const page = pageObj.page || this.DEF_PER_OBJ.page; + return this.loadAndCache( + `list_${perNum}_${page}`, + () => this.findObjects({ }, { + perNum, page, sort: { createdAt: -1 } + }) + ); + } + + /** + * Remove Usergroup By Usergroup ID + * @param gid Usergroup ID + */ + public async remove(gid: ObjectId) { + if ((await this.count()) === 1) { + throw new BadRequestException("Cant delete unique group"); + } + const p = await this.deleteById(gid); + try { + await UserUsergroupsModel.findOneAndRemove({ + usergroup: gid + }).exec(); + return p; + } catch (error) { + throw new BadRequestException(error.toString()); + } + } + + public async addUserToGroup(gid: ObjectId, uid: ObjectId) { + if (!(await UsersModel.findById(uid).exec())) { + throw new BadRequestException("The User ID is not exist"); + } + if (!(await UsergroupsModel.findById(gid).exec())) { + throw new BadRequestException("The Usergroup ID is not exist"); + } + if (await UserUsergroupsModel.findOne({ + user: uid, usergroup: gid + }).exec()) { + throw new BadRequestException("User has been in the usergroup"); + } + try { + return await UserUsergroupsModel.create({ + user: uid, usergroup: gid + }); + } catch (error) { + throw new BadRequestException(error.toString()); + } + } + + public async removeUserFromGroup(gid: ObjectId, uid: ObjectId) { + try { + await UserUsergroupsModel.findOneAndRemove({ + user: uid, usergroup: gid + }).exec(); + } catch (error) { + throw new BadRequestException(error.toString()); + } + } + +} diff --git a/src/services/users.service.ts b/src/services/users.service.ts new file mode 100644 index 0000000..52bcd24 --- /dev/null +++ b/src/services/users.service.ts @@ -0,0 +1,126 @@ +import { Component, BadRequestException } from "@nestjs/common"; +import { ObjectId } from "@models/common"; +import { Model as UsersModel, UserDoc, IUser, cache } from "@models/User"; +import { Model as UserUsergroupsModel } from "@models/User-Usergroup"; +import { IUsergroups } from "@models/Usergroup"; +import { SystemService } from "@services/system"; +import { BaseService, IGetOptions } from "@services/base"; + +@Component() +export class UsersService extends BaseService { + + constructor(private readonly sysSvr: SystemService) { + super(); + this.setCache(cache); + this.setModel(UsersModel); + } + + public async addUser(obj, gid?: ObjectId) { + try { + const user = await UsersModel.addUser(obj.username, obj.password); + if (!gid) { + gid = await this.sysSvr.getDefaultUsergroup(); + } + await UserUsergroupsModel.create({ + user: user._id, usergroup: gid + }); + return user; + } catch (error) { + throw new BadRequestException(error.toString()); + } + } + + public async removeUser(uid: ObjectId) { + await this.deleteById(uid); + try { + await UserUsergroupsModel.remove({ user: uid }).exec(); + } catch (error) { + throw new BadRequestException(error.toString()); + } + } + + public async isVaild(username: string, password: string) { + try { + return await UsersModel.isVaild(username, password); + } catch (err) { + throw new BadRequestException(err.toString()); + } + } + + public countUsergroups(uid: ObjectId) { + return UserUsergroupsModel.count({ user: uid }).exec(); + } + + public async getUsergroups( + uid: ObjectId, pageObj = this.DEF_PER_OBJ + ) { + const perNum = pageObj.perNum || this.DEF_PER_OBJ.perNum; + const page = pageObj.page || this.DEF_PER_OBJ.page; + const groups = await UserUsergroupsModel + .find({ user: uid }).populate("usergroup") + .skip((page - 1) * perNum).limit(perNum) + .exec(); + return groups.map((item) => { + return item.toObject().usergroup as IUsergroups; + }); + } + + /** + * 修改`User`属性, 除了`username`和`password` + * @param id User ID + * @param content Content + */ + public async modify(id: ObjectId, content): Promise { + for (const field of [ "username", "password" ]) { + if (content && content[field]) { + delete content[field]; + } + } + if (Object.keys(content).length === 0) { + throw new BadRequestException("Empty Content"); + } + return this.modifyById(id, content); + } + + public async passwd(id: ObjectId, oldPass: string, newPass: string) { + try { + return await UsersModel.passwd(id, oldPass, newPass); + } catch (error) { + throw new BadRequestException(error.toString()); + } + } + + /** + * 返回总数 + */ + public conut() { + return this.total({ }); + } + + /** + * 获取用户列表 + * + * @param opts.perNum 每页数量 + * @param opts.page {number} 页数 + */ + public list(opts = this.DEF_PER_OBJ) { + const perNum = opts.perNum || this.DEF_PER_OBJ.perNum; + const page = opts.page || this.DEF_PER_OBJ.page; + const Flag = `list_${perNum}_${page}`; + return this.loadAndCache( + Flag, + () => this.findObjects({ }, { + select: "-password", + perNum, page + }) + ); + } + + /** + * Get User By User ID + * @param id User ID + */ + public getById(id: ObjectId, opts?: IGetOptions) { + return this.findById(id, opts); + } +} diff --git a/src/services/util.service.ts b/src/services/util.service.ts new file mode 100644 index 0000000..552eb47 --- /dev/null +++ b/src/services/util.service.ts @@ -0,0 +1,44 @@ +import { IPerPage, DEF_PER_COUNT, ListResponse } from "@dtos/page"; + +interface IToListResponeOptions extends IPerPage { + total?: number; +} + +export abstract class UtilService { + + public static DEF_PER_OBJ: IPerPage = { + perNum: DEF_PER_COUNT, + page: 1 + }; + + /** + * 计算页数 + * @param total 总数 + * @param perNum 每页显示数 + */ + public static calPageCount( + total: number, perNum = UtilService.DEF_PER_OBJ.perNum + ) { + return Math.ceil(total / perNum); + } + + public static toListRespone( + arr: T[], opts: IToListResponeOptions = UtilService.DEF_PER_OBJ + ) { + const perNum = opts.perNum || UtilService.DEF_PER_OBJ.perNum; + const listRes = new ListResponse(); + listRes.total = opts.total || arr.length; + listRes.current = opts.page || UtilService.DEF_PER_OBJ.page; + listRes.totalPages = + UtilService.calPageCount(listRes.total, perNum); + + if (opts.total && listRes.totalPages >= listRes.current) { + listRes.data = arr; + } else { + const startIndex = (listRes.current - 1) * perNum; + listRes.data = arr.slice(startIndex, startIndex + perNum); + } + return listRes; + } + +} diff --git a/src/utils/config.d.ts b/src/utils/config.d.ts index fd629e6..6f3df17 100644 --- a/src/utils/config.d.ts +++ b/src/utils/config.d.ts @@ -19,8 +19,28 @@ interface paths { backup: string; } +interface redis { + url: string; +} + +interface IDefaults { + user: IDefaultUser; + group: IDefaultUsergroup; +} + +interface IDefaultUser { + name: string; + pass: string; +} + +interface IDefaultUsergroup { + name: string; +} + export interface ConfigObj { + redis: redis; server: server; db: db; paths: paths; -} \ No newline at end of file + defaults: IDefaults; +} diff --git a/src/utils/config.ts b/src/utils/config.ts index f4562f2..7079b1d 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -18,14 +18,44 @@ for (const filepath of CONFIG_FILEPATHS) { } } +// region Paths Process for (const item of Object.keys(configModule.paths)) { const curPath = configModule.paths[item]; if (!ph.isAbsolute(configModule.paths[item])) { configModule.paths[item] = ph.resolve(`${__dirname}/../../${curPath}`); } } +// endregion Paths Process + +// region Redis Process +configModule.redis.url = configModule.redis.url || +`redis://${configModule.redis.host}:${configModule.redis.port}`; +if ( + configModule.redis.url.indexOf("redis://") !== 0 && + configModule.redis.url.indexOf("//") !== 0 +) { + configModule.redis.url = `redis://${configModule.redis.url}`; +} +// endregion Redis Process + +// region Default Values Check +try { + const values = [ + configModule.defaults.user.name, + configModule.defaults.user.pass, + configModule.defaults.group.name + ]; + for (const item of values) { + if (item.length < 4) { + throw new TypeError("Default Value Must great than 4 words."); + } + } +} catch (error) { + throw new TypeError(error.toString()); +} +// endregion Default Values Check export const config = configModule.getConfig() as ConfigObj; -import { systemLogger } from "../modules/common/helper/log"; +import { systemLogger } from "./log"; systemLogger.debug(config); diff --git a/src/modules/common/helper/env.ts b/src/utils/env.ts similarity index 100% rename from src/modules/common/helper/env.ts rename to src/utils/env.ts diff --git a/src/modules/common/helper/log.ts b/src/utils/log.ts similarity index 100% rename from src/modules/common/helper/log.ts rename to src/utils/log.ts diff --git a/src/utils/newCache.ts b/src/utils/newCache.ts new file mode 100644 index 0000000..ee585ad --- /dev/null +++ b/src/utils/newCache.ts @@ -0,0 +1,10 @@ +import keyv = require("keyv"); +import { isTest } from "@utils/env"; +import { config } from "@utils/config"; + +export = (namespace: string) => { + return new keyv({ + uri: isTest ? undefined : config.redis.url, + namespace + }); +}; diff --git a/test/api/categroies.e2e.ts b/test/api/categories/categories.e2e.ts similarity index 61% rename from test/api/categroies.e2e.ts rename to test/api/categories/categories.e2e.ts index c1d0f21..a3500fd 100644 --- a/test/api/categroies.e2e.ts +++ b/test/api/categories/categories.e2e.ts @@ -1,8 +1,10 @@ +import { newIds } from "../../helpers/utils"; import supertest = require("supertest"); -import faker = require("faker"); -import { connect, drop, newUser } from "../helpers/database"; -import { init } from "../helpers/server"; +import { connect, drop, newUser } from "../../helpers/database"; +import { init } from "../../helpers/server"; +import { login } from "../../helpers/database/auth"; +import { newName } from "../../helpers/utils"; describe("Categories E2E Api", () => { @@ -12,11 +14,7 @@ describe("Categories E2E Api", () => { return connect(); }); - const ids = { - users: [ ], - categories: [ ], - values: [ ] - }; + const ids = newIds(); after(() => { return drop(ids); @@ -26,22 +24,13 @@ describe("Categories E2E Api", () => { request = await init(); }); - const user = { - name: faker.name.firstName(), - pass: faker.random.words() - }; - step("Login", async () => { - const doc = await newUser(user.name, user.pass); - ids.users.push(doc._id); - await request.post("/api/v1/auth/login") - .send({ - username: user.name, password: user.pass - }).then(); + step("login", async () => { + ids.users.push((await login(request))[0]); }); step("Add Category", async () => { const ctx = { - name: faker.name.firstName() + name: newName() }; const { body: result } = await request.post("/api/v1/categories") .send(ctx) @@ -56,11 +45,11 @@ describe("Categories E2E Api", () => { step("Add Category with Tags", async () => { const ctx = { - name: faker.name.firstName(), + name: newName(), tags: [ - faker.random.words(), - faker.random.words(), - faker.random.words() + newName(), + newName(), + newName() ] }; const { body: result } = await request.post("/api/v1/categories") @@ -76,17 +65,17 @@ describe("Categories E2E Api", () => { step("Add Category with Attributes", async () => { const ctx = { - name: faker.name.firstName(), + name: newName(), attributes: [ { - key: faker.random.words(), - value: faker.random.words() + key: newName(), + value: newName() }, { - key: faker.random.words(), - value: faker.random.words() + key: newName(), + value: newName() }, { - key: faker.random.words(), - value: faker.random.words() + key: newName(), + value: newName() } ].map((item) => JSON.stringify(item)) }; diff --git a/test/api/collections.e2e.ts b/test/api/collections/collections.e2e.ts similarity index 82% rename from test/api/collections.e2e.ts rename to test/api/collections/collections.e2e.ts index 3bf5f43..43dd9ac 100644 --- a/test/api/collections.e2e.ts +++ b/test/api/collections/collections.e2e.ts @@ -7,10 +7,13 @@ import { Model as GoodsModels } from "@models/Good"; import { connect, drop, newUser, addCategoryAndRegexp -} from "../helpers/database"; -import { init } from "../helpers/server"; -import { uploadFiles } from "../helpers/files"; -import { sleep } from "../helpers/utils"; +} from "../../helpers/database"; +import { init } from "../../helpers/server"; +import { uploadFiles } from "../../helpers/files"; +import { sleep, newName, newIds } from "../../helpers/utils"; +import auth = require("@db/auth"); +import goodsDb = require("@db/goods"); +import files = require("../../helpers/files"); describe("Collections E2E Api", () => { @@ -20,12 +23,7 @@ describe("Collections E2E Api", () => { return connect(); }); - const ids = { - users: [ ], - categories: [ ], - regexps: [ ], - collections: [ ] - }; + const ids = newIds(); after(() => { return drop(ids); @@ -38,45 +36,21 @@ describe("Collections E2E Api", () => { const FILE_COUNST = 10; const filepaths = [ ]; const prefix = `${faker.random.word()}_`; - before(() => { - const folderpath = `${config.paths.tmp}/test`; - if (!fs.existsSync(folderpath)) { - fs.mkdirpSync(folderpath); - } + before(async () => { // Generator Files for (let i = 0; i < FILE_COUNST; i++) { - const filepath = `${folderpath}/${prefix}${faker.random.uuid()}`; + const filename = `${prefix}${faker.random.uuid()}`; + const filepath = await files.newFile(filename); filepaths.push(filepath); - fs.writeFileSync(filepath, JSON.stringify({ - data: Math.random() - }), { encoding: "utf-8" }); } }); - after(async () => { - for (const filepath of filepaths) { - fs.removeSync(filepath); - const good = (await GoodsModels.findOne({ - originname: basename(filepath) - }).exec()).toObject(); - fs.removeSync( - `${config.paths.upload}/${good.category}/${good.filename}` - ); - await GoodsModels.findByIdAndRemove(good._id).exec(); - } + after(() => { + return files.remove(filepaths); }); - const user = { - name: faker.name.firstName(), - pass: faker.random.words() - }; - step("Login", async () => { - const doc = await newUser(user.name, user.pass); - ids.users.push(doc._id); - await request.post("/api/v1/auth/login") - .send({ - username: user.name, password: user.pass - }).then(); + step("login", async () => { + ids.users.push((await auth.login(request))[0]); }); step("Add Category and Regexp", async () => { @@ -94,12 +68,18 @@ describe("Collections E2E Api", () => { ids.collections.push(result._id); status.should.be.eql(201); result.should.have.properties("name", "_id", "goods"); + for (const good of result.goods) { + const originname = good.originname; + ids.goods.push(await goodsDb.getIdByOriginname(originname)); + } }); step("User's Collection List", async () => { + const userId = ids.users[ids.users.length - 1]; + const url = `/api/v1/users/${userId}/collections`; const { body: result, status - } = await request.get("/api/v1/users/collections").then(); + } = await request.get(url).then(); status.should.be.eql(200); result.should.have.properties("total", "data"); result.data.should.be.an.Array() @@ -111,8 +91,8 @@ describe("Collections E2E Api", () => { step("Add Other User", async () => { const user = { - name: faker.name.firstName(), - pass: faker.random.words() + name: newName(), + pass: newName() }; const doc = await newUser(user.name, user.pass); ids.users.push(doc._id); @@ -120,9 +100,10 @@ describe("Collections E2E Api", () => { step("Other User's Collection List", async () => { const userId = ids.users[ids.users.length - 1]; + const url = `/api/v1/users/${userId}/collections`; const { body: result, status - } = await request.get(`/api/v1/users/${userId}/collections`).then(); + } = await request.get(url).then(); status.should.be.eql(200); result.should.have.properties("total", "data"); result.data.should.be.an.Array() @@ -153,8 +134,8 @@ describe("Collections E2E Api", () => { } = await request.get(`/api/v1/collections/${collectionId}`).then(); status.should.be.eql(200); result.should.have.properties("name", "goods", "creator"); - result.goods.should.be.an.Array().which.have.length(FILE_COUNST); - goods = result.goods; + result.goods.data.should.be.an.Array().which.have.length(FILE_COUNST); + goods = result.goods.data; }); step("Add New Collection", async () => { @@ -293,7 +274,7 @@ describe("Collections E2E Api", () => { body: result } = await request.get(`/api/v1/collections/${id}`) .then(); - result.goods.should.have.length(4); + result.goods.data.should.have.length(4); }); step("Delete by DELETE METHOD", async () => { diff --git a/test/api/collections_token.e2e.ts b/test/api/collections/collections_token.e2e.ts similarity index 80% rename from test/api/collections_token.e2e.ts rename to test/api/collections/collections_token.e2e.ts index c3ac298..e48131c 100644 --- a/test/api/collections_token.e2e.ts +++ b/test/api/collections/collections_token.e2e.ts @@ -8,9 +8,10 @@ import { Model as GoodsModels } from "@models/Good"; import { connect, drop, newUser, addCategoryAndRegexp -} from "../helpers/database"; -import { init } from "../helpers/server"; -import { uploadFiles } from "../helpers/files"; +} from "../../helpers/database"; +import { init } from "../../helpers/server"; +import { uploadFiles, newFile } from "../../helpers/files"; +import { newIds } from "../../helpers/utils"; describe("Token to Upload Files Api", () => { @@ -20,13 +21,7 @@ describe("Token to Upload Files Api", () => { return connect(); }); - const ids = { - users: [ ], - tokens: [ ], - collections: [ ], - categories: [ ], - regexps: [ ] - }; + const ids = newIds(); after(() => { return drop(ids); @@ -39,18 +34,10 @@ describe("Token to Upload Files Api", () => { const FILE_COUNST = 10; const filepaths = [ ]; const prefix = `${faker.random.word()}_`; - before(() => { - const folderpath = `${config.paths.tmp}/test`; - if (!fs.existsSync(folderpath)) { - fs.mkdirpSync(folderpath); - } + before(async () => { // Generator Files for (let i = 0; i < FILE_COUNST; i++) { - const filepath = `${folderpath}/${prefix}${faker.random.uuid()}`; - filepaths.push(filepath); - fs.writeFileSync(filepath, JSON.stringify({ - data: Math.random() - }), { encoding: "utf-8" }); + filepaths.push(await newFile(`${prefix}${faker.random.uuid()}`)); } }); diff --git a/test/api/files.e2e.ts b/test/api/files.e2e.ts index fb16ce2..bee64e8 100644 --- a/test/api/files.e2e.ts +++ b/test/api/files.e2e.ts @@ -1,12 +1,12 @@ import supertest = require("supertest"); import path = require("path"); -import faker = require("faker"); import { HttpStatus } from "@nestjs/common"; import { connect, drop, addCategoryAndRegexp, newUser } from "../helpers/database"; import { uploadFile } from "../helpers/files"; -import { sleep } from "../helpers/utils"; +import { sleep, newIds } from "../helpers/utils"; import { init, initWithAuth } from "../helpers/server"; +import auth = require("@db/auth"); describe("Files E2E Api", () => { @@ -16,12 +16,7 @@ describe("Files E2E Api", () => { connect(); }); - const ids = { - users: [ ], - regexps: [ ], - categories: [ ], - goods: [ ] - }; + const ids = newIds(); after(() => { return drop(ids); @@ -35,17 +30,8 @@ describe("Files E2E Api", () => { return `/files/categories/${cid}/goods/${id}`; }; - const user = { - name: faker.name.firstName(), - pass: faker.random.words() - }; - step("Login", async () => { - const doc = await newUser(user.name, user.pass); - ids.users.push(doc._id); - await request.post("/api/v1/auth/login") - .send({ - username: user.name, password: user.pass - }).then(); + before("login", async () => { + ids.users.push((await auth.login(request))[0]); }); let cid = ""; @@ -54,8 +40,10 @@ describe("Files E2E Api", () => { step("Upload File", async () => { const docs = await addCategoryAndRegexp(/^icon_.+64x64\.png$/); cid = docs[0]._id; - const uploadInfo = await uploadFile(request, uploadFilepath); - id = uploadInfo.body._id; + const { body: result, status } = + await uploadFile(request, uploadFilepath); + status.should.be.eql(201); + id = result._id; ids.categories.push(docs[0]._id); ids.regexps.push(docs[1]._id); ids.goods.push(id); diff --git a/test/api/goods.e2e.ts b/test/api/goods/goods.e2e.ts similarity index 67% rename from test/api/goods.e2e.ts rename to test/api/goods/goods.e2e.ts index d6dc327..c6a460d 100644 --- a/test/api/goods.e2e.ts +++ b/test/api/goods/goods.e2e.ts @@ -1,31 +1,25 @@ import supertest = require("supertest"); import path = require("path"); -import faker = require("faker"); -import { connect, drop, addCategoryAndRegexp, newUser } from "../helpers/database"; -import { uploadFile } from "../helpers/files"; -import { init } from "../helpers/server"; +import { connect, drop, addCategoryAndRegexp } from "../../helpers/database"; +import { uploadFile } from "../../helpers/files"; +import { init } from "../../helpers/server"; +import auth = require("@db/auth"); +import { newName, newIds } from "../../helpers/utils"; describe("Goods E2E Api", () => { let request: supertest.SuperTest; const user = { - name: faker.name.firstName(), - pass: faker.random.words() + name: newName() }; before(async () => { connect(); }); - const ids = { - users: [ ], - categories: [ ], - values: [ ], - regexps: [ ], - goods: [ ] - }; + const ids = newIds(); after(() => { return drop(ids); @@ -35,13 +29,8 @@ describe("Goods E2E Api", () => { request = await init(); }); - step("Login", async () => { - const doc = await newUser(user.name, user.pass); - ids.users.push(doc._id); - const { body: result } = await request.post("/api/v1/auth/login") - .send({ - username: user.name, password: user.pass - }).then(); + before("login", async () => { + ids.users.push((await auth.login(request, user.name))[0]); }); step("Add Category", async () => { @@ -52,7 +41,7 @@ describe("Goods E2E Api", () => { let result; step("Upload File", async () => { - const filepath = `${__dirname}/../files/icon_pandorabox_64x64.png`; + const filepath = `${__dirname}/../../files/icon_pandorabox_64x64.png`; // Create result = await uploadFile(request, filepath); result = result.body; diff --git a/test/api/goods/goods_append.e2e.ts b/test/api/goods/goods_append.e2e.ts new file mode 100644 index 0000000..5ce35a9 --- /dev/null +++ b/test/api/goods/goods_append.e2e.ts @@ -0,0 +1,91 @@ +import * as supertest from "supertest"; +import path = require("path"); +import { init } from "../../helpers/server"; +import db = require("../../helpers/database"); +import auth = require("@db/auth"); +import goods = require("@db/goods"); +import regexps = require("@db/regexps"); +import files = require("../../helpers/files"); +import categories = require("@db/categories"); +import { newName, newIds } from "../../helpers/utils"; + +describe("Upload Good with Append categories", () => { + + let request: supertest.SuperTest; + + before(async () => { + request = await init(); + }); + + before(() => { + return db.connect(); + }); + + const ids = newIds(); + + after(() => { + return db.drop(ids); + }); + + const filepaths = [ ]; + after(() => { + return files.remove(filepaths); + }); + + let cids = [ ]; + before(async () => { + cids = await categories.addCategories(); + ids.categories.push(...cids); + }); + + before("login", async () => { + ids.users.push((await auth.login(request))[0]); + }); + + step("Add File and its Regexps", async () => { + for (let i = 0; i < 2; i++) { + const filepath = await files.newFile(); + filepaths.push(filepath); + const filename = path.basename(filepath); + + ids.regexps.push((await regexps.newRegexp({ + name: newName(), + value: new RegExp(filename).source, + link: ids.categories[6], + hidden: true + }))._id); + ids.regexps.push((await regexps.newRegexp({ + name: newName(), + value: new RegExp(filename).source, + link: ids.categories[7], + hidden: false + }))._id); + } + }); + + step("Upload File Fail", async () => { + const targetIndex = 6; + const targetId = ids.categories[targetIndex]; + const filepath = filepaths[0]; + const filename = path.basename(filepath); + + const { status, body: result } = await files.uploadFile( + request, filepath, { query: { + "append": encodeURI(await categories.getNameById(targetId)) + }} + ); + status.should.be.eql(400); + }); + + step("Upload File Success", async () => { + const targetIndex = 6; + const targetId = ids.categories[targetIndex]; + const filepath = filepaths[0]; + const filename = path.basename(filepath); + + const { status } = await files.uploadFile(request, filepath); + status.should.be.eql(201); + ids.goods.push(await goods.getIdByOriginname(filename)); + }); + +}); diff --git a/test/api/goods/goods_specified_category.e2e.ts b/test/api/goods/goods_specified_category.e2e.ts new file mode 100644 index 0000000..95cd205 --- /dev/null +++ b/test/api/goods/goods_specified_category.e2e.ts @@ -0,0 +1,140 @@ +import * as supertest from "supertest"; +import db = require("../../helpers/database"); +import path = require("path"); +import files = require("../../helpers/files"); +import auth = require("@db/auth"); +import categories = require("@db/categories"); +import * as regexps from "@db/regexps"; +import * as goods from "@db/goods"; +import { init } from "../../helpers/server"; +import { newName, newIds } from "../../helpers/utils"; + +describe("Upload Good with specified categories", () => { + + let request: supertest.SuperTest; + + before(async () => { + request = await init(); + }); + + before(() => { + return db.connect(); + }); + + const ids = newIds(); + + after(() => { + return db.drop(ids); + }); + + const filepaths = [ ]; + after(() => { + return files.remove(filepaths); + }); + + let cids = [ ]; + before(async () => { + cids = await categories.addCategories(); + ids.categories.push(...cids); + }); + + before("login", async () => { + ids.users.push((await auth.login(request))[0]); + }); + + const targetIndex = 6; + + step("Set Category and Regexp", async () => { + const targetId = ids.categories[targetIndex]; + + for (let i = 0; i < 3; i++) { + const filepath = await files.newFile(); + filepaths.push(filepath); + const filename = path.basename(filepath); + + const regDoc = await regexps.newRegexp({ + name: newName(), + value: new RegExp(filename).source, + link: targetId, + hidden: true + }); + ids.regexps.push(regDoc._id); + } + }); + + step("Default Upload Way Fail", async () => { + const targetId = ids.categories[targetIndex]; + const filepath = filepaths[0]; + + const { status } = await files.uploadFile(request, filepath); + status.should.be.eql(400); + }); + + step("Upload File Success with Specified Category", async () => { + const targetId = ids.categories[targetIndex]; + const filepath = filepaths[0]; + const filename = path.basename(filepath); + + const { status } = await files.uploadFile( + request, filepath, { query: { + "category": encodeURI(await categories.getNameById(targetId)) + }} + ); + status.should.be.eql(201); + ids.goods.push(await goods.getIdByOriginname(filename)); + }); + + step("Upload File Success with Specified Parent Category", async () => { + const targetId = ids.categories[targetIndex - 1]; + const filepath = filepaths[1]; + const filename = path.basename(filepath); + + const { status } = await files.uploadFile( + request, filepath, { query: { + "category": encodeURI(await categories.getNameById(targetId)) + }} + ); + status.should.be.eql(201); + ids.goods.push(await goods.getIdByOriginname(filename)); + }); + + step("Upload File Fail with Specified Child Category", async () => { + const targetId = ids.categories[8]; + const filepath = filepaths[1]; + const filename = path.basename(filepath); + + const { status } = await files.uploadFile( + request, filepath, { query: { + "category": encodeURI(await categories.getNameById(targetId)) + }} + ); + status.should.be.eql(400); + }); + + step("Upload File Fail with Specified Child Category", async () => { + const targetId = ids.categories[8]; + const filepath = filepaths[2]; + const filename = path.basename(filepath); + + const { status } = await files.uploadFile( + request, filepath, { query: { + "category": await categories.getNameById(targetId) + }} + ); + status.should.be.eql(400); + }); + + step("Upload File Fail with Specified Brother Category", async () => { + const targetId = ids.categories[7]; + const filepath = filepaths[2]; + const filename = path.basename(filepath); + + const { status } = await files.uploadFile( + request, filepath, { query: { + "category": await categories.getNameById(targetId) + }} + ); + status.should.be.eql(400); + }); + +}); diff --git a/test/api/per_page.e2e.ts b/test/api/per_page.e2e.ts index 11a466d..827db8c 100644 --- a/test/api/per_page.e2e.ts +++ b/test/api/per_page.e2e.ts @@ -1,9 +1,10 @@ -import * as faker from "faker"; import supertest = require("supertest"); import { connect, drop, newUser, newRegexp, newCategory } from "../helpers/database"; import { init } from "../helpers/server"; +import auth = require("@db/auth"); +import { newName, newIds } from "../helpers/utils"; describe("the E2E Api of display item count Per page", () => { @@ -13,11 +14,8 @@ describe("the E2E Api of display item count Per page", () => { return connect(); }); - const ids = { - users: [ ], - regexps: [ ], - categories: [ ] - }; + const ids = newIds(); + after(() => { return drop(ids); }); @@ -26,17 +24,8 @@ describe("the E2E Api of display item count Per page", () => { request = await init(); }); - before("Login", async () => { - const user = { - name: faker.name.firstName(), - pass: faker.random.words() - }; - const doc = await newUser(user.name, user.pass); - ids.users.push(doc._id); - await request.post("/api/v1/auth/login") - .send({ - username: user.name, password: user.pass - }).then(); + before("login", async () => { + ids.users.push((await auth.login(request))[0]); }); describe("Users", () => { @@ -78,8 +67,8 @@ describe("the E2E Api of display item count Per page", () => { step("Add 100 Users", async () => { for (let i = 0; i < 100; i++) { const user = { - name: i + faker.name.firstName(), - pass: i + faker.random.words() + name: newName(), + pass: newName() }; const doc = await newUser(user.name, user.pass); ids.users.push(doc._id); @@ -204,7 +193,7 @@ describe("the E2E Api of display item count Per page", () => { step("Add 100 Regexps", async () => { for (let i = 0; i < 100; i++) { const regexp = { - name: i + faker.name.firstName(), + name: newName(), regexp: "^regexp." + i }; const doc = await newRegexp( @@ -331,7 +320,7 @@ describe("the E2E Api of display item count Per page", () => { step("Add 100 categories", async () => { for (let i = 0; i < 100; i++) { const cate = { - name: i + faker.random.words() + name: newName() }; const doc = await newCategory(cate); ids.categories.push(doc._id); diff --git a/test/api/regexps.e2e.ts b/test/api/regexps.e2e.ts index 1d854ce..495d4de 100644 --- a/test/api/regexps.e2e.ts +++ b/test/api/regexps.e2e.ts @@ -1,24 +1,24 @@ import supertest = require("supertest"); -import faker = require("faker"); import { Model as RegexpsModel } from "@models/Regexp"; +import { RegexpsService } from "@services/regexps"; import { connect, drop, newUser } from "../helpers/database"; import { init } from "../helpers/server"; -import { sleep } from "../helpers/utils"; +import { sleep, newIds } from "../helpers/utils"; +import { newName } from "./../helpers/utils"; +import auth = require("../helpers/database/auth"); describe("Regexp E2E Api", () => { const URL = "/api/v1/regexps"; let request: supertest.SuperTest; + let regexpsSvr: RegexpsService; before(() => { return connect(); }); - const ids = { - users: [ ], - regexps: [ ] - }; + const ids = newIds(); after(() => { return drop(ids); @@ -28,34 +28,30 @@ describe("Regexp E2E Api", () => { request = await init(); }); - const user = { - name: faker.name.firstName(), - pass: faker.random.words() - }; - step("Login", async () => { - const doc = await newUser(user.name, user.pass); - ids.users.push(doc._id); - await request.post("/api/v1/auth/login") - .send({ - username: user.name, password: user.pass - }).then(); + before(() => { + regexpsSvr = new RegexpsService(); + }); + + before("login", async () => { + ids.users.push((await auth.login(request))[0]); }); step("Add 3 Regexps", async () => { const items = [{ - name: faker.random.word(), + name: newName(), value: "^list.0" }, { - name: faker.random.word(), + name: newName(), value: "^list.1" }, { - name: faker.random.word(), + name: newName(), value: "^list.2" }]; for (const item of items) { - const doc = await RegexpsModel.addRegexp( - item.name + Date.now(), item.value + Date.now() - ); + const doc = await regexpsSvr.create({ + name: item.name, + value: item.value + Date.now() + }); ids.regexps.push(doc._id); await sleep(100); } @@ -87,7 +83,7 @@ describe("Regexp E2E Api", () => { }); const data = { - name: faker.random.word(), + name: newName(), value: "^abc.ccd" }; step("Add Regexp", async () => { @@ -130,7 +126,7 @@ describe("Regexp E2E Api", () => { step("Modify Name", async () => { const raw = await RegexpsModel.addRegexp( - faker.random.word(), "^modify" + newName(), new RegExp(newName()).source ); ids.regexps.push(raw._id); const { body: result, status: status } = @@ -142,21 +138,22 @@ describe("Regexp E2E Api", () => { }); step("Modify Value", async () => { + const oldRegexp = new RegExp(newName()); + const newRegexp = new RegExp(newName()); const raw = await RegexpsModel.addRegexp( - faker.random.word(), "modify value" + newName(), oldRegexp.source ); ids.regexps.push(raw._id); - const { body: result, status: status } = - await request.post(`${URL}/${raw._id}`) - .send({ value: "^adb.ccd$" }).then(); + const { body: result, status } = await request.post(`${URL}/${raw._id}`) + .send({ value: newRegexp.source }).then(); status.should.be.eql(200); - const regexp = await RegexpsModel.findById(raw._id).exec(); - regexp.toObject().should.have.property("value", "^adb.ccd$"); + const regexpDoc = await RegexpsModel.findById(raw._id).exec(); + regexpDoc.toObject().should.have.property("value", newRegexp.source); }); step("Modify Exist Name", async () => { const raw = - await RegexpsModel.addRegexp(faker.random.word(), "^abc.ccd$"); + await RegexpsModel.addRegexp(newName(), "^abc.ccd$"); ids.regexps.push(raw._id); const { body: result, status: status } = await request.post(`${URL}/${raw._id}`) @@ -164,22 +161,22 @@ describe("Regexp E2E Api", () => { status.should.be.eql(400); }); - step("Modify Exist Value", async () => { + // Ignore Self Value + xstep("Modify Exist Value", async () => { const data = { - name: faker.random.word(), + name: newName(), value: "^modify.exist.value" }; const raw = await RegexpsModel.addRegexp(data.name, data.value); ids.regexps.push(raw._id); - const { body: result, status: status } = - await request.post(`${URL}/${raw._id}`) + const { body: result, status } = await request.post(`${URL}/${raw._id}`) .send({ value: data.value }).then(); status.should.be.eql(400); }); - step("Modify with Empty Param", async () => { + xstep("Modify with Empty Param", async () => { const raw = await RegexpsModel.addRegexp( - faker.random.word(), "^empty.param" + newName(), "^empty.param" ); ids.regexps.push(raw._id); const { body: result, status: status } = @@ -189,7 +186,7 @@ describe("Regexp E2E Api", () => { step("Delete Regexp By GET", async () => { const raw = await RegexpsModel.addRegexp( - faker.random.word(), "^get.delete" + newName(), "^get.delete" ); ids.regexps.push(raw._id); // Delete @@ -202,7 +199,7 @@ describe("Regexp E2E Api", () => { step("Delete Regexp By DELETE", async () => { const raw = await RegexpsModel.addRegexp( - faker.random.word(), "^delete.delete" + newName(), "^delete.delete" ); ids.regexps.push(raw._id); // Delete diff --git a/test/api/system/system.e2e.ts b/test/api/system/system.e2e.ts new file mode 100644 index 0000000..f93c07b --- /dev/null +++ b/test/api/system/system.e2e.ts @@ -0,0 +1,108 @@ +import { Model as SystemModel } from "@models/System"; +import supertest = require("supertest"); +import { + connect, drop +} from "../../helpers/database"; +import { init } from "../../helpers/server"; +import auth = require("@db/auth"); +import { newName } from "../../helpers/utils"; + +describe("Collections E2E Api", () => { + + let request: supertest.SuperTest; + + before(() => { + return connect(); + }); + + const ids = { + users: [ ] + }; + + after(() => { + return drop(ids); + }); + + before(async () => { + request = await init(); + }); + + before("login", async () => { + ids.users.push((await auth.login(request))[0]); + }); + + after(async () => { + await SystemModel + .findOneAndRemove({ key: "DEFAULT_GOOD_URL" }) + .exec(); + await SystemModel + .findOneAndRemove({ key: "DEFAULT_COLLECTION_URL" }) + .exec(); + }); + + it("Get System Info", async () => { + const { status } = await request.get("/api/v1/system/info").then(); + status.should.be.eql(200); + }); + + it("Set Good Url", async () => { + const value = `http://example.com/${newName()}/{{gid}}`; + const { status } = await request.put("/api/v1/system/vars") + .send({ key: "DEFAULT_GOOD_URL", value }).then(); + status.should.be.eql(200); + const { body } = await request.get("/api/v1/system/vars").then(); + body.should.matchAny({ key: "DEFAULT_GOOD_URL", value }); + }); + + it("Set Empty Good Url", async () => { + const value = ""; + const { status } = await request.put("/api/v1/system/vars") + .send({ key: "DEFAULT_GOOD_URL", value }).then(); + status.should.be.eql(200); + const { body } = await request.get("/api/v1/system/vars").then(); + body.should.matchAny({ key: "DEFAULT_GOOD_URL", value }); + }); + + it("Set one wrong Good Url", async () => { + const value = `http://example.com/${newName()}`; + const { status } = await request.put("/api/v1/system/vars") + .send({ key: "DEFAULT_GOOD_URL", value }).then(); + status.should.be.eql(400); + const { body } = await request.get("/api/v1/system/vars").then(); + body.should.not.matchAny({ key: "DEFAULT_GOOD_URL", value }); + }); + + it("Set Collection Url", async () => { + const value = `http://example.com/${newName()}/{{cid}}`; + const { status } = await request.put("/api/v1/system/vars") + .send({ key: "DEFAULT_COLLECTION_URL", value }).then(); + status.should.be.eql(200); + const { body } = await request.get("/api/v1/system/vars").then(); + body.should.matchAny({ key: "DEFAULT_COLLECTION_URL", value }); + }); + + it("Set Empty Collection Url", async () => { + const value = ""; + const { status } = await request.put("/api/v1/system/vars") + .send({ key: "DEFAULT_COLLECTION_URL", value }).then(); + status.should.be.eql(200); + const { body } = await request.get("/api/v1/system/vars").then(); + body.should.matchAny({ key: "DEFAULT_COLLECTION_URL", value }); + }); + + it("Set one wrong Collection Url", async () => { + const value = `http://example.com/${newName()}`; + const { status } = await request.put("/api/v1/system/vars") + .send({ key: "DEFAULT_COLLECTION_URL", value }).then(); + status.should.be.eql(400); + const { body } = await request.get("/api/v1/system/vars").then(); + body.should.not.matchAny({ key: "DEFAULT_COLLECTION_URL", value }); + }); + + it("set value to non-exist key", async () => { + const { status } = await request.put("/api/v1/system/vars") + .send({ key: newName(), value: newName() }).then(); + status.should.be.eql(400); + }); + +}); diff --git a/test/api/token.e2e.ts b/test/api/token.e2e.ts index dbc4a76..20045ad 100644 --- a/test/api/token.e2e.ts +++ b/test/api/token.e2e.ts @@ -1,9 +1,9 @@ import supertest = require("supertest"); -import faker = require("faker"); import { TokensService } from "@services/tokens"; import { connect, drop, newUser } from "../helpers/database"; import { init } from "../helpers/server"; +import { newName, newIds } from "../helpers/utils"; describe("Token E2E Test", () => { @@ -14,10 +14,7 @@ describe("Token E2E Test", () => { return connect(); }); - const ids = { - users: [ ], - tokens: [ ] - }; + const ids = newIds(); after(() => { return drop(ids); @@ -28,12 +25,12 @@ describe("Token E2E Test", () => { }); const users = [{ - name: faker.name.firstName(), - pass: faker.random.words(), + name: newName(), + pass: newName(), id: "" }, { - name: faker.name.firstName(), - pass: faker.random.words(), + name: newName(), + pass: newName(), id: "" }]; diff --git a/test/api/token_login_logout.e2e.ts b/test/api/token_login_logout.e2e.ts index 57db3d2..2e2f186 100644 --- a/test/api/token_login_logout.e2e.ts +++ b/test/api/token_login_logout.e2e.ts @@ -1,9 +1,9 @@ import supertest = require("supertest"); -import faker = require("faker"); import { connect, drop, newUser } from "../helpers/database"; import { init } from "../helpers/server"; import { Model as TokensModel } from "@models/Token"; +import { newName, newIds } from "../helpers/utils"; describe("Token E2E Api", () => { @@ -13,10 +13,7 @@ describe("Token E2E Api", () => { return connect(); }); - const ids = { - users: [ ], - tokens: [ ] - }; + const ids = newIds(); after(() => { return drop(ids); @@ -27,8 +24,8 @@ describe("Token E2E Api", () => { }); const user = { - name: faker.name.firstName(), - pass: faker.random.words(), + name: newName(), + pass: newName(), id: "", token: "" }; diff --git a/test/api/user_usergroup.e2e.ts b/test/api/user_usergroup.e2e.ts new file mode 100644 index 0000000..be94308 --- /dev/null +++ b/test/api/user_usergroup.e2e.ts @@ -0,0 +1,67 @@ +import supertest = require("supertest"); + +import { connect, drop, addCategoryAndRegexp } from "../helpers/database"; +import { init } from "../helpers/server"; +import { newUsergroup } from "@db/usergroups"; +import { newUser, newUserWithUsergroup } from "@db/user"; +import auth = require("@db/auth"); +import { newIds } from "../helpers/utils"; + +describe("User's Usergroup E2E Api", () => { + + let request: supertest.SuperTest; + + before(() => { + return connect(); + }); + + const ids = newIds(); + + after(() => { + return drop(ids); + }); + + before(async () => { + request = await init(); + }); + + before("login", async () => { + ids.users.push((await auth.login(request))[0]); + }); + + step("Get Usergroup", async () => { + const url = `/api/v1/users/${ids.users[0]}/usergroups`; + const { status, body: result } = await request.get(url).then(); + status.should.be.eql(200); + result.should.have.property("total", 1); + }); + + step("User Add 4 Usergroups", async () => { + const uid = ids.users[0]; + for (let i = 0; i < 4; i++) { + const group = await newUsergroup(undefined, uid); + ids.usergroups.push(group._id); + } + }); + + step("Have 5 Usergroups ", async () => { + const url = `/api/v1/users/${ids.users[0]}/usergroups`; + const { status, body: result } = await request.get(url).then(); + status.should.be.eql(200); + result.should.have.property("total", 1 + ids.usergroups.length); + }); + + step("Remove one usergroup", () => { + const uid = ids.users[0]; + const url = `/api/v1/usergroups/${ids.usergroups[0]}/remove/${uid}`; + return request.get(url).then(); + }); + + step("Have 4 Usergroups ", async () => { + const url = `/api/v1/users/${ids.users[0]}/usergroups`; + const { status, body: result } = await request.get(url).then(); + status.should.be.eql(200); + result.should.have.property("total", ids.usergroups.length); + }); + +}); diff --git a/test/api/usergroups.e2e.ts b/test/api/usergroups.e2e.ts new file mode 100644 index 0000000..70cf9f4 --- /dev/null +++ b/test/api/usergroups.e2e.ts @@ -0,0 +1,107 @@ +import supertest = require("supertest"); + +import { + connect, drop, addCategoryAndRegexp +} from "../helpers/database"; +import { init } from "../helpers/server"; +import { newUsergroup } from "@db/usergroups"; +import auth = require("@db/auth"); +import { newName, newIds } from "../helpers/utils"; + +describe("Usergroup E2E Api", () => { + + let request: supertest.SuperTest; + + before(() => { + return connect(); + }); + + const ids = newIds(); + + after(() => { + return drop(ids); + }); + + before(async () => { + request = await init(); + }); + + before("login", async () => { + ids.users.push((await auth.login(request))[0]); + }); + + step("New Usergroup * 2", async () => { + for (let i = 0; i < 2; i++) { + const url = `/api/v1/usergroups`; + const name = newName(); + const { status, body: result } = + await request.post(url).send({ name }).then(); + status.should.be.eql(201); + ids.usergroups.push(result._id); + } + }); + + step("Usergroup List", async () => { + const url = `/api/v1/usergroups`; + const { status, body: result } = await request.get(url).then(); + status.should.be.eql(200); + result.total.should.aboveOrEqual(2); + result.data.should.be.an.Array(); + }); + + step("Get Usergroup Info", async () => { + const id = ids.usergroups[ids.usergroups.length - 1]; + const url = `/api/v1/usergroups/${id}`; + const { status, body: result } = await request.get(url).then(); + status.should.be.eql(200); + result.users.data.should.be.an.Array(); + result.users.total.should.be.eql(0); + }); + + step("Add User into Usergroup", async () => { + const gid = ids.usergroups[ids.usergroups.length - 1]; + const uid = ids.users[0]; + const url = `/api/v1/usergroups/${gid}/add/${uid}`; + const { status } = await request.get(url).then(); + status.should.be.eql(201); + }); + + step("Have 1 User", async () => { + const id = ids.usergroups[ids.usergroups.length - 1]; + const url = `/api/v1/usergroups/${id}`; + const { status, body: result } = await request.get(url).then(); + status.should.be.eql(200); + result.users.total.should.be.eql(1); + }); + + step("Fail to Add User into Same Usergroup", async () => { + const gid = ids.usergroups[ids.usergroups.length - 1]; + const uid = ids.users[0]; + const url = `/api/v1/usergroups/${gid}/add/${uid}`; + const { status } = await request.get(url).then(); + status.should.be.eql(400); + }); + + step("Modify Usergroup's name", async () => { + const id = ids.usergroups[ids.usergroups.length - 1]; + const url = `/api/v1/usergroups/${id}`; + const name = newName(); + const { status } = await request.post(url).send({ name }).then(); + status.should.be.eql(200); + const { body: result } = await request.get(url).then(); + result.should.have.property("name", name); + }); + + step("Delete By GET", async () => { + const id = ids.usergroups[ids.usergroups.length - 1]; + const url = `/api/v1/usergroups/${id}/delete`; + const { status } = await request.get(url).then(); + }); + + step("Delete By DELETE", async () => { + const id = ids.usergroups[ids.usergroups.length - 2]; + const url = `/api/v1/usergroups/${id}`; + const { status } = await request.delete(url).then(); + }); + +}); diff --git a/test/api/users_goods.e2e.ts b/test/api/users_goods.e2e.ts index bcd0d49..c8e0494 100644 --- a/test/api/users_goods.e2e.ts +++ b/test/api/users_goods.e2e.ts @@ -1,10 +1,11 @@ import supertest = require("supertest"); -import faker = require("faker"); import { connect, drop, newUser, addCategoryAndRegexp } from "../helpers/database"; import { init } from "../helpers/server"; +import auth = require("@db/auth"); +import { newName, newIds } from "../helpers/utils"; describe("User's Goods E2E Api", () => { @@ -14,9 +15,7 @@ describe("User's Goods E2E Api", () => { return connect(); }); - const ids = { - users: [ ] - }; + const ids = newIds(); after(() => { return drop(ids); @@ -29,17 +28,15 @@ describe("User's Goods E2E Api", () => { describe("Self Goods", () => { const user = { - name: faker.name.firstName(), - pass: faker.random.words(), + name: newName(), + pass: newName(), id: "" }; - step("Login", async () => { - const doc = await newUser(user.name, user.pass); - ids.users.push(doc._id); - await request.post("/api/v1/auth/login") - .send({ - username: user.name, password: user.pass - }).then(); + + before("login", async () => { + const id = (await auth.login(request, user.name, user.pass))[0]; + ids.users.push(id); + user.id = id; }); step("Status Code isnt 500", async () => { @@ -57,18 +54,15 @@ describe("User's Goods E2E Api", () => { describe("User's Goods", () => { const user = { - name: faker.name.firstName(), - pass: faker.random.words(), + name: newName(), + pass: newName(), id: "" }; - step("Login", async () => { - const doc = await newUser(user.name, user.pass); - ids.users.push(doc._id); - user.id = doc._id; - await request.post("/api/v1/auth/login") - .send({ - username: user.name, password: user.pass - }).then(); + + before("login", async () => { + const id = (await auth.login(request, user.name, user.pass))[0]; + ids.users.push(id); + user.id = id; }); step("Status Code isnt 500", async () => { diff --git a/test/helpers/database.ts b/test/helpers/database.ts index 393c1a8..9a86e58 100644 --- a/test/helpers/database.ts +++ b/test/helpers/database.ts @@ -1,7 +1,5 @@ -import faker = require("faker"); import { config } from "@utils/config"; import { ObjectId } from "@models/common"; -import { connectDatabase } from "../../src/modules/database/database.providers"; import { Model as ValuesModel } from "@models/Value"; import { Model as GoodsModels } from "@models/Good"; @@ -10,17 +8,28 @@ import { Model as UsersModel } from "@models/User"; import { Model as TokensModel } from "@models/Token"; import { Model as CollectionsModel } from "@models/Collection"; import { Model as CategoriesModel, CategoryDoc } from "@models/Categroy"; +import { Model as UsergroupsModel } from "@models/Usergroup"; +import { Model as UserUsergroupsModel } from "@models/User-Usergroup"; + +import { connectDatabase } from "../../src/modules/database/database.providers"; +import { newUser as newUserFn } from "./database/user"; +import * as regexps from "./database/regexps"; +import * as categories from "./database/categories"; +import { newName, sleep } from "./utils"; +import { remove } from "./files"; config.db.database = "storebox-test"; -interface IIds { +export interface IIds { values?: ObjectId[]; goods?: ObjectId[]; regexps?: ObjectId[]; users?: ObjectId[]; - categories?: ObjectId[]; tokens?: ObjectId[]; + categories?: ObjectId[]; collections?: ObjectId[]; + usergroups?: ObjectId[]; + userusergroups?: ObjectId[]; } /** @@ -29,53 +38,52 @@ interface IIds { export const connect = connectDatabase; export const drop = async (ids?: IIds) => { - if (!ids) { - await ValuesModel.remove({ }).exec(); - await GoodsModels.remove({ }).exec(); - await RegexpsModel.remove({ }).exec(); - await UsersModel.remove({ }).exec(); - await CategoriesModel.remove({ }).exec(); - return; - } - for (const id of (ids.values || [ ])) { - await ValuesModel.findByIdAndRemove(id).exec(); - } - for (const id of (ids.goods || [ ])) { - await GoodsModels.findByIdAndRemove(id).exec(); - } - for (const id of (ids.regexps || [ ])) { - await RegexpsModel.findByIdAndRemove(id).exec(); - } - for (const id of (ids.users || [ ])) { - await UsersModel.findByIdAndRemove(id).exec(); - } - for (const id of (ids.tokens || [ ])) { - await TokensModel.findByIdAndRemove(id).exec(); - } - for (const id of (ids.categories || [ ])) { - await CategoriesModel.findByIdAndRemove(id).exec(); - } - for (const id of (ids.collections || [ ])) { - await CollectionsModel.findByIdAndRemove(id).exec(); + await sleep(250); + const MODEL_IDMETHOD_MAP = { + "values": ValuesModel, + "goods": GoodsModels, + "regexps": RegexpsModel, + "users": UsersModel, + "tokens": TokensModel, + "categories": CategoriesModel, + "collections": CollectionsModel, + "usergroups": UsergroupsModel, + "userusergroups": UserUsergroupsModel + }; + for (const method of Object.keys(MODEL_IDMETHOD_MAP)) { + const model = MODEL_IDMETHOD_MAP[method]; + for (const id of (ids[method] || [ ])) { + await model.findByIdAndRemove(id).exec(); + if (method === "users") { + await UserUsergroupsModel.remove({ user: id }).exec(); + } else if (method === "usergroups") { + await UserUsergroupsModel.remove({ usergroup: id }).exec(); + } + } + if (method === "categories") { + await remove((ids[method] || [ ]).reduce((arr, id) => { + arr.push(`${config.paths.upload}/${id.toString()}`); + return arr; + }, [ ])); + } } }; -export const addCategoryAndRegexp = async (regexp: RegExp) => { - const category = await CategoriesModel.create({ - name: faker.name.findName() +export const addCategoryAndRegexp = async (regexp: RegExp, pid?: ObjectId) => { + const category = await newCategory({ + name: newName(), + pid: pid }); - const reg = await newRegexp(faker.random.word(), regexp, category._id); - return [category, reg]; + const reg = await newRegexp(newName(), regexp, category._id); + return [ category, reg ]; }; -export const newUser = (username: string, password: string) => { - return UsersModel.addUser(username, password); -}; +export const newUser = newUserFn; -export const newRegexp = (name: string, value: RegExp, link?) => { - return RegexpsModel.addRegexp(name, value.source, link); +export const newRegexp = (name: string, value: RegExp, link?: ObjectId) => { + return regexps.newRegexp({ + name, value: value.source, link, hidden: false + }); }; -export const newCategory = (obj: object) => { - return CategoriesModel.create(obj) as Promise; -}; +export const newCategory = categories.newCategory; diff --git a/test/helpers/database/auth.ts b/test/helpers/database/auth.ts new file mode 100644 index 0000000..23c0d2e --- /dev/null +++ b/test/helpers/database/auth.ts @@ -0,0 +1,16 @@ +import supertest = require("supertest"); +import { newUser } from "@db/user"; +import { newName } from "../utils"; + +export const login = async ( + request: supertest.SuperTest, + username = newName(), + password = newName() +) => { + const doc = await newUser(username, password); + await request.post("/api/v1/auth/login") + .send({ + username, password + }).then(); + return [ doc._id ]; +}; diff --git a/test/helpers/database/categories.ts b/test/helpers/database/categories.ts new file mode 100644 index 0000000..64c8a64 --- /dev/null +++ b/test/helpers/database/categories.ts @@ -0,0 +1,53 @@ +import { ObjectId } from "@models/common"; +import { Model as CategoriesModel, CategoryDoc } from "@models/Categroy"; +import { newName, sleep } from "../utils"; + +export const newCategory = (obj: object) => { + return CategoriesModel.create(obj) as Promise; +}; + +/** + * Add 11 Categories + * ``` + * - 1 - 4 + * | + * - 0 - 2 - 5 - 6 - 8 + * | | | + * pid -| - 3 - 7 + * | + * - 9 - 10 + * ``` + * @param pid Parent Category ID + * @returns Categories' ID + */ +export const addCategories = async (pid?: ObjectId) => { + const cids: ObjectId[ ] = []; + // Create 11 Categories + for (let i = 0; i < 11; i++) { + const result = await CategoriesModel.create({ + name: newName() + }); + cids.push(result._id); + } + // [parent, child] + const initGroups = [ + [0, 1], [0, 2], [0, 3], + [1, 4], [2, 5], + [5, 6], [5, 7], [6, 8], + [9, 10] + ]; + for (const set of initGroups) { + await CategoriesModel.moveCategory(cids[set[1]], cids[set[0]]); + } + if (pid) { + await CategoriesModel.moveCategory(cids[9], pid); + await CategoriesModel.moveCategory(cids[0], pid); + } + await sleep(50); + return cids; +}; + +export const getNameById = async (id: ObjectId) => { + const doc = await CategoriesModel.findById(id).exec(); + return doc.toObject().name || undefined; +}; diff --git a/test/helpers/database/goods.ts b/test/helpers/database/goods.ts new file mode 100644 index 0000000..2f8a21c --- /dev/null +++ b/test/helpers/database/goods.ts @@ -0,0 +1,6 @@ +import { Model as GoodsModels } from "@models/Good"; +import { config } from "@utils/config"; + +export const getIdByOriginname = async (name: string) => { + return (await GoodsModels.findOne({ originname: name }).exec())._id; +}; diff --git a/test/helpers/database/regexps.ts b/test/helpers/database/regexps.ts new file mode 100644 index 0000000..1174b2f --- /dev/null +++ b/test/helpers/database/regexps.ts @@ -0,0 +1,16 @@ +import { IRegexpDoc } from "@models/Regexp"; +import { RegexpsService } from "@services/regexps"; + +let regexpSvr: RegexpsService; + +const init = () => { + if (!regexpSvr) { + regexpSvr = new RegexpsService(); + } + return regexpSvr; +}; + +export const newRegexp = (obj: IRegexpDoc) => { + init(); + return regexpSvr.create(obj); +}; diff --git a/test/helpers/database/user.ts b/test/helpers/database/user.ts new file mode 100644 index 0000000..c7d0fee --- /dev/null +++ b/test/helpers/database/user.ts @@ -0,0 +1,27 @@ +import { Model as UsersModel } from "@models/User"; +import { ObjectId } from "@models/common"; +import { UsersService } from "@services/users"; +import { SystemService } from "@services/system"; +import { newName } from "../utils"; + +let usersSvr: UsersService; + +const init = () => { + if (!usersSvr) { + usersSvr = new UsersService(new SystemService()); + } + return usersSvr; +}; + +export const newUser = (username?: string, password?: string) => { + return newUserWithUsergroup(username, password); +}; + +export const newUserWithUsergroup = ( + username = newName(), password = newName(), gid?: ObjectId +) => { + init(); + return usersSvr.addUser({ + username, password + }, gid); +}; diff --git a/test/helpers/database/usergroups.ts b/test/helpers/database/usergroups.ts new file mode 100644 index 0000000..08ca93d --- /dev/null +++ b/test/helpers/database/usergroups.ts @@ -0,0 +1,29 @@ +import { ObjectId } from "@models/common"; +import { Model as UserUsergroupsModel } from "@models/User-Usergroup"; +import { UsergroupsService } from "@services/usergroups"; +import { newName } from "../utils"; + +export const getLinkIdsByUserId = async (uid: ObjectId) => { + return (await UserUsergroupsModel.find({ user: uid }).exec()) + .map((item) => { + return item._id as ObjectId; + }); +}; + +export const getLinkIdsByUsergroupId = async (gid: ObjectId) => { + return (await UserUsergroupsModel.find({ usergroup: gid }).exec()) + .map((item) => { + return item._id as ObjectId; + }); +}; + +export const newUsergroup = async ( + name = newName(), uid?: ObjectId +) => { + const svr = new UsergroupsService(); + const group = await svr.add({ name }); + if (uid) { + await svr.addUserToGroup(group._id, uid); + } + return group; +}; diff --git a/test/helpers/files.ts b/test/helpers/files.ts index f3bac33..acbee9f 100644 --- a/test/helpers/files.ts +++ b/test/helpers/files.ts @@ -1,19 +1,93 @@ import supertest = require("supertest"); +import { config } from "@utils/config"; +import { isArray } from "util"; +import faker = require("faker"); +import fs = require("fs-extra"); +import { sleep } from "./utils"; + +interface IUploadFileOptions { + query?: { + [key: string]: string + }; +} + +/* tslint:disable:no-empty-interface */ +interface IUploadFilesOptions extends IUploadFileOptions { } + +const addQuery = (url: string, opts: IUploadFileOptions) => { + if (opts.query && Object.keys(opts.query).length > 0) { + const query = [ ]; + for (const key of Object.keys(opts.query)) { + const q = opts.query[key].split(/\s+/).map((val) => { + return `${key}=${val}`; + }).join("&"); + query.push(q); + } + url += `?${query.join("&")}`; + } + return url; +}; export const uploadFile = ( - request: supertest.SuperTest, filepath: string + request: supertest.SuperTest, filepath: string, + opts: IUploadFileOptions = { } ) => { - return request.post("/api/v1/goods") - .attach("file", filepath) + let url = "/api/v1/goods"; + url = addQuery(url, opts); + + return request.post(url).attach("file", filepath) .then(); }; export const uploadFiles = ( - request: supertest.SuperTest, filepaths: string[] + request: supertest.SuperTest, filepaths: string[], + opts: IUploadFilesOptions = { } ) => { - let req = request.post("/api/v1/goods/collections"); + let url = "/api/v1/goods/collections"; + url = addQuery(url, opts); + + let req = request.post(url); filepaths.forEach((filepath) => { req = req.attach("files", filepath); }); return req.then(); }; + +const newFilename = () => { + const name = `${faker.random.word()}_${faker.random.uuid()}`; + const random = Math.random(); + const randomStr = `${random}`.replace(/(^0|\D)/g, ""); + return `${randomStr}-${name}`; +}; + +/** + * Generate File + * @param filename + * @returns filepath + */ +export const newFile = async (filename = newFilename()) => { + const folderpath = `${config.paths.tmp}/test`; + if (!fs.existsSync(folderpath)) { + fs.mkdirpSync(folderpath); + } + const filepath = `${folderpath}/${filename}`; + fs.writeFileSync(filepath, JSON.stringify({ + data: Math.random() + }), { encoding: "utf-8" }); + await sleep(200); + return filepath; +}; + +export const remove = (filepaths: string[] | string) => { + if (!isArray(filepaths)) { + filepaths = [ filepaths ]; + } + if (filepaths.length === 0) { + return Promise.resolve(); + } + filepaths = Array.from(new Set(filepaths)); + return Promise.all(filepaths.map(async (filepath) => { + await sleep(200); + return fs.existsSync(filepath) ? fs.remove(filepath) : null; + })); +}; diff --git a/test/helpers/utils.ts b/test/helpers/utils.ts index 5ccfb06..ec7ad25 100644 --- a/test/helpers/utils.ts +++ b/test/helpers/utils.ts @@ -1,5 +1,28 @@ +import faker = require("faker"); +import { IIds } from "./database"; + export const sleep = (ms: number) => { return new Promise((reslove) => { setTimeout(reslove, ms); }); }; + +export const newName = (prefix = "") => { + const randomStr = `${Math.random()}`.replace(/(^0|\D)/g, ""); + const name = `${faker.random.word()}${randomStr}${Date.now()}`; + return `${prefix}${name}`; +}; + +export const newIds = (): IIds => { + return { + values: [ ], + goods: [ ], + regexps: [ ], + users: [ ], + tokens: [ ], + categories: [ ], + collections: [ ], + usergroups: [ ], + userusergroups: [ ] + }; +}; diff --git a/test/issues/ban_user_n_its_token.e2e.ts b/test/issues/ban_user_n_its_token.e2e.ts index 98ea6ee..9fa20d0 100644 --- a/test/issues/ban_user_n_its_token.e2e.ts +++ b/test/issues/ban_user_n_its_token.e2e.ts @@ -1,25 +1,23 @@ import supertest = require("supertest"); -import faker = require("faker"); import { connect, drop, newUser } from "../helpers/database"; import { init } from "../helpers/server"; import { UsersService } from "@services/users"; import { TokensService } from "@services/tokens"; +import { SystemService } from "@services/system"; +import { newName, newIds } from "../helpers/utils"; describe("Fix Issues", () => { let request: supertest.SuperTest; const tokensSvr = new TokensService(); - const usersSvr = new UsersService(); + const usersSvr = new UsersService(new SystemService()); before(() => { return connect(); }); - const ids = { - users: [ ], - tokens: [ ] - }; + const ids = newIds(); after(() => { return drop(ids); @@ -30,8 +28,8 @@ describe("Fix Issues", () => { }); const user = { - name: faker.name.firstName(), - pass: faker.random.words(), + name: newName(), + pass: newName(), token: "" }; describe("Token Action When User ban", () => { diff --git a/test/issues/github_issue_16.e2e.ts b/test/issues/github/github_issue_16.e2e.ts similarity index 68% rename from test/issues/github_issue_16.e2e.ts rename to test/issues/github/github_issue_16.e2e.ts index b87a6ac..b546b20 100644 --- a/test/issues/github_issue_16.e2e.ts +++ b/test/issues/github/github_issue_16.e2e.ts @@ -1,13 +1,14 @@ import supertest = require("supertest"); -import faker = require("faker"); import { connect, drop, newUser, addCategoryAndRegexp -} from "../helpers/database"; -import { init } from "../helpers/server"; +} from "../../helpers/database"; +import { init } from "../../helpers/server"; +import auth = require("@db/auth"); +import { newIds } from "../../helpers/utils"; /** - * Fix [Issue 16](https://github.com/Arylo/StoreBox/issues/16) + * Fix [Issue 16](https://github.com/BoxSystem/StoreBox-Api/issues/16) */ describe("Fix Issues", () => { @@ -17,11 +18,7 @@ describe("Fix Issues", () => { return connect(); }); - const ids = { - users: [ ], - categories: [ ], - regexps: [ ] - }; + const ids = newIds(); after(() => { return drop(ids); @@ -32,17 +29,9 @@ describe("Fix Issues", () => { }); describe("Github 16 [Overwrite Cache List Api]", () => { - const user = { - name: faker.name.firstName(), - pass: faker.random.words() - }; - step("Login", async () => { - const doc = await newUser(user.name, user.pass); - ids.users.push(doc._id); - await request.post("/api/v1/auth/login") - .send({ - username: user.name, password: user.pass - }).then(); + + before("login", async () => { + ids.users.push((await auth.login(request))[0]); }); step("Add Category and Regexp", async () => { diff --git a/test/issues/github_issue_21.e2e.ts b/test/issues/github/github_issue_21.e2e.ts similarity index 76% rename from test/issues/github_issue_21.e2e.ts rename to test/issues/github/github_issue_21.e2e.ts index 5acc496..f763886 100644 --- a/test/issues/github_issue_21.e2e.ts +++ b/test/issues/github/github_issue_21.e2e.ts @@ -1,13 +1,14 @@ import supertest = require("supertest"); -import faker = require("faker"); import { connect, drop, newUser, addCategoryAndRegexp -} from "../helpers/database"; -import { init } from "../helpers/server"; +} from "../../helpers/database"; +import { init } from "../../helpers/server"; +import auth = require("@db/auth"); +import { newName, newIds } from "../../helpers/utils"; /** - * About [Issue 21](https://github.com/Arylo/StoreBox/issues/21) + * About [Issue 21](https://github.com/BoxSystem/StoreBox-Api/issues/21) */ describe("Fix Issues", () => { @@ -17,9 +18,7 @@ describe("Fix Issues", () => { return connect(); }); - const ids = { - users: [ ] - }; + const ids = newIds(); after(() => { return drop(ids); @@ -32,16 +31,13 @@ describe("Fix Issues", () => { describe("Github 21", () => { const user = { - name: faker.name.firstName(), - pass: faker.random.words() + name: newName(), + pass: newName() }; - step("Login", async () => { - const doc = await newUser(user.name, user.pass); - ids.users.push(doc._id); - await request.post("/api/v1/auth/login") - .send({ - username: user.name, password: user.pass - }).then(); + before("login", async () => { + ids.users.push( + (await auth.login(request, user.name, user.pass))[0] + ); }); step("Goods List status code is 200", async () => { diff --git a/test/issues/github_issue_22.e2e.ts b/test/issues/github/github_issue_22.e2e.ts similarity index 63% rename from test/issues/github_issue_22.e2e.ts rename to test/issues/github/github_issue_22.e2e.ts index ede9585..06301d0 100644 --- a/test/issues/github_issue_22.e2e.ts +++ b/test/issues/github/github_issue_22.e2e.ts @@ -1,13 +1,14 @@ import supertest = require("supertest"); -import faker = require("faker"); import { - connect, drop, newUser, addCategoryAndRegexp -} from "../helpers/database"; -import { init } from "../helpers/server"; + connect, drop, addCategoryAndRegexp +} from "../../helpers/database"; +import { init } from "../../helpers/server"; +import auth = require("@db/auth"); +import { newName, newIds } from "../../helpers/utils"; /** - * Fix [Issue 22](https://github.com/Arylo/StoreBox/issues/22) + * Fix [Issue 22](https://github.com/BoxSystem/StoreBox-Api/issues/22) */ describe("Fix Issues", () => { @@ -17,9 +18,7 @@ describe("Fix Issues", () => { return connect(); }); - const ids = { - users: [ ] - }; + const ids = newIds(); after(() => { return drop(ids); @@ -32,16 +31,13 @@ describe("Fix Issues", () => { describe("Github 22", () => { const user = { - name: faker.name.firstName(), - pass: faker.random.words() + name: newName(), + pass: newName() }; - step("Login", async () => { - const doc = await newUser(user.name, user.pass); - ids.users.push(doc._id); - await request.post("/api/v1/auth/login") - .send({ - username: user.name, password: user.pass - }).then(); + before("login", async () => { + ids.users.push( + (await auth.login(request, user.name, user.pass))[0] + ); }); step("Status Code isnt 500 #0", async () => { diff --git a/test/issues/github_issue_27.e2e.ts b/test/issues/github/github_issue_27.e2e.ts similarity index 52% rename from test/issues/github_issue_27.e2e.ts rename to test/issues/github/github_issue_27.e2e.ts index 24ad684..b9e6a47 100644 --- a/test/issues/github_issue_27.e2e.ts +++ b/test/issues/github/github_issue_27.e2e.ts @@ -1,13 +1,14 @@ import supertest = require("supertest"); -import faker = require("faker"); import { - connect, drop, newUser, newCategory -} from "../helpers/database"; -import { init } from "../helpers/server"; + connect, drop, newCategory +} from "../../helpers/database"; +import { init } from "../../helpers/server"; +import auth = require("@db/auth"); +import { newName, newIds } from "../../helpers/utils"; /** - * Fix [Issue 27](https://github.com/Arylo/StoreBox/issues/27) + * Fix [Issue 27](https://github.com/BoxSystem/StoreBox-Api/issues/27) */ describe("Fix Issues", () => { @@ -17,11 +18,7 @@ describe("Fix Issues", () => { return connect(); }); - const ids = { - users: [ ], - categories: [ ], - regexps: [ ] - }; + const ids = newIds(); after(() => { return drop(ids); @@ -32,21 +29,13 @@ describe("Fix Issues", () => { }); describe("Github 27 ", () => { - const user = { - name: faker.name.firstName(), - pass: faker.random.words() - }; - step("Login", async () => { - const doc = await newUser(user.name, user.pass); - ids.users.push(doc._id); - await request.post("/api/v1/auth/login") - .send({ - username: user.name, password: user.pass - }).then(); + + before("login", async () => { + ids.users.push((await auth.login(request))[0]); }); step("Add Category", async () => { - const doc = await newCategory({ name: faker.random.word() }); + const doc = await newCategory({ name: newName() }); ids.categories.push(doc._id); }); @@ -55,7 +44,7 @@ describe("Fix Issues", () => { body: result, status } = await request.post("/api/v1/regexps") .send({ - name: faker.random.word() + "link_cate", + name: newName(), value: new RegExp("chchachc.+").source, link: ids.categories[0] }).then(); diff --git a/test/issues/github_issue_28.e2e.ts b/test/issues/github/github_issue_28.e2e.ts similarity index 50% rename from test/issues/github_issue_28.e2e.ts rename to test/issues/github/github_issue_28.e2e.ts index 08edaca..4d49a27 100644 --- a/test/issues/github_issue_28.e2e.ts +++ b/test/issues/github/github_issue_28.e2e.ts @@ -1,13 +1,14 @@ import supertest = require("supertest"); -import faker = require("faker"); import { connect, drop, newUser -} from "../helpers/database"; -import { init } from "../helpers/server"; +} from "../../helpers/database"; +import { init } from "../../helpers/server"; +import auth = require("@db/auth"); +import { newIds } from "../../helpers/utils"; /** - * Fix [Issue 28](https://github.com/Arylo/StoreBox/issues/28) + * Fix [Issue 28](https://github.com/BoxSystem/StoreBox-Api/issues/28) */ describe("Fix Issues", () => { @@ -17,11 +18,7 @@ describe("Fix Issues", () => { return connect(); }); - const ids = { - users: [ ], - categories: [ ], - regexps: [ ] - }; + const ids = newIds(); after(() => { return drop(ids); @@ -33,17 +30,8 @@ describe("Fix Issues", () => { describe("Github 28 [User can ban oneself]", () => { - const user = { - name: faker.name.firstName(), - pass: faker.random.words() - }; - step("Login", async () => { - const doc = await newUser(user.name, user.pass); - ids.users.push(doc._id); - await request.post("/api/v1/auth/login") - .send({ - username: user.name, password: user.pass - }).then(); + before("login", async () => { + ids.users.push((await auth.login(request))[0]); }); step("Ban self Fail", async () => { diff --git a/test/issues/github_issue_30.spec.ts b/test/issues/github/github_issue_30.spec.ts similarity index 79% rename from test/issues/github_issue_30.spec.ts rename to test/issues/github/github_issue_30.spec.ts index 5add94e..75f908f 100644 --- a/test/issues/github_issue_30.spec.ts +++ b/test/issues/github/github_issue_30.spec.ts @@ -1,10 +1,10 @@ import isRegExp = require("@utils/isRegExp"); -import { connect } from "../helpers/database"; +import { connect } from "../../helpers/database"; import { Model as RegexpsModel } from "@models/Regexp"; -import * as faker from "faker"; +import { newName } from "../../helpers/utils"; /** - * Fix [Issue 30](https://github.com/Arylo/StoreBox/issues/30) + * Fix [Issue 30](https://github.com/BoxSystem/StoreBox-Api/issues/30) */ describe("Fix Issues", () => { @@ -27,7 +27,7 @@ describe("Fix Issues", () => { it("Generate wrong Regexp item", async () => { try { await RegexpsModel.create({ - name: faker.random.word(), + name: newName(), value: "*" }); } catch (error) { diff --git a/test/issues/github_issue_31.e2e.ts b/test/issues/github/github_issue_31.e2e.ts similarity index 54% rename from test/issues/github_issue_31.e2e.ts rename to test/issues/github/github_issue_31.e2e.ts index 94e177c..7eaf7d8 100644 --- a/test/issues/github_issue_31.e2e.ts +++ b/test/issues/github/github_issue_31.e2e.ts @@ -1,19 +1,20 @@ import supertest = require("supertest"); -import faker = require("faker"); import fs = require("fs-extra"); import { basename } from "path"; import { Model as GoodsModels } from "@models/Good"; import { connect, drop, newUser, addCategoryAndRegexp -} from "../helpers/database"; -import { init } from "../helpers/server"; -import { uploadFile } from "../helpers/files"; +} from "../../helpers/database"; +import { init } from "../../helpers/server"; +import * as files from "../../helpers/files"; import { config } from "@utils/config"; +import auth = require("@db/auth"); +import { newIds } from "../../helpers/utils"; /** - * Fix [Issue 31](https://github.com/Arylo/StoreBox/issues/31) + * Fix [Issue 31](https://github.com/BoxSystem/StoreBox-Api/issues/31) */ describe("Fix Issues", () => { @@ -23,11 +24,7 @@ describe("Fix Issues", () => { return connect(); }); - const ids = { - users: [ ], - categories: [ ], - regexps: [ ] - }; + const ids = newIds(); after(() => { return drop(ids); @@ -41,19 +38,12 @@ describe("Fix Issues", () => { let filepath = ""; let filename = ""; - before(() => { - const folderpath = `${config.paths.tmp}/test`; - if (!fs.existsSync(folderpath)) { - fs.mkdirpSync(folderpath); - } - filepath = `${folderpath}/${faker.random.uuid()}`; - fs.writeFileSync(filepath, JSON.stringify({ - data: Math.random() - }), { encoding: "utf-8" }); + before(async () => { + filepath = await files.newFile(); }); after(() => { - fs.removeSync(filepath); + return files.remove(filepath); }); after(() => { @@ -62,17 +52,8 @@ describe("Fix Issues", () => { }).exec(); }); - const user = { - name: faker.name.firstName(), - pass: faker.random.words() - }; - step("Login", async () => { - const doc = await newUser(user.name, user.pass); - ids.users.push(doc._id); - await request.post("/api/v1/auth/login") - .send({ - username: user.name, password: user.pass - }).then(); + before("login", async () => { + ids.users.push((await auth.login(request))[0]); }); step("Add Category and Regexp", async () => { @@ -87,14 +68,14 @@ describe("Fix Issues", () => { step("Upload Success", async () => { const { body: result, status - } = await uploadFile(request, filepath); + } = await files.uploadFile(request, filepath); status.should.be.eql(201); }); step("Upload Fail", async () => { const { body: result, status - } = await uploadFile(request, filepath); + } = await files.uploadFile(request, filepath); status.should.be.not.eql(201); status.should.be.eql(400); }); diff --git a/test/issues/github_issue_35.e2e.ts b/test/issues/github/github_issue_35.e2e.ts similarity index 57% rename from test/issues/github_issue_35.e2e.ts rename to test/issues/github/github_issue_35.e2e.ts index f21522c..a7c2a51 100644 --- a/test/issues/github_issue_35.e2e.ts +++ b/test/issues/github/github_issue_35.e2e.ts @@ -1,14 +1,15 @@ import supertest = require("supertest"); -import faker = require("faker"); import { connect, drop, newUser -} from "../helpers/database"; -import { init } from "../helpers/server"; +} from "../../helpers/database"; +import { init } from "../../helpers/server"; +import auth = require("@db/auth"); +import { newName, newIds } from "../../helpers/utils"; /** * The Feature of Edit User - * Fix [Issue 35](https://github.com/Arylo/StoreBox/issues/35) + * Fix [Issue 35](https://github.com/BoxSystem/StoreBox-Api/issues/35) */ describe("Fix Issues", () => { @@ -18,9 +19,7 @@ describe("Fix Issues", () => { return connect(); }); - const ids = { - users: [ ] - }; + const ids = newIds(); after(() => { return drop(ids); @@ -32,21 +31,12 @@ describe("Fix Issues", () => { describe("Github 35 [The Feature of Edit User]", () => { - const user = { - name: faker.name.firstName(), - pass: faker.random.words() - }; - before("Login", async () => { - const doc = await newUser(user.name, user.pass); - ids.users.push(doc._id); - await request.post("/api/v1/auth/login") - .send({ - username: user.name, password: user.pass - }).then(); + before("login", async () => { + ids.users.push((await auth.login(request))[0]); }); step("Edit User's Nickname", async () => { - const nickname = faker.name.firstName(); + const nickname = newName(); const id = ids.users[0]; const { status } = await request.post(`/api/v1/users/${id}`) .send({ nickname }) diff --git a/test/issues/gitlab/issue_1.e2e.ts b/test/issues/gitlab/issue_1.e2e.ts new file mode 100644 index 0000000..3ab2b7f --- /dev/null +++ b/test/issues/gitlab/issue_1.e2e.ts @@ -0,0 +1,120 @@ +import * as supertest from "supertest"; +import categories = require("@db/categories"); +import auth = require("@db/auth"); +import * as db from "../../helpers/database"; +import * as server from "../../helpers/server"; +import { newName, newIds } from "../../helpers/utils"; +import * as files from "../../helpers/files"; +import goods = require("@db/goods"); + +/** + * Fix [Issue 1](http://git.pbr.link/Arylo/StoreBox/issues/1) + */ +describe("Fix Issues", () => { + + let request: supertest.SuperTest; + + before(() => { + return db.connect(); + }); + + const ids = newIds(); + + after(() => { + return db.drop(ids); + }); + + before(async () => { + request = await server.init(); + }); + + const filepaths = [ ]; + after(() => { + return files.remove(filepaths); + }); + + before("login", async () => { + ids.users.push((await auth.login(request))[0]); + }); + + let cids = [ ]; + step("Add Category group", async () => { + cids = await categories.addCategories(); + ids.categories.push(...cids); + }); + + describe("Gitlab 1 [Delete Good]", () => { + + const filename = newName(); + let cid; + + step("Add One Category with Regexp", async () => { + const docs = await db.addCategoryAndRegexp( + new RegExp(filename) + ); + cid = docs[0]._id; + ids.categories.push(cid); + ids.regexps.push(docs[1]._id); + }); + + let gid; + + step("Upload File", async () => { + const filepath = await files.newFile(filename); + filepaths.push(filepath); + await files.uploadFile(request, filepath); + gid = await goods.getIdByOriginname(filename); + ids.goods.push(gid); + }); + + step("Delete Good", async () => { + const url = `/api/v1/goods/${gid}`; + const { status } = await request.delete(url).then(); + status.should.be.eql(200); + }); + + step("404 for Good", async () => { + const url = `/files/categories/${cid}/goods/${gid}`; + const { status } = await request.get(url).then(); + status.should.be.eql(404); + }); + + }); + + describe("Gitlab 1 [Move Good to Other Category]", () => { + + const filename = newName(); + + step("Add One Category with Regexp", async () => { + const docs = await db.addCategoryAndRegexp( + new RegExp(filename) + ); + ids.categories.push(docs[0]._id); + ids.regexps.push(docs[1]._id); + }); + + let gid; + + step("Upload File", async () => { + const filepath = await files.newFile(filename); + filepaths.push(filepath); + await files.uploadFile(request, filepath); + gid = await goods.getIdByOriginname(filename); + ids.goods.push(gid); + }); + + step("Move Good ", async () => { + const url = `/api/v1/goods/${gid}`; + const cid = ids.categories[5].toString(); + const { status } = await request.put(url) + .send({ category: cid }) + .then(); + status.should.be.eql(200); + + const { body } = await request.get(url).then(); + body.category._id.toString().should.be.eql(cid); + }); + + }); + +}); diff --git a/test/issues/gitlab/issue_2.e2e.ts b/test/issues/gitlab/issue_2.e2e.ts new file mode 100644 index 0000000..be4cbd2 --- /dev/null +++ b/test/issues/gitlab/issue_2.e2e.ts @@ -0,0 +1,96 @@ +import * as supertest from "supertest"; +import categories = require("@db/categories"); +import auth = require("@db/auth"); +import * as db from "../../helpers/database"; +import * as server from "../../helpers/server"; +import { newName, newIds } from "../../helpers/utils"; + +/** + * Fix [Issue 2](http://git.pbr.link/Arylo/StoreBox/issues/2) + */ +describe("Fix Issues", () => { + + let request: supertest.SuperTest; + + before(() => { + return db.connect(); + }); + + const ids = newIds(); + + after(() => { + return db.drop(ids); + }); + + before(async () => { + request = await server.init(); + }); + + describe("Gitlab 2 [Cant Modify Regexp link]", () => { + + before("login", async () => { + ids.users.push((await auth.login(request))[0]); + }); + + let cids = [ ]; + step("Add Category group", async () => { + cids = await categories.addCategories(); + ids.categories.push(...cids); + }); + + step("Add One Category with Regexp", async () => { + const docs = await db.addCategoryAndRegexp( + new RegExp(newName()) + ); + ids.categories.push(docs[0]._id); + ids.regexps.push(docs[1]._id); + }); + + step("Modify Regexp link", async () => { + const url = `/api/v1/regexps/${ids.regexps[0]}`; + const targetId = ids.categories[10].toString(); + + const { status } = await request.post(url) + .send({ + link: targetId + }).then(); + status.should.be.not.eql(400); + + const { body } = await request.get(url).then(); + body.link._id.should.be.eql(targetId); + }); + + step("Delete Category", () => { + const targetId = ids.categories[10].toString(); + const url = `/api/v1/categories/${targetId}`; + return request.delete(url).then(); + }); + + step("Modify Regexp link", async () => { + const url = `/api/v1/regexps/${ids.regexps[0]}`; + const targetId = ids.categories[8].toString(); + + const { status } = await request.post(url) + .send({ + link: targetId + }).then(); + status.should.be.not.eql(400); + + const { body } = await request.get(url).then(); + body.link._id.should.be.eql(targetId); + }); + + step("Modify Regexp link with other fields", async () => { + const url = `/api/v1/regexps/${ids.regexps[0]}`; + const targetId = ids.categories[7].toString(); + + const { body } = await request.get(url).then(); + body.link = targetId; + + const { status, body: result } = await request.post(url) + .send(body).then(); + status.should.be.not.eql(400); + }); + + }); +}); diff --git a/test/mocha.opts b/test/mocha.opts index 9074855..9a2971a 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -6,7 +6,7 @@ --reporter mochawesome --reporter-options reportDir=./coverage/mochawesome --recursive ---timeout 10000 +--timeout 50000 --exit ./test/**/*.spec.ts ./test/**/*.e2e.ts diff --git a/test/models/Categroies.spec.ts b/test/models/Categroies.spec.ts index 001b116..5723a96 100644 --- a/test/models/Categroies.spec.ts +++ b/test/models/Categroies.spec.ts @@ -1,7 +1,8 @@ import { Model as CategoriesModel } from "@models/Categroy"; import { Model as ValuesModel } from "@models/Value"; import db = require("../helpers/database"); -import faker = require("faker"); +import { addCategories } from "../helpers/database/categories"; +import { newName, newIds } from "../helpers/utils"; describe("Category Model", () => { @@ -9,16 +10,15 @@ describe("Category Model", () => { return db.connect(); }); - const ids = { - categories: [ ] - }; + const ids = newIds(); + after(() => { return db.drop(ids); }); it("Add Category", async () => { const ctx = { - name: faker.name.firstName() + name: newName() }; const obj = await CategoriesModel.create(ctx); ids.categories.push(obj._id); @@ -33,24 +33,8 @@ describe("Category Model", () => { let cids = [ ]; before(async () => { - cids = []; - for (let i = 0; i < 10; i++) { - const result = await CategoriesModel.create({ - name: faker.name.firstName() + i - }); - cids.push(result._id); - ids.categories.push(result._id); - } - // [parent, child] - const initGroups = [ - [0, 1], [0, 2], [0, 3], - [1, 4], [2, 5], - [5, 6], [6, 7], - [8, 9] - ]; - for (const set of initGroups) { - await CategoriesModel.moveCategory(cids[set[1]], cids[set[0]]); - } + cids = await addCategories(); + ids.categories.push(...cids); }); it("# 0", async () => { diff --git a/test/models/regexp.spec.ts b/test/models/regexp.spec.ts index 9445703..3bce308 100644 --- a/test/models/regexp.spec.ts +++ b/test/models/regexp.spec.ts @@ -1,8 +1,8 @@ import * as db from "../helpers/database"; import * as md5 from "md5"; -import * as faker from "faker"; import { Model as RegexpsModel, RegexpDoc } from "@models/Regexp"; import { Model as CategoryModel, ICategoryRaw } from "@models/Categroy"; +import { newName, newIds } from "../helpers/utils"; describe("RegExp Model", () => { @@ -14,23 +14,23 @@ describe("RegExp Model", () => { beforeEach(async () => { const result = await CategoryModel.create({ - name: faker.name.findName() + name: newName() }); ids.categories.push(result._id); Category = result.toObject() as ICategoryRaw; }); - const ids = { - categories: [ ], - regexps: [ ] - }; + const ids = newIds(); + afterEach(() => { return db.drop(ids); }); + const REG = new RegExp(newName()); + it("Add Regexp", async () => { const md5sum = md5(Date.now() + ""); - const reg = await RegexpsModel.addRegexp(md5sum, /[\da-fA-F]/.source); + const reg = await RegexpsModel.addRegexp(md5sum, REG.source); ids.regexps.push(reg._id); reg.should.be.not.an.empty(); }); @@ -39,7 +39,7 @@ describe("RegExp Model", () => { const md5sum = md5(Date.now() + ""); let reg: RegexpDoc; - reg = await RegexpsModel.addRegexp(md5sum, /[\da-fA-F]/.source); + reg = await RegexpsModel.addRegexp(md5sum, REG.source); ids.regexps.push(reg._id); reg = await RegexpsModel.removeRegexp(reg._id); reg = await RegexpsModel.findById(reg._id).exec(); @@ -47,46 +47,48 @@ describe("RegExp Model", () => { should(reg).be.a.null(); }); - it("Link One Category And Undo", async () => { - const md5sum = md5(Date.now() + ""); - let reg: RegexpDoc; + it.skip("Link One Category And Undo", async () => { + // const md5sum = md5(Date.now() + ""); + // let reg: RegexpDoc; - reg = await RegexpsModel.addRegexp(md5sum, /[\da-fA-F]/.source); - ids.regexps.push(reg._id); - reg = await RegexpsModel.link(reg._id, Category._id); - reg = await RegexpsModel.link(reg._id, false); - reg = await RegexpsModel.findById(reg._id).exec(); + // reg = await RegexpsModel.addRegexp(md5sum, /[\da-fA-F]/.source); + // ids.regexps.push(reg._id); + // reg = await RegexpsModel.link(reg._id, Category._id); + // reg = await RegexpsModel.link(reg._id, false); + // reg = await RegexpsModel.findById(reg._id).exec(); - should(reg.toObject().link).be.a.undefined(); + // should(reg.toObject().link).be.a.undefined(); }); - it("Discern from No link Regexp", async () => { - const md5sum = md5(Date.now() + ""); - const regs = [ - await RegexpsModel.addRegexp(`${md5sum}1`, /[\da-fA-F]/.source), - await RegexpsModel.addRegexp(`${md5sum}2`, /[\da-fA-F]{16}/.source), - await RegexpsModel.addRegexp(`${md5sum}3`, /[\da-fA-F]{8}/.source) - ]; - for (const reg of regs) { - ids.regexps.push(reg._id); - } - const list = await RegexpsModel.discern(md5sum); - list.should.be.length(0); + // Discern Method move to Service + it.skip("Discern from No link Regexp", async () => { + // const md5sum = md5(Date.now() + ""); + // const regs = [ + // await RegexpsModel.addRegexp(`${md5sum}1`, /[\da-fA-F]/.source), + // await RegexpsModel.addRegexp(`${md5sum}2`, /[\da-fA-F]{16}/.source), + // await RegexpsModel.addRegexp(`${md5sum}3`, /[\da-fA-F]{8}/.source) + // ]; + // for (const reg of regs) { + // ids.regexps.push(reg._id); + // } + // const list = await RegexpsModel.discern(md5sum); + // list.should.be.length(0); }); - it("Discern from linked Regexps", async () => { - const md5sum = md5(Date.now() + ""); - const regs = [ - await RegexpsModel.addRegexp(`${md5sum}1`, /[\da-fA-F]/.source), - await RegexpsModel.addRegexp(`${md5sum}2`, /[\da-fA-F]{16}/.source), - await RegexpsModel.addRegexp(`${md5sum}3`, /[\da-fA-F]{8}/.source) - ]; - for (const reg of regs) { - ids.regexps.push(reg._id); - await RegexpsModel.link(reg._id, Category._id); - } - const list = await RegexpsModel.discern(md5sum); - list.should.be.length(3); + // Discern Method move to Service + it.skip("Discern from linked Regexps", async () => { + // const md5sum = md5(Date.now() + ""); + // const regs = [ + // await RegexpsModel.addRegexp(`${md5sum}1`, /[\da-fA-F]/.source), + // await RegexpsModel.addRegexp(`${md5sum}2`, /[\da-fA-F]{16}/.source), + // await RegexpsModel.addRegexp(`${md5sum}3`, /[\da-fA-F]{8}/.source) + // ]; + // for (const reg of regs) { + // ids.regexps.push(reg._id); + // await RegexpsModel.link(reg._id, Category._id); + // } + // const list = await RegexpsModel.discern(md5sum); + // list.should.be.length(3); }); }); diff --git a/test/models/user.spec.ts b/test/models/user.spec.ts index 4fa45b8..d750d6c 100644 --- a/test/models/user.spec.ts +++ b/test/models/user.spec.ts @@ -2,6 +2,7 @@ import * as db from "../helpers/database"; import * as md5 from "md5"; import { Model as UsersModel } from "@models/User"; import { Observer, Observable, Subject } from "rxjs"; +import { newIds } from "../helpers/utils"; describe("User Model", () => { @@ -15,9 +16,8 @@ describe("User Model", () => { return db.connect(); }); - const ids = { - users: [ ] - }; + const ids = newIds(); + after(() => { return db.drop(ids); }); @@ -58,11 +58,11 @@ describe("User Model", () => { UsersModel.removeUser(id); }); - it("User List", async () => { - const results = await UsersModel.list(); - results.should.be.an.Array(); - const users = results.map((item) => item.toObject()); - users.should.be.matchAny({ username: user.username }); + it.skip("User List", async () => { + // const results = await UsersModel.list(); + // results.should.be.an.Array(); + // const users = results.map((item) => item.toObject()); + // users.should.be.matchAny({ username: user.username }); }); it("Add User and use same username", async () => { diff --git a/test/pipes/to-array.spec.ts b/test/pipes/to-array.spec.ts new file mode 100644 index 0000000..d897dc8 --- /dev/null +++ b/test/pipes/to-array.spec.ts @@ -0,0 +1,79 @@ +import { ToArrayPipe } from "@pipes/to-array"; + +describe("To Array Pipe Test Unit", () => { + + const fn = (value, ...properties: string[]) => { + return new ToArrayPipe(...properties).transform(value, undefined); + }; + + it("Empty Value and Empty Property", () => { + const value = fn(undefined); + should(value).be.a.undefined(); + }); + + it("Array and Empty Property #0", () => { + const value = fn([ ]); + value.should.be.eql([ ]); + }); + + it("Array and Empty Property #1", () => { + const value = fn([ "test" ]); + value.should.be.eql([ "test" ]); + }); + + it("Array and Empty Property #2", () => { + const value = fn([ 123 ]); + value.should.be.eql([ 123 ]); + }); + + it("String and Empty Property", () => { + const value = fn("foo"); + value.should.be.an.Array().which.eql([ "foo" ]); + }); + + it("Number and Empty Property", () => { + const value = fn(10086); + value.should.be.an.Array().which.eql([ 10086 ]); + }); + + let OBJ; + beforeEach(() => { + OBJ = { + "bar": "foo", + "baz": [ "zoo" ] + }; + }); + + it("Object and Empty Property", () => { + const obj: object = fn(OBJ); + obj.should.have.properties({ + "bar": [ "foo" ], + "baz": [ "zoo" ] + }); + }); + + it("Object and Property #0", () => { + const obj: object = fn(OBJ, "bar"); + obj.should.have.properties({ + "bar": [ "foo" ], + "baz": [ "zoo" ] + }); + }); + + it("Object and Property #1", () => { + const obj: object = fn(OBJ, "baz"); + obj.should.have.properties({ + "bar": "foo", + "baz": [ "zoo" ] + }); + }); + + it("Object and Non-exist field", () => { + const obj: object = fn(OBJ, "foo"); + obj.should.have.properties({ + "bar": "foo", + "baz": [ "zoo" ] + }); + }); + +}); diff --git a/test/services/collections.spec.ts b/test/services/collections.spec.ts index 6531adb..f6e4478 100644 --- a/test/services/collections.spec.ts +++ b/test/services/collections.spec.ts @@ -1,7 +1,7 @@ import { CollectionsService } from "@services/collections"; import { Model as UsersModel } from "@models/User"; import db = require("../helpers/database"); -import faker = require("faker"); +import { newName } from "../helpers/utils"; describe("Collections Service Test Unit", () => { @@ -19,7 +19,7 @@ describe("Collections Service Test Unit", () => { const user = await UsersModel.findOne().exec(); try { await collectionsSvr.create({ - name: faker.random.word(), + name: newName(), creator: user._id }); } catch (error) { @@ -31,7 +31,7 @@ describe("Collections Service Test Unit", () => { const user = await UsersModel.findOne().exec(); try { await collectionsSvr.create({ - name: faker.random.word(), + name: newName(), goods: [ ], creator: user._id }); @@ -44,7 +44,7 @@ describe("Collections Service Test Unit", () => { const user = await UsersModel.findOne().exec(); try { await collectionsSvr.create({ - name: faker.random.word(), + name: newName(), goods: [ "5a77c24ec1ae19d4a808e134" ], creator: user._id }); @@ -59,10 +59,10 @@ describe("Collections Service Test Unit", () => { should(count).be.a.Number(); }); - it("The Function `countPage` will return number", async () => { - const user = await UsersModel.findOne().exec(); - const count = await collectionsSvr.countPage(user._id); - should(count).be.a.Number(); + it.skip("The Function `countPage` will return number", async () => { + // const user = await UsersModel.findOne().exec(); + // const count = await collectionsSvr.countPage(user._id); + // should(count).be.a.Number(); }); it("The Function `list` will return array", async () => { diff --git a/test/services/users.spec.ts b/test/services/users.spec.ts index bc0fcaa..f759c36 100644 --- a/test/services/users.spec.ts +++ b/test/services/users.spec.ts @@ -1,8 +1,9 @@ import { UsersService } from "@services/users"; +import { SystemService } from "@services/system"; import { Model as UsersModel } from "@models/User"; import db = require("../helpers/database"); -import faker = require("faker"); import { newUser } from "../helpers/database"; +import { newName, newIds } from "../helpers/utils"; describe("Users Service Test Unit", () => { @@ -12,20 +13,19 @@ describe("Users Service Test Unit", () => { return db.connect(); }); - const ids = { - users: [ ] - }; + const ids = newIds(); + after(() => { return db.drop(ids); }); beforeEach(() => { - usersSvr = new UsersService(); + usersSvr = new UsersService(new SystemService()); }); const user = { - name: faker.name.firstName(), - pass: faker.random.words() + name: newName(), + pass: newName() }; before(async () => { const doc = await newUser(user.name, user.pass); @@ -34,13 +34,13 @@ describe("Users Service Test Unit", () => { it("Cant Modify Username", async () => { const id = ids.users[0]; - usersSvr.modify(id, { username: faker.name.firstName() }) + usersSvr.modify(id, { username: newName() }) .should.be.rejected(); }); it("Modify nickname", async () => { const id = ids.users[0]; - usersSvr.modify(id, { nickname: faker.name.firstName() }) + usersSvr.modify(id, { nickname: newName() }) .should.be.fulfilled(); }); diff --git a/test/tsconfig.json b/test/tsconfig.json index 0e68987..1fa5afd 100644 --- a/test/tsconfig.json +++ b/test/tsconfig.json @@ -9,6 +9,7 @@ "mocha-steps", "should" ], + "strictFunctionTypes": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "importHelpers": true, @@ -18,13 +19,15 @@ "outDir": "../dist", "baseUrl": "../", "paths": { - "@utils/*": [ "./src/utils/*", "./dist/utils/*" ], - "@models/*": [ "./src/models/*", "./dist/models/*" ], + "@utils/*": [ "./src/utils/*" ], + "@models/*": [ "./src/models/*" ], "@decorators/*": [ "./src/modules/common/decorators/*.decorator" ], "@pipes/*": [ "./src/modules/common/pipes/*.pipe" ], "@guards/*": [ "./src/modules/common/guards/*.guard" ], "@dtos/*": [ "./src/modules/common/dtos/*.dto" ], - "@services/*": [ "./src/modules/common/services/*.service" ] + "@services/*": [ "./src/services/*.service" ], + "@interceptors/*": [ "./src/modules/common/interceptors/*.interceptor" ], + "@db/*": [ "./test/helpers/database/*" ] } }, "include": [ diff --git a/tsconfig.json b/tsconfig.json index ecc45ee..a56a1d7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "mongoose", "node" ], + "strictFunctionTypes": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, "importHelpers": true, @@ -33,9 +34,13 @@ "./src/modules/common/dtos/*.dto", "./dist/modules/common/dtos/*.dto" ], + "@interceptors/*": [ + "./src/modules/common/interceptors/*.interceptor", + "./dist/modules/common/interceptors/*.interceptor" + ], "@services/*": [ - "./src/modules/common/services/*.service", - "./dist/modules/common/services/*.service" + "./src/services/*.service", + "./dist/services/*.service" ] } },