feat(events): publish runId on a public calendar run entry (Rust D125) #208
@@ -103,9 +103,18 @@ function phaseLabel(spec, phaseId) {
|
|||||||
return (phase && (phase.label || phase.id)) || null
|
return (phase && (phase.label || phase.id)) || null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** One calendar entry, from a materialised run. */
|
/**
|
||||||
|
* One calendar entry, from a materialised run.
|
||||||
|
*
|
||||||
|
* **`runId` is published because the event page already publishes it** on every
|
||||||
|
* occurrence, and `?run=` takes it. The calendar was the one public shape that
|
||||||
|
* named a run without saying which, so a client holding a run id from elsewhere
|
||||||
|
* (a module's map marker) had no way to find its event but to fetch every event
|
||||||
|
* page. A projection has none: nothing is committed to it.
|
||||||
|
*/
|
||||||
const publicRunEntry = (run) => ({
|
const publicRunEntry = (run) => ({
|
||||||
kind: 'run',
|
kind: 'run',
|
||||||
|
runId: run.id,
|
||||||
title: run.definition_title,
|
title: run.definition_title,
|
||||||
slug: run.definition_slug,
|
slug: run.definition_slug,
|
||||||
seriesName: run.series_name || null,
|
seriesName: run.series_name || null,
|
||||||
|
|||||||
@@ -26295,6 +26295,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"runId": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "integer"
|
||||||
|
},
|
||||||
|
"example": {
|
||||||
|
"type": "number",
|
||||||
|
"example": 3692
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"example": "Runs only: the run this entry is, the same id `PublicEventOccurrence.runId` carries and `/events/{slug}?run=` takes. A projected entry has none, because nothing is committed to it."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"title": {
|
"title": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -1247,6 +1247,12 @@ const doc = {
|
|||||||
'One calendar entry. `kind` says which of two things it is: a `run` is a materialised occurrence, a `projected` entry is arithmetic past the materialisation horizon — a forecast with nothing committed to it, which a client should draw as such.',
|
'One calendar entry. `kind` says which of two things it is: a `run` is a materialised occurrence, a `projected` entry is arithmetic past the materialisation horizon — a forecast with nothing committed to it, which a client should draw as such.',
|
||||||
properties: {
|
properties: {
|
||||||
kind: { type: 'string', enum: ['run', 'projected'], example: 'run' },
|
kind: { type: 'string', enum: ['run', 'projected'], example: 'run' },
|
||||||
|
runId: {
|
||||||
|
type: 'integer',
|
||||||
|
example: 3692,
|
||||||
|
description:
|
||||||
|
'Runs only: the run this entry is, the same id `PublicEventOccurrence.runId` carries and `/events/{slug}?run=` takes. A projected entry has none, because nothing is committed to it.',
|
||||||
|
},
|
||||||
title: { type: 'string', example: 'The Yew Invasion' },
|
title: { type: 'string', example: 'The Yew Invasion' },
|
||||||
slug: { type: 'string', example: 'the-yew-invasion' },
|
slug: { type: 'string', example: 'the-yew-invasion' },
|
||||||
seriesName: { type: 'string', nullable: true, example: 'The Yew Campaign' },
|
seriesName: { type: 'string', nullable: true, example: 'The Yew Campaign' },
|
||||||
|
|||||||
@@ -164,10 +164,32 @@ test('a calendar entry carries no operational field at all', async () => {
|
|||||||
// The whole security property of this file, asserted positively: the entry has
|
// The whole security property of this file, asserted positively: the entry has
|
||||||
// exactly these keys and gaining one is a deliberate act.
|
// exactly these keys and gaining one is a deliberate act.
|
||||||
assert.deepEqual(Object.keys(entry).sort(), [
|
assert.deepEqual(Object.keys(entry).sort(), [
|
||||||
'kind', 'live', 'scheduledFor', 'seriesName', 'seriesSlug', 'slug', 'status', 'timezone', 'title',
|
'kind', 'live', 'runId', 'scheduledFor', 'seriesName', 'seriesSlug', 'slug', 'status', 'timezone',
|
||||||
|
'title',
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('a run entry names its run, and a projection names none', async () => {
|
||||||
|
// Rust phase 15, D125: a map marker carries core's run id, and the app finds
|
||||||
|
// the event it belongs to from this calendar. The id is the one the event page
|
||||||
|
// already publishes on each occurrence.
|
||||||
|
store.definitions[0].spec = {
|
||||||
|
...SPEC,
|
||||||
|
schedule: { kind: 'weekly', days: ['saturday'], time: '00:00' },
|
||||||
|
}
|
||||||
|
const result = await publicModel.calendar({ from: '2026-08-28', to: '2026-09-15', now: NOW })
|
||||||
|
const runs = result.entries.filter((e) => e.kind === 'run')
|
||||||
|
const projected = result.entries.filter((e) => e.kind === 'projected')
|
||||||
|
assert.equal(runs.length, 1)
|
||||||
|
assert.equal(runs[0].runId, store.runs[0].id)
|
||||||
|
assert.ok(projected.length > 0, 'the weekly schedule must forecast past the one run')
|
||||||
|
for (const entry of projected) assert.equal('runId' in entry, false)
|
||||||
|
|
||||||
|
const page = await publicModel.event('the-yew-invasion')
|
||||||
|
const occurrences = [page.event.current, page.event.next, ...page.event.upcoming, ...page.event.past]
|
||||||
|
assert.ok(occurrences.some((o) => o && o.runId === runs[0].runId))
|
||||||
|
})
|
||||||
|
|
||||||
test('the default window reaches back as well as forward', async () => {
|
test('the default window reaches back as well as forward', async () => {
|
||||||
// §I: this route is "upcoming, live and recent". The default used to start at
|
// §I: this route is "upcoming, live and recent". The default used to start at
|
||||||
// `now`, which left no room for the third word — an event that finished an hour
|
// `now`, which left no room for the third word — an event that finished an hour
|
||||||
|
|||||||
Reference in New Issue
Block a user