@acms-subir/client 0.1.16 → 0.1.18

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 CHANGED
@@ -44,11 +44,12 @@ language. Update the site before shipping an app build with a newer client.
44
44
  | `pages(opts)` | Page **summaries**, with pagination; use `page()` for sections |
45
45
  | `search(text, opts)` | Top matches in `posts`, `pages` and `events`, plus `more` per group; `types`, `collection`, `limit` (1–20, default 5). Blank text resolves empty without a request |
46
46
  | `events(opts)` | Occurrences with `from`, `to`, `calendar`, `tag`, `excludeTag`, `q`, pagination |
47
- | `event(slug, opts)` | First matching event from occurrences in the requested window |
47
+ | `event(slug, opts)` | One event by slug, whatever its date (`GET /events/{slug}`; on sites older than core 0.3.40, the first match in the requested window) |
48
+ | `eventDetail(slug, { on? })` | The event and its next occurrence, or the one on `on` (`YYYY-MM-DD`) |
48
49
  | `calendars()` | Public calendars |
49
50
  | `resources(opts)`, `resource(slug, opts)` | Public resources; slug lookup scans list pages |
50
51
  | `availability(opts)` | Required `resource`, `link`; optional `from`, `days`, `minutes` |
51
- | `rsvpStatus(slug, { start })` | One event date's RSVP status: `open` or `reason` (`disabled`, `ended`, `cancelled`, `full`), live `going`/`left`, `maxSeats` for one RSVP right now, `autoConfirm`, and `form` (questions and `version`, or `null`). `start` is the occurrence's start exactly as `events()` returns it (a UTC ISO string such as `2026-10-05T14:00:00.000Z`). Never cached |
52
+ | `rsvpStatus(slug, { start })` | One event date's RSVP status: `open` or `reason` (`disabled`, `ended`, `closed` past the event's deadline, `cancelled`, `full`), `closesAt` (when RSVPs to the date close, or `null`: open until it ends), live `going`/`left`, `maxSeats` for one RSVP right now, `autoConfirm`, and `form` (questions and `version`, or `null`). `start` is the occurrence's start exactly as `events()` returns it (a UTC ISO string such as `2026-10-05T14:00:00.000Z`). Never cached |
52
53
  | `book(opts)` | Resource booking or RSVP input (`RsvpInput` for an RSVP), optional `idempotencyKey`; sends the client's `locale` (or `opts.locale`) for its emails |
53
54
  | `openapi()` | Raw OpenAPI document |
54
55
  | `registerDevice({ token, platform, locale?, topics?, channels? })` | Register or refresh an Expo push token (call on every app start); `{ created, device }`. `channels`: Android channels the app created (e.g. `['urgent']`) |
@@ -121,6 +122,7 @@ if (status.open) {
121
122
  // required when the event asks questions: a changed form answers 409, so read the status again
122
123
  ...(status.form ? { formVersion: status.form.version } : {}),
123
124
  };
125
+ // an email that already has an RSVP for this date: AcmsError 409 with code 'duplicate' (its link is emailed again)
124
126
  const result = await client.book({ ...rsvp, idempotencyKey: crypto.randomUUID() });
125
127
  // pending when the organiser approves RSVPs; status.form?.confirmation is the thank-you text.
