add typed preferences

This commit is contained in:
rubikscraft 2022-03-19 15:42:25 +01:00
parent c26e1aef38
commit 721b5c05fe
No known key found for this signature in database
GPG key ID: 1463EBE9200A5CD4
6 changed files with 236 additions and 34 deletions

View file

@ -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');
}
}

View file

@ -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,
};
}

View file

@ -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> {

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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;
}