@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.
Files changed (136) hide show
  1. package/dist/airtable/api.d.ts +19 -0
  2. package/dist/airtable/api.js +44 -0
  3. package/dist/airtable/index.d.ts +353 -0
  4. package/dist/airtable/index.js +32 -0
  5. package/dist/airtable/schemas.d.ts +716 -0
  6. package/dist/airtable/schemas.js +184 -0
  7. package/dist/asana/api.d.ts +70 -0
  8. package/dist/asana/api.js +119 -0
  9. package/dist/asana/index.d.ts +384 -0
  10. package/dist/asana/index.js +38 -0
  11. package/dist/asana/schemas.d.ts +1060 -0
  12. package/dist/asana/schemas.js +512 -0
  13. package/dist/brevo/api.d.ts +31 -0
  14. package/dist/brevo/api.js +64 -0
  15. package/dist/brevo/index.d.ts +822 -0
  16. package/dist/brevo/index.js +51 -0
  17. package/dist/brevo/schemas.d.ts +1217 -0
  18. package/dist/brevo/schemas.js +332 -0
  19. package/dist/github/index.d.ts +37 -0
  20. package/dist/github/index.js +106 -0
  21. package/dist/github/schemas.d.ts +8014 -0
  22. package/dist/github/schemas.js +113 -0
  23. package/dist/gmail/gmail.d.ts +172 -0
  24. package/dist/gmail/gmail.js +405 -0
  25. package/dist/gmail/index.d.ts +262 -0
  26. package/dist/gmail/index.js +23 -0
  27. package/dist/gmail/schemas.d.ts +444 -0
  28. package/dist/gmail/schemas.js +242 -0
  29. package/dist/google-calendar/event-schemas.d.ts +117 -0
  30. package/dist/google-calendar/event-schemas.js +13 -0
  31. package/dist/google-calendar/google-calendar.d.ts +147 -0
  32. package/dist/google-calendar/google-calendar.js +583 -0
  33. package/dist/google-calendar/index.d.ts +365 -0
  34. package/dist/google-calendar/index.js +24 -0
  35. package/dist/google-calendar/schemas.d.ts +592 -0
  36. package/dist/google-calendar/schemas.js +379 -0
  37. package/dist/google-forms/event-schemas.d.ts +105 -0
  38. package/dist/google-forms/event-schemas.js +20 -0
  39. package/dist/google-forms/google-forms.d.ts +243 -0
  40. package/dist/google-forms/google-forms.js +581 -0
  41. package/dist/google-forms/index.d.ts +141 -0
  42. package/dist/google-forms/index.js +20 -0
  43. package/dist/google-forms/schemas.d.ts +618 -0
  44. package/dist/google-forms/schemas.js +383 -0
  45. package/dist/linear/api.d.ts +63 -0
  46. package/dist/linear/api.js +88 -0
  47. package/dist/linear/index.d.ts +1338 -0
  48. package/dist/linear/index.js +55 -0
  49. package/dist/linear/schemas.d.ts +1795 -0
  50. package/dist/linear/schemas.js +760 -0
  51. package/dist/outlook/graph.d.ts +34 -0
  52. package/dist/outlook/graph.js +98 -0
  53. package/dist/outlook/index.d.ts +115 -0
  54. package/dist/outlook/index.js +11 -0
  55. package/dist/outlook/schemas.d.ts +529 -0
  56. package/dist/outlook/schemas.js +312 -0
  57. package/dist/resend/index.d.ts +707 -0
  58. package/dist/resend/index.js +54 -0
  59. package/dist/resend/schemas.d.ts +1033 -0
  60. package/dist/resend/schemas.js +377 -0
  61. package/dist/slack/index.d.ts +151 -0
  62. package/dist/slack/index.js +22 -0
  63. package/dist/slack/schemas.d.ts +641 -0
  64. package/dist/slack/schemas.js +581 -0
  65. package/dist/teams/graph.d.ts +33 -0
  66. package/dist/teams/graph.js +95 -0
  67. package/dist/teams/index.d.ts +164 -0
  68. package/dist/teams/index.js +21 -0
  69. package/dist/teams/schemas.d.ts +675 -0
  70. package/dist/teams/schemas.js +365 -0
  71. package/dist/trello/api.d.ts +69 -0
  72. package/dist/trello/api.js +140 -0
  73. package/dist/trello/event-schemas.d.ts +1101 -0
  74. package/dist/trello/event-schemas.js +233 -0
  75. package/dist/trello/index.d.ts +721 -0
  76. package/dist/trello/index.js +35 -0
  77. package/dist/trello/schemas.d.ts +283 -0
  78. package/dist/trello/schemas.js +401 -0
  79. package/dist/triggers.d.ts +26 -0
  80. package/dist/triggers.js +33 -0
  81. package/dist/vercel/index.d.ts +1065 -0
  82. package/dist/vercel/index.js +114 -0
  83. package/dist/vercel/schemas.d.ts +1518 -0
  84. package/dist/vercel/schemas.js +335 -0
  85. package/dist/whatsapp/api.d.ts +62 -0
  86. package/dist/whatsapp/api.js +121 -0
  87. package/dist/whatsapp/index.d.ts +614 -0
  88. package/dist/whatsapp/index.js +35 -0
  89. package/dist/whatsapp/schemas.d.ts +926 -0
  90. package/dist/whatsapp/schemas.js +326 -0
  91. package/package.json +169 -0
  92. package/src/airtable/api.ts +66 -0
  93. package/src/airtable/index.ts +41 -0
  94. package/src/airtable/schemas.ts +231 -0
  95. package/src/asana/api.ts +171 -0
  96. package/src/asana/index.ts +49 -0
  97. package/src/asana/schemas.ts +655 -0
  98. package/src/brevo/api.ts +89 -0
  99. package/src/brevo/index.ts +66 -0
  100. package/src/brevo/schemas.ts +449 -0
  101. package/src/github/index.ts +217 -0
  102. package/src/github/schemas.ts +435 -0
  103. package/src/gmail/gmail.ts +577 -0
  104. package/src/gmail/index.ts +26 -0
  105. package/src/gmail/schemas.ts +327 -0
  106. package/src/google-calendar/event-schemas.ts +16 -0
  107. package/src/google-calendar/google-calendar.ts +727 -0
  108. package/src/google-calendar/index.ts +27 -0
  109. package/src/google-calendar/schemas.ts +529 -0
  110. package/src/google-forms/event-schemas.ts +26 -0
  111. package/src/google-forms/google-forms.ts +683 -0
  112. package/src/google-forms/index.ts +23 -0
  113. package/src/google-forms/schemas.ts +499 -0
  114. package/src/linear/api.ts +141 -0
  115. package/src/linear/index.ts +71 -0
  116. package/src/linear/schemas.ts +1060 -0
  117. package/src/outlook/graph.ts +132 -0
  118. package/src/outlook/index.ts +14 -0
  119. package/src/outlook/schemas.ts +388 -0
  120. package/src/resend/index.ts +70 -0
  121. package/src/resend/schemas.ts +462 -0
  122. package/src/slack/index.ts +29 -0
  123. package/src/slack/schemas.ts +737 -0
  124. package/src/teams/graph.ts +130 -0
  125. package/src/teams/index.ts +27 -0
  126. package/src/teams/schemas.ts +463 -0
  127. package/src/trello/api.ts +190 -0
  128. package/src/trello/event-schemas.ts +290 -0
  129. package/src/trello/index.ts +45 -0
  130. package/src/trello/schemas.ts +507 -0
  131. package/src/triggers.ts +68 -0
  132. package/src/vercel/index.ts +147 -0
  133. package/src/vercel/schemas.ts +594 -0
  134. package/src/whatsapp/api.ts +171 -0
  135. package/src/whatsapp/index.ts +46 -0
  136. 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