Skip to content

Commit e2f13c8

Browse files
committed
dbToCache: refactor the recreateCache function by object
This allows to load data from DB, then save to cache, one collection at a time. This is useful when requesting to save to cache only one collection at a time, taking it from the database, where the source of truth resides rather than coming from the UI, where it may not be the latest version of the collection. Also useful for network design, where we may add scenarios and services and want to only update those caches, without having previously loaded the collections. These methods load the data and save it to cache directly.
1 parent 1bca47c commit e2f13c8

2 files changed

Lines changed: 423 additions & 38 deletions

File tree

packages/transition-backend/src/services/capnpCache/__tests__/dbToCache.test.ts

Lines changed: 280 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,17 @@ import { lineString as turfLineString } from '@turf/helpers';
99

1010
import { saveAndUpdateAllNodes, saveAllNodesToCache } from '../../nodes/NodeCollectionUtils';
1111

12-
import { recreateCache } from '../dbToCache';
12+
import {
13+
recreateCache,
14+
loadAndSaveDataSourcesToCache,
15+
loadAndSaveAgenciesToCache,
16+
loadAndSaveServicesToCache,
17+
loadAndSaveScenariosToCache,
18+
loadAndSaveLinesToCache,
19+
loadAndSaveLinesByIdsToCache,
20+
loadAndSaveNodesToCache,
21+
loadAndSavePathsToCache
22+
} from '../dbToCache';
1323
import { EventManagerMock } from 'chaire-lib-common/lib/test';
1424
import transitLinesDbQueries from '../../../models/db/transitLines.db.queries';
1525
import transitNodesDbQueries from '../../../models/db/transitNodes.db.queries';
@@ -19,6 +29,7 @@ import transitAgenciesDbQueries from '../../../models/db/transitAgencies.db.quer
1929
import transitServicesDbQueries from '../../../models/db/transitServices.db.queries';
2030
import dataSourcesDbQueries from 'chaire-lib-backend/lib/models/db/dataSources.db.queries';
2131
import placesDbQueries from '../../../models/db/places.db.queries';
32+
import Line from 'transition-common/lib/services/line/Line';
2233

2334
//serviceLocator.socketEventManager = new EventEmitter();
2435

