@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,686 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google emulator state: the Gmail messages, labels, attachments, and drafts, the Calendar
|
|
3
|
+
* calendars and events, and the Drive files, the seed input, the default seed, and the profiles
|
|
4
|
+
* (internal; re-exported by `src/google.ts`).
|
|
5
|
+
*
|
|
6
|
+
* Entity shapes and the default entities follow the synthetic Google conformance fixtures and
|
|
7
|
+
* `googleConformanceFixtureSeeds`, copied as data (the same ids, labels, payloads, timestamps, and
|
|
8
|
+
* links), never imported from SDK code. Entities a fixture only names are implied: the paging
|
|
9
|
+
* label (`impliedLabelIds`), the five messages its listing names by id (`impliedMessages`, which
|
|
10
|
+
* only ever render as `{ id, threadId }` list entries), and the practice Drive folder
|
|
11
|
+
* (`impliedFolderIds`, a parent whose own metadata no fixture shows). References resolve through
|
|
12
|
+
* them, but no answer renders them.
|
|
13
|
+
*
|
|
14
|
+
* A stored Gmail message keeps only the renderings a fixture records for it: `minimal` (no
|
|
15
|
+
* payload), and the `metadata` and `full` payloads. A `format` with no recorded rendering is not
|
|
16
|
+
* emulated. Created labels and drafts are never seeded (they exist only once created here).
|
|
17
|
+
*
|
|
18
|
+
* Minted ids never collide with seeded ones: draft and sent message ids (`18f…d<n>`, `18f…e<n>`),
|
|
19
|
+
* which a created draft also uses as its thread id, event ids (`syntheticconformance<nnnn>`), and
|
|
20
|
+
* folder ids (`synthetic-conformance-folder-<nnnn>`) have reserved forms that no seeded message id,
|
|
21
|
+
* thread id, event id, or Drive id may use. Created label ids (`Label_<n>`, `n` from 1 to
|
|
22
|
+
* 999999999 without leading zeros) start above every seeded label number (at least `Label_9101`,
|
|
23
|
+
* the value the label fixture records); a seeded `Label_<digits>` id must have that form, and a
|
|
24
|
+
* create when no number is left is not emulated.
|
|
25
|
+
*
|
|
26
|
+
* @experimental
|
|
27
|
+
*/
|
|
28
|
+
import { Result } from 'effect'
|
|
29
|
+
import * as Schema from 'effect/Schema'
|
|
30
|
+
import { recordedInternalDate } from './shared.ts'
|
|
31
|
+
|
|
32
|
+
const JsonObject = Schema.Record(Schema.String, Schema.Json)
|
|
33
|
+
|
|
34
|
+
/** A Gmail message, thread, label, or attachment id as the fixtures use them. */
|
|
35
|
+
const GmailId = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{1,100}$/))
|
|
36
|
+
|
|
37
|
+
const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The practice account's own address: exactly one plain `local@domain` (the shape the send case's
|
|
41
|
+
* `GooglePracticeAddress` seed takes). The fixtures use it as the sender, the only recipient, and
|
|
42
|
+
* the event creator. A seed may set another one, but the send answers only while it is the
|
|
43
|
+
* recorded `practice@example.test` (the recorded `sizeEstimate` of the sent message covers it).
|
|
44
|
+
*/
|
|
45
|
+
export const GoogleEmulatorPracticeAddress = Schema.String.check(
|
|
46
|
+
Schema.isMaxLength(254),
|
|
47
|
+
Schema.isPattern(
|
|
48
|
+
/^[A-Za-z0-9_%+-]+(?:\.[A-Za-z0-9_%+-]+)*@[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)+$/
|
|
49
|
+
),
|
|
50
|
+
Schema.makeFilter(value => value.indexOf('@') <= 64)
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A stored Gmail message with the renderings a fixture records for it: `minimal` (true when a
|
|
55
|
+
* fixture records `format=minimal`), and the `metadata` and `full` payloads (null when none is
|
|
56
|
+
* recorded). The head fields are answered as stored.
|
|
57
|
+
*/
|
|
58
|
+
export const GoogleEmulatorGmailMessage = Schema.Struct({
|
|
59
|
+
id: GmailId,
|
|
60
|
+
threadId: GmailId,
|
|
61
|
+
labelIds: Schema.Array(Schema.String),
|
|
62
|
+
snippet: Schema.String,
|
|
63
|
+
sizeEstimate: NonNegativeInt,
|
|
64
|
+
historyId: Schema.String,
|
|
65
|
+
internalDate: Schema.String,
|
|
66
|
+
minimal: Schema.Boolean,
|
|
67
|
+
metadataPayload: Schema.NullOr(JsonObject),
|
|
68
|
+
fullPayload: Schema.NullOr(JsonObject)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
export type GoogleEmulatorGmailMessage = typeof GoogleEmulatorGmailMessage.Type
|
|
72
|
+
|
|
73
|
+
/** A message a fixture only names in a listing (`{ id, threadId }`), with the labels it carries. */
|
|
74
|
+
export const GoogleEmulatorImpliedMessage = Schema.Struct({
|
|
75
|
+
id: GmailId,
|
|
76
|
+
threadId: GmailId,
|
|
77
|
+
labelIds: Schema.Array(Schema.String)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
export type GoogleEmulatorImpliedMessage = typeof GoogleEmulatorImpliedMessage.Type
|
|
81
|
+
|
|
82
|
+
/** A label created here (`POST .../labels`), answered as the label fixture records it. */
|
|
83
|
+
export const GoogleEmulatorGmailLabel = Schema.Struct({
|
|
84
|
+
id: GmailId,
|
|
85
|
+
name: Schema.String,
|
|
86
|
+
messageListVisibility: Schema.String,
|
|
87
|
+
labelListVisibility: Schema.String,
|
|
88
|
+
type: Schema.String
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
export type GoogleEmulatorGmailLabel = typeof GoogleEmulatorGmailLabel.Type
|
|
92
|
+
|
|
93
|
+
/** An attachment Gmail stores apart from its message: `size` bytes as base64url `data`. */
|
|
94
|
+
export const GoogleEmulatorAttachment = Schema.Struct({
|
|
95
|
+
messageId: GmailId,
|
|
96
|
+
attachmentId: GmailId,
|
|
97
|
+
size: NonNegativeInt,
|
|
98
|
+
data: Schema.String
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
export type GoogleEmulatorAttachment = typeof GoogleEmulatorAttachment.Type
|
|
102
|
+
|
|
103
|
+
/** A draft created here and its current message. */
|
|
104
|
+
export const GoogleEmulatorDraft = Schema.Struct({
|
|
105
|
+
id: Schema.String,
|
|
106
|
+
messageId: GmailId
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
export type GoogleEmulatorDraft = typeof GoogleEmulatorDraft.Type
|
|
110
|
+
|
|
111
|
+
/** A calendar the events live in, with the fields a listing page answers. */
|
|
112
|
+
export const GoogleEmulatorCalendar = Schema.Struct({
|
|
113
|
+
id: Schema.String,
|
|
114
|
+
summary: Schema.String,
|
|
115
|
+
timeZone: Schema.String,
|
|
116
|
+
accessRole: Schema.String
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
export type GoogleEmulatorCalendar = typeof GoogleEmulatorCalendar.Type
|
|
120
|
+
|
|
121
|
+
/** An event boundary: exactly one of `dateTime` (with an optional `timeZone`) or `date`. */
|
|
122
|
+
export const GoogleEmulatorEventBoundary = Schema.Union([
|
|
123
|
+
Schema.Struct({ dateTime: Schema.String, timeZone: Schema.optionalKey(Schema.String) }),
|
|
124
|
+
Schema.Struct({ date: Schema.String })
|
|
125
|
+
])
|
|
126
|
+
|
|
127
|
+
export type GoogleEmulatorEventBoundary = typeof GoogleEmulatorEventBoundary.Type
|
|
128
|
+
|
|
129
|
+
/** A stored calendar event with its wire fields (creator and organizer derive from the state). */
|
|
130
|
+
export const GoogleEmulatorCalendarEvent = Schema.Struct({
|
|
131
|
+
calendarId: Schema.String,
|
|
132
|
+
id: Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{1,1024}$/)),
|
|
133
|
+
etag: Schema.String,
|
|
134
|
+
status: Schema.Literals(['confirmed', 'cancelled']),
|
|
135
|
+
htmlLink: Schema.String,
|
|
136
|
+
created: Schema.String,
|
|
137
|
+
updated: Schema.String,
|
|
138
|
+
summary: Schema.String,
|
|
139
|
+
description: Schema.optionalKey(Schema.String),
|
|
140
|
+
start: GoogleEmulatorEventBoundary,
|
|
141
|
+
end: GoogleEmulatorEventBoundary,
|
|
142
|
+
iCalUID: Schema.String,
|
|
143
|
+
sequence: NonNegativeInt
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
export type GoogleEmulatorCalendarEvent = typeof GoogleEmulatorCalendarEvent.Type
|
|
147
|
+
|
|
148
|
+
/** A Drive file or folder with the fields the connector's `fields` selection answers. */
|
|
149
|
+
export const GoogleEmulatorDriveFile = Schema.Struct({
|
|
150
|
+
id: Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{10,200}$/)),
|
|
151
|
+
name: Schema.String,
|
|
152
|
+
mimeType: Schema.String,
|
|
153
|
+
starred: Schema.Boolean,
|
|
154
|
+
trashed: Schema.Boolean,
|
|
155
|
+
explicitlyTrashed: Schema.Boolean,
|
|
156
|
+
trashedTime: Schema.optionalKey(Schema.String),
|
|
157
|
+
parents: Schema.Array(Schema.String),
|
|
158
|
+
spaces: Schema.Array(Schema.String),
|
|
159
|
+
version: Schema.String,
|
|
160
|
+
webViewLink: Schema.String,
|
|
161
|
+
iconLink: Schema.String,
|
|
162
|
+
hasThumbnail: Schema.Boolean,
|
|
163
|
+
viewedByMe: Schema.Boolean,
|
|
164
|
+
createdTime: Schema.String,
|
|
165
|
+
modifiedTime: Schema.String,
|
|
166
|
+
ownedByMe: Schema.Boolean,
|
|
167
|
+
shared: Schema.Boolean,
|
|
168
|
+
writersCanShare: Schema.Boolean,
|
|
169
|
+
capabilities: JsonObject
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
export type GoogleEmulatorDriveFile = typeof GoogleEmulatorDriveFile.Type
|
|
173
|
+
|
|
174
|
+
const Counter = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))
|
|
175
|
+
|
|
176
|
+
const Counters = Schema.Struct({
|
|
177
|
+
/** Next created label number (`Label_<n>`). */
|
|
178
|
+
nextLabelNumber: Counter,
|
|
179
|
+
/** Next created draft number (`r-8<n, 18 digits>`). */
|
|
180
|
+
nextDraftNumber: Counter,
|
|
181
|
+
/** Next draft message number (`18f…d<n>`). */
|
|
182
|
+
nextDraftMessageNumber: Counter,
|
|
183
|
+
/** Next sent message number (`18f…e<n>`, and the `Message-ID` header). */
|
|
184
|
+
nextSentNumber: Counter,
|
|
185
|
+
/** Next created event number (`syntheticconformance<nnnn>`). */
|
|
186
|
+
nextEventNumber: Counter,
|
|
187
|
+
/** Next created folder number (`synthetic-conformance-folder-<nnnn>`). */
|
|
188
|
+
nextFolderNumber: Counter
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
/** The whole emulator state (JSON-compatible; what `snapshot()` returns). */
|
|
192
|
+
export const GoogleEmulatorStateSchema = Schema.Struct({
|
|
193
|
+
practiceAddress: GoogleEmulatorPracticeAddress,
|
|
194
|
+
messages: Schema.Array(GoogleEmulatorGmailMessage),
|
|
195
|
+
impliedMessages: Schema.Array(GoogleEmulatorImpliedMessage),
|
|
196
|
+
impliedLabelIds: Schema.Array(GmailId),
|
|
197
|
+
labels: Schema.Array(GoogleEmulatorGmailLabel),
|
|
198
|
+
attachments: Schema.Array(GoogleEmulatorAttachment),
|
|
199
|
+
drafts: Schema.Array(GoogleEmulatorDraft),
|
|
200
|
+
calendars: Schema.Array(GoogleEmulatorCalendar),
|
|
201
|
+
events: Schema.Array(GoogleEmulatorCalendarEvent),
|
|
202
|
+
files: Schema.Array(GoogleEmulatorDriveFile),
|
|
203
|
+
impliedFolderIds: Schema.Array(Schema.String),
|
|
204
|
+
counters: Counters
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The emulator state. The container is mutable (routes replace whole lists); every entity is
|
|
209
|
+
* replaced, never edited in place.
|
|
210
|
+
*/
|
|
211
|
+
export type GoogleEmulatorState = {
|
|
212
|
+
practiceAddress: string
|
|
213
|
+
messages: ReadonlyArray<GoogleEmulatorGmailMessage>
|
|
214
|
+
impliedMessages: ReadonlyArray<GoogleEmulatorImpliedMessage>
|
|
215
|
+
impliedLabelIds: ReadonlyArray<string>
|
|
216
|
+
labels: ReadonlyArray<GoogleEmulatorGmailLabel>
|
|
217
|
+
attachments: ReadonlyArray<GoogleEmulatorAttachment>
|
|
218
|
+
drafts: ReadonlyArray<GoogleEmulatorDraft>
|
|
219
|
+
calendars: ReadonlyArray<GoogleEmulatorCalendar>
|
|
220
|
+
events: ReadonlyArray<GoogleEmulatorCalendarEvent>
|
|
221
|
+
files: ReadonlyArray<GoogleEmulatorDriveFile>
|
|
222
|
+
impliedFolderIds: ReadonlyArray<string>
|
|
223
|
+
counters: typeof Counters.Type
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Account-variance profiles for the default seed. */
|
|
227
|
+
export const GoogleEmulatorProfile = Schema.Literals(['default', 'empty'])
|
|
228
|
+
|
|
229
|
+
export type GoogleEmulatorProfile = typeof GoogleEmulatorProfile.Type
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* A typed seed. Start from `profile` (default `'default'`, the fixture entities); every other key,
|
|
233
|
+
* when given, replaces that part of the profile. Created labels and drafts are never seeded.
|
|
234
|
+
*/
|
|
235
|
+
export const GoogleEmulatorSeed = Schema.Struct({
|
|
236
|
+
profile: Schema.optionalKey(GoogleEmulatorProfile),
|
|
237
|
+
practiceAddress: Schema.optionalKey(GoogleEmulatorPracticeAddress),
|
|
238
|
+
messages: Schema.optionalKey(Schema.Array(GoogleEmulatorGmailMessage)),
|
|
239
|
+
impliedMessages: Schema.optionalKey(Schema.Array(GoogleEmulatorImpliedMessage)),
|
|
240
|
+
impliedLabelIds: Schema.optionalKey(Schema.Array(GmailId)),
|
|
241
|
+
attachments: Schema.optionalKey(Schema.Array(GoogleEmulatorAttachment)),
|
|
242
|
+
calendars: Schema.optionalKey(Schema.Array(GoogleEmulatorCalendar)),
|
|
243
|
+
events: Schema.optionalKey(Schema.Array(GoogleEmulatorCalendarEvent)),
|
|
244
|
+
files: Schema.optionalKey(Schema.Array(GoogleEmulatorDriveFile)),
|
|
245
|
+
impliedFolderIds: Schema.optionalKey(Schema.Array(Schema.String))
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
export type GoogleEmulatorSeed = typeof GoogleEmulatorSeed.Type
|
|
249
|
+
|
|
250
|
+
const strict = { onExcessProperty: 'error' } as const
|
|
251
|
+
|
|
252
|
+
const decodeSeedInput = Schema.decodeUnknownResult(GoogleEmulatorSeed, strict)
|
|
253
|
+
|
|
254
|
+
const decodeStateInput = Schema.decodeUnknownResult(GoogleEmulatorStateSchema, strict)
|
|
255
|
+
|
|
256
|
+
const issueMessage = (issue: Schema.SchemaError['issue']): string =>
|
|
257
|
+
new Schema.SchemaError(issue).message
|
|
258
|
+
|
|
259
|
+
// Minted forms (reserved: a seed may not use them).
|
|
260
|
+
|
|
261
|
+
export const pad = (value: number, width: number): string => String(value).padStart(width, '0')
|
|
262
|
+
|
|
263
|
+
/** A minted message id: `18f`, then `d<n>` (draft) or `e<n>` (sent), zero-padded to 16. */
|
|
264
|
+
export const mintedMessageId = (kind: 'd' | 'e', number: number): string =>
|
|
265
|
+
`18f${`${kind}${number}`.padStart(13, '0')}`
|
|
266
|
+
|
|
267
|
+
export const mintedMessageIdPattern = /^18f0*[de]\d+$/
|
|
268
|
+
|
|
269
|
+
/** A minted draft id: `r-8` and 18 digits (`r-8000000000000000001` first). */
|
|
270
|
+
export const mintedDraftId = (number: number): string => `r-8${pad(number, 18)}`
|
|
271
|
+
|
|
272
|
+
export const mintedEventId = (number: number): string => `syntheticconformance${pad(number, 4)}`
|
|
273
|
+
|
|
274
|
+
export const mintedEventIdPattern = /^syntheticconformance\d+$/
|
|
275
|
+
|
|
276
|
+
export const mintedFolderId = (number: number): string =>
|
|
277
|
+
`synthetic-conformance-folder-${pad(number, 4)}`
|
|
278
|
+
|
|
279
|
+
export const mintedFolderIdPattern = /^synthetic-conformance-folder-\d+$/
|
|
280
|
+
|
|
281
|
+
/** The label number the label fixture records for the first created label. */
|
|
282
|
+
const firstLabelNumber = 9101
|
|
283
|
+
|
|
284
|
+
/** The highest label number a created (minted) label id may carry: `Label_999999999`. */
|
|
285
|
+
export const lastLabelNumber = 999_999_999
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* The minted label id form, `Label_<1 to 999999999>` without leading zeros. A seeded label id of
|
|
289
|
+
* the `Label_<digits>` shape must have this form, so the created-label counter, which starts above
|
|
290
|
+
* every seeded label number, never mints an id a seed holds.
|
|
291
|
+
*/
|
|
292
|
+
export const mintedLabelIdPattern = /^Label_([1-9]\d{0,8})$/
|
|
293
|
+
|
|
294
|
+
const labelDigitsPattern = /^Label_\d+$/
|
|
295
|
+
|
|
296
|
+
/** A Gmail system label (upper case), as opposed to a user label id such as `Label_9001`. */
|
|
297
|
+
export const isSystemLabel = (id: string): boolean => /^[A-Z][A-Z_]*$/.test(id)
|
|
298
|
+
|
|
299
|
+
// Default entities, copied from the fixtures and `googleConformanceFixtureSeeds`.
|
|
300
|
+
|
|
301
|
+
/** The practice account (`practice@example.test`) of every fixture. */
|
|
302
|
+
export const googleEmulatorDefaultPracticeAddress = 'practice@example.test'
|
|
303
|
+
|
|
304
|
+
/** Base64url without padding of UTF-8 text, as the fixtures encode message parts. */
|
|
305
|
+
export const base64UrlOfText = (text: string): string => {
|
|
306
|
+
let binary = ''
|
|
307
|
+
|
|
308
|
+
for (const byte of new TextEncoder().encode(text)) {
|
|
309
|
+
binary += String.fromCharCode(byte)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '')
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** The seeded work message (`format=minimal`, as the label and trash fixtures read it). */
|
|
316
|
+
const workMessage: GoogleEmulatorGmailMessage = {
|
|
317
|
+
id: '18f00000000000b1',
|
|
318
|
+
threadId: '18f00000000000b1',
|
|
319
|
+
labelIds: ['INBOX', 'IMPORTANT'],
|
|
320
|
+
snippet: 'Synthetic practice message.',
|
|
321
|
+
sizeEstimate: 2048,
|
|
322
|
+
historyId: '900001',
|
|
323
|
+
internalDate: recordedInternalDate,
|
|
324
|
+
minimal: true,
|
|
325
|
+
metadataPayload: null,
|
|
326
|
+
fullPayload: null
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const attachmentText = 'Synthetic practice message with an attachment.'
|
|
330
|
+
|
|
331
|
+
const attachmentId = 'ANGjdJ_synthetic_attachment_0001'
|
|
332
|
+
|
|
333
|
+
/** The seeded attachment message (`format=full`, as the attachment fixture reads it). */
|
|
334
|
+
const attachmentMessage: GoogleEmulatorGmailMessage = {
|
|
335
|
+
id: '18f00000000000a1',
|
|
336
|
+
threadId: '18f00000000000a1',
|
|
337
|
+
labelIds: ['INBOX'],
|
|
338
|
+
snippet: attachmentText,
|
|
339
|
+
sizeEstimate: 4096,
|
|
340
|
+
historyId: '900010',
|
|
341
|
+
internalDate: recordedInternalDate,
|
|
342
|
+
minimal: false,
|
|
343
|
+
metadataPayload: null,
|
|
344
|
+
fullPayload: {
|
|
345
|
+
partId: '',
|
|
346
|
+
mimeType: 'multipart/mixed',
|
|
347
|
+
filename: '',
|
|
348
|
+
headers: [
|
|
349
|
+
{ name: 'Subject', value: 'Synthetic practice attachment' },
|
|
350
|
+
{ name: 'Content-Type', value: 'multipart/mixed; boundary="synthetic"' }
|
|
351
|
+
],
|
|
352
|
+
body: { size: 0 },
|
|
353
|
+
parts: [
|
|
354
|
+
{
|
|
355
|
+
partId: '0',
|
|
356
|
+
mimeType: 'text/plain',
|
|
357
|
+
filename: '',
|
|
358
|
+
headers: [{ name: 'Content-Type', value: 'text/plain; charset="UTF-8"' }],
|
|
359
|
+
body: { size: attachmentText.length, data: base64UrlOfText(attachmentText) }
|
|
360
|
+
},
|
|
361
|
+
{
|
|
362
|
+
partId: '1',
|
|
363
|
+
mimeType: 'application/octet-stream',
|
|
364
|
+
filename: 'synthetic.bin',
|
|
365
|
+
headers: [
|
|
366
|
+
{ name: 'Content-Type', value: 'application/octet-stream; name="synthetic.bin"' },
|
|
367
|
+
{ name: 'Content-Disposition', value: 'attachment; filename="synthetic.bin"' },
|
|
368
|
+
{ name: 'Content-Transfer-Encoding', value: 'base64' }
|
|
369
|
+
],
|
|
370
|
+
body: { attachmentId, size: 6 }
|
|
371
|
+
}
|
|
372
|
+
]
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const pagingLabelId = 'Label_9001'
|
|
377
|
+
|
|
378
|
+
/** The five messages the paging fixture names (listing entries only). */
|
|
379
|
+
const pagingMessages: ReadonlyArray<GoogleEmulatorImpliedMessage> = [1, 2, 3, 4, 5].map(index => ({
|
|
380
|
+
id: `18f00000000000c${index}`,
|
|
381
|
+
threadId: `18f00000000000c${index}`,
|
|
382
|
+
labelIds: [pagingLabelId]
|
|
383
|
+
}))
|
|
384
|
+
|
|
385
|
+
const calendarId = 'practice-calendar@example.test'
|
|
386
|
+
|
|
387
|
+
const practiceCalendar: GoogleEmulatorCalendar = {
|
|
388
|
+
id: calendarId,
|
|
389
|
+
summary: 'Synthetic practice calendar',
|
|
390
|
+
timeZone: 'UTC',
|
|
391
|
+
accessRole: 'owner'
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const seededAt = '2026-09-30T12:00:00.000Z'
|
|
395
|
+
|
|
396
|
+
/** An event with the fixture's derived fields (`etag`, `htmlLink`, `iCalUID`). */
|
|
397
|
+
export const eventFields = (
|
|
398
|
+
id: string
|
|
399
|
+
): Pick<GoogleEmulatorCalendarEvent, 'etag' | 'htmlLink' | 'iCalUID'> => ({
|
|
400
|
+
etag: `"3${id.length}000000000000"`,
|
|
401
|
+
htmlLink: `https://www.google.com/calendar/event?eid=${id}`,
|
|
402
|
+
iCalUID: `${id}@google.com`
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
const rangeEvent = (
|
|
406
|
+
id: string,
|
|
407
|
+
summary: string,
|
|
408
|
+
start: GoogleEmulatorEventBoundary,
|
|
409
|
+
end: GoogleEmulatorEventBoundary
|
|
410
|
+
): GoogleEmulatorCalendarEvent => ({
|
|
411
|
+
calendarId,
|
|
412
|
+
id,
|
|
413
|
+
...eventFields(id),
|
|
414
|
+
status: 'confirmed',
|
|
415
|
+
created: seededAt,
|
|
416
|
+
updated: seededAt,
|
|
417
|
+
summary,
|
|
418
|
+
start,
|
|
419
|
+
end,
|
|
420
|
+
sequence: 0
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
const utc = (dateTime: string): GoogleEmulatorEventBoundary => ({ dateTime, timeZone: 'UTC' })
|
|
424
|
+
|
|
425
|
+
/** The five events of the range fixture (one all-day, two crossing the range edges). */
|
|
426
|
+
const rangeEvents: ReadonlyArray<GoogleEmulatorCalendarEvent> = [
|
|
427
|
+
rangeEvent(
|
|
428
|
+
'syntheticrange0001',
|
|
429
|
+
'Synthetic overnight',
|
|
430
|
+
utc('2026-08-31T23:30:00Z'),
|
|
431
|
+
utc('2026-09-01T00:30:00Z')
|
|
432
|
+
),
|
|
433
|
+
rangeEvent(
|
|
434
|
+
'syntheticrange0002',
|
|
435
|
+
'Synthetic standup',
|
|
436
|
+
utc('2026-09-02T09:00:00Z'),
|
|
437
|
+
utc('2026-09-02T09:15:00Z')
|
|
438
|
+
),
|
|
439
|
+
rangeEvent(
|
|
440
|
+
'syntheticrange0003',
|
|
441
|
+
'Synthetic review',
|
|
442
|
+
utc('2026-09-03T14:00:00Z'),
|
|
443
|
+
utc('2026-09-03T15:00:00Z')
|
|
444
|
+
),
|
|
445
|
+
rangeEvent(
|
|
446
|
+
'syntheticrange0004',
|
|
447
|
+
'Synthetic all-day',
|
|
448
|
+
{ date: '2026-09-05' },
|
|
449
|
+
{ date: '2026-09-06' }
|
|
450
|
+
),
|
|
451
|
+
rangeEvent(
|
|
452
|
+
'syntheticrange0005',
|
|
453
|
+
'Synthetic late',
|
|
454
|
+
utc('2026-09-07T23:00:00Z'),
|
|
455
|
+
utc('2026-09-08T01:00:00Z')
|
|
456
|
+
)
|
|
457
|
+
]
|
|
458
|
+
|
|
459
|
+
/** The folder mime type Drive answers. */
|
|
460
|
+
export const driveFolderMimeType = 'application/vnd.google-apps.folder'
|
|
461
|
+
|
|
462
|
+
const practiceFolderId = 'synthetic-practice-folder-0001'
|
|
463
|
+
|
|
464
|
+
/** A Drive item with the fixture's derived and constant fields. */
|
|
465
|
+
export const driveFile = (fields: {
|
|
466
|
+
readonly id: string
|
|
467
|
+
readonly name: string
|
|
468
|
+
readonly mimeType: string
|
|
469
|
+
readonly parents: ReadonlyArray<string>
|
|
470
|
+
readonly createdTime: string
|
|
471
|
+
}): GoogleEmulatorDriveFile => ({
|
|
472
|
+
id: fields.id,
|
|
473
|
+
name: fields.name,
|
|
474
|
+
mimeType: fields.mimeType,
|
|
475
|
+
starred: false,
|
|
476
|
+
trashed: false,
|
|
477
|
+
explicitlyTrashed: false,
|
|
478
|
+
parents: [...fields.parents],
|
|
479
|
+
spaces: ['drive'],
|
|
480
|
+
version: '3',
|
|
481
|
+
webViewLink: `https://drive.google.com/drive/folders/${fields.id}`,
|
|
482
|
+
iconLink: `https://drive-thirdparty.googleusercontent.com/16/type/${fields.mimeType}`,
|
|
483
|
+
hasThumbnail: false,
|
|
484
|
+
viewedByMe: true,
|
|
485
|
+
createdTime: fields.createdTime,
|
|
486
|
+
modifiedTime: fields.createdTime,
|
|
487
|
+
ownedByMe: true,
|
|
488
|
+
shared: false,
|
|
489
|
+
writersCanShare: true,
|
|
490
|
+
capabilities: {
|
|
491
|
+
canAddChildren: fields.mimeType === driveFolderMimeType,
|
|
492
|
+
canDelete: true,
|
|
493
|
+
canEdit: true,
|
|
494
|
+
canRename: true,
|
|
495
|
+
canTrash: true,
|
|
496
|
+
canUntrash: true
|
|
497
|
+
}
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
const child = (id: string, name: string, mimeType: string) =>
|
|
501
|
+
driveFile({ id, name, mimeType, parents: [practiceFolderId], createdTime: seededAt })
|
|
502
|
+
|
|
503
|
+
/** The five children of the practice folder (the first is the seeded practice file). */
|
|
504
|
+
const practiceChildren: ReadonlyArray<GoogleEmulatorDriveFile> = [
|
|
505
|
+
child('synthetic-practice-file-0001', 'Synthetic practice notes.txt', 'text/plain'),
|
|
506
|
+
child('synthetic-practice-file-0002', 'Synthetic practice sheet.csv', 'text/csv'),
|
|
507
|
+
child('synthetic-practice-file-0003', 'Synthetic practice image.png', 'image/png'),
|
|
508
|
+
child('synthetic-practice-folder-0002', 'Synthetic practice subfolder', driveFolderMimeType),
|
|
509
|
+
child('synthetic-practice-file-0004', 'Synthetic practice report.pdf', 'application/pdf')
|
|
510
|
+
]
|
|
511
|
+
|
|
512
|
+
type ProfileEntities = Omit<GoogleEmulatorState, 'counters' | 'labels' | 'drafts'>
|
|
513
|
+
|
|
514
|
+
const profileEntities = (profile: GoogleEmulatorProfile): ProfileEntities =>
|
|
515
|
+
profile === 'default'
|
|
516
|
+
? {
|
|
517
|
+
practiceAddress: googleEmulatorDefaultPracticeAddress,
|
|
518
|
+
messages: [attachmentMessage, workMessage],
|
|
519
|
+
impliedMessages: pagingMessages,
|
|
520
|
+
impliedLabelIds: [pagingLabelId],
|
|
521
|
+
attachments: [{ messageId: attachmentMessage.id, attachmentId, size: 6, data: '-_-_Pj_-' }],
|
|
522
|
+
calendars: [practiceCalendar],
|
|
523
|
+
events: rangeEvents,
|
|
524
|
+
files: practiceChildren,
|
|
525
|
+
impliedFolderIds: [practiceFolderId]
|
|
526
|
+
}
|
|
527
|
+
: {
|
|
528
|
+
practiceAddress: googleEmulatorDefaultPracticeAddress,
|
|
529
|
+
messages: [],
|
|
530
|
+
impliedMessages: [],
|
|
531
|
+
impliedLabelIds: [],
|
|
532
|
+
attachments: [],
|
|
533
|
+
calendars: [],
|
|
534
|
+
events: [],
|
|
535
|
+
files: [],
|
|
536
|
+
impliedFolderIds: []
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const duplicate = (values: ReadonlyArray<string>): string | undefined =>
|
|
540
|
+
values.find((value, index) => values.indexOf(value) !== index)
|
|
541
|
+
|
|
542
|
+
/** Integrity problems a decoded seed can still have (duplicates, minted forms, references). */
|
|
543
|
+
const seedProblem = (entities: ProfileEntities): string | undefined => {
|
|
544
|
+
const messageIds = [
|
|
545
|
+
...entities.messages.map(message => message.id),
|
|
546
|
+
...entities.impliedMessages.map(message => message.id)
|
|
547
|
+
]
|
|
548
|
+
|
|
549
|
+
const fileIds = entities.files.map(file => file.id)
|
|
550
|
+
|
|
551
|
+
const duplicates: ReadonlyArray<readonly [string, string | undefined]> = [
|
|
552
|
+
['message id', duplicate(messageIds)],
|
|
553
|
+
['label id', duplicate(entities.impliedLabelIds)],
|
|
554
|
+
[
|
|
555
|
+
'attachment',
|
|
556
|
+
duplicate(entities.attachments.map(item => `${item.messageId} ${item.attachmentId}`))
|
|
557
|
+
],
|
|
558
|
+
['calendar id', duplicate(entities.calendars.map(calendar => calendar.id))],
|
|
559
|
+
['event', duplicate(entities.events.map(event => `${event.calendarId} ${event.id}`))],
|
|
560
|
+
['Drive id', duplicate([...fileIds, ...entities.impliedFolderIds])]
|
|
561
|
+
]
|
|
562
|
+
|
|
563
|
+
for (const [label, value] of duplicates) {
|
|
564
|
+
if (value !== undefined) return `duplicate ${label} ${value}`
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const minted =
|
|
568
|
+
messageIds.find(id => mintedMessageIdPattern.test(id)) ??
|
|
569
|
+
entities.events.map(event => event.id).find(id => mintedEventIdPattern.test(id)) ??
|
|
570
|
+
[...fileIds, ...entities.impliedFolderIds].find(id => mintedFolderIdPattern.test(id))
|
|
571
|
+
|
|
572
|
+
if (minted !== undefined) return `seed id ${minted} uses a form reserved for minted ids`
|
|
573
|
+
|
|
574
|
+
// A thread id shares the minted message id forms (a created draft is its own thread).
|
|
575
|
+
const mintedThread = [...entities.messages, ...entities.impliedMessages]
|
|
576
|
+
.map(message => message.threadId)
|
|
577
|
+
.find(id => mintedMessageIdPattern.test(id))
|
|
578
|
+
|
|
579
|
+
if (mintedThread !== undefined) {
|
|
580
|
+
return `seed thread id ${mintedThread} uses a form reserved for minted ids`
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const outsideRange = [
|
|
584
|
+
...entities.impliedLabelIds,
|
|
585
|
+
...[...entities.messages, ...entities.impliedMessages].flatMap(message => message.labelIds)
|
|
586
|
+
].find(id => labelDigitsPattern.test(id) && !mintedLabelIdPattern.test(id))
|
|
587
|
+
|
|
588
|
+
if (outsideRange !== undefined) {
|
|
589
|
+
return `seed label id ${outsideRange} is outside the Label_<1 to ${lastLabelNumber}> form`
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const knownLabels = new Set(entities.impliedLabelIds)
|
|
593
|
+
|
|
594
|
+
for (const message of [...entities.messages, ...entities.impliedMessages]) {
|
|
595
|
+
const unknown = message.labelIds.find(id => !isSystemLabel(id) && !knownLabels.has(id))
|
|
596
|
+
|
|
597
|
+
if (unknown !== undefined) {
|
|
598
|
+
return `message ${message.id} carries label ${unknown}, which is not in impliedLabelIds`
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const attachment = entities.attachments.find(
|
|
603
|
+
item => !entities.messages.some(message => message.id === item.messageId)
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
if (attachment !== undefined) {
|
|
607
|
+
return `attachment ${attachment.attachmentId} references missing message ${attachment.messageId}`
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const event = entities.events.find(
|
|
611
|
+
item => !entities.calendars.some(calendar => calendar.id === item.calendarId)
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
if (event !== undefined) {
|
|
615
|
+
return `event ${event.id} references missing calendar ${event.calendarId}`
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const folders = new Set([
|
|
619
|
+
...entities.files.filter(file => file.mimeType === driveFolderMimeType).map(file => file.id),
|
|
620
|
+
...entities.impliedFolderIds
|
|
621
|
+
])
|
|
622
|
+
|
|
623
|
+
const orphan = entities.files.find(file => file.parents.some(parent => !folders.has(parent)))
|
|
624
|
+
|
|
625
|
+
return orphan === undefined
|
|
626
|
+
? undefined
|
|
627
|
+
: `Drive item ${orphan.id} references a missing parent folder`
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/** Build the emulator state for a decoded seed; a string is an integrity problem. */
|
|
631
|
+
const stateFromSeed = (seed: GoogleEmulatorSeed): GoogleEmulatorState | string => {
|
|
632
|
+
const profile = profileEntities(seed.profile ?? 'default')
|
|
633
|
+
|
|
634
|
+
const entities: ProfileEntities = {
|
|
635
|
+
practiceAddress: seed.practiceAddress ?? profile.practiceAddress,
|
|
636
|
+
messages: seed.messages ?? profile.messages,
|
|
637
|
+
impliedMessages: seed.impliedMessages ?? profile.impliedMessages,
|
|
638
|
+
impliedLabelIds: seed.impliedLabelIds ?? profile.impliedLabelIds,
|
|
639
|
+
attachments: seed.attachments ?? profile.attachments,
|
|
640
|
+
calendars: seed.calendars ?? profile.calendars,
|
|
641
|
+
events: seed.events ?? profile.events,
|
|
642
|
+
files: seed.files ?? profile.files,
|
|
643
|
+
impliedFolderIds: seed.impliedFolderIds ?? profile.impliedFolderIds
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const problem = seedProblem(entities)
|
|
647
|
+
|
|
648
|
+
if (problem !== undefined) return problem
|
|
649
|
+
|
|
650
|
+
// Above every seeded label number and never below the recorded one (each at most nine digits,
|
|
651
|
+
// checked in `seedProblem`).
|
|
652
|
+
const highestLabel = Math.max(
|
|
653
|
+
firstLabelNumber - 1,
|
|
654
|
+
...entities.impliedLabelIds.map(id => Number(mintedLabelIdPattern.exec(id)?.[1] ?? 0))
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
return {
|
|
658
|
+
...entities,
|
|
659
|
+
labels: [],
|
|
660
|
+
drafts: [],
|
|
661
|
+
counters: {
|
|
662
|
+
nextLabelNumber: highestLabel + 1,
|
|
663
|
+
nextDraftNumber: 1,
|
|
664
|
+
nextDraftMessageNumber: 1,
|
|
665
|
+
nextSentNumber: 1,
|
|
666
|
+
nextEventNumber: 1,
|
|
667
|
+
nextFolderNumber: 1
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** Decode and build a seed; a string is the reason it is invalid. */
|
|
673
|
+
export const buildSeedState = (input: unknown): GoogleEmulatorState | string => {
|
|
674
|
+
const decoded = decodeSeedInput(input)
|
|
675
|
+
|
|
676
|
+
return Result.isFailure(decoded)
|
|
677
|
+
? issueMessage(decoded.failure.issue)
|
|
678
|
+
: stateFromSeed(decoded.success)
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** Decode a full state (a restored snapshot); a string is the reason it is invalid. */
|
|
682
|
+
export const decodeState = (input: unknown): GoogleEmulatorState | string => {
|
|
683
|
+
const decoded = decodeStateInput(input)
|
|
684
|
+
|
|
685
|
+
return Result.isFailure(decoded) ? issueMessage(decoded.failure.issue) : { ...decoded.success }
|
|
686
|
+
}
|