126
128
  // result.manageToken changes or cancels it later (manageBooking, cancelBooking), like the email's link:
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { Site, Theme, Page, Content, Post, ContentList, SearchResults, Links, Form, FormReceipt, FormResponseInput, ManagedBooking, RescheduleInput, ManageAvailabilityQuery, GuestCodeSent, GuestToken, GuestBookings, GuestBookingsQuery, GuestSignedOut, Events, Event, Calendars, Resources, Resource, Availability, BookingInput, BookingResult, RsvpStatus, Device, DeviceInput, DeviceUpdate, DeviceRegistration, DeviceRemoved, PushTopics, LiveStatus, Announcements, AnnouncementDetail } from './types.js';
1
+ import type { Site, Theme, Page, Content, Post, ContentList, SearchResults, Links, Form, FormReceipt, FormResponseInput, ManagedBooking, RescheduleInput, ManageAvailabilityQuery, GuestCodeSent, GuestToken, GuestBookings, GuestBookingsQuery, GuestSignedOut, Events, Event, Calendars, Resources, Resource, Availability, BookingInput, BookingResult, RsvpStatus, Device, DeviceInput, DeviceUpdate, DeviceRegistration, DeviceRemoved, PushTopics, LiveStatus, Announcements, AnnouncementDetail, EventDetail } from './types.js';
2
2
  export { AcmsError, isAcmsError } from './errors.js';
3
3
  export type * from './types.js';
4
4
  export * from './editor.js';
@@ -125,6 +125,10 @@ export declare const endpoints: {
125
125
  readonly method: "GET";
126
126
  readonly path: "/events";
127
127
  };
