Skip to content

Commit

Permalink
fix: update latest audit and allByIndexQuery (#528)
Browse files Browse the repository at this point in the history
Co-authored-by: rublea <[email protected]>
Co-authored-by: Dominique Jäggi <[email protected]>
  • Loading branch information
3 people authored Jan 7, 2025
1 parent 23ce7ac commit ff2c5c9
Show file tree
Hide file tree
Showing 12 changed files with 174 additions and 8 deletions.
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -361,19 +361,24 @@ class BaseCollection {
* the entity.
* @async
* @param {Object} item - The data for the entity to be created.
* @param {Object} [options] - Additional options for the creation process.
* @param {boolean} [options.upsert=false] - Whether to perform an upsert operation.
* @returns {Promise<BaseModel>} - A promise that resolves to the created model instance.
* @throws {DataAccessError} - Throws an error if the data is invalid or if the
* creation process fails.
*/
async create(item) {
async create(item, { upsert = false } = {}) {
if (!isNonEmptyObject(item)) {
const message = `Failed to create [${this.entityName}]: data is required`;
this.log.error(message);
throw new DataAccessError(message);
}

try {
const record = await this.entity.create(item).go();
const record = upsert
? await this.entity.put(item).go()
: await this.entity.create(item).go();

const instance = this.#createInstance(record.data);

this.#invalidateCache();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,29 @@ class SchemaBuilder {
return this;
}

/**
* By default createdAt and updatedAt are readOnly. This method allows
* to disable this behavior and allow upserts.
*
* @param {boolean} allow - Whether to allow upserts.
* @returns {SchemaBuilder}
*/
withUpsertable(allow) {
if (!isBoolean(allow)) {
throw new SchemaBuilderError(this, 'allow must be a boolean.');
}

if (allow) {
this.addAttribute('createdAt', {
type: 'string',
required: true,
default: () => new Date().toISOString(),
});
}

return this;
}

/**
* By default a schema allows removes. This method allows
* to disable removes for this entity. Note that this does
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ import { guardId, guardString } from '../../util/index.js';
* @extends AuditCollection
*/
class LatestAuditCollection extends BaseCollection {
async create(item) {
return super.create(item, { upsert: true });
}

async allByAuditType(auditType) {
guardString('auditType', auditType, this.entityName);

return this.all({ auditType });
}

async findById(siteId, auditType) {
guardId('siteId', siteId, this.entityName);
guardString('auditType', auditType, this.entityName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,11 @@ Indexes Doc: https://electrodb.dev/en/modeling/indexes/
const schema = new SchemaBuilder(LatestAudit, LatestAuditCollection)
.withPrimaryPartitionKeys(['siteId'])
.withPrimarySortKeys(['auditType'])
.withUpsertable(true)
.addReference('belongs_to', 'Site', ['auditType'])
.addReference('belongs_to', 'Audit', ['auditType'])
.addReference('has_many', 'Opportunities')
.addAllIndex(['auditType'])
.allowUpdates(false)
.allowRemove(false)
.addAttribute('auditResult', {
type: 'any',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ export function createAccessor(config) { /* eslint-disable no-underscore-dangle
});
}

if (context[name]) {
return;
}

const foreignKeys = {
...isNonEmptyObject(foreignKey) && { [foreignKey.name]: foreignKey.value },
};
Expand Down Expand Up @@ -150,9 +154,8 @@ export function createAccessor(config) { /* eslint-disable no-underscore-dangle
);
}

export function createAccessors(configs, log) {
export function createAccessors(configs) {
configs.forEach((config) => {
createAccessor(config);
log.debug(`Created accessor ${config.name} for ${config.context.schema.getModelName()} to ${config.collection.schema.getModelName()}`);
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@ describe('LatestAudit IT', async () => {
let sampleData;
let LatestAudit;
let Audit;
let dataAccess;

before(async () => {
sampleData = await seedDatabase();

const dataAccess = getDataAccess();
dataAccess = getDataAccess();
LatestAudit = dataAccess.LatestAudit;
Audit = dataAccess.Audit;
});
Expand Down Expand Up @@ -98,6 +99,17 @@ describe('LatestAudit IT', async () => {
});
});

it('gets all latest audits of a type', async () => {
const audits = await LatestAudit.allByAuditType('cwv');

expect(audits).to.be.an('array');
expect(audits.length).to.equal(9);
audits.forEach((audit) => {
expect(audit.getAuditType()).to.equal('cwv');
checkAudit(audit);
});
});

it('gets latest audits of type for a site', async () => {
const auditType = 'lhs-mobile';
const site = sampleData.sites[1];
Expand Down Expand Up @@ -133,4 +145,64 @@ describe('LatestAudit IT', async () => {

expect(audit).to.be.null;
});

it('updates a latest audit upon audit creation', async () => {
const auditType = 'lhs-mobile';
const site = sampleData.sites[1];
const previousLatestAudit = await site.getLatestAuditByAuditType(auditType);
const audit = await Audit.create({
siteId: site.getId(),
isLive: true,
auditedAt: '2025-01-06T10:11:51.833Z',
auditType,
auditResult: {
scores: {
performance: 0.4,
seo: 0.47,
accessibility: 0.27,
'best-practices': 0.55,
},
},
fullAuditRef: 'https://example.com/audit',
});
checkAudit(audit);
const updatedSite = await dataAccess.Site.findById(site.getId());
const latestAudit = await updatedSite.getLatestAuditByAuditType(auditType);
checkAudit(latestAudit);
expect(latestAudit.getSiteId()).to.equal(site.getId());
expect(latestAudit.getAuditType()).to.equal(auditType);
expect(latestAudit.getAuditedAt()).to.equal(audit.getAuditedAt());
expect(latestAudit.getAuditedAt()).to.not.equal(previousLatestAudit.getAuditedAt());
expect(latestAudit.getUpdatedAt()).to.not.equal(previousLatestAudit.getUpdatedAt());
});

it('creates a latest audit upon audit creation', async () => {
const auditType = 'broken-backlinks';
const site = sampleData.sites[0];
const previousLatestAudit = await site.getLatestAuditByAuditType(auditType);

const audit = await Audit.create({
siteId: site.getId(),
isLive: true,
auditedAt: '2025-01-06T10:11:51.833Z',
auditType,
auditResult: {
scores: {
performance: 0.4,
seo: 0.47,
accessibility: 0.27,
'best-practices': 0.55,
},
},
fullAuditRef: 'https://example.com/audit',
});
checkAudit(audit);
const updatedSite = await dataAccess.Site.findById(site.getId());
const latestAudit = await updatedSite.getLatestAuditByAuditType(auditType);
checkAudit(latestAudit);
expect(previousLatestAudit).to.be.null;
expect(latestAudit.getSiteId()).to.equal(site.getId());
expect(latestAudit.getAuditType()).to.equal(auditType);
expect(latestAudit.getAuditedAt()).to.equal(audit.getAuditedAt());
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,15 @@ describe('BaseCollection', () => {
expect(mockElectroService.entities.mockEntityModel.create.calledOnce).to.be.true;
});

it('upserts an existing entity successfully', async () => {
mockElectroService.entities.mockEntityModel.put.returns(
{ go: () => Promise.resolve({ data: mockRecord }) },
);
const result = await baseCollectionInstance.create(mockRecord, { upsert: true });
expect(result.record).to.deep.include(mockRecord);
expect(mockElectroService.entities.mockEntityModel.put.calledOnce).to.be.true;
});

it('logs an error and throws when creation fails', async () => {
const error = new Error('Create failed');
mockElectroService.entities.mockEntityModel.create.returns(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ describe('SchemaBuilder', () => {
});
});

describe('withUpsertable', () => {
it('throws error if upsertable is not a boolean', () => {
expect(() => instance.withUpsertable('test'))
.to.throw(SchemaBuilderError, '[SpaceCat -> MockModel] allow must be a boolean.');
});
});

describe('allowRemove', () => {
it('throws error if allowRemove is not a boolean', () => {
expect(() => instance.allowRemove('test'))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,29 @@ describe('LatestAuditCollection', () => {
});
});

describe('create', () => {
it('creates a new latest audit', async () => {
const result = await instance.create(mockRecord);

expect(result).to.be.an('object');
expect(result.record.latestAuditId).to.equal(mockRecord.latestAuditId);
});
});

describe('allByAuditType', () => {
it('returns all latest audits by audit type', async () => {
const auditType = 'lhs-mobile';

instance.all = stub().resolves([mockRecord]);

const audits = await instance.allByAuditType(auditType);

expect(audits).to.be.an('array');
expect(audits.length).to.equal(1);
expect(instance.all).to.have.been.calledWithExactly({ auditType });
});
});

describe('findById', () => {
it('finds latest audit by id', async () => {
const siteId = '78fec9c7-2141-4600-b7b1-ea5c78752b91';
Expand Down
2 changes: 1 addition & 1 deletion packages/spacecat-shared-data-access/test/unit/v2/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const createElectroMocks = (Model, record) => {
set: stub(),
}),
put: stub().returns({
go: stub().resolves({}),
go: stub().resolves({ data: record }),
}),
query: {
all: stub().returns({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,20 @@ describe('Accessor Utils', () => { /* eslint-disable no-underscore-dangle */

expect(mockContext._accessorCache).to.deep.equal({ a: 1 });
});

it('does not create accessor if context already has a function with the same name', async () => {
const config = {
collection: mockCollection,
context: { test: () => {} },
name: 'test',
requiredKeys: ['test'],
};

createAccessor(config);

expect(mockCollection.schema.getAttribute).to.not.have.been.called;
expect(mockCollection.findByIndexKeys).to.not.have.been.called;
});
});

describe('call accessor', () => {
Expand Down

0 comments on commit ff2c5c9

Please sign in to comment.