Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/atlas-service/src/secret-store.ts
Original file line number Diff line number Diff line change
@@ -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<typeof AtlasPluginStateSchema>;
private readonly userData: FileUserData<typeof AtlasPluginStateSchema>;
private readonly fileName = 'AtlasPluginState';
constructor(basePath?: string) {
this.userData = new UserData(AtlasPluginStateSchema, {
this.userData = new FileUserData(AtlasPluginStateSchema, {
subdir: 'AtlasState',
basePath,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -8,9 +8,11 @@ import { MongoDBDataModelDescriptionSchema } from './data-model-storage';
import { DataModelStorageServiceProvider } from '../provider';

class DataModelStorageElectron implements DataModelStorage {
private readonly userData: UserData<typeof MongoDBDataModelDescriptionSchema>;
private readonly userData: FileUserData<
typeof MongoDBDataModelDescriptionSchema
>;
constructor(basePath?: string) {
this.userData = new UserData(MongoDBDataModelDescriptionSchema, {
this.userData = new FileUserData(MongoDBDataModelDescriptionSchema, {
subdir: 'DataModelDescriptions',
basePath,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -20,12 +20,12 @@ export type PreferencesSafeStorage = {
export class PersistentStorage implements PreferencesStorage {
private readonly file = 'General';
private readonly defaultPreferences = getDefaultsForStoredPreferences();
private readonly userData: UserData<StoredPreferencesValidator>;
private readonly userData: FileUserData<StoredPreferencesValidator>;
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,
});
Expand Down
6 changes: 3 additions & 3 deletions packages/compass-preferences-model/src/user-storage.ts
Original file line number Diff line number Diff line change
@@ -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(),
Expand All @@ -24,9 +24,9 @@ export interface UserStorage {
}

export class UserStorageImpl implements UserStorage {
private readonly userData: UserData<typeof UserSchema>;
private readonly userData: FileUserData<typeof UserSchema>;
constructor(basePath?: string) {
this.userData = new UserData(UserSchema, {
this.userData = new FileUserData(UserSchema, {
subdir: 'Users',
basePath,
});
Expand Down
4 changes: 2 additions & 2 deletions packages/compass-shell/src/modules/history-storage.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/compass-user-data/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
6 changes: 3 additions & 3 deletions packages/compass-user-data/src/user-data.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -44,11 +44,11 @@ describe('user-data', function () {

const getUserData = (
userDataOpts: Partial<
UserDataOptions<z.input<ReturnType<typeof getTestSchema>>>
FileUserDataOptions<z.input<ReturnType<typeof getTestSchema>>>
> = {},
validatorOpts: ValidatorOptions = {}
) => {
return new UserData(getTestSchema(validatorOpts), {
return new FileUserData(getTestSchema(validatorOpts), {
subdir,
basePath: tmpDir,
...userDataOpts,
Expand Down
64 changes: 53 additions & 11 deletions packages/compass-user-data/src/user-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,19 @@ type SerializeContent<I> = (content: I) => string;
type DeserializeContent = (content: string) => unknown;
type GetFileName = (id: string) => string;

export type UserDataOptions<Input> = {
export type FileUserDataOptions<Input> = {
subdir: string;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The naming on this one is unfortunate, but I think it's also relevant for the shared userdata interface

basePath?: string;
serialize?: SerializeContent<Input>;
deserialize?: DeserializeContent;
getFileName?: GetFileName;
};

export type AtlasUserDataOptions<Input> = {
serialize?: SerializeContent<Input>;
deserialize?: DeserializeContent;
};

type ReadOptions = {
ignoreErrors: boolean;
};
Expand Down Expand Up @@ -63,28 +68,54 @@ export interface ReadAllWithStatsResult<T extends z.Schema> {
errors: Error[];
}

export class UserData<T extends z.Schema> {
export abstract class IUserData<T extends z.Schema> {
protected readonly validator: T;
protected readonly serialize: SerializeContent<z.input<T>>;
protected readonly deserialize: DeserializeContent;

constructor(
validator: T,
{
serialize = (content: z.input<T>) => JSON.stringify(content, null, 2),
deserialize = JSON.parse,
}: {
serialize?: SerializeContent<z.input<T>>;
deserialize?: DeserializeContent;
} = {}
) {
this.validator = validator;
this.serialize = serialize;
this.deserialize = deserialize;
}

abstract write(id: string, content: z.input<T>): Promise<boolean>;
abstract delete(id: string): Promise<boolean>;
abstract readAll(options?: ReadOptions): Promise<ReadAllResult<T>>;
abstract updateAttributes(
id: string,
data: Partial<z.input<T>>
): Promise<z.output<T>>;
}

export class FileUserData<T extends z.Schema> extends IUserData<T> {
private readonly subdir: string;
private readonly basePath?: string;
private readonly serialize: SerializeContent<z.input<T>>;
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<T>) => JSON.stringify(content, null, 2),
deserialize = JSON.parse,
serialize,
deserialize,
getFileName = (id) => `${id}.json`,
}: UserDataOptions<z.input<T>>
}: FileUserDataOptions<z.input<T>>
) {
super(validator, { serialize, deserialize });
this.subdir = subdir;
this.basePath = basePath;
this.deserialize = deserialize;
this.serialize = serialize;
this.getFileName = getFileName;
}

Expand Down Expand Up @@ -290,4 +321,15 @@ export class UserData<T extends z.Schema> {
) {
return (await this.readOneWithStats(id, options))?.[0];
}

async updateAttributes(
id: string,
data: Partial<z.input<T>>
): Promise<z.output<T>> {
await this.write(id, {
...((await this.readOne(id)) ?? {}),
...data,
});
return await this.readOne(id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -86,7 +86,7 @@ const ConnectionSchema: z.Schema<ConnectionWithLegacyProps> = z
.passthrough();

class CompassMainConnectionStorage implements ConnectionStorage {
private readonly userData: UserData<typeof ConnectionSchema>;
private readonly userData: FileUserData<typeof ConnectionSchema>;

private readonly version = 1;
private readonly maxAllowedRecentConnections = 10;
Expand All @@ -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,
});
Expand Down
6 changes: 3 additions & 3 deletions packages/my-queries-storage/src/compass-pipeline-storage.ts
Original file line number Diff line number Diff line change
@@ -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<typeof PipelineSchema>;
private readonly userData: FileUserData<typeof PipelineSchema>;
constructor(basePath?: string) {
this.userData = new UserData(PipelineSchema, {
this.userData = new FileUserData(PipelineSchema, {
subdir: 'SavedPipelines',
basePath,
});
Expand Down
55 changes: 23 additions & 32 deletions packages/my-queries-storage/src/compass-query-storage.ts
Original file line number Diff line number Diff line change
@@ -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<TData> {
loadAll(namespace?: string): Promise<TData[]>;
updateAttributes(id: string, data: Partial<TData>): Promise<TData>;
delete(id: string): Promise<boolean>;
saveQuery(data: Omit<TData, '_id' | '_lastExecuted'>): Promise<void>;
}

export abstract class CompassQueryStorage<
TSchema extends z.Schema,
TData extends z.output<TSchema> = z.output<TSchema>
> implements QueryStorageBackend<TData>
{
protected readonly userData: UserData<TSchema>;
export abstract class CompassQueryStorage<TSchema extends z.Schema> {
protected readonly userData: IUserData<TSchema>;
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<TData[]> {
async loadAll(namespace?: string): Promise<z.output<TSchema>[]> {
try {
const { data } = await this.userData.readAll();
const sortedData = data
Expand All @@ -52,23 +38,26 @@ export abstract class CompassQueryStorage<
}
}

async updateAttributes(id: string, data: Partial<TData>): Promise<TData> {
await this.userData.write(id, {
...((await this.userData.readOne(id)) ?? {}),
...data,
});
return await this.userData.readOne(id);
async write(id: string, content: z.input<TSchema>): Promise<boolean> {
return await this.userData.write(id, content);
}

async delete(id: string) {
return await this.userData.delete(id);
}

abstract saveQuery(data: any): Promise<void>;
async updateAttributes(
id: string,
data: Partial<z.input<TSchema>>
): Promise<z.output<TSchema>> {
return await this.userData.updateAttributes(id, data);
}

abstract saveQuery(data: Partial<z.input<TSchema>>): Promise<void>;
}

export class CompassRecentQueryStorage
extends CompassQueryStorage<typeof RecentQuerySchema, RecentQuery>
extends CompassQueryStorage<typeof RecentQuerySchema>
implements RecentQueryStorage
{
private readonly maxAllowedQueries = 30;
Expand All @@ -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,
Expand All @@ -98,7 +88,7 @@ export class CompassRecentQueryStorage
}

export class CompassFavoriteQueryStorage
extends CompassQueryStorage<typeof FavoriteQuerySchema, FavoriteQuery>
extends CompassQueryStorage<typeof FavoriteQuerySchema>
implements FavoriteQueryStorage
{
constructor(options: QueryStorageOptions = {}) {
Expand All @@ -112,6 +102,7 @@ export class CompassFavoriteQueryStorage
>
): Promise<void> {
const _id = new UUID().toString();
// this creates a favorite query that we will write to system/db
const favoriteQuery = {
...data,
_id,
Expand Down
Loading