diff --git a/packages/atlas-service/src/secret-store.ts b/packages/atlas-service/src/secret-store.ts index 29118af3975..a303151250f 100644 --- a/packages/atlas-service/src/secret-store.ts +++ b/packages/atlas-service/src/secret-store.ts @@ -1,13 +1,13 @@ -import { UserData, z } from '@mongodb-js/compass-user-data'; +import { FileUserData, z } from '@mongodb-js/compass-user-data'; import { safeStorage } from 'electron'; const AtlasPluginStateSchema = z.string().optional(); export class SecretStore { - private readonly userData: UserData; + private readonly userData: FileUserData; private readonly fileName = 'AtlasPluginState'; constructor(basePath?: string) { - this.userData = new UserData(AtlasPluginStateSchema, { + this.userData = new FileUserData(AtlasPluginStateSchema, { subdir: 'AtlasState', basePath, }); diff --git a/packages/compass-data-modeling/src/services/data-model-storage-electron.tsx b/packages/compass-data-modeling/src/services/data-model-storage-electron.tsx index 29c23458f3e..fa7a62f77e9 100644 --- a/packages/compass-data-modeling/src/services/data-model-storage-electron.tsx +++ b/packages/compass-data-modeling/src/services/data-model-storage-electron.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { UserData } from '@mongodb-js/compass-user-data'; +import { FileUserData } from '@mongodb-js/compass-user-data'; import type { DataModelStorage, MongoDBDataModelDescription, @@ -8,9 +8,11 @@ import { MongoDBDataModelDescriptionSchema } from './data-model-storage'; import { DataModelStorageServiceProvider } from '../provider'; class DataModelStorageElectron implements DataModelStorage { - private readonly userData: UserData; + private readonly userData: FileUserData< + typeof MongoDBDataModelDescriptionSchema + >; constructor(basePath?: string) { - this.userData = new UserData(MongoDBDataModelDescriptionSchema, { + this.userData = new FileUserData(MongoDBDataModelDescriptionSchema, { subdir: 'DataModelDescriptions', basePath, }); diff --git a/packages/compass-preferences-model/src/preferences-persistent-storage.ts b/packages/compass-preferences-model/src/preferences-persistent-storage.ts index 4507f7ccacf..2a66dee37d5 100644 --- a/packages/compass-preferences-model/src/preferences-persistent-storage.ts +++ b/packages/compass-preferences-model/src/preferences-persistent-storage.ts @@ -1,4 +1,4 @@ -import { type z, UserData } from '@mongodb-js/compass-user-data'; +import { type z, FileUserData } from '@mongodb-js/compass-user-data'; import { getDefaultsForStoredPreferences, getPreferencesValidator, @@ -20,12 +20,12 @@ export type PreferencesSafeStorage = { export class PersistentStorage implements PreferencesStorage { private readonly file = 'General'; private readonly defaultPreferences = getDefaultsForStoredPreferences(); - private readonly userData: UserData; + private readonly userData: FileUserData; private preferences: StoredPreferences = getDefaultsForStoredPreferences(); private safeStorage?: PreferencesSafeStorage; constructor(basePath?: string, safeStorage?: PreferencesSafeStorage) { - this.userData = new UserData(getPreferencesValidator(), { + this.userData = new FileUserData(getPreferencesValidator(), { subdir: 'AppPreferences', basePath, }); diff --git a/packages/compass-preferences-model/src/user-storage.ts b/packages/compass-preferences-model/src/user-storage.ts index 044d6b04b30..fbaed7f7b27 100644 --- a/packages/compass-preferences-model/src/user-storage.ts +++ b/packages/compass-preferences-model/src/user-storage.ts @@ -1,6 +1,6 @@ import { z } from '@mongodb-js/compass-user-data'; import { UUID } from 'bson'; -import { UserData } from '@mongodb-js/compass-user-data'; +import { FileUserData } from '@mongodb-js/compass-user-data'; const UserSchema = z.object({ id: z.string().uuid(), @@ -24,9 +24,9 @@ export interface UserStorage { } export class UserStorageImpl implements UserStorage { - private readonly userData: UserData; + private readonly userData: FileUserData; constructor(basePath?: string) { - this.userData = new UserData(UserSchema, { + this.userData = new FileUserData(UserSchema, { subdir: 'Users', basePath, }); diff --git a/packages/compass-shell/src/modules/history-storage.ts b/packages/compass-shell/src/modules/history-storage.ts index f302a2cf892..5f1cab73a04 100644 --- a/packages/compass-shell/src/modules/history-storage.ts +++ b/packages/compass-shell/src/modules/history-storage.ts @@ -1,12 +1,12 @@ import { getAppName } from '@mongodb-js/compass-utils'; -import { UserData, z } from '@mongodb-js/compass-user-data'; +import { FileUserData, z } from '@mongodb-js/compass-user-data'; export class HistoryStorage { fileName = 'shell-history'; userData; constructor(basePath?: string) { - this.userData = new UserData(z.string().array(), { + this.userData = new FileUserData(z.string().array(), { // Todo: https://jira.mongodb.org/browse/COMPASS-7080 subdir: getAppName() ?? '', basePath, diff --git a/packages/compass-user-data/src/index.ts b/packages/compass-user-data/src/index.ts index f5b3b57d6bc..b216fd77f99 100644 --- a/packages/compass-user-data/src/index.ts +++ b/packages/compass-user-data/src/index.ts @@ -1,3 +1,3 @@ export type { Stats, ReadAllResult, ReadAllWithStatsResult } from './user-data'; -export { UserData } from './user-data'; +export { IUserData, FileUserData } from './user-data'; export { z } from 'zod'; diff --git a/packages/compass-user-data/src/user-data.spec.ts b/packages/compass-user-data/src/user-data.spec.ts index e1f2ead78cb..155a020092a 100644 --- a/packages/compass-user-data/src/user-data.spec.ts +++ b/packages/compass-user-data/src/user-data.spec.ts @@ -3,7 +3,7 @@ import { Stats } from 'fs'; import os from 'os'; import path from 'path'; import { expect } from 'chai'; -import { UserData, type UserDataOptions } from './user-data'; +import { FileUserData, type FileUserDataOptions } from './user-data'; import { z, type ZodError } from 'zod'; type ValidatorOptions = { @@ -44,11 +44,11 @@ describe('user-data', function () { const getUserData = ( userDataOpts: Partial< - UserDataOptions>> + FileUserDataOptions>> > = {}, validatorOpts: ValidatorOptions = {} ) => { - return new UserData(getTestSchema(validatorOpts), { + return new FileUserData(getTestSchema(validatorOpts), { subdir, basePath: tmpDir, ...userDataOpts, diff --git a/packages/compass-user-data/src/user-data.ts b/packages/compass-user-data/src/user-data.ts index 92d3cd36d5e..ec4cad6868f 100644 --- a/packages/compass-user-data/src/user-data.ts +++ b/packages/compass-user-data/src/user-data.ts @@ -12,7 +12,7 @@ type SerializeContent = (content: I) => string; type DeserializeContent = (content: string) => unknown; type GetFileName = (id: string) => string; -export type UserDataOptions = { +export type FileUserDataOptions = { subdir: string; basePath?: string; serialize?: SerializeContent; @@ -20,6 +20,11 @@ export type UserDataOptions = { getFileName?: GetFileName; }; +export type AtlasUserDataOptions = { + serialize?: SerializeContent; + deserialize?: DeserializeContent; +}; + type ReadOptions = { ignoreErrors: boolean; }; @@ -63,28 +68,54 @@ export interface ReadAllWithStatsResult { errors: Error[]; } -export class UserData { +export abstract class IUserData { + protected readonly validator: T; + protected readonly serialize: SerializeContent>; + protected readonly deserialize: DeserializeContent; + + constructor( + validator: T, + { + serialize = (content: z.input) => JSON.stringify(content, null, 2), + deserialize = JSON.parse, + }: { + serialize?: SerializeContent>; + deserialize?: DeserializeContent; + } = {} + ) { + this.validator = validator; + this.serialize = serialize; + this.deserialize = deserialize; + } + + abstract write(id: string, content: z.input): Promise; + abstract delete(id: string): Promise; + abstract readAll(options?: ReadOptions): Promise>; + abstract updateAttributes( + id: string, + data: Partial> + ): Promise>; +} + +export class FileUserData extends IUserData { private readonly subdir: string; private readonly basePath?: string; - private readonly serialize: SerializeContent>; - private readonly deserialize: DeserializeContent; private readonly getFileName: GetFileName; - private readonly semaphore = new Semaphore(100); + protected readonly semaphore = new Semaphore(100); constructor( - private readonly validator: T, + validator: T, { subdir, basePath, - serialize = (content: z.input) => JSON.stringify(content, null, 2), - deserialize = JSON.parse, + serialize, + deserialize, getFileName = (id) => `${id}.json`, - }: UserDataOptions> + }: FileUserDataOptions> ) { + super(validator, { serialize, deserialize }); this.subdir = subdir; this.basePath = basePath; - this.deserialize = deserialize; - this.serialize = serialize; this.getFileName = getFileName; } @@ -290,4 +321,15 @@ export class UserData { ) { return (await this.readOneWithStats(id, options))?.[0]; } + + async updateAttributes( + id: string, + data: Partial> + ): Promise> { + await this.write(id, { + ...((await this.readOne(id)) ?? {}), + ...data, + }); + return await this.readOne(id); + } } diff --git a/packages/connection-storage/src/compass-main-connection-storage.ts b/packages/connection-storage/src/compass-main-connection-storage.ts index ade7c9e9e6e..78f5380b735 100644 --- a/packages/connection-storage/src/compass-main-connection-storage.ts +++ b/packages/connection-storage/src/compass-main-connection-storage.ts @@ -24,7 +24,7 @@ import type { ImportConnectionOptions, ExportConnectionOptions, } from './import-export-connection'; -import { UserData, z } from '@mongodb-js/compass-user-data'; +import { FileUserData, z } from '@mongodb-js/compass-user-data'; import type { ConnectionStorage, AutoConnectPreferences, @@ -86,7 +86,7 @@ const ConnectionSchema: z.Schema = z .passthrough(); class CompassMainConnectionStorage implements ConnectionStorage { - private readonly userData: UserData; + private readonly userData: FileUserData; private readonly version = 1; private readonly maxAllowedRecentConnections = 10; @@ -95,7 +95,7 @@ class CompassMainConnectionStorage implements ConnectionStorage { private readonly ipcMain: ConnectionStorageIPCMain, basePath?: string ) { - this.userData = new UserData(ConnectionSchema, { + this.userData = new FileUserData(ConnectionSchema, { subdir: 'Connections', basePath, }); diff --git a/packages/my-queries-storage/src/compass-pipeline-storage.ts b/packages/my-queries-storage/src/compass-pipeline-storage.ts index daa5ae362eb..766fc0166a0 100644 --- a/packages/my-queries-storage/src/compass-pipeline-storage.ts +++ b/packages/my-queries-storage/src/compass-pipeline-storage.ts @@ -1,13 +1,13 @@ import type { Stats } from '@mongodb-js/compass-user-data'; -import { UserData } from '@mongodb-js/compass-user-data'; +import { FileUserData } from '@mongodb-js/compass-user-data'; import { PipelineSchema } from './pipeline-storage-schema'; import type { SavedPipeline } from './pipeline-storage-schema'; import type { PipelineStorage } from './pipeline-storage'; export class CompassPipelineStorage implements PipelineStorage { - private readonly userData: UserData; + private readonly userData: FileUserData; constructor(basePath?: string) { - this.userData = new UserData(PipelineSchema, { + this.userData = new FileUserData(PipelineSchema, { subdir: 'SavedPipelines', basePath, }); diff --git a/packages/my-queries-storage/src/compass-query-storage.ts b/packages/my-queries-storage/src/compass-query-storage.ts index e9b6b3b6169..c133ee81c53 100644 --- a/packages/my-queries-storage/src/compass-query-storage.ts +++ b/packages/my-queries-storage/src/compass-query-storage.ts @@ -1,44 +1,30 @@ import { UUID, EJSON } from 'bson'; -import { UserData, type z } from '@mongodb-js/compass-user-data'; -import { - RecentQuerySchema, - FavoriteQuerySchema, - type RecentQuery, - type FavoriteQuery, -} from './query-storage-schema'; +import { type z } from '@mongodb-js/compass-user-data'; +import { type IUserData, FileUserData } from '@mongodb-js/compass-user-data'; +import { RecentQuerySchema, FavoriteQuerySchema } from './query-storage-schema'; import type { FavoriteQueryStorage, RecentQueryStorage } from './query-storage'; export type QueryStorageOptions = { basepath?: string; }; -export interface QueryStorageBackend { - loadAll(namespace?: string): Promise; - updateAttributes(id: string, data: Partial): Promise; - delete(id: string): Promise; - saveQuery(data: Omit): Promise; -} - -export abstract class CompassQueryStorage< - TSchema extends z.Schema, - TData extends z.output = z.output -> implements QueryStorageBackend -{ - protected readonly userData: UserData; +export abstract class CompassQueryStorage { + protected readonly userData: IUserData; constructor( schemaValidator: TSchema, protected readonly folder: string, protected readonly options: QueryStorageOptions ) { - this.userData = new UserData(schemaValidator, { + // TODO: logic for whether we're in compass web or compass desktop + this.userData = new FileUserData(schemaValidator, { subdir: folder, basePath: options.basepath, serialize: (content) => EJSON.stringify(content, undefined, 2), - deserialize: (content) => EJSON.parse(content), + deserialize: (content: string) => EJSON.parse(content), }); } - async loadAll(namespace?: string): Promise { + async loadAll(namespace?: string): Promise[]> { try { const { data } = await this.userData.readAll(); const sortedData = data @@ -52,23 +38,26 @@ export abstract class CompassQueryStorage< } } - async updateAttributes(id: string, data: Partial): Promise { - await this.userData.write(id, { - ...((await this.userData.readOne(id)) ?? {}), - ...data, - }); - return await this.userData.readOne(id); + async write(id: string, content: z.input): Promise { + return await this.userData.write(id, content); } async delete(id: string) { return await this.userData.delete(id); } - abstract saveQuery(data: any): Promise; + async updateAttributes( + id: string, + data: Partial> + ): Promise> { + return await this.userData.updateAttributes(id, data); + } + + abstract saveQuery(data: Partial>): Promise; } export class CompassRecentQueryStorage - extends CompassQueryStorage + extends CompassQueryStorage implements RecentQueryStorage { private readonly maxAllowedQueries = 30; @@ -88,6 +77,7 @@ export class CompassRecentQueryStorage } const _id = new UUID().toString(); + // this creates a recent query that we will write to system/db const recentQuery = { ...data, _id, @@ -98,7 +88,7 @@ export class CompassRecentQueryStorage } export class CompassFavoriteQueryStorage - extends CompassQueryStorage + extends CompassQueryStorage implements FavoriteQueryStorage { constructor(options: QueryStorageOptions = {}) { @@ -112,6 +102,7 @@ export class CompassFavoriteQueryStorage > ): Promise { const _id = new UUID().toString(); + // this creates a favorite query that we will write to system/db const favoriteQuery = { ...data, _id,