Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

first impl of db wrapper #5

Closed
wants to merge 16 commits into from
Closed
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ logs
.DS_Store
test-results.xml
.env
.idea/
7,541 changes: 5,162 additions & 2,379 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 10 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"semantic-release": "semantic-release",
"semantic-release-dry": "semantic-release --dry-run --no-ci --branches $CIRCLE_BRANCH",
"build": "hedy -v --test-bundle",
"deploy": "hedy -v --deploy --test",
"deploy": "hedy -v --deploy --test --aws-region=us-east-1",
"deploy-routes": "hedy --no-build -no-hints -l major",
"deploy-ci": "hedy -v --deploy --test --pkgVersion=ci$CIRCLE_BUILD_NUM -l ci --cleanup-ci=24h",
"deploy-secrets": "hedy --aws-update-secrets --params-file=secrets/secrets.env",
Expand Down Expand Up @@ -53,11 +53,17 @@
"reporter-options": "configFile=.mocha-multi.json"
},
"dependencies": {
"@adobe/fetch": "4.1.0",
"@adobe/helix-shared-secrets": "2.1.2",
"@adobe/fetch": "^4.1.0",
"@adobe/helix-shared-secrets": "^2.1.2",
"@adobe/helix-shared-wrap": "2.0.0",
"@adobe/helix-status": "10.0.10",
"@adobe/helix-universal-logger": "3.0.11"
"@adobe/helix-universal-logger": "3.0.11",
"@aws-sdk/client-dynamodb": "^3.427.0",
"@aws-sdk/client-s3": "^3.427.0",
"@aws-sdk/client-sqs": "^3.427.0",
"@aws-sdk/lib-dynamodb": "^3.427.0",
"axios": "1.5.1",
"esm": "3.2.25"
},
"devDependencies": {
"@adobe/eslint-config-helix": "2.0.3",
Expand Down
24 changes: 24 additions & 0 deletions src/db-wrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2023 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import createDynamoDBService from './db.js';
import serviceWrap from './service-wrap.js';

function wrapper(func) {
return (params) => serviceWrap(
func,
params,
'__ow_dynamodb',
createDynamoDBService,
);
}
const dynamoDBWrapper = wrapper;
export default { dynamoDBWrapper };
143 changes: 143 additions & 0 deletions src/db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* Copyright 2023 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetItemCommand, PutCommand } from '@aws-sdk/lib-dynamodb';
import { log } from './util.js';

const TABLE_SITES = 'spacecat-site';
const TABLE_AUDITS = 'spacecat-audit-index';

function DB(params) {
const client = new DynamoDBClient({ region: params.region });
const docClient = DynamoDBDocumentClient.from(client);

/**
* Save a record to the DynamoDB.
* @param {object} record - The new record to save.
* @param tableName - The name of the table to save the record to.
*/
async function saveRecord(record, tableName) {
try {
const command = new PutCommand({
TableName: tableName,
Item: record,
});
await docClient.send(command);
} catch (error) {
log('error', 'Error saving record: ', error);
}
}
/**
* Saves an audit to the DynamoDB.
* @param {object} site - Site object containing details of the audited site.
* @param {object} audit - Audit object containing the type and result of the audit.
* @returns {Promise<void>} Resolves once audit is saved.
*/
async function saveAuditIndex(site, audit) {
const now = new Date().toISOString();
const uuid = Date.now().toString();

const newAudit = {
id: uuid,
siteId: `${site.domain}/${site.path}`,
audit_date: now,
type: 'psi',
is_live: false,
content_publication_date: '',
git_hashes: [],
tag_manager: '',
error: '',
auditResults: [
{
strategy: 'mobile',
scores: {
performance: audit.result.mobile.categories.performance.score,
seo: audit.result.mobile.categories.seo.score,
'best-practices': audit.result.mobile.categories['best-practices'].score,
accessibility: audit.result.mobile.categories.accessibility.score,
},
},
{
strategy: 'desktop',
scores: {
performance: audit.result.desktop.categories.performance.score,
seo: audit.result.desktop.categories.seo.score,
'best-practices': audit.result.desktop.categories['best-practices'].score,
accessibility: audit.result.desktop.categories.accessibility.score,
},
},
],
};
log('info', `Audit for domain ${site.domain} saved successfully at ${now}`);
await saveRecord(newAudit, TABLE_AUDITS);
return newAudit;
}

/**
* Save an error that occurred during a Lighthouse audit to the DynamoDB.
* @param {object} site - site audited.
* @param {Error} error - The error that occurred during the audit.
*/
async function saveAuditError(site, error) {
const now = new Date().toISOString();
const newAudit = {
siteId: site.id,
auditDate: now,
isLive: site.isLive,
error: error.message,
scores: {},
};
await saveRecord(newAudit, TABLE_AUDITS);
}
/**
* Fetches a site by its ID and gets its latest audit.
* @param {string} domain - The domain of the site to fetch.
* @param {string} path - The path of the site to fetch.
* @returns {Promise<object>} Site document with its latest audit.
*/
async function getSite(domain, path) {
const commandParams = {
TableName: TABLE_SITES, // Replace with your table name
Key: {
Domain: { S: domain }, // Partition key
Path: { S: path }, // Sort key
},
};

try {
const command = new GetItemCommand(commandParams);
const response = await client.send(command);
const item = response.Item;
if (item) {
log('info', `Item retrieved successfully: ${item}`);
return item;
} else {
log('info', 'Item not found.');
return null;
}
} catch (error) {
log('error', `Error ${error}`);
throw error;
}
}
return {
getSite,
saveAuditIndex,
saveAuditError,
};
}

