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
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CollectionBeforeChangeHook } from 'payload';
|
|
2
|
+
import type { ResolvedReservationPluginConfig } from '../../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Rejects bookings against an inactive service or resource.
|
|
5
|
+
*
|
|
6
|
+
* MUST run AFTER expandRequiredResources — that hook injects a service's
|
|
7
|
+
* requiredResources into items[] with no active check of its own, so running
|
|
8
|
+
* earlier would let an inactive pool through. MUST run BEFORE validateConflicts
|
|
9
|
+
* so a cheap rejection precedes the expensive coarse-overlap queries.
|
|
10
|
+
*/
|
|
11
|
+
export declare const validateActive: (config: ResolvedReservationPluginConfig) => CollectionBeforeChangeHook;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ValidationError } from 'payload';
|
|
2
|
+
import { mergeReservationData, schedulingFieldsChanged } from '../../utilities/reservationChanges.js';
|
|
3
|
+
import { extractId, resolveReservationItems } from '../../utilities/resolveReservationItems.js';
|
|
4
|
+
/**
|
|
5
|
+
* Rejects bookings against an inactive service or resource.
|
|
6
|
+
*
|
|
7
|
+
* MUST run AFTER expandRequiredResources — that hook injects a service's
|
|
8
|
+
* requiredResources into items[] with no active check of its own, so running
|
|
9
|
+
* earlier would let an inactive pool through. MUST run BEFORE validateConflicts
|
|
10
|
+
* so a cheap rejection precedes the expensive coarse-overlap queries.
|
|
11
|
+
*/ export const validateActive = (config)=>async ({ context, data, operation, originalDoc, req })=>{
|
|
12
|
+
if (context?.skipReservationHooks || !config.enforceActive) {
|
|
13
|
+
return data;
|
|
14
|
+
}
|
|
15
|
+
// Validate the merged document — Payload usually backfills update patches
|
|
16
|
+
// from originalDoc before beforeChange, but the hook must not rely on it.
|
|
17
|
+
const merged = mergeReservationData(data, originalDoc);
|
|
18
|
+
let items;
|
|
19
|
+
let previous;
|
|
20
|
+
try {
|
|
21
|
+
items = resolveReservationItems(merged);
|
|
22
|
+
// On update, only re-check references that actually changed. Otherwise a
|
|
23
|
+
// service deactivated after booking would make its existing reservations
|
|
24
|
+
// permanently uneditable.
|
|
25
|
+
previous = operation === 'update' ? resolveReservationItems(originalDoc ?? {}) : [];
|
|
26
|
+
} catch {
|
|
27
|
+
// A malformed items[] is calculateEndTime's and validateConflicts' error
|
|
28
|
+
// to raise, and only on a real scheduling change. Rows written before
|
|
29
|
+
// that validation existed — or seeded via context.skipReservationHooks —
|
|
30
|
+
// must stay editable for benign edits, exactly as they were before this
|
|
31
|
+
// hook was added.
|
|
32
|
+
return data;
|
|
33
|
+
}
|
|
34
|
+
const previousKeys = new Set(previous.map((p)=>`${String(extractId(p.service))}|${String(extractId(p.resource))}`));
|
|
35
|
+
// A reschedule keeps the same (service, resource) pair, so the pair-only
|
|
36
|
+
// skip below would wave it through even though availability refuses to
|
|
37
|
+
// offer any slot on an inactive resource. Compare scheduling VALUES too —
|
|
38
|
+
// deliberately WITHOUT blockingStatuses, so confirming or cancelling an
|
|
39
|
+
// existing booking stays possible after its references are deactivated.
|
|
40
|
+
const schedulingChanged = operation === 'update' && schedulingFieldsChanged({
|
|
41
|
+
data: data,
|
|
42
|
+
originalDoc: originalDoc
|
|
43
|
+
});
|
|
44
|
+
// A single-entry items[] still holds resource/service ids distinct from the
|
|
45
|
+
// top-level fields, so the error path must key off whether items[] is
|
|
46
|
+
// actually populated on the merged doc — not off the resolved item count,
|
|
47
|
+
// which is always >= 1 and collapses a real items.0.* mismatch onto the
|
|
48
|
+
// (wrong) top-level `resource`/`service` path.
|
|
49
|
+
const hasItemsArray = Array.isArray(merged.items) && merged.items.length > 0;
|
|
50
|
+
for (const [index, item] of items.entries()){
|
|
51
|
+
const serviceId = extractId(item.service);
|
|
52
|
+
const resourceId = extractId(item.resource);
|
|
53
|
+
if (operation === 'update' && !schedulingChanged && previousKeys.has(`${String(serviceId)}|${String(resourceId)}`)) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
// A synthesised parent item (B1) has no items[] index — report it against
|
|
57
|
+
// the top-level field instead of a path that does not exist.
|
|
58
|
+
const prefix = hasItemsArray && !item.fromParent ? `items.${index}.` : '';
|
|
59
|
+
for (const [kind, id, slug, key] of [
|
|
60
|
+
[
|
|
61
|
+
'service',
|
|
62
|
+
serviceId,
|
|
63
|
+
config.slugs.services,
|
|
64
|
+
'errorServiceInactive'
|
|
65
|
+
],
|
|
66
|
+
[
|
|
67
|
+
'resource',
|
|
68
|
+
resourceId,
|
|
69
|
+
config.slugs.resources,
|
|
70
|
+
'errorResourceInactive'
|
|
71
|
+
]
|
|
72
|
+
]){
|
|
73
|
+
if (id === undefined) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
77
|
+
const doc = await req.payload.findByID({
|
|
78
|
+
id,
|
|
79
|
+
collection: slug,
|
|
80
|
+
depth: 0,
|
|
81
|
+
// Skip joins — internal logic never reads them, and without this
|
|
82
|
+
// every read becomes an aggregation with a $lookup.
|
|
83
|
+
joins: false,
|
|
84
|
+
req
|
|
85
|
+
}).catch(()=>null);
|
|
86
|
+
if (doc && doc.active === false) {
|
|
87
|
+
throw new ValidationError({
|
|
88
|
+
errors: [
|
|
89
|
+
{
|
|
90
|
+
message: req.t(`reservation:${key}`, {
|
|
91
|
+
name: doc.name ?? String(id)
|
|
92
|
+
}),
|
|
93
|
+
path: `${prefix}${kind}`
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return data;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
//# sourceMappingURL=validateActive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/hooks/reservations/validateActive.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\nimport type { ResolvedItem } from '../../utilities/resolveReservationItems.js'\n\nimport {\n mergeReservationData,\n schedulingFieldsChanged,\n} from '../../utilities/reservationChanges.js'\nimport { extractId, resolveReservationItems } from '../../utilities/resolveReservationItems.js'\n\n/**\n * Rejects bookings against an inactive service or resource.\n *\n * MUST run AFTER expandRequiredResources — that hook injects a service's\n * requiredResources into items[] with no active check of its own, so running\n * earlier would let an inactive pool through. MUST run BEFORE validateConflicts\n * so a cheap rejection precedes the expensive coarse-overlap queries.\n */\nexport const validateActive =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks || !config.enforceActive) {\n return data\n }\n\n // Validate the merged document — Payload usually backfills update patches\n // from originalDoc before beforeChange, but the hook must not rely on it.\n const merged = mergeReservationData(\n data as Record<string, unknown>,\n originalDoc as Record<string, unknown> | undefined,\n )\n\n let items: ResolvedItem[]\n let previous: ResolvedItem[]\n try {\n items = resolveReservationItems(merged)\n // On update, only re-check references that actually changed. Otherwise a\n // service deactivated after booking would make its existing reservations\n // permanently uneditable.\n previous =\n operation === 'update'\n ? resolveReservationItems((originalDoc ?? {}) as Record<string, unknown>)\n : []\n } catch {\n // A malformed items[] is calculateEndTime's and validateConflicts' error\n // to raise, and only on a real scheduling change. Rows written before\n // that validation existed — or seeded via context.skipReservationHooks —\n // must stay editable for benign edits, exactly as they were before this\n // hook was added.\n return data\n }\n\n const previousKeys = new Set(\n previous.map((p) => `${String(extractId(p.service))}|${String(extractId(p.resource))}`),\n )\n\n // A reschedule keeps the same (service, resource) pair, so the pair-only\n // skip below would wave it through even though availability refuses to\n // offer any slot on an inactive resource. Compare scheduling VALUES too —\n // deliberately WITHOUT blockingStatuses, so confirming or cancelling an\n // existing booking stays possible after its references are deactivated.\n const schedulingChanged =\n operation === 'update' &&\n schedulingFieldsChanged({\n data: data as Record<string, unknown>,\n originalDoc: originalDoc as Record<string, unknown> | undefined,\n })\n\n // A single-entry items[] still holds resource/service ids distinct from the\n // top-level fields, so the error path must key off whether items[] is\n // actually populated on the merged doc — not off the resolved item count,\n // which is always >= 1 and collapses a real items.0.* mismatch onto the\n // (wrong) top-level `resource`/`service` path.\n const hasItemsArray = Array.isArray(merged.items) && (merged.items as unknown[]).length > 0\n\n for (const [index, item] of items.entries()) {\n const serviceId = extractId(item.service)\n const resourceId = extractId(item.resource)\n\n if (\n operation === 'update' &&\n !schedulingChanged &&\n previousKeys.has(`${String(serviceId)}|${String(resourceId)}`)\n ) {\n continue\n }\n\n // A synthesised parent item (B1) has no items[] index — report it against\n // the top-level field instead of a path that does not exist.\n const prefix = hasItemsArray && !item.fromParent ? `items.${index}.` : ''\n\n for (const [kind, id, slug, key] of [\n ['service', serviceId, config.slugs.services, 'errorServiceInactive'],\n ['resource', resourceId, config.slugs.resources, 'errorResourceInactive'],\n ] as const) {\n if (id === undefined) {\n continue\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const doc = await (req.payload.findByID as any)({\n id,\n collection: slug,\n depth: 0,\n // Skip joins — internal logic never reads them, and without this\n // every read becomes an aggregation with a $lookup.\n joins: false,\n req,\n }).catch(() => null)\n\n if (doc && doc.active === false) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)(`reservation:${key}`, {\n name: (doc.name as string) ?? String(id),\n }),\n path: `${prefix}${kind}`,\n },\n ],\n })\n }\n }\n }\n\n return data\n }\n"],"names":["ValidationError","mergeReservationData","schedulingFieldsChanged","extractId","resolveReservationItems","validateActive","config","context","data","operation","originalDoc","req","skipReservationHooks","enforceActive","merged","items","previous","previousKeys","Set","map","p","String","service","resource","schedulingChanged","hasItemsArray","Array","isArray","length","index","item","entries","serviceId","resourceId","has","prefix","fromParent","kind","id","slug","key","slugs","services","resources","undefined","doc","payload","findByID","collection","depth","joins","catch","active","errors","message","t","name","path"],"mappings":"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"}
|
|
@@ -40,6 +40,10 @@ export const validateConflicts = (config)=>async ({ context, data, operation, or
|
|
|
40
40
|
const service = await req.payload.findByID({
|
|
41
41
|
id: itemServiceId,
|
|
42
42
|
collection: config.slugs.services,
|
|
43
|
+
depth: 0,
|
|
44
|
+
// Skip the resources join — internal logic never reads it, and without this
|
|
45
|
+
// every service read becomes an aggregation with a $lookup.
|
|
46
|
+
joins: false,
|
|
43
47
|
req
|
|
44
48
|
});
|
|
45
49
|
if (service) {
|
|
@@ -59,9 +63,11 @@ export const validateConflicts = (config)=>async ({ context, data, operation, or
|
|
|
59
63
|
bufferBefore,
|
|
60
64
|
debug: dbg,
|
|
61
65
|
endTime: new Date(item.endTime),
|
|
66
|
+
excludeHoldToken: typeof context?.holdToken === 'string' ? context.holdToken : undefined,
|
|
62
67
|
excludeReservationId: isUpdate ? originalDoc?.id : undefined,
|
|
63
68
|
getExternalBusy: config.getExternalBusy,
|
|
64
69
|
guestCount: item.guestCount,
|
|
70
|
+
holdsSlug: config.slotHolds.enabled ? config.slugs.holds : undefined,
|
|
65
71
|
payload: req.payload,
|
|
66
72
|
req,
|
|
67
73
|
reservationSlug: config.slugs.reservations,
|
|
@@ -88,7 +94,7 @@ export const validateConflicts = (config)=>async ({ context, data, operation, or
|
|
|
88
94
|
errors: [
|
|
89
95
|
{
|
|
90
96
|
message: result.reason ?? req.t('reservation:errorConflict'),
|
|
91
|
-
path: items.length > 1 ? `items.${i}.startTime` : 'startTime'
|
|
97
|
+
path: items.length > 1 && !item.fromParent ? `items.${i}.startTime` : 'startTime'
|
|
92
98
|
}
|
|
93
99
|
]
|
|
94
100
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/hooks/reservations/validateConflicts.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { checkAvailability } from '../../services/AvailabilityService.js'\nimport {\n mergeReservationData,\n schedulingFieldsChanged,\n} from '../../utilities/reservationChanges.js'\nimport { createReserveDebug } from '../../utilities/reserveDebug.js'\nimport { extractId, resolveReservationItems } from '../../utilities/resolveReservationItems.js'\n\nexport const validateConflicts =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {\n return data\n }\n\n const dbg = createReserveDebug(req.payload.logger, config.debug)\n\n const isUpdate = operation === 'update'\n\n // Skip when an update touches no scheduling-relevant field, so reservations\n // booked under older buffers/schedules can still take benign edits.\n if (\n isUpdate &&\n !schedulingFieldsChanged({\n blockingStatuses: config.statusMachine.blockingStatuses,\n data: data as Record<string, unknown>,\n originalDoc: originalDoc as Record<string, unknown> | undefined,\n })\n ) {\n return data\n }\n\n // Validate the merged document — Payload usually backfills update patches\n // from originalDoc before beforeChange, but the hook must not rely on it.\n const source = isUpdate\n ? mergeReservationData(\n data as Record<string, unknown>,\n originalDoc as Record<string, unknown> | undefined,\n )\n : (data as Record<string, unknown>)\n\n const items = resolveReservationItems(source)\n\n if (items.length === 0) {\n return data\n }\n\n for (let i = 0; i < items.length; i++) {\n const item = items[i]\n if (!item.endTime) {\n continue\n }\n\n // Fetch buffer times from the item's own service (not just the primary)\n const itemServiceId = item.service ?? extractId(source.service)\n let bufferBefore = config.defaultBufferTime\n let bufferAfter = config.defaultBufferTime\n\n if (itemServiceId) {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (req.payload.findByID as any)({\n id: itemServiceId,\n collection: config.slugs.services,\n req,\n })\n if (service) {\n bufferBefore = (service.bufferTimeBefore as number) ?? config.defaultBufferTime\n bufferAfter = (service.bufferTimeAfter as number) ?? config.defaultBufferTime\n }\n } catch {\n // Use defaults if service lookup fails\n }\n }\n\n // Other items of this same booking on the same resource count as occupancy\n // so two items can't double-book one resource within one create (review A5).\n const siblingItems = items.filter((other, j) => j !== i && other.resource === item.resource)\n\n const result = await checkAvailability({\n blockingStatuses: config.statusMachine.blockingStatuses,\n bufferAfter,\n bufferBefore,\n debug: dbg,\n endTime: new Date(item.endTime),\n excludeReservationId: isUpdate ? originalDoc?.id : undefined,\n getExternalBusy: config.getExternalBusy,\n guestCount: item.guestCount,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceId: item.resource,\n resourceSlug: config.slugs.resources,\n servicesSlug: config.slugs.services,\n siblingItems,\n startTime: new Date(item.startTime),\n })\n\n dbg.dbg('conflict_check', {\n available: result.available,\n index: i,\n resourceId: item.resource,\n startTime: new Date(item.startTime).toISOString(),\n })\n\n if (!result.available) {\n dbg.dbg('conflict_reject', {\n index: i,\n reason: result.reason,\n resourceId: item.resource,\n startTime: new Date(item.startTime).toISOString(),\n })\n\n throw new ValidationError({\n errors: [\n {\n message: result.reason ?? (req.t as PluginT)('reservation:errorConflict'),\n path: items.length > 1 ? `items.${i}.startTime` : 'startTime',\n },\n ],\n })\n }\n }\n\n return data\n }\n"],"names":["ValidationError","checkAvailability","mergeReservationData","schedulingFieldsChanged","createReserveDebug","extractId","resolveReservationItems","validateConflicts","config","context","data","operation","originalDoc","req","skipReservationHooks","dbg","payload","logger","debug","isUpdate","blockingStatuses","statusMachine","source","items","length","i","item","endTime","itemServiceId","service","bufferBefore","defaultBufferTime","bufferAfter","findByID","id","collection","slugs","services","bufferTimeBefore","bufferTimeAfter","siblingItems","filter","other","j","resource","result","Date","
|
|
1
|
+
{"version":3,"sources":["../../../src/hooks/reservations/validateConflicts.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { checkAvailability } from '../../services/AvailabilityService.js'\nimport {\n mergeReservationData,\n schedulingFieldsChanged,\n} from '../../utilities/reservationChanges.js'\nimport { createReserveDebug } from '../../utilities/reserveDebug.js'\nimport { extractId, resolveReservationItems } from '../../utilities/resolveReservationItems.js'\n\nexport const validateConflicts =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, originalDoc, req }) => {\n if (context?.skipReservationHooks) {\n return data\n }\n\n const dbg = createReserveDebug(req.payload.logger, config.debug)\n\n const isUpdate = operation === 'update'\n\n // Skip when an update touches no scheduling-relevant field, so reservations\n // booked under older buffers/schedules can still take benign edits.\n if (\n isUpdate &&\n !schedulingFieldsChanged({\n blockingStatuses: config.statusMachine.blockingStatuses,\n data: data as Record<string, unknown>,\n originalDoc: originalDoc as Record<string, unknown> | undefined,\n })\n ) {\n return data\n }\n\n // Validate the merged document — Payload usually backfills update patches\n // from originalDoc before beforeChange, but the hook must not rely on it.\n const source = isUpdate\n ? mergeReservationData(\n data as Record<string, unknown>,\n originalDoc as Record<string, unknown> | undefined,\n )\n : (data as Record<string, unknown>)\n\n const items = resolveReservationItems(source)\n\n if (items.length === 0) {\n return data\n }\n\n for (let i = 0; i < items.length; i++) {\n const item = items[i]\n if (!item.endTime) {\n continue\n }\n\n // Fetch buffer times from the item's own service (not just the primary)\n const itemServiceId = item.service ?? extractId(source.service)\n let bufferBefore = config.defaultBufferTime\n let bufferAfter = config.defaultBufferTime\n\n if (itemServiceId) {\n try {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (req.payload.findByID as any)({\n id: itemServiceId,\n collection: config.slugs.services,\n depth: 0,\n // Skip the resources join — internal logic never reads it, and without this\n // every service read becomes an aggregation with a $lookup.\n joins: false,\n req,\n })\n if (service) {\n bufferBefore = (service.bufferTimeBefore as number) ?? config.defaultBufferTime\n bufferAfter = (service.bufferTimeAfter as number) ?? config.defaultBufferTime\n }\n } catch {\n // Use defaults if service lookup fails\n }\n }\n\n // Other items of this same booking on the same resource count as occupancy\n // so two items can't double-book one resource within one create (review A5).\n const siblingItems = items.filter((other, j) => j !== i && other.resource === item.resource)\n\n const result = await checkAvailability({\n blockingStatuses: config.statusMachine.blockingStatuses,\n bufferAfter,\n bufferBefore,\n debug: dbg,\n endTime: new Date(item.endTime),\n excludeHoldToken:\n typeof context?.holdToken === 'string' ? context.holdToken : undefined,\n excludeReservationId: isUpdate ? originalDoc?.id : undefined,\n getExternalBusy: config.getExternalBusy,\n guestCount: item.guestCount,\n holdsSlug: config.slotHolds.enabled ? config.slugs.holds : undefined,\n payload: req.payload,\n req,\n reservationSlug: config.slugs.reservations,\n resourceId: item.resource,\n resourceSlug: config.slugs.resources,\n servicesSlug: config.slugs.services,\n siblingItems,\n startTime: new Date(item.startTime),\n })\n\n dbg.dbg('conflict_check', {\n available: result.available,\n index: i,\n resourceId: item.resource,\n startTime: new Date(item.startTime).toISOString(),\n })\n\n if (!result.available) {\n dbg.dbg('conflict_reject', {\n index: i,\n reason: result.reason,\n resourceId: item.resource,\n startTime: new Date(item.startTime).toISOString(),\n })\n\n throw new ValidationError({\n errors: [\n {\n message: result.reason ?? (req.t as PluginT)('reservation:errorConflict'),\n path: items.length > 1 && !item.fromParent ? `items.${i}.startTime` : 'startTime',\n },\n ],\n })\n }\n }\n\n return data\n }\n"],"names":["ValidationError","checkAvailability","mergeReservationData","schedulingFieldsChanged","createReserveDebug","extractId","resolveReservationItems","validateConflicts","config","context","data","operation","originalDoc","req","skipReservationHooks","dbg","payload","logger","debug","isUpdate","blockingStatuses","statusMachine","source","items","length","i","item","endTime","itemServiceId","service","bufferBefore","defaultBufferTime","bufferAfter","findByID","id","collection","slugs","services","depth","joins","bufferTimeBefore","bufferTimeAfter","siblingItems","filter","other","j","resource","result","Date","excludeHoldToken","holdToken","undefined","excludeReservationId","getExternalBusy","guestCount","holdsSlug","slotHolds","enabled","holds","reservationSlug","reservations","resourceId","resourceSlug","resources","servicesSlug","startTime","available","index","toISOString","reason","errors","message","t","path","fromParent"],"mappings":"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"}
|
|
@@ -69,6 +69,9 @@ export const validateGuestBooking = (config)=>async ({ context, data, operation,
|
|
|
69
69
|
id: serviceId,
|
|
70
70
|
collection: config.slugs.services,
|
|
71
71
|
depth: 0,
|
|
72
|
+
// Skip the resources join — internal logic never reads it, and without this
|
|
73
|
+
// every service read becomes an aggregation with a $lookup.
|
|
74
|
+
joins: false,
|
|
72
75
|
req
|
|
73
76
|
});
|
|
74
77
|
if (!resolveGuestBookingAllowed(service, config.allowGuestBooking)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/hooks/reservations/validateGuestBooking.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { randomUUID } from 'node:crypto'\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { resolveGuestBookingAllowed } from '../../utilities/guestBooking.js'\n\ntype GuestData = { email?: string; name?: string; phone?: string }\n\nexport const validateGuestBooking =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, req }) => {\n if (context?.skipReservationHooks) {\n return data\n }\n if (operation !== 'create') {\n return data\n }\n\n const customer = data?.customer\n const guest = data?.guest as GuestData | undefined\n const hasCustomer = customer != null && customer !== ''\n const hasGuest =\n guest != null && (Boolean(guest.name) || Boolean(guest.email) || Boolean(guest.phone))\n\n if (!hasCustomer && !hasGuest) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorGuestOrCustomerRequired'),\n path: 'customer',\n },\n ],\n })\n }\n\n if (hasCustomer && hasGuest) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorGuestAndCustomer'),\n path: 'guest',\n },\n ],\n })\n }\n\n if (hasCustomer) {\n return data\n }\n\n // Guest path\n if (!guest?.name) {\n throw new ValidationError({\n errors: [\n { message: (req.t as PluginT)('reservation:errorGuestNameRequired'), path: 'guest.name' },\n ],\n })\n }\n if (!guest.email && !guest.phone) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorGuestContactRequired'),\n path: 'guest.email',\n },\n ],\n })\n }\n\n // Gate by service — admins (non-customer collection users) bypass.\n const isAdmin = req.user != null && req.user.collection !== config.slugs.customers\n if (!isAdmin) {\n const serviceId =\n typeof data.service === 'object'\n ? (data.service as { id?: string } | null)?.id\n : data.service\n // `service` is a required field on the collection, so Payload's field\n // validation (which runs before this beforeChange hook) guarantees it is\n // present for any booking that reaches here. The guard is purely defensive.\n if (serviceId) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (req.payload.findByID as any)({\n id: serviceId,\n collection: config.slugs.services,\n depth: 0,\n req,\n })\n if (!resolveGuestBookingAllowed(service, config.allowGuestBooking)) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorGuestNotAllowed'),\n path: 'guest',\n },\n ],\n })\n }\n }\n }\n\n // Always server-generate the cancellation token the host project delivers\n // to the guest — never honor a caller-supplied value (it's a secret).\n data.cancellationToken = randomUUID()\n\n return data\n }\n"],"names":["randomUUID","ValidationError","resolveGuestBookingAllowed","validateGuestBooking","config","context","data","operation","req","skipReservationHooks","customer","guest","hasCustomer","hasGuest","Boolean","name","email","phone","errors","message","t","path","isAdmin","user","collection","slugs","customers","serviceId","service","id","payload","findByID","services","depth","allowGuestBooking","cancellationToken"],"mappings":"AAEA,SAASA,UAAU,QAAQ,cAAa;AACxC,SAASC,eAAe,QAAQ,UAAS;AAKzC,SAASC,0BAA0B,QAAQ,kCAAiC;AAI5E,OAAO,MAAMC,uBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,GAAG,EAAE;QACtC,IAAIH,SAASI,sBAAsB;YACjC,OAAOH;QACT;QACA,IAAIC,cAAc,UAAU;YAC1B,OAAOD;QACT;QAEA,MAAMI,WAAWJ,MAAMI;QACvB,MAAMC,QAAQL,MAAMK;QACpB,MAAMC,cAAcF,YAAY,QAAQA,aAAa;QACrD,MAAMG,WACJF,SAAS,QAASG,CAAAA,QAAQH,MAAMI,IAAI,KAAKD,QAAQH,MAAMK,KAAK,KAAKF,QAAQH,MAAMM,KAAK,CAAA;QAEtF,IAAI,CAACL,eAAe,CAACC,UAAU;YAC7B,MAAM,IAAIZ,gBAAgB;gBACxBiB,QAAQ;oBACN;wBACEC,SAAS,AAACX,IAAIY,CAAC,CAAa;wBAC5BC,MAAM;oBACR;iBACD;YACH;QACF;QAEA,IAAIT,eAAeC,UAAU;YAC3B,MAAM,IAAIZ,gBAAgB;gBACxBiB,QAAQ;oBACN;wBACEC,SAAS,AAACX,IAAIY,CAAC,CAAa;wBAC5BC,MAAM;oBACR;iBACD;YACH;QACF;QAEA,IAAIT,aAAa;YACf,OAAON;QACT;QAEA,aAAa;QACb,IAAI,CAACK,OAAOI,MAAM;YAChB,MAAM,IAAId,gBAAgB;gBACxBiB,QAAQ;oBACN;wBAAEC,SAAS,AAACX,IAAIY,CAAC,CAAa;wBAAuCC,MAAM;oBAAa;iBACzF;YACH;QACF;QACA,IAAI,CAACV,MAAMK,KAAK,IAAI,CAACL,MAAMM,KAAK,EAAE;YAChC,MAAM,IAAIhB,gBAAgB;gBACxBiB,QAAQ;oBACN;wBACEC,SAAS,AAACX,IAAIY,CAAC,CAAa;wBAC5BC,MAAM;oBACR;iBACD;YACH;QACF;QAEA,mEAAmE;QACnE,MAAMC,UAAUd,IAAIe,IAAI,IAAI,QAAQf,IAAIe,IAAI,CAACC,UAAU,KAAKpB,OAAOqB,KAAK,CAACC,SAAS;QAClF,IAAI,CAACJ,SAAS;YACZ,MAAMK,YACJ,OAAOrB,KAAKsB,OAAO,KAAK,WACnBtB,KAAKsB,OAAO,EAA6BC,KAC1CvB,KAAKsB,OAAO;YAClB,sEAAsE;YACtE,yEAAyE;YACzE,4EAA4E;YAC5E,IAAID,WAAW;gBACb,8DAA8D;gBAC9D,MAAMC,UAAU,MAAM,AAACpB,IAAIsB,OAAO,CAACC,QAAQ,CAAS;oBAClDF,IAAIF;oBACJH,YAAYpB,OAAOqB,KAAK,CAACO,QAAQ;oBACjCC,OAAO;
|
|
1
|
+
{"version":3,"sources":["../../../src/hooks/reservations/validateGuestBooking.ts"],"sourcesContent":["import type { CollectionBeforeChangeHook } from 'payload'\n\nimport { randomUUID } from 'node:crypto'\nimport { ValidationError } from 'payload'\n\nimport type { PluginT } from '../../translations/index.js'\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\nimport { resolveGuestBookingAllowed } from '../../utilities/guestBooking.js'\n\ntype GuestData = { email?: string; name?: string; phone?: string }\n\nexport const validateGuestBooking =\n (config: ResolvedReservationPluginConfig): CollectionBeforeChangeHook =>\n async ({ context, data, operation, req }) => {\n if (context?.skipReservationHooks) {\n return data\n }\n if (operation !== 'create') {\n return data\n }\n\n const customer = data?.customer\n const guest = data?.guest as GuestData | undefined\n const hasCustomer = customer != null && customer !== ''\n const hasGuest =\n guest != null && (Boolean(guest.name) || Boolean(guest.email) || Boolean(guest.phone))\n\n if (!hasCustomer && !hasGuest) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorGuestOrCustomerRequired'),\n path: 'customer',\n },\n ],\n })\n }\n\n if (hasCustomer && hasGuest) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorGuestAndCustomer'),\n path: 'guest',\n },\n ],\n })\n }\n\n if (hasCustomer) {\n return data\n }\n\n // Guest path\n if (!guest?.name) {\n throw new ValidationError({\n errors: [\n { message: (req.t as PluginT)('reservation:errorGuestNameRequired'), path: 'guest.name' },\n ],\n })\n }\n if (!guest.email && !guest.phone) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorGuestContactRequired'),\n path: 'guest.email',\n },\n ],\n })\n }\n\n // Gate by service — admins (non-customer collection users) bypass.\n const isAdmin = req.user != null && req.user.collection !== config.slugs.customers\n if (!isAdmin) {\n const serviceId =\n typeof data.service === 'object'\n ? (data.service as { id?: string } | null)?.id\n : data.service\n // `service` is a required field on the collection, so Payload's field\n // validation (which runs before this beforeChange hook) guarantees it is\n // present for any booking that reaches here. The guard is purely defensive.\n if (serviceId) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const service = await (req.payload.findByID as any)({\n id: serviceId,\n collection: config.slugs.services,\n depth: 0,\n // Skip the resources join — internal logic never reads it, and without this\n // every service read becomes an aggregation with a $lookup.\n joins: false,\n req,\n })\n if (!resolveGuestBookingAllowed(service, config.allowGuestBooking)) {\n throw new ValidationError({\n errors: [\n {\n message: (req.t as PluginT)('reservation:errorGuestNotAllowed'),\n path: 'guest',\n },\n ],\n })\n }\n }\n }\n\n // Always server-generate the cancellation token the host project delivers\n // to the guest — never honor a caller-supplied value (it's a secret).\n data.cancellationToken = randomUUID()\n\n return data\n }\n"],"names":["randomUUID","ValidationError","resolveGuestBookingAllowed","validateGuestBooking","config","context","data","operation","req","skipReservationHooks","customer","guest","hasCustomer","hasGuest","Boolean","name","email","phone","errors","message","t","path","isAdmin","user","collection","slugs","customers","serviceId","service","id","payload","findByID","services","depth","joins","allowGuestBooking","cancellationToken"],"mappings":"AAEA,SAASA,UAAU,QAAQ,cAAa;AACxC,SAASC,eAAe,QAAQ,UAAS;AAKzC,SAASC,0BAA0B,QAAQ,kCAAiC;AAI5E,OAAO,MAAMC,uBACX,CAACC,SACD,OAAO,EAAEC,OAAO,EAAEC,IAAI,EAAEC,SAAS,EAAEC,GAAG,EAAE;QACtC,IAAIH,SAASI,sBAAsB;YACjC,OAAOH;QACT;QACA,IAAIC,cAAc,UAAU;YAC1B,OAAOD;QACT;QAEA,MAAMI,WAAWJ,MAAMI;QACvB,MAAMC,QAAQL,MAAMK;QACpB,MAAMC,cAAcF,YAAY,QAAQA,aAAa;QACrD,MAAMG,WACJF,SAAS,QAASG,CAAAA,QAAQH,MAAMI,IAAI,KAAKD,QAAQH,MAAMK,KAAK,KAAKF,QAAQH,MAAMM,KAAK,CAAA;QAEtF,IAAI,CAACL,eAAe,CAACC,UAAU;YAC7B,MAAM,IAAIZ,gBAAgB;gBACxBiB,QAAQ;oBACN;wBACEC,SAAS,AAACX,IAAIY,CAAC,CAAa;wBAC5BC,MAAM;oBACR;iBACD;YACH;QACF;QAEA,IAAIT,eAAeC,UAAU;YAC3B,MAAM,IAAIZ,gBAAgB;gBACxBiB,QAAQ;oBACN;wBACEC,SAAS,AAACX,IAAIY,CAAC,CAAa;wBAC5BC,MAAM;oBACR;iBACD;YACH;QACF;QAEA,IAAIT,aAAa;YACf,OAAON;QACT;QAEA,aAAa;QACb,IAAI,CAACK,OAAOI,MAAM;YAChB,MAAM,IAAId,gBAAgB;gBACxBiB,QAAQ;oBACN;wBAAEC,SAAS,AAACX,IAAIY,CAAC,CAAa;wBAAuCC,MAAM;oBAAa;iBACzF;YACH;QACF;QACA,IAAI,CAACV,MAAMK,KAAK,IAAI,CAACL,MAAMM,KAAK,EAAE;YAChC,MAAM,IAAIhB,gBAAgB;gBACxBiB,QAAQ;oBACN;wBACEC,SAAS,AAACX,IAAIY,CAAC,CAAa;wBAC5BC,MAAM;oBACR;iBACD;YACH;QACF;QAEA,mEAAmE;QACnE,MAAMC,UAAUd,IAAIe,IAAI,IAAI,QAAQf,IAAIe,IAAI,CAACC,UAAU,KAAKpB,OAAOqB,KAAK,CAACC,SAAS;QAClF,IAAI,CAACJ,SAAS;YACZ,MAAMK,YACJ,OAAOrB,KAAKsB,OAAO,KAAK,WACnBtB,KAAKsB,OAAO,EAA6BC,KAC1CvB,KAAKsB,OAAO;YAClB,sEAAsE;YACtE,yEAAyE;YACzE,4EAA4E;YAC5E,IAAID,WAAW;gBACb,8DAA8D;gBAC9D,MAAMC,UAAU,MAAM,AAACpB,IAAIsB,OAAO,CAACC,QAAQ,CAAS;oBAClDF,IAAIF;oBACJH,YAAYpB,OAAOqB,KAAK,CAACO,QAAQ;oBACjCC,OAAO;oBACP,4EAA4E;oBAC5E,4DAA4D;oBAC5DC,OAAO;oBACP1B;gBACF;gBACA,IAAI,CAACN,2BAA2B0B,SAASxB,OAAO+B,iBAAiB,GAAG;oBAClE,MAAM,IAAIlC,gBAAgB;wBACxBiB,QAAQ;4BACN;gCACEC,SAAS,AAACX,IAAIY,CAAC,CAAa;gCAC5BC,MAAM;4BACR;yBACD;oBACH;gBACF;YACF;QACF;QAEA,0EAA0E;QAC1E,sEAAsE;QACtEf,KAAK8B,iBAAiB,GAAGpC;QAEzB,OAAOM;IACT,EAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { CollectionBeforeDeleteHook } from 'payload';
|
|
2
|
+
import type { ResolvedReservationPluginConfig } from '../../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Refuse to delete a Service or Resource that other documents still point at.
|
|
5
|
+
*
|
|
6
|
+
* Without this the two databases disagree, and neither behaviour was designed.
|
|
7
|
+
* The referencing fields (Reservations.service/resource, Schedules.resource)
|
|
8
|
+
* are `required: true`, so Postgres and SQLite make those columns NOT NULL
|
|
9
|
+
* while the drizzle adapter emits ON DELETE SET NULL for both — the delete
|
|
10
|
+
* fails with a raw 23502/SQLITE_CONSTRAINT_NOTNULL that means nothing to the
|
|
11
|
+
* person clicking the button. Mongo has no such constraint, so the same
|
|
12
|
+
* delete succeeds and leaves the referencing document pointing at nothing.
|
|
13
|
+
*
|
|
14
|
+
* The plugin already ships `active` for retiring something without destroying
|
|
15
|
+
* booking (or schedule) history, which is what the error points people at.
|
|
16
|
+
*
|
|
17
|
+
* `extraChecks` lets a caller add more referencing collections beyond
|
|
18
|
+
* Reservations — e.g. Resources is also referenced by Schedules.resource,
|
|
19
|
+
* which has no items[]-style nesting so a plain field-equals check suffices.
|
|
20
|
+
* Every check is a single `count` query (one query per related collection),
|
|
21
|
+
* never one query per referencing document.
|
|
22
|
+
*
|
|
23
|
+
* The counts run SEQUENTIALLY, and that is load-bearing rather than an
|
|
24
|
+
* oversight. This hook runs inside the transaction `deleteByID` opened, on the
|
|
25
|
+
* caller's `req`, and a MongoDB ClientSession cannot carry concurrent operations
|
|
26
|
+
* inside a transaction. Running these as a `Promise.all` made two `count`s share
|
|
27
|
+
* one session: the loser's `count` calls `killTransaction` from its own catch,
|
|
28
|
+
* which rolls back and clears the transaction the DELETE owns, and the delete
|
|
29
|
+
* then fails with `NoSuchTransaction` ("transaction number N does not match any
|
|
30
|
+
* in-progress transactions") instead of this hook's actionable 400. Two count
|
|
31
|
+
* queries do not need parallelism.
|
|
32
|
+
*/
|
|
33
|
+
export declare function preventDeleteWhenReferenced({ config, extraChecks, field, label, }: {
|
|
34
|
+
config: ResolvedReservationPluginConfig;
|
|
35
|
+
extraChecks?: Array<{
|
|
36
|
+
collection: string;
|
|
37
|
+
field: string;
|
|
38
|
+
label: string;
|
|
39
|
+
}>;
|
|
40
|
+
field: 'resource' | 'service';
|
|
41
|
+
label: string;
|
|
42
|
+
}): CollectionBeforeDeleteHook;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { APIError } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* Refuse to delete a Service or Resource that other documents still point at.
|
|
4
|
+
*
|
|
5
|
+
* Without this the two databases disagree, and neither behaviour was designed.
|
|
6
|
+
* The referencing fields (Reservations.service/resource, Schedules.resource)
|
|
7
|
+
* are `required: true`, so Postgres and SQLite make those columns NOT NULL
|
|
8
|
+
* while the drizzle adapter emits ON DELETE SET NULL for both — the delete
|
|
9
|
+
* fails with a raw 23502/SQLITE_CONSTRAINT_NOTNULL that means nothing to the
|
|
10
|
+
* person clicking the button. Mongo has no such constraint, so the same
|
|
11
|
+
* delete succeeds and leaves the referencing document pointing at nothing.
|
|
12
|
+
*
|
|
13
|
+
* The plugin already ships `active` for retiring something without destroying
|
|
14
|
+
* booking (or schedule) history, which is what the error points people at.
|
|
15
|
+
*
|
|
16
|
+
* `extraChecks` lets a caller add more referencing collections beyond
|
|
17
|
+
* Reservations — e.g. Resources is also referenced by Schedules.resource,
|
|
18
|
+
* which has no items[]-style nesting so a plain field-equals check suffices.
|
|
19
|
+
* Every check is a single `count` query (one query per related collection),
|
|
20
|
+
* never one query per referencing document.
|
|
21
|
+
*
|
|
22
|
+
* The counts run SEQUENTIALLY, and that is load-bearing rather than an
|
|
23
|
+
* oversight. This hook runs inside the transaction `deleteByID` opened, on the
|
|
24
|
+
* caller's `req`, and a MongoDB ClientSession cannot carry concurrent operations
|
|
25
|
+
* inside a transaction. Running these as a `Promise.all` made two `count`s share
|
|
26
|
+
* one session: the loser's `count` calls `killTransaction` from its own catch,
|
|
27
|
+
* which rolls back and clears the transaction the DELETE owns, and the delete
|
|
28
|
+
* then fails with `NoSuchTransaction` ("transaction number N does not match any
|
|
29
|
+
* in-progress transactions") instead of this hook's actionable 400. Two count
|
|
30
|
+
* queries do not need parallelism.
|
|
31
|
+
*/ export function preventDeleteWhenReferenced({ config, extraChecks = [], field, label }) {
|
|
32
|
+
return async ({ id, context, req })=>{
|
|
33
|
+
if (context?.skipReservationHooks) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
+
const countFn = req.payload.count;
|
|
38
|
+
const reservations = await countFn({
|
|
39
|
+
collection: config.slugs.reservations,
|
|
40
|
+
req,
|
|
41
|
+
where: {
|
|
42
|
+
or: [
|
|
43
|
+
{
|
|
44
|
+
[field]: {
|
|
45
|
+
equals: id
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
[`items.${field}`]: {
|
|
50
|
+
equals: id
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
const extras = [];
|
|
57
|
+
for (const check of extraChecks){
|
|
58
|
+
extras.push(await countFn({
|
|
59
|
+
collection: check.collection,
|
|
60
|
+
req,
|
|
61
|
+
where: {
|
|
62
|
+
[check.field]: {
|
|
63
|
+
equals: id
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
const blocking = [
|
|
69
|
+
{
|
|
70
|
+
count: reservations.totalDocs,
|
|
71
|
+
label: 'reservation'
|
|
72
|
+
},
|
|
73
|
+
...extraChecks.map((check, index)=>({
|
|
74
|
+
count: extras[index].totalDocs,
|
|
75
|
+
label: check.label
|
|
76
|
+
}))
|
|
77
|
+
].filter((entry)=>entry.count > 0);
|
|
78
|
+
if (blocking.length > 0) {
|
|
79
|
+
const total = blocking.reduce((sum, entry)=>sum + entry.count, 0);
|
|
80
|
+
const parts = blocking.map((entry)=>`${entry.count} ${entry.label}${entry.count === 1 ? '' : 's'}`).join(' and ');
|
|
81
|
+
const verb = total === 1 ? 'still references' : 'still reference';
|
|
82
|
+
throw new APIError(`Cannot delete this ${label}: ${parts} ${verb} it. Uncheck "active" to retire it instead — that stops new bookings while keeping existing ones intact.`, 400);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
//# sourceMappingURL=preventDeleteWhenReferenced.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/hooks/shared/preventDeleteWhenReferenced.ts"],"sourcesContent":["import type { CollectionBeforeDeleteHook } from 'payload'\n\nimport { APIError } from 'payload'\n\nimport type { ResolvedReservationPluginConfig } from '../../types.js'\n\n/**\n * Refuse to delete a Service or Resource that other documents still point at.\n *\n * Without this the two databases disagree, and neither behaviour was designed.\n * The referencing fields (Reservations.service/resource, Schedules.resource)\n * are `required: true`, so Postgres and SQLite make those columns NOT NULL\n * while the drizzle adapter emits ON DELETE SET NULL for both — the delete\n * fails with a raw 23502/SQLITE_CONSTRAINT_NOTNULL that means nothing to the\n * person clicking the button. Mongo has no such constraint, so the same\n * delete succeeds and leaves the referencing document pointing at nothing.\n *\n * The plugin already ships `active` for retiring something without destroying\n * booking (or schedule) history, which is what the error points people at.\n *\n * `extraChecks` lets a caller add more referencing collections beyond\n * Reservations — e.g. Resources is also referenced by Schedules.resource,\n * which has no items[]-style nesting so a plain field-equals check suffices.\n * Every check is a single `count` query (one query per related collection),\n * never one query per referencing document.\n *\n * The counts run SEQUENTIALLY, and that is load-bearing rather than an\n * oversight. This hook runs inside the transaction `deleteByID` opened, on the\n * caller's `req`, and a MongoDB ClientSession cannot carry concurrent operations\n * inside a transaction. Running these as a `Promise.all` made two `count`s share\n * one session: the loser's `count` calls `killTransaction` from its own catch,\n * which rolls back and clears the transaction the DELETE owns, and the delete\n * then fails with `NoSuchTransaction` (\"transaction number N does not match any\n * in-progress transactions\") instead of this hook's actionable 400. Two count\n * queries do not need parallelism.\n */\nexport function preventDeleteWhenReferenced({\n config,\n extraChecks = [],\n field,\n label,\n}: {\n config: ResolvedReservationPluginConfig\n extraChecks?: Array<{ collection: string; field: string; label: string }>\n field: 'resource' | 'service'\n label: string\n}): CollectionBeforeDeleteHook {\n return async ({ id, context, req }) => {\n if (context?.skipReservationHooks) {return}\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const countFn = req.payload.count as any\n\n const reservations = (await countFn({\n collection: config.slugs.reservations,\n req,\n where: {\n or: [{ [field]: { equals: id } }, { [`items.${field}`]: { equals: id } }],\n },\n })) as { totalDocs: number }\n\n const extras: Array<{ totalDocs: number }> = []\n for (const check of extraChecks) {\n extras.push(\n (await countFn({\n collection: check.collection,\n req,\n where: { [check.field]: { equals: id } },\n })) as { totalDocs: number },\n )\n }\n\n const blocking = [\n { count: reservations.totalDocs, label: 'reservation' },\n ...extraChecks.map((check, index) => ({\n count: extras[index].totalDocs,\n label: check.label,\n })),\n ].filter((entry) => entry.count > 0)\n\n if (blocking.length > 0) {\n const total = blocking.reduce((sum, entry) => sum + entry.count, 0)\n const parts = blocking\n .map((entry) => `${entry.count} ${entry.label}${entry.count === 1 ? '' : 's'}`)\n .join(' and ')\n const verb = total === 1 ? 'still references' : 'still reference'\n\n throw new APIError(\n `Cannot delete this ${label}: ${parts} ${verb} it. Uncheck \"active\" to retire it instead — that stops new bookings while keeping existing ones intact.`,\n 400,\n )\n }\n }\n}\n"],"names":["APIError","preventDeleteWhenReferenced","config","extraChecks","field","label","id","context","req","skipReservationHooks","countFn","payload","count","reservations","collection","slugs","where","or","equals","extras","check","push","blocking","totalDocs","map","index","filter","entry","length","total","reduce","sum","parts","join","verb"],"mappings":"AAEA,SAASA,QAAQ,QAAQ,UAAS;AAIlC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BC,GACD,OAAO,SAASC,4BAA4B,EAC1CC,MAAM,EACNC,cAAc,EAAE,EAChBC,KAAK,EACLC,KAAK,EAMN;IACC,OAAO,OAAO,EAAEC,EAAE,EAAEC,OAAO,EAAEC,GAAG,EAAE;QAChC,IAAID,SAASE,sBAAsB;YAAC;QAAM;QAE1C,8DAA8D;QAC9D,MAAMC,UAAUF,IAAIG,OAAO,CAACC,KAAK;QAEjC,MAAMC,eAAgB,MAAMH,QAAQ;YAClCI,YAAYZ,OAAOa,KAAK,CAACF,YAAY;YACrCL;YACAQ,OAAO;gBACLC,IAAI;oBAAC;wBAAE,CAACb,MAAM,EAAE;4BAAEc,QAAQZ;wBAAG;oBAAE;oBAAG;wBAAE,CAAC,CAAC,MAAM,EAAEF,OAAO,CAAC,EAAE;4BAAEc,QAAQZ;wBAAG;oBAAE;iBAAE;YAC3E;QACF;QAEA,MAAMa,SAAuC,EAAE;QAC/C,KAAK,MAAMC,SAASjB,YAAa;YAC/BgB,OAAOE,IAAI,CACR,MAAMX,QAAQ;gBACbI,YAAYM,MAAMN,UAAU;gBAC5BN;gBACAQ,OAAO;oBAAE,CAACI,MAAMhB,KAAK,CAAC,EAAE;wBAAEc,QAAQZ;oBAAG;gBAAE;YACzC;QAEJ;QAEA,MAAMgB,WAAW;YACf;gBAAEV,OAAOC,aAAaU,SAAS;gBAAElB,OAAO;YAAc;eACnDF,YAAYqB,GAAG,CAAC,CAACJ,OAAOK,QAAW,CAAA;oBACpCb,OAAOO,MAAM,CAACM,MAAM,CAACF,SAAS;oBAC9BlB,OAAOe,MAAMf,KAAK;gBACpB,CAAA;SACD,CAACqB,MAAM,CAAC,CAACC,QAAUA,MAAMf,KAAK,GAAG;QAElC,IAAIU,SAASM,MAAM,GAAG,GAAG;YACvB,MAAMC,QAAQP,SAASQ,MAAM,CAAC,CAACC,KAAKJ,QAAUI,MAAMJ,MAAMf,KAAK,EAAE;YACjE,MAAMoB,QAAQV,SACXE,GAAG,CAAC,CAACG,QAAU,GAAGA,MAAMf,KAAK,CAAC,CAAC,EAAEe,MAAMtB,KAAK,GAAGsB,MAAMf,KAAK,KAAK,IAAI,KAAK,KAAK,EAC7EqB,IAAI,CAAC;YACR,MAAMC,OAAOL,UAAU,IAAI,qBAAqB;YAEhD,MAAM,IAAI7B,SACR,CAAC,mBAAmB,EAAEK,MAAM,EAAE,EAAE2B,MAAM,CAAC,EAAEE,KAAK,wGAAwG,CAAC,EACvJ;QAEJ;IACF;AACF"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { provisionStaffResource, roleMatches } from './hooks/users/provisionStaffResource.js';
|
|
2
2
|
export { payloadReserve } from './plugin.js';
|
|
3
3
|
export { buildOverlapQuery, checkAvailability, computeEndTime, getAvailableSlots, isBlockingStatus, validateTransition, } from './services/index.js';
|
|
4
|
+
export type { EmptyReason } from './services/index.js';
|
|
4
5
|
export type { CapacityMode, DurationType, ExternalBusyInterval, GetExternalBusy, ReservationPluginConfig, ReservationPluginHooks, ResolvedReservationPluginConfig, StaffProvisioningConfig, StatusMachineConfig, } from './types.js';
|
|
5
6
|
export { DEFAULT_STATUS_MACHINE, VALID_STATUS_TRANSITIONS } from './types.js';
|
|
6
7
|
export { mergeResourceIds } from './utilities/resolveRequiredResources.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { provisionStaffResource, roleMatches } from './hooks/users/provisionStaffResource.js'\nexport { payloadReserve } from './plugin.js'\nexport {\n buildOverlapQuery,\n checkAvailability,\n computeEndTime,\n getAvailableSlots,\n isBlockingStatus,\n validateTransition,\n} from './services/index.js'\nexport type {\n CapacityMode,\n DurationType,\n ExternalBusyInterval,\n GetExternalBusy,\n ReservationPluginConfig,\n ReservationPluginHooks,\n ResolvedReservationPluginConfig,\n StaffProvisioningConfig,\n StatusMachineConfig,\n} from './types.js'\nexport { DEFAULT_STATUS_MACHINE, VALID_STATUS_TRANSITIONS } from './types.js'\nexport { mergeResourceIds } from './utilities/resolveRequiredResources.js'\nexport type { ResolvedItem } from './utilities/resolveReservationItems.js'\nexport { resolveReservationItems } from './utilities/resolveReservationItems.js'\nexport { buildSelectOptions } from './utilities/selectOptions.js'\nexport { intersectIntervals } from './utilities/slotUtils.js'\n"],"names":["provisionStaffResource","roleMatches","payloadReserve","buildOverlapQuery","checkAvailability","computeEndTime","getAvailableSlots","isBlockingStatus","validateTransition","DEFAULT_STATUS_MACHINE","VALID_STATUS_TRANSITIONS","mergeResourceIds","resolveReservationItems","buildSelectOptions","intersectIntervals"],"mappings":"AAAA,SAASA,sBAAsB,EAAEC,WAAW,QAAQ,0CAAyC;AAC7F,SAASC,cAAc,QAAQ,cAAa;AAC5C,SACEC,iBAAiB,EACjBC,iBAAiB,EACjBC,cAAc,EACdC,iBAAiB,EACjBC,gBAAgB,EAChBC,kBAAkB,QACb,sBAAqB;
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { provisionStaffResource, roleMatches } from './hooks/users/provisionStaffResource.js'\nexport { payloadReserve } from './plugin.js'\nexport {\n buildOverlapQuery,\n checkAvailability,\n computeEndTime,\n getAvailableSlots,\n isBlockingStatus,\n validateTransition,\n} from './services/index.js'\nexport type { EmptyReason } from './services/index.js'\nexport type {\n CapacityMode,\n DurationType,\n ExternalBusyInterval,\n GetExternalBusy,\n ReservationPluginConfig,\n ReservationPluginHooks,\n ResolvedReservationPluginConfig,\n StaffProvisioningConfig,\n StatusMachineConfig,\n} from './types.js'\nexport { DEFAULT_STATUS_MACHINE, VALID_STATUS_TRANSITIONS } from './types.js'\nexport { mergeResourceIds } from './utilities/resolveRequiredResources.js'\nexport type { ResolvedItem } from './utilities/resolveReservationItems.js'\nexport { resolveReservationItems } from './utilities/resolveReservationItems.js'\nexport { buildSelectOptions } from './utilities/selectOptions.js'\nexport { intersectIntervals } from './utilities/slotUtils.js'\n"],"names":["provisionStaffResource","roleMatches","payloadReserve","buildOverlapQuery","checkAvailability","computeEndTime","getAvailableSlots","isBlockingStatus","validateTransition","DEFAULT_STATUS_MACHINE","VALID_STATUS_TRANSITIONS","mergeResourceIds","resolveReservationItems","buildSelectOptions","intersectIntervals"],"mappings":"AAAA,SAASA,sBAAsB,EAAEC,WAAW,QAAQ,0CAAyC;AAC7F,SAASC,cAAc,QAAQ,cAAa;AAC5C,SACEC,iBAAiB,EACjBC,iBAAiB,EACjBC,cAAc,EACdC,iBAAiB,EACjBC,gBAAgB,EAChBC,kBAAkB,QACb,sBAAqB;AAa5B,SAASC,sBAAsB,EAAEC,wBAAwB,QAAQ,aAAY;AAC7E,SAASC,gBAAgB,QAAQ,0CAAyC;AAE1E,SAASC,uBAAuB,QAAQ,yCAAwC;AAChF,SAASC,kBAAkB,QAAQ,+BAA8B;AACjE,SAASC,kBAAkB,QAAQ,2BAA0B"}
|