diff --git a/packages/spacecat-shared-data-access/src/v2/models/audit/index.d.ts b/packages/spacecat-shared-data-access/src/v2/models/audit/index.d.ts index d08598b4..35b09ab2 100644 --- a/packages/spacecat-shared-data-access/src/v2/models/audit/index.d.ts +++ b/packages/spacecat-shared-data-access/src/v2/models/audit/index.d.ts @@ -11,7 +11,7 @@ */ import type { - BaseCollection, BaseModel, LatestAudit, Opportunity, Site, + BaseCollection, BaseModel, LatestAudit, Opportunity, QueryOptions, Site, } from '../index'; export interface Audit extends BaseModel { @@ -33,7 +33,11 @@ export interface Audit extends BaseModel { export interface AuditCollection extends BaseCollection { allBySiteId(siteId: string): Promise; - allBySiteIdAndAuditType(siteId: string, auditType: string): Promise; + allBySiteIdAndAuditType( + siteId: string, + auditType: string, + options?: QueryOptions + ): Promise; allBySiteIdAndAuditTypeAndAuditedAt( siteId: string, auditType: string, auditedAt: string ): Promise; diff --git a/packages/spacecat-shared-data-access/src/v2/models/base/base.collection.js b/packages/spacecat-shared-data-access/src/v2/models/base/base.collection.js index 8d7f8f53..f0ebb5de 100755 --- a/packages/spacecat-shared-data-access/src/v2/models/base/base.collection.js +++ b/packages/spacecat-shared-data-access/src/v2/models/base/base.collection.js @@ -326,6 +326,23 @@ class BaseCollection { return this.#createInstance(record?.data); } + /** + * Checks if an entity exists by its ID. + * @param {string} id - The UUID of the entity to check. + * @return {Promise} - A promise that resolves to true if the entity exists, + * otherwise false. + * @throws {ValidationError} - Throws an error if the ID is not provided. + */ + async existsById(id) { + guardId(this.idName, id, this.entityName); + + const record = await this.entity.get({ [this.idName]: id }).go({ + attributes: [this.idName], + }); + + return isNonEmptyObject(record?.data); + } + /** * Finds a single entity by index keys. * @param {Object} keys - The index keys to use for the query. diff --git a/packages/spacecat-shared-data-access/src/v2/models/base/index.d.ts b/packages/spacecat-shared-data-access/src/v2/models/base/index.d.ts index 908b8923..dd95fd5a 100644 --- a/packages/spacecat-shared-data-access/src/v2/models/base/index.d.ts +++ b/packages/spacecat-shared-data-access/src/v2/models/base/index.d.ts @@ -30,7 +30,7 @@ export interface BaseModel { export interface QueryOptions { index?: string; limit?: number; - sort?: string; + order?: string; attributes?: string[]; } @@ -42,6 +42,7 @@ export interface BaseCollection { allByIndexKeys(keys: object, options?: QueryOptions): Promise; create(item: object): Promise; createMany(items: object[], parent?: T): Promise>; + existsById(id: string): Promise; findByAll(sortKeys?: object, options?: QueryOptions): Promise | null; findById(id: string): Promise | null; findByIndexKeys(indexKeys: object): Promise; diff --git a/packages/spacecat-shared-data-access/src/v2/models/latest-audit/index.d.ts b/packages/spacecat-shared-data-access/src/v2/models/latest-audit/index.d.ts index c12eecce..0469219d 100644 --- a/packages/spacecat-shared-data-access/src/v2/models/latest-audit/index.d.ts +++ b/packages/spacecat-shared-data-access/src/v2/models/latest-audit/index.d.ts @@ -11,7 +11,7 @@ */ import type { - Audit, BaseCollection, BaseModel, Opportunity, Site, + Audit, BaseCollection, BaseModel, Opportunity, QueryOptions, Site, } from '../index'; export interface LatestAudit extends BaseModel { @@ -32,7 +32,7 @@ export interface LatestAudit extends BaseModel { export interface LatestAuditCollection extends BaseCollection { allByAuditId(auditId: string): Promise; allByAuditIdAndAuditType(auditId: string, auditType: string): Promise; - allByAuditType(auditType: string): Promise; + allByAuditType(auditType: string, options?: QueryOptions): Promise; allBySiteId(siteId: string): Promise; allBySiteIdAndAuditType(siteId: string, auditType: string): Promise; findByAuditId(auditId: string): Promise; diff --git a/packages/spacecat-shared-data-access/src/v2/models/site/index.d.ts b/packages/spacecat-shared-data-access/src/v2/models/site/index.d.ts index 16ed89ce..8be22ea0 100644 --- a/packages/spacecat-shared-data-access/src/v2/models/site/index.d.ts +++ b/packages/spacecat-shared-data-access/src/v2/models/site/index.d.ts @@ -43,7 +43,6 @@ export interface Site extends BaseModel { getKeyEvents(): Promise getKeyEventsByTimestamp(timestamp: string): Promise getLatestAudit(): Promise; - getLatestAuditByAuditType(auditType: string): Promise; getLatestAudits(): Promise; getLatestAuditByAuditType(auditType: string): Promise; getOpportunities(): Promise; @@ -74,6 +73,7 @@ export interface SiteCollection extends BaseCollection { allByDeliveryType(deliveryType: string): Promise; allByOrganizationId(organizationId: string): Promise; allSitesToAudit(): Promise; + allWithLatestAudit(auditType: string, order?: string, deliveryType?: string): Promise; findByBaseURL(baseURL: string): Promise; findByDeliveryType(deliveryType: string): Promise; findByOrganizationId(organizationId: string): Promise; diff --git a/packages/spacecat-shared-data-access/src/v2/models/site/site.collection.js b/packages/spacecat-shared-data-access/src/v2/models/site/site.collection.js index 6169fa0a..cf60f669 100755 --- a/packages/spacecat-shared-data-access/src/v2/models/site/site.collection.js +++ b/packages/spacecat-shared-data-access/src/v2/models/site/site.collection.js @@ -10,8 +10,13 @@ * governing permissions and limitations under the License. */ +import { hasText } from '@adobe/spacecat-shared-utils'; + +import DataAccessError from '../../errors/data-access.error.js'; import BaseCollection from '../base/base.collection.js'; +import { DELIVERY_TYPES } from './site.model.js'; + /** * SiteCollection - A collection class responsible for managing Site entities. * Extends the BaseCollection to provide specific methods for interacting with Site records. @@ -23,6 +28,47 @@ class SiteCollection extends BaseCollection { async allSitesToAudit() { return (await this.all({ attributes: ['siteId'] })).map((site) => site.getId()); } + + async allWithLatestAudit(auditType, order = 'asc', deliveryType = null) { + if (!hasText(auditType)) { + throw new DataAccessError('auditType is required', this); + } + + const latestAuditCollection = this.entityRegistry.getCollection('LatestAuditCollection'); + + const sitesQuery = Object.values(DELIVERY_TYPES) + .includes(deliveryType) + ? this.allByDeliveryType(deliveryType) + : this.all(); + + const [sites, latestAudits] = await Promise.all([ + sitesQuery, + latestAuditCollection.all([auditType], { order }), + ]); + + const sitesMap = new Map(sites.map((site) => [site.getId(), site])); + const orderedSites = []; + + // First, append sites with a latest audit in the sorted order + latestAudits.forEach((audit) => { + const site = sitesMap.get(audit.getSiteId()); + if (site) { + // eslint-disable-next-line no-underscore-dangle + site._accessorCache.getLatestAuditByAuditType = audit; + orderedSites.push(site); + sitesMap.delete(site.getId()); // Remove the site from the map to avoid adding it again + } + }); + + // Then, append the remaining sites (without a latest audit) + sitesMap.forEach((site) => { + // eslint-disable-next-line no-underscore-dangle,no-param-reassign + site._accessorCache.getLatestAuditByAuditType = null; + orderedSites.push(site); + }); + + return orderedSites; + } } export default SiteCollection; diff --git a/packages/spacecat-shared-data-access/test/it/site/site.test.js b/packages/spacecat-shared-data-access/test/it/site/site.test.js index a2431ac8..1cc1c570 100644 --- a/packages/spacecat-shared-data-access/test/it/site/site.test.js +++ b/packages/spacecat-shared-data-access/test/it/site/site.test.js @@ -131,6 +131,124 @@ describe('Site IT', async () => { expect(site.getId()).to.equal(sampleData.sites[0].getId()); }); + it('returns true when a site exists by id', async () => { + const exists = await Site.existsById(sampleData.sites[0].getId()); + expect(exists).to.be.true; + }); + + it('returns false when a site does not exist by id', async () => { + const exists = await Site.existsById('adddd03e-bde1-4340-88ef-904070457745'); + expect(exists).to.be.false; + }); + + it('gets all audits for a site', async () => { + const site = await Site.findById(sampleData.sites[1].getId()); + const audits = await site.getAudits(); + + expect(audits).to.be.an('array'); + expect(audits.length).to.equal(10); + + for (let i = 0; i < audits.length; i += 1) { + const audit = audits[i]; + + expect(audit.getId()).to.be.a('string'); + expect(audit.getSiteId()).to.equal(site.getId()); + } + }); + + it('gets all audits for a site by type', async () => { + const site = await Site.findById(sampleData.sites[1].getId()); + const audits = await site.getAuditsByAuditType('cwv'); + + expect(audits).to.be.an('array'); + expect(audits.length).to.equal(5); + + for (let i = 0; i < audits.length; i += 1) { + const audit = audits[i]; + + expect(audit.getId()).to.be.a('string'); + expect(audit.getSiteId()).to.equal(site.getId()); + expect(audit.getAuditType()).to.equal('cwv'); + } + }); + + it('gets all audits for a site by type and auditAt', async () => { + const site = await Site.findById(sampleData.sites[1].getId()); + const audits = await site.getAuditsByAuditTypeAndAuditedAt('cwv', '2024-12-03T08:00:55.754Z'); + + expect(audits).to.be.an('array'); + expect(audits.length).to.equal(5); + + for (let i = 0; i < audits.length; i += 1) { + const audit = audits[i]; + + expect(audit.getId()).to.be.a('string'); + expect(audit.getSiteId()).to.equal(site.getId()); + expect(audit.getAuditType()).to.equal('cwv'); + expect(audit.getAuditedAt()).to.equal('2024-12-03T08:00:55.754Z'); + } + }); + + it('gets latest audit for a site', async () => { + const site = await Site.findById(sampleData.sites[1].getId()); + const audit = await site.getLatestAudit(); + + expect(audit.getId()).to.be.a('string'); + expect(audit.getSiteId()).to.equal(site.getId()); + }); + + it('gets latest audit for a site by type', async () => { + const site = await Site.findById(sampleData.sites[1].getId()); + const audit = await site.getLatestAuditByAuditType('cwv'); + + expect(audit.getId()).to.be.a('string'); + expect(audit.getSiteId()).to.equal(site.getId()); + expect(audit.getAuditType()).to.equal('cwv'); + }); + + it('gets all latest audits for a site', async () => { + const site = await Site.findById(sampleData.sites[1].getId()); + const audits = await site.getLatestAudits(); + + expect(audits).to.be.an('array'); + expect(audits.length).to.equal(2); + + for (let i = 0; i < audits.length; i += 1) { + const audit = audits[i]; + + expect(audit.getId()).to.be.a('string'); + expect(audit.getSiteId()).to.equal(site.getId()); + } + }); + + it('gets all sites with latest audit by type', async () => { + const sites = await Site.allWithLatestAudit('cwv'); + + expect(sites).to.be.an('array'); + expect(sites.length).to.equal(10); + + const siteWithoutAudits = await Site.findById('5d6d4439-6659-46c2-b646-92d110fa5a52'); + await checkSite(siteWithoutAudits); + await expect(siteWithoutAudits.getLatestAuditByAuditType('cwv')).to.eventually.be.null; + + for (let i = 0; i < 10; i += 1) { + // eslint-disable-next-line no-loop-func + const site = sites[i]; + if (site.getId() === siteWithoutAudits.getId()) { + // eslint-disable-next-line no-continue + continue; + } + + await checkSite(site); + + const audit = await site.getLatestAuditByAuditType('cwv'); + + expect(audit).to.be.an('object'); + expect(audit.getSiteId()).to.equal(site.getId()); + expect(audit.getAuditType()).to.equal('cwv'); + } + }); + it('adds a new site', async () => { const newSiteData = { baseURL: 'https://newexample.com', @@ -230,52 +348,4 @@ describe('Site IT', async () => { const notFound = await Site.findById(sampleData.sites[0].getId()); expect(notFound).to.be.null; }); - - it('gets all audits for a site', async () => { - const site = await Site.findById(sampleData.sites[1].getId()); - const audits = await site.getAudits(); - - expect(audits).to.be.an('array'); - expect(audits.length).to.equal(10); - - for (let i = 0; i < audits.length; i += 1) { - const audit = audits[i]; - - expect(audit.getId()).to.be.a('string'); - expect(audit.getSiteId()).to.equal(site.getId()); - } - }); - - it('gets all audits for a site by type', async () => { - const site = await Site.findById(sampleData.sites[1].getId()); - const audits = await site.getAuditsByAuditType('cwv'); - - expect(audits).to.be.an('array'); - expect(audits.length).to.equal(5); - - for (let i = 0; i < audits.length; i += 1) { - const audit = audits[i]; - - expect(audit.getId()).to.be.a('string'); - expect(audit.getSiteId()).to.equal(site.getId()); - expect(audit.getAuditType()).to.equal('cwv'); - } - }); - - it('gets all audits for a site by type and auditAt', async () => { - const site = await Site.findById(sampleData.sites[1].getId()); - const audits = await site.getAuditsByAuditTypeAndAuditedAt('cwv', '2024-12-03T08:00:55.754Z'); - - expect(audits).to.be.an('array'); - expect(audits.length).to.equal(5); - - for (let i = 0; i < audits.length; i += 1) { - const audit = audits[i]; - - expect(audit.getId()).to.be.a('string'); - expect(audit.getSiteId()).to.equal(site.getId()); - expect(audit.getAuditType()).to.equal('cwv'); - expect(audit.getAuditedAt()).to.equal('2024-12-03T08:00:55.754Z'); - } - }); }); diff --git a/packages/spacecat-shared-data-access/test/unit/v2/models/base/base.collection.test.js b/packages/spacecat-shared-data-access/test/unit/v2/models/base/base.collection.test.js index ac3319b0..bef46571 100755 --- a/packages/spacecat-shared-data-access/test/unit/v2/models/base/base.collection.test.js +++ b/packages/spacecat-shared-data-access/test/unit/v2/models/base/base.collection.test.js @@ -251,6 +251,31 @@ describe('BaseCollection', () => { }); }); + describe('existsById', () => { + it('returns true if entity exists', async () => { + const mockFindResult = { data: mockRecord }; + mockElectroService.entities.mockEntityModel.get.returns( + { go: () => Promise.resolve(mockFindResult) }, + ); + + const result = await baseCollectionInstance.existsById('ef39921f-9a02-41db-b491-02c98987d956'); + + expect(result).to.be.true; + expect(mockElectroService.entities.mockEntityModel.get.calledOnce).to.be.true; + }); + + it('returns false if entity does not exist', async () => { + mockElectroService.entities.mockEntityModel.get.returns( + { go: () => Promise.resolve(null) }, + ); + + const result = await baseCollectionInstance.existsById('ef39921f-9a02-41db-b491-02c98987d956'); + + expect(result).to.be.false; + expect(mockElectroService.entities.mockEntityModel.get.calledOnce).to.be.true; + }); + }); + describe('findByIndexKeys', () => { it('throws error if keys is not provided', async () => { await expect(baseCollectionInstance.findByIndexKeys()) diff --git a/packages/spacecat-shared-data-access/test/unit/v2/models/site/site.collection.test.js b/packages/spacecat-shared-data-access/test/unit/v2/models/site/site.collection.test.js index 856649d9..51fcc2ee 100755 --- a/packages/spacecat-shared-data-access/test/unit/v2/models/site/site.collection.test.js +++ b/packages/spacecat-shared-data-access/test/unit/v2/models/site/site.collection.test.js @@ -33,9 +33,7 @@ describe('SiteCollection', () => { let model; let schema; - const mockRecord = { - siteId: 's12345', - }; + const mockRecord = { siteId: 's12345' }; beforeEach(() => { ({ @@ -70,4 +68,49 @@ describe('SiteCollection', () => { expect(instance.all).to.have.been.calledOnceWithExactly({ attributes: ['siteId'] }); }); }); + + describe('allWithLatestAudit', () => { + const mockAudit = { + getId: () => 's12345', + getSiteId: () => 's12345', + }; + + const mockSite = { + getId: () => 's12345', + _accessorCache: { getLatestAuditByAuditType: null }, + }; + + const mockSiteNoAudit = { + getId: () => 'x12345', + _accessorCache: { getLatestAuditByAuditType: null }, + }; + + beforeEach(() => { + mockEntityRegistry.getCollection = stub().returns({ + all: stub().resolves([mockAudit]), + }); + }); + + it('throws error if audit type is not provided', async () => { + await expect(instance.allWithLatestAudit()).to.be.rejectedWith('auditType is required'); + }); + + it('returns all sites with latest audit', async () => { + instance.all = stub().resolves([mockSite]); + + const result = await instance.allWithLatestAudit('cwv'); + + expect(result).to.deep.equal([mockSite]); + expect(instance.all).to.have.been.calledOnce; + }); + + it('returns all sites with latest audit by delivery type', async () => { + instance.allByDeliveryType = stub().resolves([mockSite, mockSiteNoAudit]); + + const result = await instance.allWithLatestAudit('cwv', 'asc', 'aem_cs'); + + expect(result).to.deep.equal([mockSite, mockSiteNoAudit]); + expect(instance.allByDeliveryType).to.have.been.calledOnce; + }); + }); }); diff --git a/packages/spacecat-shared-data-access/test/unit/v2/util.js b/packages/spacecat-shared-data-access/test/unit/v2/util.js index d9587461..075663bc 100755 --- a/packages/spacecat-shared-data-access/test/unit/v2/util.js +++ b/packages/spacecat-shared-data-access/test/unit/v2/util.js @@ -56,6 +56,9 @@ export const createElectroMocks = (Model, record) => { primary: stub(), byOpportunityId: stub(), byOpportunityIdAndStatus: stub(), + 'spacecat-data-gsi1pk-gsi1sk': stub().returns({ + go: () => ({ data: [] }), + }), }, };