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

feat: init db wrapper #7

Closed
wants to merge 34 commits into from
Closed
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
057be2d
feat: init db wrapper
alinarublea Oct 19, 2023
9d0971b
feat: create wrapper for SQSClient
dzehnder Oct 19, 2023
95ff60d
feat: init db wrapper
alinarublea Oct 19, 2023
2418fb3
feat: init db wrapper
alinarublea Oct 19, 2023
63e21cc
feat: init db wrapper
alinarublea Oct 19, 2023
b71d19b
feat: init db wrapper
alinarublea Oct 19, 2023
e678ad8
feat: init db wrapper
alinarublea Oct 19, 2023
fabbd71
feat: init db wrapper
alinarublea Oct 19, 2023
83e34ce
fix: PR review
dzehnder Oct 19, 2023
2f90385
fix: PR review
dzehnder Oct 19, 2023
c15a214
feat: init db wrapper
alinarublea Oct 19, 2023
a4a362d
feat: init db wrapper
alinarublea Oct 19, 2023
e673748
feat: init db wrapper
alinarublea Oct 19, 2023
3271e0f
feat: init db wrapper
alinarublea Oct 19, 2023
0cbdaa6
fix: secret wrapper at end
dzehnder Oct 19, 2023
930c617
feat: update according to code review
alinarublea Oct 20, 2023
306bf56
feat: update according to code review x2
alinarublea Oct 20, 2023
9734949
feat: update according to code review x2
alinarublea Oct 20, 2023
221dfe4
feat: update according to code review x2
alinarublea Oct 20, 2023
912dd28
feat: update according to code review x2
alinarublea Oct 20, 2023
71285f7
feat: change SQS queue from functional to class component
dzehnder Oct 20, 2023
a05ac6f
fix: class usage
dzehnder Oct 20, 2023
db9a7d4
Merge branch 'sqs-wrapper' into init-db-wrapper
dzehnder Oct 20, 2023
42dee05
feat: update according to code review x2
alinarublea Oct 20, 2023
a873045
test: adding unit tests for queue wrapper
dzehnder Oct 20, 2023
56f314b
test: adding unit tests for sqs client
dzehnder Oct 24, 2023
06dc51f
feat: partially write tests and fix get site command
alinarublea Oct 24, 2023
e14cce0
feat: add more code coverage
alinarublea Oct 25, 2023
b4ec36c
feat: add more code coverage
alinarublea Oct 25, 2023
3a2180a
feat: remove codeql
alinarublea Oct 25, 2023
0360407
feat: remove codeql
alinarublea Oct 25, 2023
a506edf
feat: remove codeql
alinarublea Oct 26, 2023
f4992fb
feat: remove codeql
alinarublea Nov 13, 2023
efbf8ad
Update src/queue-wrapper.js
alinarublea Nov 13, 2023
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 @@ -9,3 +9,4 @@ logs
test-results.xml
.env
.idea/
.npmrc
32 changes: 12 additions & 20 deletions src/util.js → src/db-wrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,17 @@
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
const log = (level, message, ...args) => {
const timestamp = new Date().toISOString();

switch (level) {
case 'info':
console.info(`[${timestamp}] INFO: ${message}`, ...args);
break;
case 'error':
console.error(`[${timestamp}] ERROR: ${message}`, ...args);
break;
case 'warn':
console.warn(`[${timestamp}] WARN: ${message}`, ...args);
break;
default:
console.log(`[${timestamp}] ${message}`, ...args);
break;
}
};
'use strict';

Choose a reason for hiding this comment

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

not needed. ESM is always strict.

Suggested change
'use strict';


export {
log,
};
import DB from './db.js';