@@ -167,7 +178,6 @@ const lineAttributes = {
167178
category: 'C+' as const,
168179
allow_same_line_transfers: false,
169180
color: '#ffffff',
170-
description: null,
171181
is_autonomous: false,
172182
scheduleByServiceId: { },
173183
data: {
@@ -302,6 +312,274 @@ jest.mock('../../../models/capnpCache/transitPaths.cache.queries', () => {
302312
}
303313
});
304314

315+
describe('loadAndSaveDataSourcesToCache', () => {
316+
beforeEach(() => {
317+
jest.clearAllMocks();
318+
});
319+
320+
test.each([
321+
{ cachePathDirectory: undefined, expectedPath: undefined },
322+
{ cachePathDirectory: '/custom/cache/path', expectedPath: '/custom/cache/path' }
323+
])('should load and save data sources to cache with cachePathDirectory=$cachePathDirectory', async ({ cachePathDirectory, expectedPath }) => {
324+
await loadAndSaveDataSourcesToCache({ cachePathDirectory });
325+
expect(mockedDataSourceDbCollection).toHaveBeenCalledTimes(1);
326+
expect(mockedDsToCache).toHaveBeenCalledWith(expect.objectContaining({
327+
_features: [expect.objectContaining({_attributes: expect.objectContaining(dataSourceAttributes)})]
328+
}), expectedPath);
329+
expect(mockedDsToCache).toHaveBeenCalledTimes(1);
330+
});
331+
});
332+
333+
describe('loadAndSaveAgenciesToCache', () => {
334+
beforeEach(() => {
335+
jest.clearAllMocks();
336+
});
337+
338+
test.each([
339+
{ cachePathDirectory: undefined, expectedPath: undefined },
340+
{ cachePathDirectory: '/custom/cache/path', expectedPath: '/custom/cache/path' }
341+
])('should load and save agencies to cache with cachePathDirectory=$cachePathDirectory', async ({ cachePathDirectory, expectedPath }) => {
342+
await loadAndSaveAgenciesToCache(cachePathDirectory !== undefined ? { cachePathDirectory } : undefined);
343+
expect(mockedAgencyDbCollection).toHaveBeenCalledTimes(1);
344+
expect(mockedAgToCache).toHaveBeenCalledWith(expect.objectContaining({
345+
_features: [expect.objectContaining({_attributes: expect.objectContaining(agencyAttributes)})]
346+
}), expectedPath);
347+
expect(mockedAgToCache).toHaveBeenCalledTimes(1);
348+
});
349+
});
350+
351+
describe('loadAndSaveServicesToCache', () => {
352+
beforeEach(() => {
353+
jest.clearAllMocks();
354+
});
355+
356+
test.each([
357+
{ cachePathDirectory: undefined, expectedPath: undefined },
358+
{ cachePathDirectory: '/custom/cache/path', expectedPath: '/custom/cache/path' }
359+
])('should load and save services to cache with cachePathDirectory=$cachePathDirectory', async ({ cachePathDirectory, expectedPath }) => {
360+
await loadAndSaveServicesToCache(cachePathDirectory !== undefined ? { cachePathDirectory } : undefined);
361+
expect(mockedServiceDbCollection).toHaveBeenCalledTimes(1);
362+
expect(mockedServiceToCache).toHaveBeenCalledWith(expect.objectContaining({
363+
_features: [expect.objectContaining({_attributes: expect.objectContaining(serviceAttributes)})]
364+
}), expectedPath);
365+
expect(mockedServiceToCache).toHaveBeenCalledTimes(1);
366+
});
367+
});
368+
369+
describe('loadAndSaveScenariosToCache', () => {
370+
beforeEach(() => {
371+
jest.clearAllMocks();
372+
});
373+
374+
test.each([
375+
{ cachePathDirectory: undefined, expectedPath: undefined },
376+
{ cachePathDirectory: '/custom/cache/path', expectedPath: '/custom/cache/path' }
377+
])('should load and save scenarios to cache with cachePathDirectory=$cachePathDirectory', async ({ cachePathDirectory, expectedPath }) => {
378+
await loadAndSaveScenariosToCache(cachePathDirectory !== undefined ? { cachePathDirectory } : undefined);
379+
expect(mockedScenarioDbCollection).toHaveBeenCalledTimes(1);
380+
expect(mockedScenariosToCache).toHaveBeenCalledWith(expect.objectContaining({
381+
_features: [expect.objectContaining({_attributes: expect.objectContaining(scenarioAttributes)})]
382+
}), expectedPath);
383+
expect(mockedScenariosToCache).toHaveBeenCalledTimes(1);
384+
});
385+
});
386+
387+
describe('loadAndSaveLinesToCache', () => {
388+
beforeEach(() => {
389+
jest.clearAllMocks();
390+
});
391+
392+
test.each([
393+
{ saveIndividualLines: false },
394+
{ saveIndividualLines: true }
395+
])('should load and save lines collection to cache with saveIndividualLines=$saveIndividualLines', async ({ saveIndividualLines }) => {
396+
await loadAndSaveLinesToCache({ saveIndividualLines });
397+
// collection should be called with empty parameters
398+
expect(mockedLineDbCollection).toHaveBeenCalledWith();
399+
expect(mockedLinesToCache).toHaveBeenCalledWith(expect.objectContaining({
400+
_features: [expect.objectContaining({_attributes: expect.objectContaining(lineAttributes)})]
401+
}), undefined);
402+
expect(mockedLinesToCache).toHaveBeenCalledTimes(1);
403+
if (!saveIndividualLines) {
404+
expect(mockedLinesWithSchedules).not.toHaveBeenCalled();
405+
expect(mockedObjectsToCache).not.toHaveBeenCalled();
406+
} else {
407+
expect(mockedLinesWithSchedules).toHaveBeenCalledWith([expect.objectContaining({_attributes: expect.objectContaining(lineAttributes)})]);
408+
expect(mockedObjectsToCache).toHaveBeenCalledWith(
409+
[expect.objectContaining({_attributes: expect.objectContaining(lineAttributes)})],
410+
undefined
411+
);
412+
}
413+
});
414+
415+
test.each([
416+
{ saveIndividualLines: false, cachePathDirectory: undefined, expectedPath: undefined },
417+
{ saveIndividualLines: true, cachePathDirectory: undefined, expectedPath: undefined },
418+
{ saveIndividualLines: true, cachePathDirectory: '/custom/cache/path', expectedPath: '/custom/cache/path' }
419+
])('should load and save lines with cachePathDirectory and saveIndividualLines=$saveIndividualLines', async ({ saveIndividualLines, cachePathDirectory, expectedPath }) => {
420+
const params: any = { saveIndividualLines };
421+
if (cachePathDirectory !== undefined) {
422+
params.cachePathDirectory = cachePathDirectory;
423+
}
424+
await loadAndSaveLinesToCache(params);
425+
// collection should be called with empty parameters
426+
expect(mockedLineDbCollection).toHaveBeenCalledWith();
427+
expect(mockedLinesToCache).toHaveBeenCalledWith(expect.objectContaining({
428+
_features: [expect.objectContaining({_attributes: expect.objectContaining(lineAttributes)})]
429+
}), expectedPath);
430+
if (saveIndividualLines) {
431+
expect(mockedObjectsToCache).toHaveBeenCalledWith(
432+
[expect.objectContaining({_attributes: expect.objectContaining(lineAttributes)})],
433+
expectedPath
434+
);
435+
} else {
436+
expect(mockedObjectsToCache).not.toHaveBeenCalled();
437+
}
438+
});
439+
440+
test('should save lines in chunks when collection is large', async () => {
441+
// Prepare test data with many lines
442+
const lineIds = Array.from({ length: 250 }, () => uuidV4());
443+
const linesAttributes = lineIds.map((lineId) => ({...lineAttributes, id: lineId}));
444+
const lines = linesAttributes.map((attributes) => new Line(attributes, false));
445+
mockedLineDbCollection.mockResolvedValueOnce(linesAttributes);
446+
447+
// Save with individual lines
448+
await loadAndSaveLinesToCache({ saveIndividualLines: true });
449+
expect(mockedLineDbCollection).toHaveBeenCalledWith();
450+
// Should be called 3 times: 100 + 100 + 50
451+
expect(mockedLinesWithSchedules).toHaveBeenCalledTimes(3);
452+
// Verify the calls were made with correct chunks (100 + 100 + 50)
453+
expect(mockedLinesWithSchedules).toHaveBeenNthCalledWith(1, lines.slice(0, 100));
454+
expect(mockedLinesWithSchedules).toHaveBeenNthCalledWith(2, lines.slice(100, 200));
455+
expect(mockedLinesWithSchedules).toHaveBeenNthCalledWith(3, lines.slice(200, 250));
456+
expect(mockedObjectsToCache).toHaveBeenCalledTimes(3);
457+
});
458+
});
459+
460+
describe('loadAndSaveLinesByIdsToCache', () => {
461+
beforeEach(() => {
462+
jest.clearAllMocks();
463+
});
464+
465+
test.each([
466+
{ cachePathDirectory: undefined, expectedPath: undefined },
467+
{ cachePathDirectory: '/custom/cache/path', expectedPath: '/custom/cache/path' }
468+
])('should load and save specific lines by IDs to cache with cachePathDirectory=$cachePathDirectory', async ({ cachePathDirectory, expectedPath }) => {
469+
const lineIds = [uuidV4(), uuidV4()];
470+
const params: any = { lineIds };
471+
if (cachePathDirectory !== undefined) {
472+
params.cachePathDirectory = cachePathDirectory;
473+
}
474+
await loadAndSaveLinesByIdsToCache(params);
475+
expect(mockedLineDbCollection).toHaveBeenCalledWith(lineIds);
476+
expect(mockedLinesWithSchedules).toHaveBeenCalledWith([expect.objectContaining({_attributes: expect.objectContaining(lineAttributes)})]);
477+
expect(mockedObjectsToCache).toHaveBeenCalledWith(
478+
[expect.objectContaining({_attributes: expect.objectContaining(lineAttributes)})],
479+
expectedPath
480+
);
481+
});
482+
483+
test('should not save anything when lineIds is empty', async () => {
484+
await loadAndSaveLinesByIdsToCache({ lineIds: [], cachePathDirectory: undefined });
485+
expect(mockedLineDbCollection).not.toHaveBeenCalled();
486+
expect(mockedLinesWithSchedules).not.toHaveBeenCalled();
487+
expect(mockedObjectsToCache).not.toHaveBeenCalled();
488+
});
489+
490+
test('should save lines in chunks when many lineIds are provided', async () => {
491+
// Prepare test data with many lines
492+
const lineIds = Array.from({ length: 250 }, () => uuidV4());
493+
const linesAttributes = lineIds.map((lineId) => ({...lineAttributes, id: lineId}));
494+
const lines = linesAttributes.map((attributes) => new Line(attributes, false));
495+
mockedLineDbCollection.mockResolvedValueOnce(linesAttributes);
496+
497+
await loadAndSaveLinesByIdsToCache({ lineIds, cachePathDirectory: undefined });
498+
expect(mockedLineDbCollection).toHaveBeenCalledWith(lineIds);
499+
// Should be called 3 times: 100 + 100 + 50
500+
expect(mockedLinesWithSchedules).toHaveBeenCalledTimes(3);
501+
// Verify the calls were made with correct chunks (100 + 100 + 50)
502+
expect(mockedLinesWithSchedules).toHaveBeenNthCalledWith(1, lines.slice(0, 100));
503+
expect(mockedLinesWithSchedules).toHaveBeenNthCalledWith(2, lines.slice(100, 200));
504+
expect(mockedLinesWithSchedules).toHaveBeenNthCalledWith(3, lines.slice(200, 250));
505+
expect(mockedObjectsToCache).toHaveBeenCalledTimes(3);
506+
});
507+
});
508+
509+
describe('loadAndSaveNodesToCache', () => {
510+
beforeEach(() => {
511+
jest.clearAllMocks();
512+
});
513+
514+
test.each([
515+
{ refreshTransferrableNodes: false, cachePathDirectory: undefined, expectedPath: undefined },
516+
{ refreshTransferrableNodes: true, cachePathDirectory: undefined, expectedPath: undefined },
517+
{ refreshTransferrableNodes: false, cachePathDirectory: '/custom/cache/path', expectedPath: '/custom/cache/path' }
518+
])('should load and save nodes to cache with refreshTransferrableNodes=$refreshTransferrableNodes and cachePathDirectory=$cachePathDirectory', async ({ refreshTransferrableNodes, cachePathDirectory, expectedPath }) => {
519+
const params: any = { refreshTransferrableNodes };
520+
if (cachePathDirectory !== undefined) {
521+
params.cachePathDirectory = cachePathDirectory;
522+
}
523+
await loadAndSaveNodesToCache(params);
524+
expect(mockedNodesToCache).toHaveBeenCalledWith(expect.objectContaining({
525+
_features: [expect.objectContaining({
526+
type: 'Feature' as const,
527+
properties: nodeAttributes,
528+
geometry: nodeGeography
529+
})]
530+
}), expectedPath);
531+
if (refreshTransferrableNodes) {
532+
expect(mockedSaveAndUpdateAllNodes).toHaveBeenCalledTimes(1);
533+
expect(mockedSaveAllNodesToCache).not.toHaveBeenCalled();
534+
} else {
535+
expect(mockedSaveAndUpdateAllNodes).not.toHaveBeenCalled();
536+
expect(mockedSaveAllNodesToCache).toHaveBeenLastCalledWith(
537+
expect.anything(),
538+
expect.anything(),
539+
expectedPath
540+
);
541+
}
542+
});
543+
544+
test('should load and save nodes to cache without parameters', async () => {
545+
await loadAndSaveNodesToCache();
546+
expect(mockedNodesToCache).toHaveBeenCalledWith(expect.objectContaining({
547+
_features: [expect.objectContaining({
548+
type: 'Feature' as const,
549+
properties: nodeAttributes,
550+
geometry: nodeGeography
551+
})]
552+
}), undefined);
553+
expect(mockedSaveAllNodesToCache).toHaveBeenCalledTimes(1);
554+
expect(mockedSaveAndUpdateAllNodes).not.toHaveBeenCalled();
555+
});
556+
});
557+
558+
describe('loadAndSavePathsToCache', () => {
559+
beforeEach(() => {
560+
jest.clearAllMocks();
561+
});
562+
563+
test.each([
564+
{ cachePathDirectory: undefined, expectedPath: undefined },
565+
{ cachePathDirectory: '/custom/cache/path', expectedPath: '/custom/cache/path' }
566+
])('should load and save paths to cache with cachePathDirectory=$cachePathDirectory', async ({ cachePathDirectory, expectedPath }) => {
567+
const params: any = {};
568+
if (cachePathDirectory !== undefined) {
569+
params.cachePathDirectory = cachePathDirectory;
570+
}
571+
await loadAndSavePathsToCache(params);
572+
expect(mockedPathToCache).toHaveBeenCalledWith(expect.objectContaining({
573+
_features: [expect.objectContaining({
574+
type: 'Feature' as const,
575+
properties: pathAttributes,
576+
geometry: pathGeography
577+
})]
578+
}), expectedPath);
579+
expect(mockedPathToCache).toHaveBeenCalledTimes(1);
580+
});
581+
});
582+
305583
describe('Recreate cache', () => {
306584
beforeEach(() => {
307585
jest.clearAllMocks();

0 commit comments

Comments
 (0)