@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,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
|
+
]
|