payload-reserve 2.3.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 +218 -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 +12 -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 +27 -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 +27 -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 +4 -0
- package/dist/endpoints/resourceAvailability.js +105 -4
- 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 +22 -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 +51 -5
- package/dist/services/AvailabilityService.js +315 -38
- 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 +32 -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/reserveDebug.d.ts +26 -0
- package/dist/utilities/reserveDebug.js +35 -0
- package/dist/utilities/reserveDebug.js.map +1 -0
- 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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectionHasTenantField } from './tenantFilter.js';
|
|
1
2
|
import { isValidTimezone } from './timezoneUtils.js';
|
|
2
3
|
/**
|
|
3
4
|
* Pure precedence resolver for a tenant's effective timezone:
|
|
@@ -43,7 +44,7 @@ import { isValidTimezone } from './timezoneUtils.js';
|
|
|
43
44
|
* selected, when the scoped collection lacks a tenant relationship, or when the
|
|
44
45
|
* lookup fails. Never throws.
|
|
45
46
|
*/ export async function getEffectiveTenantTimezone(args) {
|
|
46
|
-
const { globalTimezone, payload, scopedCollection, tenantField, tenantId, timezoneField } = args;
|
|
47
|
+
const { globalTimezone, payload, req, scopedCollection, tenantField, tenantId, timezoneField } = args;
|
|
47
48
|
if (!tenantId) {
|
|
48
49
|
return resolveTenantTimezone({
|
|
49
50
|
globalTimezone
|
|
@@ -57,11 +58,19 @@ import { isValidTimezone } from './timezoneUtils.js';
|
|
|
57
58
|
}
|
|
58
59
|
let tenantTimezone = null;
|
|
59
60
|
try {
|
|
61
|
+
// The tenant id comes from a cookie, so the read must be access-checked —
|
|
62
|
+
// multi-tenant constrains its tenants collection by `id`, which turns this
|
|
63
|
+
// into a membership check. Without a req we cannot check, so stay privileged
|
|
64
|
+
// and let the caller's own gate apply.
|
|
60
65
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
66
|
const tenant = await payload.findByID({
|
|
62
67
|
id: tenantId,
|
|
63
68
|
collection: tenantSlug,
|
|
64
|
-
depth: 0
|
|
69
|
+
depth: 0,
|
|
70
|
+
...req ? {
|
|
71
|
+
overrideAccess: false,
|
|
72
|
+
req
|
|
73
|
+
} : {}
|
|
65
74
|
});
|
|
66
75
|
const raw = tenant?.[timezoneField];
|
|
67
76
|
tenantTimezone = typeof raw === 'string' ? raw : null;
|
|
@@ -73,5 +82,157 @@ import { isValidTimezone } from './timezoneUtils.js';
|
|
|
73
82
|
tenantTimezone
|
|
74
83
|
});
|
|
75
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Normalizes a relationship-shaped value down to its id: a raw string/number
|
|
87
|
+
* id, or an object carrying one — `{ id }`, the shape both a populated
|
|
88
|
+
* relationship and a client sending `{ "tenant": { "id": "..." } }` produce.
|
|
89
|
+
* Returns null for anything else (including an object with no usable `id`),
|
|
90
|
+
* so callers can fail closed on a shape they don't recognize instead of
|
|
91
|
+
* silently skipping whatever check they were about to run.
|
|
92
|
+
*/ export function normalizeRelationshipId(value) {
|
|
93
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
if (value && typeof value === 'object' && 'id' in value) {
|
|
97
|
+
const id = value.id;
|
|
98
|
+
if (typeof id === 'string' || typeof id === 'number') {
|
|
99
|
+
return id;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Every tenant id a tenant-field value names, as a list.
|
|
106
|
+
*
|
|
107
|
+
* Exists because multi-tenant supports a `hasMany` tenant field, so the value
|
|
108
|
+
* can legitimately be an ARRAY of relationship-shaped entries.
|
|
109
|
+
* {@link normalizeRelationshipId} returns null for an array, and the caller
|
|
110
|
+
* fails closed on null — which turned a supported multi-tenant configuration
|
|
111
|
+
* into a blanket 403 on every authenticated booking that carried an explicit
|
|
112
|
+
* tenant (a total booking outage for that install, with no diagnostic).
|
|
113
|
+
*
|
|
114
|
+
* - `[]` -> `[]`: an empty array writes no tenant, so there is nothing to
|
|
115
|
+
* authorize; the caller permits it, exactly as it does for an absent value.
|
|
116
|
+
* - a one-or-many array -> one id per entry, all of which the caller must be
|
|
117
|
+
* authorized for (any single unrecognized entry poisons the whole value).
|
|
118
|
+
* - a scalar / `{ id }` -> a single-element list.
|
|
119
|
+
* - anything else -> null, so the caller can still fail closed, loudly.
|
|
120
|
+
*/ export function normalizeRelationshipIds(value) {
|
|
121
|
+
if (Array.isArray(value)) {
|
|
122
|
+
const ids = [];
|
|
123
|
+
for (const entry of value){
|
|
124
|
+
const id = normalizeRelationshipId(entry);
|
|
125
|
+
if (id === null) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
ids.push(id);
|
|
129
|
+
}
|
|
130
|
+
return ids;
|
|
131
|
+
}
|
|
132
|
+
const single = normalizeRelationshipId(value);
|
|
133
|
+
return single === null ? null : [
|
|
134
|
+
single
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Whether an authenticated caller may write the tenant value present in
|
|
139
|
+
* `data[config.multiTenant.tenantField]` onto a new document in
|
|
140
|
+
* `config.slugs.reservations`.
|
|
141
|
+
*
|
|
142
|
+
* Exists because delegating `overrideAccess` is NOT sufficient on its own:
|
|
143
|
+
* multi-tenant's tenant-field `validate` only checks presence, and Payload's
|
|
144
|
+
* `create` operation (`executeAccess`) only checks the TRUTHINESS of a
|
|
145
|
+
* collection access result — unlike read/update/delete, which apply a
|
|
146
|
+
* returned `Where` via `combineQueries` against a real document. So MT's own
|
|
147
|
+
* tenant-scoped `create` access can't reject an explicit foreign tenant
|
|
148
|
+
* either; an authenticated caller can write one straight through regardless
|
|
149
|
+
* of `overrideAccess`. This closes that gap the same way
|
|
150
|
+
* {@link getEffectiveTenantTimezone} closes it for a client-supplied tenant
|
|
151
|
+
* *cookie*: an access-checked (`overrideAccess: false`) probe read against
|
|
152
|
+
* the tenants collection for the *submitted* tenant id — a lookup that finds
|
|
153
|
+
* nothing means the caller isn't a member (or a multi-tenant super-admin).
|
|
154
|
+
*
|
|
155
|
+
* PRECONDITION — this is a real membership check only when the caller
|
|
156
|
+
* authenticates against the SAME collection multi-tenant wraps as its
|
|
157
|
+
* admin/tenant-owning collection. `withTenantAccess` only applies its
|
|
158
|
+
* membership constraint when `req.user.collection === adminUsersSlug`;
|
|
159
|
+
* otherwise it falls back to the tenants collection's own (here: default,
|
|
160
|
+
* unrestricted) access function, which only requires `Boolean(req.user)`. In
|
|
161
|
+
* this plugin's STANDALONE mode (`userCollection` unset), a customer
|
|
162
|
+
* authenticates against `slugs.customers` — a collection multi-tenant never
|
|
163
|
+
* wraps — so this probe passes for ANY tenant id for a logged-in customer.
|
|
164
|
+
* `src/plugin.ts`'s boot diagnostic warns about that configuration. It is
|
|
165
|
+
* also a no-op if the host set multi-tenant's own
|
|
166
|
+
* `useTenantsCollectionAccess: false`, which is not detectable from here.
|
|
167
|
+
*
|
|
168
|
+
* Returns true — nothing to check — when the reservations collection isn't
|
|
169
|
+
* tenant-scoped at all, or `data` doesn't carry an explicit tenant value (MT's
|
|
170
|
+
* own access-checked `defaultValue` applies in that case; see
|
|
171
|
+
* `getEffectiveTenantTimezone`'s doc comment for why that path is already
|
|
172
|
+
* safe). Returns false — fail closed — for an unrecognized value shape (logged,
|
|
173
|
+
* since a shape this plugin cannot read refuses every such booking), a probe
|
|
174
|
+
* that finds no matching tenant, or any error while probing.
|
|
175
|
+
*
|
|
176
|
+
* A `hasMany` tenant field is supported: the value may be an array, and EVERY
|
|
177
|
+
* id in it is probed.
|
|
178
|
+
*/ export async function callerMayUseTenant(args) {
|
|
179
|
+
const { config, data, req } = args;
|
|
180
|
+
const tenantField = config.multiTenant.tenantField;
|
|
181
|
+
const rawTenant = data[tenantField];
|
|
182
|
+
if (rawTenant === undefined || rawTenant === null) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
const reservationsCollection = req.payload.config.collections?.find((c)=>c.slug === config.slugs.reservations);
|
|
186
|
+
if (!collectionHasTenantField(reservationsCollection, tenantField)) {
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
const tenantSlug = tenantCollectionSlug(reservationsCollection, tenantField);
|
|
190
|
+
if (!tenantSlug) {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
// A `hasMany` tenant field is a supported multi-tenant configuration, so the
|
|
194
|
+
// value may be an array — every id in it must be authorized.
|
|
195
|
+
const tenantIds = normalizeRelationshipIds(rawTenant);
|
|
196
|
+
if (tenantIds === null) {
|
|
197
|
+
// Still fail closed, but say so: an unrecognized shape refuses the booking,
|
|
198
|
+
// and silently refusing every booking is indistinguishable from an outage.
|
|
199
|
+
// The value itself is caller-supplied and is deliberately NOT logged.
|
|
200
|
+
req.payload.logger.warn({
|
|
201
|
+
msg: `payload-reserve: refusing a booking because its "${tenantField}" value has an unrecognized shape. Expected an id, a { id } object, or an array of either (a "hasMany" tenant field). If your tenant field uses a shape this plugin does not recognize, every authenticated booking carrying an explicit tenant will be refused with a 403.`,
|
|
202
|
+
tenantValueShape: Array.isArray(rawTenant) ? 'array' : typeof rawTenant
|
|
203
|
+
});
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
for (const tenantId of tenantIds){
|
|
207
|
+
try {
|
|
208
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
209
|
+
const tenant = await req.payload.findByID({
|
|
210
|
+
id: tenantId,
|
|
211
|
+
collection: tenantSlug,
|
|
212
|
+
depth: 0,
|
|
213
|
+
overrideAccess: false,
|
|
214
|
+
req
|
|
215
|
+
});
|
|
216
|
+
if (!tenant) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
} catch (err) {
|
|
220
|
+
// Forbidden/NotFound is exactly the "not a member" outcome this probe
|
|
221
|
+
// exists to detect — expected, not worth logging. Anything else (a DB
|
|
222
|
+
// outage, a genuine bug) still fails closed but is worth surfacing.
|
|
223
|
+
const status = err?.status;
|
|
224
|
+
if (status !== 403 && status !== 404) {
|
|
225
|
+
req.payload.logger.warn({
|
|
226
|
+
err,
|
|
227
|
+
msg: 'payload-reserve: tenant membership probe failed'
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Includes the empty-array case: no tenant is being written, so there is
|
|
234
|
+
// nothing to authorize.
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
76
237
|
|
|
77
238
|
//# sourceMappingURL=tenantTimezone.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utilities/tenantTimezone.ts"],"sourcesContent":["import type { Payload } from 'payload'\n\nimport { isValidTimezone } from './timezoneUtils.js'\n\ntype CollectionLike = { fields?: unknown[] } | null | undefined\n\ntype FieldLike = { name?: string; relationTo?: string | string[]; type?: string }\n\n/**\n * Pure precedence resolver for a tenant's effective timezone:\n *\n * selected tenant's zone → global default → 'UTC'\n *\n * An absent or invalid zone at any step is skipped, never thrown — so a tenant\n * with a bad/empty timezone value transparently falls back to the global default.\n */\nexport function resolveTenantTimezone(args: {\n globalTimezone: string\n tenantTimezone?: null | string\n}): string {\n const { globalTimezone, tenantTimezone } = args\n if (isValidTimezone(tenantTimezone)) {\n return tenantTimezone\n }\n if (isValidTimezone(globalTimezone)) {\n return globalTimezone\n }\n return 'UTC'\n}\n\n/**\n * The tenant collection's slug, read off the scoped collection's tenant\n * relationship field (`relationTo`). Keeps the plugin tenant-agnostic — it never\n * hardcodes a tenants slug. Returns null for an absent, polymorphic, or\n * non-relationship tenant field.\n */\nexport function tenantCollectionSlug(collection: CollectionLike, tenantField: string): null | string {\n const fields = collection?.fields\n if (!Array.isArray(fields)) {\n return null\n }\n for (const f of fields) {\n if (typeof f === 'object' && f !== null && (f as FieldLike).name === tenantField) {\n const rel = (f as FieldLike).relationTo\n return typeof rel === 'string' ? rel : null\n }\n }\n return null\n}\n\n/**\n * Resolve the effective timezone for the selected tenant in multiTenant mode.\n *\n * Loads the tenant document (slug derived from the scoped collection's tenant\n * relationship) and reads `timezoneField`, then applies {@link resolveTenantTimezone}\n * precedence. Returns the global default — without a DB read — when no tenant is\n * selected, when the scoped collection lacks a tenant relationship, or when the\n * lookup fails. Never throws.\n */\nexport async function getEffectiveTenantTimezone(args: {\n globalTimezone: string\n payload: Payload\n scopedCollection: CollectionLike\n tenantField: string\n tenantId: null | string\n timezoneField: string\n}): Promise<string> {\n const { globalTimezone, payload, scopedCollection, tenantField, tenantId, timezoneField } = args\n if (!tenantId) {\n return resolveTenantTimezone({ globalTimezone })\n }\n const tenantSlug = tenantCollectionSlug(scopedCollection, tenantField)\n if (!tenantSlug) {\n return resolveTenantTimezone({ globalTimezone })\n }\n let tenantTimezone: null | string = null\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const tenant = await (payload.findByID as any)({\n id: tenantId,\n collection: tenantSlug,\n depth: 0,\n })\n const raw = (tenant as null | Record<string, unknown>)?.[timezoneField]\n tenantTimezone = typeof raw === 'string' ? raw : null\n } catch {\n tenantTimezone = null\n }\n return resolveTenantTimezone({ globalTimezone, tenantTimezone })\n}\n"],"names":["isValidTimezone","resolveTenantTimezone","args","globalTimezone","tenantTimezone","tenantCollectionSlug","collection","tenantField","fields","Array","isArray","f","name","rel","relationTo","getEffectiveTenantTimezone","payload","scopedCollection","tenantId","timezoneField","tenantSlug","tenant","findByID","id","depth","raw"],"mappings":"AAEA,SAASA,eAAe,QAAQ,qBAAoB;AAMpD;;;;;;;CAOC,GACD,OAAO,SAASC,sBAAsBC,IAGrC;IACC,MAAM,EAAEC,cAAc,EAAEC,cAAc,EAAE,GAAGF;IAC3C,IAAIF,gBAAgBI,iBAAiB;QACnC,OAAOA;IACT;IACA,IAAIJ,gBAAgBG,iBAAiB;QACnC,OAAOA;IACT;IACA,OAAO;AACT;AAEA;;;;;CAKC,GACD,OAAO,SAASE,qBAAqBC,UAA0B,EAAEC,WAAmB;IAClF,MAAMC,SAASF,YAAYE;IAC3B,IAAI,CAACC,MAAMC,OAAO,CAACF,SAAS;QAC1B,OAAO;IACT;IACA,KAAK,MAAMG,KAAKH,OAAQ;QACtB,IAAI,OAAOG,MAAM,YAAYA,MAAM,QAAQ,AAACA,EAAgBC,IAAI,KAAKL,aAAa;YAChF,MAAMM,MAAM,AAACF,EAAgBG,UAAU;YACvC,OAAO,OAAOD,QAAQ,WAAWA,MAAM;QACzC;IACF;IACA,OAAO;AACT;AAEA;;;;;;;;CAQC,GACD,OAAO,eAAeE,2BAA2Bb,IAOhD;IACC,MAAM,EAAEC,cAAc,EAAEa,OAAO,EAAEC,gBAAgB,EAAEV,WAAW,EAAEW,QAAQ,EAAEC,aAAa,EAAE,GAAGjB;IAC5F,IAAI,CAACgB,UAAU;QACb,OAAOjB,sBAAsB;YAAEE;QAAe;IAChD;IACA,MAAMiB,aAAaf,qBAAqBY,kBAAkBV;IAC1D,IAAI,CAACa,YAAY;QACf,OAAOnB,sBAAsB;YAAEE;QAAe;IAChD;IACA,IAAIC,iBAAgC;IACpC,IAAI;QACF,8DAA8D;QAC9D,MAAMiB,SAAS,MAAM,AAACL,QAAQM,QAAQ,CAAS;YAC7CC,IAAIL;YACJZ,YAAYc;YACZI,OAAO;QACT;QACA,MAAMC,MAAOJ,QAA2C,CAACF,cAAc;QACvEf,iBAAiB,OAAOqB,QAAQ,WAAWA,MAAM;IACnD,EAAE,OAAM;QACNrB,iBAAiB;IACnB;IACA,OAAOH,sBAAsB;QAAEE;QAAgBC;IAAe;AAChE"}
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/tenantTimezone.ts"],"sourcesContent":["import type { Payload, PayloadRequest } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../types.js'\n\nimport { collectionHasTenantField } from './tenantFilter.js'\nimport { isValidTimezone } from './timezoneUtils.js'\n\ntype CollectionLike = { fields?: unknown[] } | null | undefined\n\ntype FieldLike = { name?: string; relationTo?: string | string[]; type?: string }\n\n/**\n * Pure precedence resolver for a tenant's effective timezone:\n *\n * selected tenant's zone → global default → 'UTC'\n *\n * An absent or invalid zone at any step is skipped, never thrown — so a tenant\n * with a bad/empty timezone value transparently falls back to the global default.\n */\nexport function resolveTenantTimezone(args: {\n globalTimezone: string\n tenantTimezone?: null | string\n}): string {\n const { globalTimezone, tenantTimezone } = args\n if (isValidTimezone(tenantTimezone)) {\n return tenantTimezone\n }\n if (isValidTimezone(globalTimezone)) {\n return globalTimezone\n }\n return 'UTC'\n}\n\n/**\n * The tenant collection's slug, read off the scoped collection's tenant\n * relationship field (`relationTo`). Keeps the plugin tenant-agnostic — it never\n * hardcodes a tenants slug. Returns null for an absent, polymorphic, or\n * non-relationship tenant field.\n */\nexport function tenantCollectionSlug(collection: CollectionLike, tenantField: string): null | string {\n const fields = collection?.fields\n if (!Array.isArray(fields)) {\n return null\n }\n for (const f of fields) {\n if (typeof f === 'object' && f !== null && (f as FieldLike).name === tenantField) {\n const rel = (f as FieldLike).relationTo\n return typeof rel === 'string' ? rel : null\n }\n }\n return null\n}\n\n/**\n * Resolve the effective timezone for the selected tenant in multiTenant mode.\n *\n * Loads the tenant document (slug derived from the scoped collection's tenant\n * relationship) and reads `timezoneField`, then applies {@link resolveTenantTimezone}\n * precedence. Returns the global default — without a DB read — when no tenant is\n * selected, when the scoped collection lacks a tenant relationship, or when the\n * lookup fails. Never throws.\n */\nexport async function getEffectiveTenantTimezone(args: {\n globalTimezone: string\n payload: Payload\n req?: PayloadRequest\n scopedCollection: CollectionLike\n tenantField: string\n tenantId: null | string\n timezoneField: string\n}): Promise<string> {\n const { globalTimezone, payload, req, scopedCollection, tenantField, tenantId, timezoneField } =\n args\n if (!tenantId) {\n return resolveTenantTimezone({ globalTimezone })\n }\n const tenantSlug = tenantCollectionSlug(scopedCollection, tenantField)\n if (!tenantSlug) {\n return resolveTenantTimezone({ globalTimezone })\n }\n let tenantTimezone: null | string = null\n try {\n // The tenant id comes from a cookie, so the read must be access-checked —\n // multi-tenant constrains its tenants collection by `id`, which turns this\n // into a membership check. Without a req we cannot check, so stay privileged\n // and let the caller's own gate apply.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const tenant = await (payload.findByID as any)({\n id: tenantId,\n collection: tenantSlug,\n depth: 0,\n ...(req ? { overrideAccess: false, req } : {}),\n })\n const raw = (tenant as null | Record<string, unknown>)?.[timezoneField]\n tenantTimezone = typeof raw === 'string' ? raw : null\n } catch {\n tenantTimezone = null\n }\n return resolveTenantTimezone({ globalTimezone, tenantTimezone })\n}\n\n/**\n * Normalizes a relationship-shaped value down to its id: a raw string/number\n * id, or an object carrying one — `{ id }`, the shape both a populated\n * relationship and a client sending `{ \"tenant\": { \"id\": \"...\" } }` produce.\n * Returns null for anything else (including an object with no usable `id`),\n * so callers can fail closed on a shape they don't recognize instead of\n * silently skipping whatever check they were about to run.\n */\nexport function normalizeRelationshipId(value: unknown): null | number | string {\n if (typeof value === 'string' || typeof value === 'number') {\n return value\n }\n if (value && typeof value === 'object' && 'id' in value) {\n const id = (value as { id?: unknown }).id\n if (typeof id === 'string' || typeof id === 'number') {\n return id\n }\n }\n return null\n}\n\n/**\n * Every tenant id a tenant-field value names, as a list.\n *\n * Exists because multi-tenant supports a `hasMany` tenant field, so the value\n * can legitimately be an ARRAY of relationship-shaped entries.\n * {@link normalizeRelationshipId} returns null for an array, and the caller\n * fails closed on null — which turned a supported multi-tenant configuration\n * into a blanket 403 on every authenticated booking that carried an explicit\n * tenant (a total booking outage for that install, with no diagnostic).\n *\n * - `[]` -> `[]`: an empty array writes no tenant, so there is nothing to\n * authorize; the caller permits it, exactly as it does for an absent value.\n * - a one-or-many array -> one id per entry, all of which the caller must be\n * authorized for (any single unrecognized entry poisons the whole value).\n * - a scalar / `{ id }` -> a single-element list.\n * - anything else -> null, so the caller can still fail closed, loudly.\n */\nexport function normalizeRelationshipIds(value: unknown): Array<number | string> | null {\n if (Array.isArray(value)) {\n const ids: Array<number | string> = []\n for (const entry of value) {\n const id = normalizeRelationshipId(entry)\n if (id === null) {\n return null\n }\n ids.push(id)\n }\n return ids\n }\n const single = normalizeRelationshipId(value)\n return single === null ? null : [single]\n}\n\n/**\n * Whether an authenticated caller may write the tenant value present in\n * `data[config.multiTenant.tenantField]` onto a new document in\n * `config.slugs.reservations`.\n *\n * Exists because delegating `overrideAccess` is NOT sufficient on its own:\n * multi-tenant's tenant-field `validate` only checks presence, and Payload's\n * `create` operation (`executeAccess`) only checks the TRUTHINESS of a\n * collection access result — unlike read/update/delete, which apply a\n * returned `Where` via `combineQueries` against a real document. So MT's own\n * tenant-scoped `create` access can't reject an explicit foreign tenant\n * either; an authenticated caller can write one straight through regardless\n * of `overrideAccess`. This closes that gap the same way\n * {@link getEffectiveTenantTimezone} closes it for a client-supplied tenant\n * *cookie*: an access-checked (`overrideAccess: false`) probe read against\n * the tenants collection for the *submitted* tenant id — a lookup that finds\n * nothing means the caller isn't a member (or a multi-tenant super-admin).\n *\n * PRECONDITION — this is a real membership check only when the caller\n * authenticates against the SAME collection multi-tenant wraps as its\n * admin/tenant-owning collection. `withTenantAccess` only applies its\n * membership constraint when `req.user.collection === adminUsersSlug`;\n * otherwise it falls back to the tenants collection's own (here: default,\n * unrestricted) access function, which only requires `Boolean(req.user)`. In\n * this plugin's STANDALONE mode (`userCollection` unset), a customer\n * authenticates against `slugs.customers` — a collection multi-tenant never\n * wraps — so this probe passes for ANY tenant id for a logged-in customer.\n * `src/plugin.ts`'s boot diagnostic warns about that configuration. It is\n * also a no-op if the host set multi-tenant's own\n * `useTenantsCollectionAccess: false`, which is not detectable from here.\n *\n * Returns true — nothing to check — when the reservations collection isn't\n * tenant-scoped at all, or `data` doesn't carry an explicit tenant value (MT's\n * own access-checked `defaultValue` applies in that case; see\n * `getEffectiveTenantTimezone`'s doc comment for why that path is already\n * safe). Returns false — fail closed — for an unrecognized value shape (logged,\n * since a shape this plugin cannot read refuses every such booking), a probe\n * that finds no matching tenant, or any error while probing.\n *\n * A `hasMany` tenant field is supported: the value may be an array, and EVERY\n * id in it is probed.\n */\nexport async function callerMayUseTenant(args: {\n config: Pick<ResolvedReservationPluginConfig, 'multiTenant' | 'slugs'>\n data: Record<string, unknown>\n req: PayloadRequest\n}): Promise<boolean> {\n const { config, data, req } = args\n const tenantField = config.multiTenant.tenantField\n const rawTenant = data[tenantField]\n if (rawTenant === undefined || rawTenant === null) {\n return true\n }\n\n const reservationsCollection = req.payload.config.collections?.find(\n (c) => c.slug === config.slugs.reservations,\n ) as CollectionLike\n if (!collectionHasTenantField(reservationsCollection, tenantField)) {\n return true\n }\n const tenantSlug = tenantCollectionSlug(reservationsCollection, tenantField)\n if (!tenantSlug) {\n return true\n }\n\n // A `hasMany` tenant field is a supported multi-tenant configuration, so the\n // value may be an array — every id in it must be authorized.\n const tenantIds = normalizeRelationshipIds(rawTenant)\n if (tenantIds === null) {\n // Still fail closed, but say so: an unrecognized shape refuses the booking,\n // and silently refusing every booking is indistinguishable from an outage.\n // The value itself is caller-supplied and is deliberately NOT logged.\n req.payload.logger.warn({\n msg: `payload-reserve: refusing a booking because its \"${tenantField}\" value has an unrecognized shape. Expected an id, a { id } object, or an array of either (a \"hasMany\" tenant field). If your tenant field uses a shape this plugin does not recognize, every authenticated booking carrying an explicit tenant will be refused with a 403.`,\n tenantValueShape: Array.isArray(rawTenant) ? 'array' : typeof rawTenant,\n })\n return false\n }\n\n for (const tenantId of tenantIds) {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const tenant = await (req.payload.findByID as any)({\n id: tenantId,\n collection: tenantSlug,\n depth: 0,\n overrideAccess: false,\n req,\n })\n if (!tenant) {\n return false\n }\n } catch (err) {\n // Forbidden/NotFound is exactly the \"not a member\" outcome this probe\n // exists to detect — expected, not worth logging. Anything else (a DB\n // outage, a genuine bug) still fails closed but is worth surfacing.\n const status = (err as { status?: number } | undefined)?.status\n if (status !== 403 && status !== 404) {\n req.payload.logger.warn({ err, msg: 'payload-reserve: tenant membership probe failed' })\n }\n return false\n }\n }\n\n // Includes the empty-array case: no tenant is being written, so there is\n // nothing to authorize.\n return true\n}\n"],"names":["collectionHasTenantField","isValidTimezone","resolveTenantTimezone","args","globalTimezone","tenantTimezone","tenantCollectionSlug","collection","tenantField","fields","Array","isArray","f","name","rel","relationTo","getEffectiveTenantTimezone","payload","req","scopedCollection","tenantId","timezoneField","tenantSlug","tenant","findByID","id","depth","overrideAccess","raw","normalizeRelationshipId","value","normalizeRelationshipIds","ids","entry","push","single","callerMayUseTenant","config","data","multiTenant","rawTenant","undefined","reservationsCollection","collections","find","c","slug","slugs","reservations","tenantIds","logger","warn","msg","tenantValueShape","err","status"],"mappings":"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"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Payload } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* Whether this database will actually give the plugin a transaction.
|
|
4
|
+
*
|
|
5
|
+
* It matters because `acquireBookingLock` is only meaningful inside one. Without
|
|
6
|
+
* a transaction the lock write still happens but serializes nothing, so
|
|
7
|
+
* concurrent bookings silently double-book exactly as they did before the lock
|
|
8
|
+
* existed — a failure that is open rather than closed, and invisible.
|
|
9
|
+
*
|
|
10
|
+
* Payload's mongoose adapter returns null from `beginTransaction` when the
|
|
11
|
+
* connection is not a replica set. Probing costs one no-op transaction at boot.
|
|
12
|
+
*/
|
|
13
|
+
export declare function supportsTransactions(payload: Payload): Promise<boolean>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether this database will actually give the plugin a transaction.
|
|
3
|
+
*
|
|
4
|
+
* It matters because `acquireBookingLock` is only meaningful inside one. Without
|
|
5
|
+
* a transaction the lock write still happens but serializes nothing, so
|
|
6
|
+
* concurrent bookings silently double-book exactly as they did before the lock
|
|
7
|
+
* existed — a failure that is open rather than closed, and invisible.
|
|
8
|
+
*
|
|
9
|
+
* Payload's mongoose adapter returns null from `beginTransaction` when the
|
|
10
|
+
* connection is not a replica set. Probing costs one no-op transaction at boot.
|
|
11
|
+
*/ export async function supportsTransactions(payload) {
|
|
12
|
+
const db = payload.db;
|
|
13
|
+
if (typeof db?.beginTransaction !== 'function') {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const id = await db.beginTransaction();
|
|
18
|
+
if (id === null || id === undefined) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
await db.rollbackTransaction?.(id);
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
// An adapter that throws here cannot give us a transaction, which is the
|
|
25
|
+
// only thing the caller needs to know.
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//# sourceMappingURL=transactionSupport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utilities/transactionSupport.ts"],"sourcesContent":["import type { Payload } from 'payload'\n\n/**\n * Whether this database will actually give the plugin a transaction.\n *\n * It matters because `acquireBookingLock` is only meaningful inside one. Without\n * a transaction the lock write still happens but serializes nothing, so\n * concurrent bookings silently double-book exactly as they did before the lock\n * existed — a failure that is open rather than closed, and invisible.\n *\n * Payload's mongoose adapter returns null from `beginTransaction` when the\n * connection is not a replica set. Probing costs one no-op transaction at boot.\n */\nexport async function supportsTransactions(payload: Payload): Promise<boolean> {\n const db = payload.db as {\n beginTransaction?: (options?: unknown) => Promise<null | number | string>\n rollbackTransaction?: (id: unknown) => Promise<void>\n }\n\n if (typeof db?.beginTransaction !== 'function') {\n return false\n }\n\n try {\n const id = await db.beginTransaction()\n if (id === null || id === undefined) {\n return false\n }\n await db.rollbackTransaction?.(id)\n return true\n } catch {\n // An adapter that throws here cannot give us a transaction, which is the\n // only thing the caller needs to know.\n return false\n }\n}\n"],"names":["supportsTransactions","payload","db","beginTransaction","id","undefined","rollbackTransaction"],"mappings":"AAEA;;;;;;;;;;CAUC,GACD,OAAO,eAAeA,qBAAqBC,OAAgB;IACzD,MAAMC,KAAKD,QAAQC,EAAE;IAKrB,IAAI,OAAOA,IAAIC,qBAAqB,YAAY;QAC9C,OAAO;IACT;IAEA,IAAI;QACF,MAAMC,KAAK,MAAMF,GAAGC,gBAAgB;QACpC,IAAIC,OAAO,QAAQA,OAAOC,WAAW;YACnC,OAAO;QACT;QACA,MAAMH,GAAGI,mBAAmB,GAAGF;QAC/B,OAAO;IACT,EAAE,OAAM;QACN,yEAAyE;QACzE,uCAAuC;QACvC,OAAO;IACT;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payload-reserve",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "A Payload CMS 3.x plugin for reservation and booking management with conflict detection, status workflows, and calendar UI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"payload",
|
|
@@ -49,15 +49,15 @@
|
|
|
49
49
|
"@changesets/cli": "^2.30.0",
|
|
50
50
|
"@elghaied/payload-plugin-sms": "^0.3.0",
|
|
51
51
|
"@eslint/eslintrc": "^3.2.0",
|
|
52
|
-
"@payloadcms/db-mongodb": "3.
|
|
53
|
-
"@payloadcms/db-postgres": "3.
|
|
54
|
-
"@payloadcms/db-sqlite": "3.
|
|
52
|
+
"@payloadcms/db-mongodb": "3.86.0",
|
|
53
|
+
"@payloadcms/db-postgres": "3.86.0",
|
|
54
|
+
"@payloadcms/db-sqlite": "3.86.0",
|
|
55
55
|
"@payloadcms/eslint-config": "3.9.0",
|
|
56
|
-
"@payloadcms/next": "3.
|
|
57
|
-
"@payloadcms/plugin-multi-tenant": "3.
|
|
58
|
-
"@payloadcms/richtext-lexical": "3.
|
|
59
|
-
"@payloadcms/translations": "3.
|
|
60
|
-
"@payloadcms/ui": "3.
|
|
56
|
+
"@payloadcms/next": "3.86.0",
|
|
57
|
+
"@payloadcms/plugin-multi-tenant": "3.86.0",
|
|
58
|
+
"@payloadcms/richtext-lexical": "3.86.0",
|
|
59
|
+
"@payloadcms/translations": "3.86.0",
|
|
60
|
+
"@payloadcms/ui": "3.86.0",
|
|
61
61
|
"@playwright/test": "1.56.1",
|
|
62
62
|
"@swc-node/register": "1.10.9",
|
|
63
63
|
"@swc/cli": "0.6.0",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"mongodb-memory-server": "10.1.4",
|
|
73
73
|
"next": "15.4.11",
|
|
74
74
|
"open": "^10.1.0",
|
|
75
|
-
"payload": "3.
|
|
75
|
+
"payload": "3.86.0",
|
|
76
76
|
"prettier": "^3.4.2",
|
|
77
77
|
"qs-esm": "7.0.2",
|
|
78
78
|
"react": "19.2.1",
|
|
@@ -85,9 +85,9 @@
|
|
|
85
85
|
"vitest": "4.0.18"
|
|
86
86
|
},
|
|
87
87
|
"peerDependencies": {
|
|
88
|
-
"@payloadcms/translations": "^3.
|
|
89
|
-
"@payloadcms/ui": "^3.
|
|
90
|
-
"payload": "^3.
|
|
88
|
+
"@payloadcms/translations": "^3.86.0",
|
|
89
|
+
"@payloadcms/ui": "^3.86.0",
|
|
90
|
+
"payload": "^3.86.0"
|
|
91
91
|
},
|
|
92
92
|
"engines": {
|
|
93
93
|
"node": "^18.20.2 || >=20.9.0",
|