add userprefrerencedb

This commit is contained in:
rubikscraft 2022-04-13 16:33:07 +02:00
parent 9469090614
commit 2bbe798097
No known key found for this signature in database
GPG key ID: 1463EBE9200A5CD4
9 changed files with 262 additions and 219 deletions

View file

@ -0,0 +1,111 @@
import { Injectable, Logger } from '@nestjs/common';
import {
DecodedPref, PrefValueType,
PrefValueTypeStrings
} from 'picsur-shared/dist/dto/preferences.dto';
import {
AsyncFailable,
Fail,
Failable,
HasFailed
} from 'picsur-shared/dist/types';
type Enum = Record<string, string>;
type EnumValue<E> = E[keyof E];
type PrefValueTypeType<E extends Enum> = {
[key in EnumValue<E>]: PrefValueTypeStrings;
};
type KeyValuePref = {
key: string;
value: string;
};
@Injectable()
export class PreferenceCommonService {
private readonly logger = new Logger('PreferenceCommonService');
public validateAndUnpackPref<E extends Enum>(
preference: KeyValuePref,
prefType: E,
prefValueTypes: PrefValueTypeType<E>,
): Failable<DecodedPref> {
const key = this.validatePrefKey(preference.key, prefType);
if (HasFailed(key)) return key;
const type = prefValueTypes[key];
switch (type) {
case 'string':
return {
key: preference.key,
value: preference.value,
type: 'string',
};
case 'number':
return {
key: preference.key,
value: parseInt(preference.value, 10),
type: 'number',
};
case 'boolean':
return {
key: preference.key,
value: preference.value == 'true',
type: 'boolean',
};
}
return Fail('Invalid preference value');
}
public async validatePref<E extends Enum>(
key: string,
value: PrefValueType,
prefType: E,
prefValueTypes: PrefValueTypeType<E>,
): AsyncFailable<KeyValuePref> {
const validatedKey = this.validatePrefKey(key, prefType);
if (HasFailed(validatedKey)) return validatedKey;
const valueType = prefValueTypes[validatedKey];
const validatedValue = this.validateAndPackPrefValue(value, valueType);
if (HasFailed(validatedValue)) return validatedValue;
return {
key: validatedKey,
value: validatedValue,
};
}
public validatePrefKey<E extends Enum, V extends EnumValue<E>>(
key: string,
prefType: E,
): Failable<V> {
const keysList = Object.values(prefType);
if (!keysList.includes(key)) {
return Fail('Invalid preference key');
}
return key as V;
}
public validateAndPackPrefValue(
value: PrefValueType,
expectedType: PrefValueTypeStrings,
): Failable<string> {
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

@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { EarlyConfigModule } from '../../config/early/earlyconfig.module';
import { ESysPreferenceBackend } from '../../models/entities/syspreference.entity';
import { PreferenceCommonService } from './preferencecommon.service';
import { PreferenceDefaultsService } from './preferencedefaults.service';
import { SysPreferenceService } from './syspreferencedb.service';
@ -10,7 +11,11 @@ import { SysPreferenceService } from './syspreferencedb.service';
TypeOrmModule.forFeature([ESysPreferenceBackend]),
EarlyConfigModule,
],
providers: [SysPreferenceService, PreferenceDefaultsService],
providers: [
SysPreferenceService,
PreferenceDefaultsService,
PreferenceCommonService,
],
exports: [SysPreferenceService],
})
export class SysPreferenceModule {}

View file

@ -6,19 +6,14 @@ import {
PrefValueTypeStrings
} from 'picsur-shared/dist/dto/preferences.dto';
import { SysPreference } from 'picsur-shared/dist/dto/syspreferences.dto';
import { ESysPreferenceSchema } from 'picsur-shared/dist/entities/syspreference.entity';
import {
AsyncFailable,
Fail,
Failable,
HasFailed
} from 'picsur-shared/dist/types';
import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types';
import { Repository } from 'typeorm';
import {
SysPreferenceList,
SysPreferenceValueTypes
} from '../../models/dto/syspreferences.dto';
import { ESysPreferenceBackend } from '../../models/entities/syspreference.entity';
import { ESysPreferenceBackend, ESysPreferenceSchema } from '../../models/entities/syspreference.entity';
import { PreferenceCommonService } from './preferencecommon.service';
import { PreferenceDefaultsService } from './preferencedefaults.service';
@Injectable()
@ -29,6 +24,7 @@ export class SysPreferenceService {
@InjectRepository(ESysPreferenceBackend)
private sysPreferenceRepository: Repository<ESysPreferenceBackend>,
private defaultsService: PreferenceDefaultsService,
private prefCommon: PreferenceCommonService,
) {}
public async setPreference(
@ -36,7 +32,7 @@ export class SysPreferenceService {
value: PrefValueType,
): AsyncFailable<DecodedSysPref> {
// Validate
let sysPreference = await this.validatePref(key, value);
let sysPreference = await this.validateSysPref(key, value);
if (HasFailed(sysPreference)) return sysPreference;
// Set
@ -61,7 +57,10 @@ export class SysPreferenceService {
public async getPreference(key: string): AsyncFailable<DecodedSysPref> {
// Validate
let validatedKey = this.validatePrefKey(key);
let validatedKey = this.prefCommon.validatePrefKey(
key,
SysPreference,
);
if (HasFailed(validatedKey)) return validatedKey;
// Fetch
@ -89,7 +88,11 @@ export class SysPreferenceService {
}
// Return
return this.retrieveConvertedValue(result.data);
return this.prefCommon.validateAndUnpackPref(
result.data,
SysPreference,
SysPreferenceValueTypes,
);
}
public async getStringPreference(key: string): AsyncFailable<string> {
@ -135,51 +138,21 @@ export class SysPreferenceService {
return this.setPreference(key, this.defaultsService.sysDefaults[key]());
}
// This converts the raw string representation of the value to the correct type
private retrieveConvertedValue(
preference: ESysPreferenceBackend,
): Failable<DecodedSysPref> {
const key = this.validatePrefKey(preference.key);
if (HasFailed(key)) return key;
const type = SysPreferenceValueTypes[key];
switch (type) {
case 'string':
return {
key: preference.key,
value: preference.value,
type: 'string',
};
case 'number':
return {
key: preference.key,
value: parseInt(preference.value, 10),
type: 'number',
};
case 'boolean':
return {
key: preference.key,
value: preference.value == 'true',
type: 'boolean',
};
}
return Fail('Invalid preference value');
}
private async validatePref(
private async validateSysPref(
key: string,
value: PrefValueType,
): AsyncFailable<ESysPreferenceBackend> {
const validatedKey = this.validatePrefKey(key);
if (HasFailed(validatedKey)) return validatedKey;
const validatedValue = this.validatePrefValue(validatedKey, value);
if (HasFailed(validatedValue)) return validatedValue;
const validated = await this.prefCommon.validatePref(
key,
value,
SysPreference,
SysPreferenceValueTypes,
);
if (HasFailed(validated)) return validated;
let verifySysPreference = new ESysPreferenceBackend();
verifySysPreference.key = validatedKey;
verifySysPreference.value = validatedValue;
verifySysPreference.key = validated.key;
verifySysPreference.value = validated.value;
// It should already be valid, but these two validators might go out of sync
const result = ESysPreferenceSchema.safeParse(verifySysPreference);
@ -190,34 +163,4 @@ export class SysPreferenceService {
return result.data;
}
private validatePrefKey(key: string): Failable<SysPreference> {
if (!SysPreferenceList.includes(key)) return Fail('Invalid preference key');
return key as SysPreference;
}
private validatePrefValue(
// Key is required, because the type of the value depends on the key
key: SysPreference,
value: PrefValueType,
): 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

@ -6,19 +6,17 @@ import {
PrefValueTypeStrings
} from 'picsur-shared/dist/dto/preferences.dto';
import { UsrPreference } from 'picsur-shared/dist/dto/usrpreferences.dto';
import { EUsrPreferenceSchema } from 'picsur-shared/dist/entities/usrpreference';
import {
AsyncFailable,
Fail,
Failable,
HasFailed
} from 'picsur-shared/dist/types';
import { AsyncFailable, Fail, HasFailed } from 'picsur-shared/dist/types';
import { Repository } from 'typeorm';
import {
UsrPreferenceList,
UsrPreferenceValueTypes
} from '../../models/dto/usrpreferences.dto';
import { EUsrPreferenceBackend } from '../../models/entities/usrpreference.entity';
import {
EUsrPreferenceBackend,
EUsrPreferenceSchema
} from '../../models/entities/usrpreference.entity';
import { PreferenceCommonService } from './preferencecommon.service';
import { PreferenceDefaultsService } from './preferencedefaults.service';
@Injectable()
@ -27,23 +25,25 @@ export class UsrPreferenceService {
constructor(
@InjectRepository(EUsrPreferenceBackend)
private sysPreferenceRepository: Repository<EUsrPreferenceBackend>,
private usrPreferenceRepository: Repository<EUsrPreferenceBackend>,
private defaultsService: PreferenceDefaultsService,
private prefCommon: PreferenceCommonService,
) {}
public async setPreference(
userid: string,
key: string,
value: PrefValueType,
): AsyncFailable<DecodedUsrPref> {
// Validate
let sysPreference = await this.validatePref(key, value);
if (HasFailed(sysPreference)) return sysPreference;
let usrPreference = await this.validatePref(userid, key, value);
if (HasFailed(usrPreference)) return usrPreference;
// Set
try {
// Upsert here, because we want to create a new record if it does not exist
await this.sysPreferenceRepository.upsert(sysPreference, {
conflictPaths: ['key'],
await this.usrPreferenceRepository.upsert(usrPreference, {
conflictPaths: ['key', 'user'],
});
} catch (e: any) {
this.logger.warn(e);
@ -52,24 +52,27 @@ export class UsrPreferenceService {
// Return
return {
key: sysPreference.key,
key: usrPreference.key,
value,
// key has to be valid here, we validated it
type: UsrPreferenceValueTypes[key as UsrPreference],
user: '',
user: userid,
};
}
public async getPreference(key: string): AsyncFailable<DecodedUsrPref> {
public async getPreference(
userid: string,
key: string,
): AsyncFailable<DecodedUsrPref> {
// Validate
let validatedKey = this.validatePrefKey(key);
let validatedKey = this.prefCommon.validatePrefKey(key, UsrPreference);
if (HasFailed(validatedKey)) return validatedKey;
// Fetch
let foundSysPreference: EUsrPreferenceBackend | null;
let foundUsrPreference: EUsrPreferenceBackend | null;
try {
foundSysPreference = await this.sysPreferenceRepository.findOne({
where: { key: validatedKey },
foundUsrPreference = await this.usrPreferenceRepository.findOne({
where: { key: validatedKey, userId: userid },
cache: 60000,
});
} catch (e: any) {
@ -78,48 +81,81 @@ export class UsrPreferenceService {
}
// Fallback
if (!foundSysPreference) {
return this.saveDefault(validatedKey);
if (!foundUsrPreference) {
return this.saveDefault(userid, validatedKey);
}
// Validate
const result = EUsrPreferenceSchema.safeParse(foundSysPreference);
const result = EUsrPreferenceSchema.safeParse(foundUsrPreference);
if (!result.success) {
this.logger.warn(result.error);
return Fail('Invalid preference');
}
// Return
return this.retrieveConvertedValue(result.data);
const unpacked = this.prefCommon.validateAndUnpackPref(
result.data,
UsrPreference,
UsrPreferenceValueTypes,
);
if (HasFailed(unpacked)) return unpacked;
return {
...unpacked,
user: result.data.userId,
};
}
public async getStringPreference(key: string): AsyncFailable<string> {
return this.getPreferencePinned(key, 'string') as AsyncFailable<string>;
public async getStringPreference(
userid: string,
key: string,
): AsyncFailable<string> {
return this.getPreferencePinned(
userid,
key,
'string',
) as AsyncFailable<string>;
}
public async getNumberPreference(key: string): AsyncFailable<number> {
return this.getPreferencePinned(key, 'number') as AsyncFailable<number>;
public async getNumberPreference(
userid: string,
key: string,
): AsyncFailable<number> {
return this.getPreferencePinned(
userid,
key,
'number',
) as AsyncFailable<number>;
}
public async getBooleanPreference(key: string): AsyncFailable<boolean> {
return this.getPreferencePinned(key, 'boolean') as AsyncFailable<boolean>;
public async getBooleanPreference(
userid: string,
key: string,
): AsyncFailable<boolean> {
return this.getPreferencePinned(
userid,
key,
'boolean',
) as AsyncFailable<boolean>;
}
private async getPreferencePinned(
userid: string,
key: string,
type: PrefValueTypeStrings,
): AsyncFailable<PrefValueType> {
let pref = await this.getPreference(key);
let pref = await this.getPreference(userid, key);
if (HasFailed(pref)) return pref;
if (pref.type !== type) return Fail('Invalid preference type');
return pref.value;
}
public async getAllPreferences(): AsyncFailable<DecodedUsrPref[]> {
public async getAllPreferences(
userid: string,
): AsyncFailable<DecodedUsrPref[]> {
// TODO: We are fetching each value invidually, we should fetch all at once
let internalSysPrefs = await Promise.all(
UsrPreferenceList.map((key) => this.getPreference(key)),
UsrPreferenceList.map((key) => this.getPreference(userid, key)),
);
if (internalSysPrefs.some((pref) => HasFailed(pref))) {
return Fail('Could not get all preferences');
@ -131,59 +167,33 @@ export class UsrPreferenceService {
// Private
private async saveDefault(
userid: string,
key: UsrPreference, // Force enum here because we dont validate
): AsyncFailable<DecodedUsrPref> {
return this.setPreference(key, this.defaultsService.sysDefaults[key]());
}
// This converts the raw string representation of the value to the correct type
private retrieveConvertedValue(
preference: EUsrPreferenceBackend,
): Failable<DecodedUsrPref> {
const key = this.validatePrefKey(preference.key);
if (HasFailed(key)) return key;
const type = UsrPreferenceValueTypes[key];
switch (type) {
case 'string':
return {
key: preference.key,
value: preference.value,
type: 'string',
user: '',
};
case 'number':
return {
key: preference.key,
value: parseInt(preference.value, 10),
type: 'number',
user: '',
};
case 'boolean':
return {
key: preference.key,
value: preference.value == 'true',
type: 'boolean',
user: '',
};
}
return Fail('Invalid preference value');
return this.setPreference(
userid,
key,
this.defaultsService.sysDefaults[key](),
);
}
private async validatePref(
userid: string,
key: string,
value: PrefValueType,
): AsyncFailable<EUsrPreferenceBackend> {
const validatedKey = this.validatePrefKey(key);
if (HasFailed(validatedKey)) return validatedKey;
const validatedValue = this.validatePrefValue(validatedKey, value);
if (HasFailed(validatedValue)) return validatedValue;
const validated = await this.prefCommon.validatePref(
key,
value,
UsrPreference,
UsrPreferenceValueTypes,
);
if (HasFailed(validated)) return validated;
let verifySysPreference = new EUsrPreferenceBackend();
verifySysPreference.key = validatedKey;
verifySysPreference.value = validatedValue;
verifySysPreference.key = validated.key;
verifySysPreference.value = validated.value;
verifySysPreference.userId = userid;
// It should already be valid, but these two validators might go out of sync
const result = EUsrPreferenceSchema.safeParse(verifySysPreference);
@ -194,34 +204,4 @@ export class UsrPreferenceService {
return result.data;
}
private validatePrefKey(key: string): Failable<UsrPreference> {
if (!UsrPreferenceList.includes(key)) return Fail('Invalid preference key');
return key as UsrPreference;
}
private validatePrefValue(
// Key is required, because the type of the value depends on the key
key: UsrPreference,
value: PrefValueType,
): Failable<string> {
const expectedType = UsrPreferenceValueTypes[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,13 @@
import { ESysPreference } from 'picsur-shared/dist/entities/syspreference.entity';
import { IsEntityID } from 'picsur-shared/dist/validators/entity-id.validator';
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm';
import z from 'zod';
export const ESysPreferenceSchema = z.object({
id: IsEntityID().optional(),
key: z.string(),
value: z.string(),
});
type ESysPreference = z.infer<typeof ESysPreferenceSchema>;
@Entity()
export class ESysPreferenceBackend implements ESysPreference {

View file

@ -1,12 +1,22 @@
import { EUsrPreference } from 'picsur-shared/dist/entities/usrpreference';
import { Column, Index, PrimaryGeneratedColumn } from 'typeorm';
import { IsEntityID } from 'picsur-shared/dist/validators/entity-id.validator';
import { Column, Index, PrimaryGeneratedColumn, Unique } from 'typeorm';
import z from 'zod';
export const EUsrPreferenceSchema = z.object({
id: IsEntityID().optional(),
key: z.string(),
value: z.string(),
userId: IsEntityID(),
});
type EUsrPreference = z.infer<typeof EUsrPreferenceSchema>;
@Unique(['key', 'userId'])
export class EUsrPreferenceBackend implements EUsrPreference {
@PrimaryGeneratedColumn('uuid')
id?: string;
@Index()
@Column({ nullable: false, unique: true })
@Column({ nullable: false })
key: string;
@Column({ nullable: false })
@ -14,5 +24,5 @@ export class EUsrPreferenceBackend implements EUsrPreference {
@Index()
@Column({ nullable: false })
userId: number;
userId: string;
}

View file

@ -10,15 +10,21 @@ export const PrefValueTypes = tuple('string', 'number', 'boolean');
// Decoded Representations
export const DecodedSysPrefSchema = z.object({
export const DecodedPrefSchema = z.object({
key: z.string(),
value: IsPrefValue(),
type: z.enum(PrefValueTypes),
})
});
export type DecodedPref = z.infer<typeof DecodedSysPrefSchema>;
// Usr and Sys
export const DecodedSysPrefSchema = DecodedPrefSchema;
export type DecodedSysPref = z.infer<typeof DecodedSysPrefSchema>;
export const DecodedUsrPrefSchema = DecodedSysPrefSchema.merge(z.object({
user: IsEntityID(),
}))
export const DecodedUsrPrefSchema = DecodedSysPrefSchema.merge(
z.object({
user: IsEntityID(),
}),
);
export type DecodedUsrPref = z.infer<typeof DecodedUsrPrefSchema>;

View file

@ -1,9 +0,0 @@
import { z } from 'zod';
import { IsEntityID } from '../validators/entity-id.validator';
export const ESysPreferenceSchema = z.object({
id: IsEntityID().optional(),
key: z.string(),
value: z.string(),
});
export type ESysPreference = z.infer<typeof ESysPreferenceSchema>;

View file

@ -1,11 +0,0 @@
import { z } from 'zod';
import { IsEntityID } from '../validators/entity-id.validator';
import { IsPosInt } from '../validators/positive-int.validator';
export const EUsrPreferenceSchema = z.object({
id: IsEntityID().optional(),
key: z.string(),
value: z.string(),
userId: IsPosInt(),
})
export type EUsrPreference = z.infer<typeof EUsrPreferenceSchema>;