@yolk-sdk/emulators 0.1.0-canary.96

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 (333) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2135 -0
  3. package/dist/anthropic.d.mts +269 -0
  4. package/dist/anthropic.d.mts.map +1 -0
  5. package/dist/anthropic.mjs +177 -0
  6. package/dist/anthropic.mjs.map +1 -0
  7. package/dist/chat-completions.d.mts +288 -0
  8. package/dist/chat-completions.d.mts.map +1 -0
  9. package/dist/chat-completions.mjs +451 -0
  10. package/dist/chat-completions.mjs.map +1 -0
  11. package/dist/codex.d.mts +265 -0
  12. package/dist/codex.d.mts.map +1 -0
  13. package/dist/codex.mjs +199 -0
  14. package/dist/codex.mjs.map +1 -0
  15. package/dist/dropbox/api.d.mts +63 -0
  16. package/dist/dropbox/api.d.mts.map +1 -0
  17. package/dist/dropbox/api.mjs +565 -0
  18. package/dist/dropbox/api.mjs.map +1 -0
  19. package/dist/dropbox/state.d.mts +136 -0
  20. package/dist/dropbox/state.d.mts.map +1 -0
  21. package/dist/dropbox/state.mjs +209 -0
  22. package/dist/dropbox/state.mjs.map +1 -0
  23. package/dist/dropbox.d.mts +79 -0
  24. package/dist/dropbox.d.mts.map +1 -0
  25. package/dist/dropbox.mjs +124 -0
  26. package/dist/dropbox.mjs.map +1 -0
  27. package/dist/email-fixtures.d.mts +36 -0
  28. package/dist/email-fixtures.d.mts.map +1 -0
  29. package/dist/email-fixtures.mjs +1080 -0
  30. package/dist/email-fixtures.mjs.map +1 -0
  31. package/dist/email.d.mts +160 -0
  32. package/dist/email.d.mts.map +1 -0
  33. package/dist/email.mjs +608 -0
  34. package/dist/email.mjs.map +1 -0
  35. package/dist/emulator-compose.d.mts +40 -0
  36. package/dist/emulator-compose.d.mts.map +1 -0
  37. package/dist/emulator-compose.mjs +91 -0
  38. package/dist/emulator-compose.mjs.map +1 -0
  39. package/dist/emulator-http.d.mts +53 -0
  40. package/dist/emulator-http.d.mts.map +1 -0
  41. package/dist/emulator-http.mjs +117 -0
  42. package/dist/emulator-http.mjs.map +1 -0
  43. package/dist/emulator-kernel.d.mts +176 -0
  44. package/dist/emulator-kernel.d.mts.map +1 -0
  45. package/dist/emulator-kernel.mjs +413 -0
  46. package/dist/emulator-kernel.mjs.map +1 -0
  47. package/dist/fixture-route.d.mts +129 -0
  48. package/dist/fixture-route.d.mts.map +1 -0
  49. package/dist/fixture-route.mjs +257 -0
  50. package/dist/fixture-route.mjs.map +1 -0
  51. package/dist/fortnox/api.d.mts +92 -0
  52. package/dist/fortnox/api.d.mts.map +1 -0
  53. package/dist/fortnox/api.mjs +750 -0
  54. package/dist/fortnox/api.mjs.map +1 -0
  55. package/dist/fortnox/state.d.mts +533 -0
  56. package/dist/fortnox/state.d.mts.map +1 -0
  57. package/dist/fortnox/state.mjs +612 -0
  58. package/dist/fortnox/state.mjs.map +1 -0
  59. package/dist/fortnox.d.mts +128 -0
  60. package/dist/fortnox.d.mts.map +1 -0
  61. package/dist/fortnox.mjs +403 -0
  62. package/dist/fortnox.mjs.map +1 -0
  63. package/dist/gateway-evaluate-recordings.d.mts +12 -0
  64. package/dist/gateway-evaluate-recordings.d.mts.map +1 -0
  65. package/dist/gateway-evaluate-recordings.mjs +128 -0
  66. package/dist/gateway-evaluate-recordings.mjs.map +1 -0
  67. package/dist/gateway.d.mts +272 -0
  68. package/dist/gateway.d.mts.map +1 -0
  69. package/dist/gateway.mjs +400 -0
  70. package/dist/gateway.mjs.map +1 -0
  71. package/dist/github/api.d.mts +63 -0
  72. package/dist/github/api.d.mts.map +1 -0
  73. package/dist/github/api.mjs +527 -0
  74. package/dist/github/api.mjs.map +1 -0
  75. package/dist/github/state.d.mts +272 -0
  76. package/dist/github/state.d.mts.map +1 -0
  77. package/dist/github/state.mjs +303 -0
  78. package/dist/github/state.mjs.map +1 -0
  79. package/dist/github.d.mts +77 -0
  80. package/dist/github.d.mts.map +1 -0
  81. package/dist/github.mjs +180 -0
  82. package/dist/github.mjs.map +1 -0
  83. package/dist/google/calendar.d.mts +10 -0
  84. package/dist/google/calendar.d.mts.map +1 -0
  85. package/dist/google/calendar.mjs +252 -0
  86. package/dist/google/calendar.mjs.map +1 -0
  87. package/dist/google/drive.d.mts +8 -0
  88. package/dist/google/drive.d.mts.map +1 -0
  89. package/dist/google/drive.mjs +252 -0
  90. package/dist/google/drive.mjs.map +1 -0
  91. package/dist/google/gmail.d.mts +10 -0
  92. package/dist/google/gmail.d.mts.map +1 -0
  93. package/dist/google/gmail.mjs +591 -0
  94. package/dist/google/gmail.mjs.map +1 -0
  95. package/dist/google/shared.d.mts +107 -0
  96. package/dist/google/shared.d.mts.map +1 -0
  97. package/dist/google/shared.mjs +146 -0
  98. package/dist/google/shared.mjs.map +1 -0
  99. package/dist/google/state.d.mts +385 -0
  100. package/dist/google/state.d.mts.map +1 -0
  101. package/dist/google/state.mjs +496 -0
  102. package/dist/google/state.mjs.map +1 -0
  103. package/dist/google.d.mts +75 -0
  104. package/dist/google.d.mts.map +1 -0
  105. package/dist/google.mjs +210 -0
  106. package/dist/google.mjs.map +1 -0
  107. package/dist/linkedin-search/api.d.mts +45 -0
  108. package/dist/linkedin-search/api.d.mts.map +1 -0
  109. package/dist/linkedin-search/api.mjs +181 -0
  110. package/dist/linkedin-search/api.mjs.map +1 -0
  111. package/dist/linkedin-search/state.d.mts +123 -0
  112. package/dist/linkedin-search/state.d.mts.map +1 -0
  113. package/dist/linkedin-search/state.mjs +261 -0
  114. package/dist/linkedin-search/state.mjs.map +1 -0
  115. package/dist/linkedin-search.d.mts +69 -0
  116. package/dist/linkedin-search.d.mts.map +1 -0
  117. package/dist/linkedin-search.mjs +158 -0
  118. package/dist/linkedin-search.mjs.map +1 -0
  119. package/dist/mcp/api.d.mts +44 -0
  120. package/dist/mcp/api.d.mts.map +1 -0
  121. package/dist/mcp/api.mjs +557 -0
  122. package/dist/mcp/api.mjs.map +1 -0
  123. package/dist/mcp/recordings.d.mts +40 -0
  124. package/dist/mcp/recordings.d.mts.map +1 -0
  125. package/dist/mcp/recordings.mjs +2390 -0
  126. package/dist/mcp/recordings.mjs.map +1 -0
  127. package/dist/mcp/state.d.mts +71 -0
  128. package/dist/mcp/state.d.mts.map +1 -0
  129. package/dist/mcp/state.mjs +83 -0
  130. package/dist/mcp/state.mjs.map +1 -0
  131. package/dist/mcp.d.mts +84 -0
  132. package/dist/mcp.d.mts.map +1 -0
  133. package/dist/mcp.mjs +241 -0
  134. package/dist/mcp.mjs.map +1 -0
  135. package/dist/messages.d.mts +183 -0
  136. package/dist/messages.d.mts.map +1 -0
  137. package/dist/messages.mjs +532 -0
  138. package/dist/messages.mjs.map +1 -0
  139. package/dist/microsoft/api.d.mts +47 -0
  140. package/dist/microsoft/api.d.mts.map +1 -0
  141. package/dist/microsoft/api.mjs +178 -0
  142. package/dist/microsoft/api.mjs.map +1 -0
  143. package/dist/microsoft/calendar.d.mts +30 -0
  144. package/dist/microsoft/calendar.d.mts.map +1 -0
  145. package/dist/microsoft/calendar.mjs +271 -0
  146. package/dist/microsoft/calendar.mjs.map +1 -0
  147. package/dist/microsoft/drive.d.mts +36 -0
  148. package/dist/microsoft/drive.d.mts.map +1 -0
  149. package/dist/microsoft/drive.mjs +298 -0
  150. package/dist/microsoft/drive.mjs.map +1 -0
  151. package/dist/microsoft/graph.d.mts +198 -0
  152. package/dist/microsoft/graph.d.mts.map +1 -0
  153. package/dist/microsoft/graph.mjs +264 -0
  154. package/dist/microsoft/graph.mjs.map +1 -0
  155. package/dist/microsoft/mail.d.mts +48 -0
  156. package/dist/microsoft/mail.d.mts.map +1 -0
  157. package/dist/microsoft/mail.mjs +488 -0
  158. package/dist/microsoft/mail.mjs.map +1 -0
  159. package/dist/microsoft/state.d.mts +480 -0
  160. package/dist/microsoft/state.d.mts.map +1 -0
  161. package/dist/microsoft/state.mjs +625 -0
  162. package/dist/microsoft/state.mjs.map +1 -0
  163. package/dist/microsoft.d.mts +168 -0
  164. package/dist/microsoft.d.mts.map +1 -0
  165. package/dist/microsoft.mjs +483 -0
  166. package/dist/microsoft.mjs.map +1 -0
  167. package/dist/node.d.mts +47 -0
  168. package/dist/node.d.mts.map +1 -0
  169. package/dist/node.mjs +178 -0
  170. package/dist/node.mjs.map +1 -0
  171. package/dist/notion/api.d.mts +51 -0
  172. package/dist/notion/api.d.mts.map +1 -0
  173. package/dist/notion/api.mjs +507 -0
  174. package/dist/notion/api.mjs.map +1 -0
  175. package/dist/notion/state.d.mts +329 -0
  176. package/dist/notion/state.d.mts.map +1 -0
  177. package/dist/notion/state.mjs +432 -0
  178. package/dist/notion/state.mjs.map +1 -0
  179. package/dist/notion.d.mts +72 -0
  180. package/dist/notion.d.mts.map +1 -0
  181. package/dist/notion.mjs +127 -0
  182. package/dist/notion.mjs.map +1 -0
  183. package/dist/openai.d.mts +165 -0
  184. package/dist/openai.d.mts.map +1 -0
  185. package/dist/openai.mjs +141 -0
  186. package/dist/openai.mjs.map +1 -0
  187. package/dist/opencode-recordings.d.mts +7 -0
  188. package/dist/opencode-recordings.d.mts.map +1 -0
  189. package/dist/opencode-recordings.mjs +222 -0
  190. package/dist/opencode-recordings.mjs.map +1 -0
  191. package/dist/opencode.d.mts +70 -0
  192. package/dist/opencode.d.mts.map +1 -0
  193. package/dist/opencode.mjs +217 -0
  194. package/dist/opencode.mjs.map +1 -0
  195. package/dist/r2-fixtures.d.mts +36 -0
  196. package/dist/r2-fixtures.d.mts.map +1 -0
  197. package/dist/r2-fixtures.mjs +245 -0
  198. package/dist/r2-fixtures.mjs.map +1 -0
  199. package/dist/r2-guard.d.mts +43 -0
  200. package/dist/r2-guard.d.mts.map +1 -0
  201. package/dist/r2-guard.mjs +215 -0
  202. package/dist/r2-guard.mjs.map +1 -0
  203. package/dist/r2.d.mts +166 -0
  204. package/dist/r2.d.mts.map +1 -0
  205. package/dist/r2.mjs +517 -0
  206. package/dist/r2.mjs.map +1 -0
  207. package/dist/responses.d.mts +231 -0
  208. package/dist/responses.d.mts.map +1 -0
  209. package/dist/responses.mjs +556 -0
  210. package/dist/responses.mjs.map +1 -0
  211. package/dist/route-evidence.d.mts +45 -0
  212. package/dist/route-evidence.d.mts.map +1 -0
  213. package/dist/route-evidence.mjs +56 -0
  214. package/dist/route-evidence.mjs.map +1 -0
  215. package/dist/router.d.mts +93 -0
  216. package/dist/router.d.mts.map +1 -0
  217. package/dist/router.mjs +259 -0
  218. package/dist/router.mjs.map +1 -0
  219. package/dist/stateful-core.d.mts +17 -0
  220. package/dist/stateful-core.d.mts.map +1 -0
  221. package/dist/stateful-core.mjs +41 -0
  222. package/dist/stateful-core.mjs.map +1 -0
  223. package/dist/stateful-emulator.d.mts +565 -0
  224. package/dist/stateful-emulator.d.mts.map +1 -0
  225. package/dist/stateful-emulator.mjs +1228 -0
  226. package/dist/stateful-emulator.mjs.map +1 -0
  227. package/dist/stateful-secrets.d.mts +84 -0
  228. package/dist/stateful-secrets.d.mts.map +1 -0
  229. package/dist/stateful-secrets.mjs +216 -0
  230. package/dist/stateful-secrets.mjs.map +1 -0
  231. package/dist/subscription-usage-recordings.d.mts +9 -0
  232. package/dist/subscription-usage-recordings.d.mts.map +1 -0
  233. package/dist/subscription-usage-recordings.mjs +53 -0
  234. package/dist/subscription-usage-recordings.mjs.map +1 -0
  235. package/dist/subscription-usage.d.mts +53 -0
  236. package/dist/subscription-usage.d.mts.map +1 -0
  237. package/dist/subscription-usage.mjs +21 -0
  238. package/dist/subscription-usage.mjs.map +1 -0
  239. package/dist/telegram/api.d.mts +34 -0
  240. package/dist/telegram/api.d.mts.map +1 -0
  241. package/dist/telegram/api.mjs +257 -0
  242. package/dist/telegram/api.mjs.map +1 -0
  243. package/dist/telegram/state.d.mts +128 -0
  244. package/dist/telegram/state.d.mts.map +1 -0
  245. package/dist/telegram/state.mjs +154 -0
  246. package/dist/telegram/state.mjs.map +1 -0
  247. package/dist/telegram.d.mts +62 -0
  248. package/dist/telegram.d.mts.map +1 -0
  249. package/dist/telegram.mjs +127 -0
  250. package/dist/telegram.mjs.map +1 -0
  251. package/dist/todoist/api.d.mts +55 -0
  252. package/dist/todoist/api.d.mts.map +1 -0
  253. package/dist/todoist/api.mjs +415 -0
  254. package/dist/todoist/api.mjs.map +1 -0
  255. package/dist/todoist/state.d.mts +275 -0
  256. package/dist/todoist/state.d.mts.map +1 -0
  257. package/dist/todoist/state.mjs +345 -0
  258. package/dist/todoist/state.mjs.map +1 -0
  259. package/dist/todoist.d.mts +68 -0
  260. package/dist/todoist.d.mts.map +1 -0
  261. package/dist/todoist.mjs +181 -0
  262. package/dist/todoist.mjs.map +1 -0
  263. package/dist/xai.d.mts +267 -0
  264. package/dist/xai.d.mts.map +1 -0
  265. package/dist/xai.mjs +252 -0
  266. package/dist/xai.mjs.map +1 -0
  267. package/package.json +157 -0
  268. package/src/anthropic.ts +289 -0
  269. package/src/chat-completions.ts +924 -0
  270. package/src/codex.ts +296 -0
  271. package/src/dropbox/api.ts +1084 -0
  272. package/src/dropbox/state.ts +364 -0
  273. package/src/dropbox.ts +203 -0
  274. package/src/email-fixtures.ts +1297 -0
  275. package/src/email.ts +1108 -0
  276. package/src/emulator-compose.ts +147 -0
  277. package/src/emulator-http.ts +184 -0
  278. package/src/emulator-kernel.ts +844 -0
  279. package/src/fixture-route.ts +561 -0
  280. package/src/fortnox/api.ts +1352 -0
  281. package/src/fortnox/state.ts +798 -0
  282. package/src/fortnox.ts +801 -0
  283. package/src/gateway-evaluate-recordings.ts +153 -0
  284. package/src/gateway.ts +530 -0
  285. package/src/github/api.ts +986 -0
  286. package/src/github/state.ts +439 -0
  287. package/src/github.ts +271 -0
  288. package/src/google/calendar.ts +486 -0
  289. package/src/google/drive.ts +471 -0
  290. package/src/google/gmail.ts +1011 -0
  291. package/src/google/shared.ts +321 -0
  292. package/src/google/state.ts +686 -0
  293. package/src/google.ts +298 -0
  294. package/src/linkedin-search/api.ts +363 -0
  295. package/src/linkedin-search/state.ts +373 -0
  296. package/src/linkedin-search.ts +241 -0
  297. package/src/mcp/api.ts +995 -0
  298. package/src/mcp/recordings.ts +2665 -0
  299. package/src/mcp/state.ts +125 -0
  300. package/src/mcp.ts +319 -0
  301. package/src/messages.ts +844 -0
  302. package/src/microsoft/api.ts +426 -0
  303. package/src/microsoft/calendar.ts +491 -0
  304. package/src/microsoft/drive.ts +541 -0
  305. package/src/microsoft/graph.ts +550 -0
  306. package/src/microsoft/mail.ts +833 -0
  307. package/src/microsoft/state.ts +822 -0
  308. package/src/microsoft.ts +982 -0
  309. package/src/node.ts +293 -0
  310. package/src/notion/api.ts +976 -0
  311. package/src/notion/state.ts +512 -0
  312. package/src/notion.ts +210 -0
  313. package/src/openai.ts +198 -0
  314. package/src/opencode-recordings.ts +257 -0
  315. package/src/opencode.ts +271 -0
  316. package/src/r2-fixtures.ts +295 -0
  317. package/src/r2-guard.ts +323 -0
  318. package/src/r2.ts +890 -0
  319. package/src/responses.ts +901 -0
  320. package/src/route-evidence.ts +90 -0
  321. package/src/router.ts +490 -0
  322. package/src/stateful-core.ts +70 -0
  323. package/src/stateful-emulator.ts +2571 -0
  324. package/src/stateful-secrets.ts +299 -0
  325. package/src/subscription-usage-recordings.ts +77 -0
  326. package/src/subscription-usage.ts +68 -0
  327. package/src/telegram/api.ts +437 -0
  328. package/src/telegram/state.ts +229 -0
  329. package/src/telegram.ts +227 -0
  330. package/src/todoist/api.ts +736 -0
  331. package/src/todoist/state.ts +456 -0
  332. package/src/todoist.ts +316 -0
  333. package/src/xai.ts +341 -0
