Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
3f336ec
feat(save-user-data): create a user data abstract class
myang1220 Jul 10, 2025
ad5e416
Merge branch 'main' into user-data-interface
myang1220 Jul 10, 2025
78026a3
fix: rm defaults and add semaphore to FileUserData
myang1220 Jul 10, 2025
b24d748
merge remote branch back into local
myang1220 Jul 10, 2025
8413d43
Merge branch 'main' into user-data-interface
myang1220 Jul 10, 2025
dc38640
feat(save-user-data): push for help
myang1220 Jul 15, 2025
41abc96
feat(save-user-data): comment out nulls
myang1220 Jul 15, 2025
e90412e
feat(save-user-data): create a user data abstract class
myang1220 Jul 10, 2025
701adaf
fix: rm defaults and add semaphore to FileUserData
myang1220 Jul 10, 2025
daee65a
feat(save-user-data): push for help
myang1220 Jul 15, 2025
90573c7
feat(save-user-data): comment out nulls
myang1220 Jul 15, 2025
0e997ff
rebase main into branch
myang1220 Jul 15, 2025
5d39eb3
Merge branch 'main' of https://github.com/mongodb-js/compass into ext…
myang1220 Jul 15, 2025
5edf5e2
potentital org and group id retrieval
myang1220 Jul 17, 2025
729c6db
minor fixes
myang1220 Jul 17, 2025
055b898
unit testing
myang1220 Jul 18, 2025
fed4e96
fix package-lock.json merge conflict
myang1220 Jul 18, 2025
2058a32
typo
myang1220 Jul 18, 2025
296147a
package-lock.json
myang1220 Jul 18, 2025
d128447
merge origin back into branch
myang1220 Jul 18, 2025
18240da
fix: parse one by one in readAll
myang1220 Jul 21, 2025
31a8643
fix: change updateAttributes return type to boolean
myang1220 Jul 21, 2025
e88856f
stricter typing for type param and fix update tests
myang1220 Jul 22, 2025
3b8fc66
add getResourceUrl and change groupId to projectId
myang1220 Jul 22, 2025
7d42eef
merge main back into branch
myang1220 Jul 22, 2025
3c033fe
change wsBaseUrl to ccsBaseUrl in spec files
myang1220 Jul 22, 2025
ef94ff8
chore(user-data): remove redundant withStats methods
gribnoysup Jul 23, 2025
3f8405a
fix(user-data): remove nonexistent exports
gribnoysup Jul 23, 2025
7f07ddd
fix(my-queries-storage): adjust types to match the method behavior
gribnoysup Jul 23, 2025
e09d426
fix(saved-aggregations-queries): adjust test fixture
gribnoysup Jul 23, 2025
04ba804
put data type in IUserData class
myang1220 Jul 23, 2025
cf510ce
Merge branch 'main' into extend-user-data
myang1220 Jul 23, 2025
73b2bd1
fix(user-data): add datatype to IUserData abstract class & tests
myang1220 Jul 24, 2025
51ab01e
fix: merge stats change to branch
myang1220 Jul 24, 2025
df135f3
fix(user-data): make create and update return type to be boolean, lik…
myang1220 Jul 24, 2025
0bb77b8
pull remote branch back into local
myang1220 Jul 24, 2025
f6efcd6
fix(compass-aggregations): fix method typing
myang1220 Jul 24, 2025
2c0de81
feat: building demo for saving user data project
myang1220 Jul 29, 2025
3eab096
path segment and method binding
myang1220 Jul 30, 2025
61265d6
working demo
myang1220 Jul 31, 2025
59e2a40
merge
nvs119 Aug 15, 2025
8e5d686
restore again
nvs119 Aug 15, 2025
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
16 changes: 11 additions & 5 deletions packages/atlas-service/src/atlas-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,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)}`;
}
Expand All @@ -91,13 +96,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),
Expand Down
2 changes: 1 addition & 1 deletion packages/atlas-service/src/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class DataModelStorageElectron implements DataModelStorage {
private readonly userData: FileUserData<
typeof MongoDBDataModelDescriptionSchema
>;

constructor(basePath?: string) {
this.userData = new FileUserData(
MongoDBDataModelDescriptionSchema,
Expand All @@ -20,12 +21,15 @@ class DataModelStorageElectron implements DataModelStorage {
}
);
}

save(description: MongoDBDataModelDescription) {
return this.userData.write(description.id, description);
}

delete(id: MongoDBDataModelDescription['id']) {
return this.userData.delete(id);
}

async loadAll(): Promise<MongoDBDataModelDescription[]> {
try {
const res = await this.userData.readAll();
Expand All @@ -34,6 +38,7 @@ class DataModelStorageElectron implements DataModelStorage {
return [];
}
}

async load(id: string): Promise<MongoDBDataModelDescription | null> {
return (
(await this.loadAll()).find((item) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
7 changes: 4 additions & 3 deletions packages/compass-user-data/src/user-data.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import fs from 'fs/promises';
import os from 'os';
import path from 'path';
import { expect } from 'chai';
import { z, type ZodError } from 'zod';
import sinon from 'sinon';

import {
FileUserData,
AtlasUserData,
FileUserData,
type FileUserDataOptions,
} from './user-data';
import { z, type ZodError } from 'zod';
import sinon from 'sinon';

type ValidatorOptions = {
allowUnknownProps?: boolean;
Expand Down
157 changes: 85 additions & 72 deletions packages/compass-user-data/src/user-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export abstract class IUserData<T extends z.Schema> {
protected readonly dataType: string;
protected readonly serialize: SerializeContent<z.input<T>>;
protected readonly deserialize: DeserializeContent;

constructor(
validator: T,
dataType: string,
Expand All @@ -63,17 +64,25 @@ export abstract class IUserData<T extends z.Schema> {
}

abstract write(id: string, content: z.input<T>): Promise<boolean>;

abstract delete(id: string): Promise<boolean>;

abstract readAll(options?: ReadOptions): Promise<ReadAllResult<T>>;

abstract readOne(
id: string,
options?: ReadOptions
): Promise<z.output<T> | undefined>;
Copy link
Collaborator

Choose a reason for hiding this comment

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

When does this return undefined? I kinda would've expected this to return a version of ReadAllResult that either has a single item in the arrays or the properties are one item instead of an array

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Identified that readOne in AtlasUserData should not be returning null when an error is found. readOne in FileUserData originally had a return type of Promise<z.output<T> | undefined>, so the abstract method should also have type undefined.


abstract updateAttributes(
id: string,
data: Partial<z.input<T>>
): Promise<boolean>;
}

export class FileUserData<T extends z.Schema> extends IUserData<T> {
private readonly basePath?: string;
protected readonly semaphore = new Semaphore(100);
private readonly basePath?: string;

constructor(
validator: T,
Expand All @@ -84,79 +93,9 @@ export class FileUserData<T extends z.Schema> extends IUserData<T> {
this.basePath = basePath;
}

private getFileName(id: string) {
return `${id}.json`;
}

private async getEnsuredBasePath(): Promise<string> {
const basepath = this.basePath ? this.basePath : getStoragePath();

const root = path.join(basepath, this.dataType);

await fs.mkdir(root, { recursive: true });

return root;
}

private async getFileAbsolutePath(filepath?: string): Promise<string> {
const root = await this.getEnsuredBasePath();
const pathRelativeToRoot = path.relative(
root,
path.join(root, filepath ?? '')
);

if (
pathRelativeToRoot.startsWith('..') ||
path.isAbsolute(pathRelativeToRoot)
) {
throw new Error(
`Invalid file path: '${filepath}' is not a subpath of ${root}.`
);
}

return path.resolve(root, pathRelativeToRoot);
}

private async readAndParseFile(
absolutePath: string,
options: ReadOptions
): Promise<z.output<T> | undefined> {
let data: string;
let release: (() => void) | undefined = undefined;
try {
release = await this.semaphore.waitForRelease();
data = await fs.readFile(absolutePath, 'utf-8');
} catch (error) {
log.error(mongoLogId(1_001_000_234), 'Filesystem', 'Error reading file', {
path: absolutePath,
error: (error as Error).message,
});
if (options.ignoreErrors) {
return undefined;
}
throw error;
} finally {
release?.();
}

try {
const content = this.deserialize(data);
return this.validator.parse(content);
} catch (error) {
log.error(mongoLogId(1_001_000_235), 'Filesystem', 'Error parsing data', {
path: absolutePath,
error: (error as Error).message,
});
if (options.ignoreErrors) {
return undefined;
}
throw error;
}
}

async write(id: string, content: z.input<T>) {
// Validate the input. Here we are not saving the parsed content
// because after reading we validate the data again and it parses
// because after reading we validate the data again, and it parses
// the read content back to the expected output. This way we ensure
// that we exactly save what we want without transforming it.
this.validator.parse(content);
Expand Down Expand Up @@ -234,14 +173,17 @@ export class FileUserData<T extends z.Schema> extends IUserData<T> {
id: string,
options?: { ignoreErrors: false }
): Promise<z.output<T>>;

async readOne(
id: string,
options?: { ignoreErrors: true }
): Promise<z.output<T> | undefined>;

async readOne(
id: string,
options?: ReadOptions
): Promise<z.output<T> | undefined>;

async readOne(
id: string,
options: ReadOptions = {
Expand All @@ -267,6 +209,76 @@ export class FileUserData<T extends z.Schema> extends IUserData<T> {
return false;
}
}

private getFileName(id: string) {
return `${id}.json`;
}

private async getEnsuredBasePath(): Promise<string> {
const basepath = this.basePath ? this.basePath : getStoragePath();

const root = path.join(basepath, this.dataType);

await fs.mkdir(root, { recursive: true });

return root;
}

private async getFileAbsolutePath(filepath?: string): Promise<string> {
const root = await this.getEnsuredBasePath();
const pathRelativeToRoot = path.relative(
root,
path.join(root, filepath ?? '')
);

if (
pathRelativeToRoot.startsWith('..') ||
path.isAbsolute(pathRelativeToRoot)
) {
throw new Error(
`Invalid file path: '${filepath}' is not a subpath of ${root}.`
);
}

return path.resolve(root, pathRelativeToRoot);
}

private async readAndParseFile(
absolutePath: string,
options: ReadOptions
): Promise<z.output<T> | undefined> {
let data: string;
let release: (() => void) | undefined = undefined;
try {
release = await this.semaphore.waitForRelease();
data = await fs.readFile(absolutePath, 'utf-8');
} catch (error) {
log.error(mongoLogId(1_001_000_234), 'Filesystem', 'Error reading file', {
path: absolutePath,
error: (error as Error).message,
});
if (options.ignoreErrors) {
return undefined;
}
throw error;
} finally {
release?.();
}

try {
const content = this.deserialize(data);
return this.validator.parse(content);
} catch (error) {
log.error(mongoLogId(1_001_000_235), 'Filesystem', 'Error parsing data', {
path: absolutePath,
error: (error as Error).message,
});
if (options.ignoreErrors) {
return undefined;
}
throw error;
}
}
}

// TODO: update endpoints to reflect the merged api endpoints https://jira.mongodb.org/browse/CLOUDP-329716
Expand All @@ -275,6 +287,7 @@ export class AtlasUserData<T extends z.Schema> extends IUserData<T> {
private readonly getResourceUrl;
private orgId: string;
private projectId: string;

constructor(
validator: T,
dataType: string,
Expand Down
Loading
Loading