Skip to content

Commit dc38640

Browse files
committed
feat(save-user-data): push for help
1 parent 8413d43 commit dc38640

File tree

7 files changed

+262
-2
lines changed

7 files changed

+262
-2
lines changed

packages/atlas-service/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,13 @@
2828
],
2929
"license": "SSPL",
3030
"exports": {
31+
"./atlas-service": "./dist/atlas-service.js",
3132
"./main": "./main.js",
3233
"./renderer": "./renderer.js",
3334
"./provider": "./dist/provider.js"
3435
},
3536
"compass:exports": {
37+
"./atlas-service": "./src/atlas-service.ts",
3638
"./main": "./src/main.ts",
3739
"./renderer": "./src/renderer.ts",
3840
"./provider": "./src/provider.tsx"

packages/compass-global-writes/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { createLoggerLocator } from '@mongodb-js/compass-logging/provider';
88
import { telemetryLocator } from '@mongodb-js/compass-telemetry/provider';
99
import { connectionInfoRefLocator } from '@mongodb-js/compass-connections/provider';
1010
import { atlasServiceLocator } from '@mongodb-js/atlas-service/provider';
11+
export { AtlasGlobalWritesService } from './services/atlas-global-writes-service';
1112

1213
const CompassGlobalWritesPluginProvider = registerCompassPlugin(
1314
{

packages/compass-user-data/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@
5151
"dependencies": {
5252
"@mongodb-js/compass-logging": "^1.7.5",
5353
"@mongodb-js/compass-utils": "^0.9.4",
54+
"@mongodb-js/atlas-service": "^0.49.0",
55+
"@mongodb-js/compass-connections": "^0.31.0",
56+
"compass-preferences-model": "^2.44.0",
5457
"write-file-atomic": "^5.0.1",
5558
"zod": "^3.25.17"
5659
},
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export type { Stats, ReadAllResult, ReadAllWithStatsResult } from './user-data';
2-
export { IUserData, FileUserData } from './user-data';
2+
export { IUserData, FileUserData, AtlasUserData } from './user-data';
33
export { z } from 'zod';

packages/compass-user-data/src/user-data.spec.ts

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,3 +418,112 @@ describe('user-data', function () {
418418
});
419419
});
420420
});
421+
422+
describe('AtlasUserData', function () {
423+
let AtlasUserData: any;
424+
let atlasServiceMock: any;
425+
let validator: any;
426+
let instance: any;
427+
428+
before(async function () {
429+
// Dynamically import AtlasUserData to avoid circular deps
430+
const module = await import('./user-data');
431+
AtlasUserData = module.AtlasUserData;
432+
validator = getTestSchema();
433+
});
434+
435+
beforeEach(function () {
436+
atlasServiceMock = {
437+
authenticatedFetch: sinon.stub(),
438+
};
439+
instance = new AtlasUserData(validator, atlasServiceMock, {
440+
groupId: 'test-group',
441+
projectId: 'test-project',
442+
endpoint: '/api/user-data',
443+
});
444+
});
445+
446+
it('constructs with dependencies', function () {
447+
expect(instance).to.have.property('atlasService', atlasServiceMock);
448+
expect(instance).to.have.property('groupId', 'test-group');
449+
expect(instance).to.have.property('projectId', 'test-project');
450+
expect(instance).to.have.property('endpoint', '/api/user-data');
451+
});
452+
453+
it('write: calls authenticatedFetch and validates response', async function () {
454+
const item = { name: 'Atlas', hasDarkMode: false };
455+
atlasServiceMock.authenticatedFetch.resolves({
456+
ok: true,
457+
json: async () => await Promise.resolve(item),
458+
});
459+
const result = await instance.write('id1', item);
460+
expect(result).to.be.true;
461+
expect(atlasServiceMock.authenticatedFetch.calledOnce).to.be.true;
462+
});
463+
464+
it('write: handles API error', async function () {
465+
atlasServiceMock.authenticatedFetch.resolves({ ok: false, status: 500 });
466+
const result = await instance.write('id1', { name: 'Atlas' });
467+
expect(result).to.be.false;
468+
});
469+
470+
it('delete: calls authenticatedFetch and returns true on success', async function () {
471+
atlasServiceMock.authenticatedFetch.resolves({ ok: true });
472+
const result = await instance.delete('id1');
473+
expect(result).to.be.true;
474+
});
475+
476+
it('delete: returns false on API failure', async function () {
477+
atlasServiceMock.authenticatedFetch.resolves({ ok: false });
478+
const result = await instance.delete('id1');
479+
expect(result).to.be.false;
480+
});
481+
482+
it('readAll: returns validated items from API', async function () {
483+
const items = [
484+
{ name: 'Atlas', hasDarkMode: true },
485+
{ name: 'Compass', hasDarkMode: false },
486+
];
487+
atlasServiceMock.authenticatedFetch.resolves({
488+
ok: true,
489+
json: async () => await Promise.resolve(items),
490+
});
491+
const result = await instance.readAll();
492+
expect(result.data).to.have.lengthOf(2);
493+
expect(result.errors).to.have.lengthOf(0);
494+
expect(result.data[0]).to.have.property('name');
495+
});
496+
497+
it('readAll: returns errors for invalid items', async function () {
498+
const items = [
499+
{ name: 'Atlas', hasDarkMode: 'not-a-bool' },
500+
{ name: 'Compass', hasDarkMode: false },
501+
];
502+
atlasServiceMock.authenticatedFetch.resolves({
503+
ok: true,
504+
json: async () => await Promise.resolve(items),
505+
});
506+
const result = await instance.readAll();
507+
expect(result.data).to.have.lengthOf(1);
508+
expect(result.errors).to.have.lengthOf(1);
509+
});
510+
511+
it('updateAttributes: calls authenticatedFetch and validates response', async function () {
512+
const attrs = { hasDarkMode: false };
513+
atlasServiceMock.authenticatedFetch.resolves({
514+
ok: true,
515+
json: async () =>
516+
await Promise.resolve({ name: 'Atlas', hasDarkMode: false }),
517+
});
518+
const result = await instance.updateAttributes('id1', attrs);
519+
expect(result).to.deep.equal({ name: 'Atlas', hasDarkMode: false });
520+
});
521+
522+
it('updateAttributes: returns undefined on API error', async function () {
523+
atlasServiceMock.authenticatedFetch.resolves({ ok: false });
524+
const result = await instance.updateAttributes('id1', {
525+
hasDarkMode: true,
526+
});
527+
expect(result).to.be.undefined;
528+
});
529+
});

