@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.
- package/LICENSE +21 -0
- package/README.md +2135 -0
- package/dist/anthropic.d.mts +269 -0
- package/dist/anthropic.d.mts.map +1 -0
- package/dist/anthropic.mjs +177 -0
- package/dist/anthropic.mjs.map +1 -0
- package/dist/chat-completions.d.mts +288 -0
- package/dist/chat-completions.d.mts.map +1 -0
- package/dist/chat-completions.mjs +451 -0
- package/dist/chat-completions.mjs.map +1 -0
- package/dist/codex.d.mts +265 -0
- package/dist/codex.d.mts.map +1 -0
- package/dist/codex.mjs +199 -0
- package/dist/codex.mjs.map +1 -0
- package/dist/dropbox/api.d.mts +63 -0
- package/dist/dropbox/api.d.mts.map +1 -0
- package/dist/dropbox/api.mjs +565 -0
- package/dist/dropbox/api.mjs.map +1 -0
- package/dist/dropbox/state.d.mts +136 -0
- package/dist/dropbox/state.d.mts.map +1 -0
- package/dist/dropbox/state.mjs +209 -0
- package/dist/dropbox/state.mjs.map +1 -0
- package/dist/dropbox.d.mts +79 -0
- package/dist/dropbox.d.mts.map +1 -0
- package/dist/dropbox.mjs +124 -0
- package/dist/dropbox.mjs.map +1 -0
- package/dist/email-fixtures.d.mts +36 -0
- package/dist/email-fixtures.d.mts.map +1 -0
- package/dist/email-fixtures.mjs +1080 -0
- package/dist/email-fixtures.mjs.map +1 -0
- package/dist/email.d.mts +160 -0
- package/dist/email.d.mts.map +1 -0
- package/dist/email.mjs +608 -0
- package/dist/email.mjs.map +1 -0
- package/dist/emulator-compose.d.mts +40 -0
- package/dist/emulator-compose.d.mts.map +1 -0
- package/dist/emulator-compose.mjs +91 -0
- package/dist/emulator-compose.mjs.map +1 -0
- package/dist/emulator-http.d.mts +53 -0
- package/dist/emulator-http.d.mts.map +1 -0
- package/dist/emulator-http.mjs +117 -0
- package/dist/emulator-http.mjs.map +1 -0
- package/dist/emulator-kernel.d.mts +176 -0
- package/dist/emulator-kernel.d.mts.map +1 -0
- package/dist/emulator-kernel.mjs +413 -0
- package/dist/emulator-kernel.mjs.map +1 -0
- package/dist/fixture-route.d.mts +129 -0
- package/dist/fixture-route.d.mts.map +1 -0
- package/dist/fixture-route.mjs +257 -0
- package/dist/fixture-route.mjs.map +1 -0
- package/dist/fortnox/api.d.mts +92 -0
- package/dist/fortnox/api.d.mts.map +1 -0
- package/dist/fortnox/api.mjs +750 -0
- package/dist/fortnox/api.mjs.map +1 -0
- package/dist/fortnox/state.d.mts +533 -0
- package/dist/fortnox/state.d.mts.map +1 -0
- package/dist/fortnox/state.mjs +612 -0
- package/dist/fortnox/state.mjs.map +1 -0
- package/dist/fortnox.d.mts +128 -0
- package/dist/fortnox.d.mts.map +1 -0
- package/dist/fortnox.mjs +403 -0
- package/dist/fortnox.mjs.map +1 -0
- package/dist/gateway-evaluate-recordings.d.mts +12 -0
- package/dist/gateway-evaluate-recordings.d.mts.map +1 -0
- package/dist/gateway-evaluate-recordings.mjs +128 -0
- package/dist/gateway-evaluate-recordings.mjs.map +1 -0
- package/dist/gateway.d.mts +272 -0
- package/dist/gateway.d.mts.map +1 -0
- package/dist/gateway.mjs +400 -0
- package/dist/gateway.mjs.map +1 -0
- package/dist/github/api.d.mts +63 -0
- package/dist/github/api.d.mts.map +1 -0
- package/dist/github/api.mjs +527 -0
- package/dist/github/api.mjs.map +1 -0
- package/dist/github/state.d.mts +272 -0
- package/dist/github/state.d.mts.map +1 -0
- package/dist/github/state.mjs +303 -0
- package/dist/github/state.mjs.map +1 -0
- package/dist/github.d.mts +77 -0
- package/dist/github.d.mts.map +1 -0
- package/dist/github.mjs +180 -0
- package/dist/github.mjs.map +1 -0
- package/dist/google/calendar.d.mts +10 -0
- package/dist/google/calendar.d.mts.map +1 -0
- package/dist/google/calendar.mjs +252 -0
- package/dist/google/calendar.mjs.map +1 -0
- package/dist/google/drive.d.mts +8 -0
- package/dist/google/drive.d.mts.map +1 -0
- package/dist/google/drive.mjs +252 -0
- package/dist/google/drive.mjs.map +1 -0
- package/dist/google/gmail.d.mts +10 -0
- package/dist/google/gmail.d.mts.map +1 -0
- package/dist/google/gmail.mjs +591 -0
- package/dist/google/gmail.mjs.map +1 -0
- package/dist/google/shared.d.mts +107 -0
- package/dist/google/shared.d.mts.map +1 -0
- package/dist/google/shared.mjs +146 -0
- package/dist/google/shared.mjs.map +1 -0
- package/dist/google/state.d.mts +385 -0
- package/dist/google/state.d.mts.map +1 -0
- package/dist/google/state.mjs +496 -0
- package/dist/google/state.mjs.map +1 -0
- package/dist/google.d.mts +75 -0
- package/dist/google.d.mts.map +1 -0
- package/dist/google.mjs +210 -0
- package/dist/google.mjs.map +1 -0
- package/dist/linkedin-search/api.d.mts +45 -0
- package/dist/linkedin-search/api.d.mts.map +1 -0
- package/dist/linkedin-search/api.mjs +181 -0
- package/dist/linkedin-search/api.mjs.map +1 -0
- package/dist/linkedin-search/state.d.mts +123 -0
- package/dist/linkedin-search/state.d.mts.map +1 -0
- package/dist/linkedin-search/state.mjs +261 -0
- package/dist/linkedin-search/state.mjs.map +1 -0
- package/dist/linkedin-search.d.mts +69 -0
- package/dist/linkedin-search.d.mts.map +1 -0
- package/dist/linkedin-search.mjs +158 -0
- package/dist/linkedin-search.mjs.map +1 -0
- package/dist/mcp/api.d.mts +44 -0
- package/dist/mcp/api.d.mts.map +1 -0
- package/dist/mcp/api.mjs +557 -0
- package/dist/mcp/api.mjs.map +1 -0
- package/dist/mcp/recordings.d.mts +40 -0
- package/dist/mcp/recordings.d.mts.map +1 -0
- package/dist/mcp/recordings.mjs +2390 -0
- package/dist/mcp/recordings.mjs.map +1 -0
- package/dist/mcp/state.d.mts +71 -0
- package/dist/mcp/state.d.mts.map +1 -0
- package/dist/mcp/state.mjs +83 -0
- package/dist/mcp/state.mjs.map +1 -0
- package/dist/mcp.d.mts +84 -0
- package/dist/mcp.d.mts.map +1 -0
- package/dist/mcp.mjs +241 -0
- package/dist/mcp.mjs.map +1 -0
- package/dist/messages.d.mts +183 -0
- package/dist/messages.d.mts.map +1 -0
- package/dist/messages.mjs +532 -0
- package/dist/messages.mjs.map +1 -0
- package/dist/microsoft/api.d.mts +47 -0
- package/dist/microsoft/api.d.mts.map +1 -0
- package/dist/microsoft/api.mjs +178 -0
- package/dist/microsoft/api.mjs.map +1 -0
- package/dist/microsoft/calendar.d.mts +30 -0
- package/dist/microsoft/calendar.d.mts.map +1 -0
- package/dist/microsoft/calendar.mjs +271 -0
- package/dist/microsoft/calendar.mjs.map +1 -0
- package/dist/microsoft/drive.d.mts +36 -0
- package/dist/microsoft/drive.d.mts.map +1 -0
- package/dist/microsoft/drive.mjs +298 -0
- package/dist/microsoft/drive.mjs.map +1 -0
- package/dist/microsoft/graph.d.mts +198 -0
- package/dist/microsoft/graph.d.mts.map +1 -0
- package/dist/microsoft/graph.mjs +264 -0
- package/dist/microsoft/graph.mjs.map +1 -0
- package/dist/microsoft/mail.d.mts +48 -0
- package/dist/microsoft/mail.d.mts.map +1 -0
- package/dist/microsoft/mail.mjs +488 -0
- package/dist/microsoft/mail.mjs.map +1 -0
- package/dist/microsoft/state.d.mts +480 -0
- package/dist/microsoft/state.d.mts.map +1 -0
- package/dist/microsoft/state.mjs +625 -0
- package/dist/microsoft/state.mjs.map +1 -0
- package/dist/microsoft.d.mts +168 -0
- package/dist/microsoft.d.mts.map +1 -0
- package/dist/microsoft.mjs +483 -0
- package/dist/microsoft.mjs.map +1 -0
- package/dist/node.d.mts +47 -0
- package/dist/node.d.mts.map +1 -0
- package/dist/node.mjs +178 -0
- package/dist/node.mjs.map +1 -0
- package/dist/notion/api.d.mts +51 -0
- package/dist/notion/api.d.mts.map +1 -0
- package/dist/notion/api.mjs +507 -0
- package/dist/notion/api.mjs.map +1 -0
- package/dist/notion/state.d.mts +329 -0
- package/dist/notion/state.d.mts.map +1 -0
- package/dist/notion/state.mjs +432 -0
- package/dist/notion/state.mjs.map +1 -0
- package/dist/notion.d.mts +72 -0
- package/dist/notion.d.mts.map +1 -0
- package/dist/notion.mjs +127 -0
- package/dist/notion.mjs.map +1 -0
- package/dist/openai.d.mts +165 -0
- package/dist/openai.d.mts.map +1 -0
- package/dist/openai.mjs +141 -0
- package/dist/openai.mjs.map +1 -0
- package/dist/opencode-recordings.d.mts +7 -0
- package/dist/opencode-recordings.d.mts.map +1 -0
- package/dist/opencode-recordings.mjs +222 -0
- package/dist/opencode-recordings.mjs.map +1 -0
- package/dist/opencode.d.mts +70 -0
- package/dist/opencode.d.mts.map +1 -0
- package/dist/opencode.mjs +217 -0
- package/dist/opencode.mjs.map +1 -0
- package/dist/r2-fixtures.d.mts +36 -0
- package/dist/r2-fixtures.d.mts.map +1 -0
- package/dist/r2-fixtures.mjs +245 -0
- package/dist/r2-fixtures.mjs.map +1 -0
- package/dist/r2-guard.d.mts +43 -0
- package/dist/r2-guard.d.mts.map +1 -0
- package/dist/r2-guard.mjs +215 -0
- package/dist/r2-guard.mjs.map +1 -0
- package/dist/r2.d.mts +166 -0
- package/dist/r2.d.mts.map +1 -0
- package/dist/r2.mjs +517 -0
- package/dist/r2.mjs.map +1 -0
- package/dist/responses.d.mts +231 -0
- package/dist/responses.d.mts.map +1 -0
- package/dist/responses.mjs +556 -0
- package/dist/responses.mjs.map +1 -0
- package/dist/route-evidence.d.mts +45 -0
- package/dist/route-evidence.d.mts.map +1 -0
- package/dist/route-evidence.mjs +56 -0
- package/dist/route-evidence.mjs.map +1 -0
- package/dist/router.d.mts +93 -0
- package/dist/router.d.mts.map +1 -0
- package/dist/router.mjs +259 -0
- package/dist/router.mjs.map +1 -0
- package/dist/stateful-core.d.mts +17 -0
- package/dist/stateful-core.d.mts.map +1 -0
- package/dist/stateful-core.mjs +41 -0
- package/dist/stateful-core.mjs.map +1 -0
- package/dist/stateful-emulator.d.mts +565 -0
- package/dist/stateful-emulator.d.mts.map +1 -0
- package/dist/stateful-emulator.mjs +1228 -0
- package/dist/stateful-emulator.mjs.map +1 -0
- package/dist/stateful-secrets.d.mts +84 -0
- package/dist/stateful-secrets.d.mts.map +1 -0
- package/dist/stateful-secrets.mjs +216 -0
- package/dist/stateful-secrets.mjs.map +1 -0
- package/dist/subscription-usage-recordings.d.mts +9 -0
- package/dist/subscription-usage-recordings.d.mts.map +1 -0
- package/dist/subscription-usage-recordings.mjs +53 -0
- package/dist/subscription-usage-recordings.mjs.map +1 -0
- package/dist/subscription-usage.d.mts +53 -0
- package/dist/subscription-usage.d.mts.map +1 -0
- package/dist/subscription-usage.mjs +21 -0
- package/dist/subscription-usage.mjs.map +1 -0
- package/dist/telegram/api.d.mts +34 -0
- package/dist/telegram/api.d.mts.map +1 -0
- package/dist/telegram/api.mjs +257 -0
- package/dist/telegram/api.mjs.map +1 -0
- package/dist/telegram/state.d.mts +128 -0
- package/dist/telegram/state.d.mts.map +1 -0
- package/dist/telegram/state.mjs +154 -0
- package/dist/telegram/state.mjs.map +1 -0
- package/dist/telegram.d.mts +62 -0
- package/dist/telegram.d.mts.map +1 -0
- package/dist/telegram.mjs +127 -0
- package/dist/telegram.mjs.map +1 -0
- package/dist/todoist/api.d.mts +55 -0
- package/dist/todoist/api.d.mts.map +1 -0
- package/dist/todoist/api.mjs +415 -0
- package/dist/todoist/api.mjs.map +1 -0
- package/dist/todoist/state.d.mts +275 -0
- package/dist/todoist/state.d.mts.map +1 -0
- package/dist/todoist/state.mjs +345 -0
- package/dist/todoist/state.mjs.map +1 -0
- package/dist/todoist.d.mts +68 -0
- package/dist/todoist.d.mts.map +1 -0
- package/dist/todoist.mjs +181 -0
- package/dist/todoist.mjs.map +1 -0
- package/dist/xai.d.mts +267 -0
- package/dist/xai.d.mts.map +1 -0
- package/dist/xai.mjs +252 -0
- package/dist/xai.mjs.map +1 -0
- package/package.json +157 -0
- package/src/anthropic.ts +289 -0
- package/src/chat-completions.ts +924 -0
- package/src/codex.ts +296 -0
- package/src/dropbox/api.ts +1084 -0
- package/src/dropbox/state.ts +364 -0
- package/src/dropbox.ts +203 -0
- package/src/email-fixtures.ts +1297 -0
- package/src/email.ts +1108 -0
- package/src/emulator-compose.ts +147 -0
- package/src/emulator-http.ts +184 -0
- package/src/emulator-kernel.ts +844 -0
- package/src/fixture-route.ts +561 -0
- package/src/fortnox/api.ts +1352 -0
- package/src/fortnox/state.ts +798 -0
- package/src/fortnox.ts +801 -0
- package/src/gateway-evaluate-recordings.ts +153 -0
- package/src/gateway.ts +530 -0
- package/src/github/api.ts +986 -0
- package/src/github/state.ts +439 -0
- package/src/github.ts +271 -0
- package/src/google/calendar.ts +486 -0
- package/src/google/drive.ts +471 -0
- package/src/google/gmail.ts +1011 -0
- package/src/google/shared.ts +321 -0
- package/src/google/state.ts +686 -0
- package/src/google.ts +298 -0
- package/src/linkedin-search/api.ts +363 -0
- package/src/linkedin-search/state.ts +373 -0
- package/src/linkedin-search.ts +241 -0
- package/src/mcp/api.ts +995 -0
- package/src/mcp/recordings.ts +2665 -0
- package/src/mcp/state.ts +125 -0
- package/src/mcp.ts +319 -0
- package/src/messages.ts +844 -0
- package/src/microsoft/api.ts +426 -0
- package/src/microsoft/calendar.ts +491 -0
- package/src/microsoft/drive.ts +541 -0
- package/src/microsoft/graph.ts +550 -0
- package/src/microsoft/mail.ts +833 -0
- package/src/microsoft/state.ts +822 -0
- package/src/microsoft.ts +982 -0
- package/src/node.ts +293 -0
- package/src/notion/api.ts +976 -0
- package/src/notion/state.ts +512 -0
- package/src/notion.ts +210 -0
- package/src/openai.ts +198 -0
- package/src/opencode-recordings.ts +257 -0
- package/src/opencode.ts +271 -0
- package/src/r2-fixtures.ts +295 -0
- package/src/r2-guard.ts +323 -0
- package/src/r2.ts +890 -0
- package/src/responses.ts +901 -0
- package/src/route-evidence.ts +90 -0
- package/src/router.ts +490 -0
- package/src/stateful-core.ts +70 -0
- package/src/stateful-emulator.ts +2571 -0
- package/src/stateful-secrets.ts +299 -0
- package/src/subscription-usage-recordings.ts +77 -0
- package/src/subscription-usage.ts +68 -0
- package/src/telegram/api.ts +437 -0
- package/src/telegram/state.ts +229 -0
- package/src/telegram.ts +227 -0
- package/src/todoist/api.ts +736 -0
- package/src/todoist/state.ts +456 -0
- package/src/todoist.ts +316 -0
- 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
|
+
}
|