@@ -0,0 +1,486 @@
1
+ /**
2
+ * Calendar routes of the Google emulator (internal): the time-range event listing with page
3
+ * tokens, and the lifecycle of a run-scoped event without attendees (create, read, PATCH rename,
4
+ * delete to `cancelled`, and the recorded 410 of a repeated delete).
5
+ *
6
+ * Every answer comes from the Calendar fixtures, through the seed or the request, or is minted
7
+ * (event ids, page tokens, `created` / `updated` from the injectable clock). A deleted event stays
8
+ * in the state as `cancelled`, as the fixtures read it back.
9
+ *
10
+ * @experimental
11
+ */
12
+ import { Predicate } from 'effect'
13
+ import type * as Schema from 'effect/Schema'
14
+ import {
15
+ exactBodyKeys,
16
+ exactQuery,
17
+ isNotEmulated,
18
+ notEmulated,
19
+ statefulRoute,
20
+ type EmulatedRequest,
21
+ type NotEmulated
22
+ } from '../stateful-emulator.ts'
23
+ import {
24
+ answer,
25
+ evidence,
26
+ googleEmulatorApisOrigin,
27
+ googleErrorBody,
28
+ googleJson,
29
+ googleNoContent,
30
+ isRunText,
31
+ listPage,
32
+ pageSize,
33
+ param,
34
+ recordedValue,
35
+ withoutQuery,
36
+ type GoogleRoute
37
+ } from './shared.ts'
38
+ import {
39
+ eventFields,
40
+ mintedEventId,
41
+ mintedEventIdPattern,
42
+ type GoogleEmulatorCalendar,
43
+ type GoogleEmulatorCalendarEvent,
44
+ type GoogleEmulatorEventBoundary,
45
+ type GoogleEmulatorState
46
+ } from './state.ts'
47
+
48
+ /** Path of the events collection of `{calendarId}`. */
49
+ export const calendarEventsPath = '/calendar/v3/calendars/{calendarId}/events'
50
+
51
+ const listCase = 'google.calendar.list-range-paging'
52
+
53
+ const lifecycleCase = 'google.calendar.event-lifecycle'
54
+
55
+ const deletedCase = 'google.calendar.deleted-event-gone'
56
+
57
+ /** A raw calendar id segment: an address-like id, its `@` percent-encoded once (`%40`). */
58
+ const calendarIdSegment = /^[A-Za-z0-9._+-]{1,200}(?:%40[A-Za-z0-9.-]{1,200})?$/
59
+
60
+ const eventIdSegment = /^[A-Za-z0-9_-]{1,1024}$/
61
+
62
+ const calendar = (
63
+ method: string,
64
+ path: string,
65
+ write: boolean,
66
+ caseIds: ReadonlyArray<string>,
67
+ withEvent: boolean
68
+ ) =>
69
+ evidence(
70
+ googleEmulatorApisOrigin,
71
+ method,
72
+ `${calendarEventsPath}${path}`,
73
+ write,
74
+ caseIds,
75
+ withEvent
76
+ ? { calendarId: calendarIdSegment, eventId: eventIdSegment }
77
+ : { calendarId: calendarIdSegment }
78
+ )
79
+
80
+ // Recorded values.
81
+
82
+ const createdDescription = 'Synthetic conformance event without attendees.'
83
+
84
+ const renamedDescription = 'Synthetic conformance event, renamed.'
85
+
86
+ const eventSummaryRest = 'synthetic conformance event, safe to delete'
87
+
88
+ /** The fixed half hour every recorded event create sends. */
89
+ const recordedStart = { dateTime: '2030-01-07T09:00:00Z', timeZone: 'UTC' }
90
+
91
+ const recordedEnd = { dateTime: '2030-01-07T09:30:00Z', timeZone: 'UTC' }
92
+
93
+ const instantPattern =
94
+ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?(?:Z|[+-](\d{2}):(\d{2}))$/
95
+
96
+ /**
97
+ * True for an RFC 3339 instant with a `Z` or numeric offset whose components are in range (a real
98
+ * calendar date, hour 0-23, minute and second 0-59, offset hour 0-23 and minute 0-59), checked
99
+ * before `Date.parse`, which would roll an out-of-range component over instead of refusing it.
100
+ */
101
+ const isInstant = (text: string): boolean => {
102
+ const parts = instantPattern.exec(text)
103
+
104
+ if (parts === null) return false
105
+
106
+ const [year, month, day, hour, minute, second, offsetHour, offsetMinute] = parts
107
+ .slice(1)
108
+ .map(part => Number(part ?? '0'))
109
+
110
+ if (year === undefined || month === undefined || day === undefined) return false
111
+
112
+ const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0
113
+ const days = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month - 1] ?? 0
114
+
115
+ return (
116
+ day >= 1 &&
117
+ day <= days &&
118
+ (hour ?? 24) <= 23 &&
119
+ (minute ?? 60) <= 59 &&
120
+ (second ?? 60) <= 59 &&
121
+ (offsetHour ?? 24) <= 23 &&
122
+ (offsetMinute ?? 60) <= 59 &&
123
+ Number.isFinite(Date.parse(text))
124
+ )
125
+ }
126
+
127
+ // Rendering, in the fixture key order.
128
+
129
+ type EventHead = {
130
+ readonly kind: string
131
+ readonly etag: string
132
+ readonly id: string
133
+ readonly status: string
134
+ readonly htmlLink: string
135
+ readonly created: string
136
+ readonly updated: string
137
+ readonly summary: string
138
+ description?: string
139
+ }
140
+
141
+ type EventsPage = {
142
+ readonly kind: string
143
+ readonly summary: string
144
+ readonly timeZone: string
145
+ readonly accessRole: string
146
+ readonly items: ReadonlyArray<Schema.Json>
147
+ nextPageToken?: string
148
+ }
149
+
150
+ const renderEvent = (
151
+ state: GoogleEmulatorState,
152
+ owner: GoogleEmulatorCalendar,
153
+ event: GoogleEmulatorCalendarEvent
154
+ ): Schema.JsonObject => {
155
+ const head: EventHead = {
156
+ kind: 'calendar#event',
157
+ etag: event.etag,
158
+ id: event.id,
159
+ status: event.status,
160
+ htmlLink: event.htmlLink,
161
+ created: event.created,
162
+ updated: event.updated,
163
+ summary: event.summary
164
+ }
165
+
166
+ // The fixtures show `description` only on events that have one.
167
+ if (event.description !== undefined) head.description = event.description
168
+
169
+ return {
170
+ ...head,
171
+ creator: { email: state.practiceAddress, self: true },
172
+ organizer: { email: owner.id, displayName: owner.summary, self: true },
173
+ start: { ...event.start },
174
+ end: { ...event.end },
175
+ iCalUID: event.iCalUID,
176
+ sequence: event.sequence,
177
+ reminders: { useDefault: true },
178
+ eventType: 'default'
179
+ }
180
+ }
181
+
182
+ /** The instant a boundary starts (an all-day date at midnight UTC), in milliseconds. */
183
+ const instant = (boundary: GoogleEmulatorEventBoundary): number =>
184
+ 'dateTime' in boundary ? Date.parse(boundary.dateTime) : Date.parse(`${boundary.date}T00:00:00Z`)
185
+
186
+ const findCalendar = (state: GoogleEmulatorState, id: string) =>
187
+ state.calendars.find(candidate => candidate.id === id)
188
+
189
+ const findEvent = (state: GoogleEmulatorState, calendarId: string, eventId: string) =>
190
+ state.events.find(event => event.calendarId === calendarId && event.id === eventId)
191
+
192
+ const isCreatedHere = (event: GoogleEmulatorCalendarEvent): boolean =>
193
+ mintedEventIdPattern.test(event.id)
194
+
195
+ const replaceEvent = (state: GoogleEmulatorState, next: GoogleEmulatorCalendarEvent) =>
196
+ state.events.map(event =>
197
+ event.calendarId === next.calendarId && event.id === next.id ? next : event
198
+ )
199
+
200
+ /** The `{ dateTime, timeZone }` boundary a fixture records, exactly. */
201
+ const isRecordedBoundary = (
202
+ value: Schema.Json | undefined,
203
+ recorded: { readonly dateTime: string; readonly timeZone: string }
204
+ ): boolean => {
205
+ const boundary = exactBodyKeys(value, 'boundary', ['dateTime', 'timeZone'])
206
+
207
+ return (
208
+ !isNotEmulated(boundary) &&
209
+ boundary.dateTime === recorded.dateTime &&
210
+ boundary.timeZone === recorded.timeZone
211
+ )
212
+ }
213
+
214
+ // Listing.
215
+
216
+ type ListInput = {
217
+ readonly calendarId: string
218
+ readonly timeMin: string
219
+ readonly timeMax: string
220
+ readonly maxResults: number
221
+ readonly pageToken: string | undefined
222
+ }
223
+
224
+ const listEvents: GoogleRoute = statefulRoute(
225
+ calendar('GET', '', false, [listCase], false),
226
+ 'none',
227
+ (request): ListInput | NotEmulated => {
228
+ const query = exactQuery(
229
+ request,
230
+ ['timeMin', 'timeMax', 'maxResults', 'singleEvents', 'orderBy'],
231
+ ['pageToken']
232
+ )
233
+
234
+ if (isNotEmulated(query)) return query
235
+
236
+ const recorded =
237
+ recordedValue(query, 'singleEvents', 'true') ?? recordedValue(query, 'orderBy', 'startTime')
238
+
239
+ if (recorded !== undefined) return recorded
240
+
241
+ const timeMin = query['timeMin'] ?? ''
242
+ const timeMax = query['timeMax'] ?? ''
243
+
244
+ if (
245
+ !isInstant(timeMin) ||
246
+ !isInstant(timeMax) ||
247
+ !(Date.parse(timeMin) < Date.parse(timeMax))
248
+ ) {
249
+ return notEmulated('timeMin and timeMax must be RFC 3339 instants, timeMin first')
250
+ }
251
+
252
+ const maxResults = pageSize(query['maxResults'], 'maxResults', 1, 2500)
253
+
254
+ if (isNotEmulated(maxResults)) return maxResults
255
+
256
+ const pageToken = query['pageToken']
257
+
258
+ return pageToken === ''
259
+ ? notEmulated('an empty pageToken is not emulated')
260
+ : { calendarId: param(request, 'calendarId'), timeMin, timeMax, maxResults, pageToken }
261
+ },
262
+ (state, input, { env }) => {
263
+ const owner = findCalendar(state, input.calendarId)
264
+
265
+ if (owner === undefined) return notEmulated('a calendar the state does not hold')
266
+
267
+ const from = Date.parse(input.timeMin)
268
+ const to = Date.parse(input.timeMax)
269
+
270
+ // By start time (`orderBy=startTime`); `sort` is stable, so equal starts keep the state order.
271
+ const overlapping = state.events
272
+ .filter(
273
+ event =>
274
+ event.calendarId === owner.id && instant(event.start) < to && instant(event.end) > from
275
+ )
276
+ .sort((left, right) => instant(left.start) - instant(right.start))
277
+
278
+ // Every recorded listing has events, and none of them is cancelled.
279
+ if (overlapping.length === 0) {
280
+ return notEmulated('a range without events is not emulated (no fixture records one)')
281
+ }
282
+
283
+ if (overlapping.some(event => event.status === 'cancelled')) {
284
+ return notEmulated('a range holding a cancelled event is not emulated')
285
+ }
286
+
287
+ return listPage(
288
+ env,
289
+ 'calendar',
290
+ `calendar\u0000${owner.id}\u0000${input.timeMin}\u0000${input.timeMax}\u0000${input.maxResults}`,
291
+ overlapping.map(event => renderEvent(state, owner, event)),
292
+ input.maxResults,
293
+ input.pageToken,
294
+ env.drills.calendarPageRepeats,
295
+ (page, nextPageToken) => {
296
+ const body: EventsPage = {
297
+ kind: 'calendar#events',
298
+ summary: owner.summary,
299
+ timeZone: owner.timeZone,
300
+ accessRole: owner.accessRole,
301
+ items: [...page]
302
+ }
303
+
304
+ // The last page carries no `nextPageToken`.
305
+ if (nextPageToken !== undefined) body.nextPageToken = nextPageToken
306
+
307
+ return googleJson(200, body)
308
+ }
309
+ )
310
+ }
311
+ )
312
+
313
+ // The event lifecycle.
314
+
315
+ type CreateInput = { readonly calendarId: string; readonly summary: string }
316
+
317
+ const createEvent: GoogleRoute = statefulRoute(
318
+ calendar('POST', '', true, [lifecycleCase, deletedCase], false),
319
+ 'json',
320
+ (request): CreateInput | NotEmulated => {
321
+ const body =
322
+ withoutQuery(request) ??
323
+ exactBodyKeys(request.json, 'the event body', ['summary', 'description', 'start', 'end'])
324
+
325
+ if (isNotEmulated(body)) return body
326
+
327
+ const summary = Predicate.isString(body.summary) ? body.summary : ''
328
+
329
+ if (
330
+ !isRunText(summary, 'event', eventSummaryRest) &&
331
+ !isRunText(summary, 'deleted event', eventSummaryRest)
332
+ ) {
333
+ return notEmulated('an event summary other than the recorded run-scoped ones')
334
+ }
335
+
336
+ if (body.description !== createdDescription) {
337
+ return notEmulated('an event description other than the recorded one is not emulated')
338
+ }
339
+
340
+ return isRecordedBoundary(body.start, recordedStart) &&
341
+ isRecordedBoundary(body.end, recordedEnd)
342
+ ? { calendarId: param(request, 'calendarId'), summary }
343
+ : notEmulated('event times other than the recorded half hour are not emulated')
344
+ },
345
+ (state, input, { env }) => {
346
+ const owner = findCalendar(state, input.calendarId)
347
+
348
+ if (owner === undefined) return notEmulated('a calendar the state does not hold')
349
+
350
+ return () => {
351
+ // Read the clock before any write: a failing clock writes nothing.
352
+ const at = new Date(env.now()).toISOString()
353
+ const number = state.counters.nextEventNumber
354
+ const id = mintedEventId(number)
355
+
356
+ const event: GoogleEmulatorCalendarEvent = {
357
+ calendarId: owner.id,
358
+ id,
359
+ ...eventFields(id),
360
+ status: 'confirmed',
361
+ created: at,
362
+ updated: at,
363
+ summary: input.summary,
364
+ description: createdDescription,
365
+ start: { ...recordedStart },
366
+ end: { ...recordedEnd },
367
+ sequence: 0
368
+ }
369
+
370
+ state.counters = { ...state.counters, nextEventNumber: number + 1 }
371
+ state.events = [...state.events, event]
372
+
373
+ return googleJson(200, renderEvent(state, owner, event))
374
+ }
375
+ }
376
+ )
377
+
378
+ type EventRef = { readonly calendarId: string; readonly eventId: string }
379
+
380
+ const eventRef = (request: EmulatedRequest): EventRef => ({
381
+ calendarId: param(request, 'calendarId'),
382
+ eventId: param(request, 'eventId')
383
+ })
384
+
385
+ const getEvent: GoogleRoute = statefulRoute(
386
+ calendar('GET', '/{eventId}', false, [lifecycleCase, deletedCase], true),
387
+ 'none',
388
+ request => withoutQuery(request) ?? eventRef(request),
389
+ (state, input) => {
390
+ const owner = findCalendar(state, input.calendarId)
391
+ const event = findEvent(state, input.calendarId, input.eventId)
392
+
393
+ return owner === undefined || event === undefined
394
+ ? notEmulated('an event the state does not hold is not emulated')
395
+ : answer(() => googleJson(200, renderEvent(state, owner, event)))
396
+ }
397
+ )
398
+
399
+ type PatchInput = EventRef & { readonly summary: string }
400
+
401
+ const patchEvent: GoogleRoute = statefulRoute(
402
+ calendar('PATCH', '/{eventId}', true, [lifecycleCase], true),
403
+ 'json',
404
+ (request): PatchInput | NotEmulated => {
405
+ const body =
406
+ withoutQuery(request) ??
407
+ exactBodyKeys(request.json, 'the event patch', ['summary', 'description'])
408
+
409
+ if (isNotEmulated(body)) return body
410
+
411
+ const summary = Predicate.isString(body.summary) ? body.summary : ''
412
+
413
+ return isRunText(summary, 'event renamed', eventSummaryRest) &&
414
+ body.description === renamedDescription
415
+ ? { ...eventRef(request), summary }
416
+ : notEmulated('an event patch other than the recorded rename is not emulated')
417
+ },
418
+ (state, input, { env }) => {
419
+ const owner = findCalendar(state, input.calendarId)
420
+ const event = findEvent(state, input.calendarId, input.eventId)
421
+
422
+ if (
423
+ owner === undefined ||
424
+ event === undefined ||
425
+ !isCreatedHere(event) ||
426
+ event.status !== 'confirmed'
427
+ ) {
428
+ return notEmulated('renaming anything but a live event created here is not emulated')
429
+ }
430
+
431
+ return () => {
432
+ const at = new Date(env.now()).toISOString()
433
+
434
+ const next: GoogleEmulatorCalendarEvent = {
435
+ ...event,
436
+ updated: at,
437
+ summary: env.drills.eventPatchKeepsSummary ? event.summary : input.summary,
438
+ description: renamedDescription
439
+ }
440
+
441
+ state.events = replaceEvent(state, next)
442
+
443
+ return googleJson(200, renderEvent(state, owner, next))
444
+ }
445
+ }
446
+ )
447
+
448
+ const deleteEvent: GoogleRoute = statefulRoute(
449
+ calendar('DELETE', '/{eventId}', true, [lifecycleCase, deletedCase], true),
450
+ 'none',
451
+ request => withoutQuery(request) ?? eventRef(request),
452
+ (state, input, { env }) => {
453
+ const event = findEvent(state, input.calendarId, input.eventId)
454
+
455
+ if (event === undefined || !isCreatedHere(event)) {
456
+ return notEmulated('deleting anything but an event created here is not emulated')
457
+ }
458
+
459
+ // The recorded repeated delete: 410 Gone (the drill answers it as 409).
460
+ if (event.status === 'cancelled') {
461
+ return answer(() =>
462
+ googleJson(
463
+ env.drills.repeatedEventDeleteConflict ? 409 : 410,
464
+ googleErrorBody(410, 'Resource has been deleted', 'deleted', 'GONE')
465
+ )
466
+ )
467
+ }
468
+
469
+ return () => {
470
+ const at = new Date(env.now()).toISOString()
471
+
472
+ state.events = replaceEvent(state, { ...event, status: 'cancelled', updated: at })
473
+
474
+ return googleNoContent()
475
+ }
476
+ }
477
+ )
478
+
479
+ /** The Calendar routes, in manifest order. */
480
+ export const calendarRoutes: ReadonlyArray<GoogleRoute> = [
481
+ listEvents,
482
+ createEvent,
483
+ getEvent,
484
+ patchEvent,
485
+ deleteEvent
486
+ ]