diff --git a/packages/atlas-service/src/atlas-service.ts b/packages/atlas-service/src/atlas-service.ts index 8a90283693e..e1ef6957684 100644 --- a/packages/atlas-service/src/atlas-service.ts +++ b/packages/atlas-service/src/atlas-service.ts @@ -75,6 +75,11 @@ export class AtlasService { // https://github.com/10gen/mms/blob/9f858bb987aac6aa80acfb86492dd74c89cbb862/client/packages/project/common/ajaxPrefilter.ts#L34-L49 return this.cloudEndpoint(path); } + tempEndpoint(path?: string): string { + return `https://cluster-connection.cloud-dev.mongodb.com${normalizePath( + path + )}`; + } driverProxyEndpoint(path?: string): string { return `${this.config.ccsBaseUrl}${normalizePath(path)}`; } @@ -88,13 +93,14 @@ export class AtlasService { { url } ); try { + const headers = { + ...this.options?.defaultHeaders, + ...(shouldAddCSRFHeaders(init?.method) && getCSRFHeaders()), + ...init?.headers, + }; const res = await fetch(url, { ...init, - headers: { - ...this.options?.defaultHeaders, - ...(shouldAddCSRFHeaders(init?.method) && getCSRFHeaders()), - ...init?.headers, - }, + headers, }); this.logger.log.info( this.logger.mongoLogId(1_001_000_309), diff --git a/packages/atlas-service/src/provider.tsx b/packages/atlas-service/src/provider.tsx index c4786939a91..bbfe0f14c1a 100644 --- a/packages/atlas-service/src/provider.tsx +++ b/packages/atlas-service/src/provider.tsx @@ -48,7 +48,7 @@ export const AtlasServiceProvider: React.FC<{ ); }); -function useAtlasServiceContext(): AtlasService { +export function useAtlasServiceContext(): AtlasService { const service = useContext(AtlasServiceContext); if (!service) { throw new Error('No AtlasService available in this context'); diff --git a/packages/compass-query-bar/src/stores/query-bar-reducer.ts b/packages/compass-query-bar/src/stores/query-bar-reducer.ts index 0069a473753..0ce2ff7d029 100644 --- a/packages/compass-query-bar/src/stores/query-bar-reducer.ts +++ b/packages/compass-query-bar/src/stores/query-bar-reducer.ts @@ -356,10 +356,7 @@ export const saveRecentAsFavorite = ( }; // add it in the favorite - await favoriteQueryStorage?.updateAttributes( - favoriteQuery._id, - favoriteQuery - ); + await favoriteQueryStorage?.saveQuery(favoriteQuery, favoriteQuery._id); // update favorites void dispatch(fetchFavorites()); diff --git a/packages/compass-user-data/src/user-data.ts b/packages/compass-user-data/src/user-data.ts index 11de8a1230c..d27ded084fb 100644 --- a/packages/compass-user-data/src/user-data.ts +++ b/packages/compass-user-data/src/user-data.ts @@ -10,7 +10,7 @@ const { log, mongoLogId } = createLogger('COMPASS-USER-STORAGE'); type SerializeContent = (content: I) => string; type DeserializeContent = (content: string) => unknown; -type GetResourceUrl = (path?: string) => Promise; +type GetResourceUrl = (path?: string) => string; type AuthenticatedFetch = ( url: RequestInfo | URL, options?: RequestInit @@ -61,6 +61,10 @@ export abstract class IUserData { abstract write(id: string, content: z.input): Promise; abstract delete(id: string): Promise; abstract readAll(options?: ReadOptions): Promise>; + abstract readOne( + id: string, + options?: ReadOptions + ): Promise | undefined>; abstract updateAttributes( id: string, data: Partial> @@ -292,8 +296,8 @@ export class AtlasUserData extends IUserData { this.validator.parse(content); const response = await this.authenticatedFetch( - await this.getResourceUrl( - `${this.dataType}/${this.orgId}/${this.projectId}` + this.getResourceUrl( + `userData/${this.dataType}/${this.orgId}/${this.projectId}/${id}` ), { method: 'POST', @@ -301,11 +305,10 @@ export class AtlasUserData extends IUserData { 'Content-Type': 'application/json', }, body: JSON.stringify({ - id: id, data: this.serialize(content), createdAt: new Date(), - projectId: this.projectId, }), + credentials: 'include', } ); @@ -322,8 +325,8 @@ export class AtlasUserData extends IUserData { 'Atlas Backend', 'Error writing data', { - url: await this.getResourceUrl( - `${this.dataType}/${this.orgId}/${this.projectId}` + url: this.getResourceUrl( + `userData/${this.dataType}/${this.orgId}/${this.projectId}` ), error: (error as Error).message, } @@ -335,11 +338,12 @@ export class AtlasUserData extends IUserData { async delete(id: string): Promise { try { const response = await this.authenticatedFetch( - await this.getResourceUrl( - `${this.dataType}/${this.orgId}/${this.projectId}/${id}` + this.getResourceUrl( + `userData/${this.dataType}/${this.orgId}/${this.projectId}/${id}` ), { method: 'DELETE', + credentials: 'include', } ); if (!response.ok) { @@ -354,8 +358,8 @@ export class AtlasUserData extends IUserData { 'Atlas Backend', 'Error deleting data', { - url: await this.getResourceUrl( - `${this.dataType}/${this.orgId}/${this.projectId}/${id}` + url: this.getResourceUrl( + `userData/${this.dataType}/${this.orgId}/${this.projectId}/${id}` ), error: (error as Error).message, } @@ -369,13 +373,15 @@ export class AtlasUserData extends IUserData { data: [], errors: [], }; + // debugger; try { const response = await this.authenticatedFetch( - await this.getResourceUrl( - `${this.dataType}/${this.orgId}/${this.projectId}` + this.getResourceUrl( + `userData/${this.dataType}/${this.orgId}/${this.projectId}` ), { method: 'GET', + credentials: 'include', } ); if (!response.ok) { @@ -411,15 +417,19 @@ export class AtlasUserData extends IUserData { }; const response = await this.authenticatedFetch( - await this.getResourceUrl( - `${this.dataType}/${this.orgId}/${this.projectId}/${id}` + this.getResourceUrl( + `userData/${this.dataType}/${this.orgId}/${this.projectId}/${id}` ), { method: 'PUT', headers: { 'Content-Type': 'application/json', }, - body: this.serialize(newData), + body: JSON.stringify({ + data: this.serialize(newData), + createdAt: new Date(), + }), + credentials: 'include', } ); if (!response.ok) { @@ -434,8 +444,8 @@ export class AtlasUserData extends IUserData { 'Atlas Backend', 'Error updating data', { - url: await this.getResourceUrl( - `${this.dataType}/${this.orgId}/${this.projectId}/${id}` + url: this.getResourceUrl( + `userData/${this.dataType}/${this.orgId}/${this.projectId}/${id}` ), error: (error as Error).message, } @@ -445,14 +455,15 @@ export class AtlasUserData extends IUserData { } // TODO: change this depending on whether or not updateAttributes can provide all current data - async readOne(id: string): Promise> { + async readOne(id: string): Promise | undefined> { try { const getResponse = await this.authenticatedFetch( - await this.getResourceUrl( - `${this.dataType}/${this.orgId}/${this.projectId}/${id}` + this.getResourceUrl( + `userData/${this.dataType}/${this.orgId}/${this.projectId}/${id}` ), { method: 'GET', + credentials: 'include', } ); if (!getResponse.ok) { @@ -469,13 +480,12 @@ export class AtlasUserData extends IUserData { 'Atlas Backend', 'Error reading data', { - url: await this.getResourceUrl( - `${this.dataType}/${this.orgId}/${this.projectId}/${id}` + url: this.getResourceUrl( + `userData/${this.dataType}/${this.orgId}/${this.projectId}/${id}` ), error: (error as Error).message, } ); - return null; } } } diff --git a/packages/compass-web/src/entrypoint.tsx b/packages/compass-web/src/entrypoint.tsx index 7ec34abc395..48a37a25db9 100644 --- a/packages/compass-web/src/entrypoint.tsx +++ b/packages/compass-web/src/entrypoint.tsx @@ -44,7 +44,10 @@ import { import { PreferencesProvider } from 'compass-preferences-model/provider'; import type { AllPreferences } from 'compass-preferences-model/provider'; import FieldStorePlugin from '@mongodb-js/compass-field-store'; -import { AtlasServiceProvider } from '@mongodb-js/atlas-service/provider'; +import { + AtlasServiceProvider, + useAtlasServiceContext, +} from '@mongodb-js/atlas-service/provider'; import { AtlasAiServiceProvider } from '@mongodb-js/compass-generative-ai/provider'; import { LoggerProvider } from '@mongodb-js/compass-logging/provider'; import { TelemetryProvider } from '@mongodb-js/compass-telemetry/provider'; @@ -55,9 +58,22 @@ import type { LogFunction, DebugFunction } from './logger'; import { useCompassWebLogger } from './logger'; import { type TelemetryServiceOptions } from '@mongodb-js/compass-telemetry'; import { WebWorkspaceTab as WelcomeWorkspaceTab } from '@mongodb-js/compass-welcome'; +import { WorkspaceTab as MyQueriesWorkspace } from '@mongodb-js/compass-saved-aggregations-queries'; import { useCompassWebPreferences } from './preferences'; import { DataModelingWorkspaceTab as DataModelingWorkspace } from '@mongodb-js/compass-data-modeling'; import { DataModelStorageServiceProviderInMemory } from '@mongodb-js/compass-data-modeling/web'; +import { + CompassFavoriteQueryStorage, + CompassPipelineStorage, + CompassRecentQueryStorage, +} from '@mongodb-js/my-queries-storage'; +import { + PipelineStorageProvider, + FavoriteQueryStorageProvider, + RecentQueryStorageProvider, + type FavoriteQueryStorageAccess, + type RecentQueryStorageAccess, +} from '@mongodb-js/my-queries-storage/provider'; export type TrackFunction = ( event: string, @@ -78,6 +94,56 @@ const WithAtlasProviders: React.FC = ({ children }) => { ); }; +const WithStorageProviders: React.FC<{ orgId: string; projectId: string }> = ({ + children, + orgId, + projectId, +}) => { + const atlasService = useAtlasServiceContext(); + const authenticatedFetch = atlasService.authenticatedFetch.bind(atlasService); + // TODO: use non-hardcoded endpoint + const getResourceUrl = atlasService.tempEndpoint.bind(atlasService); + const pipelineStorage = useRef( + new CompassPipelineStorage({ + orgId, + projectId, + getResourceUrl, + authenticatedFetch, + }) + ); + const favoriteQueryStorage = useRef({ + getStorage(options) { + return new CompassFavoriteQueryStorage({ + ...options, + orgId, + projectId, + getResourceUrl, + authenticatedFetch, + }); + }, + }); + const recentQueryStorage = useRef({ + getStorage(options) { + return new CompassRecentQueryStorage({ + ...options, + orgId, + projectId, + getResourceUrl, + authenticatedFetch, + }); + }, + }); + return ( + + + + {children} + + + + ); +}; + type CompassWorkspaceProps = Pick< React.ComponentProps, 'initialWorkspaceTabs' | 'onActiveWorkspaceTabChange' @@ -178,6 +244,7 @@ function CompassWorkspace({ CollectionsWorkspaceTab, CollectionWorkspace, DataModelingWorkspace, + MyQueriesWorkspace, ]} > - - - { - return Promise.resolve([{}, null] as [ - Record, - null - ]); - }} - onAutoconnectInfoRequest={(connectionStore) => { - if (autoconnectId) { - return connectionStore.loadAll().then( - (connections) => { - return connections.find( - (connectionInfo) => - connectionInfo.id === autoconnectId - ); - }, - (err) => { - const { log, mongoLogId } = logger; - log.warn( - mongoLogId(1_001_000_329), - 'Compass Web', - 'Could not load connections when trying to autoconnect', - { err: err.message } - ); - return undefined; - } - ); - } - return Promise.resolve(undefined); - }} + + + - - - - { + return Promise.resolve([{}, null] as [ + Record, + null + ]); + }} + onAutoconnectInfoRequest={(connectionStore) => { + if (autoconnectId) { + return connectionStore.loadAll().then( + (connections) => { + return connections.find( + (connectionInfo) => + connectionInfo.id === autoconnectId + ); + }, + (err) => { + const { log, mongoLogId } = logger; + log.warn( + mongoLogId(1_001_000_329), + 'Compass Web', + 'Could not load connections when trying to autoconnect', + { err: err.message } + ); + return undefined; } - onOpenConnectViaModal={onOpenConnectViaModal} - > - - - - - - - + ); + } + return Promise.resolve(undefined); + }} + > + + + + + + + + + + + + diff --git a/packages/my-queries-storage/src/compass-pipeline-storage.spec.ts b/packages/my-queries-storage/src/compass-pipeline-storage.spec.ts index 26329ab6cf3..32ce5f4fd1e 100644 --- a/packages/my-queries-storage/src/compass-pipeline-storage.spec.ts +++ b/packages/my-queries-storage/src/compass-pipeline-storage.spec.ts @@ -21,7 +21,7 @@ describe('CompassPipelineStorage', function () { beforeEach(async function () { tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'saved-pipelines-tests')); - pipelineStorage = new CompassPipelineStorage(tmpDir); + pipelineStorage = new CompassPipelineStorage({ basePath: tmpDir }); }); afterEach(async function () { diff --git a/packages/my-queries-storage/src/compass-pipeline-storage.ts b/packages/my-queries-storage/src/compass-pipeline-storage.ts index 8c42eeb722b..597e5fccbae 100644 --- a/packages/my-queries-storage/src/compass-pipeline-storage.ts +++ b/packages/my-queries-storage/src/compass-pipeline-storage.ts @@ -1,14 +1,47 @@ -import { FileUserData } from '@mongodb-js/compass-user-data'; +import { + type IUserData, + FileUserData, + AtlasUserData, +} 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 type PipelineStorageOptions = { + basePath?: string; + orgId?: string; + projectId?: string; + getResourceUrl?: (path?: string) => string; + authenticatedFetch?: ( + url: RequestInfo | URL, + options?: RequestInit + ) => Promise; +}; + export class CompassPipelineStorage implements PipelineStorage { - private readonly userData: FileUserData; - constructor(basePath?: string) { - this.userData = new FileUserData(PipelineSchema, 'SavedPipelines', { - basePath, - }); + private readonly userData: IUserData; + constructor(options: PipelineStorageOptions = {}) { + const dataType = 'SavedPipelines'; + if ( + options.orgId && + options.projectId && + options.getResourceUrl && + options.authenticatedFetch + ) { + this.userData = new AtlasUserData( + PipelineSchema, + 'favoriteAggregations', + options.orgId, + options.projectId, + options.getResourceUrl, + options.authenticatedFetch, + {} + ); + } else { + this.userData = new FileUserData(PipelineSchema, dataType, { + basePath: options.basePath, + }); + } } async loadAll(): Promise { diff --git a/packages/my-queries-storage/src/compass-query-storage.ts b/packages/my-queries-storage/src/compass-query-storage.ts index dfcfbca36c5..06583ce38cd 100644 --- a/packages/my-queries-storage/src/compass-query-storage.ts +++ b/packages/my-queries-storage/src/compass-query-storage.ts @@ -1,26 +1,58 @@ -import { UUID, EJSON } from 'bson'; +import { ObjectId, EJSON } from 'bson'; import { type z } from '@mongodb-js/compass-user-data'; -import { type IUserData, FileUserData } from '@mongodb-js/compass-user-data'; +import { + type IUserData, + FileUserData, + AtlasUserData, +} 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; + orgId?: string; + projectId?: string; + getResourceUrl?: (path?: string) => string; + authenticatedFetch?: ( + url: RequestInfo | URL, + options?: RequestInit + ) => Promise; }; export abstract class CompassQueryStorage { protected readonly userData: IUserData; constructor( schemaValidator: TSchema, - protected readonly folder: string, + protected readonly dataType: string, protected readonly options: QueryStorageOptions ) { - // TODO: logic for whether we're in compass web or compass desktop - this.userData = new FileUserData(schemaValidator, folder, { - basePath: options.basepath, - serialize: (content) => EJSON.stringify(content, undefined, 2), - deserialize: (content: string) => EJSON.parse(content), - }); + if ( + options.orgId && + options.projectId && + options.getResourceUrl && + options.authenticatedFetch + ) { + const type = + dataType === 'RecentQueries' ? 'recentQueries' : 'favoriteQueries'; + this.userData = new AtlasUserData( + schemaValidator, + type, + options.orgId, + options.projectId, + options.getResourceUrl, + options.authenticatedFetch, + { + serialize: (content) => EJSON.stringify(content, undefined, 2), + deserialize: (content: string) => EJSON.parse(content), + } + ); + } else { + this.userData = new FileUserData(schemaValidator, dataType, { + basePath: options.basepath, + serialize: (content) => EJSON.stringify(content, undefined, 2), + deserialize: (content: string) => EJSON.parse(content), + }); + } } async loadAll(namespace?: string): Promise[]> { @@ -74,8 +106,8 @@ export class CompassRecentQueryStorage const lastRecent = recentQueries[recentQueries.length - 1]; await this.delete(lastRecent._id); } - - const _id = new UUID().toString(); + // TODO: verify that this doesn't break anything in compass + const _id = new ObjectId().toHexString(); // this creates a recent query that we will write to system/db const recentQuery = { ...data, @@ -98,9 +130,11 @@ export class CompassFavoriteQueryStorage data: Omit< z.input, '_id' | '_lastExecuted' | '_dateModified' | '_dateSaved' - > + >, + _id?: string ): Promise { - const _id = new UUID().toString(); + // TODO: verify that this doesn't break anything in compass + _id ??= new ObjectId().toHexString(); // this creates a favorite query that we will write to system/db const favoriteQuery = { ...data, diff --git a/packages/my-queries-storage/src/query-storage-schema.ts b/packages/my-queries-storage/src/query-storage-schema.ts index 29f27a1e749..cd74a028302 100644 --- a/packages/my-queries-storage/src/query-storage-schema.ts +++ b/packages/my-queries-storage/src/query-storage-schema.ts @@ -12,7 +12,7 @@ const queryProps = { }; const commonMetadata = { - _id: z.string().uuid(), + _id: z.string(), _lastExecuted: z .union([z.coerce.date(), z.number()]) .transform((x) => new Date(x)), diff --git a/packages/my-queries-storage/src/query-storage.ts b/packages/my-queries-storage/src/query-storage.ts index 9ee445a646c..522311e0531 100644 --- a/packages/my-queries-storage/src/query-storage.ts +++ b/packages/my-queries-storage/src/query-storage.ts @@ -23,6 +23,7 @@ export interface FavoriteQueryStorage data: Omit< z.input, '_id' | '_lastExecuted' | '_dateModified' | '_dateSaved' - > + >, + id?: string ): Promise; }