packages/compass-user-data/src/user-data.ts

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import { getStoragePath } from '@mongodb-js/compass-utils';
55
import type { z } from 'zod';
66
import writeFile from 'write-file-atomic';
77
import { Semaphore } from './semaphore';
8+
import { AtlasService } from '@mongodb-js/atlas-service/atlas-service';
9+
import { atlasAuthServiceLocator } from '@mongodb-js/atlas-service/provider';
10+
import { preferencesLocator } from 'compass-preferences-model/provider';
11+
import { connectionInfoRefLocator } from '@mongodb-js/compass-connections/provider';
812

913
const { log, mongoLogId } = createLogger('COMPASS-USER-STORAGE');
1014

@@ -333,3 +337,143 @@ export class FileUserData<T extends z.Schema> extends IUserData<T> {
333337
return await this.readOne(id);
334338
}
335339
}
340+
341+
// TODO: update endpoints to reflect the merged api endpoints
342+
export class AtlasUserData<T extends z.Schema> extends IUserData<T> {
343+
private atlasService: AtlasService;
344+
private readonly preferences = preferencesLocator();
345+
// private readonly preferences = null;
346+
private readonly connectionInfoRef = connectionInfoRefLocator();
347+
// private readonly connectionInfoRef = null;
348+
private readonly logger = createLogger('ATLAS-USER-STORAGE');
349+
private readonly groupId =
350+
this.connectionInfoRef?.current?.atlasMetadata?.projectId;
351+
private readonly orgId =
352+
this.connectionInfoRef?.current?.atlasMetadata?.orgId;
353+
// should this BASE_URL be a parameter passed to the constructor?
354+
// this might make future usage of this code easier, if we want to call a different endpoint
355+
private readonly BASE_URL = 'cluster-connection.cloud-local.mongodb.com';
356+
constructor(
357+
validator: T,
358+
{ serialize, deserialize }: AtlasUserDataOptions<z.input<T>>
359+
) {
360+
super(validator, { serialize, deserialize });
361+
const authService = atlasAuthServiceLocator();
362+
// const authService = null;
363+
const options = {};
364+
365+
this.atlasService = new AtlasService(
366+
authService,
367+
this.preferences,
368+
this.logger,
369+
options
370+
);
371+
this.atlasService = null;
372+
}
373+
374+
async write(id: string, content: z.input<T>): Promise<boolean> {
375+
this.validator.parse(content);
376+
377+
// do not need to use id because content already contains the id
378+
const response = await this.atlasService.authenticatedFetch(this.getUrl(), {
379+
method: 'POST',
380+
headers: {
381+
'Content-Type': 'application/json',
382+
},
383+
body: JSON.stringify({
384+
id: id,
385+
data: this.serialize(content),
386+
createdAt: new Date(),
387+
groupId: this.groupId,
388+
}),
389+
});
390+
// TODO: fix this error handling, should fit current compass needs
391+
if (!response.ok) {
392+
throw new Error(
393+
`Failed to post data: ${response.status} ${response.statusText}`
394+
);
395+
}
396+
return true;
397+
}
398+
399+
async delete(id: string): Promise<boolean> {
400+
const response = await this.atlasService.authenticatedFetch(
401+
this.getUrl() + `/${id}`,
402+
{
403+
method: 'DELETE',
404+
}
405+
);
406+
if (!response.ok) {
407+
throw new Error(
408+
`Failed to delete data: ${response.status} ${response.statusText}`
409+
);
410+
}
411+
return true;
412+
}
413+
414+
async readAll(): Promise<ReadAllResult<T>> {
415+
try {
416+
const response = await this.atlasService.authenticatedFetch(
417+
this.getUrl(),
418+
{
419+
method: 'GET',
420+
headers: {
421+
accept: 'application/json',
422+
},
423+
}
424+
);
425+
// TODO: fix this error handling, should fit current compass needs
426+
if (!response.ok) {
427+
throw new Error(
428+
`Failed to get data: ${response.status} ${response.statusText}`
429+
);
430+
}
431+
const json = await response.json();
432+
const data = Array.isArray(json)
433+
? json.map((item) =>
434+
this.validator.parse(this.deserialize(item.data as string))
435+
)
436+
: [];
437+
return { data, errors: [] };
438+
} catch (error) {
439+
return { data: [], errors: [error as Error] };
440+
}
441+
}
442+
443+
async updateAttributes(
444+
id: string,
445+
data: Partial<z.input<T>>
446+
): Promise<z.output<T>> {
447+
const response = await this.atlasService.authenticatedFetch(
448+
this.getUrl() + `/${id}`,
449+
{
450+
method: 'PUT',
451+
headers: {
452+
'Content-Type': 'application/json',
453+
},
454+
// TODO: not sure whether currently compass sometimes adds to data or always replaces it
455+
// figure out if we should get all data, find specific query by id, then update using JS
456+
body: this.serialize(data),
457+
}
458+
);
459+
if (!response.ok) {
460+
throw new Error(
461+
`Failed to update data: ${response.status} ${response.statusText}`
462+
);
463+
}
464+
const json = await response.json();
465+
// TODO: fix this, currently endpoint does not return the updated data
466+
// so we need to decide whether this is necessary
467+
return this.validator.parse(json.data);
468+
}
469+
470+
private getUrl() {
471+
// if (endpoint.startsWith('/')) {
472+
// endpoint = endpoint.slice(1);
473+
// }
474+
// if (endpoint.endsWith('/')) {
475+
// endpoint = endpoint.slice(0, -1);
476+
// }
477+
return `${this.BASE_URL}/queries/favorites/${this.orgId}/${this.groupId}`;
478+
}
479+
}

packages/compass-user-data/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
{
22
"extends": "@mongodb-js/tsconfig-compass/tsconfig.common.json",
33
"compilerOptions": {
4-
"outDir": "dist"
4+
"outDir": "dist",
5+
"skipLibCheck": true
56
},
67
"include": ["src/**/*"],
78
"exclude": ["./src/**/*.spec.*"]

0 commit comments

Comments
 (0)