@automate.ax/integration-contracts 0.55.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/dist/airtable/api.d.ts +19 -0
- package/dist/airtable/api.js +44 -0
- package/dist/airtable/index.d.ts +353 -0
- package/dist/airtable/index.js +32 -0
- package/dist/airtable/schemas.d.ts +716 -0
- package/dist/airtable/schemas.js +184 -0
- package/dist/asana/api.d.ts +70 -0
- package/dist/asana/api.js +119 -0
- package/dist/asana/index.d.ts +384 -0
- package/dist/asana/index.js +38 -0
- package/dist/asana/schemas.d.ts +1060 -0
- package/dist/asana/schemas.js +512 -0
- package/dist/brevo/api.d.ts +31 -0
- package/dist/brevo/api.js +64 -0
- package/dist/brevo/index.d.ts +822 -0
- package/dist/brevo/index.js +51 -0
- package/dist/brevo/schemas.d.ts +1217 -0
- package/dist/brevo/schemas.js +332 -0
- package/dist/github/index.d.ts +37 -0
- package/dist/github/index.js +106 -0
- package/dist/github/schemas.d.ts +8014 -0
- package/dist/github/schemas.js +113 -0
- package/dist/gmail/gmail.d.ts +172 -0
- package/dist/gmail/gmail.js +405 -0
- package/dist/gmail/index.d.ts +262 -0
- package/dist/gmail/index.js +23 -0
- package/dist/gmail/schemas.d.ts +444 -0
- package/dist/gmail/schemas.js +242 -0
- package/dist/google-calendar/event-schemas.d.ts +117 -0
- package/dist/google-calendar/event-schemas.js +13 -0
- package/dist/google-calendar/google-calendar.d.ts +147 -0
- package/dist/google-calendar/google-calendar.js +583 -0
- package/dist/google-calendar/index.d.ts +365 -0
- package/dist/google-calendar/index.js +24 -0
- package/dist/google-calendar/schemas.d.ts +592 -0
- package/dist/google-calendar/schemas.js +379 -0
- package/dist/google-forms/event-schemas.d.ts +105 -0
- package/dist/google-forms/event-schemas.js +20 -0
- package/dist/google-forms/google-forms.d.ts +243 -0
- package/dist/google-forms/google-forms.js +581 -0
- package/dist/google-forms/index.d.ts +141 -0
- package/dist/google-forms/index.js +20 -0
- package/dist/google-forms/schemas.d.ts +618 -0
- package/dist/google-forms/schemas.js +383 -0
- package/dist/linear/api.d.ts +63 -0
- package/dist/linear/api.js +88 -0
- package/dist/linear/index.d.ts +1338 -0
- package/dist/linear/index.js +55 -0
- package/dist/linear/schemas.d.ts +1795 -0
- package/dist/linear/schemas.js +760 -0
- package/dist/outlook/graph.d.ts +34 -0
- package/dist/outlook/graph.js +98 -0
- package/dist/outlook/index.d.ts +115 -0
- package/dist/outlook/index.js +11 -0
- package/dist/outlook/schemas.d.ts +529 -0
- package/dist/outlook/schemas.js +312 -0
- package/dist/resend/index.d.ts +707 -0
- package/dist/resend/index.js +54 -0
- package/dist/resend/schemas.d.ts +1033 -0
- package/dist/resend/schemas.js +377 -0
- package/dist/slack/index.d.ts +151 -0
- package/dist/slack/index.js +22 -0
- package/dist/slack/schemas.d.ts +641 -0
- package/dist/slack/schemas.js +581 -0
- package/dist/teams/graph.d.ts +33 -0
- package/dist/teams/graph.js +95 -0
- package/dist/teams/index.d.ts +164 -0
- package/dist/teams/index.js +21 -0
- package/dist/teams/schemas.d.ts +675 -0
- package/dist/teams/schemas.js +365 -0
- package/dist/trello/api.d.ts +69 -0
- package/dist/trello/api.js +140 -0
- package/dist/trello/event-schemas.d.ts +1101 -0
- package/dist/trello/event-schemas.js +233 -0
- package/dist/trello/index.d.ts +721 -0
- package/dist/trello/index.js +35 -0
- package/dist/trello/schemas.d.ts +283 -0
- package/dist/trello/schemas.js +401 -0
- package/dist/triggers.d.ts +26 -0
- package/dist/triggers.js +33 -0
- package/dist/vercel/index.d.ts +1065 -0
- package/dist/vercel/index.js +114 -0
- package/dist/vercel/schemas.d.ts +1518 -0
- package/dist/vercel/schemas.js +335 -0
- package/dist/whatsapp/api.d.ts +62 -0
- package/dist/whatsapp/api.js +121 -0
- package/dist/whatsapp/index.d.ts +614 -0
- package/dist/whatsapp/index.js +35 -0
- package/dist/whatsapp/schemas.d.ts +926 -0
- package/dist/whatsapp/schemas.js +326 -0
- package/package.json +169 -0
- package/src/airtable/api.ts +66 -0
- package/src/airtable/index.ts +41 -0
- package/src/airtable/schemas.ts +231 -0
- package/src/asana/api.ts +171 -0
- package/src/asana/index.ts +49 -0
- package/src/asana/schemas.ts +655 -0
- package/src/brevo/api.ts +89 -0
- package/src/brevo/index.ts +66 -0
- package/src/brevo/schemas.ts +449 -0
- package/src/github/index.ts +217 -0
- package/src/github/schemas.ts +435 -0
- package/src/gmail/gmail.ts +577 -0
- package/src/gmail/index.ts +26 -0
- package/src/gmail/schemas.ts +327 -0
- package/src/google-calendar/event-schemas.ts +16 -0
- package/src/google-calendar/google-calendar.ts +727 -0
- package/src/google-calendar/index.ts +27 -0
- package/src/google-calendar/schemas.ts +529 -0
- package/src/google-forms/event-schemas.ts +26 -0
- package/src/google-forms/google-forms.ts +683 -0
- package/src/google-forms/index.ts +23 -0
- package/src/google-forms/schemas.ts +499 -0
- package/src/linear/api.ts +141 -0
- package/src/linear/index.ts +71 -0
- package/src/linear/schemas.ts +1060 -0
- package/src/outlook/graph.ts +132 -0
- package/src/outlook/index.ts +14 -0
- package/src/outlook/schemas.ts +388 -0
- package/src/resend/index.ts +70 -0
- package/src/resend/schemas.ts +462 -0
- package/src/slack/index.ts +29 -0
- package/src/slack/schemas.ts +737 -0
- package/src/teams/graph.ts +130 -0
- package/src/teams/index.ts +27 -0
- package/src/teams/schemas.ts +463 -0
- package/src/trello/api.ts +190 -0
- package/src/trello/event-schemas.ts +290 -0
- package/src/trello/index.ts +45 -0
- package/src/trello/schemas.ts +507 -0
- package/src/triggers.ts +68 -0
- package/src/vercel/index.ts +147 -0
- package/src/vercel/schemas.ts +594 -0
- package/src/whatsapp/api.ts +171 -0
- package/src/whatsapp/index.ts +46 -0
- package/src/whatsapp/schemas.ts +341 -0
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
import { auth, forms, type forms_v1 } from "@googleapis/forms"
|
|
2
|
+
import { z } from "zod"
|
|
3
|
+
import {
|
|
4
|
+
EMAIL_COLLECTION_TYPE_SCHEMA,
|
|
5
|
+
FORM_ITEM_INPUT_SCHEMA,
|
|
6
|
+
FORM_ITEM_SCHEMA,
|
|
7
|
+
FORM_QUESTION_INPUT_SCHEMA,
|
|
8
|
+
FORM_RESPONSE_SCHEMA,
|
|
9
|
+
FORM_SCHEMA,
|
|
10
|
+
} from "./schemas"
|
|
11
|
+
|
|
12
|
+
const GOOGLE_FORMS_URL_HOSTS = new Set(["docs.google.com", "forms.google.com"])
|
|
13
|
+
const GOOGLE_FORMS_SECRET_SCHEMA = z.object({
|
|
14
|
+
accessToken: z.string().min(1),
|
|
15
|
+
})
|
|
16
|
+
const EMAIL_COLLECTION_TYPES = {
|
|
17
|
+
none: "DO_NOT_COLLECT",
|
|
18
|
+
respondentInput: "RESPONDER_INPUT",
|
|
19
|
+
verified: "VERIFIED",
|
|
20
|
+
} satisfies Record<z.infer<typeof EMAIL_COLLECTION_TYPE_SCHEMA>, string>
|
|
21
|
+
const CHOICE_TYPES = {
|
|
22
|
+
checkbox: "CHECKBOX",
|
|
23
|
+
dropdown: "DROP_DOWN",
|
|
24
|
+
multipleChoice: "RADIO",
|
|
25
|
+
} as const
|
|
26
|
+
const RATING_ICON_TYPES = {
|
|
27
|
+
heart: "HEART",
|
|
28
|
+
star: "STAR",
|
|
29
|
+
thumbUp: "THUMB_UP",
|
|
30
|
+
} as const
|
|
31
|
+
const MEDIA_ALIGNMENTS = {
|
|
32
|
+
center: "CENTER",
|
|
33
|
+
left: "LEFT",
|
|
34
|
+
right: "RIGHT",
|
|
35
|
+
} as const
|
|
36
|
+
|
|
37
|
+
export interface QuestionMetadata {
|
|
38
|
+
questionId: string
|
|
39
|
+
title?: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Creates the official Forms client from a resolved Google integration secret.
|
|
44
|
+
*
|
|
45
|
+
* @param secret - Refreshed Google integration secret.
|
|
46
|
+
*/
|
|
47
|
+
export function getGoogleFormsApi(secret: Record<string, unknown>) {
|
|
48
|
+
const { accessToken } = GOOGLE_FORMS_SECRET_SCHEMA.parse(secret)
|
|
49
|
+
return forms({
|
|
50
|
+
auth: new auth.OAuth2({
|
|
51
|
+
credentials: { access_token: accessToken },
|
|
52
|
+
}),
|
|
53
|
+
version: "v1",
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Accepts either a raw form ID or a standard Google Forms URL.
|
|
59
|
+
*
|
|
60
|
+
* @param form - Form ID or URL.
|
|
61
|
+
* @throws When a URL is not a recognizable Google Forms URL.
|
|
62
|
+
*/
|
|
63
|
+
export function normalizeFormId(form: string) {
|
|
64
|
+
const value = form.trim()
|
|
65
|
+
if (!URL.canParse(value)) return value
|
|
66
|
+
|
|
67
|
+
const url = new URL(value)
|
|
68
|
+
if (!GOOGLE_FORMS_URL_HOSTS.has(url.hostname)) {
|
|
69
|
+
throw new Error(`Expected a Google Forms URL, received "${value}".`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const pathParts = url.pathname.split("/")
|
|
73
|
+
const dIndex = pathParts.indexOf("d", pathParts.indexOf("forms") + 1)
|
|
74
|
+
const formId =
|
|
75
|
+
dIndex === -1
|
|
76
|
+
? undefined
|
|
77
|
+
: pathParts[dIndex + 1] === "e"
|
|
78
|
+
? pathParts[dIndex + 2]
|
|
79
|
+
: pathParts[dIndex + 1]
|
|
80
|
+
if (!formId) {
|
|
81
|
+
throw new Error(`Could not find a form ID in "${value}".`)
|
|
82
|
+
}
|
|
83
|
+
return formId
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Converts a code-first item definition to the provider request shape.
|
|
88
|
+
*
|
|
89
|
+
* @param input - Code-first form item.
|
|
90
|
+
*/
|
|
91
|
+
export function toGoogleFormItem(
|
|
92
|
+
input: z.infer<typeof FORM_ITEM_INPUT_SCHEMA>,
|
|
93
|
+
): forms_v1.Schema$Item {
|
|
94
|
+
const base = {
|
|
95
|
+
description: input.description,
|
|
96
|
+
itemId: input.itemId,
|
|
97
|
+
title: input.title,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
switch (input.type) {
|
|
101
|
+
case "question":
|
|
102
|
+
return {
|
|
103
|
+
...base,
|
|
104
|
+
questionItem: {
|
|
105
|
+
question: {
|
|
106
|
+
...toGoogleQuestion(input.question),
|
|
107
|
+
grading: input.grading
|
|
108
|
+
? {
|
|
109
|
+
correctAnswers: {
|
|
110
|
+
answers: input.grading.correctAnswers.map((value) => ({
|
|
111
|
+
value,
|
|
112
|
+
})),
|
|
113
|
+
},
|
|
114
|
+
generalFeedback: input.grading.generalFeedback
|
|
115
|
+
? { text: input.grading.generalFeedback }
|
|
116
|
+
: undefined,
|
|
117
|
+
pointValue: input.grading.points,
|
|
118
|
+
whenRight: input.grading.whenRight
|
|
119
|
+
? { text: input.grading.whenRight }
|
|
120
|
+
: undefined,
|
|
121
|
+
whenWrong: input.grading.whenWrong
|
|
122
|
+
? { text: input.grading.whenWrong }
|
|
123
|
+
: undefined,
|
|
124
|
+
}
|
|
125
|
+
: undefined,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
case "grid":
|
|
130
|
+
return {
|
|
131
|
+
...base,
|
|
132
|
+
questionGroupItem: {
|
|
133
|
+
grid: {
|
|
134
|
+
columns: {
|
|
135
|
+
options: input.columns.map((value) => ({ value })),
|
|
136
|
+
type: input.gridType === "checkbox" ? "CHECKBOX" : "RADIO",
|
|
137
|
+
},
|
|
138
|
+
shuffleQuestions: input.shuffleRows,
|
|
139
|
+
},
|
|
140
|
+
questions: input.rows.map((row) => ({
|
|
141
|
+
questionId: row.questionId,
|
|
142
|
+
required: row.required,
|
|
143
|
+
rowQuestion: { title: row.title },
|
|
144
|
+
})),
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
case "pageBreak":
|
|
148
|
+
return { ...base, pageBreakItem: {} }
|
|
149
|
+
case "text":
|
|
150
|
+
return { ...base, textItem: {} }
|
|
151
|
+
case "image":
|
|
152
|
+
return {
|
|
153
|
+
...base,
|
|
154
|
+
imageItem: {
|
|
155
|
+
image: {
|
|
156
|
+
altText: input.altText,
|
|
157
|
+
properties: {
|
|
158
|
+
alignment: input.alignment
|
|
159
|
+
? MEDIA_ALIGNMENTS[input.alignment]
|
|
160
|
+
: undefined,
|
|
161
|
+
width: input.width,
|
|
162
|
+
},
|
|
163
|
+
sourceUri: input.sourceUrl,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
case "video":
|
|
168
|
+
return {
|
|
169
|
+
...base,
|
|
170
|
+
videoItem: {
|
|
171
|
+
caption: input.caption,
|
|
172
|
+
video: {
|
|
173
|
+
properties: {
|
|
174
|
+
alignment: input.alignment
|
|
175
|
+
? MEDIA_ALIGNMENTS[input.alignment]
|
|
176
|
+
: undefined,
|
|
177
|
+
width: input.width,
|
|
178
|
+
},
|
|
179
|
+
youtubeUri: input.youtubeUrl,
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Normalizes a provider form and all nested items.
|
|
188
|
+
*
|
|
189
|
+
* @param form - Google Forms resource.
|
|
190
|
+
* @throws When required provider identifiers are absent.
|
|
191
|
+
*/
|
|
192
|
+
export function normalizeForm(form: forms_v1.Schema$Form) {
|
|
193
|
+
if (!form.formId) throw new Error("Google Forms returned no form ID.")
|
|
194
|
+
return FORM_SCHEMA.parse({
|
|
195
|
+
description: form.info?.description ?? undefined,
|
|
196
|
+
documentTitle: form.info?.documentTitle ?? undefined,
|
|
197
|
+
editUrl: `https://docs.google.com/forms/d/${form.formId}/edit`,
|
|
198
|
+
formId: form.formId,
|
|
199
|
+
items: (form.items ?? []).map(normalizeFormItem),
|
|
200
|
+
linkedSheetId: form.linkedSheetId ?? undefined,
|
|
201
|
+
publishSettings: form.publishSettings?.publishState
|
|
202
|
+
? {
|
|
203
|
+
isAcceptingResponses:
|
|
204
|
+
form.publishSettings.publishState.isAcceptingResponses ?? false,
|
|
205
|
+
isPublished: form.publishSettings.publishState.isPublished ?? false,
|
|
206
|
+
}
|
|
207
|
+
: undefined,
|
|
208
|
+
responderUrl: form.responderUri ?? undefined,
|
|
209
|
+
revisionId: form.revisionId ?? undefined,
|
|
210
|
+
settings: {
|
|
211
|
+
emailCollection: toPublicEmailCollectionType(
|
|
212
|
+
form.settings?.emailCollectionType,
|
|
213
|
+
),
|
|
214
|
+
isQuiz: form.settings?.quizSettings?.isQuiz ?? false,
|
|
215
|
+
},
|
|
216
|
+
title: form.info?.title ?? "",
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Normalizes a provider item at its display position.
|
|
222
|
+
*
|
|
223
|
+
* @param item - Google Forms item.
|
|
224
|
+
* @param index - Zero-based display position.
|
|
225
|
+
*/
|
|
226
|
+
export function normalizeFormItem(item: forms_v1.Schema$Item, index: number) {
|
|
227
|
+
return FORM_ITEM_SCHEMA.parse({
|
|
228
|
+
description: item.description ?? undefined,
|
|
229
|
+
index,
|
|
230
|
+
itemId: item.itemId ?? "",
|
|
231
|
+
questions: item.questionItem?.question
|
|
232
|
+
? [normalizeQuestion(item.questionItem.question)]
|
|
233
|
+
: (item.questionGroupItem?.questions ?? []).map((question) =>
|
|
234
|
+
normalizeQuestion({
|
|
235
|
+
...question,
|
|
236
|
+
choiceQuestion: item.questionGroupItem?.grid?.columns,
|
|
237
|
+
}),
|
|
238
|
+
),
|
|
239
|
+
title: item.title ?? "",
|
|
240
|
+
type: item.questionItem
|
|
241
|
+
? "question"
|
|
242
|
+
: item.questionGroupItem
|
|
243
|
+
? "grid"
|
|
244
|
+
: item.pageBreakItem
|
|
245
|
+
? "pageBreak"
|
|
246
|
+
: item.textItem
|
|
247
|
+
? "text"
|
|
248
|
+
: item.imageItem
|
|
249
|
+
? "image"
|
|
250
|
+
: item.videoItem
|
|
251
|
+
? "video"
|
|
252
|
+
: "unknown",
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Collects question titles in form display order.
|
|
258
|
+
*
|
|
259
|
+
* @param form - Google Forms resource.
|
|
260
|
+
*/
|
|
261
|
+
export function getQuestionMetadata(
|
|
262
|
+
form: forms_v1.Schema$Form,
|
|
263
|
+
): QuestionMetadata[] {
|
|
264
|
+
return (form.items ?? []).flatMap((item) => {
|
|
265
|
+
if (item.questionItem?.question?.questionId) {
|
|
266
|
+
return [
|
|
267
|
+
{
|
|
268
|
+
questionId: item.questionItem.question.questionId,
|
|
269
|
+
title: item.title ?? undefined,
|
|
270
|
+
},
|
|
271
|
+
]
|
|
272
|
+
}
|
|
273
|
+
return (item.questionGroupItem?.questions ?? []).flatMap((question) =>
|
|
274
|
+
question.questionId
|
|
275
|
+
? [
|
|
276
|
+
{
|
|
277
|
+
questionId: question.questionId,
|
|
278
|
+
title: question.rowQuestion?.title ?? item.title ?? undefined,
|
|
279
|
+
},
|
|
280
|
+
]
|
|
281
|
+
: [],
|
|
282
|
+
)
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Normalizes a submitted response and sorts answers in form display order.
|
|
288
|
+
*
|
|
289
|
+
* @param response - Google Forms response.
|
|
290
|
+
* @param formId - Form ID used when list responses omit it.
|
|
291
|
+
* @param questions - Optional form question metadata.
|
|
292
|
+
* @throws When the provider response omits its response ID.
|
|
293
|
+
*/
|
|
294
|
+
export function normalizeFormResponse(
|
|
295
|
+
response: forms_v1.Schema$FormResponse,
|
|
296
|
+
formId: string,
|
|
297
|
+
questions: QuestionMetadata[] = [],
|
|
298
|
+
) {
|
|
299
|
+
if (!response.responseId) {
|
|
300
|
+
throw new Error("Google Forms returned a response without an ID.")
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const questionOrder = new Map(
|
|
304
|
+
questions.map(({ questionId }, index) => [questionId, index]),
|
|
305
|
+
)
|
|
306
|
+
const titles = new Map(
|
|
307
|
+
questions.map(({ questionId, title }) => [questionId, title]),
|
|
308
|
+
)
|
|
309
|
+
// Keep this pipeline named so the response assembly remains readable.
|
|
310
|
+
const answers = Object.values(response.answers ?? {})
|
|
311
|
+
.filter((answer): answer is forms_v1.Schema$Answer =>
|
|
312
|
+
Boolean(answer.questionId),
|
|
313
|
+
)
|
|
314
|
+
.toSorted(
|
|
315
|
+
(left, right) =>
|
|
316
|
+
(questionOrder.get(left.questionId!) ?? Number.MAX_SAFE_INTEGER) -
|
|
317
|
+
(questionOrder.get(right.questionId!) ?? Number.MAX_SAFE_INTEGER),
|
|
318
|
+
)
|
|
319
|
+
.map((answer) => ({
|
|
320
|
+
files: (answer.fileUploadAnswers?.answers ?? []).flatMap((file) =>
|
|
321
|
+
file.fileId && file.fileName && file.mimeType
|
|
322
|
+
? [
|
|
323
|
+
{
|
|
324
|
+
fileId: file.fileId,
|
|
325
|
+
fileName: file.fileName,
|
|
326
|
+
mimeType: file.mimeType,
|
|
327
|
+
},
|
|
328
|
+
]
|
|
329
|
+
: [],
|
|
330
|
+
),
|
|
331
|
+
grade: answer.grade
|
|
332
|
+
? {
|
|
333
|
+
correct: answer.grade.correct ?? undefined,
|
|
334
|
+
correctAnswers: [],
|
|
335
|
+
feedback: answer.grade.feedback?.text ?? undefined,
|
|
336
|
+
score: answer.grade.score ?? undefined,
|
|
337
|
+
}
|
|
338
|
+
: undefined,
|
|
339
|
+
questionId: answer.questionId!,
|
|
340
|
+
questionTitle: titles.get(answer.questionId!) ?? undefined,
|
|
341
|
+
values: (answer.textAnswers?.answers ?? []).flatMap(({ value }) =>
|
|
342
|
+
value == null ? [] : [value],
|
|
343
|
+
),
|
|
344
|
+
}))
|
|
345
|
+
|
|
346
|
+
return FORM_RESPONSE_SCHEMA.parse({
|
|
347
|
+
answers,
|
|
348
|
+
createTime: response.createTime ?? undefined,
|
|
349
|
+
formId: response.formId ?? formId,
|
|
350
|
+
lastSubmittedTime: response.lastSubmittedTime ?? undefined,
|
|
351
|
+
respondentEmail: response.respondentEmail ?? undefined,
|
|
352
|
+
responseId: response.responseId,
|
|
353
|
+
totalScore: response.totalScore ?? undefined,
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Resolves a one-based item position or immutable item ID to a zero-based
|
|
359
|
+
* index.
|
|
360
|
+
*
|
|
361
|
+
* @param formsApi - Authenticated Forms client.
|
|
362
|
+
* @param formId - Immutable form ID.
|
|
363
|
+
* @param reference - One-based position or item ID.
|
|
364
|
+
* @throws When the referenced item does not exist.
|
|
365
|
+
*/
|
|
366
|
+
export async function resolveItemIndex(
|
|
367
|
+
formsApi: forms_v1.Forms,
|
|
368
|
+
formId: string,
|
|
369
|
+
reference: string | number,
|
|
370
|
+
) {
|
|
371
|
+
if (typeof reference === "number") return reference - 1
|
|
372
|
+
|
|
373
|
+
const { data } = await formsApi.forms.get({ formId })
|
|
374
|
+
const index =
|
|
375
|
+
data.items?.findIndex(({ itemId }) => itemId === reference) ?? -1
|
|
376
|
+
if (index === -1) {
|
|
377
|
+
throw new Error(`Google Forms item "${reference}" was not found.`)
|
|
378
|
+
}
|
|
379
|
+
return index
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Resolves a one-based item position or immutable item ID with its form.
|
|
384
|
+
*
|
|
385
|
+
* @param formsApi - Authenticated Forms client.
|
|
386
|
+
* @param formId - Immutable form ID.
|
|
387
|
+
* @param reference - One-based position or item ID.
|
|
388
|
+
* @throws When the referenced item does not exist.
|
|
389
|
+
*/
|
|
390
|
+
export async function resolveFormItem(
|
|
391
|
+
formsApi: forms_v1.Forms,
|
|
392
|
+
formId: string,
|
|
393
|
+
reference: string | number,
|
|
394
|
+
) {
|
|
395
|
+
const { data: form } = await formsApi.forms.get({ formId })
|
|
396
|
+
const index =
|
|
397
|
+
typeof reference === "number"
|
|
398
|
+
? reference - 1
|
|
399
|
+
: (form.items?.findIndex(({ itemId }) => itemId === reference) ?? -1)
|
|
400
|
+
const item = form.items?.[index]
|
|
401
|
+
if (!item) {
|
|
402
|
+
throw new Error(`Google Forms item "${reference}" was not found.`)
|
|
403
|
+
}
|
|
404
|
+
return { form, index, item }
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Preserves provider-generated item and question IDs during full replacement.
|
|
409
|
+
*
|
|
410
|
+
* @param next - Replacement item.
|
|
411
|
+
* @param current - Existing provider item.
|
|
412
|
+
*/
|
|
413
|
+
export function preserveItemIds(
|
|
414
|
+
next: forms_v1.Schema$Item,
|
|
415
|
+
current: forms_v1.Schema$Item,
|
|
416
|
+
): forms_v1.Schema$Item {
|
|
417
|
+
return {
|
|
418
|
+
...next,
|
|
419
|
+
itemId: next.itemId ?? current.itemId,
|
|
420
|
+
questionGroupItem: next.questionGroupItem
|
|
421
|
+
? {
|
|
422
|
+
...next.questionGroupItem,
|
|
423
|
+
questions: next.questionGroupItem.questions?.map(
|
|
424
|
+
(question, index) => ({
|
|
425
|
+
...question,
|
|
426
|
+
questionId:
|
|
427
|
+
question.questionId ??
|
|
428
|
+
current.questionGroupItem?.questions?.[index]?.questionId,
|
|
429
|
+
}),
|
|
430
|
+
),
|
|
431
|
+
}
|
|
432
|
+
: undefined,
|
|
433
|
+
questionItem: next.questionItem
|
|
434
|
+
? {
|
|
435
|
+
...next.questionItem,
|
|
436
|
+
question: next.questionItem.question
|
|
437
|
+
? {
|
|
438
|
+
...next.questionItem.question,
|
|
439
|
+
questionId:
|
|
440
|
+
next.questionItem.question.questionId ??
|
|
441
|
+
current.questionItem?.question?.questionId,
|
|
442
|
+
}
|
|
443
|
+
: undefined,
|
|
444
|
+
}
|
|
445
|
+
: undefined,
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Resolves an optional one-based insertion position, defaulting to the end.
|
|
451
|
+
*
|
|
452
|
+
* @param formsApi - Authenticated Forms client.
|
|
453
|
+
* @param formId - Immutable form ID.
|
|
454
|
+
* @param position - Optional one-based insertion position.
|
|
455
|
+
*/
|
|
456
|
+
export async function resolveInsertionIndex(
|
|
457
|
+
formsApi: forms_v1.Forms,
|
|
458
|
+
formId: string,
|
|
459
|
+
position?: number,
|
|
460
|
+
) {
|
|
461
|
+
if (position !== undefined) return position - 1
|
|
462
|
+
const { data } = await formsApi.forms.get({ formId })
|
|
463
|
+
return data.items?.length ?? 0
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Converts public settings into Google Forms settings.
|
|
468
|
+
*
|
|
469
|
+
* @param settings - Code-first form settings.
|
|
470
|
+
* @param settings.emailCollection - Email collection behavior.
|
|
471
|
+
* @param settings.isQuiz - Whether the form is a quiz.
|
|
472
|
+
*/
|
|
473
|
+
export function toGoogleFormSettings(settings: {
|
|
474
|
+
emailCollection?: z.infer<typeof EMAIL_COLLECTION_TYPE_SCHEMA>
|
|
475
|
+
isQuiz?: boolean
|
|
476
|
+
}): forms_v1.Schema$FormSettings {
|
|
477
|
+
return {
|
|
478
|
+
emailCollectionType: settings.emailCollection
|
|
479
|
+
? EMAIL_COLLECTION_TYPES[settings.emailCollection]
|
|
480
|
+
: undefined,
|
|
481
|
+
quizSettings:
|
|
482
|
+
settings.isQuiz === undefined ? undefined : { isQuiz: settings.isQuiz },
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* Builds a comma-separated settings update mask.
|
|
488
|
+
*
|
|
489
|
+
* @param settings - Code-first form settings.
|
|
490
|
+
* @param settings.emailCollection - Email collection behavior.
|
|
491
|
+
* @param settings.isQuiz - Whether the form is a quiz.
|
|
492
|
+
*/
|
|
493
|
+
export function getSettingsUpdateMask(settings: {
|
|
494
|
+
emailCollection?: z.infer<typeof EMAIL_COLLECTION_TYPE_SCHEMA>
|
|
495
|
+
isQuiz?: boolean
|
|
496
|
+
}) {
|
|
497
|
+
return [
|
|
498
|
+
settings.emailCollection === undefined ? undefined : "emailCollectionType",
|
|
499
|
+
settings.isQuiz === undefined ? undefined : "quizSettings.isQuiz",
|
|
500
|
+
]
|
|
501
|
+
.filter((field): field is string => Boolean(field))
|
|
502
|
+
.join(",")
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Converts a code-first question to Google Forms' union representation.
|
|
507
|
+
*
|
|
508
|
+
* @param input - Code-first question definition.
|
|
509
|
+
*/
|
|
510
|
+
function toGoogleQuestion(
|
|
511
|
+
input: z.infer<typeof FORM_QUESTION_INPUT_SCHEMA>,
|
|
512
|
+
): forms_v1.Schema$Question {
|
|
513
|
+
const base = {
|
|
514
|
+
questionId: input.questionId,
|
|
515
|
+
required: input.required,
|
|
516
|
+
}
|
|
517
|
+
switch (input.type) {
|
|
518
|
+
case "shortText":
|
|
519
|
+
return { ...base, textQuestion: { paragraph: false } }
|
|
520
|
+
case "paragraph":
|
|
521
|
+
return { ...base, textQuestion: { paragraph: true } }
|
|
522
|
+
case "multipleChoice":
|
|
523
|
+
case "checkbox":
|
|
524
|
+
case "dropdown":
|
|
525
|
+
return {
|
|
526
|
+
...base,
|
|
527
|
+
choiceQuestion: {
|
|
528
|
+
options: input.options.map((option) =>
|
|
529
|
+
typeof option === "string"
|
|
530
|
+
? { value: option }
|
|
531
|
+
: { isOther: true, value: option.value },
|
|
532
|
+
),
|
|
533
|
+
shuffle: input.shuffle,
|
|
534
|
+
type: CHOICE_TYPES[input.type],
|
|
535
|
+
},
|
|
536
|
+
}
|
|
537
|
+
case "scale":
|
|
538
|
+
return {
|
|
539
|
+
...base,
|
|
540
|
+
scaleQuestion: {
|
|
541
|
+
high: input.high,
|
|
542
|
+
highLabel: input.highLabel,
|
|
543
|
+
low: input.low,
|
|
544
|
+
lowLabel: input.lowLabel,
|
|
545
|
+
},
|
|
546
|
+
}
|
|
547
|
+
case "date":
|
|
548
|
+
return {
|
|
549
|
+
...base,
|
|
550
|
+
dateQuestion: {
|
|
551
|
+
includeTime: input.includeTime,
|
|
552
|
+
includeYear: input.includeYear,
|
|
553
|
+
},
|
|
554
|
+
}
|
|
555
|
+
case "time":
|
|
556
|
+
return { ...base, timeQuestion: { duration: false } }
|
|
557
|
+
case "duration":
|
|
558
|
+
return { ...base, timeQuestion: { duration: true } }
|
|
559
|
+
case "rating":
|
|
560
|
+
return {
|
|
561
|
+
...base,
|
|
562
|
+
ratingQuestion: {
|
|
563
|
+
iconType: RATING_ICON_TYPES[input.icon],
|
|
564
|
+
ratingScaleLevel: input.levels,
|
|
565
|
+
},
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Normalizes one provider question.
|
|
572
|
+
*
|
|
573
|
+
* @param question - Google Forms question.
|
|
574
|
+
*/
|
|
575
|
+
function normalizeQuestion(question: forms_v1.Schema$Question) {
|
|
576
|
+
return {
|
|
577
|
+
choices: (question.choiceQuestion?.options ?? []).flatMap(({ value }) =>
|
|
578
|
+
value == null ? [] : [value],
|
|
579
|
+
),
|
|
580
|
+
date: question.dateQuestion
|
|
581
|
+
? {
|
|
582
|
+
includeTime: question.dateQuestion.includeTime ?? false,
|
|
583
|
+
includeYear: question.dateQuestion.includeYear ?? false,
|
|
584
|
+
}
|
|
585
|
+
: undefined,
|
|
586
|
+
fileUpload: question.fileUploadQuestion
|
|
587
|
+
? {
|
|
588
|
+
folderId: question.fileUploadQuestion.folderId ?? undefined,
|
|
589
|
+
maxFileSizeBytes:
|
|
590
|
+
question.fileUploadQuestion.maxFileSize ?? undefined,
|
|
591
|
+
maxFiles: question.fileUploadQuestion.maxFiles ?? undefined,
|
|
592
|
+
types: question.fileUploadQuestion.types ?? [],
|
|
593
|
+
}
|
|
594
|
+
: undefined,
|
|
595
|
+
grading: question.grading
|
|
596
|
+
? {
|
|
597
|
+
correctAnswers: (
|
|
598
|
+
question.grading.correctAnswers?.answers ?? []
|
|
599
|
+
).flatMap(({ value }) => (value == null ? [] : [value])),
|
|
600
|
+
feedback: question.grading.generalFeedback?.text ?? undefined,
|
|
601
|
+
points: question.grading.pointValue ?? undefined,
|
|
602
|
+
whenRight: question.grading.whenRight?.text ?? undefined,
|
|
603
|
+
whenWrong: question.grading.whenWrong?.text ?? undefined,
|
|
604
|
+
}
|
|
605
|
+
: undefined,
|
|
606
|
+
questionId: question.questionId ?? "",
|
|
607
|
+
rating: question.ratingQuestion
|
|
608
|
+
? {
|
|
609
|
+
icon: question.ratingQuestion.iconType ?? "",
|
|
610
|
+
levels: question.ratingQuestion.ratingScaleLevel ?? 0,
|
|
611
|
+
}
|
|
612
|
+
: undefined,
|
|
613
|
+
required: question.required ?? false,
|
|
614
|
+
rowTitle: question.rowQuestion?.title ?? undefined,
|
|
615
|
+
scale: question.scaleQuestion
|
|
616
|
+
? {
|
|
617
|
+
high: question.scaleQuestion.high ?? 0,
|
|
618
|
+
highLabel: question.scaleQuestion.highLabel ?? undefined,
|
|
619
|
+
low: question.scaleQuestion.low ?? 0,
|
|
620
|
+
lowLabel: question.scaleQuestion.lowLabel ?? undefined,
|
|
621
|
+
}
|
|
622
|
+
: undefined,
|
|
623
|
+
type: question.rowQuestion
|
|
624
|
+
? "gridRow"
|
|
625
|
+
: question.textQuestion
|
|
626
|
+
? question.textQuestion.paragraph
|
|
627
|
+
? "paragraph"
|
|
628
|
+
: "shortText"
|
|
629
|
+
: question.choiceQuestion
|
|
630
|
+
? toPublicChoiceType(question.choiceQuestion.type)
|
|
631
|
+
: question.scaleQuestion
|
|
632
|
+
? "scale"
|
|
633
|
+
: question.dateQuestion
|
|
634
|
+
? "date"
|
|
635
|
+
: question.timeQuestion
|
|
636
|
+
? question.timeQuestion.duration
|
|
637
|
+
? "duration"
|
|
638
|
+
: "time"
|
|
639
|
+
: question.ratingQuestion
|
|
640
|
+
? "rating"
|
|
641
|
+
: question.fileUploadQuestion
|
|
642
|
+
? "fileUpload"
|
|
643
|
+
: "unknown",
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Maps the provider email collection enum to the public enum.
|
|
649
|
+
*
|
|
650
|
+
* @param type - Google Forms enum value.
|
|
651
|
+
*/
|
|
652
|
+
function toPublicEmailCollectionType(
|
|
653
|
+
type: string | null | undefined,
|
|
654
|
+
): z.infer<typeof EMAIL_COLLECTION_TYPE_SCHEMA> {
|
|
655
|
+
switch (type) {
|
|
656
|
+
case "VERIFIED":
|
|
657
|
+
return "verified"
|
|
658
|
+
case "RESPONDER_INPUT":
|
|
659
|
+
return "respondentInput"
|
|
660
|
+
default:
|
|
661
|
+
return "none"
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Maps the provider choice enum to the public question kind.
|
|
667
|
+
*
|
|
668
|
+
* @param type - Google Forms enum value.
|
|
669
|
+
*/
|
|
670
|
+
function toPublicChoiceType(
|
|
671
|
+
type: string | null | undefined,
|
|
672
|
+
): "multipleChoice" | "checkbox" | "dropdown" | "unknown" {
|
|
673
|
+
switch (type) {
|
|
674
|
+
case "RADIO":
|
|
675
|
+
return "multipleChoice"
|
|
676
|
+
case "CHECKBOX":
|
|
677
|
+
return "checkbox"
|
|
678
|
+
case "DROP_DOWN":
|
|
679
|
+
return "dropdown"
|
|
680
|
+
default:
|
|
681
|
+
return "unknown"
|
|
682
|
+
}
|
|
683
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod"
|
|
2
|
+
import { GOOGLE_FORMS_FORM_CHANGED_EVENT_SCHEMA } from "./event-schemas"
|
|
3
|
+
import { FORM_RESPONSE_SCHEMA } from "./schemas"
|
|
4
|
+
|
|
5
|
+
export * from "./event-schemas"
|
|
6
|
+
export * from "./google-forms"
|
|
7
|
+
export * from "./schemas"
|
|
8
|
+
|
|
9
|
+
/** Canonical Google Form selected by one subscription. */
|
|
10
|
+
export const GOOGLE_FORMS_TRIGGER_CONFIG_SCHEMA = z.object({
|
|
11
|
+
formId: z.string(),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
export const googleFormsTriggerContracts = {
|
|
15
|
+
"googleForms.form.changed": {
|
|
16
|
+
configSchema: GOOGLE_FORMS_TRIGGER_CONFIG_SCHEMA,
|
|
17
|
+
eventSchema: GOOGLE_FORMS_FORM_CHANGED_EVENT_SCHEMA,
|
|
18
|
+
},
|
|
19
|
+
"googleForms.response.submitted": {
|
|
20
|
+
configSchema: GOOGLE_FORMS_TRIGGER_CONFIG_SCHEMA,
|
|
21
|
+
eventSchema: FORM_RESPONSE_SCHEMA,
|
|
22
|
+
},
|
|
23
|
+
} as const
|