export default function dynamoDBWrapper(func) {
return async (request, context) => {
if (!context.db) {
context.db = new DB(context);
}
Comment on lines +17 to +19

Choose a reason for hiding this comment

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

  • what is the benefit of having this in a wrapper?
  • as mentioned earlier, do not cache directly in context, but in context.attributes
  • do you need to cleanup/close/discard the DB after use?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@tripodsan we were following the pattern from storage which is included directly in the context https://github.com/adobe/helix-universal/blob/7a63f8ddc179d5f91c1c170b40bf4bad4a17e769/src/aws-adapter.js#L158, and it s not an attribute. Sure we will cleanup after use.


return func(request, context);
};
}
85 changes: 42 additions & 43 deletions src/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,47 +11,51 @@
*/
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(config) {
const client = new DynamoDBClient({ region: config.region });
const docClient = DynamoDBDocumentClient.from(client);
export default class DB {
constructor(context) {
this.client = new DynamoDBClient({ region: context.runtime.region });
this.log = context.log;
this.docClient = DynamoDBDocumentClient.from(this.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) {
async saveRecord(record, tableName) {
try {
const command = new PutCommand({
TableName: tableName,
Item: record,
});
await docClient.send(command);
await this.docClient.send(command);
} catch (error) {
log('error', 'Error saving record: ', error);
this.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) {
* 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.

Choose a reason for hiding this comment

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

not true.... returns the new adit object

*/
async saveAuditIndex(site, audit) {
const now = new Date().toISOString();
const uuid = Date.now().toString();
const uuid = Date.now()
.toString();

const newAudit = {

Choose a reason for hiding this comment

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

maybe it would help to include a typedef for audit and site

either as jsdoc comment @typedef Audit ... or as typescript type

id: uuid,
siteId: site.id,
siteId: `${site.domain}/${site.path}`,
audit_date: now,
type: 'psi',
is_live: site.isLive,
is_live: false,
content_publication_date: '',
git_hashes: [],
tag_manager: '',
Expand All @@ -77,17 +81,17 @@ function DB(config) {
},
],
};
log('info', `Audit for domain ${site.domain} saved successfully at ${now}`);
await saveRecord(newAudit, TABLE_AUDITS);
return newAudit;
await this.saveRecord(newAudit, TABLE_AUDITS);
this.log.info(`Audit for domain ${site.domain} saved successfully at ${now}`);
return Promise.resolve(newAudit);
Copy link

@tripodsan tripodsan Oct 26, 2023

Choose a reason for hiding this comment

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

the return type of a async function is automatically a promise.

Suggested change
return Promise.resolve(newAudit);
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) {
* 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 saveAuditError(site, error) {
const now = new Date().toISOString();
const newAudit = {
siteId: site.id,
Expand All @@ -96,15 +100,17 @@ function DB(config) {
error: error.message,
scores: {},
};
await saveRecord(newAudit, TABLE_AUDITS);
await this.saveRecord(newAudit, TABLE_AUDITS);
}

/**
* Fetches a site by its ID and gets its latest audit.
* @param {string} siteId - The ID of the site to fetch.
* @returns {Promise<object>} Site document with its latest audit.
*/
async function getSite(domain, path) {
const params = {
* 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 getSite(domain, path) {
const commandParams = {
TableName: TABLE_SITES, // Replace with your table name
Key: {
Domain: { S: domain }, // Partition key
Expand All @@ -113,26 +119,19 @@ function DB(config) {
};

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

export default DB;
33 changes: 13 additions & 20 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2019 Adobe. All rights reserved.
* 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
Expand All @@ -11,11 +11,10 @@
*/
import secrets from '@adobe/helix-shared-secrets';
import wrap from '@adobe/helix-shared-wrap';
import { logger } from '@adobe/helix-universal-logger';
import { helixStatus } from '@adobe/helix-status';
import SQSQueue from './sqs-queue.js';
import DB from './db.js'; // Assuming the exported content of './db' is default exported
import PSIClient from './psi-client.js'; // Assuming the exported content of './psi-client' is default exported
import dynamoDBWrapper from './db-wrapper.js';
import PSIClient from './psi-client.js';
import queueWrapper from './queue-wrapper.js';

/**
* This is the main function
Expand All @@ -24,32 +23,26 @@ import PSIClient from './psi-client.js'; // Assuming the exported content of './
* @returns {Response} a response
*/
async function run(request, context) {
const db = DB({
region: process.env.REGION,
});
const sqsQueue = SQSQueue();
const { message } = JSON.parse(context.invocation.event.Records[0].body);
const { db, queue } = context;
const message = JSON.parse(context.invocation.event.Records[0].body);

const psiClient = PSIClient({
apiKey: process.env.PAGESPEED_API_KEY,
baseUrl: process.env.PAGESPEED_API_BASE_URL,
apiKey: context.env.PAGESPEED_API_KEY,
baseUrl: context.env.PAGESPEED_API_BASE_URL,
});

const site = {
id: message.siteId,
githubURL: message.githubURL,
domain: message.domain,
path: message.path,
isLive: message.isLive,
};
const auditResult = await psiClient.runAudit(`https://${site.domain}/${site.path}`);
const auditResultMin = await db.saveAuditIndex(site, auditResult);
await sqsQueue.sendMessage(auditResultMin);
await queue.sendAuditResult(auditResultMin);
return new Response('SUCCESS');
}

export const main = wrap(run)
.with(helixStatus)
.with(logger.trace)
.with(logger)
.with(secrets);
.with(dynamoDBWrapper)
.with(queueWrapper)
.with(secrets)
.with(helixStatus);
7 changes: 1 addition & 6 deletions src/psi-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
* governing permissions and limitations under the License.
*/
import axios from 'axios';

Choose a reason for hiding this comment

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

would be cool to use @adobe/fetch

import { log } from './util.js';

function PSIClient(config) {
const AUDIT_TYPE = 'PSI';
Expand Down Expand Up @@ -136,6 +135,7 @@ function PSIClient(config) {
* @returns {Promise<object>} The processed PageSpeed Insights audit data.
*/
const performPSICheck = async (domain, strategy) => {
// eslint-disable-next-line no-useless-catch

Choose a reason for hiding this comment

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

why not follow the eslint advice here ? :-)

try {
const apiURL = getPSIApiUrl(domain, strategy);
const { data: lhs } = await axios.get(apiURL);
Expand All @@ -144,7 +144,6 @@ function PSIClient(config) {

return processLighthouseResult(lighthouseResult);
} catch (e) {
log('error', `Error happened during PSI check: ${e}`);
throw e;
}
};
Expand All @@ -153,12 +152,8 @@ function PSIClient(config) {
const auditResults = {};

for (const strategy of PSI_STRATEGIES) {
const strategyStartTime = process.hrtime();
// eslint-disable-next-line no-await-in-loop
const psiResult = await performPSICheck(domain, strategy);
const strategyEndTime = process.hrtime(strategyStartTime);
const strategyElapsedTime = (strategyEndTime[0] + strategyEndTime[1] / 1e9).toFixed(2);
log('info', `Audited ${domain} for ${strategy} strategy in ${strategyElapsedTime} seconds`);

auditResults[strategy] = psiResult;
}
Expand Down
33 changes: 33 additions & 0 deletions src/queue-wrapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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.
*/

'use strict';
alinarublea marked this conversation as resolved.
Show resolved Hide resolved

import SqsQueue from './sqs-queue.js';

export default function queueWrapper(func) {
return async (request, context) => {
const queueUrl = context.env.AUDIT_RESULTS_QUEUE_URL;

if (!queueUrl) {
throw new Error('AUDIT_RESULTS_QUEUE_URL env variable is empty/not provided');
}

context.attributes.queueUrl = queueUrl;

if (!context.queue) {
context.queue = new SqsQueue(context);
}
Comment on lines +26 to +28

Choose a reason for hiding this comment

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

also use context.attributes


return func(request, context);

Choose a reason for hiding this comment

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

probably close/discard queue client ?

};
}
45 changes: 21 additions & 24 deletions src/sqs-queue.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,47 +9,44 @@
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs';
import { SendMessageCommand, SQSClient } from '@aws-sdk/client-sqs';

// Set up the region
const REGION = 'us-east-1'; // change this to your desired region
/**
* @class SQSQueue class to send audit results to SQS
* @param {string} region - AWS region
* @param {string} queueUrl - SQS queue URL
* @param {object} log - OpenWhisk log object
*/
export default class SQSQueue {
constructor(context) {
const { region, log } = context;
const { queueUrl } = context.attributes;

// Create SQS service client object
const sqsClient = new SQSClient({ region: REGION });
this.queueUrl = queueUrl;
this.log = log;

// Your SQS queue URL
const queueURL = 'https://sqs.us-east-1.amazonaws.com/282898975672/spacecat-audit-results';
this.sqsClient = new SQSClient({ region });
log.info(`Creating SQS client in region ${region}`);
}

function SQSQueue() {
async function sendMessage(message) {
async sendAuditResult(message) {
const body = {
message,
timestamp: new Date().toISOString(),
};

// Set up the parameters for the send message command
const params = {
DelaySeconds: 10,
Copy link
Member

Choose a reason for hiding this comment

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

what is this delay for?

MessageBody: JSON.stringify(body),
QueueUrl: queueURL,
QueueUrl: this.queueUrl,
};

try {
const data = await sqsClient.send(new SendMessageCommand(params));
console.log('Success, message sent. MessageID:', data.MessageId);
const data = await this.sqsClient.send(new SendMessageCommand(params));
this.log.info(`Success, message sent. MessageID: ${data.MessageId}`);
} catch (err) {
console.log('Error:', err);
this.log.error(`Error: ${err}`);
throw err;
}

return {
statusCode: 200,
body: JSON.stringify({ message: 'SQS message sent!' }),
};
}
return {
sendMessage,
};
}

export default SQSQueue;
Loading
Loading