fix: config readonly & clone deep
This commit is contained in:
@@ -62,6 +62,7 @@
|
||||
"bcrypt": "5.0.1",
|
||||
"cache-manager": "3.4.4",
|
||||
"cache-manager-ioredis": "2.1.0",
|
||||
"camelcase-keys": "^7.0.0",
|
||||
"chalk": "*",
|
||||
"class-transformer": "0.4.0",
|
||||
"class-validator": "0.13.1",
|
||||
|
||||
23
pnpm-lock.yaml
generated
23
pnpm-lock.yaml
generated
@@ -45,6 +45,7 @@ specifiers:
|
||||
bcrypt: 5.0.1
|
||||
cache-manager: 3.4.4
|
||||
cache-manager-ioredis: 2.1.0
|
||||
camelcase-keys: ^7.0.0
|
||||
chalk: '*'
|
||||
class-transformer: 0.4.0
|
||||
class-validator: 0.13.1
|
||||
@@ -125,6 +126,7 @@ dependencies:
|
||||
bcrypt: 5.0.1
|
||||
cache-manager: 3.4.4
|
||||
cache-manager-ioredis: 2.1.0
|
||||
camelcase-keys: 7.0.0
|
||||
chalk: 4.1.2
|
||||
class-transformer: 0.4.0
|
||||
class-validator: 0.13.1
|
||||
@@ -3104,6 +3106,16 @@ packages:
|
||||
tslib: 2.3.1
|
||||
dev: false
|
||||
|
||||
/camelcase-keys/7.0.0:
|
||||
resolution: {integrity: sha512-qlQlECgDl5Ev+gkvONaiD4X4TF2gyZKuLBvzx0zLo2UwAxmz3hJP/841aaMHTeH1T7v5HRwoRq91daulXoYWvg==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
camelcase: 6.2.0
|
||||
map-obj: 4.2.1
|
||||
quick-lru: 5.1.1
|
||||
type-fest: 1.4.0
|
||||
dev: false
|
||||
|
||||
/camelcase/5.3.1:
|
||||
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -3112,7 +3124,6 @@ packages:
|
||||
/camelcase/6.2.0:
|
||||
resolution: {integrity: sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==}
|
||||
engines: {node: '>=10'}
|
||||
dev: true
|
||||
|
||||
/caniuse-lite/1.0.30001255:
|
||||
resolution: {integrity: sha512-F+A3N9jTZL882f/fg/WWVnKSu6IOo3ueLz4zwaOPbPYHNmM/ZaDUyzyJwS1mZhX7Ex5jqTyW599Gdelh5PDYLQ==}
|
||||
@@ -7073,6 +7084,11 @@ packages:
|
||||
/quick-format-unescaped/4.0.3:
|
||||
resolution: {integrity: sha512-MaL/oqh02mhEo5m5J2rwsVL23Iw2PEaGVHgT2vFt8AAsr0lfvQA5dpXo9TPu0rz7tSBdUPgkbam0j/fj5ZM8yg==}
|
||||
|
||||
/quick-lru/5.1.1:
|
||||
resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==}
|
||||
engines: {node: '>=10'}
|
||||
dev: false
|
||||
|
||||
/randombytes/2.1.0:
|
||||
resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==}
|
||||
dependencies:
|
||||
@@ -8235,6 +8251,11 @@ packages:
|
||||
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
/type-fest/1.4.0:
|
||||
resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==}
|
||||
engines: {node: '>=10'}
|
||||
dev: false
|
||||
|
||||
/typedarray-to-buffer/3.1.5:
|
||||
resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==}
|
||||
dependencies:
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
ValidateNested,
|
||||
} from 'class-validator'
|
||||
|
||||
export class SEODto {
|
||||
export class SeoDto {
|
||||
@IsString({ message: '标题必须是字符串' })
|
||||
@IsNotEmpty({ message: '不能为空!!' })
|
||||
@IsOptional()
|
||||
@@ -80,7 +80,7 @@ export class MailOptionsDto {
|
||||
options?: MailOption
|
||||
}
|
||||
|
||||
export class CommentOptions {
|
||||
export class CommentOptionsDto {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
antiSpam: boolean
|
||||
@@ -97,7 +97,7 @@ export class CommentOptions {
|
||||
disableNoChinese?: boolean
|
||||
}
|
||||
|
||||
export class BackupOptions {
|
||||
export class BackupOptionsDto {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
enable: boolean
|
||||
@@ -119,7 +119,7 @@ export class BackupOptions {
|
||||
region: string
|
||||
}
|
||||
|
||||
export class BaiduSearchOptions {
|
||||
export class BaiduSearchOptionsDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
enable: boolean
|
||||
@@ -130,7 +130,7 @@ export class BaiduSearchOptions {
|
||||
token?: string
|
||||
}
|
||||
|
||||
export class AlgoliaSearchOptions {
|
||||
export class AlgoliaSearchOptionsDto {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
enable: boolean
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import {
|
||||
AlgoliaSearchOptions,
|
||||
BackupOptions,
|
||||
BaiduSearchOptions,
|
||||
CommentOptions,
|
||||
AlgoliaSearchOptionsDto,
|
||||
BackupOptionsDto,
|
||||
BaiduSearchOptionsDto,
|
||||
CommentOptionsDto,
|
||||
MailOptionsDto,
|
||||
SEODto,
|
||||
SeoDto,
|
||||
UrlDto,
|
||||
} from './configs.dto'
|
||||
|
||||
export interface IConfig {
|
||||
seo: SEODto
|
||||
seo: SeoDto
|
||||
url: UrlDto
|
||||
mailOptions: MailOptionsDto
|
||||
commentOptions: CommentOptions
|
||||
backupOptions: BackupOptions
|
||||
baiduSearchOptions: BaiduSearchOptions
|
||||
algoliaSearchOptions: AlgoliaSearchOptions
|
||||
commentOptions: CommentOptionsDto
|
||||
backupOptions: BackupOptionsDto
|
||||
baiduSearchOptions: BaiduSearchOptionsDto
|
||||
algoliaSearchOptions: AlgoliaSearchOptionsDto
|
||||
}
|
||||
|
||||
export type IConfigKeys = keyof IConfig
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { Injectable, Logger } from '@nestjs/common'
|
||||
import { ReturnModelType } from '@typegoose/typegoose'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { InjectModel } from 'nestjs-typegoose'
|
||||
import { sleep } from '~/utils/index.util'
|
||||
import { UserService } from '../user/user.service'
|
||||
import { BackupOptions, MailOptionsDto } from './configs.dto'
|
||||
import { BackupOptionsDto, MailOptionsDto } from './configs.dto'
|
||||
import { IConfig } from './configs.interface'
|
||||
import { OptionModel } from './configs.model'
|
||||
|
||||
const defaultConfig: IConfig = {
|
||||
const generateDefaultConfig: () => IConfig = () => ({
|
||||
seo: {
|
||||
title: 'mx-space',
|
||||
description: 'Hello World~',
|
||||
@@ -20,14 +21,14 @@ const defaultConfig: IConfig = {
|
||||
},
|
||||
mailOptions: {} as MailOptionsDto,
|
||||
commentOptions: { antiSpam: false },
|
||||
backupOptions: { enable: false } as BackupOptions,
|
||||
backupOptions: { enable: false } as BackupOptionsDto,
|
||||
baiduSearchOptions: { enable: false },
|
||||
algoliaSearchOptions: { enable: false, apiKey: '', appId: '', indexName: '' },
|
||||
}
|
||||
})
|
||||
|
||||
@Injectable()
|
||||
export class ConfigsService {
|
||||
private config: IConfig = defaultConfig
|
||||
private config: IConfig = generateDefaultConfig()
|
||||
private logger: Logger
|
||||
constructor(
|
||||
@InjectModel(OptionModel)
|
||||
@@ -75,19 +76,11 @@ export class ConfigsService {
|
||||
this.logger.log('Config 已经加载完毕!')
|
||||
}
|
||||
|
||||
public get seo() {
|
||||
return this.config.seo
|
||||
}
|
||||
|
||||
public get url() {
|
||||
return this.config.url
|
||||
}
|
||||
|
||||
public get<T extends keyof IConfig>(key: T): Readonly<IConfig[T]> {
|
||||
return this.config[key] as Readonly<IConfig[T]>
|
||||
return cloneDeep(this.config[key]) as Readonly<IConfig[T]>
|
||||
}
|
||||
public getConfig(): Readonly<IConfig> {
|
||||
return this.config
|
||||
return cloneDeep(this.config)
|
||||
}
|
||||
|
||||
public async patch<T extends keyof IConfig>(key: T, data: IConfig[T]) {
|
||||
@@ -98,9 +91,10 @@ export class ConfigsService {
|
||||
)
|
||||
const newData = (await this.optionModel.findOne({ name: key as string }))
|
||||
.value
|
||||
|
||||
this.config[key] = newData
|
||||
|
||||
return this.config[key]
|
||||
return cloneDeep(this.config[key])
|
||||
}
|
||||
|
||||
get getMaster() {
|
||||
|
||||
@@ -7,27 +7,26 @@
|
||||
* @Coding with Love
|
||||
*/
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
UnprocessableEntityException,
|
||||
ValidationPipe,
|
||||
} from '@nestjs/common'
|
||||
import { BadRequestException, Injectable, ValidationPipe } from '@nestjs/common'
|
||||
import camelcaseKeys from 'camelcase-keys'
|
||||
import { ClassConstructor, plainToClass } from 'class-transformer'
|
||||
import { validateSync, ValidatorOptions } from 'class-validator'
|
||||
import { CronService } from '~/processors/helper/helper.cron.service'
|
||||
import { EmailService } from '~/processors/helper/helper.email.service'
|
||||
import {
|
||||
AlgoliaSearchOptions,
|
||||
BackupOptions,
|
||||
BaiduSearchOptions,
|
||||
CommentOptions,
|
||||
MailOptionsDto,
|
||||
SEODto,
|
||||
UrlDto,
|
||||
} from '../configs/configs.dto'
|
||||
import * as optionDtos from '../configs/configs.dto'
|
||||
import { AlgoliaSearchOptionsDto, MailOptionsDto } from '../configs/configs.dto'
|
||||
import { IConfig } from '../configs/configs.interface'
|
||||
import { ConfigsService } from '../configs/configs.service'
|
||||
|
||||
const map: Record<string, any> = Object.entries(optionDtos).reduce(
|
||||
(obj, [key, value]) => ({
|
||||
...obj,
|
||||
[`${key.charAt(0).toLowerCase() + key.slice(1).replace(/Dto$/, '')}`]:
|
||||
value,
|
||||
}),
|
||||
{},
|
||||
)
|
||||
|
||||
@Injectable()
|
||||
export class OptionService {
|
||||
constructor(
|
||||
@@ -42,16 +41,9 @@ export class OptionService {
|
||||
}
|
||||
validate = new ValidationPipe(this.validOptions)
|
||||
patchAndValid(key: keyof IConfig, value: any) {
|
||||
switch (key) {
|
||||
case 'url': {
|
||||
this.validWithDto(UrlDto, value)
|
||||
return this.configs.patch('url', value)
|
||||
}
|
||||
case 'commentOptions': {
|
||||
this.validWithDto(CommentOptions, value)
|
||||
return this.configs.patch('commentOptions', value)
|
||||
}
|
||||
value = camelcaseKeys(value, { deep: true })
|
||||
|
||||
switch (key) {
|
||||
case 'mailOptions': {
|
||||
this.validWithDto(MailOptionsDto, value)
|
||||
const task = this.configs.patch('mailOptions', value)
|
||||
@@ -61,29 +53,24 @@ export class OptionService {
|
||||
})
|
||||
return task
|
||||
}
|
||||
case 'seo': {
|
||||
this.validWithDto(SEODto, value)
|
||||
return this.configs.patch('seo', value)
|
||||
}
|
||||
case 'backupOptions': {
|
||||
this.validWithDto(BackupOptions, value)
|
||||
return this.configs.patch('backupOptions', value)
|
||||
}
|
||||
case 'baiduSearchOptions': {
|
||||
this.validWithDto(BaiduSearchOptions, value)
|
||||
|
||||
return this.configs.patch('baiduSearchOptions', value)
|
||||
}
|
||||
case 'algoliaSearchOptions': {
|
||||
this.validWithDto(AlgoliaSearchOptions, value)
|
||||
|
||||
return this.configs.patch('algoliaSearchOptions', value).then((r) => {
|
||||
this.cronService.pushToAlgoliaSearch()
|
||||
return r
|
||||
})
|
||||
return this.configs
|
||||
.patch(
|
||||
'algoliaSearchOptions',
|
||||
this.validWithDto(AlgoliaSearchOptionsDto, value),
|
||||
)
|
||||
.then((r) => {
|
||||
this.cronService.pushToAlgoliaSearch()
|
||||
return r
|
||||
})
|
||||
}
|
||||
default: {
|
||||
throw new UnprocessableEntityException('设置不存在')
|
||||
const dto = map[key]
|
||||
if (!dto) {
|
||||
throw new BadRequestException('设置不存在')
|
||||
}
|
||||
return this.configs.patch(key, this.validWithDto(dto, value))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,6 +82,6 @@ export class OptionService {
|
||||
const error = this.validate.createExceptionFactory()(errors as any[])
|
||||
throw error
|
||||
}
|
||||
return true
|
||||
return validModel
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,6 +265,7 @@ export class CronService {
|
||||
.lean()
|
||||
.then((list) => {
|
||||
return list.map((data) => {
|
||||
Reflect.set(data, 'objectID', data._id)
|
||||
Reflect.deleteProperty(data, '_id')
|
||||
return {
|
||||
...data,
|
||||
@@ -277,6 +278,7 @@ export class CronService {
|
||||
.lean()
|
||||
.then((list) => {
|
||||
return list.map((data) => {
|
||||
Reflect.set(data, 'objectID', data._id)
|
||||
Reflect.deleteProperty(data, '_id')
|
||||
return {
|
||||
...data,
|
||||
@@ -300,6 +302,7 @@ export class CronService {
|
||||
.then((list) => {
|
||||
return list.map((data) => {
|
||||
const id = data.nid.toString()
|
||||
Reflect.set(data, 'objectID', data._id)
|
||||
Reflect.deleteProperty(data, '_id')
|
||||
Reflect.deleteProperty(data, 'nid')
|
||||
return {
|
||||
@@ -314,8 +317,9 @@ export class CronService {
|
||||
documents.push(...documents_)
|
||||
})
|
||||
try {
|
||||
await index.clearObjects()
|
||||
await index.saveObjects(documents, {
|
||||
autoGenerateObjectIDIfNotExist: true,
|
||||
autoGenerateObjectIDIfNotExist: false,
|
||||
})
|
||||
this.logger.log('--> 推送到 algoliasearch 成功')
|
||||
} catch {
|
||||
|
||||
@@ -4,3 +4,9 @@ import { isDev } from './index.util'
|
||||
Object.assign(globalThis, {
|
||||
isDev: isDev,
|
||||
})
|
||||
|
||||
console.debug = (...rest) => {
|
||||
if (isDev) {
|
||||
console.log.call(console, ...rest)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user