Skip to content
This repository was archived by the owner on Aug 6, 2025. It is now read-only.

Commit c81785e

Browse files
authored
DOP-3508: Save successful builds (#779)
* add version data to atlas fetch, create save util * add check for consistent data with saved version data * added gitignore, implemented flags for successful builds with githash, invoked save util to db * created createFetchGitHash with cache * further modularized, added ensureSavedVersionDataMatches util * added mocks and assertions to pageBuilder.test.ts * reset db env, created const for githash * move totalSuccess instantiation into closer scope * PR feedback * deleted comment
1 parent b4c02c7 commit c81785e

File tree

6 files changed

+154
-18
lines changed

6 files changed

+154
-18
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.env

modules/oas-page-builder/src/services/database.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Db, MongoClient } from 'mongodb';
2-
import { OASFile, OASFileGitHash } from './models/OASFile';
2+
import { OASFile, OASFilePartial, VersionData } from './models/OASFile';
33

44
const COLLECTION_NAME = 'oas_files';
55

@@ -49,17 +49,42 @@ const db = async () => {
4949
return dbInstance;
5050
};
5151

52-
// Finds the last saved git hash in our db for an OpenAPI spec file. This git hash
52+
// Finds the last saved git hash and version data in our db for an OpenAPI spec file. The git hash
5353
// should have an existing spec file but the hash may be subject to change every 24 hours.
54-
export const findLastSavedGitHash = async (apiKeyword: string) => {
54+
export const findLastSavedVersionData = async (apiKeyword: string) => {
5555
const dbSession = await db();
5656
try {
57-
const projection = { gitHash: 1 };
57+
const projection = { gitHash: 1, versions: 1 };
5858
const filter = { api: apiKeyword };
5959
const oasFilesCollection = dbSession.collection<OASFile>(COLLECTION_NAME);
60-
return oasFilesCollection.findOne<OASFileGitHash>(filter, { projection });
60+
return oasFilesCollection.findOne<OASFilePartial>(filter, { projection });
6161
} catch (error) {
6262
console.error(`Error fetching lastest git hash for API: ${apiKeyword}.`);
6363
throw error;
6464
}
6565
};
66+
67+
export const saveSuccessfulBuildVersionData = async (apiKeyword: string, gitHash: string, versionData: VersionData) => {
68+
const dbSession = await db();
69+
try {
70+
const query = {
71+
api: apiKeyword,
72+
};
73+
const update = {
74+
$set: {
75+
gitHash: gitHash,
76+
versions: versionData,
77+
lastUpdated: new Date().toISOString(),
78+
},
79+
};
80+
const options = {
81+
upsert: true,
82+
};
83+
84+
const oasFilesCollection = dbSession.collection<OASFile>(COLLECTION_NAME);
85+
await oasFilesCollection.updateOne(query, update, options);
86+
} catch (error) {
87+
console.error(`Error updating lastest git hash and versions for API: ${apiKeyword}.`);
88+
throw error;
89+
}
90+
};
Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
// Model for documents in the "oas_files" collection.
2+
3+
export interface VersionData {
4+
[k: string]: string[];
5+
}
6+
27
export interface OASFile {
38
api: string;
49
fileContent: string;
510
gitHash: string;
11+
lastUpdated: string;
12+
versions: VersionData;
613
}
714

8-
export type OASFileGitHash = Pick<OASFile, 'gitHash'>;
15+
export type OASFilePartial = Pick<OASFile, 'gitHash' | 'versions'>;

modules/oas-page-builder/src/services/pageBuilder.ts

Lines changed: 89 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import fetch from 'node-fetch';
22
import { normalizePath } from '../utils/normalizePath';
3+
import { fetchVersionData } from '../utils/fetchVersionData';
34
import { RedocExecutor } from './redocExecutor';
4-
import { findLastSavedGitHash } from './database';
5+
import { findLastSavedVersionData, saveSuccessfulBuildVersionData } from './database';
56
import { OASPageMetadata, PageBuilderOptions, RedocBuildOptions } from './types';
7+
import { VersionData } from './models/OASFile';
68

79
const OAS_FILE_SERVER = 'https://mongodb-mms-prod-build-server.s3.amazonaws.com/openapi/';
10+
const GIT_HASH_URL = 'https://cloud.mongodb.com/version';
811

912
const fetchTextData = async (url: string, errMsg: string) => {
1013
const res = await fetch(url);
@@ -15,12 +18,44 @@ const fetchTextData = async (url: string, errMsg: string) => {
1518
return res.text();
1619
};
1720

21+
const createFetchGitHash = () => {
22+
let gitHashCache: string;
23+
return {
24+
fetchGitHash: async () => {
25+
if (gitHashCache) return gitHashCache;
26+
try {
27+
const gitHash = await fetchTextData(GIT_HASH_URL, 'Could not find current version or git hash');
28+
gitHashCache = gitHash;
29+
return gitHash;
30+
} catch (e) {
31+
console.error(e);
32+
throw new Error(`Unsuccessful git hash fetch`);
33+
}
34+
},
35+
resetGitHashCache: () => {
36+
gitHashCache = '';
37+
},
38+
};
39+
};
40+
41+
const { fetchGitHash, resetGitHashCache } = createFetchGitHash();
42+
1843
interface AtlasSpecUrlParams {
1944
apiKeyword: string;
2045
apiVersion?: string;
2146
resourceVersion?: string;
2247
}
2348

49+
const ensureSavedVersionDataMatches = (versions: VersionData, apiVersion?: string, resourceVersion?: string) => {
50+
// Check that requested versions are included in saved version data
51+
if (apiVersion) {
52+
if (!versions.major.includes(apiVersion) || (resourceVersion && !versions[apiVersion].includes(resourceVersion))) {
53+
throw new Error(`Last successful build data does not include necessary version data:\n
54+
Version requested: ${apiVersion}${resourceVersion ? ` - ${resourceVersion}` : ``}`);
55+
}
56+
}
57+
};
58+
2459
const getAtlasSpecUrl = async ({ apiKeyword, apiVersion, resourceVersion }: AtlasSpecUrlParams) => {
2560
// Currently, the only expected API fetched programmatically is the Cloud Admin API,
2661
// but it's possible to have more in the future with varying processes.
@@ -34,10 +69,10 @@ const getAtlasSpecUrl = async ({ apiKeyword, apiVersion, resourceVersion }: Atla
3469
}`;
3570

3671
let oasFileURL;
72+
let successfulGitHash = true;
3773

3874
try {
39-
const versionURL = 'https://cloud.mongodb.com/version';
40-
const gitHash = await fetchTextData(versionURL, 'Could not find current version or git hash');
75+
const gitHash = await fetchGitHash();
4176
oasFileURL = `${OAS_FILE_SERVER}${gitHash}${versionExtension}.json`;
4277

4378
// Sometimes the latest git hash might not have a fully available spec file yet.
@@ -46,17 +81,22 @@ const getAtlasSpecUrl = async ({ apiKeyword, apiVersion, resourceVersion }: Atla
4681
await fetchTextData(oasFileURL, `Error fetching data from ${oasFileURL}`);
4782
} catch (e) {
4883
console.error(e);
84+
successfulGitHash = false;
4985

50-
const res = await findLastSavedGitHash(apiKeyword);
86+
const res = await findLastSavedVersionData(apiKeyword);
5187
if (res) {
88+
ensureSavedVersionDataMatches(res.versions, apiVersion, resourceVersion);
5289
oasFileURL = `${OAS_FILE_SERVER}${res.gitHash}${versionExtension}.json`;
5390
console.log(`Using ${oasFileURL}`);
5491
} else {
5592
throw new Error(`Could not find a saved hash for API: ${apiKeyword}`);
5693
}
5794
}
5895

59-
return oasFileURL;
96+
return {
97+
oasFileURL,
98+
successfulGitHash,
99+
};
60100
};
61101

62102
interface GetOASpecParams {
@@ -82,14 +122,21 @@ async function getOASpec({
82122
}: GetOASpecParams) {
83123
try {
84124
let spec = '';
125+
let isSuccessfulBuild = true;
85126
const buildOptions: RedocBuildOptions = {};
86127
if (sourceType === 'url') {
87128
spec = source;
88129
} else if (sourceType === 'local') {
89130
const localFilePath = normalizePath(`${repoPath}/source/${source}`);
90131
spec = localFilePath;
91132
} else if (sourceType === 'atlas') {
92-
spec = await getAtlasSpecUrl({ apiKeyword: source, apiVersion, resourceVersion });
133+
const { oasFileURL, successfulGitHash } = await getAtlasSpecUrl({
134+
apiKeyword: source,
135+
apiVersion,
136+
resourceVersion,
137+
});
138+
spec = oasFileURL;
139+
isSuccessfulBuild = successfulGitHash;
93140
// Ignore "incompatible types" warnings for Atlas Admin API/cloud-docs
94141

95142
buildOptions['ignoreIncompatibleTypes'] = true;
@@ -102,8 +149,10 @@ async function getOASpec({
102149
const path = `${output}/${pageSlug}${filePathExtension}/index.html`;
103150
const finalFilename = normalizePath(path);
104151
await redocExecutor.execute(spec, finalFilename, buildOptions);
152+
return isSuccessfulBuild;
105153
} catch (e) {
106154
console.error(e);
155+
return false;
107156
}
108157
}
109158

@@ -114,6 +163,7 @@ export const buildOpenAPIPages = async (
114163
const redocExecutor = new RedocExecutor(redocPath, siteUrl, siteTitle);
115164

116165
for (const [pageSlug, data] of entries) {
166+
let totalSuccess = true;
117167
const { source_type: sourceType, source, api_version: apiVersion, resource_versions: resourceVersions } = data;
118168

119169
if (!apiVersion && resourceVersions && resourceVersions.length > 0) {
@@ -127,10 +177,41 @@ export const buildOpenAPIPages = async (
127177
// if a resource versions array is provided, then we can loop through the resourceVersions array and call the getOASpec
128178
// for each minor version
129179
for (const resourceVersion of resourceVersions) {
130-
await getOASpec({ source, sourceType, output, pageSlug, redocExecutor, repoPath, apiVersion, resourceVersion });
180+
const isSuccessfulBuild = await getOASpec({
181+
source,
182+
sourceType,
183+
output,
184+
pageSlug,
185+
redocExecutor,
186+
repoPath,
187+
apiVersion,
188+
resourceVersion,
189+
});
190+
if (!isSuccessfulBuild) totalSuccess = false;
131191
}
132192
}
133193
// apiVersion can be undefined, this case is handled within the getOASpec function
134-
await getOASpec({ source, sourceType, output, pageSlug, redocExecutor, repoPath, apiVersion });
194+
const isSuccessfulBuild = await getOASpec({
195+
source,
196+
sourceType,
197+
output,
198+
pageSlug,
199+
redocExecutor,
200+
repoPath,
201+
apiVersion,
202+
});
203+
if (!isSuccessfulBuild) totalSuccess = false;
204+
205+
// If all builds successful, persist git hash and version data in db
206+
if (totalSuccess && sourceType == 'atlas') {
207+
try {
208+
const gitHash = await fetchGitHash();
209+
const versions = await fetchVersionData(gitHash);
210+
await saveSuccessfulBuildVersionData(source, gitHash, versions);
211+
} catch (e) {
212+
console.error(e);
213+
}
214+
}
215+
resetGitHashCache();
135216
}
136217
};
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const fetchVersionData = async (gitHash: string) => {
2+
const versionUrl = `https://mongodb-mms-prod-build-server.s3.amazonaws.com/openapi/${gitHash}-api-versions.json`;
3+
const res = await fetch(versionUrl);
4+
const { versions } = await res.json();
5+
return versions;
6+
};

modules/oas-page-builder/tests/unit/services/pageBuilder.test.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import fetch from 'node-fetch';
2-
import { findLastSavedGitHash } from '../../../src/services/database';
2+
import { findLastSavedVersionData, saveSuccessfulBuildVersionData } from '../../../src/services/database';
33
import { buildOpenAPIPages } from '../../../src/services/pageBuilder';
44
import { OASPageMetadata, PageBuilderOptions } from '../../../src/services/types';
5+
import { fetchVersionData } from '../../../src/utils/fetchVersionData';
56

67
const MOCKED_GIT_HASH = '1234';
78
const LAST_SAVED_GIT_HASH = '4321';
@@ -17,7 +18,13 @@ jest.mock('../../../src/services/redocExecutor', () => ({
1718
// Mock database since implementation relies on database instance. Returned values
1819
// are mocked for each test.
1920
jest.mock('../../../src/services/database', () => ({
20-
findLastSavedGitHash: jest.fn(),
21+
findLastSavedVersionData: jest.fn(),
22+
saveSuccessfulBuildVersionData: jest.fn(),
23+
}));
24+
25+
// Mock version data fetch to override mocked node-fetch
26+
jest.mock('../../../src/utils/fetchVersionData', () => ({
27+
fetchVersionData: jest.fn(),
2128
}));
2229

2330
// Helper function for concatenated output path
@@ -50,6 +57,7 @@ describe('pageBuilder', () => {
5057
beforeEach(() => {
5158
// Reset mock to reset call count
5259
mockExecute.mockReset();
60+
jest.clearAllMocks();
5361
});
5462

5563
it('builds OpenAPI pages', async () => {
@@ -130,6 +138,9 @@ describe('pageBuilder', () => {
130138
});
131139

132140
it('builds OpenAPI pages with api version and resource version', async () => {
141+
const expectedVersionData = { major: ['1.0', '2.0'], '2.0': ['01-01-2020'] };
142+
// @ts-ignore
143+
fetchVersionData.mockReturnValue(expectedVersionData);
133144
mockFetchImplementation(true);
134145

135146
const testEntries: [string, OASPageMetadata][] = [
@@ -191,12 +202,15 @@ describe('pageBuilder', () => {
191202
getExpectedOutputPath(testOptions.output, testEntries[2][0], '2.0'),
192203
expectedAtlasBuildOptions
193204
);
205+
206+
expect(saveSuccessfulBuildVersionData).toBeCalledTimes(1);
207+
expect(saveSuccessfulBuildVersionData).toBeCalledWith('cloud', MOCKED_GIT_HASH, expectedVersionData);
194208
});
195209

196210
it('builds Atlas Cloud API with backup git hash', async () => {
197211
mockFetchImplementation(false);
198212
// @ts-ignore
199-
findLastSavedGitHash.mockReturnValue({ gitHash: LAST_SAVED_GIT_HASH });
213+
findLastSavedVersionData.mockReturnValue({ gitHash: LAST_SAVED_GIT_HASH });
200214

201215
const testEntries: [string, OASPageMetadata][] = [['path/to/page/1', { source_type: 'atlas', source: 'cloud' }]];
202216

@@ -206,16 +220,18 @@ describe('pageBuilder', () => {
206220
getExpectedOutputPath(testOptions.output, testEntries[0][0]),
207221
expectedAtlasBuildOptions
208222
);
223+
expect(saveSuccessfulBuildVersionData).toBeCalledTimes(0);
209224
});
210225

211226
it('does not build atlas OAS when backup git hash is missing', async () => {
212227
mockFetchImplementation(false);
213228
// @ts-ignore
214-
findLastSavedGitHash.mockReturnValue(null);
229+
findLastSavedVersionData.mockReturnValue(null);
215230

216231
const testEntries: [string, OASPageMetadata][] = [['path/to/page/1', { source_type: 'atlas', source: 'cloud' }]];
217232

218233
await buildOpenAPIPages(testEntries, testOptions);
219234
expect(mockExecute).toBeCalledTimes(0);
235+
expect(saveSuccessfulBuildVersionData).toBeCalledTimes(0);
220236
});
221237
});

0 commit comments

Comments
 (0)