Skip to content

Commit

Permalink
feat: store previous latest audit result (#74)
Browse files Browse the repository at this point in the history
* feat: store previous audit result

* fix: docs / optimization
  • Loading branch information
solaris007 authored Dec 26, 2023
1 parent 9a49e70 commit 4663c43
Show file tree
Hide file tree
Showing 9 changed files with 206 additions and 31 deletions.
80 changes: 60 additions & 20 deletions docs/API.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
## Constants

<dl>
<dt><a href="#createDataAccess">createDataAccess</a> ⇒ <code>object</code></dt>
<dd><p>Creates a data access object.</p>
</dd>
</dl>

## Functions

<dl>
<dt><a href="#createClient">createClient(log, dbClient, docClient)</a> ⇒ <code>Object</code></dt>
<dd><p>Creates a client object for interacting with DynamoDB.</p>
</dd>
<dt><a href="#createResponse">createResponse(body, status, headers)</a> ⇒ <code>Response</code></dt>
<dd><p>Creates a response with a JSON body. Defaults to 200 status.</p>
</dd>
<dt><a href="#isArray">isArray(value)</a> ⇒ <code>boolean</code></dt>
<dd><p>Determines if the given parameter is an array.</p>
</dd>
Expand Down Expand Up @@ -55,20 +50,14 @@ following UTC time offsets format.</p>
<dt><a href="#arrayEquals">arrayEquals(a, b)</a> ⇒ <code>boolean</code></dt>
<dd><p>Compares two arrays for equality. Supports primitive array item types only.</p>
</dd>
<dt><a href="#dateAfterDays">dateAfterDays(days)</a> ⇒ <code>Date</code></dt>
<dd><p>Calculates the date after a specified number of days from the current date.</p>
</dd>
<dt><a href="#resolveSecretsName">resolveSecretsName(opts, ctx, defaultPath)</a> ⇒ <code>string</code></dt>
<dd><p>Resolves the name of the secret based on the function version.</p>
</dd>
</dl>

<a name="createDataAccess"></a>

## createDataAccess ⇒ <code>object</code>
Creates a data access object.

**Kind**: global constant
**Returns**: <code>object</code> - data access object

| Param | Type | Description |
| --- | --- | --- |
| log | <code>Logger</code> | logger |

<a name="createClient"></a>

## createClient(log, dbClient, docClient) ⇒ <code>Object</code>
Expand All @@ -83,6 +72,20 @@ Creates a client object for interacting with DynamoDB.
| dbClient | <code>DynamoDB</code> | The AWS SDK DynamoDB client instance. |
| docClient | <code>DynamoDBDocument</code> | The AWS SDK DynamoDB Document client instance. |

<a name="createResponse"></a>

## createResponse(body, status, headers) ⇒ <code>Response</code>
Creates a response with a JSON body. Defaults to 200 status.

**Kind**: global function
**Returns**: <code>Response</code> - Response.

| Param | Type | Description |
| --- | --- | --- |
| body | <code>object</code> | JSON body. |
| status | <code>number</code> | Optional status code. |
| headers | <code>object</code> | Optional headers. |

<a name="isArray"></a>

## isArray(value) ⇒ <code>boolean</code>
Expand Down Expand Up @@ -248,3 +251,40 @@ Compares two arrays for equality. Supports primitive array item types only.
| a | <code>Array</code> | The first array to compare. |
| b | <code>Array</code> | The second array to compare. |

<a name="dateAfterDays"></a>

## dateAfterDays(days) ⇒ <code>Date</code>
Calculates the date after a specified number of days from the current date.

**Kind**: global function
**Returns**: <code>Date</code> - A new Date object representing the calculated date after the specified days.
**Throws**:

- <code>TypeError</code> If the provided 'days' parameter is not a number.
- <code>RangeError</code> If the calculated date is outside the valid JavaScript date range.


| Param | Type | Description |
| --- | --- | --- |
| days | <code>number</code> | The number of days to add to the current date. |

**Example**
```js
// Get the date 7 days from now
const sevenDaysLater = dateAfterDays(7);
console.log(sevenDaysLater); // Outputs a Date object representing the date 7 days from now
```
<a name="resolveSecretsName"></a>

## resolveSecretsName(opts, ctx, defaultPath) ⇒ <code>string</code>
Resolves the name of the secret based on the function version.

**Kind**: global function
**Returns**: <code>string</code> - - The resolved secret name.

| Param | Type | Description |
| --- | --- | --- |
| opts | <code>Object</code> | The options object, not used in this implementation. |
| ctx | <code>Object</code> | The context object containing the function version. |
| defaultPath | <code>string</code> | The default path for the secret. |

4 changes: 4 additions & 0 deletions packages/spacecat-shared-data-access/docs/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@
"AttributeName": "auditResult",
"AttributeType": "M"
},
{
"AttributeName": "previousAuditResult",
"AttributeType": "M"
},
{
"AttributeName": "expiresAt",
"AttributeType": "N"
Expand Down
15 changes: 12 additions & 3 deletions packages/spacecat-shared-data-access/src/dto/audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
* governing permissions and limitations under the License.
*/

import { isObject } from '@adobe/spacecat-shared-utils';

import { createAudit } from '../models/audit.js';

function parseEpochToDate(epochInSeconds) {
Expand All @@ -28,10 +30,10 @@ export const AuditDto = {
/**
* Converts an Audit object into a DynamoDB item.
* @param {Readonly<Audit>} audit - Audit object.
* @param {boolean} latestAudit - If true, returns the latest audit flavor.
* @param {boolean} isLatestAudit - If true, returns the latest audit flavor.
* @returns {{siteId, auditedAt, auditResult, auditType, expiresAt, fullAuditRef, SK: string}}
*/
toDynamoItem: (audit, latestAudit = false) => {
toDynamoItem: (audit, isLatestAudit = false) => {
const GSI1PK = 'ALL_LATEST_AUDITS';
let GSI1SK;

Expand All @@ -41,7 +43,12 @@ export const AuditDto = {
GSI1SK = `${audit.getAuditType()}#${Object.values(audit.getScores()).join('#')}`;
}

const latestAuditProps = latestAudit ? { GSI1PK, GSI1SK } : {};
const latestAuditProps = isLatestAudit ? {
GSI1PK,
GSI1SK,
...(isObject(audit.getPreviousAuditResult())
&& { previousAuditResult: audit.getPreviousAuditResult() }),
} : {};

return {
siteId: audit.getSiteId(),
Expand Down Expand Up @@ -70,6 +77,8 @@ export const AuditDto = {
expiresAt: parseEpochToDate(dynamoItem.expiresAt),
fullAuditRef: dynamoItem.fullAuditRef,
isLive: dynamoItem.isLive,
...(isObject(dynamoItem.previousAuditResult)
&& { previousAuditResult: dynamoItem.previousAuditResult }),
};

return createAudit(auditData);
Expand Down
13 changes: 13 additions & 0 deletions packages/spacecat-shared-data-access/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ export interface Audit {
*/
getAuditResult: () => object;

/**
* Retrieves the result of the previous audit.
* This serves for comparison purposes.
* @returns {object|null} The parsed audit result.
*/
getPreviousAuditResult: () => object | null;

/**
* Sets the result of the previous audit.
* @param {object} result The parsed audit result.
*/
setPreviousAuditResult: (result: object) => void;

/**
* Retrieves the type of the audit.
* @returns {object} The audit type.
Expand Down
17 changes: 17 additions & 0 deletions packages/spacecat-shared-data-access/src/models/audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ const validateScores = (auditResult, auditType) => {
return true;
}

if (!isObject(auditResult.scores)) {
throw new Error(`Missing scores property for audit type '${auditType}'`);
}

const expectedProperties = AUDIT_TYPE_PROPERTIES[auditType];
if (!expectedProperties) {
throw new Error(`Unknown audit type: ${auditType}`);
Expand Down Expand Up @@ -66,6 +70,11 @@ const Audit = (data = {}) => {
self.getFullAuditRef = () => self.state.fullAuditRef;
self.isLive = () => self.state.isLive;
self.isError = () => hasText(self.getAuditResult().runtimeError?.code);
self.getPreviousAuditResult = () => self.state.previousAuditResult;
self.setPreviousAuditResult = (previousAuditResult) => {
validateScores(previousAuditResult, self.getAuditType());
self.state.previousAuditResult = previousAuditResult;
};
self.getScores = () => self.getAuditResult().scores;

return Object.freeze(self);
Expand Down Expand Up @@ -98,6 +107,14 @@ export const createAudit = (data) => {

validateScores(data.auditResult, data.auditType);

if (data.previousAuditResult && !isObject(data.previousAuditResult)) {
throw new Error('Previous audit result must be an object');
}

if (data.previousAuditResult) {
validateScores(data.previousAuditResult, data.auditType);
}

if (!hasText(newState.fullAuditRef)) {
throw new Error('Full audit ref must be provided');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,25 +184,40 @@ export const addAudit = async (
log,
auditData,
) => {
const audit = createAudit(auditData);
const newAudit = createAudit(auditData);
const existingAudit = await getAuditForSite(
dynamoClient,
config,
log,
audit.getSiteId(),
audit.getAuditType(),
audit.getAuditedAt(),
newAudit.getSiteId(),
newAudit.getAuditType(),
newAudit.getAuditedAt(),
);

if (isObject(existingAudit)) {
throw new Error('Audit already exists');
}

const latestAudit = await getLatestAuditForSite(
dynamoClient,
config,
log,
newAudit.getSiteId(),
newAudit.getAuditType(),
);

if (isObject(latestAudit)) {
newAudit.setPreviousAuditResult(latestAudit.getAuditResult());
}

// TODO: Add transaction support
await dynamoClient.putItem(config.tableNameAudits, AuditDto.toDynamoItem(audit));
await dynamoClient.putItem(config.tableNameLatestAudits, AuditDto.toDynamoItem(audit, true));
await dynamoClient.putItem(config.tableNameAudits, AuditDto.toDynamoItem(newAudit));
await dynamoClient.putItem(
config.tableNameLatestAudits,
AuditDto.toDynamoItem(newAudit, true),
);

return audit;
return newAudit;
};

/**
Expand Down
21 changes: 21 additions & 0 deletions packages/spacecat-shared-data-access/test/it/db.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,27 @@ describe('DynamoDB Integration Test', async () => {
expect(latestAudit.getSiteId()).to.equal(auditData.siteId);
expect(latestAudit.getAuditType()).to.equal(auditData.auditType);
expect(latestAudit.getAuditedAt()).to.equal(auditData.auditedAt);

const additionalAuditData = {
siteId: 'https://example1.com',
auditType: AUDIT_TYPE_LHS_MOBILE,
auditedAt: new Date().toISOString(),
isLive: true,
fullAuditRef: 's3://ref',
auditResult: {
scores: {
performance: 1,
seo: 1,
accessibility: 1,
'best-practices': 1,
},
},
};

const anotherAudit = await dataAccess.addAudit(additionalAuditData);

checkAudit(anotherAudit);
expect(anotherAudit.getPreviousAuditResult()).to.deep.equal(newAudit.getAuditResult());
});

it('throws an error when adding a duplicate audit', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,21 @@ describe('Audit Model Tests', () => {
expect(() => createAudit({ ...validData, auditResult: 'not-an-object' })).to.throw('Audit result must be an object');
});

it('throws an error if previous audit result is not an object', () => {
expect(() => createAudit({ ...validData, previousAuditResult: 'not-an-object' }))
.to.throw('Previous audit result must be an object');
});

it('throws an error if previous audit result is missing scores property', () => {
expect(() => createAudit({ ...validData, previousAuditResult: {} }))
.to.throw('Missing scores property for audit type \'lhs-mobile\'');
});

it('throws an error if previous audit result is invalid', () => {
expect(() => createAudit({ ...validData, previousAuditResult: { scores: {} } }))
.to.throw('Missing expected property \'performance\' for audit type \'lhs-mobile\'');
});

it('throws an error if fullAuditRef is not provided', () => {
expect(() => createAudit({ ...validData, fullAuditRef: '' })).to.throw('Full audit ref must be provided');
});
Expand All @@ -62,6 +77,13 @@ describe('Audit Model Tests', () => {
expect(audit.getAuditType()).to.equal(validData.auditType.toLowerCase());
expect(audit.getAuditResult()).to.deep.equal(validData.auditResult);
expect(audit.getFullAuditRef()).to.equal(validData.fullAuditRef);
expect(audit.getPreviousAuditResult()).to.be.undefined;
});

it('throws an error when updating with invalid previous audit', () => {
const audit = createAudit(validData);

expect(() => audit.setPreviousAuditResult({})).to.throw('Missing scores property for audit type \'lhs-mobile\'');
});

it('automatically sets expiresAt if not provided', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ describe('Audit Access Pattern Tests', () => {
let exportedFunctions;

const auditData = {
siteId: 'siteId',
siteId: 'site1',
auditType: 'lhs-mobile',
auditedAt: new Date().toISOString(),
auditResult: {
Expand Down Expand Up @@ -204,6 +204,40 @@ describe('Audit Access Pattern Tests', () => {
expect(result.getAuditResult()).to.deep.equal(auditData.auditResult);
expect(result.getFullAuditRef()).to.equal(auditData.fullAuditRef);
expect(result.getScores()).to.be.an('object');
expect(result.getPreviousAuditResult()).to.be.undefined;
});

it('successfully adds a new audit with a previous audit result', async () => {
const auditResult = {
scores: {
performance: 0.2,
seo: 0.3,
accessibility: 0.4,
'best-practices': 0.5,
},
};
mockDynamoClient.getItem.withArgs(TEST_DA_CONFIG.tableNameLatestAudits, {
siteId: 'site1',
auditType: 'lhs-mobile',
}).resolves({ ...auditData, auditResult });

const result = await exportedFunctions.addAudit(auditData);

// Once for 'audits' and once for 'latest_audits'
expect(mockDynamoClient.putItem.calledTwice).to.be.true;
// Once for 'audits' and once for 'latest_audits'
expect(mockDynamoClient.getItem.calledTwice).to.be.true;
expect(result.getSiteId()).to.equal(auditData.siteId);
expect(result.getAuditType()).to.equal(auditData.auditType);
expect(result.getAuditedAt()).to.equal(auditData.auditedAt);
expect(result.getAuditResult()).to.deep.equal(auditData.auditResult);
expect(result.getFullAuditRef()).to.equal(auditData.fullAuditRef);
expect(result.getScores()).to.be.an('object');
expect(result.getPreviousAuditResult()).to.be.an('object');
expect(result.getPreviousAuditResult().scores.performance).to.equal(0.2);
expect(result.getPreviousAuditResult().scores.seo).to.equal(0.3);
expect(result.getPreviousAuditResult().scores.accessibility).to.equal(0.4);
expect(result.getPreviousAuditResult().scores['best-practices']).to.equal(0.5);
});

it('successfully adds an error audit', async () => {
Expand Down

0 comments on commit 4663c43

Please sign in to comment.