payload-reserve 2.4.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +217 -4
- package/dist/collections/Holds.d.ts +18 -0
- package/dist/collections/Holds.js +113 -0
- package/dist/collections/Holds.js.map +1 -0
- package/dist/collections/Reservations.js +8 -0
- package/dist/collections/Reservations.js.map +1 -1
- package/dist/collections/Resources.js +35 -1
- package/dist/collections/Resources.js.map +1 -1
- package/dist/collections/Services.js +41 -0
- package/dist/collections/Services.js.map +1 -1
- package/dist/components/CalendarView/LaneTimelineView.js +4 -5
- package/dist/components/CalendarView/LaneTimelineView.js.map +1 -1
- package/dist/components/CalendarView/index.js +55 -71
- package/dist/components/CalendarView/index.js.map +1 -1
- package/dist/components/DashboardWidget/DashboardWidgetServer.js +11 -74
- package/dist/components/DashboardWidget/DashboardWidgetServer.js.map +1 -1
- package/dist/components/DashboardWidget/fetchDashboardStats.d.ts +42 -0
- package/dist/components/DashboardWidget/fetchDashboardStats.js +113 -0
- package/dist/components/DashboardWidget/fetchDashboardStats.js.map +1 -0
- package/dist/defaults.d.ts +2 -0
- package/dist/defaults.js +11 -0
- package/dist/defaults.js.map +1 -1
- package/dist/endpoints/cancelBooking.js +43 -14
- package/dist/endpoints/cancelBooking.js.map +1 -1
- package/dist/endpoints/checkAvailability.js +12 -1
- package/dist/endpoints/checkAvailability.js.map +1 -1
- package/dist/endpoints/createBooking.js +108 -7
- package/dist/endpoints/createBooking.js.map +1 -1
- package/dist/endpoints/customerSearch.js +6 -0
- package/dist/endpoints/customerSearch.js.map +1 -1
- package/dist/endpoints/effectiveTimezone.js +1 -0
- package/dist/endpoints/effectiveTimezone.js.map +1 -1
- package/dist/endpoints/getSlots.js +12 -1
- package/dist/endpoints/getSlots.js.map +1 -1
- package/dist/endpoints/holdSlot.d.ts +12 -0
- package/dist/endpoints/holdSlot.js +126 -0
- package/dist/endpoints/holdSlot.js.map +1 -0
- package/dist/endpoints/releaseSlot.d.ts +8 -0
- package/dist/endpoints/releaseSlot.js +34 -0
- package/dist/endpoints/releaseSlot.js.map +1 -0
- package/dist/endpoints/resourceAvailability.d.ts +2 -0
- package/dist/endpoints/resourceAvailability.js +79 -3
- package/dist/endpoints/resourceAvailability.js.map +1 -1
- package/dist/hooks/holds/validateHoldSlot.d.ts +14 -0
- package/dist/hooks/holds/validateHoldSlot.js +90 -0
- package/dist/hooks/holds/validateHoldSlot.js.map +1 -0
- package/dist/hooks/reservations/acquireBookingLock.d.ts +30 -0
- package/dist/hooks/reservations/acquireBookingLock.js +71 -0
- package/dist/hooks/reservations/acquireBookingLock.js.map +1 -0
- package/dist/hooks/reservations/calculateEndTime.js +13 -1
- package/dist/hooks/reservations/calculateEndTime.js.map +1 -1
- package/dist/hooks/reservations/expandRequiredResources.js +3 -0
- package/dist/hooks/reservations/expandRequiredResources.js.map +1 -1
- package/dist/hooks/reservations/validateActive.d.ts +11 -0
- package/dist/hooks/reservations/validateActive.js +103 -0
- package/dist/hooks/reservations/validateActive.js.map +1 -0
- package/dist/hooks/reservations/validateConflicts.js +7 -1
- package/dist/hooks/reservations/validateConflicts.js.map +1 -1
- package/dist/hooks/reservations/validateGuestBooking.js +3 -0
- package/dist/hooks/reservations/validateGuestBooking.js.map +1 -1
- package/dist/hooks/shared/preventDeleteWhenReferenced.d.ts +42 -0
- package/dist/hooks/shared/preventDeleteWhenReferenced.js +87 -0
- package/dist/hooks/shared/preventDeleteWhenReferenced.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js.map +1 -1
- package/dist/plugin.js +179 -3
- package/dist/plugin.js.map +1 -1
- package/dist/services/AvailabilityService.d.ts +46 -5
- package/dist/services/AvailabilityService.js +168 -12
- package/dist/services/AvailabilityService.js.map +1 -1
- package/dist/services/HoldService.d.ts +53 -0
- package/dist/services/HoldService.js +169 -0
- package/dist/services/HoldService.js.map +1 -0
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js.map +1 -1
- package/dist/translations/ar.json +5 -1
- package/dist/translations/de.json +5 -1
- package/dist/translations/en.json +5 -1
- package/dist/translations/es.json +5 -1
- package/dist/translations/fa.json +5 -1
- package/dist/translations/fr.json +5 -1
- package/dist/translations/hi.json +5 -1
- package/dist/translations/id.json +5 -1
- package/dist/translations/pl.json +5 -1
- package/dist/translations/ru.json +5 -1
- package/dist/translations/tr.json +5 -1
- package/dist/translations/zh.json +5 -1
- package/dist/types.d.ts +29 -0
- package/dist/types.js.map +1 -1
- package/dist/utilities/calendarGrid.d.ts +44 -0
- package/dist/utilities/calendarGrid.js +67 -0
- package/dist/utilities/calendarGrid.js.map +1 -0
- package/dist/utilities/reservationChanges.d.ts +6 -1
- package/dist/utilities/reservationChanges.js +1 -1
- package/dist/utilities/reservationChanges.js.map +1 -1
- package/dist/utilities/resolveReservationItems.d.ts +13 -2
- package/dist/utilities/resolveReservationItems.js +87 -2
- package/dist/utilities/resolveReservationItems.js.map +1 -1
- package/dist/utilities/retryOnWriteConflict.d.ts +66 -0
- package/dist/utilities/retryOnWriteConflict.js +135 -0
- package/dist/utilities/retryOnWriteConflict.js.map +1 -0
- package/dist/utilities/tenantTimezone.d.ts +77 -1
- package/dist/utilities/tenantTimezone.js +163 -2
- package/dist/utilities/tenantTimezone.js.map +1 -1
- package/dist/utilities/transactionSupport.d.ts +13 -0
- package/dist/utilities/transactionSupport.js +30 -0
- package/dist/utilities/transactionSupport.js.map +1 -0
- package/package.json +13 -13
package/dist/plugin.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { flattenAllFields, getFieldByPath } from 'payload';
|
|
1
2
|
import { deepMergeSimple } from 'payload/shared';
|
|
2
3
|
import { createCustomersCollection } from './collections/Customers.js';
|
|
4
|
+
import { createHoldsCollection } from './collections/Holds.js';
|
|
3
5
|
import { createReservationsCollection } from './collections/Reservations.js';
|
|
4
6
|
import { createResourcesCollection } from './collections/Resources.js';
|
|
5
7
|
import { createSchedulesCollection } from './collections/Schedules.js';
|
|
@@ -11,10 +13,32 @@ import { createBookingEndpoint } from './endpoints/createBooking.js';
|
|
|
11
13
|
import { createCustomerSearchEndpoint } from './endpoints/customerSearch.js';
|
|
12
14
|
import { createEffectiveTimezoneEndpoint } from './endpoints/effectiveTimezone.js';
|
|
13
15
|
import { createGetSlotsEndpoint } from './endpoints/getSlots.js';
|
|
16
|
+
import { createHoldSlotEndpoint } from './endpoints/holdSlot.js';
|
|
17
|
+
import { createReleaseSlotEndpoint } from './endpoints/releaseSlot.js';
|
|
14
18
|
import { createResourceAvailabilityEndpoint } from './endpoints/resourceAvailability.js';
|
|
15
19
|
import { provisionStaffResource } from './hooks/users/provisionStaffResource.js';
|
|
16
20
|
import { translations } from './translations/index.js';
|
|
17
21
|
import { applyCollectionOverride } from './utilities/collectionOverrides.js';
|
|
22
|
+
import { collectionHasTenantField } from './utilities/tenantFilter.js';
|
|
23
|
+
import { supportsTransactions } from './utilities/transactionSupport.js';
|
|
24
|
+
/**
|
|
25
|
+
* The array field `@payloadcms/plugin-multi-tenant` pushes onto the admin users
|
|
26
|
+
* collection to hold a user's tenant memberships. Its own default
|
|
27
|
+
* (`plugin-multi-tenant/dist/defaults.js`); consumers can rename it, which is
|
|
28
|
+
* one of the blind spots the boot diagnostic's comment calls out.
|
|
29
|
+
*/ const MT_TENANTS_ARRAY_FIELD = 'tenants';
|
|
30
|
+
/** True if the collection has a top-level `array` field named `fieldName`. */ function collectionHasArrayField(collection, fieldName) {
|
|
31
|
+
if (!collection || !Array.isArray(collection.fields)) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return collection.fields.some((f)=>{
|
|
35
|
+
if (typeof f !== 'object' || f === null) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const field = f;
|
|
39
|
+
return field.name === fieldName && field.type === 'array';
|
|
40
|
+
});
|
|
41
|
+
}
|
|
18
42
|
/**
|
|
19
43
|
* All named field paths reachable from a field list, descending through
|
|
20
44
|
* presentational containers (tabs, rows, collapsibles, unnamed groups) that
|
|
@@ -125,11 +149,38 @@ export const payloadReserve = (pluginOptions = {})=>(config)=>{
|
|
|
125
149
|
// exists, so installs without one don't hit an opaque init error (C8).
|
|
126
150
|
resolved.hasMediaCollection = config.collections.some((col)=>col.slug === resolved.slugs.media);
|
|
127
151
|
const ov = resolved.collectionOverrides;
|
|
128
|
-
|
|
152
|
+
const resourcesCollection = applyCollectionOverride(createResourcesCollection(resolved), ov.resources);
|
|
153
|
+
// The join's `on: 'services'` only resolves when Payload's own
|
|
154
|
+
// sanitizeJoinField can resolve it — mirror that resolution exactly
|
|
155
|
+
// (flattenAllFields + getFieldByPath) rather than re-implementing the
|
|
156
|
+
// traversal, so this can't drift from Payload's behavior on upgrade.
|
|
157
|
+
// Unnamed containers (row/collapsible/unnamed group/unnamed tabs) are
|
|
158
|
+
// hoisted by flattenAllFields and still resolve; only removal, renaming,
|
|
159
|
+
// or nesting inside a NAMED group/tab breaks it — that's the case this
|
|
160
|
+
// gate exists to catch instead of crashing init with InvalidFieldJoin.
|
|
161
|
+
const resourceServicesTarget = getFieldByPath({
|
|
162
|
+
// A throwaway copy: flattenAllFields caches by array identity even when
|
|
163
|
+
// `cache` isn't passed, and sanitizeFields mutates-and-returns the same
|
|
164
|
+
// array instance. Flattening resourcesCollection.fields directly would
|
|
165
|
+
// seed that cache with a pre-sanitization snapshot under a key Payload's
|
|
166
|
+
// own sanitizeJoinField (cache: true) could later be served from.
|
|
167
|
+
fields: flattenAllFields({
|
|
168
|
+
fields: [
|
|
169
|
+
...resourcesCollection.fields
|
|
170
|
+
]
|
|
171
|
+
}),
|
|
172
|
+
path: 'services'
|
|
173
|
+
});
|
|
174
|
+
resolved.hasResourceServicesField = resourceServicesTarget !== null && (resourceServicesTarget.field.type === 'relationship' || resourceServicesTarget.field.type === 'upload');
|
|
175
|
+
config.collections.push(applyCollectionOverride(createServicesCollection(resolved), ov.services), resourcesCollection, applyCollectionOverride(createSchedulesCollection(resolved), ov.schedules), applyCollectionOverride(createReservationsCollection(resolved), ov.reservations), // The customers override applies only in standalone mode; in userCollection
|
|
129
176
|
// mode the host owns that collection and can edit it directly.
|
|
130
177
|
...resolved.userCollection ? [] : [
|
|
131
178
|
applyCollectionOverride(createCustomersCollection(resolved), ov.customers)
|
|
132
|
-
]
|
|
179
|
+
], // Only when slot holds are enabled — an install that never opts in gets no
|
|
180
|
+
// extra collection and no schema change.
|
|
181
|
+
...resolved.slotHolds.enabled ? [
|
|
182
|
+
createHoldsCollection(resolved)
|
|
183
|
+
] : []);
|
|
133
184
|
// C3: collections are registered (above) even when disabled so the DB schema
|
|
134
185
|
// stays stable; behavior (hooks, endpoints, admin, provisioning) is inert.
|
|
135
186
|
if (resolved.disabled) {
|
|
@@ -141,11 +192,136 @@ export const payloadReserve = (pluginOptions = {})=>(config)=>{
|
|
|
141
192
|
}
|
|
142
193
|
return config;
|
|
143
194
|
}
|
|
195
|
+
// Boot diagnostics. ONE wrapper for all of them so a second condition can
|
|
196
|
+
// never double-wrap onInit. Each check is evaluated at init, not here:
|
|
197
|
+
// payloadReserve runs BEFORE multi-tenant, so at plugin time no tenant
|
|
198
|
+
// field exists on anything yet.
|
|
199
|
+
const hostOnInit = config.onInit;
|
|
200
|
+
config.onInit = async (payload)=>{
|
|
201
|
+
await hostOnInit?.(payload);
|
|
202
|
+
try {
|
|
203
|
+
// The Services `resources` join is omitted when a collectionOverrides.resources
|
|
204
|
+
// override removed, renamed, or nested `services` inside a NAMED group/tab.
|
|
205
|
+
// hasResourceServicesField is only ever false because of such an override —
|
|
206
|
+
// the default Resources collection always carries the field — so this cannot
|
|
207
|
+
// fire spuriously. Warn rather than throw: restructuring your own collection
|
|
208
|
+
// is a supported customization, not invalid config.
|
|
209
|
+
if (!resolved.hasResourceServicesField) {
|
|
210
|
+
payload.logger.warn(`payload-reserve: the "${resolved.slugs.services}" collection's read-only "resources" join was skipped because "${resolved.slugs.resources}.services" is not a top-level relationship field. A collectionOverrides.resources override removed, renamed, or nested it inside a named group or tab.`);
|
|
211
|
+
}
|
|
212
|
+
// Multi-tenant only scopes collections listed in ITS OWN `collections`
|
|
213
|
+
// option, and stays silent about slugs you never listed. So a consumer
|
|
214
|
+
// can enable multi-tenant, forget these collections, and get no signal
|
|
215
|
+
// while every booking stays globally readable.
|
|
216
|
+
//
|
|
217
|
+
// Detecting "multi-tenant is enabled" is a HEURISTIC — the plugin
|
|
218
|
+
// exposes nothing at init to check directly — so we look for two
|
|
219
|
+
// independent traces of it and warn if EITHER shows up. Each covers the
|
|
220
|
+
// other's blind spot:
|
|
221
|
+
// 1. some collection carries a top-level `tenantField`. Misses the
|
|
222
|
+
// consumer who scoped ONLY this plugin's collections and forgot
|
|
223
|
+
// every one of them, leaving nothing else with the field.
|
|
224
|
+
// 2. an auth collection carries multi-tenant's `tenants` membership
|
|
225
|
+
// array. Misses installs that set
|
|
226
|
+
// `tenantsArrayField.includeDefaultField: false` or renamed it.
|
|
227
|
+
// Neither is exact, and the union can still fire on a config that
|
|
228
|
+
// merely looks tenant-shaped — hence a warning, never a throw.
|
|
229
|
+
const tenantField = resolved.multiTenant.tenantField;
|
|
230
|
+
const all = payload.config.collections ?? [];
|
|
231
|
+
const scoped = all.filter((c)=>collectionHasTenantField(c, tenantField));
|
|
232
|
+
const hasMembershipArray = all.some((c)=>Boolean(c.auth) && collectionHasArrayField(c, MT_TENANTS_ARRAY_FIELD));
|
|
233
|
+
if (scoped.length > 0 || hasMembershipArray) {
|
|
234
|
+
const candidates = [
|
|
235
|
+
resolved.slugs.reservations,
|
|
236
|
+
resolved.slugs.resources,
|
|
237
|
+
resolved.slugs.schedules,
|
|
238
|
+
resolved.slugs.services
|
|
239
|
+
];
|
|
240
|
+
// Standalone mode only. `customers` is then a plugin-generated auth
|
|
241
|
+
// collection holding the PII that customer-search reads, so leaving it
|
|
242
|
+
// unscoped leaks every tenant's customers — the case most worth
|
|
243
|
+
// catching. Under `userCollection` that slug points at the HOST's auth
|
|
244
|
+
// collection, which multi-tenant scopes via the `tenants` membership
|
|
245
|
+
// array rather than a flat field, so checking it would always
|
|
246
|
+
// false-positive.
|
|
247
|
+
if (!resolved.userCollection) {
|
|
248
|
+
candidates.push(resolved.slugs.customers);
|
|
249
|
+
}
|
|
250
|
+
// Only exists when slot holds are opted in. A hold row carries a
|
|
251
|
+
// bearer `token`: unscoped, a tenant-A staff account with Local-API
|
|
252
|
+
// reach (or any consumer code that reads holds) sees tenant B's
|
|
253
|
+
// tokens, and a token is enough to release B's hold or book its slot.
|
|
254
|
+
if (resolved.slotHolds.enabled) {
|
|
255
|
+
candidates.push(resolved.slugs.holds);
|
|
256
|
+
}
|
|
257
|
+
const unscoped = candidates.filter((slug)=>{
|
|
258
|
+
const collection = all.find((c)=>c.slug === slug);
|
|
259
|
+
return collection && !collectionHasTenantField(collection, tenantField);
|
|
260
|
+
});
|
|
261
|
+
if (unscoped.length > 0) {
|
|
262
|
+
payload.logger.warn(`payload-reserve: these collections are NOT tenant-scoped: ${unscoped.join(', ')}. This config looks like it enables multi-tenancy (another collection carries a "${tenantField}" field, or an auth collection carries a "${MT_TENANTS_ARRAY_FIELD}" membership array) — detection is a heuristic, so disregard this if you are not using multi-tenancy. Otherwise add these slugs to the multi-tenant plugin's "collections" option, or their documents stay readable across tenants.`);
|
|
263
|
+
}
|
|
264
|
+
// createBooking's tenant-membership probe (callerMayUseTenant, in
|
|
265
|
+
// src/utilities/tenantTimezone.ts) is a real membership check only
|
|
266
|
+
// when the caller authenticates against the SAME collection
|
|
267
|
+
// multi-tenant wraps as its admin/tenant-owning collection —
|
|
268
|
+
// `withTenantAccess` only applies its membership constraint under
|
|
269
|
+
// that condition. In standalone mode a customer authenticates
|
|
270
|
+
// against `slugs.customers`, a collection multi-tenant never wraps,
|
|
271
|
+
// so the probe's tenants-collection read passes for ANY tenant id
|
|
272
|
+
// for a logged-in customer: this is detectable (unlike a host
|
|
273
|
+
// setting multi-tenant's own `useTenantsCollectionAccess: false`,
|
|
274
|
+
// which is not observable from here), so warn about it.
|
|
275
|
+
if (!resolved.userCollection) {
|
|
276
|
+
payload.logger.warn(`payload-reserve: in standalone mode (no "userCollection"), /reserve/book's tenant-membership check may not actually verify a logged-in customer's membership — customers authenticate against "${resolved.slugs.customers}", a collection multi-tenant never wraps, so its probe read passes for any tenant id. Detection is the same heuristic as the warning above (a top-level "${tenantField}" field is matched by NAME, not by type or relationship target, and the check cannot see whether "${resolved.slugs.customers}" is itself multi-tenant's admin/tenant-owning collection), so disregard this if you are not using multi-tenancy. Otherwise a malicious customer could supply an explicit foreign tenant: set "userCollection" to point at multi-tenant's own admin auth collection, or add your own tenant-membership check in front of /reserve/book.`);
|
|
277
|
+
}
|
|
278
|
+
// userCollection mode: the host's auth collection is where customer PII
|
|
279
|
+
// lives, and customerSearch reads it. Multi-tenant scopes it via the
|
|
280
|
+
// `tenants` membership ARRAY, so the flat-field check above deliberately
|
|
281
|
+
// skips it — but an install with neither the array nor the flat field is
|
|
282
|
+
// genuinely unscoped and would otherwise get no signal whatsoever.
|
|
283
|
+
if (resolved.userCollection) {
|
|
284
|
+
const userCollection = all.find((c)=>c.slug === resolved.slugs.customers);
|
|
285
|
+
const scopedByArray = userCollection && collectionHasArrayField(userCollection, MT_TENANTS_ARRAY_FIELD);
|
|
286
|
+
const scopedByField = userCollection && collectionHasTenantField(userCollection, tenantField);
|
|
287
|
+
if (userCollection && !scopedByArray && !scopedByField) {
|
|
288
|
+
// The remedy is NOT "add it to multi-tenant's collections option" —
|
|
289
|
+
// confirmed against @payloadcms/plugin-multi-tenant's own source
|
|
290
|
+
// (index.ts): it resolves ONE "admin users" collection (config.admin.user,
|
|
291
|
+
// else the first auth-enabled collection) and pushes the tenants ARRAY
|
|
292
|
+
// onto that one automatically; listing THAT SAME collection in
|
|
293
|
+
// `collections` is actively rejected — multi-tenant logs its own
|
|
294
|
+
// console.warn and skips tenant-field processing for it entirely. Only a
|
|
295
|
+
// SEPARATE auth collection from the admin one can be scoped via
|
|
296
|
+
// `collections` (it gets an ordinary flat tenant field there). This
|
|
297
|
+
// plugin cannot tell which case applies from here, so the message
|
|
298
|
+
// explains both rather than asserting the wrong one.
|
|
299
|
+
payload.logger.warn(`payload-reserve: the "${resolved.slugs.customers}" collection backs userCollection mode and holds the customer PII that /api/reservation-customer-search reads, but it carries neither a "${tenantField}" field nor a "${MT_TENANTS_ARRAY_FIELD}" membership array. Detection is a heuristic, so disregard this if you are not using multi-tenancy, or if you renamed the tenants array field, or set "tenantsArrayField.includeDefaultField: false" — both are supported multi-tenant configurations this check cannot see. Otherwise: multi-tenant does NOT scope an auth collection by listing it in its "collections" option — it rejects that with its own warning and skips the collection entirely. If this is the collection multi-tenant treats as its admin/tenant-owning collection ("admin.user" in your Payload config, or your only auth-enabled collection), it needs the "${MT_TENANTS_ARRAY_FIELD}" array instead, which multi-tenant adds automatically unless disabled. Only a SEPARATE auth collection from that one belongs in the "collections" option, where it gets an ordinary "${tenantField}" field.`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// The booking lock only serializes anything inside a transaction. On a
|
|
304
|
+
// standalone mongod Payload skips transactions entirely, so the lock
|
|
305
|
+
// degrades to a no-op and concurrent bookings double-book again — with
|
|
306
|
+
// no error anywhere. Say so at boot; there is no runtime signal.
|
|
307
|
+
if (!resolved.disabled && !await supportsTransactions(payload)) {
|
|
308
|
+
payload.logger.warn('payload-reserve: this database does not support transactions, so concurrent bookings for the same slot can double-book. MongoDB needs a replica set (even single-node) for transaction support. Postgres supports transactions by default. SQLite requires transactionOptions to be set on the adapter, or it silently runs without them.');
|
|
309
|
+
}
|
|
310
|
+
} catch {
|
|
311
|
+
// A diagnostic must never break boot — that is the whole reason these
|
|
312
|
+
// are warnings and not throws. The host onInit await stays OUTSIDE.
|
|
313
|
+
}
|
|
314
|
+
};
|
|
144
315
|
// Register custom endpoints
|
|
145
316
|
if (!config.endpoints) {
|
|
146
317
|
config.endpoints = [];
|
|
147
318
|
}
|
|
148
|
-
config.endpoints.push(createCancelBookingEndpoint(resolved), createCheckAvailabilityEndpoint(resolved), createBookingEndpoint(resolved), createCustomerSearchEndpoint(resolved), createEffectiveTimezoneEndpoint(resolved), createGetSlotsEndpoint(resolved), createResourceAvailabilityEndpoint(resolved)
|
|
319
|
+
config.endpoints.push(createCancelBookingEndpoint(resolved), createCheckAvailabilityEndpoint(resolved), createBookingEndpoint(resolved), createCustomerSearchEndpoint(resolved), createEffectiveTimezoneEndpoint(resolved), createGetSlotsEndpoint(resolved), createResourceAvailabilityEndpoint(resolved), // Only when slot holds are enabled — an install that never opts in gets
|
|
320
|
+
// no new routes.
|
|
321
|
+
...resolved.slotHolds.enabled ? [
|
|
322
|
+
createHoldSlotEndpoint(resolved),
|
|
323
|
+
createReleaseSlotEndpoint(resolved)
|
|
324
|
+
] : []);
|
|
149
325
|
// Wire staff auto-provisioning onto the staff user collection
|
|
150
326
|
if (resolved.staffProvisioning) {
|
|
151
327
|
const staffUserSlug = resolved.staffProvisioning.userCollection;
|
package/dist/plugin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { CollectionSlug, Config, Field } from 'payload'\n\nimport { deepMergeSimple } from 'payload/shared'\n\nimport type { ReservationPluginConfig } from './types.js'\n\nimport { createCustomersCollection } from './collections/Customers.js'\nimport { createReservationsCollection } from './collections/Reservations.js'\nimport { createResourcesCollection } from './collections/Resources.js'\nimport { createSchedulesCollection } from './collections/Schedules.js'\nimport { createServicesCollection } from './collections/Services.js'\nimport { resolveConfig } from './defaults.js'\nimport { createCancelBookingEndpoint } from './endpoints/cancelBooking.js'\nimport { createCheckAvailabilityEndpoint } from './endpoints/checkAvailability.js'\nimport { createBookingEndpoint } from './endpoints/createBooking.js'\nimport { createCustomerSearchEndpoint } from './endpoints/customerSearch.js'\nimport { createEffectiveTimezoneEndpoint } from './endpoints/effectiveTimezone.js'\nimport { createGetSlotsEndpoint } from './endpoints/getSlots.js'\nimport { createResourceAvailabilityEndpoint } from './endpoints/resourceAvailability.js'\nimport { provisionStaffResource } from './hooks/users/provisionStaffResource.js'\nimport { type PluginT, translations } from './translations/index.js'\nimport { applyCollectionOverride } from './utilities/collectionOverrides.js'\n\n/**\n * All named field paths reachable from a field list, descending through\n * presentational containers (tabs, rows, collapsibles, unnamed groups) that\n * don't create their own data nesting — so dedup catches a field declared\n * inside one of them. Named groups/arrays DO nest data, so we don't recurse\n * into them (a `name` inside a named group is a different path).\n */\nfunction collectFieldNames(fields: Field[]): Set<string> {\n const names = new Set<string>()\n const walk = (list: Field[]): void => {\n for (const field of list) {\n if ('name' in field && field.name) {\n names.add(field.name)\n } else if ('tabs' in field && Array.isArray(field.tabs)) {\n for (const tab of field.tabs) {\n if ('name' in tab && tab.name) {\n names.add(tab.name)\n } else if (Array.isArray(tab.fields)) {\n walk(tab.fields)\n }\n }\n } else if ('fields' in field && Array.isArray(field.fields)) {\n // row / collapsible / unnamed group\n walk(field.fields)\n }\n }\n }\n walk(fields)\n return names\n}\n\nexport const payloadReserve =\n (pluginOptions: ReservationPluginConfig = {}) =>\n (config: Config): Config => {\n const resolved = resolveConfig(pluginOptions)\n\n // Detect localization from the Payload config\n if (config.localization) {\n resolved.localized = true\n }\n\n if (!config.collections) {\n config.collections = []\n }\n\n if (resolved.userCollection) {\n // Extend the existing auth collection with customer fields\n const targetCollection = config.collections.find(\n (col) => col.slug === resolved.userCollection,\n )\n\n if (!targetCollection) {\n // Fail loudly rather than silently skipping field injection and pointing\n // the customers slug at a collection that doesn't exist (review C2).\n throw new Error(\n `payload-reserve: userCollection \"${resolved.userCollection}\" was not found in config.collections. ` +\n `Define it before payloadReserve() runs, or correct the slug.`,\n )\n }\n\n {\n // Collect existing field names — descend into presentational containers\n // (tabs/rows/collapsibles/groups) so a field nested there isn't\n // re-injected at the top level (review C4).\n const existingFieldNames = collectFieldNames(targetCollection.fields)\n\n // Fields to inject if not already present. `name` is added so that\n // admin.useAsTitle: 'name' works out of the box on the extended user\n // collection (matches the v1.0.0 behaviour documented in README/SKILL).\n // It is NOT required — an existing users collection may have rows\n // without a name, and forcing required would fail their next update (C4).\n const fieldsToAdd: Field[] = [\n {\n name: 'name',\n type: 'text',\n maxLength: 200,\n },\n {\n name: 'phone',\n type: 'text',\n maxLength: 50,\n },\n {\n name: 'notes',\n type: 'textarea',\n },\n {\n name: 'bookings',\n type: 'join',\n collection: resolved.slugs.reservations as unknown as CollectionSlug,\n on: 'customer',\n },\n ]\n\n for (const field of fieldsToAdd) {\n const fieldName = 'name' in field ? field.name : undefined\n if (fieldName && !existingFieldNames.has(fieldName)) {\n targetCollection.fields.push(field)\n }\n }\n }\n\n // Point the customers slug at the user collection so other parts of the\n // plugin (endpoints, hooks) reference the correct collection\n resolved.slugs.customers = resolved.userCollection\n }\n\n // The slugs this plugin is about to register (Customers only in standalone mode)\n const slugsToRegister = [\n resolved.slugs.services,\n resolved.slugs.resources,\n resolved.slugs.schedules,\n resolved.slugs.reservations,\n ...(resolved.userCollection ? [] : [resolved.slugs.customers]),\n ]\n\n // C11: fail with a clear, actionable error on slug collision instead of\n // Payload's generic DuplicateCollection throw.\n for (const slug of slugsToRegister) {\n if (config.collections.some((col) => col.slug === slug)) {\n throw new Error(\n `payload-reserve: a collection with slug \"${slug}\" already exists. ` +\n `Override the plugin's slug via the \\`slugs\\` option.`,\n )\n }\n }\n\n // Image upload fields are added only when the media collection actually\n // exists, so installs without one don't hit an opaque init error (C8).\n resolved.hasMediaCollection = config.collections.some(\n (col) => col.slug === resolved.slugs.media,\n )\n\n const ov = resolved.collectionOverrides\n config.collections.push(\n applyCollectionOverride(createServicesCollection(resolved), ov.services),\n applyCollectionOverride(createResourcesCollection(resolved), ov.resources),\n applyCollectionOverride(createSchedulesCollection(resolved), ov.schedules),\n applyCollectionOverride(createReservationsCollection(resolved), ov.reservations),\n // The customers override applies only in standalone mode; in userCollection\n // mode the host owns that collection and can edit it directly.\n ...(resolved.userCollection\n ? []\n : [applyCollectionOverride(createCustomersCollection(resolved), ov.customers)]),\n )\n\n // C3: collections are registered (above) even when disabled so the DB schema\n // stays stable; behavior (hooks, endpoints, admin, provisioning) is inert.\n if (resolved.disabled) {\n for (const slug of slugsToRegister) {\n const col = config.collections.find((c) => c.slug === slug)\n if (col) {\n delete col.hooks\n }\n }\n return config\n }\n\n // Register custom endpoints\n if (!config.endpoints) {config.endpoints = []}\n config.endpoints.push(\n createCancelBookingEndpoint(resolved),\n createCheckAvailabilityEndpoint(resolved),\n createBookingEndpoint(resolved),\n createCustomerSearchEndpoint(resolved),\n createEffectiveTimezoneEndpoint(resolved),\n createGetSlotsEndpoint(resolved),\n createResourceAvailabilityEndpoint(resolved),\n )\n\n // Wire staff auto-provisioning onto the staff user collection\n if (resolved.staffProvisioning) {\n const staffUserSlug = resolved.staffProvisioning.userCollection\n const staffCollection = config.collections.find((col) => col.slug === staffUserSlug)\n if (!staffCollection) {\n throw new Error(\n `staffProvisioning.userCollection \"${staffUserSlug}\" was not found in config.collections`,\n )\n }\n staffCollection.hooks = {\n ...staffCollection.hooks,\n afterChange: [\n ...(staffCollection.hooks?.afterChange ?? []),\n provisionStaffResource(resolved),\n ],\n }\n }\n\n // Set up admin configuration\n if (!config.admin) {config.admin = {}}\n if (!config.admin.components) {config.admin.components = {}}\n\n // Store slugs and status machine in admin custom for component access\n if (!config.admin.custom) {config.admin.custom = {}}\n config.admin.custom.reservationSlugs = {\n ...resolved.slugs,\n }\n config.admin.custom.reservationStatusMachine = resolved.statusMachine\n config.admin.custom.reservationTenant = resolved.multiTenant\n config.admin.custom.reservationTimezone = resolved.timezone\n\n // Add dashboard widget\n if (!config.admin.dashboard) {\n config.admin.dashboard = { widgets: [] }\n }\n if (!config.admin.dashboard.widgets) {\n config.admin.dashboard.widgets = []\n }\n config.admin.dashboard.widgets.push({\n slug: 'reservation-todays-reservations',\n Component: 'payload-reserve/rsc#DashboardWidgetServer',\n label: ({ t }) => (t as PluginT)('reservation:dashboardTitle'),\n maxWidth: 'large',\n minWidth: 'medium',\n })\n\n // Add availability overview as custom admin view\n if (!config.admin.components.views) {\n config.admin.components.views = {}\n }\n ;(config.admin.components.views as Record<string, unknown>)['reservation-availability'] = {\n Component: 'payload-reserve/client#AvailabilityOverview',\n path: '/reservation-availability',\n }\n\n // Merge plugin translations (user translations take precedence)\n config.i18n = {\n ...(config.i18n ?? {}),\n translations: deepMergeSimple(\n translations,\n (config.i18n?.translations as Record<string, Record<string, unknown>>) ?? {},\n ),\n }\n\n return config\n }\n"],"names":["deepMergeSimple","createCustomersCollection","createReservationsCollection","createResourcesCollection","createSchedulesCollection","createServicesCollection","resolveConfig","createCancelBookingEndpoint","createCheckAvailabilityEndpoint","createBookingEndpoint","createCustomerSearchEndpoint","createEffectiveTimezoneEndpoint","createGetSlotsEndpoint","createResourceAvailabilityEndpoint","provisionStaffResource","translations","applyCollectionOverride","collectFieldNames","fields","names","Set","walk","list","field","name","add","Array","isArray","tabs","tab","payloadReserve","pluginOptions","config","resolved","localization","localized","collections","userCollection","targetCollection","find","col","slug","Error","existingFieldNames","fieldsToAdd","type","maxLength","collection","slugs","reservations","on","fieldName","undefined","has","push","customers","slugsToRegister","services","resources","schedules","some","hasMediaCollection","media","ov","collectionOverrides","disabled","c","hooks","endpoints","staffProvisioning","staffUserSlug","staffCollection","afterChange","admin","components","custom","reservationSlugs","reservationStatusMachine","statusMachine","reservationTenant","multiTenant","reservationTimezone","timezone","dashboard","widgets","Component","label","t","maxWidth","minWidth","views","path","i18n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../src/plugin.ts"],"sourcesContent":["import type { CollectionSlug, Config, Field } from 'payload'\n\nimport { flattenAllFields, getFieldByPath } from 'payload'\nimport { deepMergeSimple } from 'payload/shared'\n\nimport type { ReservationPluginConfig } from './types.js'\n\nimport { createCustomersCollection } from './collections/Customers.js'\nimport { createHoldsCollection } from './collections/Holds.js'\nimport { createReservationsCollection } from './collections/Reservations.js'\nimport { createResourcesCollection } from './collections/Resources.js'\nimport { createSchedulesCollection } from './collections/Schedules.js'\nimport { createServicesCollection } from './collections/Services.js'\nimport { resolveConfig } from './defaults.js'\nimport { createCancelBookingEndpoint } from './endpoints/cancelBooking.js'\nimport { createCheckAvailabilityEndpoint } from './endpoints/checkAvailability.js'\nimport { createBookingEndpoint } from './endpoints/createBooking.js'\nimport { createCustomerSearchEndpoint } from './endpoints/customerSearch.js'\nimport { createEffectiveTimezoneEndpoint } from './endpoints/effectiveTimezone.js'\nimport { createGetSlotsEndpoint } from './endpoints/getSlots.js'\nimport { createHoldSlotEndpoint } from './endpoints/holdSlot.js'\nimport { createReleaseSlotEndpoint } from './endpoints/releaseSlot.js'\nimport { createResourceAvailabilityEndpoint } from './endpoints/resourceAvailability.js'\nimport { provisionStaffResource } from './hooks/users/provisionStaffResource.js'\nimport { type PluginT, translations } from './translations/index.js'\nimport { applyCollectionOverride } from './utilities/collectionOverrides.js'\nimport { collectionHasTenantField } from './utilities/tenantFilter.js'\nimport { supportsTransactions } from './utilities/transactionSupport.js'\n\n/**\n * The array field `@payloadcms/plugin-multi-tenant` pushes onto the admin users\n * collection to hold a user's tenant memberships. Its own default\n * (`plugin-multi-tenant/dist/defaults.js`); consumers can rename it, which is\n * one of the blind spots the boot diagnostic's comment calls out.\n */\nconst MT_TENANTS_ARRAY_FIELD = 'tenants'\n\n/** True if the collection has a top-level `array` field named `fieldName`. */\nfunction collectionHasArrayField(\n collection: { fields?: unknown[] } | null | undefined,\n fieldName: string,\n): boolean {\n if (!collection || !Array.isArray(collection.fields)) {\n return false\n }\n return collection.fields.some((f) => {\n if (typeof f !== 'object' || f === null) {\n return false\n }\n const field = f as { name?: string; type?: string }\n return field.name === fieldName && field.type === 'array'\n })\n}\n\n/**\n * All named field paths reachable from a field list, descending through\n * presentational containers (tabs, rows, collapsibles, unnamed groups) that\n * don't create their own data nesting — so dedup catches a field declared\n * inside one of them. Named groups/arrays DO nest data, so we don't recurse\n * into them (a `name` inside a named group is a different path).\n */\nfunction collectFieldNames(fields: Field[]): Set<string> {\n const names = new Set<string>()\n const walk = (list: Field[]): void => {\n for (const field of list) {\n if ('name' in field && field.name) {\n names.add(field.name)\n } else if ('tabs' in field && Array.isArray(field.tabs)) {\n for (const tab of field.tabs) {\n if ('name' in tab && tab.name) {\n names.add(tab.name)\n } else if (Array.isArray(tab.fields)) {\n walk(tab.fields)\n }\n }\n } else if ('fields' in field && Array.isArray(field.fields)) {\n // row / collapsible / unnamed group\n walk(field.fields)\n }\n }\n }\n walk(fields)\n return names\n}\n\nexport const payloadReserve =\n (pluginOptions: ReservationPluginConfig = {}) =>\n (config: Config): Config => {\n const resolved = resolveConfig(pluginOptions)\n\n // Detect localization from the Payload config\n if (config.localization) {\n resolved.localized = true\n }\n\n if (!config.collections) {\n config.collections = []\n }\n\n if (resolved.userCollection) {\n // Extend the existing auth collection with customer fields\n const targetCollection = config.collections.find(\n (col) => col.slug === resolved.userCollection,\n )\n\n if (!targetCollection) {\n // Fail loudly rather than silently skipping field injection and pointing\n // the customers slug at a collection that doesn't exist (review C2).\n throw new Error(\n `payload-reserve: userCollection \"${resolved.userCollection}\" was not found in config.collections. ` +\n `Define it before payloadReserve() runs, or correct the slug.`,\n )\n }\n\n {\n // Collect existing field names — descend into presentational containers\n // (tabs/rows/collapsibles/groups) so a field nested there isn't\n // re-injected at the top level (review C4).\n const existingFieldNames = collectFieldNames(targetCollection.fields)\n\n // Fields to inject if not already present. `name` is added so that\n // admin.useAsTitle: 'name' works out of the box on the extended user\n // collection (matches the v1.0.0 behaviour documented in README/SKILL).\n // It is NOT required — an existing users collection may have rows\n // without a name, and forcing required would fail their next update (C4).\n const fieldsToAdd: Field[] = [\n {\n name: 'name',\n type: 'text',\n maxLength: 200,\n },\n {\n name: 'phone',\n type: 'text',\n maxLength: 50,\n },\n {\n name: 'notes',\n type: 'textarea',\n },\n {\n name: 'bookings',\n type: 'join',\n collection: resolved.slugs.reservations as unknown as CollectionSlug,\n on: 'customer',\n },\n ]\n\n for (const field of fieldsToAdd) {\n const fieldName = 'name' in field ? field.name : undefined\n if (fieldName && !existingFieldNames.has(fieldName)) {\n targetCollection.fields.push(field)\n }\n }\n }\n\n // Point the customers slug at the user collection so other parts of the\n // plugin (endpoints, hooks) reference the correct collection\n resolved.slugs.customers = resolved.userCollection\n }\n\n // The slugs this plugin is about to register (Customers only in standalone mode)\n const slugsToRegister = [\n resolved.slugs.services,\n resolved.slugs.resources,\n resolved.slugs.schedules,\n resolved.slugs.reservations,\n ...(resolved.userCollection ? [] : [resolved.slugs.customers]),\n ]\n\n // C11: fail with a clear, actionable error on slug collision instead of\n // Payload's generic DuplicateCollection throw.\n for (const slug of slugsToRegister) {\n if (config.collections.some((col) => col.slug === slug)) {\n throw new Error(\n `payload-reserve: a collection with slug \"${slug}\" already exists. ` +\n `Override the plugin's slug via the \\`slugs\\` option.`,\n )\n }\n }\n\n // Image upload fields are added only when the media collection actually\n // exists, so installs without one don't hit an opaque init error (C8).\n resolved.hasMediaCollection = config.collections.some(\n (col) => col.slug === resolved.slugs.media,\n )\n\n const ov = resolved.collectionOverrides\n const resourcesCollection = applyCollectionOverride(\n createResourcesCollection(resolved),\n ov.resources,\n )\n // The join's `on: 'services'` only resolves when Payload's own\n // sanitizeJoinField can resolve it — mirror that resolution exactly\n // (flattenAllFields + getFieldByPath) rather than re-implementing the\n // traversal, so this can't drift from Payload's behavior on upgrade.\n // Unnamed containers (row/collapsible/unnamed group/unnamed tabs) are\n // hoisted by flattenAllFields and still resolve; only removal, renaming,\n // or nesting inside a NAMED group/tab breaks it — that's the case this\n // gate exists to catch instead of crashing init with InvalidFieldJoin.\n const resourceServicesTarget = getFieldByPath({\n // A throwaway copy: flattenAllFields caches by array identity even when\n // `cache` isn't passed, and sanitizeFields mutates-and-returns the same\n // array instance. Flattening resourcesCollection.fields directly would\n // seed that cache with a pre-sanitization snapshot under a key Payload's\n // own sanitizeJoinField (cache: true) could later be served from.\n fields: flattenAllFields({ fields: [...resourcesCollection.fields] }),\n path: 'services',\n })\n resolved.hasResourceServicesField =\n resourceServicesTarget !== null &&\n (resourceServicesTarget.field.type === 'relationship' ||\n resourceServicesTarget.field.type === 'upload')\n\n config.collections.push(\n applyCollectionOverride(createServicesCollection(resolved), ov.services),\n resourcesCollection,\n applyCollectionOverride(createSchedulesCollection(resolved), ov.schedules),\n applyCollectionOverride(createReservationsCollection(resolved), ov.reservations),\n // The customers override applies only in standalone mode; in userCollection\n // mode the host owns that collection and can edit it directly.\n ...(resolved.userCollection\n ? []\n : [applyCollectionOverride(createCustomersCollection(resolved), ov.customers)]),\n // Only when slot holds are enabled — an install that never opts in gets no\n // extra collection and no schema change.\n ...(resolved.slotHolds.enabled ? [createHoldsCollection(resolved)] : []),\n )\n\n // C3: collections are registered (above) even when disabled so the DB schema\n // stays stable; behavior (hooks, endpoints, admin, provisioning) is inert.\n if (resolved.disabled) {\n for (const slug of slugsToRegister) {\n const col = config.collections.find((c) => c.slug === slug)\n if (col) {\n delete col.hooks\n }\n }\n return config\n }\n\n // Boot diagnostics. ONE wrapper for all of them so a second condition can\n // never double-wrap onInit. Each check is evaluated at init, not here:\n // payloadReserve runs BEFORE multi-tenant, so at plugin time no tenant\n // field exists on anything yet.\n const hostOnInit = config.onInit\n config.onInit = async (payload) => {\n await hostOnInit?.(payload)\n try {\n // The Services `resources` join is omitted when a collectionOverrides.resources\n // override removed, renamed, or nested `services` inside a NAMED group/tab.\n // hasResourceServicesField is only ever false because of such an override —\n // the default Resources collection always carries the field — so this cannot\n // fire spuriously. Warn rather than throw: restructuring your own collection\n // is a supported customization, not invalid config.\n if (!resolved.hasResourceServicesField) {\n payload.logger.warn(\n `payload-reserve: the \"${resolved.slugs.services}\" collection's read-only \"resources\" join was skipped because \"${resolved.slugs.resources}.services\" is not a top-level relationship field. A collectionOverrides.resources override removed, renamed, or nested it inside a named group or tab.`,\n )\n }\n\n // Multi-tenant only scopes collections listed in ITS OWN `collections`\n // option, and stays silent about slugs you never listed. So a consumer\n // can enable multi-tenant, forget these collections, and get no signal\n // while every booking stays globally readable.\n //\n // Detecting \"multi-tenant is enabled\" is a HEURISTIC — the plugin\n // exposes nothing at init to check directly — so we look for two\n // independent traces of it and warn if EITHER shows up. Each covers the\n // other's blind spot:\n // 1. some collection carries a top-level `tenantField`. Misses the\n // consumer who scoped ONLY this plugin's collections and forgot\n // every one of them, leaving nothing else with the field.\n // 2. an auth collection carries multi-tenant's `tenants` membership\n // array. Misses installs that set\n // `tenantsArrayField.includeDefaultField: false` or renamed it.\n // Neither is exact, and the union can still fire on a config that\n // merely looks tenant-shaped — hence a warning, never a throw.\n const tenantField = resolved.multiTenant.tenantField\n const all = (payload.config.collections ?? []) as Array<{\n auth?: unknown\n fields?: unknown[]\n slug: string\n }>\n const scoped = all.filter((c) => collectionHasTenantField(c, tenantField))\n const hasMembershipArray = all.some(\n (c) => Boolean(c.auth) && collectionHasArrayField(c, MT_TENANTS_ARRAY_FIELD),\n )\n if (scoped.length > 0 || hasMembershipArray) {\n const candidates = [\n resolved.slugs.reservations,\n resolved.slugs.resources,\n resolved.slugs.schedules,\n resolved.slugs.services,\n ]\n // Standalone mode only. `customers` is then a plugin-generated auth\n // collection holding the PII that customer-search reads, so leaving it\n // unscoped leaks every tenant's customers — the case most worth\n // catching. Under `userCollection` that slug points at the HOST's auth\n // collection, which multi-tenant scopes via the `tenants` membership\n // array rather than a flat field, so checking it would always\n // false-positive.\n if (!resolved.userCollection) {\n candidates.push(resolved.slugs.customers)\n }\n // Only exists when slot holds are opted in. A hold row carries a\n // bearer `token`: unscoped, a tenant-A staff account with Local-API\n // reach (or any consumer code that reads holds) sees tenant B's\n // tokens, and a token is enough to release B's hold or book its slot.\n if (resolved.slotHolds.enabled) {\n candidates.push(resolved.slugs.holds)\n }\n const unscoped = candidates.filter((slug) => {\n const collection = all.find((c) => c.slug === slug)\n return collection && !collectionHasTenantField(collection, tenantField)\n })\n if (unscoped.length > 0) {\n payload.logger.warn(\n `payload-reserve: these collections are NOT tenant-scoped: ${unscoped.join(', ')}. This config looks like it enables multi-tenancy (another collection carries a \"${tenantField}\" field, or an auth collection carries a \"${MT_TENANTS_ARRAY_FIELD}\" membership array) — detection is a heuristic, so disregard this if you are not using multi-tenancy. Otherwise add these slugs to the multi-tenant plugin's \"collections\" option, or their documents stay readable across tenants.`,\n )\n }\n\n // createBooking's tenant-membership probe (callerMayUseTenant, in\n // src/utilities/tenantTimezone.ts) is a real membership check only\n // when the caller authenticates against the SAME collection\n // multi-tenant wraps as its admin/tenant-owning collection —\n // `withTenantAccess` only applies its membership constraint under\n // that condition. In standalone mode a customer authenticates\n // against `slugs.customers`, a collection multi-tenant never wraps,\n // so the probe's tenants-collection read passes for ANY tenant id\n // for a logged-in customer: this is detectable (unlike a host\n // setting multi-tenant's own `useTenantsCollectionAccess: false`,\n // which is not observable from here), so warn about it.\n if (!resolved.userCollection) {\n payload.logger.warn(\n `payload-reserve: in standalone mode (no \"userCollection\"), /reserve/book's tenant-membership check may not actually verify a logged-in customer's membership — customers authenticate against \"${resolved.slugs.customers}\", a collection multi-tenant never wraps, so its probe read passes for any tenant id. Detection is the same heuristic as the warning above (a top-level \"${tenantField}\" field is matched by NAME, not by type or relationship target, and the check cannot see whether \"${resolved.slugs.customers}\" is itself multi-tenant's admin/tenant-owning collection), so disregard this if you are not using multi-tenancy. Otherwise a malicious customer could supply an explicit foreign tenant: set \"userCollection\" to point at multi-tenant's own admin auth collection, or add your own tenant-membership check in front of /reserve/book.`,\n )\n }\n\n // userCollection mode: the host's auth collection is where customer PII\n // lives, and customerSearch reads it. Multi-tenant scopes it via the\n // `tenants` membership ARRAY, so the flat-field check above deliberately\n // skips it — but an install with neither the array nor the flat field is\n // genuinely unscoped and would otherwise get no signal whatsoever.\n if (resolved.userCollection) {\n const userCollection = all.find((c) => c.slug === resolved.slugs.customers)\n const scopedByArray =\n userCollection && collectionHasArrayField(userCollection, MT_TENANTS_ARRAY_FIELD)\n const scopedByField =\n userCollection && collectionHasTenantField(userCollection, tenantField)\n\n if (userCollection && !scopedByArray && !scopedByField) {\n // The remedy is NOT \"add it to multi-tenant's collections option\" —\n // confirmed against @payloadcms/plugin-multi-tenant's own source\n // (index.ts): it resolves ONE \"admin users\" collection (config.admin.user,\n // else the first auth-enabled collection) and pushes the tenants ARRAY\n // onto that one automatically; listing THAT SAME collection in\n // `collections` is actively rejected — multi-tenant logs its own\n // console.warn and skips tenant-field processing for it entirely. Only a\n // SEPARATE auth collection from the admin one can be scoped via\n // `collections` (it gets an ordinary flat tenant field there). This\n // plugin cannot tell which case applies from here, so the message\n // explains both rather than asserting the wrong one.\n payload.logger.warn(\n `payload-reserve: the \"${resolved.slugs.customers}\" collection backs userCollection mode and holds the customer PII that /api/reservation-customer-search reads, but it carries neither a \"${tenantField}\" field nor a \"${MT_TENANTS_ARRAY_FIELD}\" membership array. Detection is a heuristic, so disregard this if you are not using multi-tenancy, or if you renamed the tenants array field, or set \"tenantsArrayField.includeDefaultField: false\" — both are supported multi-tenant configurations this check cannot see. Otherwise: multi-tenant does NOT scope an auth collection by listing it in its \"collections\" option — it rejects that with its own warning and skips the collection entirely. If this is the collection multi-tenant treats as its admin/tenant-owning collection (\"admin.user\" in your Payload config, or your only auth-enabled collection), it needs the \"${MT_TENANTS_ARRAY_FIELD}\" array instead, which multi-tenant adds automatically unless disabled. Only a SEPARATE auth collection from that one belongs in the \"collections\" option, where it gets an ordinary \"${tenantField}\" field.`,\n )\n }\n }\n }\n\n // The booking lock only serializes anything inside a transaction. On a\n // standalone mongod Payload skips transactions entirely, so the lock\n // degrades to a no-op and concurrent bookings double-book again — with\n // no error anywhere. Say so at boot; there is no runtime signal.\n if (!resolved.disabled && !(await supportsTransactions(payload))) {\n payload.logger.warn(\n 'payload-reserve: this database does not support transactions, so concurrent bookings for the same slot can double-book. MongoDB needs a replica set (even single-node) for transaction support. Postgres supports transactions by default. SQLite requires transactionOptions to be set on the adapter, or it silently runs without them.',\n )\n }\n } catch {\n // A diagnostic must never break boot — that is the whole reason these\n // are warnings and not throws. The host onInit await stays OUTSIDE.\n }\n }\n\n // Register custom endpoints\n if (!config.endpoints) {config.endpoints = []}\n config.endpoints.push(\n createCancelBookingEndpoint(resolved),\n createCheckAvailabilityEndpoint(resolved),\n createBookingEndpoint(resolved),\n createCustomerSearchEndpoint(resolved),\n createEffectiveTimezoneEndpoint(resolved),\n createGetSlotsEndpoint(resolved),\n createResourceAvailabilityEndpoint(resolved),\n // Only when slot holds are enabled — an install that never opts in gets\n // no new routes.\n ...(resolved.slotHolds.enabled\n ? [createHoldSlotEndpoint(resolved), createReleaseSlotEndpoint(resolved)]\n : []),\n )\n\n // Wire staff auto-provisioning onto the staff user collection\n if (resolved.staffProvisioning) {\n const staffUserSlug = resolved.staffProvisioning.userCollection\n const staffCollection = config.collections.find((col) => col.slug === staffUserSlug)\n if (!staffCollection) {\n throw new Error(\n `staffProvisioning.userCollection \"${staffUserSlug}\" was not found in config.collections`,\n )\n }\n staffCollection.hooks = {\n ...staffCollection.hooks,\n afterChange: [\n ...(staffCollection.hooks?.afterChange ?? []),\n provisionStaffResource(resolved),\n ],\n }\n }\n\n // Set up admin configuration\n if (!config.admin) {config.admin = {}}\n if (!config.admin.components) {config.admin.components = {}}\n\n // Store slugs and status machine in admin custom for component access\n if (!config.admin.custom) {config.admin.custom = {}}\n config.admin.custom.reservationSlugs = {\n ...resolved.slugs,\n }\n config.admin.custom.reservationStatusMachine = resolved.statusMachine\n config.admin.custom.reservationTenant = resolved.multiTenant\n config.admin.custom.reservationTimezone = resolved.timezone\n\n // Add dashboard widget\n if (!config.admin.dashboard) {\n config.admin.dashboard = { widgets: [] }\n }\n if (!config.admin.dashboard.widgets) {\n config.admin.dashboard.widgets = []\n }\n config.admin.dashboard.widgets.push({\n slug: 'reservation-todays-reservations',\n Component: 'payload-reserve/rsc#DashboardWidgetServer',\n label: ({ t }) => (t as PluginT)('reservation:dashboardTitle'),\n maxWidth: 'large',\n minWidth: 'medium',\n })\n\n // Add availability overview as custom admin view\n if (!config.admin.components.views) {\n config.admin.components.views = {}\n }\n ;(config.admin.components.views as Record<string, unknown>)['reservation-availability'] = {\n Component: 'payload-reserve/client#AvailabilityOverview',\n path: '/reservation-availability',\n }\n\n // Merge plugin translations (user translations take precedence)\n config.i18n = {\n ...(config.i18n ?? {}),\n translations: deepMergeSimple(\n translations,\n (config.i18n?.translations as Record<string, Record<string, unknown>>) ?? {},\n ),\n }\n\n return config\n }\n"],"names":["flattenAllFields","getFieldByPath","deepMergeSimple","createCustomersCollection","createHoldsCollection","createReservationsCollection","createResourcesCollection","createSchedulesCollection","createServicesCollection","resolveConfig","createCancelBookingEndpoint","createCheckAvailabilityEndpoint","createBookingEndpoint","createCustomerSearchEndpoint","createEffectiveTimezoneEndpoint","createGetSlotsEndpoint","createHoldSlotEndpoint","createReleaseSlotEndpoint","createResourceAvailabilityEndpoint","provisionStaffResource","translations","applyCollectionOverride","collectionHasTenantField","supportsTransactions","MT_TENANTS_ARRAY_FIELD","collectionHasArrayField","collection","fieldName","Array","isArray","fields","some","f","field","name","type","collectFieldNames","names","Set","walk","list","add","tabs","tab","payloadReserve","pluginOptions","config","resolved","localization","localized","collections","userCollection","targetCollection","find","col","slug","Error","existingFieldNames","fieldsToAdd","maxLength","slugs","reservations","on","undefined","has","push","customers","slugsToRegister","services","resources","schedules","hasMediaCollection","media","ov","collectionOverrides","resourcesCollection","resourceServicesTarget","path","hasResourceServicesField","slotHolds","enabled","disabled","c","hooks","hostOnInit","onInit","payload","logger","warn","tenantField","multiTenant","all","scoped","filter","hasMembershipArray","Boolean","auth","length","candidates","holds","unscoped","join","scopedByArray","scopedByField","endpoints","staffProvisioning","staffUserSlug","staffCollection","afterChange","admin","components","custom","reservationSlugs","reservationStatusMachine","statusMachine","reservationTenant","reservationTimezone","timezone","dashboard","widgets","Component","label","t","maxWidth","minWidth","views","i18n"],"mappings":"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"}
|
|
@@ -58,7 +58,16 @@ export declare function itemsToOccupancies(params: {
|
|
|
58
58
|
items: ResolvedItem[];
|
|
59
59
|
resourceId: number | string;
|
|
60
60
|
}): Promise<Occupancy[]>;
|
|
61
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Occupancy a single fetched reservation imposes on `resourceId`.
|
|
63
|
+
*
|
|
64
|
+
* Resolves `reservation` LENIENTLY: this is a stored document, which — unlike
|
|
65
|
+
* a write in progress — can carry an inverted top-level window from a
|
|
66
|
+
* `context.skipReservationHooks` write or data predating that check. Lenient
|
|
67
|
+
* mode skips parent synthesis instead of throwing, so a malformed top-level
|
|
68
|
+
* window never discards the real `items[]` occupancies also present on the
|
|
69
|
+
* same row (see the lenient-mode note in resolveReservationItems.ts).
|
|
70
|
+
*/
|
|
62
71
|
export declare function reservationOccupancies(params: {
|
|
63
72
|
bufferFor: (serviceId: number | string | undefined) => Promise<{
|
|
64
73
|
after: number;
|
|
@@ -80,10 +89,14 @@ export declare function checkAvailability(params: {
|
|
|
80
89
|
/** Optional tracer — emits check/check_result/error lines when enabled. */
|
|
81
90
|
debug?: ReserveDebug;
|
|
82
91
|
endTime: Date;
|
|
92
|
+
/** Token of the hold being converted into this booking — never self-blocks. */
|
|
93
|
+
excludeHoldToken?: string;
|
|
83
94
|
excludeReservationId?: number | string;
|
|
84
95
|
/** External busy resolver (calendar sync etc.) — intervals block the whole resource. */
|
|
85
96
|
getExternalBusy?: GetExternalBusy;
|
|
86
97
|
guestCount: number;
|
|
98
|
+
/** Slug of the slot-holds collection; omit to ignore holds entirely. */
|
|
99
|
+
holdsSlug?: string;
|
|
87
100
|
payload: Payload;
|
|
88
101
|
req: PayloadRequest;
|
|
89
102
|
reservationSlug: string;
|
|
@@ -99,14 +112,39 @@ export declare function checkAvailability(params: {
|
|
|
99
112
|
reason?: string;
|
|
100
113
|
totalCapacity: number;
|
|
101
114
|
}>;
|
|
115
|
+
/**
|
|
116
|
+
* Why availability came back empty. Returned alongside the slots so an empty
|
|
117
|
+
* result is diagnosable without turning on `debug`.
|
|
118
|
+
*
|
|
119
|
+
* `no_active_schedules` and `date_excepted` are deliberately absent: they are
|
|
120
|
+
* per-resource `skip` traces inside a loop, not return points — they funnel
|
|
121
|
+
* into `no_windows` — and stay debug-only.
|
|
122
|
+
*
|
|
123
|
+
* `window_too_short` is reachable only from the general (non-full-day) branch:
|
|
124
|
+
* the full-day branch returns `no_windows` before it, so it always has at least
|
|
125
|
+
* one candidate.
|
|
126
|
+
*/
|
|
127
|
+
export type EmptyReason = 'all_slots_taken' | 'empty_intersection' | 'no_resource_ids' | 'no_windows' | 'resource_inactive' | 'service_inactive' | 'window_too_short';
|
|
102
128
|
export declare function getAvailableSlots(params: {
|
|
103
129
|
blockingStatuses: string[];
|
|
104
130
|
date: Date | string;
|
|
105
131
|
/** Optional tracer — emits per-stage slot-generation lines when enabled. */
|
|
106
132
|
debug?: ReserveDebug;
|
|
133
|
+
/** Skip the service/resource `active` short-circuits when explicitly `false`. */
|
|
134
|
+
enforceActive?: boolean;
|
|
107
135
|
/** External busy resolver (calendar sync etc.) — intervals block the whole resource. */
|
|
108
136
|
getExternalBusy?: GetExternalBusy;
|
|
109
137
|
guestCount?: number;
|
|
138
|
+
/**
|
|
139
|
+
* Slug of the slot-holds collection; omit to ignore holds entirely.
|
|
140
|
+
*
|
|
141
|
+
* Load-bearing, not optional polish: without it every read-path caller
|
|
142
|
+
* (`/reserve/availability`, `/reserve/slots`, the reservation form's slot
|
|
143
|
+
* picker) advertises a held slot as FREE, and the customer who clicks it is
|
|
144
|
+
* rejected by the write path — which DOES count holds — as a 409. Pass it
|
|
145
|
+
* whenever `slotHolds.enabled`.
|
|
146
|
+
*/
|
|
147
|
+
holdsSlug?: string;
|
|
110
148
|
payload: Payload;
|
|
111
149
|
req: PayloadRequest;
|
|
112
150
|
reservationSlug: string;
|
|
@@ -117,7 +155,10 @@ export declare function getAvailableSlots(params: {
|
|
|
117
155
|
serviceId: number | string;
|
|
118
156
|
serviceSlug: string;
|
|
119
157
|
timeZone?: string;
|
|
120
|
-
}): Promise<
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
158
|
+
}): Promise<{
|
|
159
|
+
reason?: EmptyReason;
|
|
160
|
+
slots: Array<{
|
|
161
|
+
end: Date;
|
|
162
|
+
start: Date;
|
|
163
|
+
}>;
|
|
164
|
+
}>;
|