diff --git a/package.json b/package.json index cb6c29f..fdf1aee 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "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", diff --git a/src/models/System.ts b/src/models/System.ts index 375b703..80eb992 100644 --- a/src/models/System.ts +++ b/src/models/System.ts @@ -1,5 +1,14 @@ import { model, SchemaDefinition, Model as M } from "mongoose"; -import { Base, IDoc, IDocRaw } from "./common"; +import { Base, IDoc, IDocRaw, MODIFY_MOTHODS } from "./common"; +import { config } from "@utils/config"; +import keyv = require("keyv"); + +import { isTest } from "../modules/common/helper/env"; + +export const cache = new keyv({ + uri: isTest ? undefined : config.redis.url, + namespace: "System" +}); const Definition: SchemaDefinition = { key: { type: String, required: true }, @@ -13,8 +22,14 @@ export interface ISystem extends IDocRaw { const SystemSchema = new Base(Definition).createSchema(); -export const Flag = "sys"; +export const Flag = "system"; 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/modules/common/services/base.service.ts b/src/modules/common/services/base.service.ts index 84f2e28..1bcb9fc 100644 --- a/src/modules/common/services/base.service.ts +++ b/src/modules/common/services/base.service.ts @@ -34,8 +34,26 @@ abstract class ModelService { } } + 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.beforeAll(); + await this.beforeEach(); try { return await this.model.create(obj); } catch (error) { @@ -45,6 +63,8 @@ abstract class ModelService { public async delete(cond: object) { this.checkModel(); + await this.beforeAll(); + await this.beforeEach(); try { return await this.model.findOneAndRemove(cond).exec(); } catch (error) { @@ -54,6 +74,8 @@ abstract class ModelService { public async deleteById(id: ObjectId) { this.checkModel(); + await this.beforeAll(); + await this.beforeEach(); try { return await this.model.findByIdAndRemove(id).exec(); } catch (error) { @@ -65,6 +87,8 @@ abstract class ModelService { id: ObjectId, ctx: object, opts = this.DEF_UPDATE_OPTIONS ) { this.checkModel(); + await this.beforeAll(); + await this.beforeEach(); const options = Object.assign({ }, this.DEF_UPDATE_OPTIONS, opts); try { return await this.model.update({ _id: id }, ctx, options).exec(); @@ -73,8 +97,10 @@ abstract class ModelService { } } - protected find(cond: object, opts?: IGetOptions) { + protected async find(cond: object, opts?: IGetOptions) { this.checkModel(); + await this.beforeAll(); + await this.beforeEach(); const p = this.model.find(cond); return this.documentQueryProcess(p, opts).exec(); } @@ -85,8 +111,10 @@ abstract class ModelService { }); } - protected findOne(cond: object, opts?: IGetOptions) { + protected async findOne(cond: object, opts?: IGetOptions) { this.checkModel(); + await this.beforeAll(); + await this.beforeEach(); const p = this.model.findOne(cond); return this.documentQueryProcess(p, opts).exec(); } @@ -97,8 +125,10 @@ abstract class ModelService { }); } - protected findById(id: ObjectId, opts?: IGetOptions) { + protected async findById(id: ObjectId, opts?: IGetOptions) { this.checkModel(); + await this.beforeAll(); + await this.beforeEach(); const p = this.model.findById(id); return this.documentQueryProcess(p, opts).exec(); } @@ -109,8 +139,10 @@ abstract class ModelService { }); } - protected total(cond: object = { }) { + protected async total(cond: object = { }) { this.checkModel(); + await this.beforeAll(); + await this.beforeEach(); return this.model.count(cond).exec(); } @@ -170,6 +202,12 @@ export abstract class BaseService extends ModelServ return val; } + protected runBeforeAll() { + return this.loadAndCache("_RunBeforeAll_", () => { + return super.runBeforeAll(); + }); + } + protected DEF_PER_OBJ = UtilService.DEF_PER_OBJ; /** diff --git a/src/modules/common/services/regexps.service.ts b/src/modules/common/services/regexps.service.ts index cf4d9f8..6d546e9 100644 --- a/src/modules/common/services/regexps.service.ts +++ b/src/modules/common/services/regexps.service.ts @@ -20,14 +20,15 @@ export class RegexpsService extends BaseService { super(); this.setCache(cache); this.setModel(RegexpsModel); + } + + protected async beforeAll() { // Update - setTimeout(() => { - // Add Hidden Label - RegexpsModel.update( - { hidden: { $exists: false } }, { hidden: false }, - { multi: true } - ).exec(); - }, 3000); + // Add Hidden Label + await RegexpsModel.update( + { hidden: { $exists: false } }, { hidden: false }, + { multi: true } + ).exec(); } /** diff --git a/src/modules/common/services/system.service.ts b/src/modules/common/services/system.service.ts index 5553f8f..10a50ec 100644 --- a/src/modules/common/services/system.service.ts +++ b/src/modules/common/services/system.service.ts @@ -1,30 +1,87 @@ import { Component, BadRequestException } from "@nestjs/common"; import { ObjectId } from "@models/common"; -import { Model as SystemModel } from "@models/System"; +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 "../helper/log"; +export enum DEFAULTS { + USERGROUP_FLAG = "DEFAULT_USERGROUP", + GOOD_URL_FLAG = "DEFAULT_GOOD_URL", + COLLECTION_URL_FLAG = "DEFAULT_COLLECTION_URL" +} + @Component() -export class SystemService { +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; + } - public static DEFAULT_USERGROUP_FLAG = "DEFAULT_USERGROUP"; + 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 async getDefaultUsergroup(): Promise { - let gid: any = await SystemModel.findOne({ - key: SystemService.DEFAULT_USERGROUP_FLAG - }).exec(); - /* istanbul ignore if */ - if (!gid) { - systemLogger.warn(`Miss ${SystemService.DEFAULT_USERGROUP_FLAG}`); - gid = (await UsergroupsModel.findOne().exec())._id; - this.setDefaultUsergroup(gid); - } - return gid; + 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; + }); } /** @@ -32,17 +89,97 @@ export class SystemService { * @param gid Usergroup ID */ public async setDefaultUsergroup(gid: ObjectId) { - const doc = await UsergroupsModel.findById(gid).exec(); - /* istanbul ignore if */ - if (!doc) { - throw new BadRequestException("The ID isnt a Usergroup ID"); + 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); } - return SystemModel - .findOneAndUpdate( - { key: SystemService.DEFAULT_USERGROUP_FLAG }, { value: gid }, - { upsert: true } - ) - .exec(); + 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/controllers.module.ts b/src/modules/controllers.module.ts index b383edd..cb817d1 100644 --- a/src/modules/controllers.module.ts +++ b/src/modules/controllers.module.ts @@ -15,6 +15,7 @@ import { } 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 @@ -48,7 +49,8 @@ export const controllers = [ RegexpsAdminController, CategoriesAdminController, GoodsAdminController, TokensAdminController, - CollectionsController, CollectionsAdminController + CollectionsController, CollectionsAdminController, + SystemController ]; export const services = [ diff --git a/src/modules/database/database.providers.ts b/src/modules/database/database.providers.ts index 5b40776..d03a91a 100644 --- a/src/modules/database/database.providers.ts +++ b/src/modules/database/database.providers.ts @@ -4,7 +4,6 @@ import { config } from "@utils/config"; import { Model as UsersModel } from "@models/User"; import { Model as UsergroupsModel } from "@models/Usergroup"; import { Model as UserUsergroupsModel } from "@models/User-Usergroup"; -import { Model as SystemModel } from "@models/System"; import { SystemService } from "@services/system"; import { systemLogger } from "../common/helper/log"; @@ -55,10 +54,7 @@ export const injectData = async () => { usergroup: group._id }; }); - await SystemModel.findOneAndUpdate( - { key: SystemService.DEFAULT_USERGROUP_FLAG }, - { value: group._id.toString() }, { upsert: true } - ).exec(); + await new SystemService().setDefaultUsergroup(group._id); await UserUsergroupsModel.create(conditions); } }; 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/users/auth.controller.ts b/src/modules/users/auth.controller.ts index e89a98d..629e946 100644 --- a/src/modules/users/auth.controller.ts +++ b/src/modules/users/auth.controller.ts @@ -34,7 +34,7 @@ export class AuthAdminController { @Body() ctx: LoginBodyDto, @Query() query: LoginQueryDto ) { const user = - await this.usersSvr.isVaild(ctx.username, ctx.password);; + await this.usersSvr.isVaild(ctx.username, ctx.password); session.loginUser = user.toObject().username; session.loginUserId = user.toObject()._id; const obj = new LoginRespone(); 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); + }); + +});