@pmeweb/content 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -2
- package/dist/collection.d.ts +2 -0
- package/dist/published.d.ts +7 -0
- package/dist/published.js +16 -1
- package/dist/schedule.d.ts +56 -15
- package/dist/schedule.js +217 -41
- package/dist/types.d.ts +18 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,9 +8,9 @@ collections in `src/content.schema.json`; this package is what reads that declar
|
|
|
8
8
|
| `@pmeweb/content/types` | `CollectionDecl`, `Field`, `ScheduleDecl`, `ContentSchema` | sites, portal |
|
|
9
9
|
| `@pmeweb/content/zodFrom` | declaration -> Astro zod schema (needs `astro`) | sites |
|
|
10
10
|
| `@pmeweb/content/fields` | `isDate`, `isDateTime`, `isTime`, `isDayDate`, `TIME_RE` | portal tier 1, `zodFrom` |
|
|
11
|
-
| `@pmeweb/content/schedule` | `scheduleState`, `visibleOnSite`, `portalPreview`, `scheduleErrors`, `jourQuebec`, `quebecDay`, `quebecMidnight`, `nextQuebecDay`, `scheduledCollections`, `entryBoundaries`, `nextChangeAt`, `Schedule` | sites, portal |
|
|
11
|
+
| `@pmeweb/content/schedule` | `scheduleState`, `visibleOnSite`, `portalPreview`, `scheduleErrors`, `jourQuebec`, `quebecDay`, `quebecMidnight`, `nextQuebecDay`, `scheduledCollections`, `entryBoundaries`, `nextChangeAt`, `Schedule`, `itemsField`, `itemState`, `itemStates`, `changePassed`, `entryEvents`, `ScheduleEvent` | sites, portal |
|
|
12
12
|
| `@pmeweb/content/collection` | `titleFieldError`, `thumbnailError`, `richErrors`, `collectionErrors`, `schemaErrors`, `richBodyField`, `hasThumbnails` | portal, `zodFrom` |
|
|
13
|
-
| `@pmeweb/content/published` | `getPublished`, the one filter every site's pages go through | sites |
|
|
13
|
+
| `@pmeweb/content/published` | `getPublished`, the one filter every site's pages go through; `getPublishedItems` | sites |
|
|
14
14
|
| `@pmeweb/content/prose.css` | the structure of a row of images in a rich body | sites' `Prose.astro` |
|
|
15
15
|
|
|
16
16
|
## Dated kinds
|
|
@@ -30,6 +30,15 @@ collections in `src/content.schema.json`; this package is what reads that declar
|
|
|
30
30
|
- `removeAt`: one path or several; the first that yields a value wins. `dates[].debut` takes the
|
|
31
31
|
latest value in the array. Always read as a Quebec day: the entry stays through that whole day
|
|
32
32
|
and comes off at the start of the next one.
|
|
33
|
+
- `items` (2.2.0): `{ "field": "dates", "publishAt": "publierLe", "removeAt": "debut" }` -- an
|
|
34
|
+
array whose items go on and come off one by one, by the same rules on their sub-fields
|
|
35
|
+
(`itemState`); the entry is live while any item is. A site filters them with
|
|
36
|
+
`getPublishedItems(data, decl.<collection>, now, APERCU)`. NESTED so an older reader ignores it.
|
|
37
|
+
An entry-level `removeAt` path into the same array is refused (one fact, two declarations).
|
|
38
|
+
- `changesAt` (2.2.0): `["prixFin"]` -- top-level dates whose passing changes the page without
|
|
39
|
+
changing whether the entry is on it. A `date` passes at the start of the next Quebec day, a
|
|
40
|
+
`datetime` at its instant. A site reads them with `changePassed(data, decl, field, now)`, which
|
|
41
|
+
THROWS for a field not listed, so `/schedule.json` always announces what the page compares.
|
|
33
42
|
|
|
34
43
|
A site that declares a schedule serves `/schedule.json` (`{ next }`, built with
|
|
35
44
|
`scheduledCollections`, `entryBoundaries` and `nextChangeAt`; the boilerplate ships the endpoint),
|
|
@@ -45,6 +54,18 @@ reads both once, `const APERCU = portalPreview(process.env)`, and filters with
|
|
|
45
54
|
drafts in the preview, removed ones only in a preview that asked for them. Only the exact
|
|
46
55
|
string `'1'` counts, and the past flag means nothing without the preview flag.
|
|
47
56
|
|
|
57
|
+
## 2.2.0
|
|
58
|
+
|
|
59
|
+
Additive: every 2.1.0 declaration is valid and behaves identically.
|
|
60
|
+
|
|
61
|
+
- `schedule.items` and `schedule.changesAt` (above), validated by `scheduleErrors`.
|
|
62
|
+
- `itemState`, `itemStates`, `itemsField`; `scheduleState` reads `items`.
|
|
63
|
+
- `changePassed`.
|
|
64
|
+
- `entryEvents(data, decl, { ignoreDraft? })`: every moment an entry's page changes
|
|
65
|
+
(`publish` / `remove` / `change`, with the instant, the typed day, the field and the item). The
|
|
66
|
+
portal's calendar reads it; `entryBoundaries` is its instants.
|
|
67
|
+
- `getPublishedItems` in `@pmeweb/content/published`.
|
|
68
|
+
|
|
48
69
|
## 2.1.0
|
|
49
70
|
|
|
50
71
|
Additive, for `/schedule.json`:
|
package/dist/collection.d.ts
CHANGED
package/dist/published.d.ts
CHANGED
|
@@ -2,3 +2,10 @@ import { type PortalPreview, type SchedulableLike } from './schedule.js';
|
|
|
2
2
|
export declare function getPublished<E extends {
|
|
3
3
|
data: object;
|
|
4
4
|
}>(entries: readonly E[], decl: SchedulableLike, now: Date, preview: PortalPreview): E[];
|
|
5
|
+
/**
|
|
6
|
+
* The items of `schedule.items.field` the site builds (2.2.0): each by its own `itemState`, through
|
|
7
|
+
* the same `visibleOnSite` as `getPublished`. Order kept: the site sorts. `I` is the site's item
|
|
8
|
+
* type -- the one cast, since the field is named by the declaration, not by the type. THROWS on a
|
|
9
|
+
* collection with no `items`: the call is a bug, and a build is where it should fail.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getPublishedItems<I = unknown>(data: object, decl: SchedulableLike, now: Date, preview: PortalPreview): I[];
|
package/dist/published.js
CHANGED
|
@@ -5,10 +5,25 @@
|
|
|
5
5
|
//
|
|
6
6
|
// NO ASTRO IMPORT. The site passes `getCollection(...)`'s result, so `astro` stays an optional
|
|
7
7
|
// peer and this file stays testable.
|
|
8
|
-
import { scheduleState, visibleOnSite } from './schedule.js';
|
|
8
|
+
import { itemState, itemsField, scheduleState, visibleOnSite } from './schedule.js';
|
|
9
9
|
// `data: object`, NOT `Record<string, unknown>`: every site types its entries with an
|
|
10
10
|
// `interface` (`BlogData`, `ArticleData`), and an interface has no implicit index signature, so
|
|
11
11
|
// the narrower bound would refuse exactly the call this function exists for.
|
|
12
12
|
export function getPublished(entries, decl, now, preview) {
|
|
13
13
|
return entries.filter((e) => visibleOnSite(scheduleState(e.data, decl, now), preview));
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* The items of `schedule.items.field` the site builds (2.2.0): each by its own `itemState`, through
|
|
17
|
+
* the same `visibleOnSite` as `getPublished`. Order kept: the site sorts. `I` is the site's item
|
|
18
|
+
* type -- the one cast, since the field is named by the declaration, not by the type. THROWS on a
|
|
19
|
+
* collection with no `items`: the call is a bug, and a build is where it should fail.
|
|
20
|
+
*/
|
|
21
|
+
export function getPublishedItems(data, decl, now, preview) {
|
|
22
|
+
const field = itemsField(decl);
|
|
23
|
+
if (field === null)
|
|
24
|
+
throw new Error('getPublishedItems : la collection ne déclare pas schedule.items');
|
|
25
|
+
const arr = data[field];
|
|
26
|
+
if (!Array.isArray(arr))
|
|
27
|
+
return [];
|
|
28
|
+
return arr.filter((it) => visibleOnSite(itemState(it, decl, now), preview));
|
|
29
|
+
}
|
package/dist/schedule.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface SchedulableLike {
|
|
|
16
16
|
publishAt?: unknown;
|
|
17
17
|
removeAt?: unknown;
|
|
18
18
|
draft?: unknown;
|
|
19
|
+
items?: unknown;
|
|
20
|
+
changesAt?: unknown;
|
|
19
21
|
};
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
@@ -49,8 +51,30 @@ export declare function quebecDay(value: unknown, kind: string): string | null;
|
|
|
49
51
|
* The site's `zodFrom` THROWS on any of these, so a bad declaration fails the build that
|
|
50
52
|
* introduced it. The portal DROPS the declaration with a warning: a list that hid nothing is a
|
|
51
53
|
* better failure than a page that does not open.
|
|
54
|
+
*
|
|
55
|
+
* 2.2.0 adds `items` and `changesAt`; a 2.1.0 declaration gets exactly the sentences it got before.
|
|
52
56
|
*/
|
|
53
57
|
export declare function scheduleErrors(c: SchedulableLike): string[];
|
|
58
|
+
/** The array `schedule.items` publishes item by item, or null when the collection declares none. */
|
|
59
|
+
export declare function itemsField(c: SchedulableLike): string | null;
|
|
60
|
+
export type ItemState = 'scheduled' | 'live' | 'removed';
|
|
61
|
+
/**
|
|
62
|
+
* Where ONE item of `schedule.items.field` stands at `now`: the entry's rules on its sub-fields.
|
|
63
|
+
* `scheduled` before its `publishAt`, `removed` once the Quebec day is past its `removeAt`,
|
|
64
|
+
* `scheduled` beats `removed`. No item-level draft. A non-object item, and any item of an invalid
|
|
65
|
+
* declaration, is `live`: never hide what cannot be read.
|
|
66
|
+
*/
|
|
67
|
+
export declare function itemState(item: unknown, c: SchedulableLike, now: Date): ItemState;
|
|
68
|
+
/** Every item's state, by index; `[]` when the array is absent; null when no `items` is declared. */
|
|
69
|
+
export declare function itemStates(data: object, c: SchedulableLike, now: Date): ItemState[] | null;
|
|
70
|
+
/**
|
|
71
|
+
* Whether the change a `changesAt` field announces has happened at `now`.
|
|
72
|
+
*
|
|
73
|
+
* THROWS when `field` is not in `schedule.changesAt`: a page that compares the clock to a date its
|
|
74
|
+
* `/schedule.json` does not announce is a page the scheduler never rebuilds, and a throw fails the
|
|
75
|
+
* build that introduced it, as `zodFrom` does for a bad declaration.
|
|
76
|
+
*/
|
|
77
|
+
export declare function changePassed(data: object, c: SchedulableLike, field: string, now: Date): boolean;
|
|
54
78
|
/**
|
|
55
79
|
* Where an entry stands at `now`.
|
|
56
80
|
*
|
|
@@ -60,6 +84,9 @@ export declare function scheduleErrors(c: SchedulableLike): string[];
|
|
|
60
84
|
* mis-entered, and « Programmé » is the state that makes someone look at it; « Retiré » would
|
|
61
85
|
* bury it behind the portal's filter. An invalid declaration answers `live`: it must never hide
|
|
62
86
|
* anything.
|
|
87
|
+
*
|
|
88
|
+
* WITH `items`, the entry-level state wins when it is not `live`; otherwise the entry is live
|
|
89
|
+
* while any item is (`itemStates`).
|
|
63
90
|
*/
|
|
64
91
|
export declare function scheduleState(data: Record<string, unknown>, c: SchedulableLike, now: Date): ScheduleState;
|
|
65
92
|
/**
|
|
@@ -71,22 +98,36 @@ export declare function scheduleState(data: Record<string, unknown>, c: Schedula
|
|
|
71
98
|
*/
|
|
72
99
|
export declare function scheduledCollections(schema: Record<string, SchedulableLike>): string[];
|
|
73
100
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
101
|
+
* One moment an entry's page changes. `at` is the instant it changes; `day` the Quebec day of the
|
|
102
|
+
* TYPED value (the calendar places by it); `timed` when `at` IS the typed instant (a `datetime`
|
|
103
|
+
* publication or change -- a removal always happens at a Quebec midnight, so it never is);
|
|
104
|
+
* `field` the field that gave it (the sub-field for an item); `item` the item's index or null.
|
|
105
|
+
*/
|
|
106
|
+
export interface ScheduleEvent {
|
|
107
|
+
kind: 'publish' | 'remove' | 'change';
|
|
108
|
+
at: number;
|
|
109
|
+
day: string;
|
|
110
|
+
timed: boolean;
|
|
111
|
+
field: string;
|
|
112
|
+
item: number | null;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Every moment this entry's page changes, in time order -- THE ONE LIST both a site's
|
|
116
|
+
* `/schedule.json` (through `entryBoundaries`) and the portal's calendar read, so they cannot
|
|
117
|
+
* disagree. The entry's publication and removal (`scheduleState`'s rules), each item's
|
|
118
|
+
* (`itemState`'s), each `changesAt` field (`changePassed`'s). A draft has none, unless
|
|
119
|
+
* `ignoreDraft` (the calendar's « prévu si publié »).
|
|
120
|
+
*/
|
|
121
|
+
export declare function entryEvents(data: object, c: SchedulableLike, opts?: {
|
|
122
|
+
ignoreDraft?: boolean;
|
|
123
|
+
}): ScheduleEvent[];
|
|
124
|
+
/**
|
|
125
|
+
* Every instant at which this entry's page changes -- `entryEvents`' instants. For a 2.1.0
|
|
126
|
+
* declaration the same list, in the same order, as 2.1.0 returned. A site's `/schedule.json` is
|
|
127
|
+
* the earliest across its entries (`nextChangeAt`); a boundary missing here is a site that
|
|
128
|
+
* silently stops aging.
|
|
88
129
|
*/
|
|
89
|
-
export declare function entryBoundaries(data:
|
|
130
|
+
export declare function entryBoundaries(data: object, c: SchedulableLike): number[];
|
|
90
131
|
/**
|
|
91
132
|
* The earliest candidate strictly after `now`, as ISO (`toISOString`), or null when nothing is
|
|
92
133
|
* pending. STRICTLY after: an instant that has arrived was already rendered by the build computing
|
package/dist/schedule.js
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
// A STATIC SITE APPLIES THIS WHEN IT BUILDS. `now` is build time; the site serves the next instant
|
|
15
15
|
// its output changes at `/schedule.json` (`scheduledCollections` + `entryBoundaries` +
|
|
16
16
|
// `nextChangeAt`), and the portal's hourly scheduler (pmeweb-portail/src/server/schedule/) rebuilds
|
|
17
|
-
// it once that instant has passed.
|
|
17
|
+
// it once that instant has passed. Since 2.2.0 both `/schedule.json` and the portal's calendar
|
|
18
|
+
// read ONE list, `entryEvents`: the publications, removals and declared changes of an entry.
|
|
18
19
|
import { isDate, isDateTime } from './fields.js';
|
|
19
20
|
/** The zone every PmeWeb client lives in. One constant, named where it is used. */
|
|
20
21
|
export const QUEBEC = 'America/Toronto';
|
|
@@ -95,12 +96,46 @@ function paths(v) {
|
|
|
95
96
|
return [];
|
|
96
97
|
return Array.isArray(v) ? v : [v];
|
|
97
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* `schedule.items`'s faults, pushed onto `out`; the array field's name when the declaration names
|
|
101
|
+
* a real array (even if a sub-field is wrong), so the overlap with `removeAt` can still be judged.
|
|
102
|
+
*/
|
|
103
|
+
function itemsErrors(c, items, out) {
|
|
104
|
+
if (items === undefined)
|
|
105
|
+
return null;
|
|
106
|
+
if (typeof items !== 'object' || items === null || Array.isArray(items)) {
|
|
107
|
+
out.push('schedule.items : ce n’est pas un objet');
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
const { field, publishAt, removeAt } = items;
|
|
111
|
+
const f = typeof field === 'string' ? c.fields[field] : undefined;
|
|
112
|
+
if (!f || f.kind !== 'array' || !f.of) {
|
|
113
|
+
out.push(`schedule.items.field : « ${String(field)} » n’est pas un champ tableau`);
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
const name = field;
|
|
117
|
+
const of = f.of;
|
|
118
|
+
if (publishAt === undefined && paths(removeAt).length === 0)
|
|
119
|
+
out.push('schedule.items : ni publishAt ni removeAt');
|
|
120
|
+
const sub = (key, p) => {
|
|
121
|
+
const kind = typeof p === 'string' ? of[p]?.kind : undefined;
|
|
122
|
+
if (kind === undefined || !DATED.has(kind))
|
|
123
|
+
out.push(`schedule.items.${key} : « ${String(p)} » n’est pas une date du tableau « ${name} »`);
|
|
124
|
+
};
|
|
125
|
+
if (publishAt !== undefined)
|
|
126
|
+
sub('publishAt', publishAt);
|
|
127
|
+
for (const p of paths(removeAt))
|
|
128
|
+
sub('removeAt', p);
|
|
129
|
+
return name;
|
|
130
|
+
}
|
|
98
131
|
/**
|
|
99
132
|
* Everything wrong with a collection's `schedule`, one French sentence each; `[]` means valid.
|
|
100
133
|
*
|
|
101
134
|
* The site's `zodFrom` THROWS on any of these, so a bad declaration fails the build that
|
|
102
135
|
* introduced it. The portal DROPS the declaration with a warning: a list that hid nothing is a
|
|
103
136
|
* better failure than a page that does not open.
|
|
137
|
+
*
|
|
138
|
+
* 2.2.0 adds `items` and `changesAt`; a 2.1.0 declaration gets exactly the sentences it got before.
|
|
104
139
|
*/
|
|
105
140
|
export function scheduleErrors(c) {
|
|
106
141
|
const s = c.schedule;
|
|
@@ -149,9 +184,30 @@ export function scheduleErrors(c) {
|
|
|
149
184
|
out.push(`schedule.draft : « ${name} » n’est pas une case à cocher`);
|
|
150
185
|
}
|
|
151
186
|
}
|
|
187
|
+
const itemsField = itemsErrors(c, s.items, out);
|
|
188
|
+
if (itemsField !== null) {
|
|
189
|
+
for (const p of paths(s.removeAt)) {
|
|
190
|
+
const m = typeof p === 'string' ? PATH_RE.exec(p) : null;
|
|
191
|
+
if (m && m[2] !== undefined && m[1] === itemsField) {
|
|
192
|
+
out.push(`schedule.removeAt : « ${p} » lit le tableau déjà déclaré dans items`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (s.changesAt !== undefined) {
|
|
197
|
+
if (!Array.isArray(s.changesAt))
|
|
198
|
+
out.push('schedule.changesAt : ce n’est pas une liste');
|
|
199
|
+
else {
|
|
200
|
+
for (const name of s.changesAt) {
|
|
201
|
+
const f = typeof name === 'string' ? c.fields[name] : undefined;
|
|
202
|
+
if (!f || !DATED.has(f.kind))
|
|
203
|
+
out.push(`schedule.changesAt : « ${String(name)} » n’est pas une date`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
152
207
|
return out;
|
|
153
208
|
}
|
|
154
|
-
/** The day `removeAt` resolves to
|
|
209
|
+
/** The day `removeAt` resolves to, and the top-level field that gave it: the first path that
|
|
210
|
+
* yields one; an array path yields its latest. */
|
|
155
211
|
function removeDay(data, c) {
|
|
156
212
|
for (const p of paths(c.schedule?.removeAt)) {
|
|
157
213
|
const m = typeof p === 'string' ? PATH_RE.exec(p) : null;
|
|
@@ -164,7 +220,7 @@ function removeDay(data, c) {
|
|
|
164
220
|
if (sub === undefined) {
|
|
165
221
|
const d = quebecDay(data[top], f.kind);
|
|
166
222
|
if (d)
|
|
167
|
-
return d;
|
|
223
|
+
return { day: d, field: top };
|
|
168
224
|
continue;
|
|
169
225
|
}
|
|
170
226
|
const kind = f.of?.[sub]?.kind ?? '';
|
|
@@ -178,7 +234,7 @@ function removeDay(data, c) {
|
|
|
178
234
|
latest = d;
|
|
179
235
|
}
|
|
180
236
|
if (latest)
|
|
181
|
-
return latest;
|
|
237
|
+
return { day: latest, field: top };
|
|
182
238
|
}
|
|
183
239
|
return null;
|
|
184
240
|
}
|
|
@@ -197,6 +253,95 @@ function isScheduled(data, c, now) {
|
|
|
197
253
|
const d = quebecDay(data[key], f.kind);
|
|
198
254
|
return d !== null && d > quebecDayOf(now.getTime());
|
|
199
255
|
}
|
|
256
|
+
/** When a value that STARTS something takes effect: a `date` at the start of its Quebec day, a
|
|
257
|
+
* `datetime` at its instant. Null when empty or unreadable. */
|
|
258
|
+
function startInstant(value, kind) {
|
|
259
|
+
if (kind === 'datetime')
|
|
260
|
+
return instant(value);
|
|
261
|
+
const d = quebecDay(value, kind);
|
|
262
|
+
const ms = d === null ? Number.NaN : quebecMidnight(d);
|
|
263
|
+
return Number.isFinite(ms) ? ms : null;
|
|
264
|
+
}
|
|
265
|
+
/** The start of the Quebec day after `day`: when something whose LAST day is `day` is gone. */
|
|
266
|
+
function dayAfterStart(day) {
|
|
267
|
+
const next = nextQuebecDay(day);
|
|
268
|
+
const ms = next === null ? Number.NaN : quebecMidnight(next);
|
|
269
|
+
return Number.isFinite(ms) ? ms : null;
|
|
270
|
+
}
|
|
271
|
+
/** The declared `items`, or null. Read only after `scheduleErrors` said the declaration is valid. */
|
|
272
|
+
function itemsDecl(c) {
|
|
273
|
+
const i = c.schedule?.items;
|
|
274
|
+
return typeof i === 'object' && i !== null && !Array.isArray(i) && typeof i.field === 'string'
|
|
275
|
+
? i : null;
|
|
276
|
+
}
|
|
277
|
+
/** The array `schedule.items` publishes item by item, or null when the collection declares none. */
|
|
278
|
+
export function itemsField(c) {
|
|
279
|
+
return itemsDecl(c)?.field ?? null;
|
|
280
|
+
}
|
|
281
|
+
/** The first of `subs` that yields a Quebec day on this item, and which one. */
|
|
282
|
+
function firstItemDay(item, subs, of) {
|
|
283
|
+
for (const p of paths(subs)) {
|
|
284
|
+
if (typeof p !== 'string')
|
|
285
|
+
continue;
|
|
286
|
+
const d = quebecDay(item[p], of[p]?.kind ?? '');
|
|
287
|
+
if (d)
|
|
288
|
+
return { day: d, field: p };
|
|
289
|
+
}
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Where ONE item of `schedule.items.field` stands at `now`: the entry's rules on its sub-fields.
|
|
294
|
+
* `scheduled` before its `publishAt`, `removed` once the Quebec day is past its `removeAt`,
|
|
295
|
+
* `scheduled` beats `removed`. No item-level draft. A non-object item, and any item of an invalid
|
|
296
|
+
* declaration, is `live`: never hide what cannot be read.
|
|
297
|
+
*/
|
|
298
|
+
export function itemState(item, c, now) {
|
|
299
|
+
const items = itemsDecl(c);
|
|
300
|
+
if (!items || scheduleErrors(c).length > 0)
|
|
301
|
+
return 'live';
|
|
302
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item))
|
|
303
|
+
return 'live';
|
|
304
|
+
const it = item;
|
|
305
|
+
const of = c.fields[items.field]?.of ?? {};
|
|
306
|
+
if (typeof items.publishAt === 'string') {
|
|
307
|
+
const start = startInstant(it[items.publishAt], of[items.publishAt]?.kind ?? '');
|
|
308
|
+
if (start !== null && now.getTime() < start)
|
|
309
|
+
return 'scheduled';
|
|
310
|
+
}
|
|
311
|
+
const r = firstItemDay(it, items.removeAt, of);
|
|
312
|
+
return r !== null && quebecDayOf(now.getTime()) > r.day ? 'removed' : 'live';
|
|
313
|
+
}
|
|
314
|
+
/** Every item's state, by index; `[]` when the array is absent; null when no `items` is declared. */
|
|
315
|
+
export function itemStates(data, c, now) {
|
|
316
|
+
const items = itemsDecl(c);
|
|
317
|
+
if (!items)
|
|
318
|
+
return null;
|
|
319
|
+
const arr = data[items.field];
|
|
320
|
+
return Array.isArray(arr) ? arr.map((it) => itemState(it, c, now)) : [];
|
|
321
|
+
}
|
|
322
|
+
/** When a `changesAt` value takes effect: a `date` at the start of the Quebec day AFTER it (the
|
|
323
|
+
* date is the last day the old content is right), a `datetime` at its instant. */
|
|
324
|
+
function changeInstant(value, kind) {
|
|
325
|
+
if (kind === 'datetime')
|
|
326
|
+
return instant(value);
|
|
327
|
+
const d = quebecDay(value, kind);
|
|
328
|
+
return d === null ? null : dayAfterStart(d);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Whether the change a `changesAt` field announces has happened at `now`.
|
|
332
|
+
*
|
|
333
|
+
* THROWS when `field` is not in `schedule.changesAt`: a page that compares the clock to a date its
|
|
334
|
+
* `/schedule.json` does not announce is a page the scheduler never rebuilds, and a throw fails the
|
|
335
|
+
* build that introduced it, as `zodFrom` does for a bad declaration.
|
|
336
|
+
*/
|
|
337
|
+
export function changePassed(data, c, field, now) {
|
|
338
|
+
const list = c.schedule?.changesAt;
|
|
339
|
+
if (!Array.isArray(list) || !list.includes(field)) {
|
|
340
|
+
throw new Error(`changePassed : « ${field} » n’est pas déclaré dans schedule.changesAt`);
|
|
341
|
+
}
|
|
342
|
+
const at = changeInstant(data[field], c.fields[field]?.kind ?? '');
|
|
343
|
+
return at !== null && now.getTime() >= at;
|
|
344
|
+
}
|
|
200
345
|
/**
|
|
201
346
|
* Where an entry stands at `now`.
|
|
202
347
|
*
|
|
@@ -206,6 +351,9 @@ function isScheduled(data, c, now) {
|
|
|
206
351
|
* mis-entered, and « Programmé » is the state that makes someone look at it; « Retiré » would
|
|
207
352
|
* bury it behind the portal's filter. An invalid declaration answers `live`: it must never hide
|
|
208
353
|
* anything.
|
|
354
|
+
*
|
|
355
|
+
* WITH `items`, the entry-level state wins when it is not `live`; otherwise the entry is live
|
|
356
|
+
* while any item is (`itemStates`).
|
|
209
357
|
*/
|
|
210
358
|
export function scheduleState(data, c, now) {
|
|
211
359
|
if (!c.schedule || scheduleErrors(c).length > 0)
|
|
@@ -216,7 +364,15 @@ export function scheduleState(data, c, now) {
|
|
|
216
364
|
if (isScheduled(data, c, now))
|
|
217
365
|
return 'scheduled';
|
|
218
366
|
const d = removeDay(data, c);
|
|
219
|
-
|
|
367
|
+
if (d !== null && quebecDayOf(now.getTime()) > d.day)
|
|
368
|
+
return 'removed';
|
|
369
|
+
// 2.2.0: live at entry level, so the items decide -- live while any item is, else scheduled
|
|
370
|
+
// while one is still to come, else removed. An empty array is live (an empty array path never
|
|
371
|
+
// removed in 2.1.0 either).
|
|
372
|
+
const states = itemStates(data, c, now);
|
|
373
|
+
if (states === null || states.length === 0 || states.includes('live'))
|
|
374
|
+
return 'live';
|
|
375
|
+
return states.includes('scheduled') ? 'scheduled' : 'removed';
|
|
220
376
|
}
|
|
221
377
|
/**
|
|
222
378
|
* The collections of a site's `content.schema.json` that declare a VALID `schedule`, in
|
|
@@ -230,53 +386,73 @@ export function scheduledCollections(schema) {
|
|
|
230
386
|
.filter(([, c]) => c.schedule !== undefined && scheduleErrors(c).length === 0)
|
|
231
387
|
.map(([name]) => name);
|
|
232
388
|
}
|
|
233
|
-
|
|
234
|
-
* its instant. The mirror of `isScheduled`: scheduled before it, not scheduled from it on. */
|
|
235
|
-
function publishInstant(data, c) {
|
|
236
|
-
const key = c.schedule?.publishAt;
|
|
389
|
+
function pushPublish(out, rec, key, fields, item) {
|
|
237
390
|
if (typeof key !== 'string')
|
|
238
391
|
return null;
|
|
239
|
-
const
|
|
240
|
-
|
|
392
|
+
const kind = fields[key]?.kind ?? '';
|
|
393
|
+
const at = startInstant(rec[key], kind);
|
|
394
|
+
const day = quebecDay(rec[key], kind);
|
|
395
|
+
if (at === null || day === null)
|
|
241
396
|
return null;
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
397
|
+
out.push({ kind: 'publish', at, day, timed: kind === 'datetime', field: key, item });
|
|
398
|
+
return at;
|
|
399
|
+
}
|
|
400
|
+
function pushRemove(out, r, publish, item) {
|
|
401
|
+
if (r === null)
|
|
402
|
+
return;
|
|
403
|
+
const at = dayAfterStart(r.day);
|
|
404
|
+
// A removal at or before the publication is no change of its own: `scheduled` beats `removed`,
|
|
405
|
+
// so the entry (or item) goes straight from scheduled to removed AT the publication.
|
|
406
|
+
if (at === null || (publish !== null && at <= publish))
|
|
407
|
+
return;
|
|
408
|
+
out.push({ kind: 'remove', at, day: r.day, timed: false, field: r.field, item });
|
|
247
409
|
}
|
|
248
410
|
/**
|
|
249
|
-
* Every
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
* `
|
|
254
|
-
* falls at or before the publication is not a change of its own (`scheduled` beats `removed`, so
|
|
255
|
-
* the entry goes straight from `scheduled` to `removed` at the publication), and is left out.
|
|
256
|
-
*
|
|
257
|
-
* A site's `/schedule.json` is the earliest of these across its entries (`nextChangeAt`), and the
|
|
258
|
-
* portal's hourly scheduler rebuilds the site once that instant has passed. A boundary missing
|
|
259
|
-
* here is a site that silently stops aging.
|
|
260
|
-
*
|
|
261
|
-
* Only what the DECLARATION says. A site that filters by time on fields its `schedule` does not
|
|
262
|
-
* name (jessyoga's per-date `publierLe`, its `prixFin`) must add those instants itself.
|
|
411
|
+
* Every moment this entry's page changes, in time order -- THE ONE LIST both a site's
|
|
412
|
+
* `/schedule.json` (through `entryBoundaries`) and the portal's calendar read, so they cannot
|
|
413
|
+
* disagree. The entry's publication and removal (`scheduleState`'s rules), each item's
|
|
414
|
+
* (`itemState`'s), each `changesAt` field (`changePassed`'s). A draft has none, unless
|
|
415
|
+
* `ignoreDraft` (the calendar's « prévu si publié »).
|
|
263
416
|
*/
|
|
264
|
-
export function
|
|
417
|
+
export function entryEvents(data, c, opts = {}) {
|
|
265
418
|
if (!c.schedule || scheduleErrors(c).length > 0)
|
|
266
419
|
return [];
|
|
420
|
+
const d = data;
|
|
267
421
|
const draftKey = c.schedule.draft;
|
|
268
|
-
if (typeof draftKey === 'string' &&
|
|
422
|
+
if (!opts.ignoreDraft && typeof draftKey === 'string' && d[draftKey] === true)
|
|
269
423
|
return [];
|
|
270
424
|
const out = [];
|
|
271
|
-
const publish =
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
425
|
+
const publish = pushPublish(out, d, c.schedule.publishAt, c.fields, null);
|
|
426
|
+
pushRemove(out, removeDay(d, c), publish, null);
|
|
427
|
+
const items = itemsDecl(c);
|
|
428
|
+
if (items) {
|
|
429
|
+
const of = c.fields[items.field]?.of ?? {};
|
|
430
|
+
const arr = Array.isArray(d[items.field]) ? d[items.field] : [];
|
|
431
|
+
arr.forEach((raw, i) => {
|
|
432
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
|
|
433
|
+
return;
|
|
434
|
+
const it = raw;
|
|
435
|
+
const p = pushPublish(out, it, items.publishAt, of, i);
|
|
436
|
+
pushRemove(out, firstItemDay(it, items.removeAt, of), p, i);
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
for (const f of Array.isArray(c.schedule.changesAt) ? c.schedule.changesAt : []) {
|
|
440
|
+
const kind = c.fields[f]?.kind ?? '';
|
|
441
|
+
const at = changeInstant(d[f], kind);
|
|
442
|
+
const day = quebecDay(d[f], kind);
|
|
443
|
+
if (at !== null && day !== null)
|
|
444
|
+
out.push({ kind: 'change', at, day, timed: kind === 'datetime', field: f, item: null });
|
|
445
|
+
}
|
|
446
|
+
return out.sort((a, b) => a.at - b.at);
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* Every instant at which this entry's page changes -- `entryEvents`' instants. For a 2.1.0
|
|
450
|
+
* declaration the same list, in the same order, as 2.1.0 returned. A site's `/schedule.json` is
|
|
451
|
+
* the earliest across its entries (`nextChangeAt`); a boundary missing here is a site that
|
|
452
|
+
* silently stops aging.
|
|
453
|
+
*/
|
|
454
|
+
export function entryBoundaries(data, c) {
|
|
455
|
+
return entryEvents(data, c).map((e) => e.at);
|
|
280
456
|
}
|
|
281
457
|
/** The last instant `toISOString` writes with a four-digit year, the only shape the portal reads. */
|
|
282
458
|
const LAST_ISO = Date.parse('9999-12-31T23:59:59.999Z');
|
package/dist/types.d.ts
CHANGED
|
@@ -92,6 +92,24 @@ export interface ScheduleDecl {
|
|
|
92
92
|
* is not a draft (booleans stay false-safe). May be declared alone.
|
|
93
93
|
*/
|
|
94
94
|
draft?: string;
|
|
95
|
+
/**
|
|
96
|
+
* An array whose ITEMS go on and come off the site one by one (2.2.0): jessyoga's class dates.
|
|
97
|
+
* `field` names a top-level `array`; `publishAt` one `date`/`datetime` sub-field; `removeAt`
|
|
98
|
+
* one sub-field or several, THE FIRST THAT YIELDS WINS, read as a Quebec day. At least one of
|
|
99
|
+
* the two. The entry is then live while any item is: see `scheduleState`. NESTED, not flat, so
|
|
100
|
+
* an older reader (which ignores an unknown key) cannot mistake `publishAt` for a top-level one.
|
|
101
|
+
*/
|
|
102
|
+
items?: {
|
|
103
|
+
field: string;
|
|
104
|
+
publishAt?: string;
|
|
105
|
+
removeAt?: string | string[];
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Top-level `date`/`datetime` fields whose passing CHANGES THE PAGE without changing whether the
|
|
109
|
+
* entry is on it (2.2.0): jessyoga's `prixFin`, after which « Prévente » is no longer written. A
|
|
110
|
+
* site reads them with `changePassed`, which throws on a field not listed here.
|
|
111
|
+
*/
|
|
112
|
+
changesAt?: string[];
|
|
95
113
|
}
|
|
96
114
|
export interface CollectionDecl {
|
|
97
115
|
label: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pmeweb/content",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "PmeWeb content contract: content.schema.json types, zodFrom, field checks, scheduling. Read by every site's build and by the client portal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|