From 4c83051dfbe67509fc22671a511aa0e595a78687 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Wed, 29 Nov 2023 09:41:53 +0100
Subject: [PATCH 01/28] feat: add spacecat-shared-data-access package
---
package-lock.json | 36 ++-
.../spacecat-shared-data-access/.jsdoc.json | 17 ++
.../.mocha-multi.json | 6 +
.../spacecat-shared-data-access/.npmignore | 9 +
.../spacecat-shared-data-access/.nycrc.json | 10 +
.../spacecat-shared-data-access/CHANGELOG.md | 0
.../spacecat-shared-data-access/LICENSE.txt | 264 ++++++++++++++++++
.../spacecat-shared-data-access/README.md | 2 +
.../spacecat-shared-data-access/package.json | 37 +++
packages/spacecat-shared-dynamo/package.json | 2 +-
10 files changed, 367 insertions(+), 16 deletions(-)
create mode 100644 packages/spacecat-shared-data-access/.jsdoc.json
create mode 100644 packages/spacecat-shared-data-access/.mocha-multi.json
create mode 100644 packages/spacecat-shared-data-access/.npmignore
create mode 100644 packages/spacecat-shared-data-access/.nycrc.json
create mode 100644 packages/spacecat-shared-data-access/CHANGELOG.md
create mode 100644 packages/spacecat-shared-data-access/LICENSE.txt
create mode 100644 packages/spacecat-shared-data-access/README.md
create mode 100644 packages/spacecat-shared-data-access/package.json
diff --git a/package-lock.json b/package-lock.json
index 839be8fe9..b37bce2d0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,7 +29,7 @@
"nock": "13.3.8",
"semantic-release": "19.0.5",
"semantic-release-monorepo": "7.0.5",
- "typescript": "^5.3.2"
+ "typescript": "5.3.2"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -89,12 +89,15 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@adobe/helix-universal/-/helix-universal-4.4.1.tgz",
"integrity": "sha512-YJKiuzeAZvzkardlAVcyW/PYNzKfIf/3v7Dd22wxvimmI3gct4k3V39gUSVrP+sWZPvlBS67WMNCv28n0eKjWg==",
- "optional": true,
"dependencies": {
"@adobe/fetch": "4.1.1",
"aws4": "1.12.0"
}
},
+ "node_modules/@adobe/spacecat-shared-data-access": {
+ "resolved": "packages/spacecat-shared-data-access",
+ "link": true
+ },
"node_modules/@adobe/spacecat-shared-dynamo": {
"resolved": "packages/spacecat-shared-dynamo",
"link": true
@@ -12097,12 +12100,22 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "packages/spacecat-shared-data-access": {
+ "version": "1.1.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@adobe/spacecat-shared-utils": "1.1.0"
+ },
+ "devDependencies": {
+ "chai": "4.3.10"
+ }
+ },
"packages/spacecat-shared-dynamo": {
"name": "@adobe/spacecat-shared-dynamo",
- "version": "1.0.0",
+ "version": "1.1.0",
"license": "Apache-2.0",
"dependencies": {
- "@adobe/spacecat-shared-utils": "1.0.1",
+ "@adobe/spacecat-shared-utils": "1.1.0",
"@aws-sdk/client-dynamodb": "3.454.0",
"@aws-sdk/lib-dynamodb": "3.454.0"
},
@@ -12110,28 +12123,21 @@
"chai": "4.3.10"
}
},
- "packages/spacecat-shared-dynamo/node_modules/@adobe/spacecat-shared-utils": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@adobe/spacecat-shared-utils/-/spacecat-shared-utils-1.0.1.tgz",
- "integrity": "sha512-6HiCywan5y9jpbFYkQX87Vg0q5dXoJf2m6MiDvNBEJdREdUxFwE+oYA6Fr86qqd32ECnCeYCFO13HcNRn1RxkQ=="
- },
"packages/spacecat-shared-example": {
"name": "@adobe/spacecat-shared-example",
- "version": "1.0.0",
+ "version": "1.1.0",
"license": "Apache-2.0",
"dependencies": {
"@adobe/fetch": "4.1.1",
"@adobe/helix-shared-wrap": "2.0.0",
+ "@adobe/helix-universal": "4.4.1",
"aws4": "1.12.0"
},
- "devDependencies": {},
- "optionalDependencies": {
- "@adobe/helix-universal": "4.4.1"
- }
+ "devDependencies": {}
},
"packages/spacecat-shared-utils": {
"name": "@adobe/spacecat-shared-utils",
- "version": "1.0.1",
+ "version": "1.1.0",
"license": "Apache-2.0",
"devDependencies": {
"chai": "4.3.10"
diff --git a/packages/spacecat-shared-data-access/.jsdoc.json b/packages/spacecat-shared-data-access/.jsdoc.json
new file mode 100644
index 000000000..405090f4b
--- /dev/null
+++ b/packages/spacecat-shared-data-access/.jsdoc.json
@@ -0,0 +1,17 @@
+{
+ "plugins": [],
+ "recurseDepth": 10,
+ "source": {
+ "includePattern": ".+\\.js(doc|x)?$",
+ "excludePattern": "(^|\\/|\\\\)_"
+ },
+ "sourceType": "module",
+ "tags": {
+ "allowUnknownTags": true,
+ "dictionaries": ["jsdoc","closure"]
+ },
+ "templates": {
+ "cleverLinks": false,
+ "monospaceLinks": false
+ }
+}
\ No newline at end of file
diff --git a/packages/spacecat-shared-data-access/.mocha-multi.json b/packages/spacecat-shared-data-access/.mocha-multi.json
new file mode 100644
index 000000000..aa2be2a23
--- /dev/null
+++ b/packages/spacecat-shared-data-access/.mocha-multi.json
@@ -0,0 +1,6 @@
+{
+ "reporterEnabled": "spec,xunit",
+ "xunitReporterOptions": {
+ "output": "junit/test-results.xml"
+ }
+}
diff --git a/packages/spacecat-shared-data-access/.npmignore b/packages/spacecat-shared-data-access/.npmignore
new file mode 100644
index 000000000..868317d21
--- /dev/null
+++ b/packages/spacecat-shared-data-access/.npmignore
@@ -0,0 +1,9 @@
+coverage/
+node_modules/
+junit/
+test/
+docs/
+logs/
+test-results.xml
+renovate.json
+.*
diff --git a/packages/spacecat-shared-data-access/.nycrc.json b/packages/spacecat-shared-data-access/.nycrc.json
new file mode 100644
index 000000000..78e7a0b14
--- /dev/null
+++ b/packages/spacecat-shared-data-access/.nycrc.json
@@ -0,0 +1,10 @@
+{
+ "reporter": [
+ "lcov",
+ "text"
+ ],
+ "check-coverage": true,
+ "lines": 100,
+ "branches": 97,
+ "statements": 100
+}
diff --git a/packages/spacecat-shared-data-access/CHANGELOG.md b/packages/spacecat-shared-data-access/CHANGELOG.md
new file mode 100644
index 000000000..e69de29bb
diff --git a/packages/spacecat-shared-data-access/LICENSE.txt b/packages/spacecat-shared-data-access/LICENSE.txt
new file mode 100644
index 000000000..883ab098f
--- /dev/null
+++ b/packages/spacecat-shared-data-access/LICENSE.txt
@@ -0,0 +1,264 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed 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 CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
+
+APACHE JACKRABBIT SUBCOMPONENTS
+
+Apache Jackrabbit includes parts with separate copyright notices and license
+terms. Your use of these subcomponents is subject to the terms and conditions
+of the following licenses:
+
+ XPath 2.0/XQuery 1.0 Parser:
+ http://www.w3.org/2002/11/xquery-xpath-applets/xgrammar.zip
+
+ Copyright (C) 2002 World Wide Web Consortium, (Massachusetts Institute of
+ Technology, European Research Consortium for Informatics and Mathematics,
+ Keio University). All Rights Reserved.
+
+ This work is distributed under the W3C(R) Software License in the hope
+ that it will be useful, but WITHOUT ANY WARRANTY; without even the
+ implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ W3C(R) SOFTWARE NOTICE AND LICENSE
+ http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231
+
+ This work (and included software, documentation such as READMEs, or
+ other related items) is being provided by the copyright holders under
+ the following license. By obtaining, using and/or copying this work,
+ you (the licensee) agree that you have read, understood, and will comply
+ with the following terms and conditions.
+
+ Permission to copy, modify, and distribute this software and its
+ documentation, with or without modification, for any purpose and
+ without fee or royalty is hereby granted, provided that you include
+ the following on ALL copies of the software and documentation or
+ portions thereof, including modifications:
+
+ 1. The full text of this NOTICE in a location viewable to users
+ of the redistributed or derivative work.
+
+ 2. Any pre-existing intellectual property disclaimers, notices,
+ or terms and conditions. If none exist, the W3C Software Short
+ Notice should be included (hypertext is preferred, text is
+ permitted) within the body of any redistributed or derivative code.
+
+ 3. Notice of any changes or modifications to the files, including
+ the date changes were made. (We recommend you provide URIs to the
+ location from which the code is derived.)
+
+ THIS SOFTWARE AND DOCUMENTATION IS PROVIDED "AS IS," AND COPYRIGHT
+ HOLDERS MAKE NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR IMPLIED,
+ INCLUDING BUT NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY OR FITNESS
+ FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE OR
+ DOCUMENTATION WILL NOT INFRINGE ANY THIRD PARTY PATENTS, COPYRIGHTS,
+ TRADEMARKS OR OTHER RIGHTS.
+
+ COPYRIGHT HOLDERS WILL NOT BE LIABLE FOR ANY DIRECT, INDIRECT, SPECIAL
+ OR CONSEQUENTIAL DAMAGES ARISING OUT OF ANY USE OF THE SOFTWARE OR
+ DOCUMENTATION.
+
+ The name and trademarks of copyright holders may NOT be used in
+ advertising or publicity pertaining to the software without specific,
+ written prior permission. Title to copyright in this software and
+ any associated documentation will at all times remain with
+ copyright holders.
diff --git a/packages/spacecat-shared-data-access/README.md b/packages/spacecat-shared-data-access/README.md
new file mode 100644
index 000000000..e23e5be9e
--- /dev/null
+++ b/packages/spacecat-shared-data-access/README.md
@@ -0,0 +1,2 @@
+# Data Access Module
+
diff --git a/packages/spacecat-shared-data-access/package.json b/packages/spacecat-shared-data-access/package.json
new file mode 100644
index 000000000..028e9cff9
--- /dev/null
+++ b/packages/spacecat-shared-data-access/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "@adobe/spacecat-shared-data-access",
+ "version": "1.1.0",
+ "description": "Shared modules of the Spacecat Services - Data Access",
+ "type": "module",
+ "main": "src/index.js",
+ "types": "src/index.d.ts",
+ "scripts": {
+ "test": "c8 mocha",
+ "lint": "eslint .",
+ "clean": "rm -rf package-lock.json node_modules"
+ },
+ "mocha": {
+ "reporter": "mocha-multi-reporters",
+ "reporter-options": "configFile=.mocha-multi.json",
+ "spec": "test/**/*.test.js"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/adobe-rnd/spacecat-shared.git"
+ },
+ "author": "",
+ "license": "Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/adobe-rnd/spacecat-shared/issues"
+ },
+ "homepage": "https://github.com/adobe-rnd/spacecat-shared#readme",
+ "publishConfig": {
+ "access": "public"
+ },
+ "dependencies": {
+ "@adobe/spacecat-shared-utils": "1.1.0"
+ },
+ "devDependencies": {
+ "chai": "4.3.10"
+ }
+}
diff --git a/packages/spacecat-shared-dynamo/package.json b/packages/spacecat-shared-dynamo/package.json
index bef51cb40..5cb4bc13b 100644
--- a/packages/spacecat-shared-dynamo/package.json
+++ b/packages/spacecat-shared-dynamo/package.json
@@ -31,7 +31,7 @@
"dependencies": {
"@aws-sdk/client-dynamodb": "3.454.0",
"@aws-sdk/lib-dynamodb": "3.454.0",
- "@adobe/spacecat-shared-utils": "1.0.1"
+ "@adobe/spacecat-shared-utils": "1.1.0"
},
"devDependencies": {
"chai": "4.3.10"
From 9e93178e8ad0d3ff4bafd672486502a687f26f96 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Wed, 29 Nov 2023 10:45:19 +0100
Subject: [PATCH 02/28] fix: key validation
---
.../src/utils/guards.js | 8 +++---
.../test/modules/getItem.test.js | 2 +-
.../test/modules/removeItem.test.js | 2 +-
.../test/utils/guards.test.js | 28 ++++++++++---------
4 files changed, 21 insertions(+), 19 deletions(-)
diff --git a/packages/spacecat-shared-dynamo/src/utils/guards.js b/packages/spacecat-shared-dynamo/src/utils/guards.js
index 62b392721..11d52bcc3 100644
--- a/packages/spacecat-shared-dynamo/src/utils/guards.js
+++ b/packages/spacecat-shared-dynamo/src/utils/guards.js
@@ -25,14 +25,14 @@ const guardTableName = (tableName) => {
};
/**
- * Validates that the provided key is an object and contains a partitionKey.
+ * Validates that the provided key is an object and contains at least one property.
*
* @param {object} key - The key object to validate.
- * @throws {Error} If the key is not an object or does not contain a partitionKey.
+ * @throws {Error} If the key is not an object or does not contain at least one property.
*/
const guardKey = (key) => {
- if (!isObject(key) || !key.partitionKey) {
- throw new Error('Key must be an object with a partitionKey.');
+ if (!isObject(key) || Object.keys(key).length === 0) {
+ throw new Error('Key must be a non-empty object.');
}
};
diff --git a/packages/spacecat-shared-dynamo/test/modules/getItem.test.js b/packages/spacecat-shared-dynamo/test/modules/getItem.test.js
index f89a131b3..205c0101b 100644
--- a/packages/spacecat-shared-dynamo/test/modules/getItem.test.js
+++ b/packages/spacecat-shared-dynamo/test/modules/getItem.test.js
@@ -54,7 +54,7 @@ describe('getItem', () => {
await dynamoDbClient.getItem('TestTable', null);
expect.fail('getItem did not throw with invalid key');
} catch (error) {
- expect(error.message).to.equal('Key must be an object with a partitionKey.');
+ expect(error.message).to.equal('Key must be a non-empty object.');
}
});
diff --git a/packages/spacecat-shared-dynamo/test/modules/removeItem.test.js b/packages/spacecat-shared-dynamo/test/modules/removeItem.test.js
index 37f94fd63..02e551661 100644
--- a/packages/spacecat-shared-dynamo/test/modules/removeItem.test.js
+++ b/packages/spacecat-shared-dynamo/test/modules/removeItem.test.js
@@ -54,7 +54,7 @@ describe('removeItem', () => {
await dynamoDbClient.removeItem('TestTable', null);
expect.fail('removeItem did not throw with invalid key');
} catch (error) {
- expect(error.message).to.equal('Key must be an object with a partitionKey.');
+ expect(error.message).to.equal('Key must be a non-empty object.');
}
});
diff --git a/packages/spacecat-shared-dynamo/test/utils/guards.test.js b/packages/spacecat-shared-dynamo/test/utils/guards.test.js
index 814b2c4db..26846b338 100644
--- a/packages/spacecat-shared-dynamo/test/utils/guards.test.js
+++ b/packages/spacecat-shared-dynamo/test/utils/guards.test.js
@@ -16,42 +16,44 @@ import { expect } from 'chai';
import { guardKey, guardQueryParameters, guardTableName } from '../../src/utils/guards.js';
describe('Query Parameter Guards', () => {
- // Test guardTableName
describe('guardTableName', () => {
- it('should throw an error if tableName is empty', () => {
+ it('throws an error if tableName is empty', () => {
expect(() => guardTableName('')).to.throw('Table name is required.');
});
- it('should not throw an error for valid tableName', () => {
+ it('does not throw an error for valid tableName', () => {
expect(() => guardTableName('validTableName')).to.not.throw();
});
});
- // Test guardKey
describe('guardKey', () => {
- it('should throw an error if key is not an object', () => {
- expect(() => guardKey('notAnObject')).to.throw('Key must be an object with a partitionKey.');
+ it('throws an error if key is not an object', () => {
+ expect(() => guardKey('notAnObject')).to.throw('Key must be a non-empty object.');
});
- it('should throw an error if key does not have partitionKey', () => {
- expect(() => guardKey({})).to.throw('Key must be an object with a partitionKey.');
+ it('throws an error if key is an empty object', () => {
+ expect(() => guardKey({})).to.throw('Key must be a non-empty object.');
});
- it('should not throw an error for a valid key', () => {
- expect(() => guardKey({ partitionKey: 'value' })).to.not.throw();
+ it('does not throw an error for a valid key with one property', () => {
+ expect(() => guardKey({ somePartitionKeyField: 'value' })).to.not.throw();
+ });
+
+ it('does not throw an error for a valid key with two properties', () => {
+ expect(() => guardKey({ somePartitionKeyField: 'value', someOptionalRangeKey: 'anotherValue' })).to.not.throw();
});
});
describe('guardQueryParameters', () => {
- it('should throw an error if params is not an object', () => {
+ it('throws an error if params is not an object', () => {
expect(() => guardQueryParameters('notAnObject')).to.throw('Query parameters must be an object.');
});
- it('should throw an error if any required parameter is missing', () => {
+ it('throws an error if any required parameter is missing', () => {
expect(() => guardQueryParameters({ TableName: 'table' })).to.throw('Query parameters is missing required parameter: KeyConditionExpression');
});
- it('should not throw an error for valid params', () => {
+ it('does not throw an error for valid params', () => {
const validParams = {
TableName: 'table',
KeyConditionExpression: 'expression',
From 49263dc14f30016e6eb052df00e71bc593ee855c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Wed, 29 Nov 2023 18:44:35 +0100
Subject: [PATCH 03/28] feat: add models, access patterns and tests
---
package-lock.json | 175 ++++++++++-
.../docs/schema.json | 290 ++++++++++++++++++
.../spacecat-shared-data-access/package.json | 7 +-
.../src/audits/accessPatterns.js | 95 ++++++
.../src/audits/index.js | 34 ++
.../src/index.d.ts | 71 +++++
.../spacecat-shared-data-access/src/index.js | 33 ++
.../src/models/audit.js | 60 ++++
.../src/models/base.js | 30 ++
.../src/models/site.js | 51 +++
.../src/sites/accessPatterns.js | 170 ++++++++++
.../src/sites/index.js | 59 ++++
.../test/audits/index.test.js | 79 +++++
.../test/index.test.js | 58 ++++
.../test/models/audit.test.js | 69 +++++
.../test/models/base.test.js | 57 ++++
.../test/models/site.test.js | 65 ++++
.../test/sites/index.test.js | 214 +++++++++++++
18 files changed, 1612 insertions(+), 5 deletions(-)
create mode 100644 packages/spacecat-shared-data-access/docs/schema.json
create mode 100644 packages/spacecat-shared-data-access/src/audits/accessPatterns.js
create mode 100644 packages/spacecat-shared-data-access/src/audits/index.js
create mode 100644 packages/spacecat-shared-data-access/src/index.d.ts
create mode 100644 packages/spacecat-shared-data-access/src/index.js
create mode 100644 packages/spacecat-shared-data-access/src/models/audit.js
create mode 100644 packages/spacecat-shared-data-access/src/models/base.js
create mode 100644 packages/spacecat-shared-data-access/src/models/site.js
create mode 100644 packages/spacecat-shared-data-access/src/sites/accessPatterns.js
create mode 100644 packages/spacecat-shared-data-access/src/sites/index.js
create mode 100644 packages/spacecat-shared-data-access/test/audits/index.test.js
create mode 100644 packages/spacecat-shared-data-access/test/index.test.js
create mode 100644 packages/spacecat-shared-data-access/test/models/audit.test.js
create mode 100644 packages/spacecat-shared-data-access/test/models/base.test.js
create mode 100644 packages/spacecat-shared-data-access/test/models/site.test.js
create mode 100644 packages/spacecat-shared-data-access/test/sites/index.test.js
diff --git a/package-lock.json b/package-lock.json
index b37bce2d0..0e9459ca5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1491,6 +1491,50 @@
"semantic-release": ">=18.0.0-beta.1"
}
},
+ "node_modules/@sinonjs/commons": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
+ "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
+ "dev": true,
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/fake-timers": {
+ "version": "11.2.2",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz",
+ "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.0"
+ }
+ },
+ "node_modules/@sinonjs/samsam": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz",
+ "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^2.0.0",
+ "lodash.get": "^4.4.2",
+ "type-detect": "^4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
+ "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
+ "dev": true,
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/text-encoding": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz",
+ "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==",
+ "dev": true
+ },
"node_modules/@smithy/abort-controller": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.0.14.tgz",
@@ -5588,6 +5632,12 @@
"node": "*"
}
},
+ "node_modules/just-extend": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz",
+ "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==",
+ "dev": true
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -5909,6 +5959,12 @@
"integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
"dev": true
},
+ "node_modules/lodash.get": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
+ "dev": true
+ },
"node_modules/lodash.ismatch": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz",
@@ -6620,6 +6676,46 @@
"integrity": "sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==",
"dev": true
},
+ "node_modules/nise": {
+ "version": "5.1.5",
+ "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz",
+ "integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^2.0.0",
+ "@sinonjs/fake-timers": "^10.0.2",
+ "@sinonjs/text-encoding": "^0.7.1",
+ "just-extend": "^4.0.2",
+ "path-to-regexp": "^1.7.0"
+ }
+ },
+ "node_modules/nise/node_modules/@sinonjs/commons": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
+ "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
+ "dev": true,
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/nise/node_modules/@sinonjs/fake-timers": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
+ "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.0"
+ }
+ },
+ "node_modules/nise/node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
+ "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
+ "dev": true,
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
"node_modules/nock": {
"version": "13.3.8",
"resolved": "https://registry.npmjs.org/nock/-/nock-13.3.8.tgz",
@@ -9602,6 +9698,21 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
+ "node_modules/path-to-regexp": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
+ "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
+ "dev": true,
+ "dependencies": {
+ "isarray": "0.0.1"
+ }
+ },
+ "node_modules/path-to-regexp/node_modules/isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
+ "dev": true
+ },
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -10910,6 +11021,33 @@
"node": ">=4"
}
},
+ "node_modules/sinon": {
+ "version": "17.0.1",
+ "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz",
+ "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.0",
+ "@sinonjs/fake-timers": "^11.2.2",
+ "@sinonjs/samsam": "^8.0.0",
+ "diff": "^5.1.0",
+ "nise": "^5.1.5",
+ "supports-color": "^7.2.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/sinon"
+ }
+ },
+ "node_modules/sinon/node_modules/diff": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
+ "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -12101,18 +12239,49 @@
}
},
"packages/spacecat-shared-data-access": {
+ "name": "@adobe/spacecat-shared-data-access",
"version": "1.1.0",
"license": "Apache-2.0",
"dependencies": {
- "@adobe/spacecat-shared-utils": "1.1.0"
+ "@adobe/spacecat-shared-dynamo": "1.1.2",
+ "@adobe/spacecat-shared-utils": "1.1.0",
+ "uuid": "9.0.1"
},
"devDependencies": {
- "chai": "4.3.10"
+ "chai": "4.3.10",
+ "sinon": "^17.0.1"
+ }
+ },
+ "packages/spacecat-shared-data-access/node_modules/@adobe/spacecat-shared-dynamo": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@adobe/spacecat-shared-dynamo/-/spacecat-shared-dynamo-1.1.2.tgz",
+ "integrity": "sha512-xR1iFHhDBOhQ2ci0qH6dzz954+KJpSPoHaUynTphLNTVNlToXHkQiSE8X+z8cX9G5NCm9HG3tgNYv/9wRK6zPQ==",
+ "dependencies": {
+ "@adobe/spacecat-shared-utils": "1.0.1",
+ "@aws-sdk/client-dynamodb": "3.454.0",
+ "@aws-sdk/lib-dynamodb": "3.454.0"
+ }
+ },
+ "packages/spacecat-shared-data-access/node_modules/@adobe/spacecat-shared-dynamo/node_modules/@adobe/spacecat-shared-utils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@adobe/spacecat-shared-utils/-/spacecat-shared-utils-1.0.1.tgz",
+ "integrity": "sha512-6HiCywan5y9jpbFYkQX87Vg0q5dXoJf2m6MiDvNBEJdREdUxFwE+oYA6Fr86qqd32ECnCeYCFO13HcNRn1RxkQ=="
+ },
+ "packages/spacecat-shared-data-access/node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "bin": {
+ "uuid": "dist/bin/uuid"
}
},
"packages/spacecat-shared-dynamo": {
"name": "@adobe/spacecat-shared-dynamo",
- "version": "1.1.0",
+ "version": "1.1.2",
"license": "Apache-2.0",
"dependencies": {
"@adobe/spacecat-shared-utils": "1.1.0",
diff --git a/packages/spacecat-shared-data-access/docs/schema.json b/packages/spacecat-shared-data-access/docs/schema.json
new file mode 100644
index 000000000..4ba87ec92
--- /dev/null
+++ b/packages/spacecat-shared-data-access/docs/schema.json
@@ -0,0 +1,290 @@
+{
+ "ModelName": "StarCatalogue",
+ "ModelMetadata": {
+ "Author": "Dominique Jäggi",
+ "DateCreated": "Nov 23, 2023, 07:00 AM",
+ "DateLastModified": "Nov 23, 2023, 07:00 AM",
+ "Description": "",
+ "AWSService": "Amazon DynamoDB",
+ "Version": "3.0"
+ },
+ "DataModel": [
+ {
+ "TableName": "sites",
+ "KeyAttributes": {
+ "PartitionKey": {
+ "AttributeName": "id",
+ "AttributeType": "S"
+ }
+ },
+ "NonKeyAttributes": [
+ {
+ "AttributeName": "baseURL",
+ "AttributeType": "S"
+ },
+ {
+ "AttributeName": "imsOrgId",
+ "AttributeType": "S"
+ },
+ {
+ "AttributeName": "GSI1PK",
+ "AttributeType": "S"
+ },
+ {
+ "AttributeName": "createdAt",
+ "AttributeType": "S"
+ },
+ {
+ "AttributeName": "updatedAt",
+ "AttributeType": "S"
+ }
+ ],
+ "GlobalSecondaryIndexes": [
+ {
+ "IndexName": "all_sites",
+ "KeyAttributes": {
+ "PartitionKey": {
+ "AttributeName": "GSI1PK",
+ "AttributeType": "S"
+ },
+ "SortKey": {
+ "AttributeName": "baseURL",
+ "AttributeType": "S"
+ }
+ },
+ "Projection": {
+ "ProjectionType": "ALL"
+ }
+ }
+ ],
+ "DataAccess": {
+ "MySql": {}
+ },
+ "SampleDataFormats": {
+ "id": [
+ "identifiers",
+ "UUID"
+ ],
+ "baseURL": [
+ "identifiers",
+ "URL"
+ ],
+ "imsOrgId": [
+ "identifiers",
+ "UUID"
+ ]
+ },
+ "BillingMode": "PROVISIONED",
+ "ProvisionedCapacitySettings": {
+ "ProvisionedThroughput": {
+ "ReadCapacityUnits": 5,
+ "WriteCapacityUnits": 5
+ },
+ "AutoScalingRead": {
+ "ScalableTargetRequest": {
+ "MinCapacity": 1,
+ "MaxCapacity": 10,
+ "ServiceRole": "AWSServiceRoleForApplicationAutoScaling_DynamoDBTable"
+ },
+ "ScalingPolicyConfiguration": {
+ "TargetValue": 70
+ }
+ },
+ "AutoScalingWrite": {
+ "ScalableTargetRequest": {
+ "MinCapacity": 1,
+ "MaxCapacity": 10,
+ "ServiceRole": "AWSServiceRoleForApplicationAutoScaling_DynamoDBTable"
+ },
+ "ScalingPolicyConfiguration": {
+ "TargetValue": 70
+ }
+ }
+ }
+ },
+ {
+ "TableName": "audits",
+ "KeyAttributes": {
+ "PartitionKey": {
+ "AttributeName": "siteId",
+ "AttributeType": "S"
+ },
+ "SortKey": {
+ "AttributeName": "SK",
+ "AttributeType": "S"
+ }
+ },
+ "NonKeyAttributes": [
+ {
+ "AttributeName": "auditedAt",
+ "AttributeType": "S"
+ },
+ {
+ "AttributeName": "auditResult",
+ "AttributeType": "M"
+ },
+ {
+ "AttributeName": "auditType",
+ "AttributeType": "S"
+ },
+ {
+ "AttributeName": "expiresAt",
+ "AttributeType": "N"
+ },
+ {
+ "AttributeName": "fullAuditRef",
+ "AttributeType": "S"
+ }
+ ],
+ "DataAccess": {
+ "MySql": {}
+ },
+ "SampleDataFormats": {
+ "siteId": [
+ "identifiers",
+ "UUID"
+ ],
+ "auditedAt": [
+ "date",
+ "ISO 8601 date and time"
+ ],
+ "fullAuditRef": [
+ "identifiers",
+ "URL"
+ ]
+ },
+ "BillingMode": "PROVISIONED",
+ "ProvisionedCapacitySettings": {
+ "ProvisionedThroughput": {
+ "ReadCapacityUnits": 5,
+ "WriteCapacityUnits": 5
+ },
+ "AutoScalingRead": {
+ "ScalableTargetRequest": {
+ "MinCapacity": 1,
+ "MaxCapacity": 10,
+ "ServiceRole": "AWSServiceRoleForApplicationAutoScaling_DynamoDBTable"
+ },
+ "ScalingPolicyConfiguration": {
+ "TargetValue": 70
+ }
+ },
+ "AutoScalingWrite": {
+ "ScalableTargetRequest": {
+ "MinCapacity": 1,
+ "MaxCapacity": 10,
+ "ServiceRole": "AWSServiceRoleForApplicationAutoScaling_DynamoDBTable"
+ },
+ "ScalingPolicyConfiguration": {
+ "TargetValue": 70
+ }
+ }
+ }
+ },
+ {
+ "TableName": "latest_audits",
+ "KeyAttributes": {
+ "PartitionKey": {
+ "AttributeName": "siteId",
+ "AttributeType": "S"
+ },
+ "SortKey": {
+ "AttributeName": "SK",
+ "AttributeType": "S"
+ }
+ },
+ "NonKeyAttributes": [
+ {
+ "AttributeName": "auditedAt",
+ "AttributeType": "S"
+ },
+ {
+ "AttributeName": "auditResult",
+ "AttributeType": "M"
+ },
+ {
+ "AttributeName": "auditType",
+ "AttributeType": "S"
+ },
+ {
+ "AttributeName": "expiresAt",
+ "AttributeType": "N"
+ },
+ {
+ "AttributeName": "fullAuditRef",
+ "AttributeType": "S"
+ },
+ {
+ "AttributeName": "GSI1PK",
+ "AttributeType": "S"
+ },
+ {
+ "AttributeName": "GSI1SK",
+ "AttributeType": "S"
+ }
+ ],
+ "GlobalSecondaryIndexes": [
+ {
+ "IndexName": "all_latest_audit_scores",
+ "KeyAttributes": {
+ "PartitionKey": {
+ "AttributeName": "GSI1PK",
+ "AttributeType": "S"
+ },
+ "SortKey": {
+ "AttributeName": "GSI1SK",
+ "AttributeType": "S"
+ }
+ },
+ "Projection": {
+ "ProjectionType": "ALL"
+ }
+ }
+ ],
+ "DataAccess": {
+ "MySql": {}
+ },
+ "SampleDataFormats": {
+ "siteId": [
+ "identifiers",
+ "UUID"
+ ],
+ "auditedAt": [
+ "date",
+ "ISO 8601 date and time"
+ ],
+ "fullAuditRef": [
+ "identifiers",
+ "URL"
+ ]
+ },
+ "BillingMode": "PROVISIONED",
+ "ProvisionedCapacitySettings": {
+ "ProvisionedThroughput": {
+ "ReadCapacityUnits": 5,
+ "WriteCapacityUnits": 5
+ },
+ "AutoScalingRead": {
+ "ScalableTargetRequest": {
+ "MinCapacity": 1,
+ "MaxCapacity": 10,
+ "ServiceRole": "AWSServiceRoleForApplicationAutoScaling_DynamoDBTable"
+ },
+ "ScalingPolicyConfiguration": {
+ "TargetValue": 70
+ }
+ },
+ "AutoScalingWrite": {
+ "ScalableTargetRequest": {
+ "MinCapacity": 1,
+ "MaxCapacity": 10,
+ "ServiceRole": "AWSServiceRoleForApplicationAutoScaling_DynamoDBTable"
+ },
+ "ScalingPolicyConfiguration": {
+ "TargetValue": 70
+ }
+ }
+ }
+ }
+ ]
+}
diff --git a/packages/spacecat-shared-data-access/package.json b/packages/spacecat-shared-data-access/package.json
index 028e9cff9..04ff67eb5 100644
--- a/packages/spacecat-shared-data-access/package.json
+++ b/packages/spacecat-shared-data-access/package.json
@@ -29,9 +29,12 @@
"access": "public"
},
"dependencies": {
- "@adobe/spacecat-shared-utils": "1.1.0"
+ "@adobe/spacecat-shared-dynamo": "1.1.2",
+ "@adobe/spacecat-shared-utils": "1.1.0",
+ "uuid": "9.0.1"
},
"devDependencies": {
- "chai": "4.3.10"
+ "chai": "4.3.10",
+ "sinon": "17.0.1"
}
}
diff --git a/packages/spacecat-shared-data-access/src/audits/accessPatterns.js b/packages/spacecat-shared-data-access/src/audits/accessPatterns.js
new file mode 100644
index 000000000..3be1877a2
--- /dev/null
+++ b/packages/spacecat-shared-data-access/src/audits/accessPatterns.js
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ */
+
+/**
+ * Retrieves audits for a specified site. If an audit type is provided,
+ * it returns only audits of that type.
+ *
+ * @param {DynamoDbClient} dynamoClient - The DynamoDB client.
+ * @param {Logger} log - The logger.
+ * @param {string} siteId - The ID of the site for which audits are being retrieved.
+ * @param {string} [auditType] - Optional. The type of audits to retrieve.
+ * @returns {Promise} A promise that resolves to an array of audits for the specified site.
+ */
+export const getAuditsForSite = async (dynamoClient, log, siteId, auditType) => {
+ // Base query parameters
+ const queryParams = {
+ TableName: 'audits',
+ KeyConditionExpression: 'siteId = :siteId',
+ ExpressionAttributeValues: {
+ ':siteId': siteId,
+ },
+ };
+
+ if (auditType !== undefined) {
+ queryParams.KeyConditionExpression += ' AND begins_with(SK, :auditType)';
+ queryParams.ExpressionAttributeValues[':auditType'] = `${auditType}#`;
+ }
+
+ return dynamoClient.query(queryParams);
+};
+
+/**
+ * Retrieves the latest audits of a specific type across all sites.
+ *
+ * @param {DynamoDbClient} dynamoClient - The DynamoDB client.
+ * @param {Logger} log - The logger.
+ * @param {string} auditType - The type of audits to retrieve.
+ * @param {boolean} ascending - Determines if the audits should be sorted ascending
+ * or descending by scores.
+ * @returns {Promise} A promise that resolves to an array of the latest
+ * audits of the specified type.
+ */
+export const getLatestAudits = async (
+ dynamoClient,
+ log,
+ auditType,
+ ascending = true,
+) => dynamoClient.query({
+ TableName: 'latest_audits',
+ IndexName: 'all_latest_audit_scores',
+ KeyConditionExpression: 'GSI1PK = :gsi1pk AND begins_with(GSI1SK, :auditType)',
+ ExpressionAttributeValues: {
+ ':gsi1pk': 'ALL_LATEST_AUDITS',
+ ':auditType': `${auditType}#`,
+ },
+ ScanIndexForward: ascending, // Sorts ascending if true, descending if false
+});
+
+/**
+ * Retrieves the latest audit for a specified site and audit type.
+ *
+ * @param {DynamoDbClient} dynamoClient - The DynamoDB client.
+ * @param {Logger} log - The logger.
+ * @param {string} siteId - The ID of the site for which the latest audit is being retrieved.
+ * @param {string} auditType - The type of audit to retrieve the latest instance of.
+ * @returns {Promise} A promise that resolves to the latest audit of the
+ * specified type for the site, or null if none is found.
+ */
+export const getLatestAuditForSite = async (
+ dynamoClient,
+ log,
+ siteId,
+ auditType,
+) => {
+ const latestAudit = await dynamoClient.query({
+ TableName: 'latest_audits',
+ KeyConditionExpression: 'siteId = :siteId AND begins_with(SK, :auditType)',
+ ExpressionAttributeValues: {
+ ':siteId': siteId,
+ ':auditType': `${auditType}#`,
+ },
+ Limit: 1,
+ });
+
+ return latestAudit.length > 0 ? latestAudit[0] : null;
+};
diff --git a/packages/spacecat-shared-data-access/src/audits/index.js b/packages/spacecat-shared-data-access/src/audits/index.js
new file mode 100644
index 000000000..3230ce4f0
--- /dev/null
+++ b/packages/spacecat-shared-data-access/src/audits/index.js
@@ -0,0 +1,34 @@
+/*
+ * 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 { getAuditsForSite, getLatestAuditForSite, getLatestAudits } from './accessPatterns.js';
+
+export const auditFunctions = (dynamoClient, log) => ({
+ getAuditsForSite: (siteId, auditType) => getAuditsForSite(
+ dynamoClient,
+ log,
+ siteId,
+ auditType,
+ ),
+ getLatestAudits: (auditType, ascending) => getLatestAudits(
+ dynamoClient,
+ log,
+ auditType,
+ ascending,
+ ),
+ getLatestAuditForSite: (siteId, auditType) => getLatestAuditForSite(
+ dynamoClient,
+ log,
+ siteId,
+ auditType,
+ ),
+});
diff --git a/packages/spacecat-shared-data-access/src/index.d.ts b/packages/spacecat-shared-data-access/src/index.d.ts
new file mode 100644
index 000000000..2605daff7
--- /dev/null
+++ b/packages/spacecat-shared-data-access/src/index.d.ts
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+export interface Site {
+ id: string;
+ baseURL: string;
+ imsOrgId: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface Audit {
+ siteId: string;
+ auditedAt: string;
+ auditResult: object;
+ auditType: string;
+ expiresAt: number;
+ fullAuditRef: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+export interface DataAccess {
+ getAuditsForSite: (
+ siteId: string,
+ auditType?: string
+ ) => Promise;
+ getLatestAuditForSite: (
+ siteId: string,
+ auditType: string,
+ ) => Promise;
+ getLatestAudits: (
+ auditType: string,
+ ascending?: boolean,
+ ) => Promise;
+ getSites: () => Promise;
+ getSitesToAudit: () => Promise;
+ getSitesWithLatestAudit: (
+ auditType: string,
+ sortAuditsAscending?: boolean,
+ ) => Promise;
+ getSiteByBaseURL: (
+ baseUrl: string,
+ ) => Promise;
+ getSiteByBaseURLWithAuditInfo: (
+ baseUrl: string,
+ auditType: string,
+ latestOnly?: boolean,
+ ) => Promise;
+ getSiteByBaseURLWithAudits: (
+ baseUrl: string,
+ auditType: string,
+ ) => Promise;
+ getSiteByBaseURLWithLatestAudit: (
+ baseUrl: string,
+ auditType: string,
+ ) => Promise;
+}
+
+export function createDataAccess(
+ logger: object,
+): DataAccess;
diff --git a/packages/spacecat-shared-data-access/src/index.js b/packages/spacecat-shared-data-access/src/index.js
new file mode 100644
index 000000000..6f0b9bddf
--- /dev/null
+++ b/packages/spacecat-shared-data-access/src/index.js
@@ -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.
+ */
+
+import { createClient } from '@adobe/spacecat-shared-dynamo';
+import { auditFunctions } from './audits/index.js';
+import { siteFunctions } from './sites/index.js';
+
+/**
+ * Creates a data access object.
+ *
+ * @param {Logger} log logger
+ * @returns {object} data access object
+ */
+export const createDataAccess = (log = console) => {
+ const dynamoClient = createClient(log);
+
+ const auditFuncs = auditFunctions(dynamoClient, log);
+ const siteFuncs = siteFunctions(dynamoClient, log);
+
+ return {
+ ...auditFuncs,
+ ...siteFuncs,
+ };
+};
diff --git a/packages/spacecat-shared-data-access/src/models/audit.js b/packages/spacecat-shared-data-access/src/models/audit.js
new file mode 100644
index 000000000..76d7c37f5
--- /dev/null
+++ b/packages/spacecat-shared-data-access/src/models/audit.js
@@ -0,0 +1,60 @@
+/*
+ * 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 { hasText, isIsoDate, isObject } from '@adobe/spacecat-shared-utils';
+import { Base } from './base.js';
+
+const EXPIRES_IN_DAYS = 30;
+
+const Audit = (data = {}) => {
+ const self = Base(data);
+
+ self.getSiteId = () => self.state.siteId;
+ self.getAuditedAt = () => self.state.auditedAt;
+ self.getAuditResult = () => self.state.auditResult;
+ self.getAuditType = () => self.state.auditType.toLowerCase();
+ self.getExpiresAt = () => self.state.expiresAt;
+ self.getFullAuditRef = () => self.state.fullAuditRef;
+
+ return Object.freeze(self);
+};
+
+export const createAudit = (data) => {
+ const newState = { ...data };
+
+ if (!hasText(newState.siteId)) {
+ throw new Error('Site ID must be provided');
+ }
+
+ if (!isIsoDate(newState.auditedAt)) {
+ throw new Error('Audited at must be a valid ISO date');
+ }
+
+ if (!hasText(newState.auditType)) {
+ throw new Error('Audit type must be provided');
+ }
+
+ if (!isObject(newState.auditResult)) {
+ throw new Error('Audit result must be an object');
+ }
+
+ if (!hasText(newState.fullAuditRef)) {
+ throw new Error('Full audit ref must be provided');
+ }
+
+ if (!newState.expiresAt) {
+ newState.expiresAt = new Date(newState.auditedAt);
+ newState.expiresAt.setDate(newState.expiresAt.getDate() + EXPIRES_IN_DAYS);
+ }
+
+ return Audit(newState);
+};
diff --git a/packages/spacecat-shared-data-access/src/models/base.js b/packages/spacecat-shared-data-access/src/models/base.js
new file mode 100644
index 000000000..797fa26fa
--- /dev/null
+++ b/packages/spacecat-shared-data-access/src/models/base.js
@@ -0,0 +1,30 @@
+/*
+ * 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 { v4 as uuidv4 } from 'uuid';
+import { isString } from '@adobe/spacecat-shared-utils';
+
+export const Base = (data = {}) => {
+ const self = { state: { ...data } };
+
+ const newRecord = !isString(self.state.id);
+
+ if (newRecord) {
+ self.state.id = uuidv4();
+ }
+
+ self.getId = () => self.state.id;
+ self.getCreatedAt = () => self.state.createdAt;
+ self.getUpdatedAt = () => self.state.updatedAt;
+
+ return self;
+};
diff --git a/packages/spacecat-shared-data-access/src/models/site.js b/packages/spacecat-shared-data-access/src/models/site.js
new file mode 100644
index 000000000..b08587d05
--- /dev/null
+++ b/packages/spacecat-shared-data-access/src/models/site.js
@@ -0,0 +1,51 @@
+/*
+ * 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 { hasText, isValidUrl } from '@adobe/spacecat-shared-utils';
+import { Base } from './base.js';
+
+const Site = (data = {}) => {
+ const self = Base(data);
+
+ self.getBaseURL = () => self.state.baseURL;
+ self.getImsOrgId = () => self.state.imsOrgId;
+
+ self.updateBaseURL = (baseURL) => {
+ if (!isValidUrl(baseURL)) {
+ throw new Error('Base URL must be a valid URL');
+ }
+
+ self.state.baseURL = baseURL;
+ return self;
+ };
+
+ self.updateImsOrgId = (imsOrgId) => {
+ if (!hasText(imsOrgId)) {
+ throw new Error('IMS Org ID must be provided');
+ }
+
+ self.state.imsOrgId = imsOrgId;
+ return self;
+ };
+
+ return Object.freeze(self);
+};
+
+export const createSite = (data) => {
+ const newState = { ...data };
+
+ if (!isValidUrl(newState.baseURL)) {
+ throw new Error('Base URL must be a valid URL');
+ }
+
+ return Site(newState);
+};
diff --git a/packages/spacecat-shared-data-access/src/sites/accessPatterns.js b/packages/spacecat-shared-data-access/src/sites/accessPatterns.js
new file mode 100644
index 000000000..ca862e493
--- /dev/null
+++ b/packages/spacecat-shared-data-access/src/sites/accessPatterns.js
@@ -0,0 +1,170 @@
+/*
+ * 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 {
+ getAuditsForSite,
+ getLatestAuditForSite,
+ getLatestAudits,
+} from '../audits/accessPatterns.js';
+
+/**
+ * Retrieves all sites.
+ *
+ * @param {DynamoDbClient} dynamoClient - The DynamoDB client.
+ * @returns {Promise} A promise that resolves to an array of all sites.
+ */
+export const getSites = async (dynamoClient) => dynamoClient.query({
+ TableName: 'sites',
+ IndexName: 'all_sites', // GSI name
+ KeyConditionExpression: 'GSI1PK = :gsi1pk',
+ ExpressionAttributeValues: {
+ ':gsi1pk': 'ALL_SITES',
+ },
+});
+
+/**
+ * Retrieves a list of base URLs for all sites.
+ *
+ * @param {DynamoDbClient} dynamoClient - The DynamoDB client.
+ * @param {Logger} log - The logger.
+ * @returns {Promise>} A promise that resolves to an array of base URLs for all sites.
+ */
+export const getSitesToAudit = async (dynamoClient, log) => {
+ const sites = await getSites(dynamoClient, log);
+
+ return sites.map((item) => item.baseURL);
+};
+
+/**
+ * Retrieves sites with their latest audit of a specified type.
+ *
+ * @param {DynamoDbClient} dynamoClient - The DynamoDB client.
+ * @param {Logger} log - The logger.
+ * @param {string} auditType - The type of the latest audits to retrieve for each site.
+ * @param {boolean} [sortAuditsAscending] - Optional. Determines if the audits
+ * should be sorted ascending or descending by scores.
+ * @returns {Promise} A promise that resolves to an array of site objects,
+ * each with its latest audit of the specified type.
+ */
+export const getSitesWithLatestAudit = async (
+ dynamoClient,
+ log,
+ auditType,
+ sortAuditsAscending = true,
+) => {
+ const [sites, latestAudits] = await Promise.all([
+ getSites(dynamoClient),
+ getLatestAudits(dynamoClient, log, auditType, sortAuditsAscending),
+ ]);
+
+ const sitesMap = new Map(sites.map((site) => [site.id, site]));
+
+ return latestAudits.reduce((result, audit) => {
+ const site = sitesMap.get(audit.siteId);
+ if (site) {
+ site.audits = [audit];
+ result.push(site);
+ }
+ return result;
+ }, []);
+};
+
+/**
+ * Retrieves a site by its base URL.
+ *
+ * @param {DynamoDbClient} dynamoClient - The DynamoDB client.
+ * @param {Logger} log - The logger.
+ * @param {string} baseUrl - The base URL of the site to retrieve.
+ * @returns {Promise} A promise that resolves to the site object if found,
+ * otherwise null.
+ */
+export const getSiteByBaseURL = async (
+ dynamoClient,
+ log,
+ baseUrl,
+) => dynamoClient.getItem('sites', {
+ GSI1PK: 'ALL_SITES',
+ baseUrl,
+});
+
+/**
+ * Retrieves a site by its base URL, along with associated audit information.
+ *
+ * @param {DynamoDbClient} dynamoClient - The DynamoDB client.
+ * @param {Logger} log - The logger.
+ * @param {string} baseUrl - The base URL of the site to retrieve.
+ * @param {string} auditType - The type of audits to retrieve for the site.
+ * @param {boolean} [latestOnly=false] - Determines if only the latest audit should be retrieved.
+ * @returns {Promise} A promise that resolves to the site object with audit
+ * data if found, otherwise null.
+ */
+export const getSiteByBaseURLWithAuditInfo = async (
+ dynamoClient,
+ log,
+ baseUrl,
+ auditType,
+ latestOnly = false,
+) => {
+ const site = await getSiteByBaseURL(dynamoClient, log, baseUrl);
+
+ if (!site) {
+ return null;
+ }
+
+ site.audits = latestOnly
+ ? [await getLatestAuditForSite(
+ dynamoClient,
+ log,
+ site.id,
+ auditType,
+ )].filter((audit) => audit != null)
+ : await getAuditsForSite(
+ dynamoClient,
+ log,
+ site.id,
+ auditType,
+ );
+
+ return site;
+};
+
+/**
+ * Retrieves a site by its base URL, including all its audits.
+ *
+ * @param {DynamoDbClient} dynamoClient - The DynamoDB client.
+ * @param {Logger} log - The logger.
+ * @param {string} baseUrl - The base URL of the site to retrieve.
+ * @param {string} auditType - The type of audits to retrieve for the site.
+ * @returns {Promise} A promise that resolves to the site object with all its audits.
+ */
+export const getSiteByBaseURLWithAudits = async (
+ dynamoClient,
+ log,
+ baseUrl,
+ auditType,
+) => getSiteByBaseURLWithAuditInfo(dynamoClient, log, baseUrl, auditType, false);
+
+/**
+ * Retrieves a site by its base URL, including only its latest audit.
+ *
+ * @param {DynamoDbClient} dynamoClient - The DynamoDB client.
+ * @param {Logger} log - The logger.
+ * @param {string} baseUrl - The base URL of the site to retrieve.
+ * @param {string} auditType - The type of the latest audit to retrieve for the site.
+ * @returns {Promise} A promise that resolves to the site object with its latest audit.
+ */
+export const getSiteByBaseURLWithLatestAudit = async (
+ dynamoClient,
+ log,
+ baseUrl,
+ auditType,
+) => getSiteByBaseURLWithAuditInfo(dynamoClient, log, baseUrl, auditType, true);
diff --git a/packages/spacecat-shared-data-access/src/sites/index.js b/packages/spacecat-shared-data-access/src/sites/index.js
new file mode 100644
index 000000000..e9ac693bd
--- /dev/null
+++ b/packages/spacecat-shared-data-access/src/sites/index.js
@@ -0,0 +1,59 @@
+/*
+ * 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 {
+ getSiteByBaseURL,
+ getSiteByBaseURLWithAuditInfo,
+ getSiteByBaseURLWithAudits,
+ getSiteByBaseURLWithLatestAudit, getSites, getSitesToAudit, getSitesWithLatestAudit,
+} from './accessPatterns.js';
+
+export const siteFunctions = (dynamoClient, log) => ({
+ getSites: () => getSites(
+ dynamoClient,
+ log,
+ ),
+ getSitesToAudit: () => getSitesToAudit(
+ dynamoClient,
+ log,
+ ),
+ getSitesWithLatestAudit: (auditType, sortAuditsAscending) => getSitesWithLatestAudit(
+ dynamoClient,
+ log,
+ auditType,
+ sortAuditsAscending,
+ ),
+ getSiteByBaseURL: (baseUrl) => getSiteByBaseURL(
+ dynamoClient,
+ log,
+ baseUrl,
+ ),
+ getSiteByBaseURLWithAuditInfo: (baseUrl, auditType, latestOnly) => getSiteByBaseURLWithAuditInfo(
+ dynamoClient,
+ log,
+ baseUrl,
+ auditType,
+ latestOnly,
+ ),
+ getSiteByBaseURLWithAudits: (baseUrl, auditType) => getSiteByBaseURLWithAudits(
+ dynamoClient,
+ log,
+ baseUrl,
+ auditType,
+ ),
+ getSiteByBaseURLWithLatestAudit: (baseUrl, auditType) => getSiteByBaseURLWithLatestAudit(
+ dynamoClient,
+ log,
+ baseUrl,
+ auditType,
+ ),
+});
diff --git a/packages/spacecat-shared-data-access/test/audits/index.test.js b/packages/spacecat-shared-data-access/test/audits/index.test.js
new file mode 100644
index 000000000..9e8a63db3
--- /dev/null
+++ b/packages/spacecat-shared-data-access/test/audits/index.test.js
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+
+/* eslint-env mocha */
+
+import { expect } from 'chai';
+import sinon from 'sinon';
+
+import { auditFunctions } from '../../src/audits/index.js';
+
+describe('Audit Index Tests', () => {
+ describe('Audit Functions Export Tests', () => {
+ const mockDynamoClient = {};
+ const mockLog = {};
+
+ const exportedFunctions = auditFunctions(mockDynamoClient, mockLog);
+
+ it('exports getAuditsForSite function', () => {
+ expect(exportedFunctions).to.have.property('getAuditsForSite');
+ expect(exportedFunctions.getAuditsForSite).to.be.a('function');
+ });
+
+ it('exports getLatestAudits function', () => {
+ expect(exportedFunctions).to.have.property('getLatestAudits');
+ expect(exportedFunctions.getLatestAudits).to.be.a('function');
+ });
+
+ it('exports getLatestAuditForSite function', () => {
+ expect(exportedFunctions).to.have.property('getLatestAuditForSite');
+ expect(exportedFunctions.getLatestAuditForSite).to.be.a('function');
+ });
+ });
+
+ describe('Audit Functions Tests', () => {
+ let mockDynamoClient;
+ let mockLog;
+ let exportedFunctions;
+
+ beforeEach(() => {
+ mockDynamoClient = {
+ query: sinon.stub().returns(Promise.resolve([])),
+ };
+ mockLog = { log: sinon.stub() };
+
+ exportedFunctions = auditFunctions(mockDynamoClient, mockLog);
+ });
+
+ it('calls getAuditsForSite and return an array', async () => {
+ const result = await exportedFunctions.getAuditsForSite('siteId', 'auditType');
+ expect(result).to.be.an('array');
+ // eslint-disable-next-line no-unused-expressions
+ expect(mockDynamoClient.query.called).to.be.true;
+ });
+
+ it('calls getLatestAudits and return an array', async () => {
+ const result = await exportedFunctions.getLatestAudits('auditType', true);
+ expect(result).to.be.an('array');
+ // eslint-disable-next-line no-unused-expressions
+ expect(mockDynamoClient.query.called).to.be.true;
+ });
+
+ it('calls getLatestAuditForSite and return an array', async () => {
+ const result = await exportedFunctions.getLatestAuditForSite('siteId', 'auditType');
+ // eslint-disable-next-line no-unused-expressions
+ expect(result).to.be.null;
+ // eslint-disable-next-line no-unused-expressions
+ expect(mockDynamoClient.query.called).to.be.true;
+ });
+ });
+});
diff --git a/packages/spacecat-shared-data-access/test/index.test.js b/packages/spacecat-shared-data-access/test/index.test.js
new file mode 100644
index 000000000..656b22525
--- /dev/null
+++ b/packages/spacecat-shared-data-access/test/index.test.js
@@ -0,0 +1,58 @@
+/*
+ * 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.
+ */
+
+/* eslint-env mocha */
+
+import { expect } from 'chai';
+import { createDataAccess } from '../src/index.js';
+
+describe('Data Access Object Tests', () => {
+ const auditFunctions = [
+ 'getAuditsForSite',
+ 'getLatestAudits',
+ 'getLatestAuditForSite',
+ ];
+ const siteFunctions = [
+ 'getSites',
+ 'getSitesToAudit',
+ 'getSitesWithLatestAudit',
+ 'getSiteByBaseURL',
+ 'getSiteByBaseURLWithAuditInfo',
+ 'getSiteByBaseURLWithAudits',
+ 'getSiteByBaseURLWithLatestAudit',
+ ];
+
+ let dao;
+
+ before(() => {
+ dao = createDataAccess();
+ });
+
+ it('contains all known audit functions', () => {
+ auditFunctions.forEach((funcName) => {
+ expect(dao).to.have.property(funcName);
+ });
+ });
+
+ it('contains all known site functions', () => {
+ siteFunctions.forEach((funcName) => {
+ expect(dao).to.have.property(funcName);
+ });
+ });
+
+ it('does not contain any unexpected functions', () => {
+ const expectedFunctions = new Set([...auditFunctions, ...siteFunctions]);
+ Object.keys(dao).forEach((funcName) => {
+ expect(expectedFunctions).to.include(funcName);
+ });
+ });
+});
diff --git a/packages/spacecat-shared-data-access/test/models/audit.test.js b/packages/spacecat-shared-data-access/test/models/audit.test.js
new file mode 100644
index 000000000..f6d48683e
--- /dev/null
+++ b/packages/spacecat-shared-data-access/test/models/audit.test.js
@@ -0,0 +1,69 @@
+/*
+ * 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.
+ */
+
+/* eslint-env mocha */
+
+import { expect } from 'chai';
+import { createAudit } from '../../src/models/audit.js';
+
+// Constants for testing
+const validData = {
+ siteId: '123',
+ auditedAt: new Date().toISOString(),
+ auditType: 'Type',
+ auditResult: {},
+ fullAuditRef: 'ref123',
+};
+
+describe('Audit Module Tests', () => {
+ describe('Validation Tests', () => {
+ it('throws an error if siteId is not provided', () => {
+ expect(() => createAudit({ ...validData, siteId: '' })).to.throw('Site ID must be provided');
+ });
+
+ it('throws an error if auditedAt is not a valid ISO date', () => {
+ expect(() => createAudit({ ...validData, auditedAt: 'invalid-date' })).to.throw('Audited at must be a valid ISO date');
+ });
+
+ it('throws an error if auditType is not provided', () => {
+ expect(() => createAudit({ ...validData, auditType: '' })).to.throw('Audit type must be provided');
+ });
+
+ it('throws an error if auditResult is not an object', () => {
+ expect(() => createAudit({ ...validData, auditResult: 'not-an-object' })).to.throw('Audit result must be an object');
+ });
+
+ it('throws an error if fullAuditRef is not provided', () => {
+ expect(() => createAudit({ ...validData, fullAuditRef: '' })).to.throw('Full audit ref must be provided');
+ });
+ });
+
+ describe('Functionality Tests', () => {
+ it('creates an audit object with correct properties', () => {
+ const audit = createAudit(validData);
+ expect(audit).to.be.an('object');
+ expect(audit.getSiteId()).to.equal(validData.siteId);
+ expect(audit.getAuditedAt()).to.equal(validData.auditedAt);
+ expect(audit.getAuditType()).to.equal(validData.auditType.toLowerCase());
+ expect(audit.getAuditResult()).to.deep.equal(validData.auditResult);
+ expect(audit.getFullAuditRef()).to.equal(validData.fullAuditRef);
+ });
+
+ it('automatically sets expiresAt if not provided', () => {
+ const audit = createAudit(validData);
+ expect(audit.getExpiresAt()).to.be.a('Date');
+ const expectedDate = new Date(validData.auditedAt);
+ expectedDate.setDate(expectedDate.getDate() + 30);
+ expect(audit.getExpiresAt().toDateString()).to.equal(expectedDate.toDateString());
+ });
+ });
+});
diff --git a/packages/spacecat-shared-data-access/test/models/base.test.js b/packages/spacecat-shared-data-access/test/models/base.test.js
new file mode 100644
index 000000000..e9264aad9
--- /dev/null
+++ b/packages/spacecat-shared-data-access/test/models/base.test.js
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+
+/* eslint-env mocha */
+
+import { expect } from 'chai';
+import { Base } from '../../src/models/base.js';
+
+describe('Base Entity Tests', () => {
+ describe('Initialization Tests', () => {
+ it('should automatically assign a UUID if no id is provided', () => {
+ const baseEntity = Base();
+ expect(baseEntity.getId()).to.match(/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/);
+ });
+
+ it('should retain the provided id if one is provided', () => {
+ const id = 'test-id';
+ const baseEntity = Base({ id });
+ expect(baseEntity.getId()).to.equal(id);
+ });
+ });
+
+ describe('Getter Method Tests', () => {
+ it('should correctly return the createdAt date if provided', () => {
+ const createdAt = new Date().toISOString();
+ const baseEntity = Base({ createdAt });
+ expect(baseEntity.getCreatedAt()).to.equal(createdAt);
+ });
+
+ it('should return undefined for createdAt if not provided', () => {
+ const baseEntity = Base();
+ // eslint-disable-next-line no-unused-expressions
+ expect(baseEntity.getCreatedAt()).to.be.undefined;
+ });
+
+ it('should correctly return the updatedAt date if provided', () => {
+ const updatedAt = new Date().toISOString();
+ const baseEntity = Base({ updatedAt });
+ expect(baseEntity.getUpdatedAt()).to.equal(updatedAt);
+ });
+
+ it('should return undefined for updatedAt if not provided', () => {
+ const baseEntity = Base();
+ // eslint-disable-next-line no-unused-expressions
+ expect(baseEntity.getUpdatedAt()).to.be.undefined;
+ });
+ });
+});
diff --git a/packages/spacecat-shared-data-access/test/models/site.test.js b/packages/spacecat-shared-data-access/test/models/site.test.js
new file mode 100644
index 000000000..899c8fadc
--- /dev/null
+++ b/packages/spacecat-shared-data-access/test/models/site.test.js
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+
+/* eslint-env mocha */
+
+import { expect } from 'chai';
+import { createSite } from '../../src/models/site.js';
+
+// Constants for testing
+const validData = {
+ baseURL: 'https://www.example.com',
+ imsOrgId: 'org123',
+};
+
+describe('Site Module Tests', () => {
+ describe('Validation Tests', () => {
+ it('throws an error if baseURL is not a valid URL', () => {
+ expect(() => createSite({ ...validData, baseURL: 'invalid-url' })).to.throw('Base URL must be a valid URL');
+ });
+
+ it('creates a site object with valid baseURL', () => {
+ const site = createSite({ ...validData });
+ expect(site).to.be.an('object');
+ expect(site.getBaseURL()).to.equal(validData.baseURL);
+ });
+ });
+
+ describe('Site Object Functionality', () => {
+ let site;
+
+ beforeEach(() => {
+ // Reset site object before each test
+ site = createSite(validData);
+ });
+
+ it('updates baseURL correctly', () => {
+ const newURL = 'https://www.newexample.com';
+ site.updateBaseURL(newURL);
+ expect(site.getBaseURL()).to.equal(newURL);
+ });
+
+ it('throws an error when updating with an invalid baseURL', () => {
+ expect(() => site.updateBaseURL('invalid-url')).to.throw('Base URL must be a valid URL');
+ });
+
+ it('updates imsOrgId correctly', () => {
+ const newImsOrgId = 'newOrg123';
+ site.updateImsOrgId(newImsOrgId);
+ expect(site.getImsOrgId()).to.equal(newImsOrgId);
+ });
+
+ it('throws an error when updating with an empty imsOrgId', () => {
+ expect(() => site.updateImsOrgId('')).to.throw('IMS Org ID must be provided');
+ });
+ });
+});
diff --git a/packages/spacecat-shared-data-access/test/sites/index.test.js b/packages/spacecat-shared-data-access/test/sites/index.test.js
new file mode 100644
index 000000000..a3874983b
--- /dev/null
+++ b/packages/spacecat-shared-data-access/test/sites/index.test.js
@@ -0,0 +1,214 @@
+/*
+ * 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.
+ */
+
+/* eslint-env mocha */
+
+import { expect } from 'chai';
+import sinon from 'sinon';
+
+import { siteFunctions } from '../../src/sites/index.js';
+
+describe('Site Index Tests', () => {
+ describe('Site Functions Export Tests', () => {
+ const mockDynamoClient = {};
+ const mockLog = {};
+
+ const exportedFunctions = siteFunctions(mockDynamoClient, mockLog);
+
+ it('exports getSites function', () => {
+ expect(exportedFunctions).to.have.property('getSites');
+ expect(exportedFunctions.getSites).to.be.a('function');
+ });
+
+ it('exports getSitesToAudit function', () => {
+ expect(exportedFunctions).to.have.property('getSitesToAudit');
+ expect(exportedFunctions.getSitesToAudit).to.be.a('function');
+ });
+
+ it('exports getSitesWithLatestAudit function', () => {
+ expect(exportedFunctions).to.have.property('getSitesWithLatestAudit');
+ expect(exportedFunctions.getSitesWithLatestAudit).to.be.a('function');
+ });
+
+ it('exports getSiteByBaseURL function', () => {
+ expect(exportedFunctions).to.have.property('getSiteByBaseURL');
+ expect(exportedFunctions.getSiteByBaseURL).to.be.a('function');
+ });
+
+ it('exports getSiteByBaseURLWithAuditInfo function', () => {
+ expect(exportedFunctions).to.have.property('getSiteByBaseURLWithAuditInfo');
+ expect(exportedFunctions.getSiteByBaseURLWithAuditInfo).to.be.a('function');
+ });
+
+ it('exports getSiteByBaseURLWithAudits function', () => {
+ expect(exportedFunctions).to.have.property('getSiteByBaseURLWithAudits');
+ expect(exportedFunctions.getSiteByBaseURLWithAudits).to.be.a('function');
+ });
+
+ it('exports getSiteByBaseURLWithLatestAudit function', () => {
+ expect(exportedFunctions).to.have.property('getSiteByBaseURLWithLatestAudit');
+ expect(exportedFunctions.getSiteByBaseURLWithLatestAudit).to.be.a('function');
+ });
+ });
+
+ describe('Site Functions Tests', () => {
+ let mockDynamoClient;
+ let mockLog;
+ let exportedFunctions;
+
+ beforeEach(() => {
+ mockDynamoClient = {
+ query: sinon.stub().returns(Promise.resolve([])),
+ getItem: sinon.stub().returns(Promise.resolve(null)),
+ };
+ mockLog = { log: sinon.stub() };
+
+ exportedFunctions = siteFunctions(mockDynamoClient, mockLog);
+ });
+
+ it('calls getSites and returns an array', async () => {
+ const result = await exportedFunctions.getSites();
+ expect(result).to.be.an('array');
+ // eslint-disable-next-line no-unused-expressions
+ expect(mockDynamoClient.query.called).to.be.true;
+ });
+
+ it('calls getSitesToAudit and returns an array', async () => {
+ const result = await exportedFunctions.getSitesToAudit();
+ expect(result).to.be.an('array');
+ // eslint-disable-next-line no-unused-expressions
+ expect(mockDynamoClient.query.called).to.be.true;
+ });
+
+ it('calls getSitesWithLatestAudit and returns an array', async () => {
+ const result = await exportedFunctions.getSitesWithLatestAudit();
+ expect(result).to.be.an('array');
+ // eslint-disable-next-line no-unused-expressions
+ expect(mockDynamoClient.query.called).to.be.true;
+ });
+
+ it('calls getSitesWithLatestAudit and handles latestAudits', async () => {
+ const mockSiteData = [{
+ id: 'site1',
+ baseUrl: 'https://example.com',
+ }];
+
+ const mockAuditData = [{
+ id: 'audit1',
+ siteId: 'site1',
+ auditType: 'type1',
+ }];
+
+ mockDynamoClient.query.onFirstCall().resolves(mockSiteData);
+ mockDynamoClient.query.onSecondCall().resolves(mockAuditData);
+
+ const result = await exportedFunctions.getSitesWithLatestAudit('auditType');
+ // eslint-disable-next-line no-unused-expressions
+ expect(result).to.be.an('array').that.has.lengthOf(1);
+ });
+
+ it('calls getSitesWithLatestAudit and handles empty latestAudits', async () => {
+ const mockSiteData = [{
+ id: 'site1',
+ baseUrl: 'https://example.com',
+ }];
+
+ const mockAuditData = [];
+
+ mockDynamoClient.query.onFirstCall().resolves(mockSiteData);
+ mockDynamoClient.query.onSecondCall().resolves(mockAuditData);
+
+ const result = await exportedFunctions.getSitesWithLatestAudit('auditType');
+ // eslint-disable-next-line no-unused-expressions
+ expect(result).to.be.an('array').that.is.empty;
+ });
+
+ it('calls getSiteByBaseURL and returns an array/object', async () => {
+ const result = await exportedFunctions.getSiteByBaseURL();
+ // eslint-disable-next-line no-unused-expressions
+ expect(result).to.be.null;
+ // eslint-disable-next-line no-unused-expressions
+ expect(mockDynamoClient.getItem.called).to.be.true;
+ });
+
+ it('calls getSiteByBaseURLWithAuditInfo and returns an array/object', async () => {
+ const result = await exportedFunctions.getSiteByBaseURLWithAuditInfo();
+ // eslint-disable-next-line no-unused-expressions
+ expect(result).to.be.null;
+ // eslint-disable-next-line no-unused-expressions
+ expect(mockDynamoClient.getItem.called).to.be.true;
+ });
+
+ it('calls getSiteByBaseURLWithAuditInfo and returns null when site is undefined', async () => {
+ mockDynamoClient.query.resolves(undefined);
+
+ const result = await exportedFunctions.getSiteByBaseURLWithAuditInfo('baseUrl', 'auditType');
+ // eslint-disable-next-line no-unused-expressions
+ expect(result).to.be.null;
+ });
+
+ it('calls getSiteByBaseURLWithAuditInfo and assigns latest audit when latestOnly is true', async () => {
+ const mockSiteData = {
+ id: 'site1',
+ baseUrl: 'https://example.com',
+ };
+
+ const mockLatestAuditData = [{
+ id: 'audit1',
+ siteId: 'site1',
+ auditType: 'type1',
+ }];
+
+ mockDynamoClient.getItem.onFirstCall().resolves(mockSiteData);
+ mockDynamoClient.query.onFirstCall().resolves(mockLatestAuditData);
+
+ const result = await exportedFunctions.getSiteByBaseURLWithAuditInfo('https://example.com', 'type1', true);
+ expect(result).to.have.property('audits').that.is.an('array').with.lengthOf(1);
+ expect(result.audits[0]).to.deep.equal(mockLatestAuditData[0]);
+ });
+
+ it('calls getSiteByBaseURLWithAuditInfo and assigns all audits when latestOnly is false', async () => {
+ const mockSiteData = {
+ id: 'site1',
+ baseUrl: 'https://example.com',
+ };
+
+ const mockLatestAuditData = [{
+ id: 'audit1',
+ siteId: 'site1',
+ auditType: 'type1',
+ }];
+
+ mockDynamoClient.getItem.onFirstCall().resolves(mockSiteData);
+ mockDynamoClient.query.onFirstCall().resolves(mockLatestAuditData);
+
+ const result = await exportedFunctions.getSiteByBaseURLWithAuditInfo('baseUrl', 'auditType', false);
+ expect(result).to.have.property('audits').that.is.an('array');
+ });
+
+ it('calls getSiteByBaseURLWithAudits and returns an array/object', async () => {
+ const result = await exportedFunctions.getSiteByBaseURLWithAudits();
+ // eslint-disable-next-line no-unused-expressions
+ expect(result).to.be.null;
+ // eslint-disable-next-line no-unused-expressions
+ expect(mockDynamoClient.getItem.called).to.be.true;
+ });
+
+ it('calls getSiteByBaseURLWithLatestAudit and returns an array/object', async () => {
+ const result = await exportedFunctions.getSiteByBaseURLWithLatestAudit();
+ // eslint-disable-next-line no-unused-expressions
+ expect(result).to.be.null;
+ // eslint-disable-next-line no-unused-expressions
+ expect(mockDynamoClient.getItem.called).to.be.true;
+ });
+ });
+});
From 25cbba04843d59a965f8a826c5a1b7e2ef21c82e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Thu, 30 Nov 2023 10:06:13 +0100
Subject: [PATCH 04/28] feat: add data access layer
---
.eslintrc.cjs | 6 +
package-lock.json | 15 +-
.../spacecat-shared-data-access/package.json | 1 +
.../src/audits/accessPatterns.js | 96 ++++++++++--
.../src/audits/index.js | 19 ++-
.../src/dto/audit.js | 60 ++++++++
.../src/dto/site.js | 49 +++++++
.../src/index.d.ts | 39 +++--
.../src/models/audit.js | 30 ++++
.../src/models/base.js | 6 +
.../src/models/site.js | 24 ++-
.../src/sites/accessPatterns.js | 123 ++++++++++++----
.../src/sites/index.js | 11 +-
.../test/audits/index.test.js | 83 ++++++++++-
.../test/index.test.js | 4 +
.../test/models/base.test.js | 2 -
.../test/models/site.test.js | 1 -
.../test/sites/index.test.js | 137 ++++++++++++++----
.../spacecat-shared-dynamo/src/index.d.ts | 9 +-
.../src/modules/getItem.js | 2 +-
.../src/modules/removeItem.js | 2 +-
21 files changed, 616 insertions(+), 103 deletions(-)
create mode 100644 packages/spacecat-shared-data-access/src/dto/audit.js
create mode 100644 packages/spacecat-shared-data-access/src/dto/site.js
diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 3462360d2..cdc3af607 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -34,5 +34,11 @@ module.exports = {
files: ['*.js', '*.cjs'],
rules: {},
},
+ {
+ files: ["*.test.js"],
+ rules: {
+ "no-unused-expressions": "off"
+ }
+ }
],
};
diff --git a/package-lock.json b/package-lock.json
index 0e9459ca5..efe9d97a7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -2856,6 +2856,18 @@
"node": ">=4"
}
},
+ "node_modules/chai-as-promised": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz",
+ "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==",
+ "dev": true,
+ "dependencies": {
+ "check-error": "^1.0.2"
+ },
+ "peerDependencies": {
+ "chai": ">= 2.1.2 < 5"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -12249,7 +12261,8 @@
},
"devDependencies": {
"chai": "4.3.10",
- "sinon": "^17.0.1"
+ "chai-as-promised": "^7.1.1",
+ "sinon": "17.0.1"
}
},
"packages/spacecat-shared-data-access/node_modules/@adobe/spacecat-shared-dynamo": {
diff --git a/packages/spacecat-shared-data-access/package.json b/packages/spacecat-shared-data-access/package.json
index 04ff67eb5..87c38fd66 100644
--- a/packages/spacecat-shared-data-access/package.json
+++ b/packages/spacecat-shared-data-access/package.json
@@ -35,6 +35,7 @@
},
"devDependencies": {
"chai": "4.3.10",
+ "chai-as-promised": "7.1.1",
"sinon": "17.0.1"
}
}
diff --git a/packages/spacecat-shared-data-access/src/audits/accessPatterns.js b/packages/spacecat-shared-data-access/src/audits/accessPatterns.js
index 3be1877a2..a3b0ac77d 100644
--- a/packages/spacecat-shared-data-access/src/audits/accessPatterns.js
+++ b/packages/spacecat-shared-data-access/src/audits/accessPatterns.js
@@ -10,6 +10,11 @@
* governing permissions and limitations under the License.
*/
+import { isObject } from '@adobe/spacecat-shared-utils';
+
+import { AuditDto } from '../dto/audit.js';
+import { createAudit } from '../models/audit.js';
+
/**
* Retrieves audits for a specified site. If an audit type is provided,
* it returns only audits of that type.
@@ -35,7 +40,39 @@ export const getAuditsForSite = async (dynamoClient, log, siteId, auditType) =>
queryParams.ExpressionAttributeValues[':auditType'] = `${auditType}#`;
}
- return dynamoClient.query(queryParams);
+ const dynamoItems = await dynamoClient.query(queryParams);
+
+ return dynamoItems.map((item) => AuditDto.fromDynamoItem(item));
+};
+
+/**
+ * Retrieves a specific audit for a specified site.
+ *
+ * @param {DynamoDbClient} dynamoClient - The DynamoDB client.
+ * @param {Logger} log - The logger.
+ * @param {string} siteId - The ID of the site for which to retrieve the audit.
+ * @param {string} auditType - The type of audit to retrieve.
+ * @param auditedAt - The ISO 8601 timestamp of the audit.
+ * @returns {Promise|null>}
+ */
+export const getAuditForSite = async (
+ dynamoClient,
+ log,
+ siteId,
+ auditType,
+ auditedAt,
+) => {
+ const audit = await dynamoClient.query({
+ TableName: 'audits',
+ KeyConditionExpression: 'siteId = :siteId AND SK = :sk',
+ ExpressionAttributeValues: {
+ ':siteId': siteId,
+ ':sk': `${auditType}#${auditedAt}}`,
+ },
+ Limit: 1,
+ });
+
+ return audit.length > 0 ? AuditDto.fromDynamoItem(audit[0]) : null;
};
/**
@@ -54,16 +91,20 @@ export const getLatestAudits = async (
log,
auditType,
ascending = true,
-) => dynamoClient.query({
- TableName: 'latest_audits',
- IndexName: 'all_latest_audit_scores',
- KeyConditionExpression: 'GSI1PK = :gsi1pk AND begins_with(GSI1SK, :auditType)',
- ExpressionAttributeValues: {
- ':gsi1pk': 'ALL_LATEST_AUDITS',
- ':auditType': `${auditType}#`,
- },
- ScanIndexForward: ascending, // Sorts ascending if true, descending if false
-});
+) => {
+ const dynamoItems = await dynamoClient.query({
+ TableName: 'latest_audits',
+ IndexName: 'all_latest_audit_scores',
+ KeyConditionExpression: 'GSI1PK = :gsi1pk AND begins_with(GSI1SK, :auditType)',
+ ExpressionAttributeValues: {
+ ':gsi1pk': 'ALL_LATEST_AUDITS',
+ ':auditType': `${auditType}#`,
+ },
+ ScanIndexForward: ascending, // Sorts ascending if true, descending if false
+ });
+
+ return dynamoItems.map((item) => AuditDto.fromDynamoItem(item));
+};
/**
* Retrieves the latest audit for a specified site and audit type.
@@ -72,7 +113,7 @@ export const getLatestAudits = async (
* @param {Logger} log - The logger.
* @param {string} siteId - The ID of the site for which the latest audit is being retrieved.
* @param {string} auditType - The type of audit to retrieve the latest instance of.
- * @returns {Promise} A promise that resolves to the latest audit of the
+ * @returns {Promise} A promise that resolves to the latest audit of the
* specified type for the site, or null if none is found.
*/
export const getLatestAuditForSite = async (
@@ -91,5 +132,34 @@ export const getLatestAuditForSite = async (
Limit: 1,
});
- return latestAudit.length > 0 ? latestAudit[0] : null;
+ return latestAudit.length > 0 ? AuditDto.fromDynamoItem(latestAudit[0]) : null;
+};
+
+/**
+ * Adds an audit.
+ *
+ * @param {DynamoDbClient} dynamoClient - The DynamoDB client.
+ * @param {Logger} log - The logger.
+ * @param {object} auditData - The audit data.
+ * @returns {Promise>}
+ */
+export const addAudit = async (dynamoClient, log, auditData) => {
+ const audit = createAudit(auditData);
+ const existingAudit = await getAuditForSite(
+ dynamoClient,
+ log,
+ audit.getSiteId(),
+ audit.getAuditType(),
+ audit.getAuditedAt(),
+ );
+
+ if (isObject(existingAudit)) {
+ throw new Error('Audit already exists');
+ }
+
+ // TODO: Add transaction support
+ await dynamoClient.putItem('audits', AuditDto.toDynamoItem(audit));
+ await dynamoClient.putItem('latest_audits', AuditDto.toDynamoItem(audit, true));
+
+ return audit;
};
diff --git a/packages/spacecat-shared-data-access/src/audits/index.js b/packages/spacecat-shared-data-access/src/audits/index.js
index 3230ce4f0..87645dde7 100644
--- a/packages/spacecat-shared-data-access/src/audits/index.js
+++ b/packages/spacecat-shared-data-access/src/audits/index.js
@@ -10,9 +10,21 @@
* governing permissions and limitations under the License.
*/
-import { getAuditsForSite, getLatestAuditForSite, getLatestAudits } from './accessPatterns.js';
+import {
+ addAudit, getAuditForSite,
+ getAuditsForSite,
+ getLatestAuditForSite,
+ getLatestAudits,
+} from './accessPatterns.js';
export const auditFunctions = (dynamoClient, log) => ({
+ getAuditForSite: (siteId, auditType, auditedAt) => getAuditForSite(
+ dynamoClient,
+ log,
+ siteId,
+ auditType,
+ auditedAt,
+ ),
getAuditsForSite: (siteId, auditType) => getAuditsForSite(
dynamoClient,
log,
@@ -31,4 +43,9 @@ export const auditFunctions = (dynamoClient, log) => ({
siteId,
auditType,
),
+ addAudit: (auditData) => addAudit(
+ dynamoClient,
+ log,
+ auditData,
+ ),
});
diff --git a/packages/spacecat-shared-data-access/src/dto/audit.js b/packages/spacecat-shared-data-access/src/dto/audit.js
new file mode 100644
index 000000000..ba621f439
--- /dev/null
+++ b/packages/spacecat-shared-data-access/src/dto/audit.js
@@ -0,0 +1,60 @@
+/*
+ * 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 { createAudit } from '../models/audit.js';
+
+/**
+ * Data transfer object for Audit.
+ */
+export const AuditDto = {
+ /**
+ * Converts a Audit object into a DynamoDB item.
+ * @param {Readonly} audit - Audit object.
+ * @param {boolean} latestAudit - If true, returns the latest audit flavor.
+ * @returns {{siteId, auditedAt, auditResult, auditType, expiresAt, fullAuditRef, SK: string}}
+ */
+ toDynamoItem: (audit, latestAudit = false) => {
+ const latestAuditProps = latestAudit ? {
+ GSI1PK: 'ALL_LATEST_AUDITS',
+ GSI1SK: `${audit.getAuditType()}#${Object.values(audit.getScores()).join('#')}`,
+ } : {};
+
+ return {
+ siteId: audit.getSiteId(),
+ auditedAt: audit.getAuditedAt(),
+ auditResult: audit.getAuditResult(),
+ auditType: audit.getAuditType(),
+ expiresAt: audit.getExpiresAt(),
+ fullAuditRef: audit.getFullAuditRef(),
+ SK: `${audit.getAuditType()}#${audit.getAuditedAt()}`,
+ ...latestAuditProps,
+ };
+ },
+
+ /**
+ * Converts a DynamoDB item into a Audit object.
+ * @param {object } dynamoItem - DynamoDB item.
+ * @returns {Readonly} Audit object.
+ */
+ fromDynamoItem: (dynamoItem) => {
+ const auditData = {
+ siteId: dynamoItem.siteId,
+ auditedAt: dynamoItem.auditedAt,
+ auditResult: dynamoItem.auditResult,
+ auditType: dynamoItem.auditType,
+ expiresAt: dynamoItem.expiresAt,
+ fullAuditRef: dynamoItem.fullAuditRef,
+ };
+
+ return createAudit(auditData);
+ },
+};
diff --git a/packages/spacecat-shared-data-access/src/dto/site.js b/packages/spacecat-shared-data-access/src/dto/site.js
new file mode 100644
index 000000000..c2203f5dc
--- /dev/null
+++ b/packages/spacecat-shared-data-access/src/dto/site.js
@@ -0,0 +1,49 @@
+/*
+ * 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 { createSite } from '../models/site.js';
+
+/**
+ * Data transfer object for Site.
+ */
+export const SiteDto = {
+ /**
+ * Converts a Site object into a DynamoDB item.
+ * @param {Readonly} site - Site object.
+ * @returns {{createdAt, baseURL, GSI1PK: string, id, imsOrgId, updatedAt}}
+ */
+ toDynamoItem: (site) => ({
+ id: site.getId(),
+ baseURL: site.getBaseURL(),
+ imsOrgId: site.getImsOrgId(),
+ createdAt: site.getCreatedAt(),
+ updatedAt: site.getUpdatedAt(),
+ GSI1PK: 'ALL_SITES',
+ }),
+
+ /**
+ * Converts a DynamoDB item into a Site object.
+ * @param {object } dynamoItem - DynamoDB item.
+ * @returns {Readonly} Site object.
+ */
+ fromDynamoItem: (dynamoItem) => {
+ const siteData = {
+ id: dynamoItem.id,
+ baseURL: dynamoItem.baseURL,
+ imsOrgId: dynamoItem.imsOrgId,
+ createdAt: dynamoItem.createdAt,
+ updatedAt: dynamoItem.updatedAt,
+ };
+
+ return createSite(siteData);
+ },
+};
diff --git a/packages/spacecat-shared-data-access/src/index.d.ts b/packages/spacecat-shared-data-access/src/index.d.ts
index 2605daff7..3aa03b507 100644
--- a/packages/spacecat-shared-data-access/src/index.d.ts
+++ b/packages/spacecat-shared-data-access/src/index.d.ts
@@ -10,23 +10,26 @@
* governing permissions and limitations under the License.
*/
-export interface Site {
- id: string;
- baseURL: string;
- imsOrgId: string;
- createdAt: string;
- updatedAt: string;
+export interface Audit {
+ getSiteId: () => string;
+ getAuditedAt: () => string;
+ getAuditResult: () => object;
+ getAuditType: () => object;
+ getExpiresAt: () => Date;
+ getFullAuditRef: () => string;
+ getScores: () => object;
}
-export interface Audit {
- siteId: string;
- auditedAt: string;
- auditResult: object;
- auditType: string;
- expiresAt: number;
- fullAuditRef: string;
- createdAt: string;
- updatedAt: string;
+export interface Site {
+ getId: () => string;
+ getBaseURL: () => string;
+ getImsOrgId: () => string;
+ getCreatedAt: () => string;
+ getUpdatedAt: () => string;
+ getAudits: () => Audit[];
+ updateBaseURL: (baseURL: string) => Site;
+ updateImsOrgId: (imsOrgId: string) => Site;
+ setAudits: (audits: Audit[]) => Site;
}
export interface DataAccess {
@@ -64,6 +67,12 @@ export interface DataAccess {
baseUrl: string,
auditType: string,
) => Promise;
+ addSite: (
+ siteData: object,
+ ) => Promise;
+ updateSite: (
+ site: Site,
+ ) => Promise;
}
export function createDataAccess(
diff --git a/packages/spacecat-shared-data-access/src/models/audit.js b/packages/spacecat-shared-data-access/src/models/audit.js
index 76d7c37f5..2d5668df9 100644
--- a/packages/spacecat-shared-data-access/src/models/audit.js
+++ b/packages/spacecat-shared-data-access/src/models/audit.js
@@ -13,8 +13,15 @@
import { hasText, isIsoDate, isObject } from '@adobe/spacecat-shared-utils';
import { Base } from './base.js';
+export const AUDIT_TYPE_LHS = 'lhs';
+
const EXPIRES_IN_DAYS = 30;
+/**
+ * Creates a new Audit.
+ * @param {object } data - audit data
+ * @returns {Readonly} audit - new audit
+ */
const Audit = (data = {}) => {
const self = Base(data);
@@ -24,10 +31,33 @@ const Audit = (data = {}) => {
self.getAuditType = () => self.state.auditType.toLowerCase();
self.getExpiresAt = () => self.state.expiresAt;
self.getFullAuditRef = () => self.state.fullAuditRef;
+ self.getScores = () => {
+ const auditResult = self.getAuditResult();
+
+ if (self.getAuditType() === AUDIT_TYPE_LHS) {
+ const {
+ performance, seo, accessibility, bestPractices,
+ } = auditResult;
+ return {
+ performance,
+ seo,
+ accessibility,
+ bestPractices,
+ };
+ }
+
+ return auditResult;
+ };
return Object.freeze(self);
};
+/**
+ * Creates a new Audit.
+ *
+ * @param {object } data - audit data
+ * @returns {Readonly} audit - new audit
+ */
export const createAudit = (data) => {
const newState = { ...data };
diff --git a/packages/spacecat-shared-data-access/src/models/base.js b/packages/spacecat-shared-data-access/src/models/base.js
index 797fa26fa..e75dd979f 100644
--- a/packages/spacecat-shared-data-access/src/models/base.js
+++ b/packages/spacecat-shared-data-access/src/models/base.js
@@ -13,6 +13,12 @@
import { v4 as uuidv4 } from 'uuid';
import { isString } from '@adobe/spacecat-shared-utils';
+/**
+ * Base model.
+ *
+ * @param {object} data data
+ * @returns {Base} base model
+ */
export const Base = (data = {}) => {
const self = { state: { ...data } };
diff --git a/packages/spacecat-shared-data-access/src/models/site.js b/packages/spacecat-shared-data-access/src/models/site.js
index b08587d05..61d5cb8b0 100644
--- a/packages/spacecat-shared-data-access/src/models/site.js
+++ b/packages/spacecat-shared-data-access/src/models/site.js
@@ -10,12 +10,19 @@
* governing permissions and limitations under the License.
*/
-import { hasText, isValidUrl } from '@adobe/spacecat-shared-utils';
+import { hasText, isObject, isValidUrl } from '@adobe/spacecat-shared-utils';
import { Base } from './base.js';
+/**
+ * Creates a new Site.
+ *
+ * @param {object} data - site data
+ * @returns {Readonly} site - new site
+ */
const Site = (data = {}) => {
const self = Base(data);
+ self.getAudits = () => self.state.audits;
self.getBaseURL = () => self.state.baseURL;
self.getImsOrgId = () => self.state.imsOrgId;
@@ -37,9 +44,20 @@ const Site = (data = {}) => {
return self;
};
+ self.setAudits = (audits) => {
+ self.state.audits = audits;
+ return self;
+ };
+
return Object.freeze(self);
};
+/**
+ * Creates a new Site.
+ *
+ * @param {object} data - site data
+ * @returns {Readonly} site - new site
+ */
export const createSite = (data) => {
const newState = { ...data };
@@ -47,5 +65,9 @@ export const createSite = (data) => {
throw new Error('Base URL must be a valid URL');
}
+ if (!isObject(newState.audits)) {
+ newState.audits = {};
+ }
+
return Site(newState);
};
diff --git a/packages/spacecat-shared-data-access/src/sites/accessPatterns.js b/packages/spacecat-shared-data-access/src/sites/accessPatterns.js
index ca862e493..7116b9d6e 100644
--- a/packages/spacecat-shared-data-access/src/sites/accessPatterns.js
+++ b/packages/spacecat-shared-data-access/src/sites/accessPatterns.js
@@ -10,38 +10,50 @@
* governing permissions and limitations under the License.
*/
+import { isObject } from '@adobe/spacecat-shared-utils';
+
import {
getAuditsForSite,
getLatestAuditForSite,
getLatestAudits,
} from '../audits/accessPatterns.js';
+import { createSite } from '../models/site.js';
+import { SiteDto } from '../dto/site.js';
+
+const INDEX_NAME_ALL_SITES = 'all_sites';
+const PK_ALL_SITES = 'ALL_SITES';
+const TABLE_NAME_SITES = 'sites';
+
/**
* Retrieves all sites.
*
* @param {DynamoDbClient} dynamoClient - The DynamoDB client.
- * @returns {Promise} A promise that resolves to an array of all sites.
+ * @returns {Promise>} A promise that resolves to an array of all sites.
*/
-export const getSites = async (dynamoClient) => dynamoClient.query({
- TableName: 'sites',
- IndexName: 'all_sites', // GSI name
- KeyConditionExpression: 'GSI1PK = :gsi1pk',
- ExpressionAttributeValues: {
- ':gsi1pk': 'ALL_SITES',
- },
-});
+export const getSites = async (dynamoClient) => {
+ const dynamoItems = await dynamoClient.query({
+ TableName: TABLE_NAME_SITES,
+ IndexName: INDEX_NAME_ALL_SITES, // GSI name
+ KeyConditionExpression: 'GSI1PK = :gsi1pk',
+ ExpressionAttributeValues: {
+ ':gsi1pk': PK_ALL_SITES,
+ },
+ });
+
+ return dynamoItems.map((dynamoItem) => SiteDto.fromDynamoItem(dynamoItem));
+};
/**
* Retrieves a list of base URLs for all sites.
*
* @param {DynamoDbClient} dynamoClient - The DynamoDB client.
- * @param {Logger} log - The logger.
* @returns {Promise>} A promise that resolves to an array of base URLs for all sites.
*/
-export const getSitesToAudit = async (dynamoClient, log) => {
- const sites = await getSites(dynamoClient, log);
+export const getSitesToAudit = async (dynamoClient) => {
+ const sites = await getSites(dynamoClient);
- return sites.map((item) => item.baseURL);
+ return sites.map((site) => site.getBaseURL());
};
/**
@@ -66,12 +78,12 @@ export const getSitesWithLatestAudit = async (
getLatestAudits(dynamoClient, log, auditType, sortAuditsAscending),
]);
- const sitesMap = new Map(sites.map((site) => [site.id, site]));
+ const sitesMap = new Map(sites.map((site) => [site.getId(), site]));
return latestAudits.reduce((result, audit) => {
- const site = sitesMap.get(audit.siteId);
+ const site = sitesMap.get(audit.getSiteId());
if (site) {
- site.audits = [audit];
+ site.setAudits([audit]);
result.push(site);
}
return result;
@@ -84,17 +96,25 @@ export const getSitesWithLatestAudit = async (
* @param {DynamoDbClient} dynamoClient - The DynamoDB client.
* @param {Logger} log - The logger.
* @param {string} baseUrl - The base URL of the site to retrieve.
- * @returns {Promise} A promise that resolves to the site object if found,
+ * @returns {Promise} A promise that resolves to the site object if found,
* otherwise null.
*/
export const getSiteByBaseURL = async (
dynamoClient,
log,
baseUrl,
-) => dynamoClient.getItem('sites', {
- GSI1PK: 'ALL_SITES',
- baseUrl,
-});
+) => {
+ const dynamoItem = await dynamoClient.getItem(TABLE_NAME_SITES, {
+ GSI1PK: PK_ALL_SITES,
+ baseUrl,
+ });
+
+ if (dynamoItem === null) {
+ return null;
+ }
+
+ return SiteDto.fromDynamoItem(dynamoItem);
+};
/**
* Retrieves a site by its base URL, along with associated audit information.
@@ -104,7 +124,7 @@ export const getSiteByBaseURL = async (
* @param {string} baseUrl - The base URL of the site to retrieve.
* @param {string} auditType - The type of audits to retrieve for the site.
* @param {boolean} [latestOnly=false] - Determines if only the latest audit should be retrieved.
- * @returns {Promise} A promise that resolves to the site object with audit
+ * @returns {Promise} A promise that resolves to the site object with audit
* data if found, otherwise null.
*/
export const getSiteByBaseURLWithAuditInfo = async (
@@ -116,24 +136,26 @@ export const getSiteByBaseURLWithAuditInfo = async (
) => {
const site = await getSiteByBaseURL(dynamoClient, log, baseUrl);
- if (!site) {
+ if (!isObject(site)) {
return null;
}
- site.audits = latestOnly
+ const audits = latestOnly
? [await getLatestAuditForSite(
dynamoClient,
log,
- site.id,
+ site.getId(),
auditType,
)].filter((audit) => audit != null)
: await getAuditsForSite(
dynamoClient,
log,
- site.id,
+ site.getId(),
auditType,
);
+ site.setAudits(audits);
+
return site;
};
@@ -144,7 +166,7 @@ export const getSiteByBaseURLWithAuditInfo = async (
* @param {Logger} log - The logger.
* @param {string} baseUrl - The base URL of the site to retrieve.
* @param {string} auditType - The type of audits to retrieve for the site.
- * @returns {Promise} A promise that resolves to the site object with all its audits.
+ * @returns {Promise} A promise that resolves to the site object with all its audits.
*/
export const getSiteByBaseURLWithAudits = async (
dynamoClient,
@@ -160,7 +182,7 @@ export const getSiteByBaseURLWithAudits = async (
* @param {Logger} log - The logger.
* @param {string} baseUrl - The base URL of the site to retrieve.
* @param {string} auditType - The type of the latest audit to retrieve for the site.
- * @returns {Promise} A promise that resolves to the site object with its latest audit.
+ * @returns {Promise} A promise that resolves to the site object with its latest audit.
*/
export const getSiteByBaseURLWithLatestAudit = async (
dynamoClient,
@@ -168,3 +190,48 @@ export const getSiteByBaseURLWithLatestAudit = async (
baseUrl,
auditType,
) => getSiteByBaseURLWithAuditInfo(dynamoClient, log, baseUrl, auditType, true);
+
+/**
+ * Adds a site.
+ *
+ * @param {DynamoDbClient} dynamoClient - The DynamoDB client.
+ * @param {Logger} log - The logger.
+ * @param {object} siteData - The site data.
+ * @returns {Promise>}
+ */
+export const addSite = async (dynamoClient, log, siteData) => {
+ const site = createSite(siteData);
+ const existingSite = await getSiteByBaseURL(
+ dynamoClient,
+ log,
+ site.getBaseURL(),
+ );
+
+ if (isObject(existingSite)) {
+ throw new Error('Site already exists');
+ }
+
+ await dynamoClient.putItem(TABLE_NAME_SITES, SiteDto.toDynamoItem(site));
+
+ return site;
+};
+
+/**
+ * Updates a site.
+ *
+ * @param {DynamoDbClient} dynamoClient - The DynamoDB client.
+ * @param {Logger} log - The logger.
+ * @param {Site} site - The site.
+ * @returns {Promise} - The updated site.
+ */
+export const updateSite = async (dynamoClient, log, site) => {
+ const existingSite = await getSiteByBaseURL(dynamoClient, log, site.getBaseURL());
+
+ if (!isObject(existingSite)) {
+ throw new Error('Site not found');
+ }
+
+ await dynamoClient.putItem(TABLE_NAME_SITES, SiteDto.toDynamoItem(site));
+
+ return site;
+};
diff --git a/packages/spacecat-shared-data-access/src/sites/index.js b/packages/spacecat-shared-data-access/src/sites/index.js
index e9ac693bd..1009eccbe 100644
--- a/packages/spacecat-shared-data-access/src/sites/index.js
+++ b/packages/spacecat-shared-data-access/src/sites/index.js
@@ -11,20 +11,23 @@
*/
import {
+ addSite,
getSiteByBaseURL,
getSiteByBaseURLWithAuditInfo,
getSiteByBaseURLWithAudits,
- getSiteByBaseURLWithLatestAudit, getSites, getSitesToAudit, getSitesWithLatestAudit,
+ getSiteByBaseURLWithLatestAudit,
+ getSites,
+ getSitesToAudit,
+ getSitesWithLatestAudit,
+ updateSite,
} from './accessPatterns.js';
export const siteFunctions = (dynamoClient, log) => ({
getSites: () => getSites(
dynamoClient,
- log,
),
getSitesToAudit: () => getSitesToAudit(
dynamoClient,
- log,
),
getSitesWithLatestAudit: (auditType, sortAuditsAscending) => getSitesWithLatestAudit(
dynamoClient,
@@ -56,4 +59,6 @@ export const siteFunctions = (dynamoClient, log) => ({
baseUrl,
auditType,
),
+ addSite: (siteData) => addSite(dynamoClient, log, siteData),
+ updateSite: (site) => updateSite(dynamoClient, log, site),
});
diff --git a/packages/spacecat-shared-data-access/test/audits/index.test.js b/packages/spacecat-shared-data-access/test/audits/index.test.js
index 9e8a63db3..8c689596e 100644
--- a/packages/spacecat-shared-data-access/test/audits/index.test.js
+++ b/packages/spacecat-shared-data-access/test/audits/index.test.js
@@ -57,23 +57,98 @@ describe('Audit Index Tests', () => {
it('calls getAuditsForSite and return an array', async () => {
const result = await exportedFunctions.getAuditsForSite('siteId', 'auditType');
expect(result).to.be.an('array');
- // eslint-disable-next-line no-unused-expressions
expect(mockDynamoClient.query.called).to.be.true;
});
it('calls getLatestAudits and return an array', async () => {
const result = await exportedFunctions.getLatestAudits('auditType', true);
expect(result).to.be.an('array');
- // eslint-disable-next-line no-unused-expressions
expect(mockDynamoClient.query.called).to.be.true;
});
it('calls getLatestAuditForSite and return an array', async () => {
const result = await exportedFunctions.getLatestAuditForSite('siteId', 'auditType');
- // eslint-disable-next-line no-unused-expressions
expect(result).to.be.null;
- // eslint-disable-next-line no-unused-expressions
expect(mockDynamoClient.query.called).to.be.true;
});
});
+
+ describe('getAuditForSite Tests', () => {
+ let mockDynamoClient;
+ let mockLog;
+ let exportedFunctions;
+
+ beforeEach(() => {
+ mockDynamoClient = {
+ query: sinon.stub().returns(Promise.resolve([])),
+ };
+ mockLog = { log: sinon.stub() };
+ exportedFunctions = auditFunctions(mockDynamoClient, mockLog);
+ });
+
+ it('successfully retrieves an audit for a site', async () => {
+ const mockAuditData = [{
+ siteId: 'siteId',
+ auditType: 'type1',
+ auditedAt: new Date().toISOString(),
+ auditResult: { score: 1 },
+ fullAuditRef: 'https://someurl.com',
+ }];
+ mockDynamoClient.query.returns(Promise.resolve(mockAuditData));
+
+ const result = await exportedFunctions.getAuditForSite('siteId', 'auditType', 'auditedAt');
+ expect(result).to.not.be.null;
+ expect(result.getScores()).to.be.an('object');
+ expect(mockDynamoClient.query.calledOnce).to.be.true;
+ });
+
+ it('returns null if no audit is found for a site', async () => {
+ mockDynamoClient.query.returns(Promise.resolve([]));
+
+ const result = await exportedFunctions.getAuditForSite('siteId', 'auditType', 'auditedAt');
+ expect(result).to.be.null;
+ expect(mockDynamoClient.query.calledOnce).to.be.true;
+ });
+ });
+
+ describe('addAudit Tests', () => {
+ let mockDynamoClient;
+ let mockLog;
+ let exportedFunctions;
+
+ const auditData = {
+ siteId: 'siteId',
+ auditType: 'lhs',
+ auditedAt: new Date().toISOString(),
+ auditResult: { score: 1 },
+ fullAuditRef: 'https://someurl.com',
+ };
+
+ beforeEach(() => {
+ mockDynamoClient = {
+ query: sinon.stub().returns(Promise.resolve([])),
+ putItem: sinon.stub().returns(Promise.resolve()),
+ };
+ mockLog = { log: sinon.stub() };
+ exportedFunctions = auditFunctions(mockDynamoClient, mockLog);
+ });
+
+ it('successfully adds a new audit', async () => {
+ const result = await exportedFunctions.addAudit(auditData);
+ // Once for 'audits' and once for 'latest_audits'
+ expect(mockDynamoClient.putItem.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');
+ });
+
+ it('throws an error if audit already exists', async () => {
+ mockDynamoClient.query.returns(Promise.resolve([auditData]));
+
+ await expect(exportedFunctions.addAudit(auditData)).to.be.rejectedWith('Audit already exists');
+ });
+ });
});
diff --git a/packages/spacecat-shared-data-access/test/index.test.js b/packages/spacecat-shared-data-access/test/index.test.js
index 656b22525..d6c933f4a 100644
--- a/packages/spacecat-shared-data-access/test/index.test.js
+++ b/packages/spacecat-shared-data-access/test/index.test.js
@@ -17,11 +17,15 @@ import { createDataAccess } from '../src/index.js';
describe('Data Access Object Tests', () => {
const auditFunctions = [
+ 'addAudit',
+ 'getAuditForSite',
'getAuditsForSite',
'getLatestAudits',
'getLatestAuditForSite',
];
const siteFunctions = [
+ 'addSite',
+ 'updateSite',
'getSites',
'getSitesToAudit',
'getSitesWithLatestAudit',
diff --git a/packages/spacecat-shared-data-access/test/models/base.test.js b/packages/spacecat-shared-data-access/test/models/base.test.js
index e9264aad9..0b2759434 100644
--- a/packages/spacecat-shared-data-access/test/models/base.test.js
+++ b/packages/spacecat-shared-data-access/test/models/base.test.js
@@ -38,7 +38,6 @@ describe('Base Entity Tests', () => {
it('should return undefined for createdAt if not provided', () => {
const baseEntity = Base();
- // eslint-disable-next-line no-unused-expressions
expect(baseEntity.getCreatedAt()).to.be.undefined;
});
@@ -50,7 +49,6 @@ describe('Base Entity Tests', () => {
it('should return undefined for updatedAt if not provided', () => {
const baseEntity = Base();
- // eslint-disable-next-line no-unused-expressions
expect(baseEntity.getUpdatedAt()).to.be.undefined;
});
});
diff --git a/packages/spacecat-shared-data-access/test/models/site.test.js b/packages/spacecat-shared-data-access/test/models/site.test.js
index 899c8fadc..a195af4ff 100644
--- a/packages/spacecat-shared-data-access/test/models/site.test.js
+++ b/packages/spacecat-shared-data-access/test/models/site.test.js
@@ -38,7 +38,6 @@ describe('Site Module Tests', () => {
let site;
beforeEach(() => {
- // Reset site object before each test
site = createSite(validData);
});
diff --git a/packages/spacecat-shared-data-access/test/sites/index.test.js b/packages/spacecat-shared-data-access/test/sites/index.test.js
index a3874983b..0774d2516 100644
--- a/packages/spacecat-shared-data-access/test/sites/index.test.js
+++ b/packages/spacecat-shared-data-access/test/sites/index.test.js
@@ -12,10 +12,16 @@
/* eslint-env mocha */
-import { expect } from 'chai';
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
import sinon from 'sinon';
import { siteFunctions } from '../../src/sites/index.js';
+import { createSite } from '../../src/models/site.js';
+
+chai.use(chaiAsPromised);
+
+const { expect } = chai;
describe('Site Index Tests', () => {
describe('Site Functions Export Tests', () => {
@@ -78,48 +84,46 @@ describe('Site Index Tests', () => {
it('calls getSites and returns an array', async () => {
const result = await exportedFunctions.getSites();
expect(result).to.be.an('array');
- // eslint-disable-next-line no-unused-expressions
expect(mockDynamoClient.query.called).to.be.true;
});
it('calls getSitesToAudit and returns an array', async () => {
const result = await exportedFunctions.getSitesToAudit();
expect(result).to.be.an('array');
- // eslint-disable-next-line no-unused-expressions
expect(mockDynamoClient.query.called).to.be.true;
});
it('calls getSitesWithLatestAudit and returns an array', async () => {
const result = await exportedFunctions.getSitesWithLatestAudit();
expect(result).to.be.an('array');
- // eslint-disable-next-line no-unused-expressions
expect(mockDynamoClient.query.called).to.be.true;
});
it('calls getSitesWithLatestAudit and handles latestAudits', async () => {
const mockSiteData = [{
id: 'site1',
- baseUrl: 'https://example.com',
+ baseURL: 'https://example.com',
}];
const mockAuditData = [{
- id: 'audit1',
siteId: 'site1',
auditType: 'type1',
+ auditedAt: new Date().toISOString(),
+ auditResult: {},
+ fullAuditRef: 'https://example.com',
}];
mockDynamoClient.query.onFirstCall().resolves(mockSiteData);
mockDynamoClient.query.onSecondCall().resolves(mockAuditData);
const result = await exportedFunctions.getSitesWithLatestAudit('auditType');
- // eslint-disable-next-line no-unused-expressions
expect(result).to.be.an('array').that.has.lengthOf(1);
});
it('calls getSitesWithLatestAudit and handles empty latestAudits', async () => {
const mockSiteData = [{
id: 'site1',
- baseUrl: 'https://example.com',
+ baseURL: 'https://example.com',
}];
const mockAuditData = [];
@@ -128,23 +132,18 @@ describe('Site Index Tests', () => {
mockDynamoClient.query.onSecondCall().resolves(mockAuditData);
const result = await exportedFunctions.getSitesWithLatestAudit('auditType');
- // eslint-disable-next-line no-unused-expressions
expect(result).to.be.an('array').that.is.empty;
});
it('calls getSiteByBaseURL and returns an array/object', async () => {
const result = await exportedFunctions.getSiteByBaseURL();
- // eslint-disable-next-line no-unused-expressions
expect(result).to.be.null;
- // eslint-disable-next-line no-unused-expressions
expect(mockDynamoClient.getItem.called).to.be.true;
});
it('calls getSiteByBaseURLWithAuditInfo and returns an array/object', async () => {
const result = await exportedFunctions.getSiteByBaseURLWithAuditInfo();
- // eslint-disable-next-line no-unused-expressions
expect(result).to.be.null;
- // eslint-disable-next-line no-unused-expressions
expect(mockDynamoClient.getItem.called).to.be.true;
});
@@ -152,63 +151,151 @@ describe('Site Index Tests', () => {
mockDynamoClient.query.resolves(undefined);
const result = await exportedFunctions.getSiteByBaseURLWithAuditInfo('baseUrl', 'auditType');
- // eslint-disable-next-line no-unused-expressions
expect(result).to.be.null;
});
it('calls getSiteByBaseURLWithAuditInfo and assigns latest audit when latestOnly is true', async () => {
const mockSiteData = {
id: 'site1',
- baseUrl: 'https://example.com',
+ baseURL: 'https://example.com',
};
const mockLatestAuditData = [{
- id: 'audit1',
siteId: 'site1',
auditType: 'type1',
+ auditedAt: new Date().toISOString(),
+ auditResult: {},
+ fullAuditRef: 'https://example.com',
}];
mockDynamoClient.getItem.onFirstCall().resolves(mockSiteData);
mockDynamoClient.query.onFirstCall().resolves(mockLatestAuditData);
const result = await exportedFunctions.getSiteByBaseURLWithAuditInfo('https://example.com', 'type1', true);
- expect(result).to.have.property('audits').that.is.an('array').with.lengthOf(1);
- expect(result.audits[0]).to.deep.equal(mockLatestAuditData[0]);
+ const audits = result.getAudits();
+ expect(audits).to.be.an('array').with.lengthOf(1);
+
+ const audit = audits[0];
+ expect(audit.getId()).to.be.a('string').that.is.not.empty;
+ expect(audit.getSiteId()).to.equal(mockLatestAuditData[0].siteId);
+ expect(audit.getAuditType()).to.equal(mockLatestAuditData[0].auditType);
+ expect(audit.getAuditedAt()).to.equal(mockLatestAuditData[0].auditedAt);
+ expect(audit.getAuditResult()).to.deep.equal(mockLatestAuditData[0].auditResult);
+ expect(audit.getFullAuditRef()).to.equal(mockLatestAuditData[0].fullAuditRef);
});
it('calls getSiteByBaseURLWithAuditInfo and assigns all audits when latestOnly is false', async () => {
const mockSiteData = {
id: 'site1',
- baseUrl: 'https://example.com',
+ baseURL: 'https://example.com',
};
const mockLatestAuditData = [{
- id: 'audit1',
siteId: 'site1',
auditType: 'type1',
+ auditedAt: new Date().toISOString(),
+ auditResult: {},
+ fullAuditRef: 'https://example.com',
+ },
+ {
+ siteId: 'site1',
+ auditType: 'type2',
+ auditedAt: new Date().toISOString(),
+ auditResult: {},
+ fullAuditRef: 'https://example2.com',
}];
mockDynamoClient.getItem.onFirstCall().resolves(mockSiteData);
mockDynamoClient.query.onFirstCall().resolves(mockLatestAuditData);
const result = await exportedFunctions.getSiteByBaseURLWithAuditInfo('baseUrl', 'auditType', false);
- expect(result).to.have.property('audits').that.is.an('array');
+ const audits = result.getAudits();
+ expect(audits).to.be.an('array').with.lengthOf(2);
+
+ for (let i = 0; i < mockLatestAuditData.length; i += 1) {
+ const mockAudit = mockLatestAuditData[i];
+ const audit = audits[i];
+
+ expect(audit.getId()).to.be.a('string').that.is.not.empty;
+ expect(audit.getSiteId()).to.equal(mockAudit.siteId);
+ expect(audit.getAuditType()).to.equal(mockAudit.auditType);
+ expect(audit.getAuditedAt()).to.equal(mockAudit.auditedAt);
+ expect(audit.getAuditResult()).to.deep.equal(mockAudit.auditResult);
+ expect(audit.getFullAuditRef()).to.equal(mockAudit.fullAuditRef);
+ }
});
it('calls getSiteByBaseURLWithAudits and returns an array/object', async () => {
const result = await exportedFunctions.getSiteByBaseURLWithAudits();
- // eslint-disable-next-line no-unused-expressions
expect(result).to.be.null;
- // eslint-disable-next-line no-unused-expressions
expect(mockDynamoClient.getItem.called).to.be.true;
});
it('calls getSiteByBaseURLWithLatestAudit and returns an array/object', async () => {
const result = await exportedFunctions.getSiteByBaseURLWithLatestAudit();
- // eslint-disable-next-line no-unused-expressions
expect(result).to.be.null;
- // eslint-disable-next-line no-unused-expressions
expect(mockDynamoClient.getItem.called).to.be.true;
});
+
+ describe('addSite Tests', () => {
+ beforeEach(() => {
+ mockDynamoClient = {
+ getItem: sinon.stub().returns(Promise.resolve(null)),
+ putItem: sinon.stub().returns(Promise.resolve()),
+ };
+ mockLog = { log: sinon.stub() };
+ exportedFunctions = siteFunctions(mockDynamoClient, mockLog);
+ });
+
+ it('adds a new site successfully', async () => {
+ const siteData = { baseURL: 'https://newsite.com' };
+ const result = await exportedFunctions.addSite(siteData);
+ expect(mockDynamoClient.putItem.calledOnce).to.be.true;
+ expect(result.getBaseURL()).to.equal(siteData.baseURL);
+ expect(result.getId()).to.be.a('string');
+ expect(result.getAudits()).to.be.an('object').that.is.empty;
+ });
+
+ it('throws an error if site already exists', async () => {
+ const siteData = { baseURL: 'https://existingsite.com' };
+ mockDynamoClient.getItem.returns(Promise.resolve(siteData));
+
+ await expect(exportedFunctions.addSite(siteData)).to.be.rejectedWith('Site already exists');
+ });
+ });
+ });
+
+ describe('updateSite Tests', () => {
+ let mockDynamoClient;
+ let mockLog;
+ let exportedFunctions;
+
+ beforeEach(() => {
+ mockDynamoClient = {
+ getItem: sinon.stub().returns(Promise.resolve(null)),
+ putItem: sinon.stub().returns(Promise.resolve()),
+ };
+ mockLog = { log: sinon.stub() };
+ exportedFunctions = siteFunctions(mockDynamoClient, mockLog);
+ });
+
+ it('updates an existing site successfully', async () => {
+ const siteData = { baseURL: 'https://existingsite.com' };
+ mockDynamoClient.getItem.returns(Promise.resolve(siteData));
+
+ const site = await exportedFunctions.getSiteByBaseURL(siteData.baseURL);
+ site.updateBaseURL('https://newsite.com');
+ site.updateImsOrgId('newOrg123');
+
+ const result = await exportedFunctions.updateSite(site);
+ expect(mockDynamoClient.putItem.calledOnce).to.be.true;
+ expect(result.getBaseURL()).to.equal(site.getBaseURL());
+ expect(result.getImsOrgId()).to.equal(site.getImsOrgId());
+ });
+
+ it('throws an error if site does not exist', async () => {
+ const site = createSite({ baseURL: 'https://nonexistingsite.com' });
+ await expect(exportedFunctions.updateSite(site)).to.be.rejectedWith('Site not found');
+ });
});
});
diff --git a/packages/spacecat-shared-dynamo/src/index.d.ts b/packages/spacecat-shared-dynamo/src/index.d.ts
index 8599d9c16..21bb25c46 100644
--- a/packages/spacecat-shared-dynamo/src/index.d.ts
+++ b/packages/spacecat-shared-dynamo/src/index.d.ts
@@ -18,16 +18,11 @@ export declare interface Logger {
info(message: string, ...args: unknown[]): void;
}
-export declare interface DynamoDbKey {
- partitionKey: string;
- sortKey?: string;
-}
-
export declare interface DynamoDbClient {
query(originalParams: QueryCommandInput): Promise;
- getItem(tableName: string, key: DynamoDbKey): Promise;
+ getItem(tableName: string, key: object): Promise;
putItem(tableName: string, item: object): Promise<{ message: string }>;
- removeItem(tableName: string, key: DynamoDbKey): Promise<{ message: string }>;
+ removeItem(tableName: string, key: object): Promise<{ message: string }>;
}
export function createClient(
diff --git a/packages/spacecat-shared-dynamo/src/modules/getItem.js b/packages/spacecat-shared-dynamo/src/modules/getItem.js
index 5a4e24e8a..ba0d32232 100644
--- a/packages/spacecat-shared-dynamo/src/modules/getItem.js
+++ b/packages/spacecat-shared-dynamo/src/modules/getItem.js
@@ -19,7 +19,7 @@ import { guardKey, guardTableName } from '../utils/guards.js';
*
* @param {DynamoDBDocumentClient} docClient - The AWS SDK DynamoDB Document client instance.
* @param {string} tableName - The name of the DynamoDB table.
- * @param {DynamoDbKey} key - The key object containing partitionKey and optionally sortKey.
+ * @param {object} key - The key object containing partitionKey and optionally sortKey.
* @param {Logger} log - The logging object, defaults to console.
* @returns {Promise} A promise that resolves to the retrieved item.
* @throws {Error} Throws an error if the DynamoDB get operation fails or input validation fails.
diff --git a/packages/spacecat-shared-dynamo/src/modules/removeItem.js b/packages/spacecat-shared-dynamo/src/modules/removeItem.js
index 2978278ca..3ab723e9b 100644
--- a/packages/spacecat-shared-dynamo/src/modules/removeItem.js
+++ b/packages/spacecat-shared-dynamo/src/modules/removeItem.js
@@ -19,7 +19,7 @@ import { guardKey, guardTableName } from '../utils/guards.js';
*
* @param {DynamoDBDocumentClient} docClient - The AWS SDK DynamoDB Document client instance.
* @param {string} tableName - The name of the DynamoDB table.
- * @param {DynamoDbKey} key - The key object containing partitionKey and optionally sortKey.
+ * @param {object} key - The key object containing partitionKey and optionally sortKey.
* @param {Logger} log - The logging object, defaults to console.
* @returns {Promise} A promise that resolves to a message indicating successful removal.
* @throws {Error} Throws an error if the DynamoDB delete operation fails or input validation fails.
From 9954c80dea332ca119cea1f78ba38017b48e4d94 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Thu, 30 Nov 2023 11:57:59 +0100
Subject: [PATCH 05/28] fix: use correct doc client
---
packages/spacecat-shared-dynamo/src/index.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/packages/spacecat-shared-dynamo/src/index.js b/packages/spacecat-shared-dynamo/src/index.js
index 6b09ed72e..51301776a 100644
--- a/packages/spacecat-shared-dynamo/src/index.js
+++ b/packages/spacecat-shared-dynamo/src/index.js
@@ -11,7 +11,7 @@
*/
import { DynamoDB } from '@aws-sdk/client-dynamodb';
-import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
+import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb';
import query from './modules/query.js';
import getItem from './modules/getItem.js';
@@ -23,13 +23,13 @@ import removeItem from './modules/removeItem.js';
*
* @param {Object} log - The logging object, defaults to console.
* @param {DynamoDB} dbClient - The AWS SDK DynamoDB client instance.
- * @param {DynamoDBDocumentClient} docClient - The AWS SDK DynamoDB Document client instance.
+ * @param {DynamoDBDocument} docClient - The AWS SDK DynamoDB Document client instance.
* @returns {Object} A client object with methods to interact with DynamoDB.
*/
const createClient = (
log = console,
dbClient = new DynamoDB(),
- docClient = DynamoDBDocumentClient.from(dbClient),
+ docClient = DynamoDBDocument.from(dbClient),
) => ({
query: (params) => query(docClient, params, log),
getItem: (tableName, key) => getItem(docClient, tableName, key, log),
From 41f4782c2f0a6ed6b930a47c451fd004e9d54ea5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Thu, 30 Nov 2023 14:11:54 +0100
Subject: [PATCH 06/28] fix: allow use of index for getItem
---
packages/spacecat-shared-dynamo/src/index.d.ts | 2 +-
packages/spacecat-shared-dynamo/src/index.js | 2 +-
.../src/modules/getItem.js | 17 ++++++++++++++++-
.../test/modules/getItem.test.js | 14 ++++++++++----
4 files changed, 28 insertions(+), 7 deletions(-)
diff --git a/packages/spacecat-shared-dynamo/src/index.d.ts b/packages/spacecat-shared-dynamo/src/index.d.ts
index 21bb25c46..f49671ab6 100644
--- a/packages/spacecat-shared-dynamo/src/index.d.ts
+++ b/packages/spacecat-shared-dynamo/src/index.d.ts
@@ -20,7 +20,7 @@ export declare interface Logger {
export declare interface DynamoDbClient {
query(originalParams: QueryCommandInput): Promise;
- getItem(tableName: string, key: object): Promise;
+ getItem(tableName: string, indexName: string, key: object): Promise;
putItem(tableName: string, item: object): Promise<{ message: string }>;
removeItem(tableName: string, key: object): Promise<{ message: string }>;
}
diff --git a/packages/spacecat-shared-dynamo/src/index.js b/packages/spacecat-shared-dynamo/src/index.js
index 51301776a..e42d60ba8 100644
--- a/packages/spacecat-shared-dynamo/src/index.js
+++ b/packages/spacecat-shared-dynamo/src/index.js
@@ -32,7 +32,7 @@ const createClient = (
docClient = DynamoDBDocument.from(dbClient),
) => ({
query: (params) => query(docClient, params, log),
- getItem: (tableName, key) => getItem(docClient, tableName, key, log),
+ getItem: (tableName, indexName, key) => getItem(docClient, tableName, indexName, key, log),
putItem: (tableName, item) => putItem(docClient, tableName, item, log),
removeItem: (tableName, key) => removeItem(docClient, tableName, key, log),
});
diff --git a/packages/spacecat-shared-dynamo/src/modules/getItem.js b/packages/spacecat-shared-dynamo/src/modules/getItem.js
index ba0d32232..164307616 100644
--- a/packages/spacecat-shared-dynamo/src/modules/getItem.js
+++ b/packages/spacecat-shared-dynamo/src/modules/getItem.js
@@ -12,6 +12,8 @@
import { performance } from 'perf_hooks';
+import { hasText } from '@adobe/spacecat-shared-utils';
+
import { guardKey, guardTableName } from '../utils/guards.js';
/**
@@ -19,18 +21,31 @@ import { guardKey, guardTableName } from '../utils/guards.js';
*
* @param {DynamoDBDocumentClient} docClient - The AWS SDK DynamoDB Document client instance.
* @param {string} tableName - The name of the DynamoDB table.
+ * @param {string} [indexName] - Optional. The name of the DynamoDB index.
* @param {object} key - The key object containing partitionKey and optionally sortKey.
* @param {Logger} log - The logging object, defaults to console.
* @returns {Promise} A promise that resolves to the retrieved item.
* @throws {Error} Throws an error if the DynamoDB get operation fails or input validation fails.
*/
-async function getItem(docClient, tableName, key, log = console) {
+async function getItem(
+ docClient,
+ tableName,
+ indexName,
+ key,
+ log = console,
+) {
guardTableName(tableName);
guardKey(key);
+ const indexProperties = {};
+ if (hasText(indexName)) {
+ indexProperties.IndexName = indexName;
+ }
+
const params = {
TableName: tableName,
Key: key,
+ ...indexProperties,
};
try {
diff --git a/packages/spacecat-shared-dynamo/test/modules/getItem.test.js b/packages/spacecat-shared-dynamo/test/modules/getItem.test.js
index 205c0101b..677b19c0e 100644
--- a/packages/spacecat-shared-dynamo/test/modules/getItem.test.js
+++ b/packages/spacecat-shared-dynamo/test/modules/getItem.test.js
@@ -29,13 +29,19 @@ describe('getItem', () => {
it('gets an item from the database', async () => {
const key = { partitionKey: 'testPartitionKey' };
- const result = await dynamoDbClient.getItem('TestTable', key);
+ const result = await dynamoDbClient.getItem('TestTable', null, key);
expect(result).to.be.an('object');
});
it('gets an item from the database with sort key', async () => {
const key = { partitionKey: 'testPartitionKey', sortKey: 'testSortKey' };
- const result = await dynamoDbClient.getItem('TestTable', key);
+ const result = await dynamoDbClient.getItem('TestTable', null, key);
+ expect(result).to.be.an('object');
+ });
+
+ it('gets an item from the database with index', async () => {
+ const key = { partitionKey: 'testPartitionKey', sortKey: 'testSortKey' };
+ const result = await dynamoDbClient.getItem('TestTable', 'test-index', key);
expect(result).to.be.an('object');
});
@@ -51,7 +57,7 @@ describe('getItem', () => {
it('throws an error for getItem with invalid key', async () => {
try {
- await dynamoDbClient.getItem('TestTable', null);
+ await dynamoDbClient.getItem('TestTable', null, null);
expect.fail('getItem did not throw with invalid key');
} catch (error) {
expect(error.message).to.equal('Key must be a non-empty object.');
@@ -64,7 +70,7 @@ describe('getItem', () => {
};
try {
- await dynamoDbClient.getItem('TestTable', { partitionKey: 'testPartitionKey' });
+ await dynamoDbClient.getItem('TestTable', null, { partitionKey: 'testPartitionKey' });
expect.fail('getItem did not throw as expected');
} catch (error) {
expect(error.message).to.equal('Get failed');
From 6b94f3823d50d3cbc1776dd26decd497af392a28 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Thu, 30 Nov 2023 14:52:56 +0100
Subject: [PATCH 07/28] feat: add data access
---
package-lock.json | 225 +++++++++++++++++-
.../spacecat-shared-data-access/package.json | 8 +-
.../src/models/audit.js | 4 +-
.../src/models/site.js | 2 +-
.../src/sites/accessPatterns.js | 20 +-
.../test-it/auditUtils.js | 58 +++++
.../spacecat-shared-data-access/test-it/db.js | 22 ++
.../test-it/db.test.js | 112 +++++++++
.../test-it/generateSampleData.js | 168 +++++++++++++
.../test-it/tableOperations.js | 161 +++++++++++++
.../test-it/util.js | 22 ++
.../test/models/audit.test.js | 1 -
.../test/sites/index.test.js | 36 +--
13 files changed, 802 insertions(+), 37 deletions(-)
create mode 100644 packages/spacecat-shared-data-access/test-it/auditUtils.js
create mode 100644 packages/spacecat-shared-data-access/test-it/db.js
create mode 100644 packages/spacecat-shared-data-access/test-it/db.test.js
create mode 100644 packages/spacecat-shared-data-access/test-it/generateSampleData.js
create mode 100644 packages/spacecat-shared-data-access/test-it/tableOperations.js
create mode 100644 packages/spacecat-shared-data-access/test-it/util.js
diff --git a/package-lock.json b/package-lock.json
index eb9658331..b5cb53622 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -94,6 +94,10 @@
"aws4": "1.12.0"
}
},
+ "node_modules/@adobe/spacecat-shared-data-access": {
+ "resolved": "packages/spacecat-shared-data-access",
+ "link": true
+ },
"node_modules/@adobe/spacecat-shared-dynamo": {
"resolved": "packages/spacecat-shared-dynamo",
"link": true
@@ -1487,6 +1491,50 @@
"semantic-release": ">=18.0.0-beta.1"
}
},
+ "node_modules/@sinonjs/commons": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
+ "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
+ "dev": true,
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/fake-timers": {
+ "version": "11.2.2",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz",
+ "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.0"
+ }
+ },
+ "node_modules/@sinonjs/samsam": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz",
+ "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^2.0.0",
+ "lodash.get": "^4.4.2",
+ "type-detect": "^4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
+ "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
+ "dev": true,
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/@sinonjs/text-encoding": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz",
+ "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==",
+ "dev": true
+ },
"node_modules/@smithy/abort-controller": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.0.14.tgz",
@@ -2808,6 +2856,18 @@
"node": ">=4"
}
},
+ "node_modules/chai-as-promised": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz",
+ "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==",
+ "dev": true,
+ "dependencies": {
+ "check-error": "^1.0.2"
+ },
+ "peerDependencies": {
+ "chai": ">= 2.1.2 < 5"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -3587,6 +3647,15 @@
"readable-stream": "^2.0.2"
}
},
+ "node_modules/dynamo-db-local": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/dynamo-db-local/-/dynamo-db-local-6.1.0.tgz",
+ "integrity": "sha512-YdF22fGHJzWoHcO9X8soOgCF1Q7AmEf/0fSEovzXCMSOZJ/Mb8nXvyMj4s75aswLJfb8Ujyd64HdFYiofC9Mkg==",
+ "dev": true,
+ "engines": {
+ "node": ">=16.1.0"
+ }
+ },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -5584,6 +5653,12 @@
"node": "*"
}
},
+ "node_modules/just-extend": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz",
+ "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==",
+ "dev": true
+ },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -5905,6 +5980,12 @@
"integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==",
"dev": true
},
+ "node_modules/lodash.get": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz",
+ "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
+ "dev": true
+ },
"node_modules/lodash.ismatch": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz",
@@ -6616,6 +6697,46 @@
"integrity": "sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==",
"dev": true
},
+ "node_modules/nise": {
+ "version": "5.1.5",
+ "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz",
+ "integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^2.0.0",
+ "@sinonjs/fake-timers": "^10.0.2",
+ "@sinonjs/text-encoding": "^0.7.1",
+ "just-extend": "^4.0.2",
+ "path-to-regexp": "^1.7.0"
+ }
+ },
+ "node_modules/nise/node_modules/@sinonjs/commons": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz",
+ "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==",
+ "dev": true,
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
+ "node_modules/nise/node_modules/@sinonjs/fake-timers": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz",
+ "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.0"
+ }
+ },
+ "node_modules/nise/node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz",
+ "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==",
+ "dev": true,
+ "dependencies": {
+ "type-detect": "4.0.8"
+ }
+ },
"node_modules/nock": {
"version": "13.3.8",
"resolved": "https://registry.npmjs.org/nock/-/nock-13.3.8.tgz",
@@ -9598,6 +9719,21 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
+ "node_modules/path-to-regexp": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
+ "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
+ "dev": true,
+ "dependencies": {
+ "isarray": "0.0.1"
+ }
+ },
+ "node_modules/path-to-regexp/node_modules/isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==",
+ "dev": true
+ },
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -10906,6 +11042,33 @@
"node": ">=4"
}
},
+ "node_modules/sinon": {
+ "version": "17.0.1",
+ "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz",
+ "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==",
+ "dev": true,
+ "dependencies": {
+ "@sinonjs/commons": "^3.0.0",
+ "@sinonjs/fake-timers": "^11.2.2",
+ "@sinonjs/samsam": "^8.0.0",
+ "diff": "^5.1.0",
+ "nise": "^5.1.5",
+ "supports-color": "^7.2.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/sinon"
+ }
+ },
+ "node_modules/sinon/node_modules/diff": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz",
+ "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -12096,12 +12259,62 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "packages/spacecat-shared-data-access": {
+ "name": "@adobe/spacecat-shared-data-access",
+ "version": "1.1.0",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@adobe/spacecat-shared-dynamo": "1.1.4",
+ "@adobe/spacecat-shared-utils": "1.1.0",
+ "uuid": "9.0.1"
+ },
+ "devDependencies": {
+ "@aws-sdk/client-dynamodb": "3.454.0",
+ "@aws-sdk/lib-dynamodb": "3.454.0",
+ "chai": "4.3.10",
+ "chai-as-promised": "7.1.1",
+ "dynamo-db-local": "6.1.0",
+ "sinon": "17.0.1"
+ }
+ },
+ "packages/spacecat-shared-data-access/node_modules/@adobe/spacecat-shared-dynamo": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/@adobe/spacecat-shared-dynamo/-/spacecat-shared-dynamo-1.1.4.tgz",
+ "integrity": "sha512-geJZoXHOH3uZ5x3pFn05C1Vu5yrS2vsequYdxRLk03BjyynhLBuW0tT3vvTc9sTfFf1ZuEGhiT9hdWOQvJFh2g==",
+ "dependencies": {
+ "@adobe/spacecat-shared-utils": "1.0.1",
+ "@aws-sdk/client-dynamodb": "3.454.0",
+ "@aws-sdk/lib-dynamodb": "3.454.0"
+ }
+ },
+ "packages/spacecat-shared-data-access/node_modules/@adobe/spacecat-shared-dynamo/node_modules/@adobe/spacecat-shared-utils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@adobe/spacecat-shared-utils/-/spacecat-shared-utils-1.0.1.tgz",
+ "integrity": "sha512-6HiCywan5y9jpbFYkQX87Vg0q5dXoJf2m6MiDvNBEJdREdUxFwE+oYA6Fr86qqd32ECnCeYCFO13HcNRn1RxkQ=="
+ },
+ "packages/spacecat-shared-data-access/node_modules/@adobe/spacecat-shared-utils": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@adobe/spacecat-shared-utils/-/spacecat-shared-utils-1.1.0.tgz",
+ "integrity": "sha512-yq6o47d6IuMApgvlGWdIfUOkqYNwcEm4IzO5WHnrTpWXPK4vvssfAfFtoeki7NQ+A4nFoc7/esqOfX42Q452nA=="
+ },
+ "packages/spacecat-shared-data-access/node_modules/uuid": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
+ "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
+ "funding": [
+ "https://github.com/sponsors/broofa",
+ "https://github.com/sponsors/ctavan"
+ ],
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
"packages/spacecat-shared-dynamo": {
"name": "@adobe/spacecat-shared-dynamo",
- "version": "1.1.2",
+ "version": "1.1.3",
"license": "Apache-2.0",
"dependencies": {
- "@adobe/spacecat-shared-utils": "1.0.1",
+ "@adobe/spacecat-shared-utils": "1.1.0",
"@aws-sdk/client-dynamodb": "3.454.0",
"@aws-sdk/lib-dynamodb": "3.454.0"
},
@@ -12110,9 +12323,9 @@
}
},
"packages/spacecat-shared-dynamo/node_modules/@adobe/spacecat-shared-utils": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@adobe/spacecat-shared-utils/-/spacecat-shared-utils-1.0.1.tgz",
- "integrity": "sha512-6HiCywan5y9jpbFYkQX87Vg0q5dXoJf2m6MiDvNBEJdREdUxFwE+oYA6Fr86qqd32ECnCeYCFO13HcNRn1RxkQ=="
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@adobe/spacecat-shared-utils/-/spacecat-shared-utils-1.1.0.tgz",
+ "integrity": "sha512-yq6o47d6IuMApgvlGWdIfUOkqYNwcEm4IzO5WHnrTpWXPK4vvssfAfFtoeki7NQ+A4nFoc7/esqOfX42Q452nA=="
},
"packages/spacecat-shared-example": {
"name": "@adobe/spacecat-shared-example",
@@ -12128,7 +12341,7 @@
},
"packages/spacecat-shared-utils": {
"name": "@adobe/spacecat-shared-utils",
- "version": "1.1.0",
+ "version": "1.2.0",
"license": "Apache-2.0",
"devDependencies": {
"chai": "4.3.10"
diff --git a/packages/spacecat-shared-data-access/package.json b/packages/spacecat-shared-data-access/package.json
index 87c38fd66..90fea4b06 100644
--- a/packages/spacecat-shared-data-access/package.json
+++ b/packages/spacecat-shared-data-access/package.json
@@ -6,7 +6,8 @@
"main": "src/index.js",
"types": "src/index.d.ts",
"scripts": {
- "test": "c8 mocha",
+ "test:it": "mocha test-it/",
+ "test": "c8 mocha test/",
"lint": "eslint .",
"clean": "rm -rf package-lock.json node_modules"
},
@@ -29,13 +30,16 @@
"access": "public"
},
"dependencies": {
- "@adobe/spacecat-shared-dynamo": "1.1.2",
+ "@adobe/spacecat-shared-dynamo": "1.1.4",
"@adobe/spacecat-shared-utils": "1.1.0",
+ "@aws-sdk/client-dynamodb": "3.454.0",
+ "@aws-sdk/lib-dynamodb": "3.454.0",
"uuid": "9.0.1"
},
"devDependencies": {
"chai": "4.3.10",
"chai-as-promised": "7.1.1",
+ "dynamo-db-local": "6.1.0",
"sinon": "17.0.1"
}
}
diff --git a/packages/spacecat-shared-data-access/src/models/audit.js b/packages/spacecat-shared-data-access/src/models/audit.js
index 2d5668df9..6f603c1b5 100644
--- a/packages/spacecat-shared-data-access/src/models/audit.js
+++ b/packages/spacecat-shared-data-access/src/models/audit.js
@@ -36,13 +36,13 @@ const Audit = (data = {}) => {
if (self.getAuditType() === AUDIT_TYPE_LHS) {
const {
- performance, seo, accessibility, bestPractices,
+ performance, seo, accessibility, 'best-practices': bestPractices,
} = auditResult;
return {
performance,
seo,
accessibility,
- bestPractices,
+ 'best-practices': bestPractices,
};
}
diff --git a/packages/spacecat-shared-data-access/src/models/site.js b/packages/spacecat-shared-data-access/src/models/site.js
index 61d5cb8b0..10eee5201 100644
--- a/packages/spacecat-shared-data-access/src/models/site.js
+++ b/packages/spacecat-shared-data-access/src/models/site.js
@@ -66,7 +66,7 @@ export const createSite = (data) => {
}
if (!isObject(newState.audits)) {
- newState.audits = {};
+ newState.audits = [];
}
return Site(newState);
diff --git a/packages/spacecat-shared-data-access/src/sites/accessPatterns.js b/packages/spacecat-shared-data-access/src/sites/accessPatterns.js
index 7116b9d6e..7881d69de 100644
--- a/packages/spacecat-shared-data-access/src/sites/accessPatterns.js
+++ b/packages/spacecat-shared-data-access/src/sites/accessPatterns.js
@@ -95,25 +95,31 @@ export const getSitesWithLatestAudit = async (
*
* @param {DynamoDbClient} dynamoClient - The DynamoDB client.
* @param {Logger} log - The logger.
- * @param {string} baseUrl - The base URL of the site to retrieve.
+ * @param {string} baseURL - The base URL of the site to retrieve.
* @returns {Promise} A promise that resolves to the site object if found,
* otherwise null.
*/
export const getSiteByBaseURL = async (
dynamoClient,
log,
- baseUrl,
+ baseURL,
) => {
- const dynamoItem = await dynamoClient.getItem(TABLE_NAME_SITES, {
- GSI1PK: PK_ALL_SITES,
- baseUrl,
+ const dynamoItems = await dynamoClient.query({
+ TableName: TABLE_NAME_SITES,
+ IndexName: INDEX_NAME_ALL_SITES,
+ KeyConditionExpression: 'GSI1PK = :gsi1pk AND baseURL = :baseURL',
+ ExpressionAttributeValues: {
+ ':gsi1pk': PK_ALL_SITES,
+ ':baseURL': baseURL,
+ },
+ Limit: 1,
});
- if (dynamoItem === null) {
+ if (dynamoItems.length === 0) {
return null;
}
- return SiteDto.fromDynamoItem(dynamoItem);
+ return SiteDto.fromDynamoItem(dynamoItems[0]);
};
/**
diff --git a/packages/spacecat-shared-data-access/test-it/auditUtils.js b/packages/spacecat-shared-data-access/test-it/auditUtils.js
new file mode 100644
index 000000000..9b0babd82
--- /dev/null
+++ b/packages/spacecat-shared-data-access/test-it/auditUtils.js
@@ -0,0 +1,58 @@
+/*
+ * 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 { v4 as uuidv4 } from 'uuid';
+
+import { randomDate } from './util.js';
+
+function generateRandomAudit(siteId, auditType) {
+ let auditResult = {};
+ const auditedAt = randomDate(new Date(2020, 0, 1), new Date()).toISOString();
+ const expiresAt = new Date(auditedAt);
+ expiresAt.setDate(expiresAt.getDate() + 30);
+ const fullAuditRef = `s3://audit-results/${uuidv4()}.json`;
+
+ function getRandomDecimal(precision) {
+ return parseFloat(Math.random().toFixed(precision));
+ }
+
+ function getRandomInt(max) {
+ return Math.floor(Math.random() * max);
+ }
+
+ if (auditType === 'lhs') {
+ auditResult = {
+ performance: getRandomDecimal(2),
+ accessibility: getRandomDecimal(2),
+ 'best-practices': getRandomDecimal(2),
+ SEO: getRandomDecimal(2),
+ };
+ } else if (auditType === 'cwv') {
+ auditResult = {
+ LCP: getRandomInt(4000), // LCP in milliseconds
+ FID: getRandomInt(100), // FID in milliseconds
+ CLS: getRandomDecimal(2), // CLS score
+ };
+ }
+
+ return {
+ siteId,
+ SK: `${auditType}#${auditedAt}`,
+ auditType,
+ auditedAt,
+ auditResult,
+ expiresAt: Math.floor(expiresAt.getTime() / 1000), // AWS expects unix epoch in seconds
+ fullAuditRef,
+ };
+}
+
+export { generateRandomAudit };
diff --git a/packages/spacecat-shared-data-access/test-it/db.js b/packages/spacecat-shared-data-access/test-it/db.js
new file mode 100644
index 000000000..7bff40e77
--- /dev/null
+++ b/packages/spacecat-shared-data-access/test-it/db.js
@@ -0,0 +1,22 @@
+/*
+ * 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 { DynamoDB } from '@aws-sdk/client-dynamodb';
+import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb';
+
+const dbClient = new DynamoDB({
+ endpoint: 'http://127.0.0.1:8000',
+ region: 'local',
+});
+const docClient = DynamoDBDocument.from(dbClient);
+
+export { dbClient, docClient };
diff --git a/packages/spacecat-shared-data-access/test-it/db.test.js b/packages/spacecat-shared-data-access/test-it/db.test.js
new file mode 100644
index 000000000..bb43b5b52
--- /dev/null
+++ b/packages/spacecat-shared-data-access/test-it/db.test.js
@@ -0,0 +1,112 @@
+/*
+ * 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.
+ */
+
+/* eslint-env mocha */
+
+import { expect } from 'chai';
+
+import dynamoDbLocal from 'dynamo-db-local';
+import { isValidUrl } from '@adobe/spacecat-shared-utils';
+
+import { createDataAccess } from '../src/index.js';
+import { AUDIT_TYPE_LHS } from '../src/models/audit.js';
+
+import generateSampleData from './generateSampleData.js';
+
+async function sleep(ms) {
+ return new Promise((resolve) => {
+ setTimeout(resolve, ms);
+ });
+}
+
+function checkSite(site) {
+ expect(site.getId()).to.be.a('string');
+ expect(site.getBaseURL()).to.be.a('string');
+ expect(site.getImsOrgId()).to.be.a('string');
+ expect(site.getCreatedAt()).to.be.a('string');
+ expect(site.getUpdatedAt()).to.be.a('string');
+ expect(site.getAudits()).to.be.an('array');
+}
+
+describe('DynamoDB Integration Test', async () => {
+ let dynamoDbLocalProcess;
+ let dataAccess;
+
+ const NUMBER_OF_SITES = 10;
+ const NUMBER_OF_AUDITS_PER_SITE = 3;
+
+ before(async function () {
+ this.timeout(3000);
+
+ process.env.AWS_REGION = 'local';
+
+ dynamoDbLocalProcess = dynamoDbLocal.spawn({ port: 8000, sharedDb: true });
+
+ await sleep(500);
+
+ await generateSampleData(NUMBER_OF_SITES, NUMBER_OF_AUDITS_PER_SITE);
+
+ dataAccess = createDataAccess(console);
+ });
+
+ after(() => {
+ dynamoDbLocalProcess.kill();
+ });
+
+ it('gets sites', async () => {
+ const sites = await dataAccess.getSites();
+
+ expect(sites.length).to.equal(NUMBER_OF_SITES);
+
+ sites.forEach((site) => {
+ checkSite(site);
+ expect(site.getAudits()).to.be.an('array').that.has.lengthOf(0);
+ });
+ });
+
+ it('gets sites to audit', async () => {
+ const sites = await dataAccess.getSitesToAudit();
+
+ expect(sites.length).to.equal(NUMBER_OF_SITES);
+
+ sites.forEach((baseURL) => {
+ expect(baseURL).to.be.a('string');
+ expect(isValidUrl(baseURL)).to.equal(true);
+ });
+ });
+
+ it('gets sites with latest audit', async () => {
+ const sites = await dataAccess.getSitesWithLatestAudit(AUDIT_TYPE_LHS);
+
+ // Every tenth site will not have any audits
+ expect(sites.length).to.equal(NUMBER_OF_SITES - 1);
+
+ sites.forEach((site) => {
+ checkSite(site);
+ expect(site.getAudits()).to.be.an('array').that.has.lengthOf(1);
+ site.getAudits().forEach((audit) => {
+ expect(audit.getAuditType()).to.equal(AUDIT_TYPE_LHS);
+ expect(Object.keys(audit.getScores())).to.have.members(
+ ['performance', 'seo', 'accessibility', 'best-practices'],
+ );
+ });
+ });
+ });
+
+ it('gets site by baseURL', async () => {
+ const site = await dataAccess.getSiteByBaseURL('https://example1.com');
+
+ expect(site).to.be.an('object');
+
+ checkSite(site);
+ });
+});
diff --git a/packages/spacecat-shared-data-access/test-it/generateSampleData.js b/packages/spacecat-shared-data-access/test-it/generateSampleData.js
new file mode 100644
index 000000000..7f74c3b4a
--- /dev/null
+++ b/packages/spacecat-shared-data-access/test-it/generateSampleData.js
@@ -0,0 +1,168 @@
+/*
+ * 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 { v4 as uuidv4 } from 'uuid';
+
+import { dbClient, docClient as client } from './db.js';
+import { generateRandomAudit } from './auditUtils.js';
+import { createTable, deleteTable } from './tableOperations.js';
+
+import schema from '../docs/schema.json' assert { type: 'json' };
+
+/**
+ * Creates all tables defined in a schema.
+ *
+ * Iterates over a predefined schema object and creates each table using the createTable function.
+ * The schema object should define all required attributes and configurations for each table.
+ */
+async function createTablesFromSchema() {
+ const creationPromises = schema.DataModel.map(
+ (tableDefinition) => createTable(dbClient, tableDefinition),
+ );
+ await Promise.all(creationPromises);
+}
+
+/**
+ * Deletes a predefined set of tables from the database.
+ *
+ * Iterates over a list of table names and deletes each one using the deleteTable function.
+ * This is typically used to clean up the database before creating new tables or
+ * generating test data.
+ */
+async function deleteExistingTables() {
+ const deletionPromises = ['sites', 'audits', 'latest_audits']
+ .map((tableName) => deleteTable(dbClient, tableName));
+ await Promise.all(deletionPromises);
+}
+
+/**
+ * Performs a batch write operation for a specified table in DynamoDB.
+ *
+ * @param {string} tableName - The name of the table to perform the batch write operation on.
+ * @param {Array} items - An array of items to be written to the table.
+ *
+ * @example
+ * // Example usage
+ * const itemsToWrite = [{ id: '1', data: 'example' }, { id: '2', data: 'sample' }];
+ * batchWrite('myTable', itemsToWrite);
+ */
+async function batchWrite(tableName, items) {
+ const batchWriteRequests = [];
+ while (items.length) {
+ const batch = items.splice(0, 25).map((item) => ({
+ PutRequest: { Item: item },
+ }));
+
+ batchWriteRequests.push(client.batchWrite({
+ RequestItems: { [tableName]: batch },
+ }));
+ }
+
+ await Promise.all(batchWriteRequests);
+}
+
+/**
+ * Generates audit data for a specific site.
+ *
+ * @param {string} siteId - The ID of the site for which to generate audit data.
+ * @param {Array} auditTypes - An array of audit types to generate data for.
+ * @param {number} numberOfAuditsPerType - The number of audits to generate for each type.
+ * @returns {Object} An object containing arrays of audit data and latest audit data for the site.
+ *
+ * @example
+ * // Example usage
+ * const audits = generateAuditData('site123', ['lhs', 'cwv'], 5);
+ */
+function generateAuditData(siteId, auditTypes, numberOfAuditsPerType) {
+ const latestAudits = {};
+ const auditData = [];
+
+ for (const type of auditTypes) {
+ for (let j = 0; j < numberOfAuditsPerType; j += 1) {
+ const audit = generateRandomAudit(siteId, type);
+ auditData.push(audit);
+
+ // Update latest audit for each type
+ if (!latestAudits[type]
+ || new Date(audit.auditedAt) > new Date(latestAudits[type].auditedAt)) {
+ latestAudits[type] = audit;
+ }
+ }
+ }
+
+ const latestAuditData = Object.values(latestAudits).map((audit) => {
+ // Modify the audit data for the latest_audits table
+ let GSI1SK = `${audit.auditType}#`;
+ if (audit.auditType === 'lhs') {
+ GSI1SK += Object.values(audit.auditResult).map((score) => (parseFloat(score) * 100).toFixed(0)).join('#');
+ } else {
+ GSI1SK += Object.values(audit.auditResult).join('#');
+ }
+
+ return {
+ ...audit,
+ GSI1PK: 'ALL_LATEST_AUDITS',
+ GSI1SK,
+ };
+ });
+
+ return { auditData, latestAuditData };
+}
+
+/**
+ * Generates sample data for testing purposes.
+ *
+ * @param {number} [numberOfSites=10] - The number of sites to generate.
+ * @param {number} [numberOfAuditsPerType=5] - The number of audits per type to generate
+ * for each site.
+ *
+ * @example
+ * // Example usage
+ * generateSampleData(20, 10); // Generates 20 sites with 10 audits per type for each site
+ */
+export default async function generateSampleData(numberOfSites = 10, numberOfAuditsPerType = 5) {
+ console.time('Sample data generated in');
+ await deleteExistingTables();
+ await createTablesFromSchema();
+
+ const auditTypes = ['lhs', 'cwv'];
+ const sites = [];
+ const auditItems = [];
+ const latestAuditItems = [];
+ const nowIso = new Date().toISOString();
+
+ // Generate site data
+ for (let i = 0; i < numberOfSites; i += 1) {
+ const siteId = uuidv4();
+ sites.push({
+ id: siteId,
+ baseURL: `https://example${i}.com`,
+ imsOrgId: `${i}-1234@AdobeOrg`,
+ GSI1PK: 'ALL_SITES',
+ createdAt: nowIso,
+ updatedAt: nowIso,
+ });
+
+ if (i % 10 !== 0) { // Every tenth site will not have any audits
+ const latestAudits = generateAuditData(siteId, auditTypes, numberOfAuditsPerType);
+ auditItems.push(...latestAudits.auditData);
+ latestAuditItems.push(...latestAudits.latestAuditData);
+ }
+ }
+
+ await batchWrite('sites', sites);
+ await batchWrite('audits', auditItems);
+ await batchWrite('latest_audits', latestAuditItems);
+
+ console.log(`Generated ${numberOfSites} sites with ${numberOfAuditsPerType} audits per type for each site`);
+ console.timeEnd('Sample data generated in');
+}
diff --git a/packages/spacecat-shared-data-access/test-it/tableOperations.js b/packages/spacecat-shared-data-access/test-it/tableOperations.js
new file mode 100644
index 000000000..b7799143d
--- /dev/null
+++ b/packages/spacecat-shared-data-access/test-it/tableOperations.js
@@ -0,0 +1,161 @@
+/*
+ * 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 { CreateTableCommand, DeleteTableCommand } from '@aws-sdk/client-dynamodb';
+
+/**
+ * Creates a DynamoDB table based on the provided table definition.
+ *
+ * The function defines key schema and attribute definitions for the table,
+ * including the partition key and optional sort key. It also handles the
+ * configuration of global secondary indexes (GSIs) if provided
+ * in the table definition.
+ *
+ * @param {Object} dbClient - The DynamoDB client instance used for sending commands.
+ * @param {Object} tableDefinition - An object describing the table to be created. It should contain
+ * the table name, key attributes, and optionally GSIs.
+ *
+ * @example
+ * // Example of tableDefinition object
+ * {
+ * TableName: 'MyTable',
+ * KeyAttributes: {
+ * PartitionKey: { AttributeName: 'Id', AttributeType: 'S' },
+ * SortKey: { AttributeName: 'SortKey', AttributeType: 'N' }
+ * },
+ * GlobalSecondaryIndexes: [
+ * {
+ * IndexName: 'MyGSI',
+ * KeyAttributes: {
+ * PartitionKey: { AttributeName: 'GSIKey', AttributeType: 'S' },
+ * SortKey: { AttributeName: 'GSISortKey', AttributeType: 'N' }
+ * },
+ * Projection: {
+ * ProjectionType: 'ALL'
+ * }
+ * }
+ * ]
+ * }
+ */
+async function createTable(dbClient, tableDefinition) {
+ const keySchema = [];
+ const attributeDefinitions = [];
+
+ // Define partition key
+ if (tableDefinition.KeyAttributes.PartitionKey) {
+ keySchema.push({ AttributeName: tableDefinition.KeyAttributes.PartitionKey.AttributeName, KeyType: 'HASH' });
+ attributeDefinitions.push({
+ AttributeName: tableDefinition.KeyAttributes.PartitionKey.AttributeName,
+ AttributeType: tableDefinition.KeyAttributes.PartitionKey.AttributeType,
+ });
+ }
+
+ // Define sort key if present
+ if (tableDefinition.KeyAttributes.SortKey) {
+ keySchema.push({ AttributeName: tableDefinition.KeyAttributes.SortKey.AttributeName, KeyType: 'RANGE' });
+ attributeDefinitions.push({
+ AttributeName: tableDefinition.KeyAttributes.SortKey.AttributeName,
+ AttributeType: tableDefinition.KeyAttributes.SortKey.AttributeType,
+ });
+ }
+
+ const params = {
+ TableName: tableDefinition.TableName,
+ KeySchema: keySchema,
+ AttributeDefinitions: attributeDefinitions,
+ BillingMode: 'PAY_PER_REQUEST', // or specify ProvisionedThroughput
+ };
+
+ // Add GSI configuration if present
+ if (tableDefinition.GlobalSecondaryIndexes) {
+ params.GlobalSecondaryIndexes = tableDefinition.GlobalSecondaryIndexes.map((gsi) => {
+ // Add GSI key attributes to AttributeDefinitions
+ if (gsi.KeyAttributes.PartitionKey) {
+ if (!attributeDefinitions.some(
+ (attr) => attr.AttributeName === gsi.KeyAttributes.PartitionKey.AttributeName,
+ )
+ ) {
+ attributeDefinitions.push({
+ AttributeName: gsi.KeyAttributes.PartitionKey.AttributeName,
+ AttributeType: gsi.KeyAttributes.PartitionKey.AttributeType,
+ });
+ }
+ }
+ if (gsi.KeyAttributes.SortKey) {
+ if (!attributeDefinitions.some(
+ (attr) => attr.AttributeName === gsi.KeyAttributes.SortKey.AttributeName,
+ )
+ ) {
+ attributeDefinitions.push({
+ AttributeName: gsi.KeyAttributes.SortKey.AttributeName,
+ AttributeType: gsi.KeyAttributes.SortKey.AttributeType,
+ });
+ }
+ }
+
+ // Define GSI Key Schema
+ const gsiKeySchema = [
+ { AttributeName: gsi.KeyAttributes.PartitionKey.AttributeName, KeyType: 'HASH' },
+ gsi.KeyAttributes.SortKey ? {
+ AttributeName: gsi.KeyAttributes.SortKey.AttributeName,
+ KeyType: 'RANGE',
+ } : null,
+ ].filter(Boolean);
+
+ return {
+ IndexName: gsi.IndexName,
+ KeySchema: gsiKeySchema,
+ Projection: gsi.Projection,
+ };
+ });
+ }
+
+ try {
+ await dbClient.send(new CreateTableCommand(params));
+ console.log(`Table ${tableDefinition.TableName} created successfully.`);
+ } catch (error) {
+ console.error(`Error creating table ${tableDefinition.TableName}:`, error);
+ }
+}
+
+/**
+ * Deletes a specified DynamoDB table.
+ *
+ * The function sends a command to delete the table with the given table name.
+ * It handles the response and logs the result of the operation, including handling the case
+ * where the table does not exist.
+ *
+ * @param {Object} dbClient - The DynamoDB client instance used for sending commands.
+ * @param {string} tableName - The name of the table to be deleted.
+ *
+ * @example
+ * // Example usage
+ * deleteTable(dynamoDBClient, 'MyTable');
+ */
+async function deleteTable(dbClient, tableName) {
+ const deleteParams = {
+ TableName: tableName,
+ };
+
+ try {
+ await dbClient.send(new DeleteTableCommand(deleteParams));
+ console.log(`Table ${tableName} deleted successfully.`);
+ } catch (error) {
+ if (error.name === 'ResourceNotFoundException') {
+ console.log(`Table ${tableName} does not exist.`);
+ } else {
+ console.error(`Error deleting table ${tableName}:`, error);
+ }
+ }
+}
+
+export { createTable, deleteTable };
diff --git a/packages/spacecat-shared-data-access/test-it/util.js b/packages/spacecat-shared-data-access/test-it/util.js
new file mode 100644
index 000000000..2d49f7274
--- /dev/null
+++ b/packages/spacecat-shared-data-access/test-it/util.js
@@ -0,0 +1,22 @@
+/*
+ * 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 randomDate = (start, end) => new Date(
+ start.getTime() + Math.random() * (end.getTime() - start.getTime()),
+);
+
+// Generates a random decimal number with given precision
+const getRandomDecimal = (precision) => parseFloat(Math.random().toFixed(precision));
+
+// Generates a random integer up to a given maximum
+const getRandomInt = (max) => Math.floor(Math.random() * max);
+
+export { randomDate, getRandomDecimal, getRandomInt };
diff --git a/packages/spacecat-shared-data-access/test/models/audit.test.js b/packages/spacecat-shared-data-access/test/models/audit.test.js
index f6d48683e..477df696b 100644
--- a/packages/spacecat-shared-data-access/test/models/audit.test.js
+++ b/packages/spacecat-shared-data-access/test/models/audit.test.js
@@ -15,7 +15,6 @@
import { expect } from 'chai';
import { createAudit } from '../../src/models/audit.js';
-// Constants for testing
const validData = {
siteId: '123',
auditedAt: new Date().toISOString(),
diff --git a/packages/spacecat-shared-data-access/test/sites/index.test.js b/packages/spacecat-shared-data-access/test/sites/index.test.js
index 0774d2516..804d21e8f 100644
--- a/packages/spacecat-shared-data-access/test/sites/index.test.js
+++ b/packages/spacecat-shared-data-access/test/sites/index.test.js
@@ -138,27 +138,27 @@ describe('Site Index Tests', () => {
it('calls getSiteByBaseURL and returns an array/object', async () => {
const result = await exportedFunctions.getSiteByBaseURL();
expect(result).to.be.null;
- expect(mockDynamoClient.getItem.called).to.be.true;
+ expect(mockDynamoClient.query.called).to.be.true;
});
it('calls getSiteByBaseURLWithAuditInfo and returns an array/object', async () => {
const result = await exportedFunctions.getSiteByBaseURLWithAuditInfo();
expect(result).to.be.null;
- expect(mockDynamoClient.getItem.called).to.be.true;
+ expect(mockDynamoClient.query.called).to.be.true;
});
it('calls getSiteByBaseURLWithAuditInfo and returns null when site is undefined', async () => {
- mockDynamoClient.query.resolves(undefined);
+ mockDynamoClient.query.resolves([]);
const result = await exportedFunctions.getSiteByBaseURLWithAuditInfo('baseUrl', 'auditType');
expect(result).to.be.null;
});
it('calls getSiteByBaseURLWithAuditInfo and assigns latest audit when latestOnly is true', async () => {
- const mockSiteData = {
+ const mockSiteData = [{
id: 'site1',
baseURL: 'https://example.com',
- };
+ }];
const mockLatestAuditData = [{
siteId: 'site1',
@@ -168,8 +168,8 @@ describe('Site Index Tests', () => {
fullAuditRef: 'https://example.com',
}];
- mockDynamoClient.getItem.onFirstCall().resolves(mockSiteData);
- mockDynamoClient.query.onFirstCall().resolves(mockLatestAuditData);
+ mockDynamoClient.query.onFirstCall().resolves(mockSiteData);
+ mockDynamoClient.query.onSecondCall().resolves(mockLatestAuditData);
const result = await exportedFunctions.getSiteByBaseURLWithAuditInfo('https://example.com', 'type1', true);
const audits = result.getAudits();
@@ -185,10 +185,10 @@ describe('Site Index Tests', () => {
});
it('calls getSiteByBaseURLWithAuditInfo and assigns all audits when latestOnly is false', async () => {
- const mockSiteData = {
+ const mockSiteData = [{
id: 'site1',
baseURL: 'https://example.com',
- };
+ }];
const mockLatestAuditData = [{
siteId: 'site1',
@@ -205,8 +205,8 @@ describe('Site Index Tests', () => {
fullAuditRef: 'https://example2.com',
}];
- mockDynamoClient.getItem.onFirstCall().resolves(mockSiteData);
- mockDynamoClient.query.onFirstCall().resolves(mockLatestAuditData);
+ mockDynamoClient.query.onFirstCall().resolves(mockSiteData);
+ mockDynamoClient.query.onSecondCall().resolves(mockLatestAuditData);
const result = await exportedFunctions.getSiteByBaseURLWithAuditInfo('baseUrl', 'auditType', false);
const audits = result.getAudits();
@@ -228,19 +228,19 @@ describe('Site Index Tests', () => {
it('calls getSiteByBaseURLWithAudits and returns an array/object', async () => {
const result = await exportedFunctions.getSiteByBaseURLWithAudits();
expect(result).to.be.null;
- expect(mockDynamoClient.getItem.called).to.be.true;
+ expect(mockDynamoClient.query.called).to.be.true;
});
it('calls getSiteByBaseURLWithLatestAudit and returns an array/object', async () => {
const result = await exportedFunctions.getSiteByBaseURLWithLatestAudit();
expect(result).to.be.null;
- expect(mockDynamoClient.getItem.called).to.be.true;
+ expect(mockDynamoClient.query.called).to.be.true;
});
describe('addSite Tests', () => {
beforeEach(() => {
mockDynamoClient = {
- getItem: sinon.stub().returns(Promise.resolve(null)),
+ query: sinon.stub().returns(Promise.resolve([])),
putItem: sinon.stub().returns(Promise.resolve()),
};
mockLog = { log: sinon.stub() };
@@ -253,12 +253,12 @@ describe('Site Index Tests', () => {
expect(mockDynamoClient.putItem.calledOnce).to.be.true;
expect(result.getBaseURL()).to.equal(siteData.baseURL);
expect(result.getId()).to.be.a('string');
- expect(result.getAudits()).to.be.an('object').that.is.empty;
+ expect(result.getAudits()).to.be.an('array').that.is.empty;
});
it('throws an error if site already exists', async () => {
const siteData = { baseURL: 'https://existingsite.com' };
- mockDynamoClient.getItem.returns(Promise.resolve(siteData));
+ mockDynamoClient.query.returns(Promise.resolve([siteData]));
await expect(exportedFunctions.addSite(siteData)).to.be.rejectedWith('Site already exists');
});
@@ -272,7 +272,7 @@ describe('Site Index Tests', () => {
beforeEach(() => {
mockDynamoClient = {
- getItem: sinon.stub().returns(Promise.resolve(null)),
+ query: sinon.stub().returns(Promise.resolve([])),
putItem: sinon.stub().returns(Promise.resolve()),
};
mockLog = { log: sinon.stub() };
@@ -281,7 +281,7 @@ describe('Site Index Tests', () => {
it('updates an existing site successfully', async () => {
const siteData = { baseURL: 'https://existingsite.com' };
- mockDynamoClient.getItem.returns(Promise.resolve(siteData));
+ mockDynamoClient.query.returns(Promise.resolve([siteData]));
const site = await exportedFunctions.getSiteByBaseURL(siteData.baseURL);
site.updateBaseURL('https://newsite.com');
From 0d04ae86ec1bc5b3ab89be411a0228c3a8f6d187 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Thu, 30 Nov 2023 18:03:08 +0100
Subject: [PATCH 08/28] chore: doc
---
docs/API.md | 65 +++++++++++----
.../spacecat-shared-data-access/README.md | 80 ++++++++++++++++++-
2 files changed, 130 insertions(+), 15 deletions(-)
diff --git a/docs/API.md b/docs/API.md
index 6392f8287..32227de18 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -1,11 +1,23 @@
+## Constants
+
+
+createDataAccess ⇒ object
+Creates a data access object.
+
+
+
## Functions
createClient(log, dbClient, docClient) ⇒ Object
Creates a client object for interacting with DynamoDB.
+isArray(value) ⇒ boolean
+Determines if the given parameter is an array.
+
isBoolean(value) ⇒ boolean
-Determines if the given value is a boolean or a string representation of a boolean.
+Determines case-insensitively if the given value is a boolean or a string
+representation of a boolean.
isInteger(value) ⇒ boolean
Checks if the given value is an integer.
@@ -13,16 +25,16 @@
isNumber(value) ⇒ boolean
Determines if the given value is a number.
-isObject(obj) ⇒ boolean
+isObject(value) ⇒ boolean
Checks if the given parameter is an object and not an array or null.
-isString(str) ⇒ boolean
+isString(value) ⇒ boolean
Determines if the given parameter is a string.
hasText(str) ⇒ boolean
Checks if the given string is not empty.
-isValidDate(obj) ⇒ boolean
+isValidDate(value) ⇒ boolean
Checks whether the given object is a valid JavaScript Date.
isIsoDate(str) ⇒ boolean
@@ -41,10 +53,22 @@ following UTC time offsets format.
Converts a given value to a boolean. Throws an error if the value is not a boolean.
arrayEquals(a, b) ⇒ boolean
-Compares two arrays for equality.
+Compares two arrays for equality. Supports primitive array item types only.
+
+
+## createDataAccess ⇒ object
+Creates a data access object.
+
+**Kind**: global constant
+**Returns**: object - data access object
+
+| Param | Type | Description |
+| --- | --- | --- |
+| log | Logger | logger |
+
## createClient(log, dbClient, docClient) ⇒ Object
@@ -57,12 +81,25 @@ Creates a client object for interacting with DynamoDB.
| --- | --- | --- |
| log | Object | The logging object, defaults to console. |
| dbClient | DynamoDB | The AWS SDK DynamoDB client instance. |
-| docClient | DynamoDBDocumentClient | The AWS SDK DynamoDB Document client instance. |
+| docClient | DynamoDBDocument | The AWS SDK DynamoDB Document client instance. |
+
+
+
+## isArray(value) ⇒ boolean
+Determines if the given parameter is an array.
+
+**Kind**: global function
+**Returns**: boolean - True if the parameter is an array, false otherwise.
+
+| Param | Type | Description |
+| --- | --- | --- |
+| value | \* | The value to check. |
## isBoolean(value) ⇒ boolean
-Determines if the given value is a boolean or a string representation of a boolean.
+Determines case-insensitively if the given value is a boolean or a string
+representation of a boolean.
**Kind**: global function
**Returns**: boolean - True if the value is a boolean or a string representation of a boolean.
@@ -97,7 +134,7 @@ Determines if the given value is a number.
-## isObject(obj) ⇒ boolean
+## isObject(value) ⇒ boolean
Checks if the given parameter is an object and not an array or null.
**Kind**: global function
@@ -105,11 +142,11 @@ Checks if the given parameter is an object and not an array or null.
| Param | Type | Description |
| --- | --- | --- |
-| obj | \* | The object to check. |
+| value | \* | The value to check. |
-## isString(str) ⇒ boolean
+## isString(value) ⇒ boolean
Determines if the given parameter is a string.
**Kind**: global function
@@ -117,7 +154,7 @@ Determines if the given parameter is a string.
| Param | Type | Description |
| --- | --- | --- |
-| str | \* | The string to check. |
+| value | \* | The value to check. |
@@ -133,7 +170,7 @@ Checks if the given string is not empty.
-## isValidDate(obj) ⇒ boolean
+## isValidDate(value) ⇒ boolean
Checks whether the given object is a valid JavaScript Date.
**Kind**: global function
@@ -141,7 +178,7 @@ Checks whether the given object is a valid JavaScript Date.
| Param | Type | Description |
| --- | --- | --- |
-| obj | \* | The object to check. |
+| value | \* | The value to check. |
@@ -201,7 +238,7 @@ Converts a given value to a boolean. Throws an error if the value is not a boole
## arrayEquals(a, b) ⇒ boolean
-Compares two arrays for equality.
+Compares two arrays for equality. Supports primitive array item types only.
**Kind**: global function
**Returns**: boolean - True if the arrays are equal, false otherwise.
diff --git a/packages/spacecat-shared-data-access/README.md b/packages/spacecat-shared-data-access/README.md
index e23e5be9e..588bf8b79 100644
--- a/packages/spacecat-shared-data-access/README.md
+++ b/packages/spacecat-shared-data-access/README.md
@@ -1,2 +1,80 @@
-# Data Access Module
+# SpaceCat Shared Data Access
+This Node.js module, `spacecat-shared-data-access`, is a comprehensive data access layer for managing sites and their audits, leveraging Amazon DynamoDB. It's tailored for the `StarCatalogue` model, ensuring efficient querying and robust data manipulation.
+
+## Installation
+
+```bash
+npm install @adobe/spacecat-shared-data-access
+```
+
+## Entities
+
+### Sites
+- **id** (String): Unique identifier for a site.
+- **baseURL** (String): Base URL of the site.
+- **imsOrgId** (String): Organization ID associated with the site.
+- **createdAt** (String): Timestamp of creation.
+- **updatedAt** (String): Timestamp of the last update.
+- **GSI1PK** (String): Partition key for the Global Secondary Index.
+
+### Audits
+- **siteId** (String): Identifier of the site being audited.
+- **SK** (String): Sort key, typically a composite of audit type and timestamp.
+- **auditedAt** (String): Timestamp of the audit.
+- **auditResult** (Map): Results of the audit.
+- **auditType** (String): Type of the audit.
+- **expiresAt** (Number): Expiry timestamp of the audit.
+- **fullAuditRef** (String): Reference to the full audit details.
+
+## DynamoDB Data Model
+
+The module is designed to work with the following DynamoDB tables:
+
+1. **Sites Table**: Manages site records.
+2. **Audits Table**: Stores audit information for each site.
+3. **Latest Audits Table**: Holds only the latest audit for each site for quick access.
+
+Each table is designed with scalability and efficient querying in mind, utilizing both key and non-key attributes effectively.
+
+For a detailed schema, refer to `docs/schema.json`. This schema is importable to Amazon NoSQL Workbench and used by the integration tests.
+
+## Integration Testing
+
+The module includes comprehensive integration tests embedding a local DynamoDB server with in-memory storage for testing:
+
+```bash
+npm run test:it
+```
+
+These tests create the schema, generate sample data, and test the data access patterns against the local DynamoDB instance.
+
+## Data Access API
+
+The module provides two main DAOs:
+
+### Site Functions
+- `getSites`
+- `getSitesToAudit`
+- `getSitesWithLatestAudit`
+- `getSiteByBaseURL`
+- `getSiteByBaseURLWithAuditInfo`
+- `getSiteByBaseURLWithAudits`
+- `getSiteByBaseURLWithLatestAudit`
+- `addSite`
+- `updateSite`
+
+### Audit Functions
+- `getAuditsForSite`
+- `getAuditForSite`
+- `getLatestAudits`
+- `getLatestAuditForSite`
+- `addAudit`
+
+## Contributing
+
+Contributions to `spacecat-shared-data-access` are welcome. Please adhere to the standard Git workflow and submit pull requests for proposed changes.
+
+## License
+
+Licensed under the Apache-2.0 License.
From a27ccb2cfc88b860d36a63d16db992a5b829a928 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Thu, 30 Nov 2023 18:06:41 +0100
Subject: [PATCH 09/28] fix: verify start before end date
---
packages/spacecat-shared-data-access/test-it/util.js | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/packages/spacecat-shared-data-access/test-it/util.js b/packages/spacecat-shared-data-access/test-it/util.js
index 2d49f7274..c2ce6fc12 100644
--- a/packages/spacecat-shared-data-access/test-it/util.js
+++ b/packages/spacecat-shared-data-access/test-it/util.js
@@ -9,9 +9,14 @@
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
-const randomDate = (start, end) => new Date(
- start.getTime() + Math.random() * (end.getTime() - start.getTime()),
-);
+const randomDate = (start, end) => {
+ if (start.getTime() >= end.getTime()) {
+ throw new Error('start must be before end');
+ }
+ return new Date(
+ start.getTime() + Math.random() * (end.getTime() - start.getTime()),
+ );
+};
// Generates a random decimal number with given precision
const getRandomDecimal = (precision) => parseFloat(Math.random().toFixed(precision));
From 2047fbfffb40302222a2e8e3b654cf1e8c0fc5e7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Thu, 30 Nov 2023 18:23:20 +0100
Subject: [PATCH 10/28] fix: add audit result validation, tests
---
.../src/models/audit.js | 45 ++++++++++++-------
.../test/audits/index.test.js | 40 +++++++++++++++--
.../test/models/audit.test.js | 11 +++--
.../test/models/base.test.js | 2 +-
.../test/models/site.test.js | 2 +-
.../test/sites/index.test.js | 44 +++++++++++++-----
6 files changed, 106 insertions(+), 38 deletions(-)
diff --git a/packages/spacecat-shared-data-access/src/models/audit.js b/packages/spacecat-shared-data-access/src/models/audit.js
index 6f603c1b5..82cd68dc3 100644
--- a/packages/spacecat-shared-data-access/src/models/audit.js
+++ b/packages/spacecat-shared-data-access/src/models/audit.js
@@ -17,6 +17,31 @@ export const AUDIT_TYPE_LHS = 'lhs';
const EXPIRES_IN_DAYS = 30;
+const AUDIT_TYPE_PROPERTIES = {
+ [AUDIT_TYPE_LHS]: ['performance', 'seo', 'accessibility', 'best-practices'],
+};
+
+/**
+ * Validates if the auditResult contains the required properties for the given audit type.
+ * @param {object} auditResult - The audit result to validate.
+ * @param {string} auditType - The type of the audit.
+ * @returns {boolean} - True if valid, false otherwise.
+ */
+const validateAuditResult = (auditResult, auditType) => {
+ const expectedProperties = AUDIT_TYPE_PROPERTIES[auditType];
+ if (!expectedProperties) {
+ throw new Error(`Unknown audit type: ${auditType}`);
+ }
+
+ for (const prop of expectedProperties) {
+ if (!(prop in auditResult)) {
+ throw new Error(`Missing expected property '${prop}' for audit type '${auditType}'`);
+ }
+ }
+
+ return true;
+};
+
/**
* Creates a new Audit.
* @param {object } data - audit data
@@ -31,23 +56,7 @@ const Audit = (data = {}) => {
self.getAuditType = () => self.state.auditType.toLowerCase();
self.getExpiresAt = () => self.state.expiresAt;
self.getFullAuditRef = () => self.state.fullAuditRef;
- self.getScores = () => {
- const auditResult = self.getAuditResult();
-
- if (self.getAuditType() === AUDIT_TYPE_LHS) {
- const {
- performance, seo, accessibility, 'best-practices': bestPractices,
- } = auditResult;
- return {
- performance,
- seo,
- accessibility,
- 'best-practices': bestPractices,
- };
- }
-
- return auditResult;
- };
+ self.getScores = () => self.getAuditResult();
return Object.freeze(self);
};
@@ -77,6 +86,8 @@ export const createAudit = (data) => {
throw new Error('Audit result must be an object');
}
+ validateAuditResult(data.auditResult, data.auditType);
+
if (!hasText(newState.fullAuditRef)) {
throw new Error('Full audit ref must be provided');
}
diff --git a/packages/spacecat-shared-data-access/test/audits/index.test.js b/packages/spacecat-shared-data-access/test/audits/index.test.js
index 8c689596e..ce3cf4c3a 100644
--- a/packages/spacecat-shared-data-access/test/audits/index.test.js
+++ b/packages/spacecat-shared-data-access/test/audits/index.test.js
@@ -17,7 +17,7 @@ import sinon from 'sinon';
import { auditFunctions } from '../../src/audits/index.js';
-describe('Audit Index Tests', () => {
+describe('Audit Access Pattern Tests', () => {
describe('Audit Functions Export Tests', () => {
const mockDynamoClient = {};
const mockLog = {};
@@ -89,9 +89,14 @@ describe('Audit Index Tests', () => {
it('successfully retrieves an audit for a site', async () => {
const mockAuditData = [{
siteId: 'siteId',
- auditType: 'type1',
+ auditType: 'lhs',
auditedAt: new Date().toISOString(),
- auditResult: { score: 1 },
+ auditResult: {
+ performance: 0.9,
+ seo: 0.9,
+ accessibility: 0.9,
+ 'best-practices': 0.9,
+ },
fullAuditRef: 'https://someurl.com',
}];
mockDynamoClient.query.returns(Promise.resolve(mockAuditData));
@@ -120,7 +125,12 @@ describe('Audit Index Tests', () => {
siteId: 'siteId',
auditType: 'lhs',
auditedAt: new Date().toISOString(),
- auditResult: { score: 1 },
+ auditResult: {
+ performance: 0.9,
+ seo: 0.9,
+ accessibility: 0.9,
+ 'best-practices': 0.9,
+ },
fullAuditRef: 'https://someurl.com',
};
@@ -150,5 +160,27 @@ describe('Audit Index Tests', () => {
await expect(exportedFunctions.addAudit(auditData)).to.be.rejectedWith('Audit already exists');
});
+
+ it('throws an error for unknown audit type', async () => {
+ const invalidAuditData = {
+ ...auditData,
+ auditType: 'unknownType', // An unknown audit type
+ };
+
+ await expect(exportedFunctions.addAudit(invalidAuditData)).to.be.rejectedWith('Unknown audit type');
+ });
+
+ it('throws an error if an expected property is missing in audit results', async () => {
+ const incompleteAuditData = {
+ ...auditData,
+ auditResult: {
+ performance: 0.9,
+ seo: 0.9,
+ // 'accessibility' and 'best-practices' are missing
+ },
+ };
+
+ await expect(exportedFunctions.addAudit(incompleteAuditData)).to.be.rejectedWith('Missing expected property');
+ });
});
});
diff --git a/packages/spacecat-shared-data-access/test/models/audit.test.js b/packages/spacecat-shared-data-access/test/models/audit.test.js
index 477df696b..a5fa9658d 100644
--- a/packages/spacecat-shared-data-access/test/models/audit.test.js
+++ b/packages/spacecat-shared-data-access/test/models/audit.test.js
@@ -18,12 +18,17 @@ import { createAudit } from '../../src/models/audit.js';
const validData = {
siteId: '123',
auditedAt: new Date().toISOString(),
- auditType: 'Type',
- auditResult: {},
+ auditType: 'lhs',
+ auditResult: {
+ performance: 0.9,
+ seo: 0.9,
+ accessibility: 0.9,
+ 'best-practices': 0.9,
+ },
fullAuditRef: 'ref123',
};
-describe('Audit Module Tests', () => {
+describe('Audit Model Tests', () => {
describe('Validation Tests', () => {
it('throws an error if siteId is not provided', () => {
expect(() => createAudit({ ...validData, siteId: '' })).to.throw('Site ID must be provided');
diff --git a/packages/spacecat-shared-data-access/test/models/base.test.js b/packages/spacecat-shared-data-access/test/models/base.test.js
index 0b2759434..8d320cee3 100644
--- a/packages/spacecat-shared-data-access/test/models/base.test.js
+++ b/packages/spacecat-shared-data-access/test/models/base.test.js
@@ -15,7 +15,7 @@
import { expect } from 'chai';
import { Base } from '../../src/models/base.js';
-describe('Base Entity Tests', () => {
+describe('Base Model Tests', () => {
describe('Initialization Tests', () => {
it('should automatically assign a UUID if no id is provided', () => {
const baseEntity = Base();
diff --git a/packages/spacecat-shared-data-access/test/models/site.test.js b/packages/spacecat-shared-data-access/test/models/site.test.js
index a195af4ff..4d1dd5b63 100644
--- a/packages/spacecat-shared-data-access/test/models/site.test.js
+++ b/packages/spacecat-shared-data-access/test/models/site.test.js
@@ -21,7 +21,7 @@ const validData = {
imsOrgId: 'org123',
};
-describe('Site Module Tests', () => {
+describe('Site Model Tests', () => {
describe('Validation Tests', () => {
it('throws an error if baseURL is not a valid URL', () => {
expect(() => createSite({ ...validData, baseURL: 'invalid-url' })).to.throw('Base URL must be a valid URL');
diff --git a/packages/spacecat-shared-data-access/test/sites/index.test.js b/packages/spacecat-shared-data-access/test/sites/index.test.js
index 804d21e8f..51376bb80 100644
--- a/packages/spacecat-shared-data-access/test/sites/index.test.js
+++ b/packages/spacecat-shared-data-access/test/sites/index.test.js
@@ -23,7 +23,7 @@ chai.use(chaiAsPromised);
const { expect } = chai;
-describe('Site Index Tests', () => {
+describe('Site Access Pattern Tests', () => {
describe('Site Functions Export Tests', () => {
const mockDynamoClient = {};
const mockLog = {};
@@ -107,16 +107,21 @@ describe('Site Index Tests', () => {
const mockAuditData = [{
siteId: 'site1',
- auditType: 'type1',
+ auditType: 'lhs',
auditedAt: new Date().toISOString(),
- auditResult: {},
+ auditResult: {
+ performance: 0.9,
+ seo: 0.9,
+ accessibility: 0.9,
+ 'best-practices': 0.9,
+ },
fullAuditRef: 'https://example.com',
}];
mockDynamoClient.query.onFirstCall().resolves(mockSiteData);
mockDynamoClient.query.onSecondCall().resolves(mockAuditData);
- const result = await exportedFunctions.getSitesWithLatestAudit('auditType');
+ const result = await exportedFunctions.getSitesWithLatestAudit('lhs');
expect(result).to.be.an('array').that.has.lengthOf(1);
});
@@ -162,16 +167,21 @@ describe('Site Index Tests', () => {
const mockLatestAuditData = [{
siteId: 'site1',
- auditType: 'type1',
+ auditType: 'lhs',
auditedAt: new Date().toISOString(),
- auditResult: {},
+ auditResult: {
+ performance: 0.9,
+ seo: 0.9,
+ accessibility: 0.9,
+ 'best-practices': 0.9,
+ },
fullAuditRef: 'https://example.com',
}];
mockDynamoClient.query.onFirstCall().resolves(mockSiteData);
mockDynamoClient.query.onSecondCall().resolves(mockLatestAuditData);
- const result = await exportedFunctions.getSiteByBaseURLWithAuditInfo('https://example.com', 'type1', true);
+ const result = await exportedFunctions.getSiteByBaseURLWithAuditInfo('https://example.com', 'lhs', true);
const audits = result.getAudits();
expect(audits).to.be.an('array').with.lengthOf(1);
@@ -192,23 +202,33 @@ describe('Site Index Tests', () => {
const mockLatestAuditData = [{
siteId: 'site1',
- auditType: 'type1',
+ auditType: 'lhs',
auditedAt: new Date().toISOString(),
- auditResult: {},
+ auditResult: {
+ performance: 0.9,
+ seo: 0.9,
+ accessibility: 0.9,
+ 'best-practices': 0.9,
+ },
fullAuditRef: 'https://example.com',
},
{
siteId: 'site1',
- auditType: 'type2',
+ auditType: 'lhs',
auditedAt: new Date().toISOString(),
- auditResult: {},
+ auditResult: {
+ performance: 0.9,
+ seo: 0.9,
+ accessibility: 0.9,
+ 'best-practices': 0.9,
+ },
fullAuditRef: 'https://example2.com',
}];
mockDynamoClient.query.onFirstCall().resolves(mockSiteData);
mockDynamoClient.query.onSecondCall().resolves(mockLatestAuditData);
- const result = await exportedFunctions.getSiteByBaseURLWithAuditInfo('baseUrl', 'auditType', false);
+ const result = await exportedFunctions.getSiteByBaseURLWithAuditInfo('baseUrl', 'lhs', false);
const audits = result.getAudits();
expect(audits).to.be.an('array').with.lengthOf(2);
From dfbcba18599ed4ace9afc7aceb46cffa8433b945 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Thu, 30 Nov 2023 18:43:08 +0100
Subject: [PATCH 11/28] fix: set updated and created at, add touch
---
.../src/models/base.js | 8 ++++-
.../src/models/site.js | 4 +++
.../test-it/auditUtils.js | 2 +-
.../test-it/db.test.js | 7 +----
.../test/models/base.test.js | 31 +++++++++++++------
.../test/models/site.test.js | 27 ++++++++++++++++
.../spacecat-shared-data-access/test/util.js | 17 ++++++++++
7 files changed, 79 insertions(+), 17 deletions(-)
create mode 100644 packages/spacecat-shared-data-access/test/util.js
diff --git a/packages/spacecat-shared-data-access/src/models/base.js b/packages/spacecat-shared-data-access/src/models/base.js
index e75dd979f..fa4787cd6 100644
--- a/packages/spacecat-shared-data-access/src/models/base.js
+++ b/packages/spacecat-shared-data-access/src/models/base.js
@@ -21,16 +21,22 @@ import { isString } from '@adobe/spacecat-shared-utils';
*/
export const Base = (data = {}) => {
const self = { state: { ...data } };
-
const newRecord = !isString(self.state.id);
+ const nowISO = new Date().toISOString();
if (newRecord) {
self.state.id = uuidv4();
+ self.state.createdAt = nowISO;
+ self.state.updatedAt = nowISO;
}
self.getId = () => self.state.id;
self.getCreatedAt = () => self.state.createdAt;
self.getUpdatedAt = () => self.state.updatedAt;
+ self.touch = () => {
+ self.state.updatedAt = new Date().toISOString();
+ };
+
return self;
};
diff --git a/packages/spacecat-shared-data-access/src/models/site.js b/packages/spacecat-shared-data-access/src/models/site.js
index 10eee5201..14cca4c21 100644
--- a/packages/spacecat-shared-data-access/src/models/site.js
+++ b/packages/spacecat-shared-data-access/src/models/site.js
@@ -32,6 +32,8 @@ const Site = (data = {}) => {
}
self.state.baseURL = baseURL;
+ self.touch();
+
return self;
};
@@ -41,6 +43,8 @@ const Site = (data = {}) => {
}
self.state.imsOrgId = imsOrgId;
+ self.touch();
+
return self;
};
diff --git a/packages/spacecat-shared-data-access/test-it/auditUtils.js b/packages/spacecat-shared-data-access/test-it/auditUtils.js
index 9b0babd82..9d1170036 100644
--- a/packages/spacecat-shared-data-access/test-it/auditUtils.js
+++ b/packages/spacecat-shared-data-access/test-it/auditUtils.js
@@ -32,9 +32,9 @@ function generateRandomAudit(siteId, auditType) {
if (auditType === 'lhs') {
auditResult = {
performance: getRandomDecimal(2),
+ seo: getRandomDecimal(2),
accessibility: getRandomDecimal(2),
'best-practices': getRandomDecimal(2),
- SEO: getRandomDecimal(2),
};
} else if (auditType === 'cwv') {
auditResult = {
diff --git a/packages/spacecat-shared-data-access/test-it/db.test.js b/packages/spacecat-shared-data-access/test-it/db.test.js
index bb43b5b52..30258dff2 100644
--- a/packages/spacecat-shared-data-access/test-it/db.test.js
+++ b/packages/spacecat-shared-data-access/test-it/db.test.js
@@ -17,17 +17,12 @@ import { expect } from 'chai';
import dynamoDbLocal from 'dynamo-db-local';
import { isValidUrl } from '@adobe/spacecat-shared-utils';
+import { sleep } from '../test/util.js';
import { createDataAccess } from '../src/index.js';
import { AUDIT_TYPE_LHS } from '../src/models/audit.js';
import generateSampleData from './generateSampleData.js';
-async function sleep(ms) {
- return new Promise((resolve) => {
- setTimeout(resolve, ms);
- });
-}
-
function checkSite(site) {
expect(site.getId()).to.be.a('string');
expect(site.getBaseURL()).to.be.a('string');
diff --git a/packages/spacecat-shared-data-access/test/models/base.test.js b/packages/spacecat-shared-data-access/test/models/base.test.js
index 8d320cee3..d4feb48d1 100644
--- a/packages/spacecat-shared-data-access/test/models/base.test.js
+++ b/packages/spacecat-shared-data-access/test/models/base.test.js
@@ -12,8 +12,11 @@
/* eslint-env mocha */
+import { isIsoDate } from '@adobe/spacecat-shared-utils';
+
import { expect } from 'chai';
import { Base } from '../../src/models/base.js';
+import { sleep } from '../util.js';
describe('Base Model Tests', () => {
describe('Initialization Tests', () => {
@@ -30,26 +33,36 @@ describe('Base Model Tests', () => {
});
describe('Getter Method Tests', () => {
- it('should correctly return the createdAt date if provided', () => {
+ it('correctly returns the createdAt date if provided', () => {
const createdAt = new Date().toISOString();
const baseEntity = Base({ createdAt });
expect(baseEntity.getCreatedAt()).to.equal(createdAt);
});
- it('should return undefined for createdAt if not provided', () => {
- const baseEntity = Base();
- expect(baseEntity.getCreatedAt()).to.be.undefined;
- });
-
- it('should correctly return the updatedAt date if provided', () => {
+ it('correctly returns the updatedAt date if provided', () => {
const updatedAt = new Date().toISOString();
const baseEntity = Base({ updatedAt });
expect(baseEntity.getUpdatedAt()).to.equal(updatedAt);
});
+ });
- it('should return undefined for updatedAt if not provided', () => {
+ describe('Timestamp Tests', () => {
+ it('should set createdAt and updatedAt for new records', () => {
const baseEntity = Base();
- expect(baseEntity.getUpdatedAt()).to.be.undefined;
+ expect(isIsoDate(baseEntity.getCreatedAt())).to.be.true;
+ expect(isIsoDate(baseEntity.getUpdatedAt())).to.be.true;
+ expect(baseEntity.getCreatedAt()).to.equal(baseEntity.getUpdatedAt());
+ });
+
+ it('should update updatedAt using touch method', async () => {
+ const baseEntity = Base();
+ const initialUpdatedAt = baseEntity.getUpdatedAt();
+
+ await sleep(10);
+
+ baseEntity.touch();
+
+ expect(baseEntity.getUpdatedAt()).to.not.equal(initialUpdatedAt);
});
});
});
diff --git a/packages/spacecat-shared-data-access/test/models/site.test.js b/packages/spacecat-shared-data-access/test/models/site.test.js
index 4d1dd5b63..65afd37ae 100644
--- a/packages/spacecat-shared-data-access/test/models/site.test.js
+++ b/packages/spacecat-shared-data-access/test/models/site.test.js
@@ -14,6 +14,7 @@
import { expect } from 'chai';
import { createSite } from '../../src/models/site.js';
+import { sleep } from '../util.js';
// Constants for testing
const validData = {
@@ -60,5 +61,31 @@ describe('Site Model Tests', () => {
it('throws an error when updating with an empty imsOrgId', () => {
expect(() => site.updateImsOrgId('')).to.throw('IMS Org ID must be provided');
});
+
+ it('sets audits correctly', () => {
+ const audits = [{ id: 'audit1' }, { id: 'audit2' }];
+ site.setAudits(audits);
+ expect(site.getAudits()).to.deep.equal(audits);
+ });
+
+ it('updates updatedAt when base URL is updated', async () => {
+ const initialUpdatedAt = site.getUpdatedAt();
+
+ await sleep(10);
+
+ site.updateBaseURL('https://www.newexample.com');
+
+ expect(site.getUpdatedAt()).to.not.equal(initialUpdatedAt);
+ });
+
+ it('updates updatedAt when imsOrgId is updated', async () => {
+ const initialUpdatedAt = site.getUpdatedAt();
+
+ await sleep(10);
+
+ site.updateImsOrgId('newOrg123');
+
+ expect(site.getUpdatedAt()).to.not.equal(initialUpdatedAt);
+ });
});
});
diff --git a/packages/spacecat-shared-data-access/test/util.js b/packages/spacecat-shared-data-access/test/util.js
new file mode 100644
index 000000000..b416000e1
--- /dev/null
+++ b/packages/spacecat-shared-data-access/test/util.js
@@ -0,0 +1,17 @@
+/*
+ * 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.
+ */
+
+export async function sleep(ms) {
+ return new Promise((resolve) => {
+ setTimeout(resolve, ms);
+ });
+}
From ca51663e472d625112ea46fc2b0a30858bcd925d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Thu, 30 Nov 2023 18:48:35 +0100
Subject: [PATCH 12/28] fix: use array check instead of object
---
package-lock.json | 11 +++--------
packages/spacecat-shared-data-access/package.json | 2 +-
.../spacecat-shared-data-access/src/models/site.js | 4 ++--
3 files changed, 6 insertions(+), 11 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index b5cb53622..7b8081e17 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12265,12 +12265,12 @@
"license": "Apache-2.0",
"dependencies": {
"@adobe/spacecat-shared-dynamo": "1.1.4",
- "@adobe/spacecat-shared-utils": "1.1.0",
+ "@adobe/spacecat-shared-utils": "1.2.0",
+ "@aws-sdk/client-dynamodb": "3.454.0",
+ "@aws-sdk/lib-dynamodb": "3.454.0",
"uuid": "9.0.1"
},
"devDependencies": {
- "@aws-sdk/client-dynamodb": "3.454.0",
- "@aws-sdk/lib-dynamodb": "3.454.0",
"chai": "4.3.10",
"chai-as-promised": "7.1.1",
"dynamo-db-local": "6.1.0",
@@ -12292,11 +12292,6 @@
"resolved": "https://registry.npmjs.org/@adobe/spacecat-shared-utils/-/spacecat-shared-utils-1.0.1.tgz",
"integrity": "sha512-6HiCywan5y9jpbFYkQX87Vg0q5dXoJf2m6MiDvNBEJdREdUxFwE+oYA6Fr86qqd32ECnCeYCFO13HcNRn1RxkQ=="
},
- "packages/spacecat-shared-data-access/node_modules/@adobe/spacecat-shared-utils": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/@adobe/spacecat-shared-utils/-/spacecat-shared-utils-1.1.0.tgz",
- "integrity": "sha512-yq6o47d6IuMApgvlGWdIfUOkqYNwcEm4IzO5WHnrTpWXPK4vvssfAfFtoeki7NQ+A4nFoc7/esqOfX42Q452nA=="
- },
"packages/spacecat-shared-data-access/node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
diff --git a/packages/spacecat-shared-data-access/package.json b/packages/spacecat-shared-data-access/package.json
index 90fea4b06..45e3518c8 100644
--- a/packages/spacecat-shared-data-access/package.json
+++ b/packages/spacecat-shared-data-access/package.json
@@ -31,7 +31,7 @@
},
"dependencies": {
"@adobe/spacecat-shared-dynamo": "1.1.4",
- "@adobe/spacecat-shared-utils": "1.1.0",
+ "@adobe/spacecat-shared-utils": "1.2.0",
"@aws-sdk/client-dynamodb": "3.454.0",
"@aws-sdk/lib-dynamodb": "3.454.0",
"uuid": "9.0.1"
diff --git a/packages/spacecat-shared-data-access/src/models/site.js b/packages/spacecat-shared-data-access/src/models/site.js
index 14cca4c21..9556db417 100644
--- a/packages/spacecat-shared-data-access/src/models/site.js
+++ b/packages/spacecat-shared-data-access/src/models/site.js
@@ -10,7 +10,7 @@
* governing permissions and limitations under the License.
*/
-import { hasText, isObject, isValidUrl } from '@adobe/spacecat-shared-utils';
+import { hasText, isValidUrl } from '@adobe/spacecat-shared-utils';
import { Base } from './base.js';
/**
@@ -69,7 +69,7 @@ export const createSite = (data) => {
throw new Error('Base URL must be a valid URL');
}
- if (!isObject(newState.audits)) {
+ if (!Array.isArray(newState.audits)) {
newState.audits = [];
}
From d07dc7accc73b332d5262a1d5bcfb36f1cbd450e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Thu, 30 Nov 2023 18:49:39 +0100
Subject: [PATCH 13/28] chore: typo
---
packages/spacecat-shared-data-access/src/dto/audit.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/spacecat-shared-data-access/src/dto/audit.js b/packages/spacecat-shared-data-access/src/dto/audit.js
index ba621f439..afcff63d7 100644
--- a/packages/spacecat-shared-data-access/src/dto/audit.js
+++ b/packages/spacecat-shared-data-access/src/dto/audit.js
@@ -17,7 +17,7 @@ import { createAudit } from '../models/audit.js';
*/
export const AuditDto = {
/**
- * Converts a Audit object into a DynamoDB item.
+ * Converts an Audit object into a DynamoDB item.
* @param {Readonly} audit - Audit object.
* @param {boolean} latestAudit - If true, returns the latest audit flavor.
* @returns {{siteId, auditedAt, auditResult, auditType, expiresAt, fullAuditRef, SK: string}}
@@ -41,7 +41,7 @@ export const AuditDto = {
},
/**
- * Converts a DynamoDB item into a Audit object.
+ * Converts a DynamoDB item into an Audit object.
* @param {object } dynamoItem - DynamoDB item.
* @returns {Readonly} Audit object.
*/
From 3be0749483bc4584f7ce5298d5f667ec46d092ed Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Fri, 1 Dec 2023 07:52:04 +0100
Subject: [PATCH 14/28] fix: bugs and more integration tests
---
.../src/audits/accessPatterns.js | 2 +-
.../src/dto/audit.js | 13 +-
.../src/models/audit.js | 2 +
.../src/models/site.js | 30 ++-
.../test-it/db.test.js | 232 +++++++++++++++++-
.../test/models/site.test.js | 10 +-
.../test/sites/index.test.js | 2 +-
7 files changed, 276 insertions(+), 15 deletions(-)
diff --git a/packages/spacecat-shared-data-access/src/audits/accessPatterns.js b/packages/spacecat-shared-data-access/src/audits/accessPatterns.js
index a3b0ac77d..aace72881 100644
--- a/packages/spacecat-shared-data-access/src/audits/accessPatterns.js
+++ b/packages/spacecat-shared-data-access/src/audits/accessPatterns.js
@@ -67,7 +67,7 @@ export const getAuditForSite = async (
KeyConditionExpression: 'siteId = :siteId AND SK = :sk',
ExpressionAttributeValues: {
':siteId': siteId,
- ':sk': `${auditType}#${auditedAt}}`,
+ ':sk': `${auditType}#${auditedAt}`,
},
Limit: 1,
});
diff --git a/packages/spacecat-shared-data-access/src/dto/audit.js b/packages/spacecat-shared-data-access/src/dto/audit.js
index afcff63d7..156fc5645 100644
--- a/packages/spacecat-shared-data-access/src/dto/audit.js
+++ b/packages/spacecat-shared-data-access/src/dto/audit.js
@@ -12,6 +12,15 @@
import { createAudit } from '../models/audit.js';
+function parseEpochToDate(epochInSeconds) {
+ const milliseconds = epochInSeconds * 1000;
+ return new Date(milliseconds);
+}
+
+function convertDateToEpochSeconds(date) {
+ return Math.floor(date.getTime() / 1000);
+}
+
/**
* Data transfer object for Audit.
*/
@@ -33,7 +42,7 @@ export const AuditDto = {
auditedAt: audit.getAuditedAt(),
auditResult: audit.getAuditResult(),
auditType: audit.getAuditType(),
- expiresAt: audit.getExpiresAt(),
+ expiresAt: convertDateToEpochSeconds(audit.getExpiresAt()),
fullAuditRef: audit.getFullAuditRef(),
SK: `${audit.getAuditType()}#${audit.getAuditedAt()}`,
...latestAuditProps,
@@ -51,7 +60,7 @@ export const AuditDto = {
auditedAt: dynamoItem.auditedAt,
auditResult: dynamoItem.auditResult,
auditType: dynamoItem.auditType,
- expiresAt: dynamoItem.expiresAt,
+ expiresAt: parseEpochToDate(dynamoItem.expiresAt),
fullAuditRef: dynamoItem.fullAuditRef,
};
diff --git a/packages/spacecat-shared-data-access/src/models/audit.js b/packages/spacecat-shared-data-access/src/models/audit.js
index 82cd68dc3..1f4b9ef81 100644
--- a/packages/spacecat-shared-data-access/src/models/audit.js
+++ b/packages/spacecat-shared-data-access/src/models/audit.js
@@ -13,11 +13,13 @@
import { hasText, isIsoDate, isObject } from '@adobe/spacecat-shared-utils';
import { Base } from './base.js';
+export const AUDIT_TYPE_CWV = 'cwv';
export const AUDIT_TYPE_LHS = 'lhs';
const EXPIRES_IN_DAYS = 30;
const AUDIT_TYPE_PROPERTIES = {
+ [AUDIT_TYPE_CWV]: [],
[AUDIT_TYPE_LHS]: ['performance', 'seo', 'accessibility', 'best-practices'],
};
diff --git a/packages/spacecat-shared-data-access/src/models/site.js b/packages/spacecat-shared-data-access/src/models/site.js
index 9556db417..94fe8cc35 100644
--- a/packages/spacecat-shared-data-access/src/models/site.js
+++ b/packages/spacecat-shared-data-access/src/models/site.js
@@ -26,7 +26,33 @@ const Site = (data = {}) => {
self.getBaseURL = () => self.state.baseURL;
self.getImsOrgId = () => self.state.imsOrgId;
- self.updateBaseURL = (baseURL) => {
+ // TODO: updating the baseURL is not supported yet, it will require a transact write
+ // on dynamodb (put then delete) since baseURL is part of the primary key, something like:
+ // const updateSiteBaseURL = async (oldBaseURL, updatedSiteData) => {
+ // const params = {
+ // TransactItems: [
+ // {
+ // Put: {
+ // TableName: 'YourSiteTableName',
+ // Item: updatedSiteData,
+ // },
+ // },
+ // {
+ // Delete: {
+ // TableName: 'YourSiteTableName',
+ // Key: {
+ // baseURL: oldBaseURL,
+ // },
+ // },
+ // },
+ // ],
+ // };
+ //
+ // await dynamoDbClient.transactWrite(params).promise();
+ //
+ // return createSite(updatedSiteData);
+ // };
+ /* self.updateBaseURL = (baseURL) => {
if (!isValidUrl(baseURL)) {
throw new Error('Base URL must be a valid URL');
}
@@ -35,7 +61,7 @@ const Site = (data = {}) => {
self.touch();
return self;
- };
+ }; */
self.updateImsOrgId = (imsOrgId) => {
if (!hasText(imsOrgId)) {
diff --git a/packages/spacecat-shared-data-access/test-it/db.test.js b/packages/spacecat-shared-data-access/test-it/db.test.js
index 30258dff2..0718a234b 100644
--- a/packages/spacecat-shared-data-access/test-it/db.test.js
+++ b/packages/spacecat-shared-data-access/test-it/db.test.js
@@ -15,7 +15,7 @@
import { expect } from 'chai';
import dynamoDbLocal from 'dynamo-db-local';
-import { isValidUrl } from '@adobe/spacecat-shared-utils';
+import { isIsoDate, isValidUrl } from '@adobe/spacecat-shared-utils';
import { sleep } from '../test/util.js';
import { createDataAccess } from '../src/index.js';
@@ -24,20 +24,33 @@ import { AUDIT_TYPE_LHS } from '../src/models/audit.js';
import generateSampleData from './generateSampleData.js';
function checkSite(site) {
+ expect(site).to.be.an('object');
expect(site.getId()).to.be.a('string');
expect(site.getBaseURL()).to.be.a('string');
expect(site.getImsOrgId()).to.be.a('string');
- expect(site.getCreatedAt()).to.be.a('string');
- expect(site.getUpdatedAt()).to.be.a('string');
+ expect(isIsoDate(site.getCreatedAt())).to.be.true;
+ expect(isIsoDate(site.getUpdatedAt())).to.be.true;
expect(site.getAudits()).to.be.an('array');
}
+function checkAudit(audit) {
+ expect(audit).to.be.an('object');
+ expect(audit.getId()).to.be.a('string');
+ expect(audit.getSiteId()).to.be.a('string');
+ expect(audit.getAuditType()).to.be.a('string');
+ expect(isIsoDate(audit.getAuditedAt())).to.be.true;
+ expect(audit.getExpiresAt()).to.be.a('date');
+ expect(audit.getAuditResult()).to.be.an('object');
+ expect(audit.getScores()).to.be.an('object');
+ expect(audit.getFullAuditRef()).to.be.a('string');
+}
+
describe('DynamoDB Integration Test', async () => {
let dynamoDbLocalProcess;
let dataAccess;
const NUMBER_OF_SITES = 10;
- const NUMBER_OF_AUDITS_PER_SITE = 3;
+ const NUMBER_OF_AUDITS_PER_TYPE_AND_SITE = 3;
before(async function () {
this.timeout(3000);
@@ -46,9 +59,9 @@ describe('DynamoDB Integration Test', async () => {
dynamoDbLocalProcess = dynamoDbLocal.spawn({ port: 8000, sharedDb: true });
- await sleep(500);
+ await sleep(700); // give db time to start up
- await generateSampleData(NUMBER_OF_SITES, NUMBER_OF_AUDITS_PER_SITE);
+ await generateSampleData(NUMBER_OF_SITES, NUMBER_OF_AUDITS_PER_TYPE_AND_SITE);
dataAccess = createDataAccess(console);
});
@@ -104,4 +117,211 @@ describe('DynamoDB Integration Test', async () => {
checkSite(site);
});
+
+ it('adds a new site', async () => {
+ const newSiteData = {
+ baseURL: 'https://newexample.com',
+ imsOrgId: 'newOrg123',
+ audits: [],
+ };
+
+ const addedSite = await dataAccess.addSite(newSiteData);
+
+ expect(addedSite).to.be.an('object');
+
+ const newSite = await dataAccess.getSiteByBaseURL(newSiteData.baseURL);
+
+ checkSite(newSite);
+
+ expect(newSite.getId()).to.to.be.a('string');
+ expect(newSite.getBaseURL()).to.equal(newSiteData.baseURL);
+ expect(newSite.getImsOrgId()).to.equal(newSiteData.imsOrgId);
+ expect(newSite.getAudits()).to.be.an('array').that.is.empty;
+ });
+
+ it('updates an existing site', async () => {
+ const siteToUpdate = await dataAccess.getSiteByBaseURL('https://example1.com');
+ const originalUpdatedAt = siteToUpdate.getUpdatedAt();
+ const newImsOrgId = 'updatedOrg123';
+
+ await sleep(10); // Make sure updatedAt is different
+
+ siteToUpdate.updateImsOrgId(newImsOrgId);
+
+ const updatedSite = await dataAccess.updateSite(siteToUpdate);
+
+ expect(updatedSite.getImsOrgId()).to.equal(newImsOrgId);
+ expect(updatedSite.getUpdatedAt()).to.not.equal(originalUpdatedAt);
+ });
+
+ it('retrieves all audits for a site', async () => {
+ const site = await dataAccess.getSiteByBaseURL('https://example1.com');
+ const siteId = site.getId();
+ const audits = await dataAccess.getAuditsForSite(siteId);
+
+ expect(audits).to.be.an('array').that.has.lengthOf(NUMBER_OF_AUDITS_PER_TYPE_AND_SITE * 2);
+
+ audits.forEach((audit) => {
+ checkAudit(audit);
+ expect(audit.getSiteId()).to.equal(siteId);
+ });
+ });
+
+ it('retrieves audits of a specific type for a site', async () => {
+ const site = await dataAccess.getSiteByBaseURL('https://example1.com');
+ const siteId = site.getId();
+ const auditType = AUDIT_TYPE_LHS;
+ const audits = await dataAccess.getAuditsForSite(siteId, auditType);
+
+ expect(audits).to.be.an('array').that.has.lengthOf(NUMBER_OF_AUDITS_PER_TYPE_AND_SITE);
+
+ audits.forEach((audit) => {
+ checkAudit(audit);
+ expect(audit.getSiteId()).to.equal(siteId);
+ expect(audit.getAuditType()).to.equal(auditType);
+ });
+ });
+
+ it('retrieves a specific audit for a site', async () => {
+ const site = await dataAccess.getSiteByBaseURL('https://example1.com');
+ const siteId = site.getId();
+ const auditType = AUDIT_TYPE_LHS;
+ const audits = await dataAccess.getAuditsForSite(site.getId(), auditType);
+ const auditedAt = audits[0].getAuditedAt();
+
+ const audit = await dataAccess.getAuditForSite(siteId, auditType, auditedAt);
+
+ checkAudit(audit);
+
+ expect(audit.getSiteId()).to.equal(siteId);
+ expect(audit.getAuditType()).to.equal(auditType);
+ expect(audit.getAuditedAt()).to.equal(auditedAt);
+ });
+
+ it('returns null for non-existing audit', async () => {
+ const site = await dataAccess.getSiteByBaseURL('https://example1.com');
+ const siteId = site.getId();
+ const auditType = 'non-existing-type';
+ const auditedAt = '2023-01-01T00:00:00Z';
+
+ const audit = await dataAccess.getAuditForSite(siteId, auditType, auditedAt);
+
+ expect(audit).to.be.null;
+ });
+
+ it('retrieves the latest audits of a specific type', async () => {
+ const audits = await dataAccess.getLatestAudits(AUDIT_TYPE_LHS, true);
+
+ // Every tenth site will not have any audits
+ expect(audits).to.be.an('array').that.has.lengthOf(NUMBER_OF_SITES - 1);
+
+ audits.forEach((audit) => {
+ checkAudit(audit);
+ expect(audit.getAuditType()).to.equal(AUDIT_TYPE_LHS);
+ });
+
+ // verify the sorting order
+ let lastScoresString = '';
+ audits.forEach((audit) => {
+ const currentScoresString = `${AUDIT_TYPE_LHS}#${Object.keys(audit.getScores()).join('#')}`;
+ expect(currentScoresString.localeCompare(lastScoresString)).to.be.at.least(0);
+ lastScoresString = currentScoresString;
+ });
+ });
+
+ it('retrieves the latest audits in descending order', async () => {
+ const audits = await dataAccess.getLatestAudits(AUDIT_TYPE_LHS, false);
+
+ expect(audits).to.be.an('array').that.has.lengthOf(NUMBER_OF_SITES - 1);
+
+ // verify the sorting order is descending
+ // assuming 'z' will be lexicographically after any realistic score string
+ let lastScoresString = 'z';
+ audits.forEach((audit) => {
+ const currentScoresString = `${AUDIT_TYPE_LHS}#${Object.keys(audit.getScores()).join('#')}`;
+ expect(currentScoresString.localeCompare(lastScoresString)).to.be.at.most(0);
+ lastScoresString = currentScoresString;
+ });
+ });
+
+ it('retrieves the latest audit for a specific site and audit type', async () => {
+ const site = await dataAccess.getSiteByBaseURL('https://example1.com');
+ const siteId = site.getId();
+
+ const latestAudit = await dataAccess.getLatestAuditForSite(siteId, AUDIT_TYPE_LHS);
+
+ checkAudit(latestAudit);
+ expect(latestAudit.getSiteId()).to.equal(siteId);
+ expect(latestAudit.getAuditType()).to.equal(AUDIT_TYPE_LHS);
+
+ const allAudits = await dataAccess.getAuditsForSite(siteId, AUDIT_TYPE_LHS);
+ const mostRecentAudit = allAudits.reduce((latest, current) => (
+ new Date(latest.getAuditedAt()) > new Date(current.getAuditedAt()) ? latest : current
+ ));
+
+ expect(latestAudit.getAuditedAt()).to.equal(mostRecentAudit.getAuditedAt());
+ });
+
+ it('returns null for a site with no audits of the specified type', async () => {
+ const site = await dataAccess.getSiteByBaseURL('https://example1.com');
+ const siteId = site.getId();
+ const auditType = 'non-existing-type';
+
+ const latestAudit = await dataAccess.getLatestAuditForSite(siteId, auditType);
+
+ expect(latestAudit).to.be.null;
+ });
+
+ it('successfully adds a new audit', async () => {
+ const auditData = {
+ siteId: 'https://example1.com',
+ auditType: AUDIT_TYPE_LHS,
+ auditedAt: new Date().toISOString(),
+ fullAuditRef: 's3://ref',
+ auditResult: {
+ performance: 0,
+ seo: 0,
+ accessibility: 0,
+ 'best-practices': 0,
+ },
+ };
+
+ const newAudit = await dataAccess.addAudit(auditData);
+
+ checkAudit(newAudit);
+ expect(newAudit.getSiteId()).to.equal(auditData.siteId);
+ expect(newAudit.getAuditType()).to.equal(auditData.auditType);
+ expect(newAudit.getAuditedAt()).to.equal(auditData.auditedAt);
+
+ // Retrieve the latest audit for the site from the latest_audits table
+ const latestAudit = await dataAccess.getLatestAuditForSite(
+ auditData.siteId,
+ auditData.auditType,
+ );
+
+ checkAudit(latestAudit);
+ expect(latestAudit.getSiteId()).to.equal(auditData.siteId);
+ expect(latestAudit.getAuditType()).to.equal(auditData.auditType);
+ expect(latestAudit.getAuditedAt()).to.equal(auditData.auditedAt);
+ });
+
+ it('throws an error when adding a duplicate audit', async () => {
+ const auditData = {
+ siteId: 'https://example1.com',
+ auditType: AUDIT_TYPE_LHS,
+ auditedAt: new Date().toISOString(),
+ fullAuditRef: 's3://ref',
+ auditResult: {
+ performance: 0,
+ seo: 0,
+ accessibility: 0,
+ 'best-practices': 0,
+ },
+ };
+
+ await dataAccess.addAudit(auditData);
+
+ // Try to add the same audit again
+ await expect(dataAccess.addAudit(auditData)).to.eventually.be.rejectedWith('Audit already exists');
+ });
});
diff --git a/packages/spacecat-shared-data-access/test/models/site.test.js b/packages/spacecat-shared-data-access/test/models/site.test.js
index 65afd37ae..5489f1ee6 100644
--- a/packages/spacecat-shared-data-access/test/models/site.test.js
+++ b/packages/spacecat-shared-data-access/test/models/site.test.js
@@ -42,7 +42,8 @@ describe('Site Model Tests', () => {
site = createSite(validData);
});
- it('updates baseURL correctly', () => {
+ // see TODO in src/models/site.js
+ /* it('updates baseURL correctly', () => {
const newURL = 'https://www.newexample.com';
site.updateBaseURL(newURL);
expect(site.getBaseURL()).to.equal(newURL);
@@ -51,6 +52,7 @@ describe('Site Model Tests', () => {
it('throws an error when updating with an invalid baseURL', () => {
expect(() => site.updateBaseURL('invalid-url')).to.throw('Base URL must be a valid URL');
});
+ */
it('updates imsOrgId correctly', () => {
const newImsOrgId = 'newOrg123';
@@ -68,6 +70,8 @@ describe('Site Model Tests', () => {
expect(site.getAudits()).to.deep.equal(audits);
});
+ // see TODO in src/models/site.js
+ /*
it('updates updatedAt when base URL is updated', async () => {
const initialUpdatedAt = site.getUpdatedAt();
@@ -76,12 +80,12 @@ describe('Site Model Tests', () => {
site.updateBaseURL('https://www.newexample.com');
expect(site.getUpdatedAt()).to.not.equal(initialUpdatedAt);
- });
+ }); */
it('updates updatedAt when imsOrgId is updated', async () => {
const initialUpdatedAt = site.getUpdatedAt();
- await sleep(10);
+ await sleep(20);
site.updateImsOrgId('newOrg123');
diff --git a/packages/spacecat-shared-data-access/test/sites/index.test.js b/packages/spacecat-shared-data-access/test/sites/index.test.js
index 51376bb80..ac71dc507 100644
--- a/packages/spacecat-shared-data-access/test/sites/index.test.js
+++ b/packages/spacecat-shared-data-access/test/sites/index.test.js
@@ -304,7 +304,7 @@ describe('Site Access Pattern Tests', () => {
mockDynamoClient.query.returns(Promise.resolve([siteData]));
const site = await exportedFunctions.getSiteByBaseURL(siteData.baseURL);
- site.updateBaseURL('https://newsite.com');
+ // site.updateBaseURL('https://newsite.com');
site.updateImsOrgId('newOrg123');
const result = await exportedFunctions.updateSite(site);
From 32ec0891ae201bcb348e2019c99477738cd36b5d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Fri, 1 Dec 2023 07:59:56 +0100
Subject: [PATCH 15/28] fix: integration test rejectedWith
---
packages/spacecat-shared-data-access/package.json | 7 +++----
.../spacecat-shared-data-access/test-it/db.test.js | 11 +++++++----
2 files changed, 10 insertions(+), 8 deletions(-)
diff --git a/packages/spacecat-shared-data-access/package.json b/packages/spacecat-shared-data-access/package.json
index 45e3518c8..47f543f9d 100644
--- a/packages/spacecat-shared-data-access/package.json
+++ b/packages/spacecat-shared-data-access/package.json
@@ -6,15 +6,14 @@
"main": "src/index.js",
"types": "src/index.d.ts",
"scripts": {
- "test:it": "mocha test-it/",
- "test": "c8 mocha test/",
+ "test:it": "mocha --spec \"test-it/**/*.test.js\"",
+ "test": "c8 mocha --spec \"test/**/*.test.js\"",
"lint": "eslint .",
"clean": "rm -rf package-lock.json node_modules"
},
"mocha": {
"reporter": "mocha-multi-reporters",
- "reporter-options": "configFile=.mocha-multi.json",
- "spec": "test/**/*.test.js"
+ "reporter-options": "configFile=.mocha-multi.json"
},
"repository": {
"type": "git",
diff --git a/packages/spacecat-shared-data-access/test-it/db.test.js b/packages/spacecat-shared-data-access/test-it/db.test.js
index 0718a234b..be7df18a1 100644
--- a/packages/spacecat-shared-data-access/test-it/db.test.js
+++ b/packages/spacecat-shared-data-access/test-it/db.test.js
@@ -12,17 +12,20 @@
/* eslint-env mocha */
-import { expect } from 'chai';
-
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
import dynamoDbLocal from 'dynamo-db-local';
-import { isIsoDate, isValidUrl } from '@adobe/spacecat-shared-utils';
+import { isIsoDate, isValidUrl } from '@adobe/spacecat-shared-utils';
import { sleep } from '../test/util.js';
import { createDataAccess } from '../src/index.js';
import { AUDIT_TYPE_LHS } from '../src/models/audit.js';
import generateSampleData from './generateSampleData.js';
+const { expect } = chai;
+chai.use(chaiAsPromised);
+
function checkSite(site) {
expect(site).to.be.an('object');
expect(site.getId()).to.be.a('string');
@@ -322,6 +325,6 @@ describe('DynamoDB Integration Test', async () => {
await dataAccess.addAudit(auditData);
// Try to add the same audit again
- await expect(dataAccess.addAudit(auditData)).to.eventually.be.rejectedWith('Audit already exists');
+ await expect(dataAccess.addAudit(auditData)).to.be.rejectedWith('Audit already exists');
});
});
From 3fb1b02662071662e0735ce63463c013cdad179b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Fri, 1 Dec 2023 08:14:17 +0100
Subject: [PATCH 16/28] chore: run integration tests in CI
---
.circleci/config.yml | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 99fb81f52..495f6fe64 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -34,6 +34,9 @@ jobs:
- run:
name: Running tests and getting code coverage
command: cat /tmp/tests-to-run | xargs -I % npm run test -w %
+ - run:
+ name: Running data access integration tests
+ command: npm run test:it -w packages/spacecat-shared-data-access
- codecov/upload
- run:
name: Copy test results
From 38e1748d5bffc45646516cc338bd493ea7a99484 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Fri, 1 Dec 2023 08:17:34 +0100
Subject: [PATCH 17/28] fix: add java to ci image
---
.circleci/config.yml | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 495f6fe64..acf550569 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -1,8 +1,8 @@
version: 2.1
executors:
- node18:
+ node-java:
docker:
- - image: cimg/node:18.18
+ - image: cimg/node:18.18-browsers
orbs:
codecov: codecov/codecov@3.3.0
@@ -20,7 +20,7 @@ commands:
jobs:
build:
- executor: node18
+ executor: node-java
parallelism: 15
steps:
From 9545fc4eb053428758150ba8385fb274d59bc3f6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Fri, 1 Dec 2023 08:19:12 +0100
Subject: [PATCH 18/28] fix: adjust dynamo launch for CI env
---
packages/spacecat-shared-data-access/test-it/db.test.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/spacecat-shared-data-access/test-it/db.test.js b/packages/spacecat-shared-data-access/test-it/db.test.js
index be7df18a1..febd722dd 100644
--- a/packages/spacecat-shared-data-access/test-it/db.test.js
+++ b/packages/spacecat-shared-data-access/test-it/db.test.js
@@ -56,13 +56,13 @@ describe('DynamoDB Integration Test', async () => {
const NUMBER_OF_AUDITS_PER_TYPE_AND_SITE = 3;
before(async function () {
- this.timeout(3000);
+ this.timeout(20000);
process.env.AWS_REGION = 'local';
dynamoDbLocalProcess = dynamoDbLocal.spawn({ port: 8000, sharedDb: true });
- await sleep(700); // give db time to start up
+ await sleep(1000); // give db time to start up
await generateSampleData(NUMBER_OF_SITES, NUMBER_OF_AUDITS_PER_TYPE_AND_SITE);
From a70fd89c668311571714232cdeeee45d8608281b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Fri, 1 Dec 2023 08:23:03 +0100
Subject: [PATCH 19/28] fix: provide dummy creds for it-test
---
packages/spacecat-shared-data-access/test-it/db.js | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/packages/spacecat-shared-data-access/test-it/db.js b/packages/spacecat-shared-data-access/test-it/db.js
index 7bff40e77..391712498 100644
--- a/packages/spacecat-shared-data-access/test-it/db.js
+++ b/packages/spacecat-shared-data-access/test-it/db.js
@@ -16,6 +16,10 @@ import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb';
const dbClient = new DynamoDB({
endpoint: 'http://127.0.0.1:8000',
region: 'local',
+ credentials: {
+ accessKeyId: 'dummy',
+ secretAccessKey: 'dummy',
+ },
});
const docClient = DynamoDBDocument.from(dbClient);
From 4f3aae6af8d10520e00ecf4fcd2745e47275325d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Fri, 1 Dec 2023 08:27:44 +0100
Subject: [PATCH 20/28] fix: set dummy creds in env
---
packages/spacecat-shared-data-access/test-it/db.test.js | 3 +++
1 file changed, 3 insertions(+)
diff --git a/packages/spacecat-shared-data-access/test-it/db.test.js b/packages/spacecat-shared-data-access/test-it/db.test.js
index febd722dd..63de90031 100644
--- a/packages/spacecat-shared-data-access/test-it/db.test.js
+++ b/packages/spacecat-shared-data-access/test-it/db.test.js
@@ -59,6 +59,9 @@ describe('DynamoDB Integration Test', async () => {
this.timeout(20000);
process.env.AWS_REGION = 'local';
+ process.env.AWS_DEFAULT_REGION = 'local';
+ process.env.AWS_ACCESS_KEY_ID = 'dummy';
+ process.env.AWS_SECRET_ACCESS_KEY = 'dummy';
dynamoDbLocalProcess = dynamoDbLocal.spawn({ port: 8000, sharedDb: true });
From 67c2ec2cc6d95604723e76dadf52efc880a9f79c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Fri, 1 Dec 2023 08:32:42 +0100
Subject: [PATCH 21/28] fix: only run IT tests if needed
---
.circleci/config.yml | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/.circleci/config.yml b/.circleci/config.yml
index acf550569..f8a73a67b 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -35,8 +35,14 @@ jobs:
name: Running tests and getting code coverage
command: cat /tmp/tests-to-run | xargs -I % npm run test -w %
- run:
- name: Running data access integration tests
- command: npm run test:it -w packages/spacecat-shared-data-access
+ name: Running integration tests
+ command: |
+ if grep -q "packages/spacecat-shared-data-access" /tmp/tests-to-run; then
+ echo "Running integration tests for spacecat-shared-data-access"
+ npm run test:it -w packages/spacecat-shared-data-access
+ else
+ echo "Skipping integration tests"
+ fi
- codecov/upload
- run:
name: Copy test results
From ab64662baec5876f4e7789150dd8fdfabe23d13c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Fri, 1 Dec 2023 08:41:14 +0100
Subject: [PATCH 22/28] fix: directory structure
---
packages/spacecat-shared-data-access/src/index.js | 4 ++--
.../src/{ => service}/audits/accessPatterns.js | 4 ++--
.../src/{ => service}/audits/index.js | 0
.../src/{ => service}/sites/accessPatterns.js | 4 ++--
.../src/{ => service}/sites/index.js | 0
.../test/{ => service}/audits/index.test.js | 2 +-
.../test/{ => service}/sites/index.test.js | 4 ++--
7 files changed, 9 insertions(+), 9 deletions(-)
rename packages/spacecat-shared-data-access/src/{ => service}/audits/accessPatterns.js (98%)
rename packages/spacecat-shared-data-access/src/{ => service}/audits/index.js (100%)
rename packages/spacecat-shared-data-access/src/{ => service}/sites/accessPatterns.js (98%)
rename packages/spacecat-shared-data-access/src/{ => service}/sites/index.js (100%)
rename packages/spacecat-shared-data-access/test/{ => service}/audits/index.test.js (98%)
rename packages/spacecat-shared-data-access/test/{ => service}/sites/index.test.js (98%)
diff --git a/packages/spacecat-shared-data-access/src/index.js b/packages/spacecat-shared-data-access/src/index.js
index 6f0b9bddf..2b6006e82 100644
--- a/packages/spacecat-shared-data-access/src/index.js
+++ b/packages/spacecat-shared-data-access/src/index.js
@@ -11,8 +11,8 @@
*/
import { createClient } from '@adobe/spacecat-shared-dynamo';
-import { auditFunctions } from './audits/index.js';
-import { siteFunctions } from './sites/index.js';
+import { auditFunctions } from './service/audits/index.js';
+import { siteFunctions } from './service/sites/index.js';
/**
* Creates a data access object.
diff --git a/packages/spacecat-shared-data-access/src/audits/accessPatterns.js b/packages/spacecat-shared-data-access/src/service/audits/accessPatterns.js
similarity index 98%
rename from packages/spacecat-shared-data-access/src/audits/accessPatterns.js
rename to packages/spacecat-shared-data-access/src/service/audits/accessPatterns.js
index aace72881..cbda82f1e 100644
--- a/packages/spacecat-shared-data-access/src/audits/accessPatterns.js
+++ b/packages/spacecat-shared-data-access/src/service/audits/accessPatterns.js
@@ -12,8 +12,8 @@
import { isObject } from '@adobe/spacecat-shared-utils';
-import { AuditDto } from '../dto/audit.js';
-import { createAudit } from '../models/audit.js';
+import { AuditDto } from '../../dto/audit.js';
+import { createAudit } from '../../models/audit.js';
/**
* Retrieves audits for a specified site. If an audit type is provided,
diff --git a/packages/spacecat-shared-data-access/src/audits/index.js b/packages/spacecat-shared-data-access/src/service/audits/index.js
similarity index 100%
rename from packages/spacecat-shared-data-access/src/audits/index.js
rename to packages/spacecat-shared-data-access/src/service/audits/index.js
diff --git a/packages/spacecat-shared-data-access/src/sites/accessPatterns.js b/packages/spacecat-shared-data-access/src/service/sites/accessPatterns.js
similarity index 98%
rename from packages/spacecat-shared-data-access/src/sites/accessPatterns.js
rename to packages/spacecat-shared-data-access/src/service/sites/accessPatterns.js
index 7881d69de..4aab3a33d 100644
--- a/packages/spacecat-shared-data-access/src/sites/accessPatterns.js
+++ b/packages/spacecat-shared-data-access/src/service/sites/accessPatterns.js
@@ -18,8 +18,8 @@ import {
getLatestAudits,
} from '../audits/accessPatterns.js';
-import { createSite } from '../models/site.js';
-import { SiteDto } from '../dto/site.js';
+import { createSite } from '../../models/site.js';
+import { SiteDto } from '../../dto/site.js';
const INDEX_NAME_ALL_SITES = 'all_sites';
const PK_ALL_SITES = 'ALL_SITES';
diff --git a/packages/spacecat-shared-data-access/src/sites/index.js b/packages/spacecat-shared-data-access/src/service/sites/index.js
similarity index 100%
rename from packages/spacecat-shared-data-access/src/sites/index.js
rename to packages/spacecat-shared-data-access/src/service/sites/index.js
diff --git a/packages/spacecat-shared-data-access/test/audits/index.test.js b/packages/spacecat-shared-data-access/test/service/audits/index.test.js
similarity index 98%
rename from packages/spacecat-shared-data-access/test/audits/index.test.js
rename to packages/spacecat-shared-data-access/test/service/audits/index.test.js
index ce3cf4c3a..2d127b046 100644
--- a/packages/spacecat-shared-data-access/test/audits/index.test.js
+++ b/packages/spacecat-shared-data-access/test/service/audits/index.test.js
@@ -15,7 +15,7 @@
import { expect } from 'chai';
import sinon from 'sinon';
-import { auditFunctions } from '../../src/audits/index.js';
+import { auditFunctions } from '../../../src/service/audits/index.js';
describe('Audit Access Pattern Tests', () => {
describe('Audit Functions Export Tests', () => {
diff --git a/packages/spacecat-shared-data-access/test/sites/index.test.js b/packages/spacecat-shared-data-access/test/service/sites/index.test.js
similarity index 98%
rename from packages/spacecat-shared-data-access/test/sites/index.test.js
rename to packages/spacecat-shared-data-access/test/service/sites/index.test.js
index ac71dc507..3dcf23463 100644
--- a/packages/spacecat-shared-data-access/test/sites/index.test.js
+++ b/packages/spacecat-shared-data-access/test/service/sites/index.test.js
@@ -16,8 +16,8 @@ import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import sinon from 'sinon';
-import { siteFunctions } from '../../src/sites/index.js';
-import { createSite } from '../../src/models/site.js';
+import { siteFunctions } from '../../../src/service/sites/index.js';
+import { createSite } from '../../../src/models/site.js';
chai.use(chaiAsPromised);
From 5c2499859b250fce723fc03b5fc81deb5fd3648c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Fri, 1 Dec 2023 09:38:40 +0100
Subject: [PATCH 23/28] feat: add removeSite pattern
---
.../src/index.d.ts | 6 ++
.../src/service/audits/accessPatterns.js | 73 +++++++++++++++++--
.../src/service/audits/index.js | 6 ++
.../src/service/sites/accessPatterns.js | 21 +++++-
.../src/service/sites/index.js | 3 +-
.../test/index.test.js | 2 +
.../test/service/audits/index.test.js | 61 +++++++++++++++-
.../test/service/sites/index.test.js | 32 ++++++++
8 files changed, 192 insertions(+), 12 deletions(-)
diff --git a/packages/spacecat-shared-data-access/src/index.d.ts b/packages/spacecat-shared-data-access/src/index.d.ts
index 3aa03b507..b2aef3069 100644
--- a/packages/spacecat-shared-data-access/src/index.d.ts
+++ b/packages/spacecat-shared-data-access/src/index.d.ts
@@ -45,6 +45,9 @@ export interface DataAccess {
auditType: string,
ascending?: boolean,
) => Promise;
+ getLatestAuditsForSite: (
+ siteId: string,
+ ) => Promise;
getSites: () => Promise;
getSitesToAudit: () => Promise;
getSitesWithLatestAudit: (
@@ -73,6 +76,9 @@ export interface DataAccess {
updateSite: (
site: Site,
) => Promise;
+ removeSite: (
+ siteId: string,
+ ) => Promise;
}
export function createDataAccess(
diff --git a/packages/spacecat-shared-data-access/src/service/audits/accessPatterns.js b/packages/spacecat-shared-data-access/src/service/audits/accessPatterns.js
index cbda82f1e..e4e4439ca 100644
--- a/packages/spacecat-shared-data-access/src/service/audits/accessPatterns.js
+++ b/packages/spacecat-shared-data-access/src/service/audits/accessPatterns.js
@@ -15,6 +15,11 @@ import { isObject } from '@adobe/spacecat-shared-utils';
import { AuditDto } from '../../dto/audit.js';
import { createAudit } from '../../models/audit.js';
+const TABLE_NAME_AUDITS = 'audits';
+const TABLE_NAME_LATEST_AUDITS = 'latest_audits';
+const INDEX_NAME_ALL_LATEST_AUDIT_SCORES = 'all_latest_audit_scores';
+const PK_ALL_LATEST_AUDITS = 'ALL_LATEST_AUDITS';
+
/**
* Retrieves audits for a specified site. If an audit type is provided,
* it returns only audits of that type.
@@ -26,9 +31,8 @@ import { createAudit } from '../../models/audit.js';
* @returns {Promise} A promise that resolves to an array of audits for the specified site.
*/
export const getAuditsForSite = async (dynamoClient, log, siteId, auditType) => {
- // Base query parameters
const queryParams = {
- TableName: 'audits',
+ TableName: TABLE_NAME_AUDITS,
KeyConditionExpression: 'siteId = :siteId',
ExpressionAttributeValues: {
':siteId': siteId,
@@ -63,7 +67,7 @@ export const getAuditForSite = async (
auditedAt,
) => {
const audit = await dynamoClient.query({
- TableName: 'audits',
+ TableName: TABLE_NAME_AUDITS,
KeyConditionExpression: 'siteId = :siteId AND SK = :sk',
ExpressionAttributeValues: {
':siteId': siteId,
@@ -93,11 +97,11 @@ export const getLatestAudits = async (
ascending = true,
) => {
const dynamoItems = await dynamoClient.query({
- TableName: 'latest_audits',
- IndexName: 'all_latest_audit_scores',
+ TableName: TABLE_NAME_LATEST_AUDITS,
+ IndexName: INDEX_NAME_ALL_LATEST_AUDIT_SCORES,
KeyConditionExpression: 'GSI1PK = :gsi1pk AND begins_with(GSI1SK, :auditType)',
ExpressionAttributeValues: {
- ':gsi1pk': 'ALL_LATEST_AUDITS',
+ ':gsi1pk': PK_ALL_LATEST_AUDITS,
':auditType': `${auditType}#`,
},
ScanIndexForward: ascending, // Sorts ascending if true, descending if false
@@ -106,6 +110,27 @@ export const getLatestAudits = async (
return dynamoItems.map((item) => AuditDto.fromDynamoItem(item));
};
+/**
+ * Retrieves latest audits for a specified site.
+ *
+ * @param {DynamoDbClient} dynamoClient - The DynamoDB client.
+ * @param {Logger} log - The logger.
+ * @param {string} siteId - The ID of the site for which audits are being retrieved.
+ * @returns {Promise} A promise that resolves to an array of latest audits
+ * for the specified site.
+ */
+export const getLatestAuditsForSite = async (dynamoClient, log, siteId) => {
+ const queryParams = {
+ TableName: TABLE_NAME_LATEST_AUDITS,
+ KeyConditionExpression: 'siteId = :siteId',
+ ExpressionAttributeValues: { ':siteId': siteId },
+ };
+
+ const dynamoItems = await dynamoClient.query(queryParams);
+
+ return dynamoItems.map((item) => AuditDto.fromDynamoItem(item));
+};
+
/**
* Retrieves the latest audit for a specified site and audit type.
*
@@ -123,7 +148,7 @@ export const getLatestAuditForSite = async (
auditType,
) => {
const latestAudit = await dynamoClient.query({
- TableName: 'latest_audits',
+ TableName: TABLE_NAME_LATEST_AUDITS,
KeyConditionExpression: 'siteId = :siteId AND begins_with(SK, :auditType)',
ExpressionAttributeValues: {
':siteId': siteId,
@@ -163,3 +188,37 @@ export const addAudit = async (dynamoClient, log, auditData) => {
return audit;
};
+
+async function removeAudits(dynamoClient, audits) {
+ // TODO: use batch-remove (needs dynamo client update)
+ const removeAuditPromises = audits.map((audit) => dynamoClient.removeItem({
+ TableName: TABLE_NAME_AUDITS,
+ Key: {
+ siteId: audit.getSiteId(),
+ auditedAt: audit.getAuditedAt(),
+ },
+ }));
+
+ await Promise.all(removeAuditPromises);
+}
+
+/**
+ * Removes all audits for a specified site and the latest audit entry.
+ *
+ * @param {DynamoDbClient} dynamoClient - The DynamoDB client.
+ * @param {Logger} log - The logger.
+ * @param {string} siteId - The ID of the site for which audits are being removed.
+ * @returns {Promise}
+ */
+export const removeAuditsForSite = async (dynamoClient, log, siteId) => {
+ try {
+ const audits = await getAuditsForSite(dynamoClient, log, siteId);
+ const latestAudits = await getLatestAuditsForSite(dynamoClient, log, siteId);
+
+ await removeAudits(dynamoClient, audits);
+ await removeAudits(dynamoClient, latestAudits);
+ } catch (error) {
+ log.error(`Error removing audits for site ${siteId}: ${error.message}`);
+ throw error;
+ }
+};
diff --git a/packages/spacecat-shared-data-access/src/service/audits/index.js b/packages/spacecat-shared-data-access/src/service/audits/index.js
index 87645dde7..f21e86b27 100644
--- a/packages/spacecat-shared-data-access/src/service/audits/index.js
+++ b/packages/spacecat-shared-data-access/src/service/audits/index.js
@@ -15,6 +15,7 @@ import {
getAuditsForSite,
getLatestAuditForSite,
getLatestAudits,
+ removeAuditsForSite,
} from './accessPatterns.js';
export const auditFunctions = (dynamoClient, log) => ({
@@ -48,4 +49,9 @@ export const auditFunctions = (dynamoClient, log) => ({
log,
auditData,
),
+ removeAuditsForSite: (siteId) => removeAuditsForSite(
+ dynamoClient,
+ log,
+ siteId,
+ ),
});
diff --git a/packages/spacecat-shared-data-access/src/service/sites/accessPatterns.js b/packages/spacecat-shared-data-access/src/service/sites/accessPatterns.js
index 4aab3a33d..e7dde5ec6 100644
--- a/packages/spacecat-shared-data-access/src/service/sites/accessPatterns.js
+++ b/packages/spacecat-shared-data-access/src/service/sites/accessPatterns.js
@@ -15,7 +15,7 @@ import { isObject } from '@adobe/spacecat-shared-utils';
import {
getAuditsForSite,
getLatestAuditForSite,
- getLatestAudits,
+ getLatestAudits, removeAuditsForSite,
} from '../audits/accessPatterns.js';
import { createSite } from '../../models/site.js';
@@ -241,3 +241,22 @@ export const updateSite = async (dynamoClient, log, site) => {
return site;
};
+
+/**
+ * Removes a site and its related audits.
+ *
+ * @param {DynamoDbClient} dynamoClient - The DynamoDB client.
+ * @param {Logger} log - The logger.
+ * @param {string} siteId - The ID of the site to remove.
+ * @returns {Promise}
+ */
+export const removeSite = async (dynamoClient, log, siteId) => {
+ try {
+ await removeAuditsForSite(dynamoClient, log, siteId);
+
+ await dynamoClient.removeItem(TABLE_NAME_SITES, { siteId });
+ } catch (error) {
+ log.error(`Error removing site: ${error.message}`);
+ throw error;
+ }
+};
diff --git a/packages/spacecat-shared-data-access/src/service/sites/index.js b/packages/spacecat-shared-data-access/src/service/sites/index.js
index 1009eccbe..f43a2cb5c 100644
--- a/packages/spacecat-shared-data-access/src/service/sites/index.js
+++ b/packages/spacecat-shared-data-access/src/service/sites/index.js
@@ -18,7 +18,7 @@ import {
getSiteByBaseURLWithLatestAudit,
getSites,
getSitesToAudit,
- getSitesWithLatestAudit,
+ getSitesWithLatestAudit, removeSite,
updateSite,
} from './accessPatterns.js';
@@ -61,4 +61,5 @@ export const siteFunctions = (dynamoClient, log) => ({
),
addSite: (siteData) => addSite(dynamoClient, log, siteData),
updateSite: (site) => updateSite(dynamoClient, log, site),
+ removeSite: (siteId) => removeSite(dynamoClient, log, siteId),
});
diff --git a/packages/spacecat-shared-data-access/test/index.test.js b/packages/spacecat-shared-data-access/test/index.test.js
index d6c933f4a..9b9572066 100644
--- a/packages/spacecat-shared-data-access/test/index.test.js
+++ b/packages/spacecat-shared-data-access/test/index.test.js
@@ -22,10 +22,12 @@ describe('Data Access Object Tests', () => {
'getAuditsForSite',
'getLatestAudits',
'getLatestAuditForSite',
+ 'removeAuditsForSite',
];
const siteFunctions = [
'addSite',
'updateSite',
+ 'removeSite',
'getSites',
'getSitesToAudit',
'getSitesWithLatestAudit',
diff --git a/packages/spacecat-shared-data-access/test/service/audits/index.test.js b/packages/spacecat-shared-data-access/test/service/audits/index.test.js
index 2d127b046..3dfbf707c 100644
--- a/packages/spacecat-shared-data-access/test/service/audits/index.test.js
+++ b/packages/spacecat-shared-data-access/test/service/audits/index.test.js
@@ -12,11 +12,16 @@
/* eslint-env mocha */
-import { expect } from 'chai';
+import chai from 'chai';
+import chaiAsPromised from 'chai-as-promised';
import sinon from 'sinon';
import { auditFunctions } from '../../../src/service/audits/index.js';
+chai.use(chaiAsPromised);
+
+const { expect } = chai;
+
describe('Audit Access Pattern Tests', () => {
describe('Audit Functions Export Tests', () => {
const mockDynamoClient = {};
@@ -48,8 +53,12 @@ describe('Audit Access Pattern Tests', () => {
beforeEach(() => {
mockDynamoClient = {
query: sinon.stub().returns(Promise.resolve([])),
+ removeItem: sinon.stub().resolves(),
+ };
+ mockLog = {
+ log: sinon.stub(),
+ error: sinon.stub(),
};
- mockLog = { log: sinon.stub() };
exportedFunctions = auditFunctions(mockDynamoClient, mockLog);
});
@@ -138,8 +147,12 @@ describe('Audit Access Pattern Tests', () => {
mockDynamoClient = {
query: sinon.stub().returns(Promise.resolve([])),
putItem: sinon.stub().returns(Promise.resolve()),
+ removeItem: sinon.stub().returns(Promise.resolve()),
+ };
+ mockLog = {
+ log: sinon.stub(),
+ error: sinon.stub(),
};
- mockLog = { log: sinon.stub() };
exportedFunctions = auditFunctions(mockDynamoClient, mockLog);
});
@@ -182,5 +195,47 @@ describe('Audit Access Pattern Tests', () => {
await expect(exportedFunctions.addAudit(incompleteAuditData)).to.be.rejectedWith('Missing expected property');
});
+
+ it('should remove all audits and latest audits for a site', async () => {
+ const mockAuditData = [{
+ siteId: 'siteId',
+ auditType: 'lhs',
+ auditedAt: new Date().toISOString(),
+ auditResult: {
+ performance: 0.9,
+ seo: 0.9,
+ accessibility: 0.9,
+ 'best-practices': 0.9,
+ },
+ fullAuditRef: 'https://someurl.com',
+ }];
+ mockDynamoClient.query.returns(Promise.resolve(mockAuditData));
+
+ await exportedFunctions.removeAuditsForSite('test-id');
+
+ expect(mockDynamoClient.query.calledTwice).to.be.true;
+ expect(mockDynamoClient.removeItem.calledTwice).to.be.true;
+ });
+
+ it('should log an error if the removal fails', async () => {
+ const mockAuditData = [{
+ siteId: 'siteId',
+ auditType: 'lhs',
+ auditedAt: new Date().toISOString(),
+ auditResult: {
+ performance: 0.9,
+ seo: 0.9,
+ accessibility: 0.9,
+ 'best-practices': 0.9,
+ },
+ fullAuditRef: 'https://someurl.com',
+ }];
+ mockDynamoClient.query.returns(Promise.resolve(mockAuditData));
+
+ const errorMessage = 'Failed to delete item';
+ mockDynamoClient.removeItem.rejects(new Error(errorMessage));
+
+ await expect(exportedFunctions.removeAuditsForSite('some-id')).to.be.rejectedWith(errorMessage);
+ });
});
});
diff --git a/packages/spacecat-shared-data-access/test/service/sites/index.test.js b/packages/spacecat-shared-data-access/test/service/sites/index.test.js
index 3dcf23463..738544540 100644
--- a/packages/spacecat-shared-data-access/test/service/sites/index.test.js
+++ b/packages/spacecat-shared-data-access/test/service/sites/index.test.js
@@ -318,4 +318,36 @@ describe('Site Access Pattern Tests', () => {
await expect(exportedFunctions.updateSite(site)).to.be.rejectedWith('Site not found');
});
});
+
+ describe('removeSite Tests', () => {
+ let mockDynamoClient;
+ let mockLog;
+ let exportedFunctions;
+
+ beforeEach(() => {
+ mockDynamoClient = {
+ query: sinon.stub().returns(Promise.resolve([])),
+ removeItem: sinon.stub().returns(Promise.resolve()),
+ };
+ mockLog = {
+ log: sinon.stub(),
+ error: sinon.stub(),
+ };
+ exportedFunctions = siteFunctions(mockDynamoClient, mockLog);
+ });
+
+ it('removes the site and its related audits', async () => {
+ await exportedFunctions.removeSite('some-id');
+
+ expect(mockDynamoClient.removeItem.calledOnce).to.be.true;
+ });
+
+ it('logs an error and reject if the site removal fails', async () => {
+ const errorMessage = 'Failed to delete site';
+ mockDynamoClient.removeItem.rejects(new Error(errorMessage));
+
+ await expect(exportedFunctions.removeSite('some-id')).to.be.rejectedWith(errorMessage);
+ expect(mockLog.error.calledOnce).to.be.true;
+ });
+ });
});
From 5d0b00f62d11b4435f6354b35cd97c2ab430e6da Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Fri, 1 Dec 2023 12:02:58 +0100
Subject: [PATCH 24/28] fix: removeSite, add integration test
---
.../src/service/audits/accessPatterns.js | 14 ++++++--------
.../src/service/sites/accessPatterns.js | 2 +-
.../test-it/db.test.js | 16 ++++++++++++++++
3 files changed, 23 insertions(+), 9 deletions(-)
diff --git a/packages/spacecat-shared-data-access/src/service/audits/accessPatterns.js b/packages/spacecat-shared-data-access/src/service/audits/accessPatterns.js
index e4e4439ca..a0c381c42 100644
--- a/packages/spacecat-shared-data-access/src/service/audits/accessPatterns.js
+++ b/packages/spacecat-shared-data-access/src/service/audits/accessPatterns.js
@@ -189,14 +189,12 @@ export const addAudit = async (dynamoClient, log, auditData) => {
return audit;
};
-async function removeAudits(dynamoClient, audits) {
+async function removeAudits(dynamoClient, audits, latest = false) {
+ const tableName = latest ? TABLE_NAME_LATEST_AUDITS : TABLE_NAME_AUDITS;
// TODO: use batch-remove (needs dynamo client update)
- const removeAuditPromises = audits.map((audit) => dynamoClient.removeItem({
- TableName: TABLE_NAME_AUDITS,
- Key: {
- siteId: audit.getSiteId(),
- auditedAt: audit.getAuditedAt(),
- },
+ const removeAuditPromises = audits.map((audit) => dynamoClient.removeItem(tableName, {
+ siteId: audit.getSiteId(),
+ SK: `${audit.getAuditType()}#${audit.getAuditedAt()}`,
}));
await Promise.all(removeAuditPromises);
@@ -216,7 +214,7 @@ export const removeAuditsForSite = async (dynamoClient, log, siteId) => {
const latestAudits = await getLatestAuditsForSite(dynamoClient, log, siteId);
await removeAudits(dynamoClient, audits);
- await removeAudits(dynamoClient, latestAudits);
+ await removeAudits(dynamoClient, latestAudits, true);
} catch (error) {
log.error(`Error removing audits for site ${siteId}: ${error.message}`);
throw error;
diff --git a/packages/spacecat-shared-data-access/src/service/sites/accessPatterns.js b/packages/spacecat-shared-data-access/src/service/sites/accessPatterns.js
index e7dde5ec6..910ba0863 100644
--- a/packages/spacecat-shared-data-access/src/service/sites/accessPatterns.js
+++ b/packages/spacecat-shared-data-access/src/service/sites/accessPatterns.js
@@ -254,7 +254,7 @@ export const removeSite = async (dynamoClient, log, siteId) => {
try {
await removeAuditsForSite(dynamoClient, log, siteId);
- await dynamoClient.removeItem(TABLE_NAME_SITES, { siteId });
+ await dynamoClient.removeItem(TABLE_NAME_SITES, { id: siteId });
} catch (error) {
log.error(`Error removing site: ${error.message}`);
throw error;
diff --git a/packages/spacecat-shared-data-access/test-it/db.test.js b/packages/spacecat-shared-data-access/test-it/db.test.js
index 63de90031..f4d63071d 100644
--- a/packages/spacecat-shared-data-access/test-it/db.test.js
+++ b/packages/spacecat-shared-data-access/test-it/db.test.js
@@ -330,4 +330,20 @@ describe('DynamoDB Integration Test', async () => {
// Try to add the same audit again
await expect(dataAccess.addAudit(auditData)).to.be.rejectedWith('Audit already exists');
});
+
+ it('successfully removes a site and its related audits', async () => {
+ const siteToRemove = await dataAccess.getSiteByBaseURL('https://example1.com');
+ const siteId = siteToRemove.getId();
+
+ await expect(dataAccess.removeSite(siteId)).to.eventually.be.fulfilled;
+
+ const siteAfterRemoval = await dataAccess.getSiteByBaseURL('https://example1.com');
+ expect(siteAfterRemoval).to.be.null;
+
+ const auditsAfterRemoval = await dataAccess.getAuditsForSite(siteId);
+ expect(auditsAfterRemoval).to.be.an('array').that.is.empty;
+
+ const latestAuditAfterRemoval = await dataAccess.getLatestAuditForSite(siteId, AUDIT_TYPE_LHS);
+ expect(latestAuditAfterRemoval).to.be.null;
+ });
});
From cfc2448b8a9aa835b11f17cc60a9d369840f7279 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Fri, 1 Dec 2023 12:16:51 +0100
Subject: [PATCH 25/28] chore: move tests to appropriate folders
---
packages/spacecat-shared-data-access/package.json | 4 ++--
.../{test-it => test/it}/auditUtils.js | 0
.../spacecat-shared-data-access/{test-it => test/it}/db.js | 0
.../{test-it => test/it}/db.test.js | 6 +++---
.../{test-it => test/it}/generateSampleData.js | 2 +-
.../{test-it => test/it}/tableOperations.js | 0
.../{test-it => test/it}/util.js | 0
.../test/{ => unit}/index.test.js | 2 +-
.../test/{ => unit}/models/audit.test.js | 2 +-
.../test/{ => unit}/models/base.test.js | 2 +-
.../test/{ => unit}/models/site.test.js | 2 +-
.../test/{ => unit}/service/audits/index.test.js | 2 +-
.../test/{ => unit}/service/sites/index.test.js | 4 ++--
.../spacecat-shared-data-access/test/{ => unit}/util.js | 0
14 files changed, 13 insertions(+), 13 deletions(-)
rename packages/spacecat-shared-data-access/{test-it => test/it}/auditUtils.js (100%)
rename packages/spacecat-shared-data-access/{test-it => test/it}/db.js (100%)
rename packages/spacecat-shared-data-access/{test-it => test/it}/db.test.js (98%)
rename packages/spacecat-shared-data-access/{test-it => test/it}/generateSampleData.js (98%)
rename packages/spacecat-shared-data-access/{test-it => test/it}/tableOperations.js (100%)
rename packages/spacecat-shared-data-access/{test-it => test/it}/util.js (100%)
rename packages/spacecat-shared-data-access/test/{ => unit}/index.test.js (97%)
rename packages/spacecat-shared-data-access/test/{ => unit}/models/audit.test.js (97%)
rename packages/spacecat-shared-data-access/test/{ => unit}/models/base.test.js (97%)
rename packages/spacecat-shared-data-access/test/{ => unit}/models/site.test.js (98%)
rename packages/spacecat-shared-data-access/test/{ => unit}/service/audits/index.test.js (99%)
rename packages/spacecat-shared-data-access/test/{ => unit}/service/sites/index.test.js (98%)
rename packages/spacecat-shared-data-access/test/{ => unit}/util.js (100%)
diff --git a/packages/spacecat-shared-data-access/package.json b/packages/spacecat-shared-data-access/package.json
index 47f543f9d..15303bac3 100644
--- a/packages/spacecat-shared-data-access/package.json
+++ b/packages/spacecat-shared-data-access/package.json
@@ -6,8 +6,8 @@
"main": "src/index.js",
"types": "src/index.d.ts",
"scripts": {
- "test:it": "mocha --spec \"test-it/**/*.test.js\"",
- "test": "c8 mocha --spec \"test/**/*.test.js\"",
+ "test:it": "mocha --spec \"test/it/**/*.test.js\"",
+ "test": "c8 mocha --spec \"test/unit/**/*.test.js\"",
"lint": "eslint .",
"clean": "rm -rf package-lock.json node_modules"
},
diff --git a/packages/spacecat-shared-data-access/test-it/auditUtils.js b/packages/spacecat-shared-data-access/test/it/auditUtils.js
similarity index 100%
rename from packages/spacecat-shared-data-access/test-it/auditUtils.js
rename to packages/spacecat-shared-data-access/test/it/auditUtils.js
diff --git a/packages/spacecat-shared-data-access/test-it/db.js b/packages/spacecat-shared-data-access/test/it/db.js
similarity index 100%
rename from packages/spacecat-shared-data-access/test-it/db.js
rename to packages/spacecat-shared-data-access/test/it/db.js
diff --git a/packages/spacecat-shared-data-access/test-it/db.test.js b/packages/spacecat-shared-data-access/test/it/db.test.js
similarity index 98%
rename from packages/spacecat-shared-data-access/test-it/db.test.js
rename to packages/spacecat-shared-data-access/test/it/db.test.js
index f4d63071d..504f229ba 100644
--- a/packages/spacecat-shared-data-access/test-it/db.test.js
+++ b/packages/spacecat-shared-data-access/test/it/db.test.js
@@ -17,9 +17,9 @@ import chaiAsPromised from 'chai-as-promised';
import dynamoDbLocal from 'dynamo-db-local';
import { isIsoDate, isValidUrl } from '@adobe/spacecat-shared-utils';
-import { sleep } from '../test/util.js';
-import { createDataAccess } from '../src/index.js';
-import { AUDIT_TYPE_LHS } from '../src/models/audit.js';
+import { sleep } from '../unit/util.js';
+import { createDataAccess } from '../../src/index.js';
+import { AUDIT_TYPE_LHS } from '../../src/models/audit.js';
import generateSampleData from './generateSampleData.js';
diff --git a/packages/spacecat-shared-data-access/test-it/generateSampleData.js b/packages/spacecat-shared-data-access/test/it/generateSampleData.js
similarity index 98%
rename from packages/spacecat-shared-data-access/test-it/generateSampleData.js
rename to packages/spacecat-shared-data-access/test/it/generateSampleData.js
index 7f74c3b4a..60719db65 100644
--- a/packages/spacecat-shared-data-access/test-it/generateSampleData.js
+++ b/packages/spacecat-shared-data-access/test/it/generateSampleData.js
@@ -16,7 +16,7 @@ import { dbClient, docClient as client } from './db.js';
import { generateRandomAudit } from './auditUtils.js';
import { createTable, deleteTable } from './tableOperations.js';
-import schema from '../docs/schema.json' assert { type: 'json' };
+import schema from '../../docs/schema.json' assert { type: 'json' };
/**
* Creates all tables defined in a schema.
diff --git a/packages/spacecat-shared-data-access/test-it/tableOperations.js b/packages/spacecat-shared-data-access/test/it/tableOperations.js
similarity index 100%
rename from packages/spacecat-shared-data-access/test-it/tableOperations.js
rename to packages/spacecat-shared-data-access/test/it/tableOperations.js
diff --git a/packages/spacecat-shared-data-access/test-it/util.js b/packages/spacecat-shared-data-access/test/it/util.js
similarity index 100%
rename from packages/spacecat-shared-data-access/test-it/util.js
rename to packages/spacecat-shared-data-access/test/it/util.js
diff --git a/packages/spacecat-shared-data-access/test/index.test.js b/packages/spacecat-shared-data-access/test/unit/index.test.js
similarity index 97%
rename from packages/spacecat-shared-data-access/test/index.test.js
rename to packages/spacecat-shared-data-access/test/unit/index.test.js
index 9b9572066..0a992aa12 100644
--- a/packages/spacecat-shared-data-access/test/index.test.js
+++ b/packages/spacecat-shared-data-access/test/unit/index.test.js
@@ -13,7 +13,7 @@
/* eslint-env mocha */
import { expect } from 'chai';
-import { createDataAccess } from '../src/index.js';
+import { createDataAccess } from '../../src/index.js';
describe('Data Access Object Tests', () => {
const auditFunctions = [
diff --git a/packages/spacecat-shared-data-access/test/models/audit.test.js b/packages/spacecat-shared-data-access/test/unit/models/audit.test.js
similarity index 97%
rename from packages/spacecat-shared-data-access/test/models/audit.test.js
rename to packages/spacecat-shared-data-access/test/unit/models/audit.test.js
index a5fa9658d..e1a3c82d8 100644
--- a/packages/spacecat-shared-data-access/test/models/audit.test.js
+++ b/packages/spacecat-shared-data-access/test/unit/models/audit.test.js
@@ -13,7 +13,7 @@
/* eslint-env mocha */
import { expect } from 'chai';
-import { createAudit } from '../../src/models/audit.js';
+import { createAudit } from '../../../src/models/audit.js';
const validData = {
siteId: '123',
diff --git a/packages/spacecat-shared-data-access/test/models/base.test.js b/packages/spacecat-shared-data-access/test/unit/models/base.test.js
similarity index 97%
rename from packages/spacecat-shared-data-access/test/models/base.test.js
rename to packages/spacecat-shared-data-access/test/unit/models/base.test.js
index d4feb48d1..8005fff40 100644
--- a/packages/spacecat-shared-data-access/test/models/base.test.js
+++ b/packages/spacecat-shared-data-access/test/unit/models/base.test.js
@@ -15,7 +15,7 @@
import { isIsoDate } from '@adobe/spacecat-shared-utils';
import { expect } from 'chai';
-import { Base } from '../../src/models/base.js';
+import { Base } from '../../../src/models/base.js';
import { sleep } from '../util.js';
describe('Base Model Tests', () => {
diff --git a/packages/spacecat-shared-data-access/test/models/site.test.js b/packages/spacecat-shared-data-access/test/unit/models/site.test.js
similarity index 98%
rename from packages/spacecat-shared-data-access/test/models/site.test.js
rename to packages/spacecat-shared-data-access/test/unit/models/site.test.js
index 5489f1ee6..72adcd26a 100644
--- a/packages/spacecat-shared-data-access/test/models/site.test.js
+++ b/packages/spacecat-shared-data-access/test/unit/models/site.test.js
@@ -13,7 +13,7 @@
/* eslint-env mocha */
import { expect } from 'chai';
-import { createSite } from '../../src/models/site.js';
+import { createSite } from '../../../src/models/site.js';
import { sleep } from '../util.js';
// Constants for testing
diff --git a/packages/spacecat-shared-data-access/test/service/audits/index.test.js b/packages/spacecat-shared-data-access/test/unit/service/audits/index.test.js
similarity index 99%
rename from packages/spacecat-shared-data-access/test/service/audits/index.test.js
rename to packages/spacecat-shared-data-access/test/unit/service/audits/index.test.js
index 3dfbf707c..ec064c789 100644
--- a/packages/spacecat-shared-data-access/test/service/audits/index.test.js
+++ b/packages/spacecat-shared-data-access/test/unit/service/audits/index.test.js
@@ -16,7 +16,7 @@ import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import sinon from 'sinon';
-import { auditFunctions } from '../../../src/service/audits/index.js';
+import { auditFunctions } from '../../../../src/service/audits/index.js';
chai.use(chaiAsPromised);
diff --git a/packages/spacecat-shared-data-access/test/service/sites/index.test.js b/packages/spacecat-shared-data-access/test/unit/service/sites/index.test.js
similarity index 98%
rename from packages/spacecat-shared-data-access/test/service/sites/index.test.js
rename to packages/spacecat-shared-data-access/test/unit/service/sites/index.test.js
index 738544540..6082a81a5 100644
--- a/packages/spacecat-shared-data-access/test/service/sites/index.test.js
+++ b/packages/spacecat-shared-data-access/test/unit/service/sites/index.test.js
@@ -16,8 +16,8 @@ import chai from 'chai';
import chaiAsPromised from 'chai-as-promised';
import sinon from 'sinon';
-import { siteFunctions } from '../../../src/service/sites/index.js';
-import { createSite } from '../../../src/models/site.js';
+import { siteFunctions } from '../../../../src/service/sites/index.js';
+import { createSite } from '../../../../src/models/site.js';
chai.use(chaiAsPromised);
diff --git a/packages/spacecat-shared-data-access/test/util.js b/packages/spacecat-shared-data-access/test/unit/util.js
similarity index 100%
rename from packages/spacecat-shared-data-access/test/util.js
rename to packages/spacecat-shared-data-access/test/unit/util.js
From a15ac06f9443a07d347835627fb65acaed6cd350 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Fri, 1 Dec 2023 14:38:12 +0100
Subject: [PATCH 26/28] chore: types and small corrections
---
.../src/dto/audit.js | 2 +-
.../spacecat-shared-data-access/src/index.d.ts | 3 ++-
.../src/models/audit.js | 2 +-
.../src/service/audits/accessPatterns.js | 17 +++++++++++++----
.../src/service/sites/accessPatterns.js | 17 ++++++++++-------
5 files changed, 27 insertions(+), 14 deletions(-)
diff --git a/packages/spacecat-shared-data-access/src/dto/audit.js b/packages/spacecat-shared-data-access/src/dto/audit.js
index 156fc5645..0903e251c 100644
--- a/packages/spacecat-shared-data-access/src/dto/audit.js
+++ b/packages/spacecat-shared-data-access/src/dto/audit.js
@@ -51,7 +51,7 @@ export const AuditDto = {
/**
* Converts a DynamoDB item into an Audit object.
- * @param {object } dynamoItem - DynamoDB item.
+ * @param {object} dynamoItem - DynamoDB item.
* @returns {Readonly} Audit object.
*/
fromDynamoItem: (dynamoItem) => {
diff --git a/packages/spacecat-shared-data-access/src/index.d.ts b/packages/spacecat-shared-data-access/src/index.d.ts
index b2aef3069..09a4d509d 100644
--- a/packages/spacecat-shared-data-access/src/index.d.ts
+++ b/packages/spacecat-shared-data-access/src/index.d.ts
@@ -10,6 +10,8 @@
* governing permissions and limitations under the License.
*/
+// TODO: introduce AuditType interface or Scores interface
+
export interface Audit {
getSiteId: () => string;
getAuditedAt: () => string;
@@ -27,7 +29,6 @@ export interface Site {
getCreatedAt: () => string;
getUpdatedAt: () => string;
getAudits: () => Audit[];
- updateBaseURL: (baseURL: string) => Site;
updateImsOrgId: (imsOrgId: string) => Site;
setAudits: (audits: Audit[]) => Site;
}
diff --git a/packages/spacecat-shared-data-access/src/models/audit.js b/packages/spacecat-shared-data-access/src/models/audit.js
index 1f4b9ef81..6b9d77f34 100644
--- a/packages/spacecat-shared-data-access/src/models/audit.js
+++ b/packages/spacecat-shared-data-access/src/models/audit.js
@@ -66,7 +66,7 @@ const Audit = (data = {}) => {
/**
* Creates a new Audit.
*
- * @param {object } data - audit data
+ * @param {object} data - audit data
* @returns {Readonly} audit - new audit
*/
export const createAudit = (data) => {
diff --git a/packages/spacecat-shared-data-access/src/service/audits/accessPatterns.js b/packages/spacecat-shared-data-access/src/service/audits/accessPatterns.js
index a0c381c42..df611922d 100644
--- a/packages/spacecat-shared-data-access/src/service/audits/accessPatterns.js
+++ b/packages/spacecat-shared-data-access/src/service/audits/accessPatterns.js
@@ -28,7 +28,8 @@ const PK_ALL_LATEST_AUDITS = 'ALL_LATEST_AUDITS';
* @param {Logger} log - The logger.
* @param {string} siteId - The ID of the site for which audits are being retrieved.
* @param {string} [auditType] - Optional. The type of audits to retrieve.
- * @returns {Promise} A promise that resolves to an array of audits for the specified site.
+ * @returns {Promise[]>} A promise that resolves to an array of audits
+ * for the specified site.
*/
export const getAuditsForSite = async (dynamoClient, log, siteId, auditType) => {
const queryParams = {
@@ -87,7 +88,7 @@ export const getAuditForSite = async (
* @param {string} auditType - The type of audits to retrieve.
* @param {boolean} ascending - Determines if the audits should be sorted ascending
* or descending by scores.
- * @returns {Promise} A promise that resolves to an array of the latest
+ * @returns {Promise[]>} A promise that resolves to an array of the latest
* audits of the specified type.
*/
export const getLatestAudits = async (
@@ -116,7 +117,7 @@ export const getLatestAudits = async (
* @param {DynamoDbClient} dynamoClient - The DynamoDB client.
* @param {Logger} log - The logger.
* @param {string} siteId - The ID of the site for which audits are being retrieved.
- * @returns {Promise} A promise that resolves to an array of latest audits
+ * @returns {Promise[]>} A promise that resolves to an array of latest audits
* for the specified site.
*/
export const getLatestAuditsForSite = async (dynamoClient, log, siteId) => {
@@ -166,7 +167,7 @@ export const getLatestAuditForSite = async (
* @param {DynamoDbClient} dynamoClient - The DynamoDB client.
* @param {Logger} log - The logger.
* @param {object} auditData - The audit data.
- * @returns {Promise>}
+ * @returns {Promise>}
*/
export const addAudit = async (dynamoClient, log, auditData) => {
const audit = createAudit(auditData);
@@ -189,6 +190,14 @@ export const addAudit = async (dynamoClient, log, auditData) => {
return audit;
};
+/**
+ * Removes audits from the database.
+ * @param {DynamoDbClient} dynamoClient - The DynamoDB client.
+ * @param {Logger} log - The logger.
+ * @param audits
+ * @param latest
+ * @returns {Promise}
+ */
async function removeAudits(dynamoClient, audits, latest = false) {
const tableName = latest ? TABLE_NAME_LATEST_AUDITS : TABLE_NAME_AUDITS;
// TODO: use batch-remove (needs dynamo client update)
diff --git a/packages/spacecat-shared-data-access/src/service/sites/accessPatterns.js b/packages/spacecat-shared-data-access/src/service/sites/accessPatterns.js
index 910ba0863..f5bcf99c6 100644
--- a/packages/spacecat-shared-data-access/src/service/sites/accessPatterns.js
+++ b/packages/spacecat-shared-data-access/src/service/sites/accessPatterns.js
@@ -29,7 +29,7 @@ const TABLE_NAME_SITES = 'sites';
* Retrieves all sites.
*
* @param {DynamoDbClient} dynamoClient - The DynamoDB client.
- * @returns {Promise>} A promise that resolves to an array of all sites.
+ * @returns {Promise[]>} A promise that resolves to an array of all sites.
*/
export const getSites = async (dynamoClient) => {
const dynamoItems = await dynamoClient.query({
@@ -64,7 +64,7 @@ export const getSitesToAudit = async (dynamoClient) => {
* @param {string} auditType - The type of the latest audits to retrieve for each site.
* @param {boolean} [sortAuditsAscending] - Optional. Determines if the audits
* should be sorted ascending or descending by scores.
- * @returns {Promise} A promise that resolves to an array of site objects,
+ * @returns {Promise[]>} A promise that resolves to an array of site objects,
* each with its latest audit of the specified type.
*/
export const getSitesWithLatestAudit = async (
@@ -96,7 +96,7 @@ export const getSitesWithLatestAudit = async (
* @param {DynamoDbClient} dynamoClient - The DynamoDB client.
* @param {Logger} log - The logger.
* @param {string} baseURL - The base URL of the site to retrieve.
- * @returns {Promise} A promise that resolves to the site object if found,
+ * @returns {Promise|null>} A promise that resolves to the site object if found,
* otherwise null.
*/
export const getSiteByBaseURL = async (
@@ -130,7 +130,7 @@ export const getSiteByBaseURL = async (
* @param {string} baseUrl - The base URL of the site to retrieve.
* @param {string} auditType - The type of audits to retrieve for the site.
* @param {boolean} [latestOnly=false] - Determines if only the latest audit should be retrieved.
- * @returns {Promise} A promise that resolves to the site object with audit
+ * @returns {Promise|null>} A promise that resolves to the site object with audit
* data if found, otherwise null.
*/
export const getSiteByBaseURLWithAuditInfo = async (
@@ -172,7 +172,8 @@ export const getSiteByBaseURLWithAuditInfo = async (
* @param {Logger} log - The logger.
* @param {string} baseUrl - The base URL of the site to retrieve.
* @param {string} auditType - The type of audits to retrieve for the site.
- * @returns {Promise} A promise that resolves to the site object with all its audits.
+ * @returns {Promise|null>} A promise that resolves to the site object
+ * with all its audits.
*/
export const getSiteByBaseURLWithAudits = async (
dynamoClient,
@@ -188,7 +189,8 @@ export const getSiteByBaseURLWithAudits = async (
* @param {Logger} log - The logger.
* @param {string} baseUrl - The base URL of the site to retrieve.
* @param {string} auditType - The type of the latest audit to retrieve for the site.
- * @returns {Promise} A promise that resolves to the site object with its latest audit.
+ * @returns {Promise|null>} A promise that resolves to the site object
+ * with its latest audit.
*/
export const getSiteByBaseURLWithLatestAudit = async (
dynamoClient,
@@ -228,7 +230,7 @@ export const addSite = async (dynamoClient, log, siteData) => {
* @param {DynamoDbClient} dynamoClient - The DynamoDB client.
* @param {Logger} log - The logger.
* @param {Site} site - The site.
- * @returns {Promise} - The updated site.
+ * @returns {Promise>} - The updated site.
*/
export const updateSite = async (dynamoClient, log, site) => {
const existingSite = await getSiteByBaseURL(dynamoClient, log, site.getBaseURL());
@@ -252,6 +254,7 @@ export const updateSite = async (dynamoClient, log, site) => {
*/
export const removeSite = async (dynamoClient, log, siteId) => {
try {
+ // TODO: Add transaction support
await removeAuditsForSite(dynamoClient, log, siteId);
await dynamoClient.removeItem(TABLE_NAME_SITES, { id: siteId });
From e33e1a5a27986ec7130586d2d24f0e4c2cefbd3b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Fri, 1 Dec 2023 15:17:44 +0100
Subject: [PATCH 27/28] feat: add wrapper, update doc
---
.../spacecat-shared-data-access/README.md | 72 +++++++++++++++++++
.../spacecat-shared-data-access/package.json | 2 +-
.../spacecat-shared-data-access/src/index.js | 27 +++----
.../src/service/index.js | 33 +++++++++
.../test/it/db.test.js | 2 +-
.../test/unit/index.test.js | 67 +++++++----------
.../test/unit/service/index.test.js | 64 +++++++++++++++++
7 files changed, 207 insertions(+), 60 deletions(-)
create mode 100644 packages/spacecat-shared-data-access/src/service/index.js
create mode 100644 packages/spacecat-shared-data-access/test/unit/service/index.test.js
diff --git a/packages/spacecat-shared-data-access/README.md b/packages/spacecat-shared-data-access/README.md
index 588bf8b79..3fb6fe1fe 100644
--- a/packages/spacecat-shared-data-access/README.md
+++ b/packages/spacecat-shared-data-access/README.md
@@ -71,6 +71,78 @@ The module provides two main DAOs:
- `getLatestAuditForSite`
- `addAudit`
+
+## Integrating Data Access in AWS Lambda Functions
+
+Our `spacecat-shared-data-access` module includes a wrapper that can be easily integrated into AWS Lambda functions using `@adobe/helix-shared-wrap`. This integration allows your Lambda functions to access and manipulate data seamlessly.
+
+### Steps for Integration
+
+1. **Import the Data Access Wrapper**
+
+ Along with other wrappers and utilities, import the `dataAccessWrapper`.
+
+ ```javascript
+ import dataAccessWrapper from '@adobe/spacecat-shared-data-access/wrapper';
+ ```
+
+2. **Modify Your Lambda Wrapper Script**
+
+ Include `dataAccessWrapper` in the chain of wrappers when defining your Lambda handler.
+
+ ```javascript
+ export const main = wrap(run)
+ .with(sqsEventAdapter)
+ .with(dataAccessWrapper) // Add this line
+ .with(sqs)
+ .with(secrets)
+ .with(helixStatus);
+ ```
+
+3. **Access Data in Your Lambda Function**
+
+ Use the `dataAccess` object from the context to interact with your data layer.
+
+ ```javascript
+ async function run(message, context) {
+ const { dataAccess } = context;
+
+ // Example: Retrieve all sites
+ const sites = await dataAccess.getSites();
+ // ... more logic ...
+ }
+ ```
+
+### Example
+
+Here's a complete example of a Lambda function utilizing the data access wrapper:
+
+```javascript
+import wrap from '@adobe/helix-shared-wrap';
+import dataAccessWrapper from '@adobe/spacecat-shared-data-access/wrapper';
+import sqsEventAdapter from './sqsEventAdapter';
+import sqs from './sqs';
+import secrets from '@adobe/helix-shared-secrets';
+import helixStatus from '@adobe/helix-status';
+
+async function run(message, context) {
+ const { dataAccess } = context;
+ try {
+ const sites = await dataAccess.getSites();
+ // Function logic here
+ } catch (error) {
+ // Error handling
+ }
+}
+
+export const main = wrap(run)
+ .with(sqsEventAdapter)
+ .with(dataAccessWrapper)
+ .with(sqs)
+ .with(secrets)
+ .with(helixStatus);
+```
+
## Contributing
Contributions to `spacecat-shared-data-access` are welcome. Please adhere to the standard Git workflow and submit pull requests for proposed changes.
diff --git a/packages/spacecat-shared-data-access/package.json b/packages/spacecat-shared-data-access/package.json
index 15303bac3..327071b69 100644
--- a/packages/spacecat-shared-data-access/package.json
+++ b/packages/spacecat-shared-data-access/package.json
@@ -3,7 +3,7 @@
"version": "1.1.0",
"description": "Shared modules of the Spacecat Services - Data Access",
"type": "module",
- "main": "src/index.js",
+ "main": "src/service/index.js",
"types": "src/index.d.ts",
"scripts": {
"test:it": "mocha --spec \"test/it/**/*.test.js\"",
diff --git a/packages/spacecat-shared-data-access/src/index.js b/packages/spacecat-shared-data-access/src/index.js
index 2b6006e82..3fab17cb4 100644
--- a/packages/spacecat-shared-data-access/src/index.js
+++ b/packages/spacecat-shared-data-access/src/index.js
@@ -10,24 +10,15 @@
* governing permissions and limitations under the License.
*/
-import { createClient } from '@adobe/spacecat-shared-dynamo';
-import { auditFunctions } from './service/audits/index.js';
-import { siteFunctions } from './service/sites/index.js';
+import { createDataAccess } from './service/index.js';
-/**
- * Creates a data access object.
- *
- * @param {Logger} log logger
- * @returns {object} data access object
- */
-export const createDataAccess = (log = console) => {
- const dynamoClient = createClient(log);
-
- const auditFuncs = auditFunctions(dynamoClient, log);
- const siteFuncs = siteFunctions(dynamoClient, log);
+export default function dataAccessWrapper(fn) {
+ return async (request, context) => {
+ if (!context.dataAccess) {
+ const { log } = context;
+ context.dataAccess = createDataAccess(log);
+ }
- return {
- ...auditFuncs,
- ...siteFuncs,
+ return fn(request, context);
};
-};
+}
diff --git a/packages/spacecat-shared-data-access/src/service/index.js b/packages/spacecat-shared-data-access/src/service/index.js
new file mode 100644
index 000000000..6f0b9bddf
--- /dev/null
+++ b/packages/spacecat-shared-data-access/src/service/index.js
@@ -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.
+ */
+
+import { createClient } from '@adobe/spacecat-shared-dynamo';
+import { auditFunctions } from './audits/index.js';
+import { siteFunctions } from './sites/index.js';
+
+/**
+ * Creates a data access object.
+ *
+ * @param {Logger} log logger
+ * @returns {object} data access object
+ */
+export const createDataAccess = (log = console) => {
+ const dynamoClient = createClient(log);
+
+ const auditFuncs = auditFunctions(dynamoClient, log);
+ const siteFuncs = siteFunctions(dynamoClient, log);
+
+ return {
+ ...auditFuncs,
+ ...siteFuncs,
+ };
+};
diff --git a/packages/spacecat-shared-data-access/test/it/db.test.js b/packages/spacecat-shared-data-access/test/it/db.test.js
index 504f229ba..102842a18 100644
--- a/packages/spacecat-shared-data-access/test/it/db.test.js
+++ b/packages/spacecat-shared-data-access/test/it/db.test.js
@@ -18,7 +18,7 @@ import dynamoDbLocal from 'dynamo-db-local';
import { isIsoDate, isValidUrl } from '@adobe/spacecat-shared-utils';
import { sleep } from '../unit/util.js';
-import { createDataAccess } from '../../src/index.js';
+import { createDataAccess } from '../../src/service/index.js';
import { AUDIT_TYPE_LHS } from '../../src/models/audit.js';
import generateSampleData from './generateSampleData.js';
diff --git a/packages/spacecat-shared-data-access/test/unit/index.test.js b/packages/spacecat-shared-data-access/test/unit/index.test.js
index 0a992aa12..893ca5042 100644
--- a/packages/spacecat-shared-data-access/test/unit/index.test.js
+++ b/packages/spacecat-shared-data-access/test/unit/index.test.js
@@ -13,52 +13,39 @@
/* eslint-env mocha */
import { expect } from 'chai';
-import { createDataAccess } from '../../src/index.js';
+import sinon from 'sinon';
+import dataAccessWrapper from '../../src/index.js';
+
+describe('Data Access Wrapper Tests', () => {
+ let mockFn;
+ let mockContext;
+ let mockRequest;
+
+ beforeEach(() => {
+ mockFn = sinon.stub().resolves('function response');
+ mockContext = { log: sinon.spy() };
+ mockRequest = {};
+ });
-describe('Data Access Object Tests', () => {
- const auditFunctions = [
- 'addAudit',
- 'getAuditForSite',
- 'getAuditsForSite',
- 'getLatestAudits',
- 'getLatestAuditForSite',
- 'removeAuditsForSite',
- ];
- const siteFunctions = [
- 'addSite',
- 'updateSite',
- 'removeSite',
- 'getSites',
- 'getSitesToAudit',
- 'getSitesWithLatestAudit',
- 'getSiteByBaseURL',
- 'getSiteByBaseURLWithAuditInfo',
- 'getSiteByBaseURLWithAudits',
- 'getSiteByBaseURLWithLatestAudit',
- ];
+ afterEach(() => {
+ sinon.restore();
+ });
- let dao;
+ it('adds dataAccess to context and calls the wrapped function', async () => {
+ const wrappedFn = dataAccessWrapper(mockFn);
- before(() => {
- dao = createDataAccess();
- });
+ const response = await wrappedFn(mockRequest, mockContext);
- it('contains all known audit functions', () => {
- auditFunctions.forEach((funcName) => {
- expect(dao).to.have.property(funcName);
- });
+ expect(mockFn.calledOnceWithExactly(mockRequest, mockContext)).to.be.true;
+ expect(response).to.equal('function response');
});
- it('contains all known site functions', () => {
- siteFunctions.forEach((funcName) => {
- expect(dao).to.have.property(funcName);
- });
- });
+ it('does not recreate dataAccess if already present in context', async () => {
+ mockContext.dataAccess = { existingDataAccess: true };
+ const wrappedFn = dataAccessWrapper(mockFn);
+
+ await wrappedFn(mockRequest, mockContext);
- it('does not contain any unexpected functions', () => {
- const expectedFunctions = new Set([...auditFunctions, ...siteFunctions]);
- Object.keys(dao).forEach((funcName) => {
- expect(expectedFunctions).to.include(funcName);
- });
+ expect(mockContext.dataAccess).to.deep.equal({ existingDataAccess: true });
});
});
diff --git a/packages/spacecat-shared-data-access/test/unit/service/index.test.js b/packages/spacecat-shared-data-access/test/unit/service/index.test.js
new file mode 100644
index 000000000..2c2f537ed
--- /dev/null
+++ b/packages/spacecat-shared-data-access/test/unit/service/index.test.js
@@ -0,0 +1,64 @@
+/*
+ * 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.
+ */
+
+/* eslint-env mocha */
+
+import { expect } from 'chai';
+import { createDataAccess } from '../../../src/service/index.js';
+
+describe('Data Access Object Tests', () => {
+ const auditFunctions = [
+ 'addAudit',
+ 'getAuditForSite',
+ 'getAuditsForSite',
+ 'getLatestAudits',
+ 'getLatestAuditForSite',
+ 'removeAuditsForSite',
+ ];
+ const siteFunctions = [
+ 'addSite',
+ 'updateSite',
+ 'removeSite',
+ 'getSites',
+ 'getSitesToAudit',
+ 'getSitesWithLatestAudit',
+ 'getSiteByBaseURL',
+ 'getSiteByBaseURLWithAuditInfo',
+ 'getSiteByBaseURLWithAudits',
+ 'getSiteByBaseURLWithLatestAudit',
+ ];
+
+ let dao;
+
+ before(() => {
+ dao = createDataAccess();
+ });
+
+ it('contains all known audit functions', () => {
+ auditFunctions.forEach((funcName) => {
+ expect(dao).to.have.property(funcName);
+ });
+ });
+
+ it('contains all known site functions', () => {
+ siteFunctions.forEach((funcName) => {
+ expect(dao).to.have.property(funcName);
+ });
+ });
+
+ it('does not contain any unexpected functions', () => {
+ const expectedFunctions = new Set([...auditFunctions, ...siteFunctions]);
+ Object.keys(dao).forEach((funcName) => {
+ expect(expectedFunctions).to.include(funcName);
+ });
+ });
+});
From 5d50b16d4342e5f277e7e324e53e3d63689ce95b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Dominique=20J=C3=A4ggi?=
Date: Fri, 1 Dec 2023 15:23:19 +0100
Subject: [PATCH 28/28] chore: doc
---
packages/spacecat-shared-data-access/README.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/packages/spacecat-shared-data-access/README.md b/packages/spacecat-shared-data-access/README.md
index 3fb6fe1fe..65e91a262 100644
--- a/packages/spacecat-shared-data-access/README.md
+++ b/packages/spacecat-shared-data-access/README.md
@@ -1,6 +1,6 @@
# SpaceCat Shared Data Access
-This Node.js module, `spacecat-shared-data-access`, is a comprehensive data access layer for managing sites and their audits, leveraging Amazon DynamoDB. It's tailored for the `StarCatalogue` model, ensuring efficient querying and robust data manipulation.
+This Node.js module, `spacecat-shared-data-access`, is a data access layer for managing sites and their audits, leveraging Amazon DynamoDB.
## Installation
@@ -63,6 +63,7 @@ The module provides two main DAOs:
- `getSiteByBaseURLWithLatestAudit`
- `addSite`
- `updateSite`
+- `removeSite`
### Audit Functions
- `getAuditsForSite`