@agent-native/scheduling 0.2.4 → 0.2.5-nightly-20260929152122
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/dist/actions/assign-round-robin-host.js +3 -1
- package/dist/actions/assign-round-robin-host.js.map +1 -1
- package/dist/actions/create-booking.js +3 -3
- package/dist/actions/create-booking.js.map +1 -1
- package/dist/actions/submit-routing-form-response.js +4 -4
- package/dist/actions/submit-routing-form-response.js.map +1 -1
- package/dist/server/booking-service.d.ts.map +1 -1
- package/dist/server/booking-service.js +27 -20
- package/dist/server/booking-service.js.map +1 -1
- package/dist/server/bookings-repo.d.ts +1 -1
- package/dist/server/bookings-repo.d.ts.map +1 -1
- package/dist/server/bookings-repo.js +8 -3
- package/dist/server/bookings-repo.js.map +1 -1
- package/dist/server/event-types-repo.d.ts.map +1 -1
- package/dist/server/event-types-repo.js +6 -2
- package/dist/server/event-types-repo.js.map +1 -1
- package/dist/server/hooks.d.ts.map +1 -1
- package/dist/server/hooks.js +68 -20
- package/dist/server/hooks.js.map +1 -1
- package/dist/shared/index.d.ts +4 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/actions/assign-round-robin-host.ts +8 -0
- package/src/actions/create-booking.ts +3 -4
- package/src/actions/event-type-authz.spec.ts +35 -0
- package/src/actions/submit-routing-form-response.ts +10 -4
- package/src/server/booking-service.spec.ts +458 -1
- package/src/server/booking-service.ts +27 -20
- package/src/server/bookings-repo.ts +21 -1
- package/src/server/event-types-repo.ts +16 -2
- package/src/server/hooks.ts +124 -23
- package/src/shared/index.ts +4 -0
package/src/server/hooks.ts
CHANGED
|
@@ -1,27 +1,38 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { accessFilter } from "@agent-native/core/sharing";
|
|
2
|
+
import { and, eq, inArray, isNull, like, or, sql } from "drizzle-orm";
|
|
2
3
|
import { nanoid } from "nanoid";
|
|
3
4
|
|
|
4
5
|
import { addMinutes } from "../core/time.js";
|
|
5
6
|
import type { Booking, Workflow, WorkflowStep } from "../shared/index.js";
|
|
6
7
|
import { getSchedulingContext } from "./context.js";
|
|
7
8
|
|
|
9
|
+
interface BookingPrincipal {
|
|
10
|
+
ownerEmail: string;
|
|
11
|
+
orgId: string | null;
|
|
12
|
+
teamId: string | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
8
15
|
export async function onBookingCreated(booking: Booking): Promise<void> {
|
|
16
|
+
const principal = getBookingPrincipal(booking);
|
|
9
17
|
await materializeReminders(booking, "new-booking");
|
|
10
18
|
await materializeRemindersBefore(booking);
|
|
11
|
-
await enqueueWebhooks(booking, "BOOKING_CREATED");
|
|
19
|
+
await enqueueWebhooks(booking, "BOOKING_CREATED", principal);
|
|
12
20
|
}
|
|
13
21
|
|
|
14
22
|
export async function onBookingRescheduled(
|
|
15
23
|
_original: Booking,
|
|
16
24
|
next: Booking,
|
|
17
25
|
): Promise<void> {
|
|
18
|
-
await
|
|
19
|
-
|
|
26
|
+
const scopedBooking = await withBookingTeamScope(next);
|
|
27
|
+
const principal = getBookingPrincipal(scopedBooking);
|
|
28
|
+
await materializeReminders(scopedBooking, "reschedule");
|
|
29
|
+
await enqueueWebhooks(scopedBooking, "BOOKING_RESCHEDULED", principal);
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
export async function onBookingCancelled(booking: Booking): Promise<void> {
|
|
33
|
+
const principal = getBookingPrincipal(booking);
|
|
23
34
|
await materializeReminders(booking, "cancellation");
|
|
24
|
-
await enqueueWebhooks(booking, "BOOKING_CANCELLED");
|
|
35
|
+
await enqueueWebhooks(booking, "BOOKING_CANCELLED", principal);
|
|
25
36
|
const { getDb, schema } = getSchedulingContext();
|
|
26
37
|
await getDb()
|
|
27
38
|
.update(schema.scheduledReminders)
|
|
@@ -35,15 +46,47 @@ export async function onBookingCancelled(booking: Booking): Promise<void> {
|
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
export async function onBookingNoShow(booking: Booking): Promise<void> {
|
|
38
|
-
await
|
|
39
|
-
|
|
49
|
+
const scopedBooking = await withBookingTeamScope(booking);
|
|
50
|
+
const principal = getBookingPrincipal(scopedBooking);
|
|
51
|
+
await materializeReminders(scopedBooking, "no-show");
|
|
52
|
+
await enqueueWebhooks(scopedBooking, "BOOKING_NO_SHOW", principal);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function withBookingTeamScope(booking: Booking): Promise<Booking> {
|
|
56
|
+
if (booking.teamId) return booking;
|
|
57
|
+
const { getDb, schema } = getSchedulingContext();
|
|
58
|
+
const principal = getBookingPrincipal(booking);
|
|
59
|
+
const [eventType] = await getDb()
|
|
60
|
+
.select({ teamId: schema.eventTypes.teamId })
|
|
61
|
+
.from(schema.eventTypes)
|
|
62
|
+
.where(
|
|
63
|
+
and(
|
|
64
|
+
eq(schema.eventTypes.id, booking.eventTypeId),
|
|
65
|
+
principal.orgId
|
|
66
|
+
? or(
|
|
67
|
+
eq(schema.eventTypes.ownerEmail, principal.ownerEmail),
|
|
68
|
+
eq(schema.eventTypes.orgId, principal.orgId),
|
|
69
|
+
)
|
|
70
|
+
: eq(schema.eventTypes.ownerEmail, principal.ownerEmail),
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
.limit(1);
|
|
74
|
+
return eventType?.teamId ? { ...booking, teamId: eventType.teamId } : booking;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getBookingPrincipal(booking: Booking): BookingPrincipal {
|
|
78
|
+
return {
|
|
79
|
+
ownerEmail: (booking.ownerEmail ?? booking.hostEmail).trim().toLowerCase(),
|
|
80
|
+
orgId: booking.orgId ?? null,
|
|
81
|
+
teamId: booking.teamId ?? null,
|
|
82
|
+
};
|
|
40
83
|
}
|
|
41
84
|
|
|
42
85
|
async function materializeReminders(
|
|
43
86
|
booking: Booking,
|
|
44
87
|
trigger: "new-booking" | "reschedule" | "cancellation" | "no-show",
|
|
45
88
|
): Promise<void> {
|
|
46
|
-
const workflows = await activeWorkflowsForEvent(booking
|
|
89
|
+
const workflows = await activeWorkflowsForEvent(booking, trigger);
|
|
47
90
|
const now = new Date();
|
|
48
91
|
for (const wf of workflows) {
|
|
49
92
|
for (const step of wf.steps) {
|
|
@@ -57,10 +100,7 @@ async function materializeReminders(
|
|
|
57
100
|
}
|
|
58
101
|
|
|
59
102
|
async function materializeRemindersBefore(booking: Booking): Promise<void> {
|
|
60
|
-
const workflows = await activeWorkflowsForEvent(
|
|
61
|
-
booking.eventTypeId,
|
|
62
|
-
"before-event",
|
|
63
|
-
);
|
|
103
|
+
const workflows = await activeWorkflowsForEvent(booking, "before-event");
|
|
64
104
|
const start = new Date(booking.startTime);
|
|
65
105
|
for (const wf of workflows) {
|
|
66
106
|
for (const step of wf.steps) {
|
|
@@ -69,10 +109,7 @@ async function materializeRemindersBefore(booking: Booking): Promise<void> {
|
|
|
69
109
|
await writeReminder(booking, step, scheduledFor);
|
|
70
110
|
}
|
|
71
111
|
}
|
|
72
|
-
const afterWorkflows = await activeWorkflowsForEvent(
|
|
73
|
-
booking.eventTypeId,
|
|
74
|
-
"after-event",
|
|
75
|
-
);
|
|
112
|
+
const afterWorkflows = await activeWorkflowsForEvent(booking, "after-event");
|
|
76
113
|
const end = new Date(booking.endTime);
|
|
77
114
|
for (const wf of afterWorkflows) {
|
|
78
115
|
for (const step of wf.steps) {
|
|
@@ -112,11 +149,21 @@ function methodForAction(
|
|
|
112
149
|
}
|
|
113
150
|
|
|
114
151
|
async function activeWorkflowsForEvent(
|
|
115
|
-
|
|
152
|
+
booking: Booking,
|
|
116
153
|
trigger: string,
|
|
117
154
|
): Promise<Workflow[]> {
|
|
118
155
|
const { getDb, schema } = getSchedulingContext();
|
|
119
156
|
const db = getDb();
|
|
157
|
+
const unteamedAccessScope = and(
|
|
158
|
+
isNull(schema.workflows.teamId),
|
|
159
|
+
accessFilter(schema.workflows, schema.workflowShares, {
|
|
160
|
+
userEmail: booking.ownerEmail ?? booking.hostEmail,
|
|
161
|
+
orgId: booking.orgId ?? undefined,
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
const accessScope = booking.teamId
|
|
165
|
+
? or(eq(schema.workflows.teamId, booking.teamId), unteamedAccessScope)
|
|
166
|
+
: unteamedAccessScope;
|
|
120
167
|
const wfRows = await db
|
|
121
168
|
.select()
|
|
122
169
|
.from(schema.workflows)
|
|
@@ -124,11 +171,16 @@ async function activeWorkflowsForEvent(
|
|
|
124
171
|
and(
|
|
125
172
|
eq(schema.workflows.trigger, trigger as any),
|
|
126
173
|
eq(schema.workflows.disabled, false),
|
|
174
|
+
like(
|
|
175
|
+
schema.workflows.activeOnEventTypeIds,
|
|
176
|
+
`%"${booking.eventTypeId}"%`,
|
|
177
|
+
),
|
|
178
|
+
accessScope,
|
|
127
179
|
),
|
|
128
180
|
);
|
|
129
181
|
const active = wfRows.filter((w: any) => {
|
|
130
182
|
const ids = safeJson<string[]>(w.activeOnEventTypeIds) ?? [];
|
|
131
|
-
return ids.includes(eventTypeId);
|
|
183
|
+
return ids.includes(booking.eventTypeId);
|
|
132
184
|
});
|
|
133
185
|
if (active.length === 0) return [];
|
|
134
186
|
const stepRows = await db
|
|
@@ -143,21 +195,70 @@ async function activeWorkflowsForEvent(
|
|
|
143
195
|
return active.map((w: any) => hydrateWorkflow(w, stepRows));
|
|
144
196
|
}
|
|
145
197
|
|
|
146
|
-
async function enqueueWebhooks(
|
|
198
|
+
async function enqueueWebhooks(
|
|
199
|
+
booking: Booking,
|
|
200
|
+
event: string,
|
|
201
|
+
principal: BookingPrincipal,
|
|
202
|
+
): Promise<void> {
|
|
147
203
|
const { getDb, schema } = getSchedulingContext();
|
|
148
204
|
const db = getDb();
|
|
149
|
-
const
|
|
205
|
+
const teamScope = principal.teamId
|
|
206
|
+
? or(
|
|
207
|
+
eq(schema.webhooks.teamId, principal.teamId),
|
|
208
|
+
isNull(schema.webhooks.teamId),
|
|
209
|
+
)
|
|
210
|
+
: isNull(schema.webhooks.teamId);
|
|
211
|
+
const webhookScopes = [
|
|
212
|
+
and(
|
|
213
|
+
sql`lower(${schema.webhooks.ownerEmail}) = ${principal.ownerEmail.trim().toLowerCase()}`,
|
|
214
|
+
principal.orgId
|
|
215
|
+
? or(
|
|
216
|
+
eq(schema.webhooks.orgId, principal.orgId),
|
|
217
|
+
isNull(schema.webhooks.orgId),
|
|
218
|
+
)
|
|
219
|
+
: isNull(schema.webhooks.orgId),
|
|
220
|
+
teamScope,
|
|
221
|
+
)!,
|
|
222
|
+
];
|
|
223
|
+
if (principal.orgId) {
|
|
224
|
+
webhookScopes.push(
|
|
225
|
+
and(
|
|
226
|
+
eq(schema.webhooks.orgId, principal.orgId),
|
|
227
|
+
eq(schema.webhooks.visibility, "org"),
|
|
228
|
+
teamScope,
|
|
229
|
+
)!,
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
const webhooks = await db
|
|
233
|
+
.select()
|
|
234
|
+
.from(schema.webhooks)
|
|
235
|
+
.where(
|
|
236
|
+
and(
|
|
237
|
+
eq(schema.webhooks.active, true),
|
|
238
|
+
or(...webhookScopes),
|
|
239
|
+
like(schema.webhooks.eventTriggers, `%"${event}"%`),
|
|
240
|
+
or(
|
|
241
|
+
isNull(schema.webhooks.eventTypeId),
|
|
242
|
+
eq(schema.webhooks.eventTypeId, booking.eventTypeId),
|
|
243
|
+
),
|
|
244
|
+
),
|
|
245
|
+
);
|
|
150
246
|
const now = new Date().toISOString();
|
|
247
|
+
const {
|
|
248
|
+
cancelToken: _cancelToken,
|
|
249
|
+
rescheduleToken: _rescheduleToken,
|
|
250
|
+
...webhookBooking
|
|
251
|
+
} = booking;
|
|
252
|
+
void _cancelToken;
|
|
253
|
+
void _rescheduleToken;
|
|
151
254
|
for (const wh of webhooks) {
|
|
152
|
-
if (!wh.active) continue;
|
|
153
255
|
const triggers = safeJson<string[]>(wh.eventTriggers) ?? [];
|
|
154
256
|
if (!triggers.includes(event)) continue;
|
|
155
|
-
if (wh.eventTypeId && wh.eventTypeId !== booking.eventTypeId) continue;
|
|
156
257
|
await db.insert(schema.webhookDeliveries).values({
|
|
157
258
|
id: nanoid(),
|
|
158
259
|
webhookId: wh.id,
|
|
159
260
|
triggeredAt: now,
|
|
160
|
-
payload: JSON.stringify({ event, booking }),
|
|
261
|
+
payload: JSON.stringify({ event, booking: webhookBooking }),
|
|
161
262
|
success: false,
|
|
162
263
|
attempts: 0,
|
|
163
264
|
});
|
package/src/shared/index.ts
CHANGED
|
@@ -78,6 +78,7 @@ export interface EventType {
|
|
|
78
78
|
position: number;
|
|
79
79
|
schedulingType: SchedulingType;
|
|
80
80
|
ownerEmail?: string;
|
|
81
|
+
orgId?: string;
|
|
81
82
|
teamId?: string;
|
|
82
83
|
scheduleId?: string;
|
|
83
84
|
locations: Location[];
|
|
@@ -161,6 +162,9 @@ export interface Booking {
|
|
|
161
162
|
uid: string;
|
|
162
163
|
eventTypeId: string;
|
|
163
164
|
hostEmail: string;
|
|
165
|
+
ownerEmail?: string;
|
|
166
|
+
orgId?: string | null;
|
|
167
|
+
teamId?: string;
|
|
164
168
|
title: string;
|
|
165
169
|
description?: string;
|
|
166
170
|
startTime: string;
|