@substrat-run/engine-booking 0.5.2 → 0.6.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/dist/index.d.ts +59 -182
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +107 -154
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.d.ts +89 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +84 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/model.d.ts +4 -3
- package/dist/model.d.ts.map +1 -1
- package/dist/model.js +5 -4
- package/dist/model.js.map +1 -1
- package/dist/operations.d.ts +701 -0
- package/dist/operations.d.ts.map +1 -0
- package/dist/operations.js +193 -0
- package/dist/operations.js.map +1 -0
- package/dist/schemas.d.ts +178 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +163 -0
- package/dist/schemas.js.map +1 -0
- package/package.json +9 -5
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { type
|
|
2
|
+
import { type ListPage, type Page } from '@substrat-run/contracts';
|
|
3
3
|
/**
|
|
4
4
|
* The conflict reasons this engine raises — its own vocabulary, narrowing the platform's
|
|
5
5
|
* `conflict` code (#113). Exported so a vertical can branch on WHY a refusal happened
|
|
@@ -11,8 +11,12 @@ import { type Money } from '@substrat-run/contracts';
|
|
|
11
11
|
*/
|
|
12
12
|
export declare const BOOKING_CONFLICT_REASONS: readonly ['already_joined', 'already_left', 'capacity_below_joined', 'hold_expired', "invalid_transition", 'not_yet_expired', 'reservation_full', 'resource_inactive'];
|
|
13
13
|
export type BookingConflictReason = (typeof BOOKING_CONFLICT_REASONS)[number];
|
|
14
|
+
export { availabilityInput, cancelReservationInput, createResourceInput, freeInterval, holdReservationInput, instantIn, joinReservationInput, leaveReservationInput, listReservationsInput, listResourcesInput, moveReservationInput, openReservationInput, participant, reservation, reservationAtInput, reservationIdIn, reservationState, resource, setResourceActiveInput, toInstant, type CreateResourceInput, type FreeInterval, type HoldReservationInput, type JoinReservationInput, type MoveReservationInput, type Participant, type Reservation, type ReservationState, type Resource, type SetResourceActiveInput, } from './schemas.js';
|
|
15
|
+
export { bookingOperations, BOOKING_PERMISSIONS } from './operations.js';
|
|
16
|
+
export { bookingLifecycles } from './lifecycle.js';
|
|
17
|
+
import { type CreateResourceInput, type FreeInterval, type HoldReservationInput, type JoinReservationInput, type MoveReservationInput, type Participant, type Reservation, type ReservationState, type Resource } from './schemas.js';
|
|
14
18
|
export { bookingEntities, reservationRow, resourceRow } from './entities.js';
|
|
15
|
-
import { type ModuleRegistration, type OperationContext } from '@substrat-run/kernel';
|
|
19
|
+
import { type ModuleRegistration, type OperationContext, type PageParams } from '@substrat-run/kernel';
|
|
16
20
|
export declare const PERM: {
|
|
17
21
|
create: string & z.$brand<"PermissionKey">;
|
|
18
22
|
read: string & z.$brand<"PermissionKey">;
|
|
@@ -138,103 +142,6 @@ export declare class SlotUnavailable extends Error {
|
|
|
138
142
|
readonly code = "SLOT_UNAVAILABLE";
|
|
139
143
|
constructor(resourceId: string, startsAt: string, endsAt: string);
|
|
140
144
|
}
|
|
141
|
-
/**
|
|
142
|
-
* The reservation's states — **taken from the entity registry, not restated**.
|
|
143
|
-
*
|
|
144
|
-
* These seven values used to be written out twice: here, and as the `state`
|
|
145
|
-
* column's `z.enum` in `entities.ts`. Two descriptions of one fact, agreeing
|
|
146
|
-
* only by everyone remembering to change both (#844). Reading the column's own
|
|
147
|
-
* schema makes storage and domain unable to disagree, the same way
|
|
148
|
-
* `engine-workorder` takes its published `status` from the registry.
|
|
149
|
-
*/
|
|
150
|
-
export declare const reservationState: z.ZodEnum<{
|
|
151
|
-
cancelled: "cancelled";
|
|
152
|
-
completed: "completed";
|
|
153
|
-
confirmed: "confirmed";
|
|
154
|
-
expired: "expired";
|
|
155
|
-
held: "held";
|
|
156
|
-
in_service: "in_service";
|
|
157
|
-
no_show: "no_show";
|
|
158
|
-
}>;
|
|
159
|
-
export type ReservationState = z.infer<typeof reservationState>;
|
|
160
|
-
export declare const createResourceInput: z.ZodObject<{
|
|
161
|
-
kind: z.ZodString;
|
|
162
|
-
name: z.ZodString;
|
|
163
|
-
capacity: z.ZodOptional<z.ZodNumber>;
|
|
164
|
-
}, z.core.$strip>;
|
|
165
|
-
export type CreateResourceInput = z.infer<typeof createResourceInput>;
|
|
166
|
-
export declare const holdReservationInput: z.ZodObject<{
|
|
167
|
-
resourceId: z.ZodString;
|
|
168
|
-
startsAt: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
169
|
-
endsAt: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
170
|
-
expiresAt: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
171
|
-
quantity: z.ZodOptional<z.ZodNumber>;
|
|
172
|
-
fillTarget: z.ZodOptional<z.ZodNumber>;
|
|
173
|
-
note: z.ZodOptional<z.ZodString>;
|
|
174
|
-
now: z.ZodOptional<z.ZodString>;
|
|
175
|
-
}, z.core.$strip>;
|
|
176
|
-
export type HoldReservationInput = z.infer<typeof holdReservationInput>;
|
|
177
|
-
export declare const joinReservationInput: z.ZodObject<{
|
|
178
|
-
reservationId: z.ZodString;
|
|
179
|
-
partyRef: z.core.$ZodBranded<z.ZodString, "DataSubjectId", "out">;
|
|
180
|
-
share: z.ZodOptional<z.ZodObject<{
|
|
181
|
-
amount: z.core.$ZodBranded<z.ZodString, "MoneyAmount", "out">;
|
|
182
|
-
currency: z.core.$ZodBranded<z.ZodString, "CurrencyCode", "out">;
|
|
183
|
-
}, z.core.$strip>>;
|
|
184
|
-
now: z.ZodOptional<z.ZodString>;
|
|
185
|
-
}, z.core.$strip>;
|
|
186
|
-
export type JoinReservationInput = z.infer<typeof joinReservationInput>;
|
|
187
|
-
export declare const moveReservationInput: z.ZodObject<{
|
|
188
|
-
reservationId: z.ZodString;
|
|
189
|
-
resourceId: z.ZodOptional<z.ZodString>;
|
|
190
|
-
startsAt: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
191
|
-
endsAt: z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>;
|
|
192
|
-
now: z.ZodOptional<z.ZodString>;
|
|
193
|
-
}, z.core.$strip>;
|
|
194
|
-
export type MoveReservationInput = z.infer<typeof moveReservationInput>;
|
|
195
|
-
export interface Resource {
|
|
196
|
-
id: string;
|
|
197
|
-
kind: string;
|
|
198
|
-
name: string;
|
|
199
|
-
capacity: number;
|
|
200
|
-
active: boolean;
|
|
201
|
-
createdAt: string;
|
|
202
|
-
}
|
|
203
|
-
export interface Participant {
|
|
204
|
-
id: string;
|
|
205
|
-
partyRef: string;
|
|
206
|
-
share: Money | null;
|
|
207
|
-
joinedAt: string;
|
|
208
|
-
leftAt: string | null;
|
|
209
|
-
}
|
|
210
|
-
export interface Reservation {
|
|
211
|
-
id: string;
|
|
212
|
-
resourceId: string;
|
|
213
|
-
startsAt: string;
|
|
214
|
-
endsAt: string;
|
|
215
|
-
/** The state as stored. A `held` row keeps saying `held` until someone sweeps it. */
|
|
216
|
-
state: ReservationState;
|
|
217
|
-
/**
|
|
218
|
-
* What the row actually means *now* — `expired` once a hold's deadline has passed,
|
|
219
|
-
* whether or not anyone has swept it.
|
|
220
|
-
*
|
|
221
|
-
* Expiry is lazy, so `state` alone would render a dead hold as a live one: the
|
|
222
|
-
* console calendar would show a HELD cell counting down past 0:00 forever. Read
|
|
223
|
-
* paths render this; the transition guards use the stored `state`.
|
|
224
|
-
*/
|
|
225
|
-
effectiveState: ReservationState;
|
|
226
|
-
quantity: number;
|
|
227
|
-
expiresAt: string | null;
|
|
228
|
-
fillTarget: number | null;
|
|
229
|
-
note: string | null;
|
|
230
|
-
createdBy: string;
|
|
231
|
-
createdAt: string;
|
|
232
|
-
}
|
|
233
|
-
export interface FreeInterval {
|
|
234
|
-
startsAt: string;
|
|
235
|
-
endsAt: string;
|
|
236
|
-
available: number;
|
|
237
|
-
}
|
|
238
145
|
/** The one definition of "a hold past its deadline is expired". */
|
|
239
146
|
export declare function effectiveStateOf(state: ReservationState, expiresAt: string | null, now: string): ReservationState;
|
|
240
147
|
export declare function createResource(ctx: OperationContext, rawInput: CreateResourceInput): Resource;
|
|
@@ -242,6 +149,20 @@ export declare function setResourceActive(ctx: OperationContext, input: {
|
|
|
242
149
|
resourceId: string;
|
|
243
150
|
active: boolean;
|
|
244
151
|
}): Resource;
|
|
152
|
+
/**
|
|
153
|
+
* The same resources, as a PAGE — what `booking/list-resources` answers (#811).
|
|
154
|
+
*
|
|
155
|
+
* Kernel-composed: the `WHERE`, the `ORDER BY`, the keyset tie-break, the
|
|
156
|
+
* `LIMIT` and the indexes behind them are all composed from this operation's
|
|
157
|
+
* declared `paged.over` vocabulary. What stays here is the projection, which is
|
|
158
|
+
* why `mapPage` exists — it re-shapes the entries and leaves the walk alone.
|
|
159
|
+
*
|
|
160
|
+
* `listResources` below is NOT replaced by it. That one is an in-scope fold a
|
|
161
|
+
* vertical calls inside its own transaction, where the bound is the vertical's
|
|
162
|
+
* (a club has eight courts, not eight thousand). The unbounded read #811 was
|
|
163
|
+
* filed against is the invocable ENDPOINT, and that is this one.
|
|
164
|
+
*/
|
|
165
|
+
export declare function listResourcesPage(ctx: OperationContext, page: PageParams): Page<Resource>;
|
|
245
166
|
export declare function listResources(ctx: OperationContext, kind?: string): Resource[];
|
|
246
167
|
/**
|
|
247
168
|
* Place a tentative hold. Throws {@link SlotUnavailable} if the interval would
|
|
@@ -339,6 +260,27 @@ export declare function getReservation(ctx: OperationContext, reservationId: str
|
|
|
339
260
|
reservation: Reservation;
|
|
340
261
|
participants: Participant[];
|
|
341
262
|
};
|
|
263
|
+
/**
|
|
264
|
+
* Reservations overlapping a window, as a PAGE — what `booking/list` answers.
|
|
265
|
+
*
|
|
266
|
+
* Handler-composed rather than kernel-composed, and the cursor is `id`. The
|
|
267
|
+
* window is an OVERLAP test (`starts_at < to AND ends_at > from`), which the
|
|
268
|
+
* kernel's equality-only filter vocabulary cannot express — deliberately, since
|
|
269
|
+
* a range vocabulary is where a filter becomes a query language. So this read
|
|
270
|
+
* owns its own `WHERE`.
|
|
271
|
+
*
|
|
272
|
+
* The cursor has to be UNIQUE. This list shipped `ORDER BY starts_at, id`, and a
|
|
273
|
+
* keyset cursor on `starts_at` skips and repeats rows wherever two reservations
|
|
274
|
+
* share a start — which on a court schedule is every hour. Ids are ULIDs, so
|
|
275
|
+
* `id` is unique and still roughly chronological by creation; a caller rendering
|
|
276
|
+
* a calendar sorts the page it got by `startsAt` itself.
|
|
277
|
+
*/
|
|
278
|
+
export declare function listReservationsPage(ctx: OperationContext, input: {
|
|
279
|
+
resourceId?: string;
|
|
280
|
+
from?: string;
|
|
281
|
+
to?: string;
|
|
282
|
+
now?: string;
|
|
283
|
+
} & ListPage): Page<Reservation>;
|
|
342
284
|
export declare function listReservations(ctx: OperationContext, input: {
|
|
343
285
|
resourceId?: string;
|
|
344
286
|
from?: string;
|
|
@@ -356,6 +298,24 @@ export declare function listReservations(ctx: OperationContext, input: {
|
|
|
356
298
|
* because capacity may exceed 1 (fungible pools), where "free" is a number and not
|
|
357
299
|
* a boolean.
|
|
358
300
|
*/
|
|
301
|
+
/**
|
|
302
|
+
* The same free intervals, as a PAGE — what `booking/availability` answers.
|
|
303
|
+
*
|
|
304
|
+
* A computed fold rather than a table walk, so the whole fold runs and the page
|
|
305
|
+
* is taken off the end of it. That is not the waste it looks like: the segments
|
|
306
|
+
* are derived by merging every live reservation in the window, so there is no
|
|
307
|
+
* partial computation to push into SQL.
|
|
308
|
+
*
|
|
309
|
+
* The segments are DISJOINT and returned in order, so `startsAt` is unique among
|
|
310
|
+
* them — which is what makes it a sound cursor here where it would not be over
|
|
311
|
+
* reservation rows.
|
|
312
|
+
*/
|
|
313
|
+
export declare function availabilityPage(ctx: OperationContext, input: {
|
|
314
|
+
resourceId: string;
|
|
315
|
+
from: string;
|
|
316
|
+
to: string;
|
|
317
|
+
now?: string;
|
|
318
|
+
} & ListPage): Page<FreeInterval>;
|
|
359
319
|
export declare function availability(ctx: OperationContext, input: {
|
|
360
320
|
resourceId: string;
|
|
361
321
|
from: string;
|
|
@@ -363,87 +323,4 @@ export declare function availability(ctx: OperationContext, input: {
|
|
|
363
323
|
now?: string;
|
|
364
324
|
}): FreeInterval[];
|
|
365
325
|
export declare const bookingModule: ModuleRegistration;
|
|
366
|
-
/**
|
|
367
|
-
* The reservation's state machine, declared (#844).
|
|
368
|
-
*
|
|
369
|
-
* ## `on` versus `allow` — the distinction this engine forced
|
|
370
|
-
*
|
|
371
|
-
* Three of the nine guards gate operations that move NOTHING. `booking/move`
|
|
372
|
-
* changes a reservation's times, `booking/open` its fill target, `booking/join`
|
|
373
|
-
* adds a participant — each legal only in `held` or `confirmed`, and none of
|
|
374
|
-
* them a transition. Declaring them as edges would have put three self-loops on
|
|
375
|
-
* the diagram that no code performs.
|
|
376
|
-
*
|
|
377
|
-
* ## Why `booking/join` is `allow` even though joining can confirm
|
|
378
|
-
*
|
|
379
|
-
* A join that fills the last place calls `confirmReservation` — so a join CAN
|
|
380
|
-
* end with a confirmed reservation. It is still not an edge. The move is
|
|
381
|
-
* `booking/confirm`'s, performed by the in-scope function join composes, and it
|
|
382
|
-
* goes through this same check on the way. Declaring `join: 'confirmed'` would
|
|
383
|
-
* claim every join confirms, which is false for all but the last one.
|
|
384
|
-
*
|
|
385
|
-
* That is the composition rule holding: an engine composed BY CALL gets its
|
|
386
|
-
* invariant from the callee, so the machine describes what each verb does
|
|
387
|
-
* itself, not what its callees might do next.
|
|
388
|
-
*
|
|
389
|
-
* ## Lazy expiry is not an edge either
|
|
390
|
-
*
|
|
391
|
-
* `held → expired` IS declared — `booking/expire` performs it. What is not
|
|
392
|
-
* declared is the lapse: a hold past its deadline reads as `expired` through
|
|
393
|
-
* `effectiveStateOf` without any transition occurring, which is a projection for
|
|
394
|
-
* display and allocation. The condition on the edge (`not_yet_expired`) stays in
|
|
395
|
-
* the handler, where a lifecycle deliberately cannot reach.
|
|
396
|
-
*
|
|
397
|
-
* ## No `extensible` states
|
|
398
|
-
*
|
|
399
|
-
* Every state here carries an allocation or capacity consequence, and the four
|
|
400
|
-
* terminal ones release capacity. Refining any of them is not a vertical's to do
|
|
401
|
-
* yet, and the absence says so rather than leaving it to be inferred.
|
|
402
|
-
*/
|
|
403
|
-
export declare const bookingLifecycles: {
|
|
404
|
-
readonly reservation: {
|
|
405
|
-
readonly field: "state";
|
|
406
|
-
readonly initial: "held";
|
|
407
|
-
readonly states: {
|
|
408
|
-
/** A deadline-bearing claim on capacity. */
|
|
409
|
-
readonly held: {
|
|
410
|
-
readonly on: {
|
|
411
|
-
readonly 'booking/confirm': "confirmed";
|
|
412
|
-
readonly 'booking/expire': "expired";
|
|
413
|
-
readonly 'booking/cancel': "cancelled";
|
|
414
|
-
};
|
|
415
|
-
readonly allow: readonly ["booking/join", "booking/open", "booking/move"];
|
|
416
|
-
};
|
|
417
|
-
/** Capacity is committed. `complete` may skip `in_service` — starting is optional. */
|
|
418
|
-
readonly confirmed: {
|
|
419
|
-
readonly on: {
|
|
420
|
-
readonly 'booking/start': "in_service";
|
|
421
|
-
readonly 'booking/complete': "completed";
|
|
422
|
-
readonly 'booking/no-show': "no_show";
|
|
423
|
-
readonly 'booking/cancel': "cancelled";
|
|
424
|
-
};
|
|
425
|
-
readonly allow: readonly ["booking/join", "booking/open", "booking/move"];
|
|
426
|
-
};
|
|
427
|
-
/** Under way. No longer cancellable, and no longer re-timeable. */
|
|
428
|
-
readonly in_service: {
|
|
429
|
-
readonly on: {
|
|
430
|
-
readonly 'booking/complete': "completed";
|
|
431
|
-
readonly 'booking/no-show': "no_show";
|
|
432
|
-
};
|
|
433
|
-
};
|
|
434
|
-
readonly expired: {
|
|
435
|
-
readonly terminal: true;
|
|
436
|
-
};
|
|
437
|
-
readonly cancelled: {
|
|
438
|
-
readonly terminal: true;
|
|
439
|
-
};
|
|
440
|
-
readonly completed: {
|
|
441
|
-
readonly terminal: true;
|
|
442
|
-
};
|
|
443
|
-
readonly no_show: {
|
|
444
|
-
readonly terminal: true;
|
|
445
|
-
};
|
|
446
|
-
};
|
|
447
|
-
};
|
|
448
|
-
};
|
|
449
326
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { assertTransition, dataSubjectId,
|
|
2
|
+
import { assertTransition, dataSubjectId, INVALID_TRANSITION, LIST_PAGE_DEFAULT, LIST_PAGE_MAX, listsDeclaredBy, mapPage, moduleManifest, money, pageOf, permissionKey, substratError, operationInputsOf, } from '@substrat-run/contracts';
|
|
3
3
|
/**
|
|
4
4
|
* The conflict reasons this engine raises — its own vocabulary, narrowing the platform's
|
|
5
5
|
* `conflict` code (#113). Exported so a vertical can branch on WHY a refusal happened
|
|
@@ -27,6 +27,14 @@ const conflict = (reason, message) => substratError('conflict', message, { reaso
|
|
|
27
27
|
// entity-type constants its relation edges name, and the row schema to declare
|
|
28
28
|
// an operation's output against without retyping this engine's shape.
|
|
29
29
|
import { bookingEntities } from './entities.js';
|
|
30
|
+
// The schemas are PUBLIC and re-exported unchanged: they were declared here as
|
|
31
|
+
// interfaces until `defineOperations` needed them as schemas (see schemas.ts).
|
|
32
|
+
export { availabilityInput, cancelReservationInput, createResourceInput, freeInterval, holdReservationInput, instantIn, joinReservationInput, leaveReservationInput, listReservationsInput, listResourcesInput, moveReservationInput, openReservationInput, participant, reservation, reservationAtInput, reservationIdIn, reservationState, resource, setResourceActiveInput, toInstant, } from './schemas.js';
|
|
33
|
+
export { bookingOperations, BOOKING_PERMISSIONS } from './operations.js';
|
|
34
|
+
import { bookingOperations } from './operations.js';
|
|
35
|
+
export { bookingLifecycles } from './lifecycle.js';
|
|
36
|
+
import { bookingLifecycles } from './lifecycle.js';
|
|
37
|
+
import { createResourceInput, holdReservationInput, joinReservationInput, moveReservationInput, toInstant, } from './schemas.js';
|
|
30
38
|
export { bookingEntities, reservationRow, resourceRow } from './entities.js';
|
|
31
39
|
import { assertAllowed, ulid, } from '@substrat-run/kernel';
|
|
32
40
|
// ============================================================================
|
|
@@ -82,6 +90,10 @@ export const bookingManifest = moduleManifest.parse({
|
|
|
82
90
|
migrations: { journalDir: './migrations', compatibleFrom: '0.0.1' },
|
|
83
91
|
attachmentTargets: [{ entityType: 'reservation', readPermission: 'booking:read' }],
|
|
84
92
|
entityRelations: [{ entityType: 'reservation', parentType: 'resource' }],
|
|
93
|
+
// #811: DERIVED from the operations' own `paged.over`, never written twice —
|
|
94
|
+
// the index the kernel provisions and the vocabulary the operation offers are
|
|
95
|
+
// one fact. `table` and `idColumn` come from the entity registry.
|
|
96
|
+
lists: listsDeclaredBy(bookingOperations, bookingEntities),
|
|
85
97
|
entitlementKey: 'booking',
|
|
86
98
|
ui: {
|
|
87
99
|
routes: [
|
|
@@ -139,26 +151,6 @@ export const bookingMigrations = [
|
|
|
139
151
|
// ---------------------------------------------------------------------------
|
|
140
152
|
// Instants
|
|
141
153
|
// ---------------------------------------------------------------------------
|
|
142
|
-
/**
|
|
143
|
-
* Canonicalise to UTC before storing or comparing.
|
|
144
|
-
*
|
|
145
|
-
* This is load-bearing, not hygiene. The overlap check compares instants as
|
|
146
|
-
* **strings** in SQL, and `contracts.instant` permits any offset — so
|
|
147
|
-
* `2026-07-18T19:00:00+02:00` and `2026-07-18T17:00:00Z` are the same moment but
|
|
148
|
-
* sort differently as text. Normalising every instant to `…Z` on the way in makes
|
|
149
|
-
* lexicographic comparison equal chronological comparison, which is the only
|
|
150
|
-
* reason the SQL in `allocatedOver` is correct.
|
|
151
|
-
*/
|
|
152
|
-
function toInstant(value) {
|
|
153
|
-
const ms = Date.parse(value);
|
|
154
|
-
if (Number.isNaN(ms))
|
|
155
|
-
throw substratError('validation_failed', `invalid instant: ${value}`);
|
|
156
|
-
return new Date(ms).toISOString();
|
|
157
|
-
}
|
|
158
|
-
const instantIn = z
|
|
159
|
-
.string()
|
|
160
|
-
.refine((s) => !Number.isNaN(Date.parse(s)), { message: 'invalid instant' })
|
|
161
|
-
.transform(toInstant);
|
|
162
154
|
/**
|
|
163
155
|
* `now` is injectable so hold expiry is testable and replayable; absent, it is the
|
|
164
156
|
* operation's instant (#812) rather than a fresh wall-clock reading, so every
|
|
@@ -185,54 +177,8 @@ export class SlotUnavailable extends Error {
|
|
|
185
177
|
// ---------------------------------------------------------------------------
|
|
186
178
|
// Schemas & shapes
|
|
187
179
|
// ---------------------------------------------------------------------------
|
|
188
|
-
/**
|
|
189
|
-
* The reservation's states — **taken from the entity registry, not restated**.
|
|
190
|
-
*
|
|
191
|
-
* These seven values used to be written out twice: here, and as the `state`
|
|
192
|
-
* column's `z.enum` in `entities.ts`. Two descriptions of one fact, agreeing
|
|
193
|
-
* only by everyone remembering to change both (#844). Reading the column's own
|
|
194
|
-
* schema makes storage and domain unable to disagree, the same way
|
|
195
|
-
* `engine-workorder` takes its published `status` from the registry.
|
|
196
|
-
*/
|
|
197
|
-
export const reservationState = bookingEntities.reservation.fields.shape.state;
|
|
198
180
|
/** States that consume capacity. `held` additionally requires an unexpired `expires_at`. */
|
|
199
181
|
const LIVE_STATES = ['held', 'confirmed', 'in_service'];
|
|
200
|
-
export const createResourceInput = z.object({
|
|
201
|
-
kind: z.string().min(1),
|
|
202
|
-
name: z.string().min(1),
|
|
203
|
-
capacity: z.number().int().min(1).optional(),
|
|
204
|
-
});
|
|
205
|
-
export const holdReservationInput = z.object({
|
|
206
|
-
resourceId: z.string().min(1),
|
|
207
|
-
startsAt: instantIn,
|
|
208
|
-
endsAt: instantIn,
|
|
209
|
-
expiresAt: instantIn,
|
|
210
|
-
quantity: z.number().int().min(1).optional(),
|
|
211
|
-
fillTarget: z.number().int().min(1).optional(),
|
|
212
|
-
note: z.string().optional(),
|
|
213
|
-
now: z.string().optional(),
|
|
214
|
-
});
|
|
215
|
-
export const joinReservationInput = z.object({
|
|
216
|
-
reservationId: z.string().min(1),
|
|
217
|
-
/**
|
|
218
|
-
* The participant, as an opaque **data-subject** id — never a `PrincipalId`.
|
|
219
|
-
* A participant is a person, so this must be shreddable: it keys the erasure
|
|
220
|
-
* of the `participant-joined` / `participant-left` events below.
|
|
221
|
-
*/
|
|
222
|
-
partyRef: dataSubjectId,
|
|
223
|
-
share: money.optional(),
|
|
224
|
-
now: z.string().optional(),
|
|
225
|
-
});
|
|
226
|
-
export const moveReservationInput = z.object({
|
|
227
|
-
reservationId: z.string().min(1),
|
|
228
|
-
/** Target resource. Omitted = stay on the current one. */
|
|
229
|
-
resourceId: z.string().min(1).optional(),
|
|
230
|
-
/** New start. Given alone, the booking is *shifted* — its duration is preserved. */
|
|
231
|
-
startsAt: instantIn.optional(),
|
|
232
|
-
/** New end. Given alone, the booking is re-sized from its existing start. */
|
|
233
|
-
endsAt: instantIn.optional(),
|
|
234
|
-
now: z.string().optional(),
|
|
235
|
-
});
|
|
236
182
|
const toResource = (r) => ({
|
|
237
183
|
id: r.id,
|
|
238
184
|
kind: r.kind,
|
|
@@ -303,7 +249,7 @@ function getRow(ctx, id) {
|
|
|
303
249
|
* `entities.ts`, once as `reservationState` above (now derived from it) — and
|
|
304
250
|
* the EDGES between them a third time, as the states each of these nine call
|
|
305
251
|
* sites happened to pass. The
|
|
306
|
-
* machine now lives in `bookingLifecycles` (
|
|
252
|
+
* machine now lives in `bookingLifecycles` (`lifecycle.ts`), where the
|
|
307
253
|
* compiler holds it to that enum.
|
|
308
254
|
*
|
|
309
255
|
* **Gated on the STORED state, not the effective one, and that is deliberate.**
|
|
@@ -379,6 +325,22 @@ export function setResourceActive(ctx, input) {
|
|
|
379
325
|
]);
|
|
380
326
|
return toResource(getResourceRow(ctx, row.id));
|
|
381
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* The same resources, as a PAGE — what `booking/list-resources` answers (#811).
|
|
330
|
+
*
|
|
331
|
+
* Kernel-composed: the `WHERE`, the `ORDER BY`, the keyset tie-break, the
|
|
332
|
+
* `LIMIT` and the indexes behind them are all composed from this operation's
|
|
333
|
+
* declared `paged.over` vocabulary. What stays here is the projection, which is
|
|
334
|
+
* why `mapPage` exists — it re-shapes the entries and leaves the walk alone.
|
|
335
|
+
*
|
|
336
|
+
* `listResources` below is NOT replaced by it. That one is an in-scope fold a
|
|
337
|
+
* vertical calls inside its own transaction, where the bound is the vertical's
|
|
338
|
+
* (a club has eight courts, not eight thousand). The unbounded read #811 was
|
|
339
|
+
* filed against is the invocable ENDPOINT, and that is this one.
|
|
340
|
+
*/
|
|
341
|
+
export function listResourcesPage(ctx, page) {
|
|
342
|
+
return mapPage(ctx.page('resource', page), toResource);
|
|
343
|
+
}
|
|
382
344
|
export function listResources(ctx, kind) {
|
|
383
345
|
const rows = kind
|
|
384
346
|
? ctx.sql.query('SELECT * FROM booking_resources WHERE kind = ? ORDER BY name', [kind])
|
|
@@ -757,6 +719,46 @@ export function getReservation(ctx, reservationId, now) {
|
|
|
757
719
|
participants: allParticipants(ctx, reservationId),
|
|
758
720
|
};
|
|
759
721
|
}
|
|
722
|
+
/**
|
|
723
|
+
* Reservations overlapping a window, as a PAGE — what `booking/list` answers.
|
|
724
|
+
*
|
|
725
|
+
* Handler-composed rather than kernel-composed, and the cursor is `id`. The
|
|
726
|
+
* window is an OVERLAP test (`starts_at < to AND ends_at > from`), which the
|
|
727
|
+
* kernel's equality-only filter vocabulary cannot express — deliberately, since
|
|
728
|
+
* a range vocabulary is where a filter becomes a query language. So this read
|
|
729
|
+
* owns its own `WHERE`.
|
|
730
|
+
*
|
|
731
|
+
* The cursor has to be UNIQUE. This list shipped `ORDER BY starts_at, id`, and a
|
|
732
|
+
* keyset cursor on `starts_at` skips and repeats rows wherever two reservations
|
|
733
|
+
* share a start — which on a court schedule is every hour. Ids are ULIDs, so
|
|
734
|
+
* `id` is unique and still roughly chronological by creation; a caller rendering
|
|
735
|
+
* a calendar sorts the page it got by `startsAt` itself.
|
|
736
|
+
*/
|
|
737
|
+
export function listReservationsPage(ctx, input) {
|
|
738
|
+
const clauses = [];
|
|
739
|
+
const params = [];
|
|
740
|
+
if (input.resourceId) {
|
|
741
|
+
clauses.push('resource_id = ?');
|
|
742
|
+
params.push(input.resourceId);
|
|
743
|
+
}
|
|
744
|
+
if (input.to) {
|
|
745
|
+
clauses.push('starts_at < ?');
|
|
746
|
+
params.push(toInstant(input.to));
|
|
747
|
+
}
|
|
748
|
+
if (input.from) {
|
|
749
|
+
clauses.push('ends_at > ?');
|
|
750
|
+
params.push(toInstant(input.from));
|
|
751
|
+
}
|
|
752
|
+
if (input.cursor) {
|
|
753
|
+
clauses.push('id > ?');
|
|
754
|
+
params.push(input.cursor);
|
|
755
|
+
}
|
|
756
|
+
const where = clauses.length ? ` WHERE ${clauses.join(' AND ')}` : '';
|
|
757
|
+
const limit = Math.min(input.limit ?? LIST_PAGE_DEFAULT, LIST_PAGE_MAX);
|
|
758
|
+
const now = nowOr(ctx, input.now);
|
|
759
|
+
const rows = ctx.sql.query(`SELECT * FROM booking_reservations${where} ORDER BY id LIMIT ?`, [...params, limit]);
|
|
760
|
+
return pageOf(rows.map((r) => toReservation(r, now)), limit, (e) => e.id);
|
|
761
|
+
}
|
|
760
762
|
export function listReservations(ctx, input) {
|
|
761
763
|
const clauses = [];
|
|
762
764
|
const params = [];
|
|
@@ -789,6 +791,26 @@ export function listReservations(ctx, input) {
|
|
|
789
791
|
* because capacity may exceed 1 (fungible pools), where "free" is a number and not
|
|
790
792
|
* a boolean.
|
|
791
793
|
*/
|
|
794
|
+
/**
|
|
795
|
+
* The same free intervals, as a PAGE — what `booking/availability` answers.
|
|
796
|
+
*
|
|
797
|
+
* A computed fold rather than a table walk, so the whole fold runs and the page
|
|
798
|
+
* is taken off the end of it. That is not the waste it looks like: the segments
|
|
799
|
+
* are derived by merging every live reservation in the window, so there is no
|
|
800
|
+
* partial computation to push into SQL.
|
|
801
|
+
*
|
|
802
|
+
* The segments are DISJOINT and returned in order, so `startsAt` is unique among
|
|
803
|
+
* them — which is what makes it a sound cursor here where it would not be over
|
|
804
|
+
* reservation rows.
|
|
805
|
+
*/
|
|
806
|
+
export function availabilityPage(ctx, input) {
|
|
807
|
+
const all = availability(ctx, input);
|
|
808
|
+
const limit = Math.min(input.limit ?? LIST_PAGE_DEFAULT, LIST_PAGE_MAX);
|
|
809
|
+
const cursor = input.cursor;
|
|
810
|
+
const at = cursor === undefined ? 0 : all.findIndex((s) => s.startsAt > cursor);
|
|
811
|
+
const start = at < 0 ? all.length : at;
|
|
812
|
+
return pageOf(all.slice(start, start + limit), limit, (e) => e.startsAt);
|
|
813
|
+
}
|
|
792
814
|
export function availability(ctx, input) {
|
|
793
815
|
const from = toInstant(input.from);
|
|
794
816
|
const to = toInstant(input.to);
|
|
@@ -844,7 +866,8 @@ const setResourceActiveOp = async (ctx, input) => {
|
|
|
844
866
|
};
|
|
845
867
|
const listResourcesOp = async (ctx, input) => {
|
|
846
868
|
assertAllowed(await ctx.check(PERM.read));
|
|
847
|
-
|
|
869
|
+
// `input` is genuinely absent when invoked with no body at all (`inputOptional`).
|
|
870
|
+
return listResourcesPage(ctx, { ...input, filters: { kind: input?.kind } });
|
|
848
871
|
};
|
|
849
872
|
const holdOp = async (ctx, input) => {
|
|
850
873
|
assertAllowed(await ctx.check(PERM.hold));
|
|
@@ -897,22 +920,21 @@ const getOp = async (ctx, input) => {
|
|
|
897
920
|
};
|
|
898
921
|
const listOp = async (ctx, input) => {
|
|
899
922
|
assertAllowed(await ctx.check(PERM.read));
|
|
900
|
-
return
|
|
923
|
+
return listReservationsPage(ctx, input ?? {});
|
|
901
924
|
};
|
|
902
925
|
const availabilityOp = async (ctx, input) => {
|
|
903
926
|
assertAllowed(await ctx.check(PERM.read));
|
|
904
|
-
return
|
|
927
|
+
return availabilityPage(ctx, input);
|
|
905
928
|
};
|
|
906
929
|
/**
|
|
907
|
-
* The registered handlers
|
|
908
|
-
* against them.
|
|
930
|
+
* The registered handlers — the implementation half of `bookingOperations`.
|
|
909
931
|
*
|
|
910
|
-
* This
|
|
911
|
-
*
|
|
912
|
-
* `
|
|
913
|
-
*
|
|
914
|
-
*
|
|
915
|
-
*
|
|
932
|
+
* This map used to be the only description of booking's operation surface, and
|
|
933
|
+
* the lifecycle had to live at the bottom of this file because of it: a
|
|
934
|
+
* `lifecycle.ts` importing the map would have closed a cycle. `operations.ts`
|
|
935
|
+
* ended that — a DECLARATION reaches only `entities.ts` and `schemas.ts`, so the
|
|
936
|
+
* lifecycle now checks itself against the declared registry from its own file,
|
|
937
|
+
* the way workorder's always has.
|
|
916
938
|
*/
|
|
917
939
|
const OPERATIONS = {
|
|
918
940
|
'booking/create-resource': createResourceOp,
|
|
@@ -937,80 +959,11 @@ export const bookingModule = {
|
|
|
937
959
|
manifest: bookingManifest,
|
|
938
960
|
migrations: bookingMigrations,
|
|
939
961
|
operations: OPERATIONS,
|
|
962
|
+
/**
|
|
963
|
+
* #893: the host parses each operation's declared `input` before the guards
|
|
964
|
+
* and the handler see it. Derived from the same declaration that produces the
|
|
965
|
+
* manifest and the routes — the schema is written once, in `operations.ts`.
|
|
966
|
+
*/
|
|
967
|
+
operationInputs: operationInputsOf(bookingOperations),
|
|
940
968
|
};
|
|
941
|
-
/**
|
|
942
|
-
* The reservation's state machine, declared (#844).
|
|
943
|
-
*
|
|
944
|
-
* ## `on` versus `allow` — the distinction this engine forced
|
|
945
|
-
*
|
|
946
|
-
* Three of the nine guards gate operations that move NOTHING. `booking/move`
|
|
947
|
-
* changes a reservation's times, `booking/open` its fill target, `booking/join`
|
|
948
|
-
* adds a participant — each legal only in `held` or `confirmed`, and none of
|
|
949
|
-
* them a transition. Declaring them as edges would have put three self-loops on
|
|
950
|
-
* the diagram that no code performs.
|
|
951
|
-
*
|
|
952
|
-
* ## Why `booking/join` is `allow` even though joining can confirm
|
|
953
|
-
*
|
|
954
|
-
* A join that fills the last place calls `confirmReservation` — so a join CAN
|
|
955
|
-
* end with a confirmed reservation. It is still not an edge. The move is
|
|
956
|
-
* `booking/confirm`'s, performed by the in-scope function join composes, and it
|
|
957
|
-
* goes through this same check on the way. Declaring `join: 'confirmed'` would
|
|
958
|
-
* claim every join confirms, which is false for all but the last one.
|
|
959
|
-
*
|
|
960
|
-
* That is the composition rule holding: an engine composed BY CALL gets its
|
|
961
|
-
* invariant from the callee, so the machine describes what each verb does
|
|
962
|
-
* itself, not what its callees might do next.
|
|
963
|
-
*
|
|
964
|
-
* ## Lazy expiry is not an edge either
|
|
965
|
-
*
|
|
966
|
-
* `held → expired` IS declared — `booking/expire` performs it. What is not
|
|
967
|
-
* declared is the lapse: a hold past its deadline reads as `expired` through
|
|
968
|
-
* `effectiveStateOf` without any transition occurring, which is a projection for
|
|
969
|
-
* display and allocation. The condition on the edge (`not_yet_expired`) stays in
|
|
970
|
-
* the handler, where a lifecycle deliberately cannot reach.
|
|
971
|
-
*
|
|
972
|
-
* ## No `extensible` states
|
|
973
|
-
*
|
|
974
|
-
* Every state here carries an allocation or capacity consequence, and the four
|
|
975
|
-
* terminal ones release capacity. Refining any of them is not a vertical's to do
|
|
976
|
-
* yet, and the absence says so rather than leaving it to be inferred.
|
|
977
|
-
*/
|
|
978
|
-
export const bookingLifecycles = defineLifecycles(bookingEntities, OPERATIONS)({
|
|
979
|
-
reservation: {
|
|
980
|
-
field: 'state',
|
|
981
|
-
initial: 'held',
|
|
982
|
-
states: {
|
|
983
|
-
/** A deadline-bearing claim on capacity. */
|
|
984
|
-
held: {
|
|
985
|
-
on: {
|
|
986
|
-
'booking/confirm': 'confirmed',
|
|
987
|
-
'booking/expire': 'expired',
|
|
988
|
-
'booking/cancel': 'cancelled',
|
|
989
|
-
},
|
|
990
|
-
allow: ['booking/join', 'booking/open', 'booking/move'],
|
|
991
|
-
},
|
|
992
|
-
/** Capacity is committed. `complete` may skip `in_service` — starting is optional. */
|
|
993
|
-
confirmed: {
|
|
994
|
-
on: {
|
|
995
|
-
'booking/start': 'in_service',
|
|
996
|
-
'booking/complete': 'completed',
|
|
997
|
-
'booking/no-show': 'no_show',
|
|
998
|
-
'booking/cancel': 'cancelled',
|
|
999
|
-
},
|
|
1000
|
-
allow: ['booking/join', 'booking/open', 'booking/move'],
|
|
1001
|
-
},
|
|
1002
|
-
/** Under way. No longer cancellable, and no longer re-timeable. */
|
|
1003
|
-
in_service: {
|
|
1004
|
-
on: {
|
|
1005
|
-
'booking/complete': 'completed',
|
|
1006
|
-
'booking/no-show': 'no_show',
|
|
1007
|
-
},
|
|
1008
|
-
},
|
|
1009
|
-
expired: { terminal: true },
|
|
1010
|
-
cancelled: { terminal: true },
|
|
1011
|
-
completed: { terminal: true },
|
|
1012
|
-
no_show: { terminal: true },
|
|
1013
|
-
},
|
|
1014
|
-
},
|
|
1015
|
-
});
|
|
1016
969
|
//# sourceMappingURL=index.js.map
|