mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
d72352911e
A saved tour now has a technical revision, raised inside the transaction of
every write that changes it: the route and its controls, the plan laid onto a
recorded paddle track, going back to that track. A write that names the
revision it was based on (expected_revision) is refused with a 409
{ code: 'tour_revision_conflict', revision } before anything of it is
written, so a stale draft never overwrites a newer tour unseen. A write that
names none writes as before, so older clients and queued offline writes keep
working; the queue never carries a base. A copy starts at 1, a source's
version stays provenance. Migration 044800 adds the column; list_tours and
get_tour_plan read it.
The editor follows the trip's tours live: a newer revision of the open tour
loads at once while the editor has no changes of its own, and shows a notice
while it has. A refused save keeps the draft and asks: save it as a new tour,
load the newer version, or overwrite on purpose.
useTourPlanner was at the size limit; its derived facts, undo history and
draft storage move into tourPlannerFacts, usePlannerHistory and
useTourDraftStorage, unchanged in behaviour.
46 lines
1.8 KiB
TypeScript
46 lines
1.8 KiB
TypeScript
/**
|
|
* advanceTourRevision (P2C-03): the one revision contract every tour write runs
|
|
* inside its transaction. The SQL is covered by Tours.repository.test.ts and
|
|
* the routes by tours-planned.e2e.test.ts; this pins the three answers.
|
|
*/
|
|
import type { ToursRepository } from '../../../src/db/repositories/Tours.repository';
|
|
import { DomainError } from '../../../src/nest/common/domain-error';
|
|
import { advanceTourRevision } from '../../../src/nest/tours/tour-revision';
|
|
import { NotFoundException } from '@nestjs/common';
|
|
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
function repo(next: number | null, current: number | undefined) {
|
|
return {
|
|
advanceRevision: vi.fn(async () => next),
|
|
revisionOf: vi.fn(async () => current),
|
|
} as unknown as ToursRepository & {
|
|
advanceRevision: ReturnType<typeof vi.fn>;
|
|
revisionOf: ReturnType<typeof vi.fn>;
|
|
};
|
|
}
|
|
|
|
describe('advanceTourRevision', () => {
|
|
it('answers the raised revision and passes the expected one on', async () => {
|
|
const tours = repo(4, 4);
|
|
expect(await advanceTourRevision(tours, 7, 3)).toBe(4);
|
|
expect(tours.advanceRevision).toHaveBeenCalledWith(7, 3);
|
|
expect(tours.revisionOf).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('refuses a stale base with a 409 that names the revision the tour has now', async () => {
|
|
const refusal = await advanceTourRevision(repo(null, 5), 7, 3).catch((err: unknown) => err);
|
|
expect(refusal).toBeInstanceOf(DomainError);
|
|
expect((refusal as DomainError).getStatus()).toBe(409);
|
|
expect((refusal as DomainError).toBody()).toEqual({
|
|
error: 'This tour was changed in the meantime',
|
|
code: 'tour_revision_conflict',
|
|
revision: 5,
|
|
});
|
|
});
|
|
|
|
it('is a 404 when the tour went away meanwhile', async () => {
|
|
await expect(advanceTourRevision(repo(null, undefined), 7)).rejects.toBeInstanceOf(NotFoundException);
|
|
});
|
|
});
|