@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,491 @@
1
+ /**
2
+ * Microsoft Graph emulator calendar routes (internal): `calendarView`, event create, get, update,
3
+ * delete, and cancel. Wire shapes follow the synthetic calendar conformance fixtures.
4
+ *
5
+ * Event times are stored in UTC with seven fractional digits and answered in UTC. Every calendar
6
+ * fixture sends `Prefer: outlook.timezone="UTC"`, so every calendar route needs it; without it, or
7
+ * with another time zone, a request is refused (fail closed). Calendar views need `$top` and answer
8
+ * one page (no `$skip`, no `@odata.nextLink`), as the fixtures record them. Events are answered with
9
+ * the fixture fields only (`@odata.etag`, `id`, `subject`, `start`, `end`, `isCancelled`); a create
10
+ * takes the fixture's fields (`subject`, a text `body`, `start`, `end`, `isReminderOn`, `showAs`)
11
+ * and an update only `subject`; anything else, including unknown nested keys, fails closed.
12
+ *
13
+ * @experimental
14
+ */
15
+ import { Predicate } from 'effect'
16
+ import type * as Schema from 'effect/Schema'
17
+ import {
18
+ bodyObject,
19
+ codes,
20
+ collection,
21
+ emptyResponse,
22
+ entity,
23
+ invalidValue,
24
+ jsonResponse,
25
+ metadataContext,
26
+ nestedObject,
27
+ nextChangeKey,
28
+ notEmulated,
29
+ nowTimestamp,
30
+ odataKey,
31
+ padded,
32
+ project,
33
+ resolveUser,
34
+ selectSuffix,
35
+ selectedFields,
36
+ singlePage,
37
+ textBody,
38
+ type MicrosoftApiEnv,
39
+ type RouteHandler,
40
+ type RouteRequest
41
+ } from './graph.ts'
42
+ import {
43
+ formatLocalDateTime,
44
+ parseInstant,
45
+ type MicrosoftEmulatorEvent,
46
+ type MicrosoftEmulatorState
47
+ } from './state.ts'
48
+
49
+ /** The event fields the fixtures answer (and `$select`). */
50
+ const eventFields: ReadonlyArray<string> = ['id', 'subject', 'start', 'end', 'isCancelled']
51
+
52
+ const showAsValues: ReadonlyArray<MicrosoftEmulatorEvent['showAs']> = [
53
+ 'free',
54
+ 'tentative',
55
+ 'busy',
56
+ 'oof',
57
+ 'workingElsewhere',
58
+ 'unknown'
59
+ ]
60
+
61
+ const isShowAs = (value: unknown): value is MicrosoftEmulatorEvent['showAs'] =>
62
+ showAsValues.some(candidate => candidate === value)
63
+
64
+ const eventTagPrefix = 'DwAAABYAAAAsynthetic'
65
+
66
+ const renderDateTime = (env: MicrosoftApiEnv, value: string) => {
67
+ const ticks = parseInstant(value, 'forbidden')
68
+
69
+ return {
70
+ dateTime:
71
+ ticks === undefined ? value : formatLocalDateTime(ticks, env.drills.timestampPrecisionDigits),
72
+ timeZone: 'UTC'
73
+ }
74
+ }
75
+
76
+ /** The event as the fixtures answer it, in their key order. */
77
+ const renderEvent = (env: MicrosoftApiEnv, event: MicrosoftEmulatorEvent): Schema.JsonObject => ({
78
+ '@odata.etag': `W/"${event.changeKey}"`,
79
+ id: event.id,
80
+ subject: event.subject,
81
+ start: renderDateTime(env, event.start),
82
+ end: renderDateTime(env, event.end),
83
+ isCancelled: event.isCancelled
84
+ })
85
+
86
+ /** `users('{userId}')`, the context prefix of every calendar route. */
87
+ const userContext = (request: RouteRequest): string =>
88
+ `users${odataKey(request.params.userId ?? '')}`
89
+
90
+ /** `users('{userId}')/calendars('{calendarId}')`. */
91
+ const calendarContext = (request: RouteRequest): string =>
92
+ `${userContext(request)}/calendars${odataKey(request.params.calendarId ?? '')}`
93
+
94
+ /**
95
+ * Every calendar fixture sends `Prefer: outlook.timezone="UTC"`: a request without it, or with any
96
+ * other time zone, is not emulated.
97
+ */
98
+ const timezoneProblem = (request: RouteRequest): Response | undefined => {
99
+ const timezone = request.prefer.timezone
100
+
101
+ if (timezone === undefined) {
102
+ return notEmulated(
103
+ request,
104
+ 'calendar requests without Prefer: outlook.timezone="UTC" are not emulated.'
105
+ )
106
+ }
107
+
108
+ return timezone.toUpperCase() === 'UTC'
109
+ ? undefined
110
+ : notEmulated(request, `outlook.timezone ${timezone} is not emulated (UTC only).`)
111
+ }
112
+
113
+ /** `preference-applied` on reads (which asked for UTC), as the fixtures record it. */
114
+ const readHeaders: HeadersInit = { 'preference-applied': 'outlook.timezone="UTC"' }
115
+
116
+ const findCalendar = (state: MicrosoftEmulatorState, request: RouteRequest) =>
117
+ state.calendars.find(calendar => calendar.id === request.params.calendarId)
118
+
119
+ const calendarNotFound = (request: RouteRequest): Response =>
120
+ request.error(404, codes.mailItemNotFound, 'The specified object was not found in the store.')
121
+
122
+ const eventNotFound = calendarNotFound
123
+
124
+ const findEvent = (state: MicrosoftEmulatorState, request: RouteRequest) =>
125
+ state.events.find(event => event.id === request.params.eventId)
126
+
127
+ /** Ticks of a stored UTC date-time (always readable: the state schema checks the pattern). */
128
+ const ticksOf = (value: string): bigint => parseInstant(value, 'forbidden') ?? BigInt(0)
129
+
130
+ /** The largest calendar view `$top` a fixture sends (`$top=50`, list-range and precision cases). */
131
+ const calendarViewMaxTop = 50
132
+
133
+ /**
134
+ * `GET .../calendars/{calendarId}/calendarView?startDateTime=&endDateTime=`: the events that
135
+ * overlap `[start, end)` (start before the range end and end after the range start), ordered by
136
+ * start, at most `$top` of them (more is not emulated: no fixture pages a calendar view; `$top`
137
+ * is required, at most `calendarViewMaxTop`).
138
+ */
139
+ export const calendarView: RouteHandler = (state, request, env) => {
140
+ const user = resolveUser(state, request)
141
+
142
+ if (user instanceof Response) return user
143
+
144
+ const zone = timezoneProblem(request)
145
+
146
+ if (zone !== undefined) return zone
147
+
148
+ if (findCalendar(state, request) === undefined) return calendarNotFound(request)
149
+
150
+ const rawStart = request.query.get('startDateTime')
151
+ const rawEnd = request.query.get('endDateTime')
152
+
153
+ if (rawStart === null || rawEnd === null) {
154
+ return invalidValue(request, 'calendarView needs startDateTime and endDateTime.')
155
+ }
156
+
157
+ const start = parseInstant(rawStart, 'optional')
158
+ const end = parseInstant(rawEnd, 'optional')
159
+
160
+ if (start === undefined || end === undefined) {
161
+ return invalidValue(request, 'startDateTime and endDateTime must be ISO-8601 date-times.')
162
+ }
163
+
164
+ const fields = selectedFields(request, eventFields)
165
+
166
+ if (fields instanceof Response) return fields
167
+
168
+ const matching = env.drills.calendarRangeEmpty
169
+ ? []
170
+ : state.events
171
+ .filter(
172
+ event =>
173
+ event.calendarId === request.params.calendarId &&
174
+ ticksOf(event.start) < end &&
175
+ ticksOf(event.end) > start
176
+ )
177
+ .sort((left, right) =>
178
+ left.start === right.start
179
+ ? left.id.localeCompare(right.id)
180
+ : left.start < right.start
181
+ ? -1
182
+ : 1
183
+ )
184
+
185
+ const page = singlePage(matching, request, calendarViewMaxTop)
186
+
187
+ if (page instanceof Response) return page
188
+
189
+ return jsonResponse(
190
+ 200,
191
+ collection(
192
+ metadataContext(env, `${calendarContext(request)}/calendarView${selectSuffix(fields)}`),
193
+ page.map(event => project(renderEvent(env, event), fields)),
194
+ undefined
195
+ ),
196
+ readHeaders
197
+ )
198
+ }
199
+
200
+ /** A `dateTimeTimeZone` write value in UTC, as a seven-digit local date-time. */
201
+ const writeDateTime = (
202
+ request: RouteRequest,
203
+ key: string,
204
+ value: Schema.Json
205
+ ): string | Response => {
206
+ const dateTimeZone = nestedObject(
207
+ request,
208
+ value,
209
+ ['dateTime', 'timeZone'],
210
+ `${key} { dateTime, timeZone }`
211
+ )
212
+
213
+ if (dateTimeZone instanceof Response) return dateTimeZone
214
+
215
+ const { dateTime, timeZone } = dateTimeZone
216
+
217
+ if (!Predicate.isString(dateTime) || !Predicate.isString(timeZone)) {
218
+ return invalidValue(request, `${key} must be { dateTime, timeZone } with string values.`)
219
+ }
220
+
221
+ if (timeZone.toUpperCase() !== 'UTC') {
222
+ return notEmulated(request, `${key}.timeZone ${timeZone} is not emulated (UTC only).`)
223
+ }
224
+
225
+ const ticks = parseInstant(dateTime, 'forbidden')
226
+
227
+ return ticks === undefined
228
+ ? invalidValue(request, `${key}.dateTime must be a local date-time without offset.`)
229
+ : formatLocalDateTime(ticks, 7)
230
+ }
231
+
232
+ type EventWrite = {
233
+ subject?: string
234
+ bodyContent?: string
235
+ start?: string
236
+ end?: string
237
+ isReminderOn?: boolean
238
+ showAs?: MicrosoftEmulatorEvent['showAs']
239
+ }
240
+
241
+ /** Event create keys, as the create fixture sends them. */
242
+ const createEventKeys: ReadonlyArray<string> = [
243
+ 'subject',
244
+ 'body',
245
+ 'start',
246
+ 'end',
247
+ 'isReminderOn',
248
+ 'showAs'
249
+ ]
250
+
251
+ /** Event update keys, as the update fixture sends them. */
252
+ const updateEventKeys: ReadonlyArray<string> = ['subject']
253
+
254
+ /** The emulated event fields of a create or update body; attendees and the rest fail closed. */
255
+ const eventWrite = (
256
+ request: RouteRequest,
257
+ allowed: ReadonlyArray<string>
258
+ ): EventWrite | Response => {
259
+ const fields = bodyObject(request, allowed)
260
+
261
+ if (fields instanceof Response) return fields
262
+
263
+ const write: EventWrite = {}
264
+
265
+ for (const [key, value] of Object.entries(fields)) {
266
+ switch (key) {
267
+ case 'subject':
268
+ if (!Predicate.isString(value)) return invalidValue(request, 'subject must be a string.')
269
+
270
+ write.subject = value
271
+ break
272
+ case 'body': {
273
+ const content = textBody(request, value)
274
+
275
+ if (content instanceof Response) return content
276
+
277
+ write.bodyContent = content
278
+ break
279
+ }
280
+
281
+ case 'start':
282
+ case 'end': {
283
+ const dateTime = writeDateTime(request, key, value)
284
+
285
+ if (dateTime instanceof Response) return dateTime
286
+
287
+ write[key] = dateTime
288
+ break
289
+ }
290
+
291
+ case 'isReminderOn':
292
+ if (!Predicate.isBoolean(value)) {
293
+ return invalidValue(request, 'isReminderOn must be a boolean.')
294
+ }
295
+
296
+ write.isReminderOn = value
297
+ break
298
+ case 'showAs':
299
+ if (!isShowAs(value)) return invalidValue(request, 'showAs is not a Graph freeBusyStatus.')
300
+
301
+ write.showAs = value
302
+ break
303
+ }
304
+ }
305
+
306
+ return write
307
+ }
308
+
309
+ /**
310
+ * `POST .../calendars/{calendarId}/events`: create a single-instance, attendee-free event (201
311
+ * with the event; the `createOmitsId` drill knob drops its `id`).
312
+ */
313
+ export const createEvent: RouteHandler = (state, request, env) => {
314
+ const user = resolveUser(state, request)
315
+
316
+ if (user instanceof Response) return user
317
+
318
+ const zone = timezoneProblem(request)
319
+
320
+ if (zone !== undefined) return zone
321
+
322
+ if (findCalendar(state, request) === undefined) return calendarNotFound(request)
323
+
324
+ const write = eventWrite(request, createEventKeys)
325
+
326
+ if (write instanceof Response) return write
327
+
328
+ if (write.start === undefined || write.end === undefined) {
329
+ return invalidValue(request, 'a new event needs start and end.')
330
+ }
331
+
332
+ if (write.end < write.start) return invalidValue(request, 'the event end is before its start.')
333
+
334
+ let number = state.counters.nextEventNumber
335
+ let id = `AAMkAGI2-synthetic-event-${padded(number, 4)}=`
336
+
337
+ while (state.events.some(event => event.id === id)) {
338
+ number += 1
339
+ id = `AAMkAGI2-synthetic-event-${padded(number, 4)}=`
340
+ }
341
+
342
+ state.counters = { ...state.counters, nextEventNumber: number + 1 }
343
+
344
+ const now = nowTimestamp(env)
345
+
346
+ const event: MicrosoftEmulatorEvent = {
347
+ id,
348
+ calendarId: request.params.calendarId ?? '',
349
+ subject: write.subject ?? '',
350
+ bodyContentType: 'text',
351
+ bodyContent: write.bodyContent ?? '',
352
+ start: write.start,
353
+ end: write.end,
354
+ isCancelled: false,
355
+ isReminderOn: write.isReminderOn ?? true,
356
+ showAs: write.showAs ?? 'busy',
357
+ changeKey: nextChangeKey(state, eventTagPrefix),
358
+ createdDateTime: now,
359
+ lastModifiedDateTime: now
360
+ }
361
+
362
+ state.events = [...state.events, event]
363
+
364
+ const { id: _id, ...withoutId } = renderEvent(env, event)
365
+ const context = metadataContext(env, `${calendarContext(request)}/events/$entity`)
366
+
367
+ return jsonResponse(
368
+ 201,
369
+ entity(context, env.drills.createOmitsId ? withoutId : renderEvent(env, event))
370
+ )
371
+ }
372
+
373
+ /** `GET /users/{userId}/events/{eventId}` (`$select`). */
374
+ export const getEvent: RouteHandler = (state, request, env) => {
375
+ const user = resolveUser(state, request)
376
+
377
+ if (user instanceof Response) return user
378
+
379
+ const zone = timezoneProblem(request)
380
+
381
+ if (zone !== undefined) return zone
382
+
383
+ const fields = selectedFields(request, eventFields)
384
+
385
+ if (fields instanceof Response) return fields
386
+
387
+ const event = findEvent(state, request)
388
+
389
+ const context = metadataContext(
390
+ env,
391
+ `${userContext(request)}/events${selectSuffix(fields)}/$entity`
392
+ )
393
+
394
+ return event === undefined
395
+ ? eventNotFound(request)
396
+ : jsonResponse(200, entity(context, project(renderEvent(env, event), fields)), readHeaders)
397
+ }
398
+
399
+ /** `PATCH /users/{userId}/events/{eventId}`: update `subject`; 200 with the event. */
400
+ export const updateEvent: RouteHandler = (state, request, env) => {
401
+ const user = resolveUser(state, request)
402
+
403
+ if (user instanceof Response) return user
404
+
405
+ const zone = timezoneProblem(request)
406
+
407
+ if (zone !== undefined) return zone
408
+
409
+ const event = findEvent(state, request)
410
+
411
+ if (event === undefined) return eventNotFound(request)
412
+
413
+ const write = eventWrite(request, updateEventKeys)
414
+
415
+ if (write instanceof Response) return write
416
+
417
+ const updated: MicrosoftEmulatorEvent = { ...event, ...write }
418
+
419
+ if (updated.end < updated.start) {
420
+ return invalidValue(request, 'the event end is before its start.')
421
+ }
422
+
423
+ const stored: MicrosoftEmulatorEvent = {
424
+ ...updated,
425
+ changeKey: nextChangeKey(state, eventTagPrefix),
426
+ lastModifiedDateTime: nowTimestamp(env)
427
+ }
428
+
429
+ state.events = state.events.map(candidate => (candidate.id === stored.id ? stored : candidate))
430
+
431
+ return jsonResponse(
432
+ 200,
433
+ entity(metadataContext(env, `${userContext(request)}/events/$entity`), renderEvent(env, stored))
434
+ )
435
+ }
436
+
437
+ const removeEvent = (state: MicrosoftEmulatorState, id: string): void => {
438
+ state.events = state.events.filter(event => event.id !== id)
439
+ }
440
+
441
+ /** `DELETE /users/{userId}/events/{eventId}`: 204; later reads answer 404. */
442
+ export const deleteEvent: RouteHandler = (state, request) => {
443
+ const user = resolveUser(state, request)
444
+
445
+ if (user instanceof Response) return user
446
+
447
+ const zone = timezoneProblem(request)
448
+
449
+ if (zone !== undefined) return zone
450
+
451
+ const event = findEvent(state, request)
452
+
453
+ if (event === undefined) return eventNotFound(request)
454
+
455
+ removeEvent(state, event.id)
456
+
457
+ return emptyResponse(204)
458
+ }
459
+
460
+ /**
461
+ * `POST /users/{userId}/events/{eventId}/cancel` (`{ comment? }`): 202 with an empty body, and
462
+ * the event is removed (not kept with `isCancelled: true`), so a later GET or DELETE answers 404,
463
+ * as the cancel fixture records. The emulated events have no attendees: nothing is sent.
464
+ */
465
+ export const cancelEvent: RouteHandler = (state, request) => {
466
+ const user = resolveUser(state, request)
467
+
468
+ if (user instanceof Response) return user
469
+
470
+ const zone = timezoneProblem(request)
471
+
472
+ if (zone !== undefined) return zone
473
+
474
+ const event = findEvent(state, request)
475
+
476
+ if (event === undefined) return eventNotFound(request)
477
+
478
+ if (request.body !== undefined) {
479
+ const fields = bodyObject(request, ['comment'])
480
+
481
+ if (fields instanceof Response) return fields
482
+
483
+ if (fields.comment !== undefined && !Predicate.isString(fields.comment)) {
484
+ return invalidValue(request, 'comment must be a string.')
485
+ }
486
+ }
487
+
488
+ removeEvent(state, event.id)
489
+
490
+ return emptyResponse(202)
491
+ }