Skip to content

Commit

Permalink
chore(compass-preferences-model): remove compass specific imports fro…
Browse files Browse the repository at this point in the history
…m the provider entrypoint in compass-preferences-model (#5381)

* chore: split utils into compass-utils and non-compass-utils

* chore: split in-memory-storage into its own export to avoid importing compass specific stuff from storage

* chore: untangle the react related exports

* chore: ignore dist for depcheck in databases-collection

* chore: pr review fixup

* chore: remove irrelevant depcheckrc change

* chore: fix import
  • Loading branch information
himanshusinghs authored Jan 29, 2024
1 parent 7470fcc commit df6f378
Show file tree
Hide file tree
Showing 17 changed files with 520 additions and 507 deletions.
20 changes: 20 additions & 0 deletions packages/compass-preferences-model/src/compass-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { ParsedGlobalPreferencesResult } from './global-config';
import type { PreferencesAccess } from './preferences';
import type { UserStorage } from './user-storage';
import { UserStorageImpl } from './user-storage';
import { getActiveUserId } from './utils';
import { setupPreferences } from './setup-preferences';

export async function setupPreferencesAndUser(
globalPreferences: ParsedGlobalPreferencesResult
): Promise<{ userStorage: UserStorage; preferences: PreferencesAccess }> {
const preferences = await setupPreferences(globalPreferences);
const userStorage = new UserStorageImpl();
const user = await userStorage.getOrCreate(getActiveUserId(preferences));
// update user id (telemetryAnonymousId) in preferences if new user was created.
await preferences.savePreferences({ telemetryAnonymousId: user.id });
await userStorage.updateUser(user.id, {
lastUsed: new Date(),
});
return { preferences, userStorage };
}
9 changes: 3 additions & 6 deletions packages/compass-preferences-model/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,9 @@ export {
getExampleConfigFile,
} from './global-config';
export type { ParsedGlobalPreferencesResult } from './global-config';
export {
setupPreferencesAndUser,
getActiveUser,
isAIFeatureEnabled,
} from './utils';
export type { User, UserStorage } from './storage';
export { getActiveUser, isAIFeatureEnabled } from './utils';
export { setupPreferencesAndUser } from './compass-utils';
export type { User, UserStorage } from './user-storage';
export type { PreferencesAccess };
export { setupPreferences };
export const defaultPreferencesInstance: PreferencesAccess =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { PreferencesStorage } from './preferences-storage';
import { getDefaultsForStoredPreferences } from './preferences-schema';
import type { AllPreferences, StoredPreferences } from './preferences-schema';

export class InMemoryStorage implements PreferencesStorage {
private preferences = getDefaultsForStoredPreferences();

constructor(preferencesOverrides?: Partial<AllPreferences>) {
this.preferences = {
...this.preferences,
...preferencesOverrides,
};
}

getPreferences() {
return this.preferences;
}

// eslint-disable-next-line @typescript-eslint/require-await
async updatePreferences(attributes: Partial<StoredPreferences>) {
this.preferences = {
...this.preferences,
...attributes,
};
}

async setup() {
// noop
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
import { PersistentStorage } from './preferences-persistent-storage';
import { getDefaultsForStoredPreferences } from './preferences-schema';
import { expect } from 'chai';

const getPreferencesFolder = (tmpDir: string) => {
return path.join(tmpDir, 'AppPreferences');
};

const getPreferencesFile = (tmpDir: string) => {
return path.join(getPreferencesFolder(tmpDir), 'General.json');
};

describe('PersistentStorage', function () {
let tmpDir: string;
beforeEach(async function () {
tmpDir = await fs.mkdtemp(
path.join(os.tmpdir(), 'compass-preferences-storage')
);
});

afterEach(async function () {
await fs.rmdir(tmpDir, { recursive: true });
});

it('sets up the storage', async function () {
// When user starts compass first time, it creates AppPreferences folder with
// General.json to store default preferences.

const storage = new PersistentStorage(tmpDir);

const preferencesDir = getPreferencesFolder(tmpDir);
const preferencesFile = getPreferencesFile(tmpDir);

expect(async () => await fs.access(preferencesDir)).to.throw;
expect(async () => await fs.access(preferencesFile)).to.throw;

await storage.setup();

expect(async () => await fs.access(preferencesDir)).to.not.throw;
expect(async () => await fs.access(preferencesFile)).to.not.throw;

expect(
JSON.parse((await fs.readFile(preferencesFile)).toString())
).to.deep.equal(getDefaultsForStoredPreferences());
});

it('when invalid json is stored, it sets the defaults', async function () {
const storage = new PersistentStorage(tmpDir);

const preferencesFile = getPreferencesFile(tmpDir);
await fs.mkdir(getPreferencesFolder(tmpDir));
await fs.writeFile(preferencesFile, '{}}', 'utf-8');

// Ensure it exists
expect(async () => await fs.access(preferencesFile)).to.not.throw;

await storage.setup();

expect(
JSON.parse((await fs.readFile(preferencesFile)).toString())
).to.deep.equal(getDefaultsForStoredPreferences());
});

it('updates preferences', async function () {
const storage = new PersistentStorage(tmpDir);
await storage.setup();

await storage.updatePreferences({ currentUserId: '123456789' });

const newPreferences = storage.getPreferences();

expect(newPreferences).to.deep.equal({
...getDefaultsForStoredPreferences(),
currentUserId: '123456789',
});
});

it('returns default preference values if its not stored on disk', async function () {
const storage = new PersistentStorage(tmpDir);

// manually setup the file with no content
await fs.mkdir(getPreferencesFolder(tmpDir));
await fs.writeFile(getPreferencesFile(tmpDir), JSON.stringify({}), 'utf-8');

await storage.updatePreferences({
currentUserId: '123456789',
});

expect(storage.getPreferences()).to.deep.equal({
...getDefaultsForStoredPreferences(),
currentUserId: '123456789',
});
});

it('does not save random props', async function () {
const storage = new PersistentStorage(tmpDir);
await storage.setup();

await storage.updatePreferences({ someThingNotSupported: 'abc' } as any);

const newPreferences = storage.getPreferences();

expect(newPreferences).to.deep.equal(getDefaultsForStoredPreferences());
});

it('strips unknown props when reading from disk', async function () {
const storage = new PersistentStorage(tmpDir);

// manually setup the file with default props and unknown prop
await fs.mkdir(getPreferencesFolder(tmpDir));
await fs.writeFile(
getPreferencesFile(tmpDir),
JSON.stringify({
...getDefaultsForStoredPreferences(),
somethingUnknown: true,
}),
'utf-8'
);

await storage.setup();

expect(storage.getPreferences()).to.deep.equal(
getDefaultsForStoredPreferences()
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { z } from 'zod';
import { UserData } from '@mongodb-js/compass-user-data';
import {
getDefaultsForStoredPreferences,
getPreferencesValidator,
} from './preferences-schema';
import type {
StoredPreferences,
StoredPreferencesValidator,
} from './preferences-schema';

import type { PreferencesStorage } from './preferences-storage';

export class PersistentStorage implements PreferencesStorage {
private readonly file = 'General';
private readonly defaultPreferences = getDefaultsForStoredPreferences();
private readonly userData: UserData<StoredPreferencesValidator>;
private preferences: StoredPreferences = getDefaultsForStoredPreferences();

constructor(basePath?: string) {
this.userData = new UserData(getPreferencesValidator(), {
subdir: 'AppPreferences',
basePath,
});
}

async setup() {
try {
this.preferences = await this.readPreferences();
} catch (e) {
if (
(e as any).code === 'ENOENT' || // First time user
e instanceof SyntaxError // Invalid json
) {
// Create the file for the first time
await this.userData.write(this.file, this.defaultPreferences);
return;
}
throw e;
}
}

private async readPreferences(): Promise<StoredPreferences> {
return await this.userData.readOne(this.file, {
ignoreErrors: false,
});
}

getPreferences(): StoredPreferences {
return {
...this.defaultPreferences,
...this.preferences,
};
}

async updatePreferences(
attributes: Partial<z.input<StoredPreferencesValidator>>
) {
await this.userData.write(this.file, {
...(await this.readPreferences()),
...attributes,
});

this.preferences = await this.readPreferences();
}
}
7 changes: 7 additions & 0 deletions packages/compass-preferences-model/src/preferences-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type { StoredPreferences } from './preferences-schema';

export interface PreferencesStorage {
setup(): Promise<void>;
getPreferences(): StoredPreferences;
updatePreferences(attributes: Partial<StoredPreferences>): Promise<void>;
}
2 changes: 1 addition & 1 deletion packages/compass-preferences-model/src/preferences.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import os from 'os';
import { Preferences } from './preferences';
import { expect } from 'chai';
import { featureFlags } from './feature-flags';
import { PersistentStorage } from './storage';
import { PersistentStorage } from './preferences-persistent-storage';
import { createLoggerAndTelemetry } from '@mongodb-js/compass-logging';

const releasedFeatureFlags = Object.entries(featureFlags)
Expand Down
7 changes: 4 additions & 3 deletions packages/compass-preferences-model/src/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import type {
DeriveValueFunction,
} from './preferences-schema';
import { allPreferencesProps } from './preferences-schema';
import { InMemoryStorage, type BasePreferencesStorage } from './storage';
import { InMemoryStorage } from './preferences-in-memory-storage';
import type { PreferencesStorage } from './preferences-storage';

export interface PreferencesAccess {
savePreferences(
Expand Down Expand Up @@ -43,7 +44,7 @@ type PreferenceSandboxPropertiesImpl = {
export class Preferences {
private _logger: LoggerAndTelemetry;
private _onPreferencesChangedCallbacks: OnPreferencesChangedCallback[];
private _preferencesStorage: BasePreferencesStorage;
private _preferencesStorage: PreferencesStorage;
private _globalPreferences: {
cli: Partial<AllPreferences>;
global: Partial<AllPreferences>;
Expand All @@ -56,7 +57,7 @@ export class Preferences {
preferencesStorage = new InMemoryStorage(),
}: {
logger: LoggerAndTelemetry;
preferencesStorage: BasePreferencesStorage;
preferencesStorage: PreferencesStorage;
globalPreferences?: Partial<ParsedGlobalPreferencesResult>;
}) {
this._logger = logger;
Expand Down
65 changes: 2 additions & 63 deletions packages/compass-preferences-model/src/provider.ts
Original file line number Diff line number Diff line change
@@ -1,67 +1,6 @@
import { createContext, useContext } from 'react';
import { createNoopLoggerAndTelemetry } from '@mongodb-js/compass-logging/provider';
import { Preferences, type PreferencesAccess } from './preferences';
import { type AllPreferences } from './preferences-schema';
import { InMemoryStorage } from './storage';
export { usePreference, withPreferences } from './react';
export * from './react';
export { ReadOnlyPreferenceAccess } from './read-only-preferences-access';
export { useIsAIFeatureEnabled } from './utils';
export { capMaxTimeMSAtPreferenceLimit } from './maxtimems';
export { featureFlags } from './feature-flags';
export { getSettingDescription } from './preferences-schema';

export class ReadOnlyPreferenceAccess implements PreferencesAccess {
private _preferences: Preferences;
constructor(preferencesOverrides?: Partial<AllPreferences>) {
this._preferences = new Preferences({
logger: createNoopLoggerAndTelemetry(),
preferencesStorage: new InMemoryStorage(preferencesOverrides),
});
}

savePreferences() {
return Promise.resolve(this._preferences.getPreferences());
}

refreshPreferences() {
return Promise.resolve(this._preferences.getPreferences());
}

getPreferences() {
return this._preferences.getPreferences();
}

ensureDefaultConfigurableUserPreferences() {
return this._preferences.ensureDefaultConfigurableUserPreferences();
}

getConfigurableUserPreferences() {
return Promise.resolve(this._preferences.getConfigurableUserPreferences());
}

getPreferenceStates() {
return Promise.resolve(this._preferences.getPreferenceStates());
}

onPreferenceValueChanged() {
return () => {
// noop
};
}

createSandbox() {
return Promise.resolve(new ReadOnlyPreferenceAccess(this.getPreferences()));
}
}

const PreferencesContext = createContext<PreferencesAccess>(
// Our context starts with our read-only preference access but we expect
// different runtimes to provide their own access implementation at render.
new ReadOnlyPreferenceAccess()
);

export const PreferencesProvider = PreferencesContext.Provider;

export function preferencesLocator(): PreferencesAccess {
return useContext(PreferencesContext);
}
export type { PreferencesAccess };
Loading

0 comments on commit df6f378

Please sign in to comment.