@acms-subir/client 0.1.2 → 0.1.4
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 +62 -5
- package/dist/editor.d.ts +111 -1
- package/dist/editor.js +42 -0
- package/dist/index.d.ts +92 -1
- package/dist/index.js +57 -8
- package/dist/types.d.ts +1379 -37
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,11 +31,12 @@ the cache and transport configuration with the original client.
|
|
|
31
31
|
| --- | --- |
|
|
32
32
|
| `site()`, `theme()`, `home()` | Identity, tokens, resolved homepage |
|
|
33
33
|
| `page(slug, opts)` | Resolved page including sections; nested slugs supported |
|
|
34
|
-
| `content(opts)` | `kind`, `collection`, `page`, `limit`, `cursor` |
|
|
34
|
+
| `content(opts)` | `kind`, `collection`, `q`, `page`, `limit`, `cursor` |
|
|
35
35
|
| `contentById(id, opts)` | Optional `kind` to disambiguate imported IDs |
|
|
36
|
-
| `posts(opts)` | Content filtered to posts; `collection`, `tag`, pagination |
|
|
36
|
+
| `posts(opts)` | Content filtered to posts; `collection`, `tag`, `q`, pagination |
|
|
37
37
|
| `post(slug, opts)` | Scans post summaries, then loads detail by ID; pass `collection` for ambiguous slugs |
|
|
38
38
|
| `pages(opts)` | Page **summaries**, with pagination; use `page()` for sections |
|
|
39
|
+
| `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 |
|
|
39
40
|
| `events(opts)` | Occurrences with `from`, `to`, `calendar`, `q`, pagination |
|
|
40
41
|
| `event(slug, opts)` | First matching event from occurrences in the requested window |
|
|
41
42
|
| `calendars()` | Public calendars |
|
|
@@ -47,6 +48,12 @@ the cache and transport configuration with the original client.
|
|
|
47
48
|
| `updateDevice(token, { topics?, locale? })` | Change a device's topics or language |
|
|
48
49
|
| `unregisterDevice(token)` | Opt out (idempotent) |
|
|
49
50
|
| `pushTopics()` | Topics to offer as notification toggles |
|
|
51
|
+
| `links()` | The links page: profile, socials and visible blocks (`link`, `email`, `phone`, `contact`, `heading`, `text`, `spacer`, `live`, `embed`) |
|
|
52
|
+
| `form(id)`, `submitForm(id, { answers, version?, idempotencyKey? })` | A published form's questions; send answers (checkbox groups as arrays). Each call makes one key; retries reuse it |
|
|
53
|
+
| `manageBooking(token)`, `cancelBooking(token)` | The booker's own booking from its `manageToken`, and cancelling it; `can` says what is allowed |
|
|
54
|
+
| `rescheduleAvailability(token, { from?, days?, minutes? })`, `rescheduleBooking(token, { start, minutes? })` | Open times for moving it (its own time counts as free), then the move |
|
|
55
|
+
| `bookingIcs(token)` | The booking as an iCalendar file (string) |
|
|
56
|
+
| `requestGuestCode(email)`, `guestSignIn({ email, code, deviceName? })`, `guestBookings(guestToken, { when? })`, `guestSignOut(guestToken)` | Guest sign-in for "My bookings" |
|
|
50
57
|
|
|
51
58
|
The server has no post-slug, event-detail or resource-detail endpoint. Those
|
|
52
59
|
convenience methods may make multiple requests. `event()` only finds events
|
|
@@ -54,6 +61,17 @@ with occurrences in its window (default: next 30 UTC days); an unlisted
|
|
|
54
61
|
calendar requires `calendar`. It does not fetch arbitrary historical events.
|
|
55
62
|
If post slugs collide and no collection is supplied, the first listed match wins.
|
|
56
63
|
|
|
64
|
+
Search needs `site.features.search` (on unless the site sets
|
|
65
|
+
`acms({ search: { enabled: false } })`). It uses the website's search index and
|
|
66
|
+
ranking: titles count most, then excerpts, tags and body text. Every word
|
|
67
|
+
must match, and the last one matches as a prefix (`birth` finds "birthday"). `q` on `content()`/`posts()` returns
|
|
68
|
+
ranked summaries, posts before pages, and pages through at most 200 matches per
|
|
69
|
+
kind. `search()` is for a combined results screen: each hit carries a plain-text
|
|
70
|
+
`snippet` with `highlights` (`{ start, end }` ranges) to bold, and events add
|
|
71
|
+
their next `occurrence`. Use `more.posts` to offer "See all", then
|
|
72
|
+
`posts({ q, collection })`. Matching uses the main-language text, so a
|
|
73
|
+
translated title is shown but not searched.
|
|
74
|
+
|
|
57
75
|
Pagination uses `page`/`nextPage`, not opaque cursors. `cursor` is a numeric
|
|
58
76
|
alias for `page`; an explicit `page` wins. `tag` filters **each fetched page
|
|
59
77
|
locally**, so a page can be empty with a non-null `nextPage`. Iterators continue
|
|
@@ -86,9 +104,14 @@ controller.abort();
|
|
|
86
104
|
await pending.catch(() => {});
|
|
87
105
|
```
|
|
88
106
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
107
|
+
Requests that are safe to repeat (GET, PUT, DELETE, device registration, and
|
|
108
|
+
POSTs with an Idempotency-Key: bookings and form responses) retry network
|
|
109
|
+
failures, 5xx and 429 responses at most twice (three attempts total), with
|
|
110
|
+
250/500ms backoff and at least the server's `Retry-After` delay (seconds or HTTP
|
|
111
|
+
date). A wait that would outlast `timeoutMs` throws the 429 straight away, with
|
|
112
|
+
`retryAfter`. One-shot writes (guest code requests and sign-in, cancelling and
|
|
113
|
+
moving a booking, guest sign-out) are never repeated automatically: on a
|
|
114
|
+
network error, check the state before trying again. Booking retries always
|
|
92
115
|
reuse the same serialized body and UUID. By default each `book()` call creates
|
|
93
116
|
a key using `crypto.randomUUID`, falling back to a Math.random UUID v4 where
|
|
94
117
|
unavailable. For recovery across calls/app restarts, supply and persist your own
|
|
@@ -155,6 +178,29 @@ and checks generated request/response types byte-for-byte. HEAD and OPTIONS
|
|
|
155
178
|
are transport operations and have no separate data-returning methods. No core
|
|
156
179
|
code, Zod, or Node built-ins ship in this package's runtime.
|
|
157
180
|
|
|
181
|
+
## Managing bookings and "My bookings"
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
// a booking the app made (result.manageToken from book()), or one from guestBookings()
|
|
185
|
+
const view = await client.manageBooking(manageToken);
|
|
186
|
+
if (view.can.reschedule) {
|
|
187
|
+
const open = await client.rescheduleAvailability(manageToken, { days: 7 });
|
|
188
|
+
await client.rescheduleBooking(manageToken, { start: open.days[0].slots[0].start });
|
|
189
|
+
}
|
|
190
|
+
if (view.can.cancel) await client.cancelBooking(manageToken);
|
|
191
|
+
|
|
192
|
+
// guest sign-in (check site.features.guestSignIn first)
|
|
193
|
+
await client.requestGuestCode('ana@example.com');
|
|
194
|
+
const { accessToken } = await client.guestSignIn({ email: 'ana@example.com', code: '481203' });
|
|
195
|
+
const mine = await client.guestBookings(accessToken, { when: 'upcoming' });
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Booking and guest tokens travel in headers (`Booking-Token`,
|
|
199
|
+
`Authorization: Bearer`), never in URLs, and their responses are never cached.
|
|
200
|
+
Keep both in secure storage (`expo-secure-store`). `can.closed` means the
|
|
201
|
+
booking type's change cutoff has passed: show "Contact us" instead of the
|
|
202
|
+
buttons. A guest token ends after 90 days unused; 401 means sign in again.
|
|
203
|
+
|
|
158
204
|
## Editor apps (staff sign-in)
|
|
159
205
|
|
|
160
206
|
For sites with `acms({ mobile: { editor: { redirectUris } } })`. Staff sign in
|
|
@@ -180,3 +226,14 @@ In React Native pass `crypto: { randomBytes, sha256 }` from expo-crypto to
|
|
|
180
226
|
`createEditorAuth` (Web Crypto is the default). Refreshes are single-flight;
|
|
181
227
|
a 401 is retried once after a refresh, then `onAuthError` runs. Writes carry
|
|
182
228
|
the `revision` you loaded (409 when someone saved first).
|
|
229
|
+
|
|
230
|
+
Schedule and setup: `schedule({ from, to, resource? })` (up to 63 days of
|
|
231
|
+
bookings, blocked time, events and feeds), `createBooking()` (on someone's
|
|
232
|
+
behalf), `rescheduleBooking(id, …)`, `listResources()` / `getResource()` /
|
|
233
|
+
`createResource()` / `updateResource()` / `deleteResource()`,
|
|
234
|
+
`createBookingLink()` / `updateBookingLink()` / `deleteBookingLink()`,
|
|
235
|
+
`blockTime()` / `unblockTime()`, `listCalendars()` / `createCalendar()` /
|
|
236
|
+
`updateCalendar()` / `deleteCalendar()`, `listForms()` /
|
|
237
|
+
`listFormResponses()`, `getLinksPage()` / `saveLinksPage()`. Setup writes need
|
|
238
|
+
`me().permissions.schedule.manage`; form responses and HTML link blocks need
|
|
239
|
+
an administrator or owner (403 otherwise).
|
package/dist/editor.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AcmsError } from './errors.js';
|
|
2
|
-
import type { EditorToken, EditorMe, EditorDocumentList, EditorDocument, EditorSections, EditorSectionResult, EditorRevision, EditorDocumentTranslation, EditorSectionTranslation, EditorMedia, EditorMediaList, EditorBooking, EditorBookingList, EditorCreateInput, EditorSaveInput, EditorListQuery, EditorSectionsQuery, EditorMediaQuery, EditorBookingsQuery } from './types.js';
|
|
2
|
+
import type { EditorToken, EditorMe, EditorDocumentList, EditorDocument, EditorSections, EditorSectionResult, EditorRevision, EditorDocumentTranslation, EditorSectionTranslation, EditorMedia, EditorMediaList, EditorBooking, EditorBookingList, EditorCreateInput, EditorSaveInput, EditorListQuery, EditorSectionsQuery, EditorMediaQuery, EditorBookingsQuery, EditorSchedule, EditorScheduleQuery, EditorStaffBookingInput, EditorRescheduleInput, EditorResource, EditorResourceList, EditorResourceInput, EditorResourcePatch, EditorBookingLink, EditorLinkInput, EditorLinkPatch, EditorBlock, EditorBlockInput, EditorCalendar, EditorCalendarList, EditorCalendarInput, EditorCalendarPatch, EditorDeleted, EditorFormList, EditorFormResponses, EditorResponsesQuery, EditorLinksPage, EditorLinksPut } from './types.js';
|
|
3
3
|
/** Randomness + SHA-256. Defaults to Web Crypto; in React Native pass expo-crypto:
|
|
4
4
|
* { randomBytes: (n) => Crypto.getRandomBytes(n), sha256: (d) => Crypto.digest(Crypto.CryptoDigestAlgorithm.SHA256, d) } */
|
|
5
5
|
export interface PkceCrypto {
|
|
@@ -201,6 +201,90 @@ export declare const editorEndpoints: {
|
|
|
201
201
|
readonly method: "POST";
|
|
202
202
|
readonly path: "/admin/bookings/{id}/cancel";
|
|
203
203
|
};
|
|
204
|
+
readonly createBooking: {
|
|
205
|
+
readonly method: "POST";
|
|
206
|
+
readonly path: "/admin/bookings";
|
|
207
|
+
};
|
|
208
|
+
readonly rescheduleBooking: {
|
|
209
|
+
readonly method: "POST";
|
|
210
|
+
readonly path: "/admin/bookings/{id}/reschedule";
|
|
211
|
+
};
|
|
212
|
+
readonly schedule: {
|
|
213
|
+
readonly method: "GET";
|
|
214
|
+
readonly path: "/admin/schedule";
|
|
215
|
+
};
|
|
216
|
+
readonly listResources: {
|
|
217
|
+
readonly method: "GET";
|
|
218
|
+
readonly path: "/admin/resources";
|
|
219
|
+
};
|
|
220
|
+
readonly createResource: {
|
|
221
|
+
readonly method: "POST";
|
|
222
|
+
readonly path: "/admin/resources";
|
|
223
|
+
};
|
|
224
|
+
readonly getResource: {
|
|
225
|
+
readonly method: "GET";
|
|
226
|
+
readonly path: "/admin/resources/{id}";
|
|
227
|
+
};
|
|
228
|
+
readonly updateResource: {
|
|
229
|
+
readonly method: "PATCH";
|
|
230
|
+
readonly path: "/admin/resources/{id}";
|
|
231
|
+
};
|
|
232
|
+
readonly deleteResource: {
|
|
233
|
+
readonly method: "DELETE";
|
|
234
|
+
readonly path: "/admin/resources/{id}";
|
|
235
|
+
};
|
|
236
|
+
readonly createBookingLink: {
|
|
237
|
+
readonly method: "POST";
|
|
238
|
+
readonly path: "/admin/resources/{id}/links";
|
|
239
|
+
};
|
|
240
|
+
readonly updateBookingLink: {
|
|
241
|
+
readonly method: "PATCH";
|
|
242
|
+
readonly path: "/admin/resources/{id}/links/{linkId}";
|
|
243
|
+
};
|
|
244
|
+
readonly deleteBookingLink: {
|
|
245
|
+
readonly method: "DELETE";
|
|
246
|
+
readonly path: "/admin/resources/{id}/links/{linkId}";
|
|
247
|
+
};
|
|
248
|
+
readonly blockTime: {
|
|
249
|
+
readonly method: "POST";
|
|
250
|
+
readonly path: "/admin/resources/{id}/blocks";
|
|
251
|
+
};
|
|
252
|
+
readonly unblockTime: {
|
|
253
|
+
readonly method: "DELETE";
|
|
254
|
+
readonly path: "/admin/resources/{id}/blocks/{blockId}";
|
|
255
|
+
};
|
|
256
|
+
readonly listCalendars: {
|
|
257
|
+
readonly method: "GET";
|
|
258
|
+
readonly path: "/admin/calendars";
|
|
259
|
+
};
|
|
260
|
+
readonly createCalendar: {
|
|
261
|
+
readonly method: "POST";
|
|
262
|
+
readonly path: "/admin/calendars";
|
|
263
|
+
};
|
|
264
|
+
readonly updateCalendar: {
|
|
265
|
+
readonly method: "PATCH";
|
|
266
|
+
readonly path: "/admin/calendars/{id}";
|
|
267
|
+
};
|
|
268
|
+
readonly deleteCalendar: {
|
|
269
|
+
readonly method: "DELETE";
|
|
270
|
+
readonly path: "/admin/calendars/{id}";
|
|
271
|
+
};
|
|
272
|
+
readonly listForms: {
|
|
273
|
+
readonly method: "GET";
|
|
274
|
+
readonly path: "/admin/forms";
|
|
275
|
+
};
|
|
276
|
+
readonly listFormResponses: {
|
|
277
|
+
readonly method: "GET";
|
|
278
|
+
readonly path: "/admin/forms/{id}/responses";
|
|
279
|
+
};
|
|
280
|
+
readonly getLinksPage: {
|
|
281
|
+
readonly method: "GET";
|
|
282
|
+
readonly path: "/admin/links";
|
|
283
|
+
};
|
|
284
|
+
readonly saveLinksPage: {
|
|
285
|
+
readonly method: "PUT";
|
|
286
|
+
readonly path: "/admin/links";
|
|
287
|
+
};
|
|
204
288
|
};
|
|
205
289
|
export type DocKind = 'post' | 'page' | 'event';
|
|
206
290
|
/** React Native's FormData file shape, or a Blob/File on the web and Node */
|
|
@@ -287,5 +371,31 @@ export declare function createEditorClient(options: EditorClientOptions): {
|
|
|
287
371
|
cancelBooking: (id: string, input?: {
|
|
288
372
|
message?: string;
|
|
289
373
|
}, o?: EditorRequestOptions) => Promise<EditorBooking>;
|
|
374
|
+
/** book on someone's behalf; ignoreRules needs an administrator or owner (see me().permissions.schedule) */
|
|
375
|
+
createBooking: (input: EditorStaffBookingInput, o?: EditorRequestOptions) => Promise<EditorBooking>;
|
|
376
|
+
rescheduleBooking: (id: string, input: EditorRescheduleInput, o?: EditorRequestOptions) => Promise<EditorBooking>;
|
|
377
|
+
/** bookings, blocked time, events and subscribed calendars; from/to are site-local days, up to 63 days */
|
|
378
|
+
schedule: (q: EditorScheduleQuery, o?: EditorRequestOptions) => Promise<EditorSchedule>;
|
|
379
|
+
listResources: (o?: EditorRequestOptions) => Promise<EditorResourceList>;
|
|
380
|
+
getResource: (id: string, o?: EditorRequestOptions) => Promise<EditorResource>;
|
|
381
|
+
createResource: (input: EditorResourceInput, o?: EditorRequestOptions) => Promise<EditorResource>;
|
|
382
|
+
updateResource: (id: string, input: EditorResourcePatch, o?: EditorRequestOptions) => Promise<EditorResource>;
|
|
383
|
+
deleteResource: (id: string, o?: EditorRequestOptions) => Promise<EditorDeleted>;
|
|
384
|
+
createBookingLink: (resourceId: string, input: EditorLinkInput, o?: EditorRequestOptions) => Promise<EditorBookingLink>;
|
|
385
|
+
updateBookingLink: (resourceId: string, linkId: string, input: EditorLinkPatch, o?: EditorRequestOptions) => Promise<EditorBookingLink>;
|
|
386
|
+
deleteBookingLink: (resourceId: string, linkId: string, o?: EditorRequestOptions) => Promise<EditorDeleted>;
|
|
387
|
+
blockTime: (resourceId: string, input: EditorBlockInput, o?: EditorRequestOptions) => Promise<EditorBlock>;
|
|
388
|
+
unblockTime: (resourceId: string, blockId: string, o?: EditorRequestOptions) => Promise<EditorDeleted>;
|
|
389
|
+
listCalendars: (o?: EditorRequestOptions) => Promise<EditorCalendarList>;
|
|
390
|
+
createCalendar: (input: EditorCalendarInput, o?: EditorRequestOptions) => Promise<EditorCalendar>;
|
|
391
|
+
updateCalendar: (id: string, input: EditorCalendarPatch, o?: EditorRequestOptions) => Promise<EditorCalendar>;
|
|
392
|
+
deleteCalendar: (id: string, o?: EditorRequestOptions) => Promise<EditorDeleted>;
|
|
393
|
+
listForms: (o?: EditorRequestOptions) => Promise<EditorFormList>;
|
|
394
|
+
/** administrators and owners only (me().permissions.forms.readResponses) */
|
|
395
|
+
listFormResponses: (id: string, q?: EditorResponsesQuery, o?: EditorRequestOptions) => Promise<EditorFormResponses>;
|
|
396
|
+
getLinksPage: (o?: EditorRequestOptions) => Promise<EditorLinksPage>;
|
|
397
|
+
/** replaces the whole page: send every block in order (omit id for new ones) */
|
|
398
|
+
/** send the `revision` from linksPage(); 409 when the page changed since (nothing saved) */
|
|
399
|
+
saveLinksPage: (input: EditorLinksPut, o?: EditorRequestOptions) => Promise<EditorLinksPage>;
|
|
290
400
|
};
|
|
291
401
|
export type EditorClient = ReturnType<typeof createEditorClient>;
|
package/dist/editor.js
CHANGED
|
@@ -220,6 +220,21 @@ export const editorEndpoints = {
|
|
|
220
220
|
approveBooking: { method: 'POST', path: '/admin/bookings/{id}/approve' },
|
|
221
221
|
declineBooking: { method: 'POST', path: '/admin/bookings/{id}/decline' },
|
|
222
222
|
cancelBooking: { method: 'POST', path: '/admin/bookings/{id}/cancel' },
|
|
223
|
+
createBooking: { method: 'POST', path: '/admin/bookings' },
|
|
224
|
+
rescheduleBooking: { method: 'POST', path: '/admin/bookings/{id}/reschedule' },
|
|
225
|
+
schedule: { method: 'GET', path: '/admin/schedule' },
|
|
226
|
+
listResources: { method: 'GET', path: '/admin/resources' }, createResource: { method: 'POST', path: '/admin/resources' },
|
|
227
|
+
getResource: { method: 'GET', path: '/admin/resources/{id}' }, updateResource: { method: 'PATCH', path: '/admin/resources/{id}' },
|
|
228
|
+
deleteResource: { method: 'DELETE', path: '/admin/resources/{id}' },
|
|
229
|
+
createBookingLink: { method: 'POST', path: '/admin/resources/{id}/links' },
|
|
230
|
+
updateBookingLink: { method: 'PATCH', path: '/admin/resources/{id}/links/{linkId}' },
|
|
231
|
+
deleteBookingLink: { method: 'DELETE', path: '/admin/resources/{id}/links/{linkId}' },
|
|
232
|
+
blockTime: { method: 'POST', path: '/admin/resources/{id}/blocks' },
|
|
233
|
+
unblockTime: { method: 'DELETE', path: '/admin/resources/{id}/blocks/{blockId}' },
|
|
234
|
+
listCalendars: { method: 'GET', path: '/admin/calendars' }, createCalendar: { method: 'POST', path: '/admin/calendars' },
|
|
235
|
+
updateCalendar: { method: 'PATCH', path: '/admin/calendars/{id}' }, deleteCalendar: { method: 'DELETE', path: '/admin/calendars/{id}' },
|
|
236
|
+
listForms: { method: 'GET', path: '/admin/forms' }, listFormResponses: { method: 'GET', path: '/admin/forms/{id}/responses' },
|
|
237
|
+
getLinksPage: { method: 'GET', path: '/admin/links' }, saveLinksPage: { method: 'PUT', path: '/admin/links' },
|
|
223
238
|
};
|
|
224
239
|
export function createEditorClient(options) {
|
|
225
240
|
const { api } = origin(options.baseUrl);
|
|
@@ -350,5 +365,32 @@ export function createEditorClient(options) {
|
|
|
350
365
|
approveBooking: (id, input = {}, o) => call('approveBooking', { id }, { ...R(o), body: input }),
|
|
351
366
|
declineBooking: (id, input = {}, o) => call('declineBooking', { id }, { ...R(o), body: input }),
|
|
352
367
|
cancelBooking: (id, input = {}, o) => call('cancelBooking', { id }, { ...R(o), body: input }),
|
|
368
|
+
/** book on someone's behalf; ignoreRules needs an administrator or owner (see me().permissions.schedule) */
|
|
369
|
+
createBooking: (input, o) => call('createBooking', {}, { ...R(o), body: input }),
|
|
370
|
+
rescheduleBooking: (id, input, o) => call('rescheduleBooking', { id }, { ...R(o), body: input }),
|
|
371
|
+
/** bookings, blocked time, events and subscribed calendars; from/to are site-local days, up to 63 days */
|
|
372
|
+
schedule: (q, o) => call('schedule', {}, { ...R(o), query: q }),
|
|
373
|
+
listResources: (o) => call('listResources', {}, R(o)),
|
|
374
|
+
getResource: (id, o) => call('getResource', { id }, R(o)),
|
|
375
|
+
// setup writes need me().permissions.schedule.manage (403 otherwise)
|
|
376
|
+
createResource: (input, o) => call('createResource', {}, { ...R(o), body: input }),
|
|
377
|
+
updateResource: (id, input, o) => call('updateResource', { id }, { ...R(o), body: input }),
|
|
378
|
+
deleteResource: (id, o) => call('deleteResource', { id }, R(o)),
|
|
379
|
+
createBookingLink: (resourceId, input, o) => call('createBookingLink', { id: resourceId }, { ...R(o), body: input }),
|
|
380
|
+
updateBookingLink: (resourceId, linkId, input, o) => call('updateBookingLink', { id: resourceId, linkId }, { ...R(o), body: input }),
|
|
381
|
+
deleteBookingLink: (resourceId, linkId, o) => call('deleteBookingLink', { id: resourceId, linkId }, R(o)),
|
|
382
|
+
blockTime: (resourceId, input, o) => call('blockTime', { id: resourceId }, { ...R(o), body: input }),
|
|
383
|
+
unblockTime: (resourceId, blockId, o) => call('unblockTime', { id: resourceId, blockId }, R(o)),
|
|
384
|
+
listCalendars: (o) => call('listCalendars', {}, R(o)),
|
|
385
|
+
createCalendar: (input, o) => call('createCalendar', {}, { ...R(o), body: input }),
|
|
386
|
+
updateCalendar: (id, input, o) => call('updateCalendar', { id }, { ...R(o), body: input }),
|
|
387
|
+
deleteCalendar: (id, o) => call('deleteCalendar', { id }, R(o)),
|
|
388
|
+
listForms: (o) => call('listForms', {}, R(o)),
|
|
389
|
+
/** administrators and owners only (me().permissions.forms.readResponses) */
|
|
390
|
+
listFormResponses: (id, q = {}, o) => call('listFormResponses', { id }, { ...R(o), query: q }),
|
|
391
|
+
getLinksPage: (o) => call('getLinksPage', {}, R(o)),
|
|
392
|
+
/** replaces the whole page: send every block in order (omit id for new ones) */
|
|
393
|
+
/** send the `revision` from linksPage(); 409 when the page changed since (nothing saved) */
|
|
394
|
+
saveLinksPage: (input, o) => call('saveLinksPage', {}, { ...R(o), body: input }),
|
|
353
395
|
};
|
|
354
396
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Site, Theme, Page, Content, Post, ContentList, Events, Event, Calendars, Resources, Resource, Availability, BookingInput, BookingResult, Device, DeviceInput, DeviceUpdate, DeviceRegistration, DeviceRemoved, PushTopics } 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, Device, DeviceInput, DeviceUpdate, DeviceRegistration, DeviceRemoved, PushTopics } from './types.js';
|
|
2
2
|
export { AcmsError, isAcmsError } from './errors.js';
|
|
3
3
|
export type * from './types.js';
|
|
4
4
|
export * from './editor.js';
|
|
@@ -14,6 +14,8 @@ export interface ListOptions extends RequestOptions {
|
|
|
14
14
|
export interface ContentOptions extends ListOptions {
|
|
15
15
|
kind?: 'post' | 'page';
|
|
16
16
|
collection?: string;
|
|
17
|
+
/** Search text (up to 100 characters): ranked matches instead of newest first, posts before pages. Blank means no search. Requires `site.features.search`. */
|
|
18
|
+
q?: string;
|
|
17
19
|
}
|
|
18
20
|
export interface PostOptions extends Omit<ContentOptions, 'kind'> {
|
|
19
21
|
tag?: string;
|
|
@@ -30,6 +32,14 @@ export interface EventOptions extends ListOptions {
|
|
|
30
32
|
calendar?: string;
|
|
31
33
|
q?: string;
|
|
32
34
|
}
|
|
35
|
+
export interface SearchOptions extends RequestOptions {
|
|
36
|
+
/** Which groups to search; default all three. */
|
|
37
|
+
types?: Array<'post' | 'page' | 'event'>;
|
|
38
|
+
/** Only posts in this collection (pages and events are skipped). */
|
|
39
|
+
collection?: string;
|
|
40
|
+
/** Matches per group, 1–20; default 5. */
|
|
41
|
+
limit?: number;
|
|
42
|
+
}
|
|
33
43
|
export interface AvailabilityOptions extends RequestOptions {
|
|
34
44
|
resource: string;
|
|
35
45
|
link: string;
|
|
@@ -94,6 +104,10 @@ export declare const endpoints: {
|
|
|
94
104
|
readonly method: "GET";
|
|
95
105
|
readonly path: "/content/{id}";
|
|
96
106
|
};
|
|
107
|
+
readonly search: {
|
|
108
|
+
readonly method: "GET";
|
|
109
|
+
readonly path: "/search";
|
|
110
|
+
};
|
|
97
111
|
readonly events: {
|
|
98
112
|
readonly method: "GET";
|
|
99
113
|
readonly path: "/events";
|
|
@@ -134,6 +148,54 @@ export declare const endpoints: {
|
|
|
134
148
|
readonly method: "GET";
|
|
135
149
|
readonly path: "/push/topics";
|
|
136
150
|
};
|
|
151
|
+
readonly links: {
|
|
152
|
+
readonly method: "GET";
|
|
153
|
+
readonly path: "/links";
|
|
154
|
+
};
|
|
155
|
+
readonly form: {
|
|
156
|
+
readonly method: "GET";
|
|
157
|
+
readonly path: "/forms/{id}";
|
|
158
|
+
};
|
|
159
|
+
readonly submitForm: {
|
|
160
|
+
readonly method: "POST";
|
|
161
|
+
readonly path: "/forms/{id}/responses";
|
|
162
|
+
};
|
|
163
|
+
readonly manageBooking: {
|
|
164
|
+
readonly method: "GET";
|
|
165
|
+
readonly path: "/bookings/manage";
|
|
166
|
+
};
|
|
167
|
+
readonly cancelBooking: {
|
|
168
|
+
readonly method: "POST";
|
|
169
|
+
readonly path: "/bookings/manage/cancel";
|
|
170
|
+
};
|
|
171
|
+
readonly rescheduleAvailability: {
|
|
172
|
+
readonly method: "GET";
|
|
173
|
+
readonly path: "/bookings/manage/availability";
|
|
174
|
+
};
|
|
175
|
+
readonly rescheduleBooking: {
|
|
176
|
+
readonly method: "POST";
|
|
177
|
+
readonly path: "/bookings/manage/reschedule";
|
|
178
|
+
};
|
|
179
|
+
readonly bookingIcs: {
|
|
180
|
+
readonly method: "GET";
|
|
181
|
+
readonly path: "/bookings/manage/ics";
|
|
182
|
+
};
|
|
183
|
+
readonly requestGuestCode: {
|
|
184
|
+
readonly method: "POST";
|
|
185
|
+
readonly path: "/guest/code";
|
|
186
|
+
};
|
|
187
|
+
readonly guestSignIn: {
|
|
188
|
+
readonly method: "POST";
|
|
189
|
+
readonly path: "/guest/token";
|
|
190
|
+
};
|
|
191
|
+
readonly guestBookings: {
|
|
192
|
+
readonly method: "GET";
|
|
193
|
+
readonly path: "/guest/bookings";
|
|
194
|
+
};
|
|
195
|
+
readonly guestSignOut: {
|
|
196
|
+
readonly method: "POST";
|
|
197
|
+
readonly path: "/guest/revoke";
|
|
198
|
+
};
|
|
137
199
|
};
|
|
138
200
|
export declare function createClient(options: ClientOptions): AcmsClient;
|
|
139
201
|
export interface AcmsClient {
|
|
@@ -150,6 +212,8 @@ export interface AcmsClient {
|
|
|
150
212
|
collection?: string;
|
|
151
213
|
}): Promise<Post>;
|
|
152
214
|
pages(o?: ListOptions): Promise<ContentList>;
|
|
215
|
+
/** Ranked matches across posts, pages and events, the top few per group. Requires `site.features.search`. */
|
|
216
|
+
search(q: string, o?: SearchOptions): Promise<SearchResults>;
|
|
153
217
|
events(o?: EventOptions): Promise<Events>;
|
|
154
218
|
event(slug: string, o?: EventOptions): Promise<Event>;
|
|
155
219
|
calendars(o?: RequestOptions): Promise<Calendars>;
|
|
@@ -166,6 +230,33 @@ export interface AcmsClient {
|
|
|
166
230
|
unregisterDevice(token: string, o?: RequestOptions): Promise<DeviceRemoved>;
|
|
167
231
|
/** Topics the app can offer as notification toggles. */
|
|
168
232
|
pushTopics(o?: RequestOptions): Promise<PushTopics>;
|
|
233
|
+
/** The links page: profile, socials and visible blocks. Requires `site.features.links`. */
|
|
234
|
+
links(o?: RequestOptions): Promise<Links>;
|
|
235
|
+
/** A published form's questions. 410 when it stopped accepting responses. Requires `site.features.forms`. */
|
|
236
|
+
form(id: string, o?: RequestOptions): Promise<Form>;
|
|
237
|
+
/** Send answers (checkbox groups as arrays). Retries reuse the same Idempotency-Key, so they never duplicate. */
|
|
238
|
+
submitForm(id: string, input: FormResponseInput & {
|
|
239
|
+
idempotencyKey?: string;
|
|
240
|
+
}, o?: RequestOptions): Promise<FormReceipt>;
|
|
241
|
+
/** The booker's own booking, from its private token (`manageToken`). Never cached. Requires `site.features.bookingChanges`. */
|
|
242
|
+
manageBooking(token: string, o?: RequestOptions): Promise<ManagedBooking>;
|
|
243
|
+
cancelBooking(token: string, o?: RequestOptions): Promise<ManagedBooking>;
|
|
244
|
+
/** Open times for moving the booking (its own time counts as free). */
|
|
245
|
+
rescheduleAvailability(token: string, q?: ManageAvailabilityQuery, o?: RequestOptions): Promise<Availability>;
|
|
246
|
+
rescheduleBooking(token: string, input: RescheduleInput, o?: RequestOptions): Promise<ManagedBooking>;
|
|
247
|
+
/** The booking as an iCalendar file (text). */
|
|
248
|
+
bookingIcs(token: string, o?: RequestOptions): Promise<string>;
|
|
249
|
+
/** Email a 6-digit sign-in code. Always resolves the same way. Requires `site.features.guestSignIn`. */
|
|
250
|
+
requestGuestCode(email: string, o?: RequestOptions): Promise<GuestCodeSent>;
|
|
251
|
+
/** Email + code → guest token. Keep it in secure storage; it ends after 90 days unused. */
|
|
252
|
+
guestSignIn(input: {
|
|
253
|
+
email: string;
|
|
254
|
+
code: string;
|
|
255
|
+
deviceName?: string;
|
|
256
|
+
}, o?: RequestOptions): Promise<GuestToken>;
|
|
257
|
+
/** Bookings and RSVPs for the signed-in email; each has a `manageToken` for the methods above. */
|
|
258
|
+
guestBookings(guestToken: string, q?: GuestBookingsQuery, o?: RequestOptions): Promise<GuestBookings>;
|
|
259
|
+
guestSignOut(guestToken: string, o?: RequestOptions): Promise<GuestSignedOut>;
|
|
169
260
|
withLocale(locale: string): AcmsClient;
|
|
170
261
|
paginate: {
|
|
171
262
|
content(o?: ContentOptions): AsyncGenerator<ContentList['items'][number]>;
|
package/dist/index.js
CHANGED
|
@@ -6,14 +6,24 @@ export const endpoints = {
|
|
|
6
6
|
site: { method: 'GET', path: '/site' }, theme: { method: 'GET', path: '/theme' },
|
|
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
|
+
search: { method: 'GET', path: '/search' },
|
|
9
10
|
events: { method: 'GET', path: '/events' }, calendars: { method: 'GET', path: '/calendars' },
|
|
10
11
|
resources: { method: 'GET', path: '/resources' }, availability: { method: 'GET', path: '/availability' },
|
|
11
12
|
book: { method: 'POST', path: '/bookings' }, openapi: { method: 'GET', path: '/openapi.json' },
|
|
12
13
|
// push notifications (only when the site enables push; 404 otherwise)
|
|
13
14
|
registerDevice: { method: 'POST', path: '/devices' }, updateDevice: { method: 'PUT', path: '/devices/{token}' },
|
|
14
15
|
unregisterDevice: { method: 'DELETE', path: '/devices/{token}' }, pushTopics: { method: 'GET', path: '/push/topics' },
|
|
16
|
+
// links page and forms (owner switches in Admin → Mobile app)
|
|
17
|
+
links: { method: 'GET', path: '/links' }, form: { method: 'GET', path: '/forms/{id}' }, submitForm: { method: 'POST', path: '/forms/{id}/responses' },
|
|
18
|
+
// the booker's own booking: its private token goes in the Booking-Token header
|
|
19
|
+
manageBooking: { method: 'GET', path: '/bookings/manage' }, cancelBooking: { method: 'POST', path: '/bookings/manage/cancel' },
|
|
20
|
+
rescheduleAvailability: { method: 'GET', path: '/bookings/manage/availability' }, rescheduleBooking: { method: 'POST', path: '/bookings/manage/reschedule' },
|
|
21
|
+
bookingIcs: { method: 'GET', path: '/bookings/manage/ics' },
|
|
22
|
+
// guest sign-in for "My bookings" (emailed code → guest token)
|
|
23
|
+
requestGuestCode: { method: 'POST', path: '/guest/code' }, guestSignIn: { method: 'POST', path: '/guest/token' },
|
|
24
|
+
guestBookings: { method: 'GET', path: '/guest/bookings' }, guestSignOut: { method: 'POST', path: '/guest/revoke' },
|
|
15
25
|
};
|
|
16
|
-
const localized = new Set(['home', 'page', 'content', 'contentById', 'events']);
|
|
26
|
+
const localized = new Set(['home', 'page', 'content', 'contentById', 'search', 'events']);
|
|
17
27
|
function uuid() {
|
|
18
28
|
if (globalThis.crypto?.randomUUID)
|
|
19
29
|
return globalThis.crypto.randomUUID();
|
|
@@ -72,7 +82,8 @@ export function createClient(options) {
|
|
|
72
82
|
memory.delete(url); },
|
|
73
83
|
} : options.cache;
|
|
74
84
|
const defaultHeaders = new Headers(options.headers);
|
|
75
|
-
|
|
85
|
+
// private: credentials in `headers`, never cached; text: a non-JSON body (calendar files)
|
|
86
|
+
async function request(name, opts = {}, query = {}, param, payload, key, extra = {}) {
|
|
76
87
|
const endpoint = endpoints[name];
|
|
77
88
|
const path = endpoint.path.replace(/\{\w+\}/, () => {
|
|
78
89
|
if (!param || param.split('/').some(p => p === '.' || p === '..'))
|
|
@@ -94,7 +105,11 @@ export function createClient(options) {
|
|
|
94
105
|
opts.signal?.addEventListener('abort', abort, { once: true });
|
|
95
106
|
const timer = setTimeout(() => controller.abort(new AcmsError(0, 'timeout', 'Request timed out.', undefined, undefined, key)), timeoutMs);
|
|
96
107
|
const signal = controller.signal;
|
|
97
|
-
const cacheable = endpoint.method === 'GET' && name !== 'availability' && cache;
|
|
108
|
+
const cacheable = endpoint.method === 'GET' && name !== 'availability' && !extra.headers && cache;
|
|
109
|
+
// only replay what the server treats as repeatable: a lost reply to a code
|
|
110
|
+
// exchange or a move must not run it twice
|
|
111
|
+
const replayable = endpoint.method !== 'POST' || !!key || name === 'registerDevice';
|
|
112
|
+
const deadline = Date.now() + timeoutMs;
|
|
98
113
|
try {
|
|
99
114
|
if (signal.aborted)
|
|
100
115
|
throw signal.reason;
|
|
@@ -107,6 +122,10 @@ export function createClient(options) {
|
|
|
107
122
|
headers.set('If-None-Match', cached.etag);
|
|
108
123
|
if (key)
|
|
109
124
|
headers.set('Idempotency-Key', key);
|
|
125
|
+
for (const [k, v] of Object.entries(extra.headers ?? {}))
|
|
126
|
+
headers.set(k, v);
|
|
127
|
+
if (extra.text)
|
|
128
|
+
headers.set('Accept', 'text/calendar');
|
|
110
129
|
if (payload !== undefined)
|
|
111
130
|
headers.set('Content-Type', 'application/json');
|
|
112
131
|
const body = payload !== undefined ? JSON.stringify(payload) : undefined;
|
|
@@ -118,22 +137,26 @@ export function createClient(options) {
|
|
|
118
137
|
catch (error) {
|
|
119
138
|
if (signal.aborted)
|
|
120
139
|
throw signal.reason;
|
|
121
|
-
if (attempt < 2) {
|
|
140
|
+
if (replayable && attempt < 2) {
|
|
122
141
|
await pause(250 * 2 ** attempt, signal);
|
|
123
142
|
continue;
|
|
124
143
|
}
|
|
125
144
|
throw new AcmsError(0, 'network_error', error instanceof Error ? error.message : 'Network request failed.', undefined, undefined, key);
|
|
126
145
|
}
|
|
127
146
|
const retryAfter = retrySeconds(res.headers.get('Retry-After'));
|
|
128
|
-
|
|
147
|
+
const wait = Math.max(250 * 2 ** attempt, (retryAfter ?? 0) * 1000);
|
|
148
|
+
// a wait that would outlast the timeout returns the 429/5xx (with retryAfter) instead
|
|
149
|
+
if ((res.status === 429 || res.status >= 500) && replayable && attempt < 2 && Date.now() + wait < deadline) {
|
|
129
150
|
// Release the response stream before retrying.
|
|
130
151
|
if (res.body)
|
|
131
152
|
await abortable(res.body.cancel().catch(() => { }), signal);
|
|
132
|
-
await pause(
|
|
153
|
+
await pause(wait, signal);
|
|
133
154
|
continue;
|
|
134
155
|
}
|
|
135
156
|
if (res.status === 304 && cached)
|
|
136
157
|
return cached.body;
|
|
158
|
+
if (extra.text && res.ok)
|
|
159
|
+
return await abortable(res.text(), signal);
|
|
137
160
|
let json;
|
|
138
161
|
try {
|
|
139
162
|
json = await abortable(res.json(), signal);
|
|
@@ -143,7 +166,7 @@ export function createClient(options) {
|
|
|
143
166
|
throw signal.reason;
|
|
144
167
|
// fetch can succeed before a network failure interrupts the body stream.
|
|
145
168
|
if (error instanceof TypeError) {
|
|
146
|
-
if (attempt < 2) {
|
|
169
|
+
if (replayable && attempt < 2) {
|
|
147
170
|
await pause(250 * 2 ** attempt, signal);
|
|
148
171
|
continue;
|
|
149
172
|
}
|
|
@@ -169,6 +192,10 @@ export function createClient(options) {
|
|
|
169
192
|
}
|
|
170
193
|
}
|
|
171
194
|
const paging = (o) => ({ page: o.page ?? o.cursor, limit: o.limit });
|
|
195
|
+
const secret = (value, what) => { if (!value || /[\r\n]/.test(value))
|
|
196
|
+
throw new TypeError(`A ${what} is required.`); return value; };
|
|
197
|
+
const bookingAuth = (token) => ({ headers: { 'Booking-Token': secret(token, 'booking token') } });
|
|
198
|
+
const guestAuth = (token) => ({ headers: { Authorization: `Bearer ${secret(token, 'guest token')}` } });
|
|
172
199
|
async function* iterate(load, field, opts) {
|
|
173
200
|
let page = opts.page ?? opts.cursor ?? 1;
|
|
174
201
|
const seen = new Set();
|
|
@@ -186,7 +213,7 @@ export function createClient(options) {
|
|
|
186
213
|
}
|
|
187
214
|
throw new AcmsError(0, 'invalid_response', 'Pagination repeated a page.');
|
|
188
215
|
}
|
|
189
|
-
const content = (o = {}) => request('content', o, { ...paging(o), kind: o.kind, collection: o.collection });
|
|
216
|
+
const content = (o = {}) => request('content', o, { ...paging(o), kind: o.kind, collection: o.collection, q: o.q?.trim() || undefined });
|
|
190
217
|
const posts = async (o = {}) => {
|
|
191
218
|
const result = await content({ ...o, kind: 'post' });
|
|
192
219
|
return { ...result, items: result.items.filter(p => !o.tag || p.tags.includes(o.tag)) };
|
|
@@ -207,6 +234,13 @@ export function createClient(options) {
|
|
|
207
234
|
home: (o = {}) => request('home', o),
|
|
208
235
|
page: (slug, o = {}) => request('page', o, {}, slug),
|
|
209
236
|
content, posts, pages, events, resources, paginate,
|
|
237
|
+
async search(q, o = {}) {
|
|
238
|
+
const query = q.trim();
|
|
239
|
+
// blank input (an emptied search box) needs no round trip
|
|
240
|
+
if (!query)
|
|
241
|
+
return { locale: o.locale ?? options.locale ?? null, query: '', posts: [], pages: [], events: [], more: { posts: false, pages: false, events: false } };
|
|
242
|
+
return request('search', o, { q: query, types: o.types?.join(','), collection: o.collection, limit: o.limit });
|
|
243
|
+
},
|
|
210
244
|
contentById: (id, o = {}) => request('contentById', o, { kind: o.kind }, id),
|
|
211
245
|
async post(slug, o = {}) {
|
|
212
246
|
for await (const item of paginate.posts(o))
|
|
@@ -239,6 +273,21 @@ export function createClient(options) {
|
|
|
239
273
|
updateDevice: (token, input, o = {}) => request('updateDevice', o, {}, token, input),
|
|
240
274
|
unregisterDevice: (token, o = {}) => request('unregisterDevice', o, {}, token),
|
|
241
275
|
pushTopics: (o = {}) => request('pushTopics', o),
|
|
276
|
+
links: (o = {}) => request('links', o),
|
|
277
|
+
form: (id, o = {}) => request('form', o, {}, id),
|
|
278
|
+
submitForm: (id, input, o = {}) => {
|
|
279
|
+
const { idempotencyKey, ...body } = input;
|
|
280
|
+
return request('submitForm', o, {}, id, body, idempotencyKey ?? uuid());
|
|
281
|
+
},
|
|
282
|
+
manageBooking: (token, o = {}) => request('manageBooking', o, {}, undefined, undefined, undefined, bookingAuth(token)),
|
|
283
|
+
cancelBooking: (token, o = {}) => request('cancelBooking', o, {}, undefined, undefined, undefined, bookingAuth(token)),
|
|
284
|
+
rescheduleAvailability: (token, q = {}, o = {}) => request('rescheduleAvailability', o, { from: q.from, days: q.days, minutes: q.minutes }, undefined, undefined, undefined, bookingAuth(token)),
|
|
285
|
+
rescheduleBooking: (token, input, o = {}) => request('rescheduleBooking', o, {}, undefined, input, undefined, bookingAuth(token)),
|
|
286
|
+
bookingIcs: (token, o = {}) => request('bookingIcs', o, {}, undefined, undefined, undefined, { ...bookingAuth(token), text: true }),
|
|
287
|
+
requestGuestCode: (email, o = {}) => request('requestGuestCode', o, {}, undefined, { email }),
|
|
288
|
+
guestSignIn: (input, o = {}) => request('guestSignIn', o, {}, undefined, input),
|
|
289
|
+
guestBookings: (guestToken, q = {}, o = {}) => request('guestBookings', o, { when: q.when, page: q.page, limit: q.limit }, undefined, undefined, undefined, guestAuth(guestToken)),
|
|
290
|
+
guestSignOut: (guestToken, o = {}) => request('guestSignOut', o, {}, undefined, undefined, undefined, guestAuth(guestToken)),
|
|
242
291
|
withLocale: (locale) => createClient({ ...options, locale, cache, headers: defaultHeaders }),
|
|
243
292
|
};
|
|
244
293
|
}
|