const createDynamoDBService = (params) => Object.freeze({
getInstance: () => DB(params),
});

export default { createDynamoDBService };
44 changes: 44 additions & 0 deletions src/edge-delivery-service-admin-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2023 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
const BASE_URL = 'https://www.hlx.live/';

/**
* Retrieve the status of the admin endpoint.
* @returns {Promise<string>} - The lastModification property.
* @throws {Error} - Throws an error if there's a network issue
* or some other error while fetching data.
*/
function EdgeDeliveryServiceAdminClient() {
const getLastModification = async () => {
try {
const response = await fetch(`${BASE_URL}docs/status.json`);
if (!response.ok) {
throw new Error(`Failed to fetch status: ${response.statusText}`);
}
const data = await response.json();
if (!data || !data.lastModification) {
throw new Error('lastModification property not found in response data');
}
return data.lastModification;
} catch (error) {
console.error('Error fetching lastModification:', error);
throw error;
}
};

// Return the function from within the EdgeDeliveryServiceAdminClient
return {
getLastModification,
};
}

export default EdgeDeliveryServiceAdminClient;
123 changes: 123 additions & 0 deletions src/github-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright 2023 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import axios from 'axios';
import { log } from './util.js';

const SECONDS_IN_A_DAY = 86400; // Number of seconds in a day

function GithubClient(config) {
const { baseUrl, githubId, githubSecret } = config;

/**
* Creates a URL for the GitHub API.
*
* @param {string} githubOrg - The name of the GitHub organization.
* @param {string} repoName - The name of the repository (optional).
* @param {string} path - Additional path (optional).
* @param {number} page - The page number for pagination (optional).
* @returns {string} The created GitHub API URL.
*/
function createGithubApiUrl(githubOrg, repoName = '', path = '', page = 1) {
const repoPart = repoName ? `/${repoName}` : '';
const pathPart = path ? `/${path}` : '';

return `${baseUrl}/repos/${githubOrg}${repoPart}${pathPart}?page=${page}&per_page=100`;
}

/**
* Creates a Basic Authentication header value from a given GitHub ID and secret.
*
* @returns {string} - The Basic Authentication header value.
* @throws {Error} - Throws an error if GitHub credentials are not provided.
*/
function createGithubAuthHeaderValue() {
if (!githubId || !githubSecret) {
throw new Error('GitHub credentials not provided');
}
return `Basic ${Buffer.from(`${githubId}:${githubSecret}`).toString('base64')}`;
}

/**
* Fetches the SHAs of all commits made in a GitHub repository between
* two date-times using the GitHub API.
*
* @async
* @function
* @param {object} domain - The domain of the audited site.
* @param {string} latestAuditTime - The end date-time in ISO format
* (e.g. 'YYYY-MM-DDTHH:mm:ss.sssZ').
* @param {string} lastAuditedAt - The start date-time in ISO format
* (e.g. 'YYYY-MM-DDTHH:mm:ss.sssZ').
* If not provided, it defaults to 24 hours before the end date-time.
* @param {string} gitHubURL - The URL of the GitHub repository from which the SHAs will be fetched (e.g. 'https://github.com/user/repo').
* @returns {Promise<string[]>} A promise that resolves to an array of SHAs
* of commits between the given date-times.
* If there's an error fetching the data, the promise resolves to an empty array.
* @throws {Error} Will throw an error if there's a network issue
* or some other error while fetching data from the GitHub API.
* @example
* fetchGithubCommitsSHA(
* { gitHubURL: 'https://github.com/myOrg/myRepo', lastAudited: '2023-06-15T00:00:00.000Z' },
* { result: { fetchTime: '2023-06-16T00:00:00.000Z' } },
* 'yourGithubId',
* 'yourGithubSecret'
* ).then(SHAs => console.log(SHAs));
*/
async function fetchGithubCommitsSHA(domain, latestAuditTime, lastAuditedAt, gitHubURL) {
if (!gitHubURL) {
log('info', `No github repo defined for site ${domain}. Skipping github SHA retrieval`);
return [];
}

try {
const until = new Date(latestAuditTime);
const since = lastAuditedAt
? new Date(lastAuditedAt)
: new Date(until - SECONDS_IN_A_DAY * 1000); // 24 hours before until
const repoPath = new URL(gitHubURL).pathname.slice(1); // Removes leading '/'

log('info', `Fetching SHAs for domain ${domain} with repo ${repoPath} between ${since.toISOString()} and ${until.toISOString()}`);

const [githubOrg, repoName] = repoPath.split('/');

const authHeader = createGithubAuthHeaderValue();
const commitsUrl = createGithubApiUrl(githubOrg, repoName, 'commits');

const response = await axios.get(commitsUrl, {
params: {
since: since.toISOString(),
until: until.toISOString(),
},
headers: {
Authorization: authHeader,
},
});

const commitSHAs = response.data.map((commit) => commit.sha);

log('info', `Found ${commitSHAs.length} commits for site ${domain}.`);

return commitSHAs;
} catch (error) {
log('error', `Error fetching GitHub SHAs for site ${domain}:`, error.response ? error.response.data : error);
return [];
}
}

return {
createGithubApiUrl,
createGithubAuthHeaderValue,
fetchGithubCommitsSHA,
};
}

export default GithubClient;
Loading
Loading