add typed preferences
This commit is contained in:
parent
c26e1aef38
commit
721b5c05fe
|
@ -2,8 +2,18 @@ import { Injectable, Logger } from '@nestjs/common';
|
|||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { plainToClass } from 'class-transformer';
|
||||
import { validate } from 'class-validator';
|
||||
import { SysPreferences } from 'picsur-shared/dist/dto/syspreferences.dto';
|
||||
import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types';
|
||||
import {
|
||||
InternalSysprefRepresentation,
|
||||
SysPreferences,
|
||||
SysPreferenceValueTypes,
|
||||
SysPrefValueType
|
||||
} from 'picsur-shared/dist/dto/syspreferences.dto';
|
||||
import {
|
||||
AsyncFailable,
|
||||
Fail,
|
||||
Failable,
|
||||
HasFailed
|
||||
} from 'picsur-shared/dist/types';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ESysPreferenceBackend } from '../../models/entities/syspreference.entity';
|
||||
import { SysPreferenceDefaultsService } from './syspreferencedefaults.service';
|
||||
|
@ -20,11 +30,13 @@ export class SysPreferenceService {
|
|||
|
||||
public async setPreference(
|
||||
key: SysPreferences,
|
||||
value: string,
|
||||
): AsyncFailable<ESysPreferenceBackend> {
|
||||
value: SysPrefValueType,
|
||||
): AsyncFailable<InternalSysprefRepresentation> {
|
||||
// Validate
|
||||
let sysPreference = await this.validatePref(key, value);
|
||||
if (HasFailed(sysPreference)) return sysPreference;
|
||||
|
||||
// Set
|
||||
try {
|
||||
await this.sysPreferenceRepository.upsert(sysPreference, {
|
||||
conflictPaths: ['key'],
|
||||
|
@ -34,19 +46,25 @@ export class SysPreferenceService {
|
|||
return Fail('Could not save preference');
|
||||
}
|
||||
|
||||
return sysPreference;
|
||||
// Return
|
||||
return {
|
||||
value,
|
||||
type: SysPreferenceValueTypes[key],
|
||||
};
|
||||
}
|
||||
|
||||
public async getPreference(
|
||||
key: SysPreferences,
|
||||
): AsyncFailable<ESysPreferenceBackend> {
|
||||
let sysPreference = await this.validatePref(key);
|
||||
if (HasFailed(sysPreference)) return sysPreference;
|
||||
): AsyncFailable<InternalSysprefRepresentation> {
|
||||
// Validate
|
||||
let validatedKey = this.validatePrefKey(key);
|
||||
if (HasFailed(validatedKey)) return validatedKey;
|
||||
|
||||
// Fetch
|
||||
let foundSysPreference: ESysPreferenceBackend | undefined;
|
||||
try {
|
||||
foundSysPreference = await this.sysPreferenceRepository.findOne(
|
||||
{ key: sysPreference.key },
|
||||
{ key: validatedKey },
|
||||
{ cache: 60000 },
|
||||
);
|
||||
} catch (e: any) {
|
||||
|
@ -54,8 +72,9 @@ export class SysPreferenceService {
|
|||
return Fail('Could not get preference');
|
||||
}
|
||||
|
||||
// Fallback
|
||||
if (!foundSysPreference) {
|
||||
return this.saveDefault(sysPreference.key);
|
||||
return this.saveDefault(validatedKey);
|
||||
} else {
|
||||
foundSysPreference = plainToClass(
|
||||
ESysPreferenceBackend,
|
||||
|
@ -70,23 +89,84 @@ export class SysPreferenceService {
|
|||
}
|
||||
}
|
||||
|
||||
return foundSysPreference;
|
||||
// Return
|
||||
return this.retrieveConvertedValue(foundSysPreference);
|
||||
}
|
||||
|
||||
public async getStringPreference(key: SysPreferences): AsyncFailable<string> {
|
||||
const pref = await this.getPreference(key);
|
||||
if (HasFailed(pref)) return pref;
|
||||
if (pref.type !== 'string') return Fail('Invalid preference type');
|
||||
|
||||
return pref.value as string;
|
||||
}
|
||||
|
||||
public async getNumberPreference(key: SysPreferences): AsyncFailable<number> {
|
||||
const pref = await this.getPreference(key);
|
||||
if (HasFailed(pref)) return pref;
|
||||
if (pref.type !== 'number') return Fail('Invalid preference type');
|
||||
|
||||
return pref.value as number;
|
||||
}
|
||||
|
||||
public async getBooleanPreference(
|
||||
key: SysPreferences,
|
||||
): AsyncFailable<boolean> {
|
||||
const pref = await this.getPreference(key);
|
||||
if (HasFailed(pref)) return pref;
|
||||
if (pref.type !== 'boolean') return Fail('Invalid preference type');
|
||||
|
||||
return pref.value as boolean;
|
||||
}
|
||||
|
||||
// Private
|
||||
|
||||
private async saveDefault(
|
||||
key: SysPreferences,
|
||||
): AsyncFailable<ESysPreferenceBackend> {
|
||||
): AsyncFailable<InternalSysprefRepresentation> {
|
||||
return this.setPreference(key, this.defaultsService.defaults[key]());
|
||||
}
|
||||
|
||||
private retrieveConvertedValue(
|
||||
preference: ESysPreferenceBackend,
|
||||
): Failable<InternalSysprefRepresentation> {
|
||||
const type = SysPreferenceValueTypes[preference.key];
|
||||
switch (type) {
|
||||
case 'string':
|
||||
return {
|
||||
value: preference.value,
|
||||
type: 'string',
|
||||
};
|
||||
case 'number':
|
||||
return {
|
||||
value: parseInt(preference.value, 10),
|
||||
type: 'number',
|
||||
};
|
||||
case 'boolean':
|
||||
return {
|
||||
value: preference.value == 'true',
|
||||
type: 'boolean',
|
||||
};
|
||||
}
|
||||
|
||||
return Fail('Invalid preference value');
|
||||
}
|
||||
|
||||
private async validatePref(
|
||||
key: string,
|
||||
value: string = 'validate',
|
||||
value: SysPrefValueType,
|
||||
): AsyncFailable<ESysPreferenceBackend> {
|
||||
let verifySysPreference = new ESysPreferenceBackend();
|
||||
verifySysPreference.key = key as SysPreferences;
|
||||
verifySysPreference.value = value;
|
||||
const validatedKey = this.validatePrefKey(key);
|
||||
if (HasFailed(validatedKey)) return validatedKey;
|
||||
|
||||
const validatedValue = this.validatePrefValue(validatedKey, value);
|
||||
if (HasFailed(validatedValue)) return validatedValue;
|
||||
|
||||
let verifySysPreference = new ESysPreferenceBackend();
|
||||
verifySysPreference.key = validatedKey;
|
||||
verifySysPreference.value = validatedValue;
|
||||
|
||||
// Just to be sure
|
||||
const errors = await validate(verifySysPreference, {
|
||||
forbidUnknownValues: true,
|
||||
});
|
||||
|
@ -97,4 +177,35 @@ export class SysPreferenceService {
|
|||
|
||||
return verifySysPreference;
|
||||
}
|
||||
|
||||
private validatePrefKey(key: string): Failable<SysPreferences> {
|
||||
if (!SysPreferences.includes(key)) {
|
||||
return Fail('Invalid preference key');
|
||||
}
|
||||
|
||||
return key as SysPreferences;
|
||||
}
|
||||
|
||||
private validatePrefValue(
|
||||
key: SysPreferences,
|
||||
value: SysPrefValueType,
|
||||
): Failable<string> {
|
||||
const expectedType = SysPreferenceValueTypes[key];
|
||||
|
||||
const type = typeof value;
|
||||
if (type != expectedType) {
|
||||
return Fail('Invalid preference value');
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'string':
|
||||
return value as string;
|
||||
case 'number':
|
||||
return value.toString();
|
||||
case 'boolean':
|
||||
return value ? 'true' : 'false';
|
||||
}
|
||||
|
||||
return Fail('Invalid preference value');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { SysPreferences } from 'picsur-shared/dist/dto/syspreferences.dto';
|
||||
import {
|
||||
SysPreferences,
|
||||
SysPrefValueType
|
||||
} from 'picsur-shared/dist/dto/syspreferences.dto';
|
||||
import { generateRandomString } from 'picsur-shared/dist/util/random';
|
||||
import { EnvJwtConfigService } from '../../config/jwt.config.service';
|
||||
|
||||
|
@ -10,7 +13,7 @@ export class SysPreferenceDefaultsService {
|
|||
constructor(private jwtConfigService: EnvJwtConfigService) {}
|
||||
|
||||
public readonly defaults: {
|
||||
[key in SysPreferences]: () => string;
|
||||
[key in SysPreferences]: () => SysPrefValueType;
|
||||
} = {
|
||||
jwt_secret: () => {
|
||||
const envSecret = this.jwtConfigService.getJwtSecret();
|
||||
|
@ -24,6 +27,10 @@ export class SysPreferenceDefaultsService {
|
|||
}
|
||||
},
|
||||
jwt_expires_in: () => this.jwtConfigService.getJwtExpiresIn() ?? '7d',
|
||||
upload_require_auth: () => 'true',
|
||||
upload_require_auth: () => true,
|
||||
|
||||
test_string: () => 'test_string',
|
||||
test_number: () => 123,
|
||||
test_boolean: () => true,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -19,19 +19,21 @@ export class JwtConfigService implements JwtOptionsFactory {
|
|||
}
|
||||
|
||||
public async getJwtSecret(): Promise<string> {
|
||||
const secret = await this.prefService.getPreference('jwt_secret');
|
||||
const secret = await this.prefService.getStringPreference('jwt_secret');
|
||||
if (HasFailed(secret)) {
|
||||
throw new Error('JWT secret could not be retrieved');
|
||||
}
|
||||
return secret.value;
|
||||
return secret;
|
||||
}
|
||||
|
||||
public async getJwtExpiresIn(): Promise<string> {
|
||||
const expiresIn = await this.prefService.getPreference('jwt_expires_in');
|
||||
const expiresIn = await this.prefService.getStringPreference(
|
||||
'jwt_expires_in',
|
||||
);
|
||||
if (HasFailed(expiresIn)) {
|
||||
throw new Error('JWT expiresIn could not be retrieved');
|
||||
}
|
||||
return expiresIn.value;
|
||||
return expiresIn;
|
||||
}
|
||||
|
||||
public async createJwtOptions(): Promise<JwtModuleOptions> {
|
||||
|
|
|
@ -26,14 +26,17 @@ export class PrefController {
|
|||
|
||||
@Get('sys/:key')
|
||||
async getSysPref(@Param('key') key: string): Promise<SysPreferenceResponse> {
|
||||
const returned = await this.prefService.getPreference(
|
||||
key as SysPreferences,
|
||||
);
|
||||
if (HasFailed(returned)) {
|
||||
this.logger.warn(returned.getReason());
|
||||
const pref = await this.prefService.getPreference(key as SysPreferences);
|
||||
if (HasFailed(pref)) {
|
||||
this.logger.warn(pref.getReason());
|
||||
throw new InternalServerErrorException('Could not get preference');
|
||||
}
|
||||
|
||||
const returned = new SysPreferenceResponse();
|
||||
returned.key = key as SysPreferences;
|
||||
returned.value = pref.value;
|
||||
returned.type = pref.type;
|
||||
|
||||
return returned;
|
||||
}
|
||||
|
||||
|
@ -43,15 +46,21 @@ export class PrefController {
|
|||
@Body() body: UpdateSysPreferenceRequest,
|
||||
): Promise<SysPreferenceResponse> {
|
||||
const value = body.value;
|
||||
const returned = await this.prefService.setPreference(
|
||||
|
||||
const pref = await this.prefService.setPreference(
|
||||
key as SysPreferences,
|
||||
value,
|
||||
);
|
||||
if (HasFailed(returned)) {
|
||||
this.logger.warn(returned.getReason());
|
||||
if (HasFailed(pref)) {
|
||||
this.logger.warn(pref.getReason());
|
||||
throw new InternalServerErrorException('Could not set preference');
|
||||
}
|
||||
|
||||
const returned = new SysPreferenceResponse();
|
||||
returned.key = key as SysPreferences;
|
||||
returned.value = pref.value;
|
||||
returned.type = pref.type;
|
||||
|
||||
return returned;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,27 @@
|
|||
import { IsNotEmpty } from 'class-validator';
|
||||
import { ESysPreference } from '../../entities/syspreference.entity';
|
||||
import { IsEnum, IsNotEmpty } from 'class-validator';
|
||||
import {
|
||||
IsSysPrefValue,
|
||||
SysPreferences,
|
||||
SysPrefValueType,
|
||||
SysPrefValueTypes,
|
||||
SysPrefValueTypeStrings
|
||||
} from '../syspreferences.dto';
|
||||
|
||||
export class UpdateSysPreferenceRequest {
|
||||
@IsNotEmpty()
|
||||
value: string;
|
||||
}
|
||||
|
||||
export class SysPreferenceResponse extends ESysPreference {}
|
||||
export class SysPreferenceResponse {
|
||||
@IsNotEmpty()
|
||||
@IsEnum(SysPreferences)
|
||||
key: SysPreferences;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsSysPrefValue()
|
||||
value: SysPrefValueType;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsEnum(SysPrefValueTypes)
|
||||
type: SysPrefValueTypeStrings;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,65 @@
|
|||
import {
|
||||
registerDecorator,
|
||||
ValidationArguments,
|
||||
ValidationOptions
|
||||
} from 'class-validator';
|
||||
import tuple from '../types/tuple';
|
||||
|
||||
// Syspref keys
|
||||
|
||||
const SysPreferencesTuple = tuple(
|
||||
'jwt_secret',
|
||||
'jwt_expires_in',
|
||||
'upload_require_auth',
|
||||
'test_string',
|
||||
'test_number',
|
||||
'test_boolean',
|
||||
);
|
||||
|
||||
export const SysPreferences: string[] = SysPreferencesTuple;
|
||||
export type SysPreferences = typeof SysPreferencesTuple[number];
|
||||
|
||||
// Syspref Values
|
||||
|
||||
export type SysPrefValueType = string | number | boolean;
|
||||
export type SysPrefValueTypeStrings = 'string' | 'number' | 'boolean';
|
||||
export const SysPrefValueTypes = ['string', 'number', 'boolean'];
|
||||
|
||||
export const SysPreferenceValueTypes: {
|
||||
[key in SysPreferences]: SysPrefValueTypeStrings;
|
||||
} = {
|
||||
jwt_secret: 'string',
|
||||
jwt_expires_in: 'string',
|
||||
upload_require_auth: 'boolean',
|
||||
test_string: 'string',
|
||||
test_number: 'number',
|
||||
test_boolean: 'boolean',
|
||||
};
|
||||
|
||||
// Validators
|
||||
|
||||
export function isSysPrefValue(value: any, args: ValidationArguments) {
|
||||
const type = typeof value;
|
||||
return SysPrefValueTypes.includes(type);
|
||||
}
|
||||
|
||||
export function IsSysPrefValue(validationOptions?: ValidationOptions) {
|
||||
return function (object: Object, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: 'isSysPrefValue',
|
||||
target: object.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate: isSysPrefValue,
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// interfaces
|
||||
|
||||
export interface InternalSysprefRepresentation {
|
||||
value: SysPrefValueType;
|
||||
type: SysPrefValueTypeStrings;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue