From 2bbe79809731a103c58389769f3d3cb62def6fdd Mon Sep 17 00:00:00 2001 From: rubikscraft Date: Wed, 13 Apr 2022 16:33:07 +0200 Subject: [PATCH] add userprefrerencedb --- .../preferencesdb/preferencecommon.service.ts | 111 ++++++++++ .../preferencesdb/preferencedb.module.ts | 7 +- .../preferencesdb/syspreferencedb.service.ts | 105 +++------- .../preferencesdb/usrpreferencedb.service.ts | 192 ++++++++---------- .../models/entities/syspreference.entity.ts | 10 +- .../models/entities/usrpreference.entity.ts | 18 +- shared/src/dto/preferences.dto.ts | 18 +- shared/src/entities/syspreference.entity.ts | 9 - shared/src/entities/usrpreference.ts | 11 - 9 files changed, 262 insertions(+), 219 deletions(-) create mode 100644 backend/src/collections/preferencesdb/preferencecommon.service.ts delete mode 100644 shared/src/entities/syspreference.entity.ts delete mode 100644 shared/src/entities/usrpreference.ts diff --git a/backend/src/collections/preferencesdb/preferencecommon.service.ts b/backend/src/collections/preferencesdb/preferencecommon.service.ts new file mode 100644 index 0000000..fc93940 --- /dev/null +++ b/backend/src/collections/preferencesdb/preferencecommon.service.ts @@ -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; +type EnumValue = E[keyof E]; +type PrefValueTypeType = { + [key in EnumValue]: PrefValueTypeStrings; +}; +type KeyValuePref = { + key: string; + value: string; +}; + +@Injectable() +export class PreferenceCommonService { + private readonly logger = new Logger('PreferenceCommonService'); + + public validateAndUnpackPref( + preference: KeyValuePref, + prefType: E, + prefValueTypes: PrefValueTypeType, + ): Failable { + 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( + key: string, + value: PrefValueType, + prefType: E, + prefValueTypes: PrefValueTypeType, + ): AsyncFailable { + 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>( + key: string, + prefType: E, + ): Failable { + 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 { + 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'); + } +} diff --git a/backend/src/collections/preferencesdb/preferencedb.module.ts b/backend/src/collections/preferencesdb/preferencedb.module.ts index 2c0029b..4329811 100644 --- a/backend/src/collections/preferencesdb/preferencedb.module.ts +++ b/backend/src/collections/preferencesdb/preferencedb.module.ts @@ -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 {} diff --git a/backend/src/collections/preferencesdb/syspreferencedb.service.ts b/backend/src/collections/preferencesdb/syspreferencedb.service.ts index 8d31a77..15e65bc 100644 --- a/backend/src/collections/preferencesdb/syspreferencedb.service.ts +++ b/backend/src/collections/preferencesdb/syspreferencedb.service.ts @@ -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, private defaultsService: PreferenceDefaultsService, + private prefCommon: PreferenceCommonService, ) {} public async setPreference( @@ -36,7 +32,7 @@ export class SysPreferenceService { value: PrefValueType, ): AsyncFailable { // 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 { // 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 { @@ -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 { - 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 { - 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 { - 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 { - 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'); - } } diff --git a/backend/src/collections/preferencesdb/usrpreferencedb.service.ts b/backend/src/collections/preferencesdb/usrpreferencedb.service.ts index 14a5e98..bcbdcca 100644 --- a/backend/src/collections/preferencesdb/usrpreferencedb.service.ts +++ b/backend/src/collections/preferencesdb/usrpreferencedb.service.ts @@ -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, + private usrPreferenceRepository: Repository, private defaultsService: PreferenceDefaultsService, + private prefCommon: PreferenceCommonService, ) {} public async setPreference( + userid: string, key: string, value: PrefValueType, ): AsyncFailable { // 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 { + public async getPreference( + userid: string, + key: string, + ): AsyncFailable { // 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 { - return this.getPreferencePinned(key, 'string') as AsyncFailable; + public async getStringPreference( + userid: string, + key: string, + ): AsyncFailable { + return this.getPreferencePinned( + userid, + key, + 'string', + ) as AsyncFailable; } - public async getNumberPreference(key: string): AsyncFailable { - return this.getPreferencePinned(key, 'number') as AsyncFailable; + public async getNumberPreference( + userid: string, + key: string, + ): AsyncFailable { + return this.getPreferencePinned( + userid, + key, + 'number', + ) as AsyncFailable; } - public async getBooleanPreference(key: string): AsyncFailable { - return this.getPreferencePinned(key, 'boolean') as AsyncFailable; + public async getBooleanPreference( + userid: string, + key: string, + ): AsyncFailable { + return this.getPreferencePinned( + userid, + key, + 'boolean', + ) as AsyncFailable; } private async getPreferencePinned( + userid: string, key: string, type: PrefValueTypeStrings, ): AsyncFailable { - 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 { + public async getAllPreferences( + userid: string, + ): AsyncFailable { // 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 { - 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 { - 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 { - 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 { - 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 { - 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'); - } } diff --git a/backend/src/models/entities/syspreference.entity.ts b/backend/src/models/entities/syspreference.entity.ts index 411483c..9f72f22 100644 --- a/backend/src/models/entities/syspreference.entity.ts +++ b/backend/src/models/entities/syspreference.entity.ts @@ -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; @Entity() export class ESysPreferenceBackend implements ESysPreference { diff --git a/backend/src/models/entities/usrpreference.entity.ts b/backend/src/models/entities/usrpreference.entity.ts index b60a145..17149c6 100644 --- a/backend/src/models/entities/usrpreference.entity.ts +++ b/backend/src/models/entities/usrpreference.entity.ts @@ -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; + +@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; } diff --git a/shared/src/dto/preferences.dto.ts b/shared/src/dto/preferences.dto.ts index 555f010..d9e7ae5 100644 --- a/shared/src/dto/preferences.dto.ts +++ b/shared/src/dto/preferences.dto.ts @@ -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; + +// Usr and Sys + +export const DecodedSysPrefSchema = DecodedPrefSchema; export type DecodedSysPref = z.infer; -export const DecodedUsrPrefSchema = DecodedSysPrefSchema.merge(z.object({ - user: IsEntityID(), -})) +export const DecodedUsrPrefSchema = DecodedSysPrefSchema.merge( + z.object({ + user: IsEntityID(), + }), +); export type DecodedUsrPref = z.infer; - diff --git a/shared/src/entities/syspreference.entity.ts b/shared/src/entities/syspreference.entity.ts deleted file mode 100644 index 1d09f06..0000000 --- a/shared/src/entities/syspreference.entity.ts +++ /dev/null @@ -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; diff --git a/shared/src/entities/usrpreference.ts b/shared/src/entities/usrpreference.ts deleted file mode 100644 index edee6eb..0000000 --- a/shared/src/entities/usrpreference.ts +++ /dev/null @@ -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;