@substrat-run/engine-booking 0.5.3 → 0.6.1

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { type Money } from '@substrat-run/contracts';
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
@@ -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,EASL,KAAK,KAAK,EAEX,MAAM,yBAAyB,CAAC;AAEjC;;;;;;;;GAQG;AACH,eAAO,MAAM,wBAAwB,YACnC,gBAAgB,EAChB,cAAc,EACd,uBAAuB,EACvB,cAAc,wBAId,iBAAiB,EACjB,kBAAkB,EAClB,mBAAmB,CACX,CAAC;AACX,MAAM,MAAM,qBAAqB,GAAG,CAAC,OAAO,wBAAwB,CAAC,CAAC,MAAM,CAAC,CAAC;AAU9E,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC7E,OAAO,EAGL,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EAEtB,MAAM,sBAAsB,CAAC;AAY9B,eAAO,MAAM,IAAI;IACf,MAAM;IACN,IAAI;IACJ,IAAI;IACJ,OAAO;IACP,MAAM;IACN,IAAI;IACJ,QAAQ;IACR,eAAe;CAChB,CAAC;AAEF,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2C1B,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;GA2C7B,CAAC;AAsCF,6EAA6E;AAC7E,qBAAa,eAAgB,SAAQ,KAAK;IAGtC,QAAQ,CAAC,UAAU,EAAE,MAAM;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM;IAJzB,QAAQ,CAAC,IAAI,sBAAsB;IACnC,YACW,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EAIxB;CACF;AAMD;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;EAAiD,CAAC;AAC/E,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAKhE,eAAO,MAAM,mBAAmB;;;;iBAI9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,eAAO,MAAM,oBAAoB;;;;;;;;;iBAS/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE,eAAO,MAAM,oBAAoB;;;;;;;;iBAU/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE,eAAO,MAAM,oBAAoB;;;;;;iBAS/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAmCxE,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,qFAAqF;IACrF,KAAK,EAAE,gBAAgB,CAAC;IACxB;;;;;;;OAOG;IACH,cAAc,EAAE,gBAAgB,CAAC;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAWD,mEAAmE;AACnE,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,gBAAgB,EACvB,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,GAAG,EAAE,MAAM,GACV,gBAAgB,CAElB;AA2ID,wBAAgB,cAAc,CAAC,GAAG,EAAE,gBAAgB,EAAE,QAAQ,EAAE,mBAAmB,GAAG,QAAQ,CAiB7F;AAED,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,gBAAgB,EACrB,KAAK,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAC7C,QAAQ,CAOV;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,gBAAgB,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,QAAQ,EAAE,CAQ9E;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,gBAAgB,EACrB,QAAQ,EAAE,oBAAoB,GAC7B,WAAW,CAuDb;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,gBAAgB,EACrB,KAAK,EAAE;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7C,WAAW,CAiCb;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,gBAAgB,EACrB,KAAK,EAAE;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7C,WAAW,CAsBb;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,gBAAgB,EACrB,QAAQ,EAAE,oBAAoB,GAC7B;IAAE,WAAW,EAAE,WAAW,CAAC;IAAC,WAAW,EAAE,WAAW,CAAA;CAAE,CAgDxD;AAED,sFAAsF;AACtF;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,gBAAgB,EACrB,KAAK,EAAE;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACxE,WAAW,CA4Bb;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,gBAAgB,EACrB,KAAK,EAAE;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,aAAa,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACpE,WAAW,CA0Bb;AAED,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,gBAAgB,EACrB,KAAK,EAAE;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAC9D,WAAW,CAmBb;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,eAAe,CAC7B,GAAG,EAAE,gBAAgB,EACrB,QAAQ,EAAE,oBAAoB,GAC7B,WAAW,CAkDb;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,gBAAgB,EACrB,KAAK,EAAE;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7C,WAAW,CAYb;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,gBAAgB,EACrB,KAAK,EAAE;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7C,WAAW,CAoBb;AAED,wBAAgB,UAAU,CACxB,GAAG,EAAE,gBAAgB,EACrB,KAAK,EAAE;IAAE,aAAa,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7C,WAAW,CAkBb;AAED,wBAAgB,cAAc,CAC5B,GAAG,EAAE,gBAAgB,EACrB,aAAa,EAAE,MAAM,EACrB,GAAG,CAAC,EAAE,MAAM,GACX;IAAE,WAAW,EAAE,WAAW,CAAC;IAAC,YAAY,EAAE,WAAW,EAAE,CAAA;CAAE,CAK3D;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,gBAAgB,EACrB,KAAK,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACvE,WAAW,EAAE,CAuBf;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAC1B,GAAG,EAAE,gBAAgB,EACrB,KAAK,EAAE;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACpE,YAAY,EAAE,CAyChB;AA4JD,eAAO,MAAM,aAAa,EAAE,kBAI3B,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AACH,eAAO,MAAM,iBAAiB;;;;;YAQxB,4CAA4C;;;;;;;;;YAS5C,sFAAsF;;;;;;;;;;YAUtF,mEAAmE;;;;;;;;;;;;;;;;;;;;;CAavE,CAAC"}
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, defineLifecycles, INVALID_TRANSITION, moduleManifest, money, permissionKey, substratError, } from '@substrat-run/contracts';
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` (bottom of this file), where the
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
- return listResources(ctx, input?.kind);
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 listReservations(ctx, input ?? {});
923
+ return listReservationsPage(ctx, input ?? {});
901
924
  };
902
925
  const availabilityOp = async (ctx, input) => {
903
926
  assertAllowed(await ctx.check(PERM.read));
904
- return availability(ctx, input);
927
+ return availabilityPage(ctx, input);
905
928
  };
906
929
  /**
907
- * The registered handlers, extracted so the lifecycle below can be checked
908
- * against them.
930
+ * The registered handlers the implementation half of `bookingOperations`.
909
931
  *
910
- * This is why booking's lifecycle lives at the bottom of `index.ts` rather than
911
- * in its own `lifecycle.ts` the way workorder's does: booking has not adopted
912
- * `defineOperations`, so the only description of its operation surface is this
913
- * mapand a separate file importing it would close a cycle. Declaring an
914
- * operation-name list beside it instead would be exactly the second description
915
- * this change exists to delete.
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