Skip to content

Commit 849955d

Browse files
fix(compass-preference-model): account for all the preferences for the implementation of ReadOnlyPreferenceService (#5356)
* fix: fixed the simple preference service implementation * fix: use ref for simple service * chore: moved preference schema definitions to a different file to avoid compass specific imports in provider * chore: review fixup * chore: further fixes to imports * chore: fix lost type assertion * chore: fix the import for our service * chore: fix testcases by incorporating the derived values in preference defaults * chore: further adaptation to read only preference service * chore: provide an implementation for CreateSandbox * chore: further refactor, remove getDefaultPreferences duplication * chore: decouple preferences and storage and use them separately * chore: import z from zod * chore: split preferences and schema definitions
1 parent a2acdd8 commit 849955d

File tree

10 files changed

+290
-199
lines changed

10 files changed

+290
-199
lines changed

packages/compass-preferences-model/src/index.ts

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import type {
88
PreferenceStateInformation,
99
AllPreferences,
1010
} from './preferences-schema';
11-
import type { Preferences } from './preferences';
11+
import type { Preferences, PreferencesAccess } from './preferences';
12+
1213
export type {
1314
UserPreferences,
1415
UserConfigurablePreferences,
@@ -33,22 +34,7 @@ export {
3334
isAIFeatureEnabled,
3435
} from './utils';
3536
export type { User, UserStorage } from './storage';
36-
37-
export interface PreferencesAccess {
38-
savePreferences(
39-
attributes: Partial<UserPreferences>
40-
): Promise<AllPreferences>;
41-
refreshPreferences(): Promise<AllPreferences>;
42-
getPreferences(): AllPreferences;
43-
ensureDefaultConfigurableUserPreferences(): Promise<void>;
44-
getConfigurableUserPreferences(): Promise<UserConfigurablePreferences>;
45-
getPreferenceStates(): Promise<PreferenceStateInformation>;
46-
onPreferenceValueChanged<K extends keyof AllPreferences>(
47-
preferenceName: K,
48-
callback: (value: AllPreferences[K]) => void
49-
): () => void;
50-
createSandbox(): Promise<PreferencesAccess>;
51-
}
37+
export type { PreferencesAccess };
5238
export { setupPreferences };
5339
export const defaultPreferencesInstance: PreferencesAccess =
5440
preferencesIpc ?? preferencesMain;

packages/compass-preferences-model/src/preferences-schema.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,12 @@ export type PreferenceStateInformation = Partial<
172172
Record<keyof AllPreferences, PreferenceState>
173173
>;
174174

175+
export type StoredPreferencesValidator = ReturnType<
176+
typeof getPreferencesValidator
177+
>;
178+
179+
export type StoredPreferences = z.output<StoredPreferencesValidator>;
180+
175181
// Preference definitions
176182
const featureFlagsProps: Required<{
177183
[K in keyof FeatureFlags]: PreferenceDefinition<K>;
@@ -862,6 +868,76 @@ function featureFlagToPreferenceDefinition(
862868
};
863869
}
864870

871+
export function makeComputePreferencesValuesAndStates(
872+
values: AllPreferences,
873+
states: Partial<Record<string, PreferenceState>> = {}
874+
) {
875+
return function _computePreferenceValuesAndStates() {
876+
const originalValues = { ...values };
877+
const originalStates = { ...states };
878+
879+
function deriveValue<K extends keyof AllPreferences>(
880+
key: K
881+
): {
882+
value: AllPreferences[K];
883+
state: PreferenceState;
884+
} {
885+
const descriptor = allPreferencesProps[key];
886+
if (!descriptor.deriveValue) {
887+
return { value: originalValues[key], state: originalStates[key] };
888+
}
889+
return (descriptor.deriveValue as DeriveValueFunction<AllPreferences[K]>)(
890+
// `as unknown` to work around TS bug(?) https://twitter.com/addaleax/status/1572191664252551169
891+
(k) =>
892+
(k as unknown) === key ? originalValues[k] : deriveValue(k).value,
893+
(k) =>
894+
(k as unknown) === key ? originalStates[k] : deriveValue(k).state
895+
);
896+
}
897+
898+
for (const key of Object.keys(allPreferencesProps)) {
899+
// awkward IIFE to make typescript understand that `key` is the *same* key
900+
// in each loop iteration
901+
(<K extends keyof AllPreferences>(key: K) => {
902+
const result = deriveValue(key);
903+
values[key] = result.value;
904+
if (result.state !== undefined) states[key] = result.state;
905+
})(key as keyof AllPreferences);
906+
}
907+
908+
return { values, states };
909+
};
910+
}
911+
912+
export function getPreferencesValidator() {
913+
const preferencesPropsValidator = Object.fromEntries(
914+
Object.entries(storedUserPreferencesProps).map(([key, { validator }]) => [
915+
key,
916+
validator,
917+
])
918+
) as {
919+
[K in keyof typeof storedUserPreferencesProps]: typeof storedUserPreferencesProps[K]['validator'];
920+
};
921+
922+
return z.object(preferencesPropsValidator);
923+
}
924+
925+
export function getDefaultsForStoredPreferences(): StoredPreferences {
926+
return Object.fromEntries(
927+
Object.entries(storedUserPreferencesProps)
928+
.map(([key, value]) => [key, value.validator.parse(undefined)])
929+
.filter(([, value]) => value !== undefined)
930+
);
931+
}
932+
933+
export function getInitialValuesForStoredPreferences() {
934+
const computeValuesAndStates = makeComputePreferencesValuesAndStates(
935+
getDefaultsForStoredPreferences()
936+
);
937+
938+
return computeValuesAndStates().values;
939+
}
940+
865941
export function getSettingDescription<
866942
Name extends Exclude<keyof AllPreferences, keyof InternalUserPreferences>
867943
>(

packages/compass-preferences-model/src/preferences.spec.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import os from 'os';
44
import { Preferences } from './preferences';
55
import { expect } from 'chai';
66
import { featureFlags } from './feature-flags';
7+
import { PersistentStorage } from './storage';
8+
import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging';
79

810
const releasedFeatureFlags = Object.entries(featureFlags)
911
.filter(([, v]) => v.stage === 'released')
@@ -13,10 +15,20 @@ const expectedReleasedFeatureFlagsStates = Object.fromEntries(
1315
releasedFeatureFlags.map((ff) => [ff, 'hardcoded'])
1416
);
1517

18+
const logger = createLoggerAndTelemetry('COMPASS-PREFERENCES');
19+
1620
const setupPreferences = async (
17-
...args: ConstructorParameters<typeof Preferences>
21+
basePath: string,
22+
globalPreferences?: ConstructorParameters<
23+
typeof Preferences
24+
>[0]['globalPreferences']
1825
) => {
19-
const preferences = new Preferences(...args);
26+
const preferencesStorage = new PersistentStorage(basePath);
27+
const preferences = new Preferences({
28+
preferencesStorage,
29+
globalPreferences,
30+
logger,
31+
});
2032
await preferences.setupStorage();
2133
return preferences;
2234
};
@@ -270,7 +282,8 @@ describe('Preferences class', function () {
270282
});
271283
await mainPreferences.savePreferences({ trackUsageStatistics: true });
272284
const sandbox = await Preferences.CreateSandbox(
273-
await mainPreferences.getPreferenceSandboxProperties()
285+
await mainPreferences.getPreferenceSandboxProperties(),
286+
logger
274287
);
275288

276289
await sandbox.savePreferences({ readOnly: true });

packages/compass-preferences-model/src/preferences.ts

Lines changed: 55 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,35 @@
1-
import type { ParsedGlobalPreferencesResult } from './global-config';
2-
import {
3-
SandboxPreferences,
4-
StoragePreferences,
5-
type BasePreferencesStorage,
6-
} from './storage';
7-
import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging';
81
import { z } from 'zod';
2+
import { type LoggerAndTelemetry } from '@mongodb-js/compass-logging';
3+
4+
import type { ParsedGlobalPreferencesResult } from './global-config';
5+
import type {
6+
AllPreferences,
7+
PreferenceState,
8+
PreferenceStateInformation,
9+
UserConfigurablePreferences,
10+
UserPreferences,
11+
} from './preferences-schema';
912
import {
10-
type AllPreferences,
11-
type PreferenceState,
12-
type UserPreferences,
13-
type DeriveValueFunction,
14-
type UserConfigurablePreferences,
15-
type PreferenceStateInformation,
1613
allPreferencesProps,
14+
makeComputePreferencesValuesAndStates,
1715
} from './preferences-schema';
16+
import { InMemoryStorage, type BasePreferencesStorage } from './storage';
1817

19-
const { log, mongoLogId } = createLoggerAndTelemetry('COMPASS-PREFERENCES');
18+
export interface PreferencesAccess {
19+
savePreferences(
20+
attributes: Partial<UserPreferences>
21+
): Promise<AllPreferences>;
22+
refreshPreferences(): Promise<AllPreferences>;
23+
getPreferences(): AllPreferences;
24+
ensureDefaultConfigurableUserPreferences(): Promise<void>;
25+
getConfigurableUserPreferences(): Promise<UserConfigurablePreferences>;
26+
getPreferenceStates(): Promise<PreferenceStateInformation>;
27+
onPreferenceValueChanged<K extends keyof AllPreferences>(
28+
preferenceName: K,
29+
callback: (value: AllPreferences[K]) => void
30+
): () => void;
31+
createSandbox(): Promise<PreferencesAccess>;
32+
}
2033

2134
type OnPreferencesChangedCallback = (
2235
changedPreferencesValues: Partial<AllPreferences>
@@ -30,6 +43,7 @@ type PreferenceSandboxPropertiesImpl = {
3043
};
3144

3245
export class Preferences {
46+
private _logger: LoggerAndTelemetry;
3347
private _onPreferencesChangedCallbacks: OnPreferencesChangedCallback[];
3448
private _preferencesStorage: BasePreferencesStorage;
3549
private _globalPreferences: {
@@ -38,14 +52,17 @@ export class Preferences {
3852
hardcoded: Partial<AllPreferences>;
3953
};
4054

41-
constructor(
42-
basepath?: string,
43-
globalPreferences?: Partial<ParsedGlobalPreferencesResult>,
44-
isSandbox?: boolean
45-
) {
46-
this._preferencesStorage = isSandbox
47-
? new SandboxPreferences()
48-
: new StoragePreferences(basepath);
55+
constructor({
56+
logger,
57+
globalPreferences,
58+
preferencesStorage = new InMemoryStorage(),
59+
}: {
60+
logger: LoggerAndTelemetry;
61+
preferencesStorage: BasePreferencesStorage;
62+
globalPreferences?: Partial<ParsedGlobalPreferencesResult>;
63+
}) {
64+
this._logger = logger;
65+
this._preferencesStorage = preferencesStorage;
4966

5067
this._onPreferencesChangedCallbacks = [];
5168
this._globalPreferences = {
@@ -56,8 +73,8 @@ export class Preferences {
5673
};
5774

5875
if (Object.keys(this._globalPreferences.hardcoded).length > 0) {
59-
log.info(
60-
mongoLogId(1_001_000_159),
76+
this._logger.log.info(
77+
this._logger.mongoLogId(1_001_000_159),
6178
'preferences',
6279
'Created Preferences object with hardcoded options',
6380
{ options: this._globalPreferences.hardcoded }
@@ -80,12 +97,17 @@ export class Preferences {
8097

8198
// Create a
8299
static async CreateSandbox(
83-
props: PreferenceSandboxProperties | undefined
100+
props: PreferenceSandboxProperties | undefined,
101+
logger: LoggerAndTelemetry
84102
): Promise<Preferences> {
85103
const { user, global } = props
86104
? (JSON.parse(props) as PreferenceSandboxPropertiesImpl)
87105
: { user: {}, global: {} };
88-
const instance = new Preferences(undefined, global, true);
106+
const instance = new Preferences({
107+
logger,
108+
globalPreferences: global,
109+
preferencesStorage: new InMemoryStorage(),
110+
});
89111
await instance.savePreferences(user);
90112
return instance;
91113
}
@@ -114,8 +136,8 @@ export class Preferences {
114136
try {
115137
await this._preferencesStorage.updatePreferences(attributes);
116138
} catch (err) {
117-
log.error(
118-
mongoLogId(1_001_000_157),
139+
this._logger.log.error(
140+
this._logger.mongoLogId(1_001_000_157),
119141
'preferences',
120142
'Failed to save preferences, error while saving models',
121143
{
@@ -185,39 +207,12 @@ export class Preferences {
185207
for (const key of Object.keys(this._globalPreferences.hardcoded))
186208
states[key] = 'hardcoded';
187209

188-
const originalValues = { ...values };
189-
const originalStates = { ...states };
190-
191-
function deriveValue<K extends keyof AllPreferences>(
192-
key: K
193-
): {
194-
value: AllPreferences[K];
195-
state: PreferenceState;
196-
} {
197-
const descriptor = allPreferencesProps[key];
198-
if (!descriptor.deriveValue) {
199-
return { value: originalValues[key], state: originalStates[key] };
200-
}
201-
return (descriptor.deriveValue as DeriveValueFunction<AllPreferences[K]>)(
202-
// `as unknown` to work around TS bug(?) https://twitter.com/addaleax/status/1572191664252551169
203-
(k) =>
204-
(k as unknown) === key ? originalValues[k] : deriveValue(k).value,
205-
(k) =>
206-
(k as unknown) === key ? originalStates[k] : deriveValue(k).state
207-
);
208-
}
209-
210-
for (const key of Object.keys(allPreferencesProps)) {
211-
// awkward IIFE to make typescript understand that `key` is the *same* key
212-
// in each loop iteration
213-
(<K extends keyof AllPreferences>(key: K) => {
214-
const result = deriveValue(key);
215-
values[key] = result.value;
216-
if (result.state !== undefined) states[key] = result.state;
217-
})(key as keyof AllPreferences);
218-
}
210+
const computeValuesAndStates = makeComputePreferencesValuesAndStates(
211+
values,
212+
states
213+
);
219214

220-
return { values, states };
215+
return computeValuesAndStates();
221216
}
222217

223218
/**

0 commit comments

Comments
 (0)