Skip to content

Commit 78ff82e

Browse files
committed
Add findRecent method
1 parent de0c622 commit 78ff82e

5 files changed

Lines changed: 332 additions & 0 deletions

File tree

src/archivist/recorder/repositories/git/index.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,26 @@ export default class GitRepository extends RepositoryInterface {
9292
return Promise.all((await this.#getCommits()).map(commit => this.#toDomain(commit, { deferContentLoading: true })));
9393
}
9494

95+
async findRecent(limit, { serviceId, termsType } = {}) {
96+
const commits = (await this.#getCommits()).reverse();
97+
const records = [];
98+
99+
for (const commit of commits) {
100+
if (records.length >= limit) break;
101+
102+
const record = await this.#toDomain(commit, { deferContentLoading: true });
103+
104+
if (!record) continue;
105+
106+
if (serviceId !== undefined && record.serviceId !== serviceId) continue;
107+
if (termsType !== undefined && record.termsType !== termsType) continue;
108+
109+
records.push(record);
110+
}
111+
112+
return records;
113+
}
114+
95115
async count() {
96116
return (await this.git.log(Object.values(DataMapper.COMMIT_MESSAGE_PREFIXES).map(prefix => `--grep=${prefix}`))).length;
97117
}

src/archivist/recorder/repositories/git/index.test.js

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,148 @@ describe('GitRepository', () => {
584584
});
585585
});
586586

587+
describe('#findRecent', () => {
588+
const OTHER_SERVICE = 'other_service';
589+
const OTHER_TERMS = 'Privacy Policy';
590+
591+
before(async function () {
592+
this.timeout(5000);
593+
594+
await subject.save(new Version({
595+
serviceId: SERVICE_PROVIDER_ID,
596+
termsType: TERMS_TYPE,
597+
content: CONTENT,
598+
fetchDate: FETCH_DATE_EARLIER,
599+
snapshotIds: [SNAPSHOT_ID],
600+
}));
601+
await subject.save(new Version({
602+
serviceId: SERVICE_PROVIDER_ID,
603+
termsType: TERMS_TYPE,
604+
content: `${CONTENT} - updated`,
605+
fetchDate: FETCH_DATE,
606+
snapshotIds: [SNAPSHOT_ID],
607+
}));
608+
await subject.save(new Version({
609+
serviceId: SERVICE_PROVIDER_ID,
610+
termsType: OTHER_TERMS,
611+
content: CONTENT,
612+
fetchDate: FETCH_DATE_LATER,
613+
snapshotIds: [SNAPSHOT_ID],
614+
}));
615+
await subject.save(new Version({
616+
serviceId: OTHER_SERVICE,
617+
termsType: TERMS_TYPE,
618+
content: CONTENT,
619+
fetchDate: FETCH_DATE_LATER,
620+
snapshotIds: [SNAPSHOT_ID],
621+
}));
622+
});
623+
624+
after(() => subject.removeAll());
625+
626+
context('without filters', () => {
627+
let records;
628+
629+
before(async () => {
630+
records = await subject.findRecent(10);
631+
});
632+
633+
it('returns records in descending chronological order', () => {
634+
const dates = records.map(record => record.fetchDate.getTime());
635+
636+
expect(dates).to.deep.equal([...dates].sort((a, b) => b - a));
637+
});
638+
639+
it('returns all matching records', () => {
640+
expect(records).to.have.length(4);
641+
});
642+
643+
it('does not load content eagerly', () => {
644+
for (const record of records) {
645+
expect(() => record.content).to.throw('Content not defined');
646+
}
647+
});
648+
649+
it('exposes the metadata needed for feed entries', () => {
650+
const [record] = records;
651+
652+
expect(record.id).to.be.a('string');
653+
expect(record.serviceId).to.be.a('string');
654+
expect(record.termsType).to.be.a('string');
655+
expect(record.fetchDate).to.be.an.instanceof(Date);
656+
expect(record.isFirstRecord).to.be.a('boolean');
657+
expect(record.isTechnicalUpgrade).to.be.a('boolean');
658+
});
659+
});
660+
661+
context('when limit is smaller than the number of matching records', () => {
662+
let records;
663+
664+
before(async () => {
665+
records = await subject.findRecent(2);
666+
});
667+
668+
it('returns at most limit records', () => {
669+
expect(records).to.have.length(2);
670+
});
671+
672+
it('returns the most recent records', () => {
673+
for (const record of records) {
674+
expect(record.fetchDate.getTime()).to.be.at.least(FETCH_DATE.getTime());
675+
}
676+
});
677+
});
678+
679+
context('when a serviceId filter is given', () => {
680+
let records;
681+
682+
before(async () => {
683+
records = await subject.findRecent(10, { serviceId: SERVICE_PROVIDER_ID });
684+
});
685+
686+
it('returns only records for that service', () => {
687+
for (const record of records) {
688+
expect(record.serviceId).to.equal(SERVICE_PROVIDER_ID);
689+
}
690+
});
691+
692+
it('returns all records that match', () => {
693+
expect(records).to.have.length(3);
694+
});
695+
});
696+
697+
context('when both serviceId and termsType filters are given', () => {
698+
let records;
699+
700+
before(async () => {
701+
records = await subject.findRecent(10, { serviceId: SERVICE_PROVIDER_ID, termsType: TERMS_TYPE });
702+
});
703+
704+
it('returns only records for that service and terms type', () => {
705+
for (const record of records) {
706+
expect(record.serviceId).to.equal(SERVICE_PROVIDER_ID);
707+
expect(record.termsType).to.equal(TERMS_TYPE);
708+
}
709+
});
710+
711+
it('returns all records that match', () => {
712+
expect(records).to.have.length(2);
713+
});
714+
});
715+
716+
context('when filters match no record', () => {
717+
let records;
718+
719+
before(async () => {
720+
records = await subject.findRecent(10, { serviceId: 'unknown' });
721+
});
722+
723+
it('returns an empty array', () => {
724+
expect(records).to.deep.equal([]);
725+
});
726+
});
727+
});
728+
587729
describe('#findLatest', () => {
588730
context('when there are records for the given service', () => {
589731
let lastSnapshotId;

src/archivist/recorder/repositories/interface.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,20 @@ class RepositoryInterface {
7979
throw new Error(`#findAll method is not implemented in ${this.constructor.name}`);
8080
}
8181

82+
/**
83+
* Find the most recent records in the repository, optionally filtered by service ID and terms type
84+
* For performance reasons, the content of the records will not be loaded. Use #loadRecordContent to load the content of individual records
85+
* @see RepositoryInterface#loadRecordContent
86+
* @param {number} limit - Maximum number of records to return
87+
* @param {object} [filters] - Optional filters
88+
* @param {string} [filters.serviceId] - Restrict results to this service ID
89+
* @param {string} [filters.termsType] - Restrict results to this terms type
90+
* @returns {Promise<Array<Record>>} Promise that will be resolved with an array of records in descending chronological order
91+
*/
92+
async findRecent(limit, filters) {
93+
throw new Error(`#findRecent method is not implemented in ${this.constructor.name}`);
94+
}
95+
8296
/**
8397
* Count the total number of records in the repository
8498
* For performance reasons, use this method rather than counting the number of entries returned by #findAll if you only need the size of a repository

src/archivist/recorder/repositories/mongo/index.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,22 @@ export default class MongoRepository extends RepositoryInterface {
9393
.map(mongoDocument => this.#toDomain(mongoDocument, { deferContentLoading: true })));
9494
}
9595

96+
async findRecent(limit, { serviceId, termsType } = {}) {
97+
const query = {};
98+
99+
if (serviceId !== undefined) query.serviceId = serviceId;
100+
if (termsType !== undefined) query.termsType = termsType;
101+
102+
const mongoDocuments = await this.collection
103+
.find(query)
104+
.project({ content: 0 })
105+
.sort({ fetchDate: -1 })
106+
.limit(limit)
107+
.toArray();
108+
109+
return Promise.all(mongoDocuments.map(mongoDocument => this.#toDomain(mongoDocument, { deferContentLoading: true })));
110+
}
111+
96112
count() {
97113
return this.collection.countDocuments();
98114
}

src/archivist/recorder/repositories/mongo/index.test.js

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,146 @@ describe('MongoRepository', () => {
671671
});
672672
});
673673

674+
describe('#findRecent', () => {
675+
const OTHER_SERVICE = 'other_service';
676+
const OTHER_TERMS = 'Privacy Policy';
677+
678+
before(async () => {
679+
await subject.save(new Version({
680+
serviceId: SERVICE_PROVIDER_ID,
681+
termsType: TERMS_TYPE,
682+
content: CONTENT,
683+
fetchDate: FETCH_DATE_EARLIER,
684+
snapshotIds: [SNAPSHOT_ID],
685+
}));
686+
await subject.save(new Version({
687+
serviceId: SERVICE_PROVIDER_ID,
688+
termsType: TERMS_TYPE,
689+
content: `${CONTENT} - updated`,
690+
fetchDate: FETCH_DATE,
691+
snapshotIds: [SNAPSHOT_ID],
692+
}));
693+
await subject.save(new Version({
694+
serviceId: SERVICE_PROVIDER_ID,
695+
termsType: OTHER_TERMS,
696+
content: CONTENT,
697+
fetchDate: FETCH_DATE_LATER,
698+
snapshotIds: [SNAPSHOT_ID],
699+
}));
700+
await subject.save(new Version({
701+
serviceId: OTHER_SERVICE,
702+
termsType: TERMS_TYPE,
703+
content: CONTENT,
704+
fetchDate: FETCH_DATE_LATER,
705+
snapshotIds: [SNAPSHOT_ID],
706+
}));
707+
});
708+
709+
after(() => subject.removeAll());
710+
711+
context('without filters', () => {
712+
let records;
713+
714+
before(async () => {
715+
records = await subject.findRecent(10);
716+
});
717+
718+
it('returns records in descending chronological order', () => {
719+
const dates = records.map(record => record.fetchDate.getTime());
720+
721+
expect(dates).to.deep.equal([...dates].sort((a, b) => b - a));
722+
});
723+
724+
it('returns all matching records', () => {
725+
expect(records).to.have.length(4);
726+
});
727+
728+
it('does not load content eagerly', () => {
729+
for (const record of records) {
730+
expect(() => record.content).to.throw('Content not defined');
731+
}
732+
});
733+
734+
it('exposes the metadata needed for feed entries', () => {
735+
const [record] = records;
736+
737+
expect(record.id).to.be.a('string');
738+
expect(record.serviceId).to.be.a('string');
739+
expect(record.termsType).to.be.a('string');
740+
expect(record.fetchDate).to.be.an.instanceof(Date);
741+
expect(record.isFirstRecord).to.be.a('boolean');
742+
expect(record.isTechnicalUpgrade).to.be.a('boolean');
743+
});
744+
});
745+
746+
context('when limit is smaller than the number of matching records', () => {
747+
let records;
748+
749+
before(async () => {
750+
records = await subject.findRecent(2);
751+
});
752+
753+
it('returns at most limit records', () => {
754+
expect(records).to.have.length(2);
755+
});
756+
757+
it('returns the most recent records', () => {
758+
for (const record of records) {
759+
expect(record.fetchDate.getTime()).to.be.at.least(FETCH_DATE.getTime());
760+
}
761+
});
762+
});
763+
764+
context('when a serviceId filter is given', () => {
765+
let records;
766+
767+
before(async () => {
768+
records = await subject.findRecent(10, { serviceId: SERVICE_PROVIDER_ID });
769+
});
770+
771+
it('returns only records for that service', () => {
772+
for (const record of records) {
773+
expect(record.serviceId).to.equal(SERVICE_PROVIDER_ID);
774+
}
775+
});
776+
777+
it('returns all records that match', () => {
778+
expect(records).to.have.length(3);
779+
});
780+
});
781+
782+
context('when both serviceId and termsType filters are given', () => {
783+
let records;
784+
785+
before(async () => {
786+
records = await subject.findRecent(10, { serviceId: SERVICE_PROVIDER_ID, termsType: TERMS_TYPE });
787+
});
788+
789+
it('returns only records for that service and terms type', () => {
790+
for (const record of records) {
791+
expect(record.serviceId).to.equal(SERVICE_PROVIDER_ID);
792+
expect(record.termsType).to.equal(TERMS_TYPE);
793+
}
794+
});
795+
796+
it('returns all records that match', () => {
797+
expect(records).to.have.length(2);
798+
});
799+
});
800+
801+
context('when filters match no record', () => {
802+
let records;
803+
804+
before(async () => {
805+
records = await subject.findRecent(10, { serviceId: 'unknown' });
806+
});
807+
808+
it('returns an empty array', () => {
809+
expect(records).to.deep.equal([]);
810+
});
811+
});
812+
});
813+
674814
describe('#findLatest', () => {
675815
context('when there are records for the given service', () => {
676816
let lastSnapshotId;

0 commit comments

Comments
 (0)