128
+ readonly eventDetail: {
129
+ readonly method: "GET";
130
+ readonly path: "/events/{slug}";
131
+ };
128
132
  readonly calendars: {
129
133
  readonly method: "GET";
130
134
  readonly path: "/calendars";
@@ -244,7 +248,12 @@ export interface AcmsClient {
244
248
  /** Ranked matches across posts, pages and events, the top few per group. Requires `site.features.search`. */
245
249
  search(q: string, o?: SearchOptions): Promise<SearchResults>;
246
250
  events(o?: EventOptions): Promise<Events>;
251
+ /** One event by slug, whatever its date (older sites: searched in the window). */
247
252
  event(slug: string, o?: EventOptions): Promise<Event>;
253
+ /** One event by slug or id with its next occurrence, or the one on `on` (YYYY-MM-DD, like the website's ?on=). */
254
+ eventDetail(slug: string, o?: RequestOptions & {
255
+ on?: string;
256
+ }): Promise<EventDetail>;
248
257
  calendars(o?: RequestOptions): Promise<Calendars>;
249
258
  resources(o?: ListOptions): Promise<Resources>;
250
259
  resource(slug: string, o?: RequestOptions): Promise<Resource>;
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ export const endpoints = {
7
7
  home: { method: 'GET', path: '/pages/home' }, page: { method: 'GET', path: '/pages/{slug}' },
8
8
  content: { method: 'GET', path: '/content' }, contentById: { method: 'GET', path: '/content/{id}' },
9
9
  search: { method: 'GET', path: '/search' },
10
- events: { method: 'GET', path: '/events' }, calendars: { method: 'GET', path: '/calendars' },
10
+ events: { method: 'GET', path: '/events' }, eventDetail: { method: 'GET', path: '/events/{slug}' }, calendars: { method: 'GET', path: '/calendars' },
11
11
  // an event date's RSVP status: live seats and questions (never cached); RSVP with book()
12
12
  rsvpStatus: { method: 'GET', path: '/events/{slug}/rsvp' },
13
13
  resources: { method: 'GET', path: '/resources' }, availability: { method: 'GET', path: '/availability' },
@@ -30,7 +30,7 @@ export const endpoints = {
30
30
  guestBookings: { method: 'GET', path: '/guest/bookings' }, guestSignOut: { method: 'POST', path: '/guest/revoke' },
31
31
  };
32
32
  // site: its legal links; pushTopics: topic names
33
- const localized = new Set(['home', 'page', 'content', 'contentById', 'search', 'events', 'rsvpStatus', 'site', 'pushTopics', 'announcements', 'announcement']);
33
+ const localized = new Set(['home', 'page', 'content', 'contentById', 'search', 'events', 'rsvpStatus', 'site', 'pushTopics', 'announcements', 'announcement', 'eventDetail']);
34
34
  // localized since core 0.3.32; older sites answer ?locale= with a 400, so ask them again without it
35
35
  const lateLocalized = new Set(['site', 'pushTopics']);
36
36
  function uuid() {
@@ -264,8 +264,17 @@ export function createClient(options) {
264
264
  return request('contentById', o, { kind: 'post' }, item.id);
265
265
  throw new AcmsError(404, 'not_found', 'Post not found.');
266
266
  },
267
- /** Searches occurrences in the supplied window (server default: next 30 UTC days). */
267
+ eventDetail: (slug, o = {}) => request('eventDetail', o, { on: o.on }, slug),
268
+ /** One event by slug, whatever its date. Sites older than core 0.3.40 have no such route: then it searches occurrences in the window (default: next 30 days). */
268
269
  async event(slug, o = {}) {
270
+ try {
271
+ return (await request('eventDetail', o, {}, slug)).event;
272
+ }
273
+ catch (e) {
274
+ // a real "not found" comes as JSON with code not_found; an older site answers its HTML 404 page
275
+ if (!(e instanceof AcmsError) || e.status !== 404 || e.code === 'not_found')
276
+ throw e;
277
+ }
269
278
  for await (const item of paginate.events(o))
270
279
  if (item.event.slug === slug)
271
280
  return item.event;
package/dist/types.d.ts CHANGED
@@ -221,6 +221,8 @@ export type Event = {
221
221
  "capacity": number;
222
222
  "autoConfirm": boolean;
223
223
  "maxSeats": number;
224
+ "closesAt": string | null;
225
+ "closeBefore": number;
224
226
  "form"?: {
225
227
  "title": string;
226
228
  "intro": string;
@@ -311,6 +313,8 @@ export type Events = {
311
313
  "capacity": number;
312
314
  "autoConfirm": boolean;
313
315
  "maxSeats": number;
316
+ "closesAt": string | null;
317
+ "closeBefore": number;
314
318
  "form"?: {
315
319
  "title": string;
316
320
  "intro": string;
@@ -347,6 +351,92 @@ export type Events = {
347
351
  };
348
352
  }>;
349
353
  };
354
+ export type EventDetail = {
355
+ "locale": string | null;
356
+ "occurrence": ({
357
+ "id": string;
358
+ "start": string;
359
+ "end": string;
360
+ "allDay": boolean;
361
+ "cancelled": boolean;
362
+ }) | (null);
363
+ "event": {
364
+ "id": string;
365
+ "slug": string;
366
+ "title": string;
367
+ "excerpt": string;
368
+ "status": "draft" | "scheduled" | "published" | "archived";
369
+ "cancelled": boolean;
370
+ "featured": boolean;
371
+ "start": string;
372
+ "end": string;
373
+ "allDay": boolean;
374
+ "timezone": string;
375
+ "rrule": string;
376
+ "exdates": Array<string>;
377
+ "cancelledDates": Array<string>;
378
+ "location": {
379
+ "name": string;
380
+ "address": string;
381
+ "url": string;
382
+ "lat": number | null;
383
+ "lng": number | null;
384
+ "placeId": string;
385
+ "showMap": boolean;
386
+ "mapImage": string;
387
+ "mapImageDark": string;
388
+ };
389
+ "cta": {
390
+ "label": string;
391
+ "href": string;
392
+ "style": "primary" | "outline";
393
+ };
394
+ "tags": Array<string>;
395
+ "fields": {
396
+ [key: string]: string;
397
+ };
398
+ "rsvp": {
399
+ "enabled": boolean;
400
+ "capacity": number;
401
+ "autoConfirm": boolean;
402
+ "maxSeats": number;
403
+ "closesAt": string | null;
404
+ "closeBefore": number;
405
+ "form"?: {
406
+ "title": string;
407
+ "intro": string;
408
+ "submitLabel": string;
409
+ "confirmation": string;
410
+ "fields": Array<{
411
+ "id": string;
412
+ "type": "text" | "textarea" | "email" | "phone" | "number" | "select" | "checkbox" | "date" | "radio" | "checkboxes" | "url" | "consent" | "heading";
413
+ "label": string;
414
+ "required": boolean;
415
+ "help": string;
416
+ "options": Array<string>;
417
+ "placeholder"?: string;
418
+ "width"?: "full" | "half";
419
+ "allowOther"?: boolean;
420
+ }>;
421
+ };
422
+ };
423
+ "url": string;
424
+ "icsUrl": string;
425
+ "repeats": string;
426
+ "image": string;
427
+ "imageAlt": string;
428
+ "updatedAt": string;
429
+ "calendars": Array<{
430
+ "id": string;
431
+ "slug": string;
432
+ "name": string;
433
+ "color": string;
434
+ }>;
435
+ "notify": {
436
+ "changes": Array<string>;
437
+ };
438
+ };
439
+ };
350
440
  export type SearchResults = {
351
441
  "locale": string | null;
352
442
  "query": string;
@@ -450,6 +540,8 @@ export type SearchResults = {
450
540
  "capacity": number;
451
541
  "autoConfirm": boolean;
452
542
  "maxSeats": number;
543
+ "closesAt": string | null;
544
+ "closeBefore": number;
453
545
  "form"?: {
454
546
  "title": string;
455
547
  "intro": string;
@@ -610,12 +702,13 @@ export type RsvpStatus = {
610
702
  "start": string;
611
703
  "end": string;
612
704
  "open": boolean;
613
- "reason": ("disabled" | "ended" | "cancelled" | "full") | (null);
705
+ "reason": ("disabled" | "ended" | "closed" | "cancelled" | "full") | (null);
614
706
  "going": number;
615
707
  "left": (number) | (null);
616
708
  "capacity": number;
617
709
  "maxSeats": number;
618
710
  "autoConfirm": boolean;
711
+ "closesAt": (string) | (null);
619
712
  "form": ({
620
713
  "version": string;
621
714
  "title": string;
@@ -967,7 +1060,7 @@ export type LiveStatus = {
967
1060
  export type ApiErrorResponse = {
968
1061
  "version": 1;
969
1062
  "error": {
970
- "code": "invalid_request" | "unauthorized" | "forbidden" | "not_found" | "method_not_allowed" | "conflict" | "gone" | "payload_too_large" | "unsupported_media_type" | "rate_limited" | "upstream_error" | "internal_error";
1063
+ "code": "invalid_request" | "unauthorized" | "forbidden" | "not_found" | "method_not_allowed" | "conflict" | "duplicate" | "gone" | "payload_too_large" | "unsupported_media_type" | "rate_limited" | "upstream_error" | "internal_error";
971
1064
  "message": string;
972
1065
  "issues"?: Array<{
973
1066
  "path": Array<string | number>;
@@ -1153,6 +1246,9 @@ export type EditorDocument = {
1153
1246
  "capacity": number;
1154
1247
  "autoConfirm": boolean;
1155
1248
  "maxSeats": number;
1249
+ "closesAt": string | null;
1250
+ "closeBefore": number;
1251
+ "notifyStaff": boolean;
1156
1252
  "form"?: {
1157
1253
  "title": string;
1158
1254
  "intro": string;
@@ -2083,6 +2179,9 @@ export type EditorSaveInput = {
2083
2179
  "capacity": number;
2084
2180
  "autoConfirm": boolean;
2085
2181
  "maxSeats": number;
2182
+ "closesAt"?: (string) | (null);
2183
+ "closeBefore"?: number;
2184
+ "notifyStaff"?: boolean;
2086
2185
  "form"?: {
2087
2186
  "title"?: string;
2088
2187
  "intro"?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@acms-subir/client",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Typed, dependency-free client for the ACMS public mobile API.",
5
5
  "type": "module",
6
6
  "license": "MIT",