@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,507 @@
|
|
|
1
|
+
import { exactObject, integerIn, isJsonObject, isNotEmulated, notEmulated, statefulRoute } from "../stateful-emulator.mjs";
|
|
2
|
+
import { plainRichText, titleProperty } from "./state.mjs";
|
|
3
|
+
import { Predicate } from "effect";
|
|
4
|
+
//#region src/notion/api.ts
|
|
5
|
+
/**
|
|
6
|
+
* Notion emulator API: the route table (evidence, request-shape checks, and stateful handlers),
|
|
7
|
+
* the fixture error envelopes, object rendering, and cursor paging (internal; re-exported by
|
|
8
|
+
* `src/notion.ts`).
|
|
9
|
+
*
|
|
10
|
+
* Only the `/v1` routes the eight Notion conformance cases (and their cleanup) send are emulated,
|
|
11
|
+
* at API version 2025-09-03, with the wire shapes of their synthetic fixtures:
|
|
12
|
+
* search, the bot user, page read, create, and archive, block children, paginated property items,
|
|
13
|
+
* and the database / data source split (the database lists its data sources; the data source
|
|
14
|
+
* holds the schema and the rows). List cursors are the next result's id, as the search, block, and
|
|
15
|
+
* query fixtures show; property item cursors are opaque. A cursor is accepted only when this
|
|
16
|
+
* emulator issued it for the same list since the last reset and the list is unchanged. Every
|
|
17
|
+
* route plans first (reading the state, writing nothing) and commits only an eligible request:
|
|
18
|
+
* anything no fixture shows is not emulated (400, nothing written, no fault used up).
|
|
19
|
+
*
|
|
20
|
+
* @experimental
|
|
21
|
+
*/
|
|
22
|
+
/** API version prefix of every Notion route. */
|
|
23
|
+
const notionEmulatorBasePath = "/v1";
|
|
24
|
+
/** The only `Notion-Version` the emulator answers (every fixture sends it). */
|
|
25
|
+
const notionEmulatorVersion = "2025-09-03";
|
|
26
|
+
/** Content type of every Notion response, as the fixtures record it. */
|
|
27
|
+
const notionContentType = "application/json; charset=utf-8";
|
|
28
|
+
/** The only page title a fixture records a create with, and the URL slug it records for it. */
|
|
29
|
+
const recordedCreateTitle = "yolk-conformance page: safe to delete";
|
|
30
|
+
const recordedCreateSlug = "yolk-conformance-page-safe-to-delete";
|
|
31
|
+
const notionEmulatorDrillKnobs = [
|
|
32
|
+
"searchRepeatsResults",
|
|
33
|
+
"botUserAsPerson",
|
|
34
|
+
"envelopeStatusMismatch",
|
|
35
|
+
"omitTitlePlainText",
|
|
36
|
+
"blockCursorRepeats",
|
|
37
|
+
"rejectDoubleEncodedPropertyId",
|
|
38
|
+
"rowParentAsDatabase",
|
|
39
|
+
"trashedPageNotFound"
|
|
40
|
+
];
|
|
41
|
+
/** The origin every Notion fixture records; another origin is not emulated. */
|
|
42
|
+
const notionEmulatorOrigin = "https://api.notion.com";
|
|
43
|
+
const searchCase = "notion.search.cursor-paging";
|
|
44
|
+
const versionCase = "notion.api.pinned-version-accepted";
|
|
45
|
+
const errorCase = "notion.errors.error-envelope";
|
|
46
|
+
const titleCase = "notion.pages.title-plain-text";
|
|
47
|
+
const blocksCase = "notion.blocks.children-cursor-paging";
|
|
48
|
+
const propertyCase = "notion.pages.property-item-paging";
|
|
49
|
+
const dataSourceCase = "notion.data-sources.database-split";
|
|
50
|
+
const archiveCase = "notion.pages.archive-in-trash";
|
|
51
|
+
const evidence = (method, path, write, caseIds) => ({
|
|
52
|
+
method,
|
|
53
|
+
path: `/v1${path}`,
|
|
54
|
+
kind: "connector",
|
|
55
|
+
write,
|
|
56
|
+
caseIds,
|
|
57
|
+
evidence: "unverified",
|
|
58
|
+
origin: notionEmulatorOrigin
|
|
59
|
+
});
|
|
60
|
+
const ok = (body) => new Response(JSON.stringify(body), {
|
|
61
|
+
status: 200,
|
|
62
|
+
headers: { "content-type": notionContentType }
|
|
63
|
+
});
|
|
64
|
+
/** A read-only commit. */
|
|
65
|
+
const answer = (response) => () => response();
|
|
66
|
+
const pad = (value, width) => String(value).padStart(width, "0");
|
|
67
|
+
/** A synthetic request id for ledger sequence `seq`. */
|
|
68
|
+
const requestId = (seq) => `00000000-0000-4000-8000-${pad(seq % 0xe8d4a51000, 12)}`;
|
|
69
|
+
/** The fixture error envelope `{ object: "error", status, code, message, request_id }`. */
|
|
70
|
+
const errorEnvelope = (env, seq, status, code, message) => new Response(JSON.stringify({
|
|
71
|
+
object: "error",
|
|
72
|
+
status: env.drills.envelopeStatusMismatch ? 500 : status,
|
|
73
|
+
code,
|
|
74
|
+
message,
|
|
75
|
+
request_id: requestId(seq)
|
|
76
|
+
}), {
|
|
77
|
+
status,
|
|
78
|
+
headers: { "content-type": notionContentType }
|
|
79
|
+
});
|
|
80
|
+
/** The error-envelope fixture's 404 for a well-formed id that addresses no page. */
|
|
81
|
+
const pageNotFound = (env, seq, id) => errorEnvelope(env, seq, 404, "object_not_found", `Could not find page with ID: ${id}. Make sure the relevant pages and databases are shared with your integration.`);
|
|
82
|
+
/** The error-envelope fixture's 400 for a malformed page id. */
|
|
83
|
+
const malformedPageId = (env, seq, raw) => errorEnvelope(env, seq, 400, "validation_error", `path failed validation: path.page_id should be a valid uuid, instead was \`${JSON.stringify(raw)}\`.`);
|
|
84
|
+
const compactIdPattern = /^[0-9a-f]{32}$/i;
|
|
85
|
+
const dashedIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
86
|
+
/** A Notion id with or without dashes, in any case, as stored (dashed, lower case). */
|
|
87
|
+
const normalizeId = (raw) => {
|
|
88
|
+
if (dashedIdPattern.test(raw)) return raw.toLowerCase();
|
|
89
|
+
if (!compactIdPattern.test(raw)) return void 0;
|
|
90
|
+
const hex = raw.toLowerCase();
|
|
91
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
92
|
+
};
|
|
93
|
+
const idParam = (request, name) => normalizeId(request.params[name] ?? "") ?? notEmulated(`a malformed ${name} is not emulated on this route`);
|
|
94
|
+
const withoutQuery = (request) => request.query.size > 0 ? notEmulated("query parameters are not emulated on this route") : void 0;
|
|
95
|
+
/** `page_size` (required, 1-100) and an optional `start_cursor`, as the list fixtures send them. */
|
|
96
|
+
const listQuery = (request) => {
|
|
97
|
+
const keys = [...request.query.keys()];
|
|
98
|
+
const unknown = keys.find((key) => key !== "page_size" && key !== "start_cursor");
|
|
99
|
+
if (unknown !== void 0) return notEmulated(`query parameter ${unknown} is not emulated`);
|
|
100
|
+
if (keys.length !== new Set(keys).size) return notEmulated("repeated query parameters are not emulated");
|
|
101
|
+
const raw = request.query.get("page_size");
|
|
102
|
+
if (raw === null) return notEmulated("requests without page_size are not emulated");
|
|
103
|
+
const pageSize = integerIn(/^\d+$/.test(raw) ? Number(raw) : null, "page_size", 1, 100);
|
|
104
|
+
if (isNotEmulated(pageSize)) return pageSize;
|
|
105
|
+
const startCursor = request.query.get("start_cursor") ?? void 0;
|
|
106
|
+
return startCursor === "" ? notEmulated("an empty start_cursor is not emulated") : {
|
|
107
|
+
pageSize,
|
|
108
|
+
startCursor
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
const jsonBody = (request, label, required, optional = []) => withoutQuery(request) ?? exactObject(request.json, label, required, optional);
|
|
112
|
+
/** The plain text of a page's `title` property (empty without one). */
|
|
113
|
+
const pageTitle = (page) => {
|
|
114
|
+
const title = Object.values(page.properties).find((property) => isJsonObject(property) && property.type === "title");
|
|
115
|
+
return (isJsonObject(title) && Array.isArray(title.title) ? title.title : []).map((item) => isJsonObject(item) && Predicate.isString(item.plain_text) ? item.plain_text : "").join("");
|
|
116
|
+
};
|
|
117
|
+
/** Title items without `plain_text` (the `omitTitlePlainText` drill). */
|
|
118
|
+
const withoutTitlePlainText = (properties) => Object.fromEntries(Object.entries(properties).map(([name, property]) => [name, isJsonObject(property) && property.type === "title" && Array.isArray(property.title) ? {
|
|
119
|
+
...property,
|
|
120
|
+
title: property.title.map((item) => isJsonObject(item) ? Object.fromEntries(Object.entries(item).filter(([key]) => key !== "plain_text")) : item)
|
|
121
|
+
} : property]));
|
|
122
|
+
const renderPage = (env, page, read) => {
|
|
123
|
+
const parent = page.parent.type === "data_source_id" && env.drills.rowParentAsDatabase ? {
|
|
124
|
+
type: "database_id",
|
|
125
|
+
database_id: page.parent.database_id
|
|
126
|
+
} : page.parent;
|
|
127
|
+
const properties = read === "page" && env.drills.omitTitlePlainText ? withoutTitlePlainText(page.properties) : page.properties;
|
|
128
|
+
const head = {
|
|
129
|
+
object: "page",
|
|
130
|
+
id: page.id
|
|
131
|
+
};
|
|
132
|
+
const tail = {
|
|
133
|
+
parent,
|
|
134
|
+
archived: page.archived,
|
|
135
|
+
in_trash: page.inTrash,
|
|
136
|
+
properties,
|
|
137
|
+
url: page.url
|
|
138
|
+
};
|
|
139
|
+
return page.times === null ? {
|
|
140
|
+
...head,
|
|
141
|
+
...tail
|
|
142
|
+
} : {
|
|
143
|
+
...head,
|
|
144
|
+
created_time: page.times.created,
|
|
145
|
+
last_edited_time: page.times.lastEdited,
|
|
146
|
+
...tail
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
const isTrashed = (page) => page.archived || page.inTrash;
|
|
150
|
+
const isImplied = (state, id) => state.impliedPages.some((page) => page.id === id);
|
|
151
|
+
const impliedRefusal = () => notEmulated("an answer showing a page a fixture only names by id is not emulated");
|
|
152
|
+
const list = (results, nextCursor, type) => ({
|
|
153
|
+
object: "list",
|
|
154
|
+
results: [...results],
|
|
155
|
+
next_cursor: nextCursor,
|
|
156
|
+
has_more: nextCursor !== null,
|
|
157
|
+
type,
|
|
158
|
+
[type]: {}
|
|
159
|
+
});
|
|
160
|
+
/** Where a page of a list starts: 0, or an issued cursor's offset; not emulated otherwise. */
|
|
161
|
+
const startOf = (env, listKey, fingerprint, cursor) => {
|
|
162
|
+
if (cursor === void 0) return 0;
|
|
163
|
+
const issued = env.cursors.get(`${listKey}\u0000${cursor}`);
|
|
164
|
+
if (issued === void 0) return notEmulated("a start_cursor this emulator did not issue for this list since the last reset is not emulated");
|
|
165
|
+
return issued.fingerprint === fingerprint ? issued.offset : notEmulated("continuing a list that changed since its cursor was issued is not emulated");
|
|
166
|
+
};
|
|
167
|
+
/** Register a cursor (in a commit). */
|
|
168
|
+
const issue = (env, listKey, cursor, offset, fingerprint) => {
|
|
169
|
+
env.cursors.set(`${listKey}\u0000${cursor}`, {
|
|
170
|
+
offset,
|
|
171
|
+
fingerprint
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* The cursor value for the next item `id` of a list (in a commit): the id itself, as the fixtures
|
|
176
|
+
* record, in the generation that first issued it for this list; a distinct value in any later
|
|
177
|
+
* generation (after a reset or seed), so no cursor value crosses a reset or seed.
|
|
178
|
+
*/
|
|
179
|
+
const idCursor = (env, listKey, id) => {
|
|
180
|
+
const key = `${listKey}\u0000${id}`;
|
|
181
|
+
const first = env.firstIssued.get(key);
|
|
182
|
+
const generation = env.generation.current;
|
|
183
|
+
if (first === void 0) env.firstIssued.set(key, generation);
|
|
184
|
+
return first === void 0 || first === generation ? id : `${id}.g${generation}`;
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* The commit answering one page of an id-cursor list (search results, blocks): the next cursor is
|
|
188
|
+
* the next item's id, as the fixtures record (see `idCursor`), issued for this list.
|
|
189
|
+
*/
|
|
190
|
+
const idListPage = (env, listKey, items, query, repeats, type) => {
|
|
191
|
+
const fingerprint = JSON.stringify(items.map((item) => item.rendered));
|
|
192
|
+
const offset = startOf(env, listKey, fingerprint, query.startCursor);
|
|
193
|
+
if (isNotEmulated(offset)) return offset;
|
|
194
|
+
const start = repeats && offset > 0 ? offset - 1 : offset;
|
|
195
|
+
const end = start + query.pageSize;
|
|
196
|
+
const next = items[end];
|
|
197
|
+
return () => {
|
|
198
|
+
const cursor = next === void 0 ? null : idCursor(env, listKey, next.id);
|
|
199
|
+
if (cursor !== null) issue(env, listKey, cursor, end, fingerprint);
|
|
200
|
+
return ok(list(items.slice(start, end).map((item) => item.rendered), cursor, type));
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
const search = statefulRoute(evidence("POST", "/search", false, [searchCase]), "json", (request) => {
|
|
204
|
+
const body = jsonBody(request, "the search body", [
|
|
205
|
+
"query",
|
|
206
|
+
"filter",
|
|
207
|
+
"page_size"
|
|
208
|
+
], ["start_cursor"]);
|
|
209
|
+
if (isNotEmulated(body)) return body;
|
|
210
|
+
if (!Predicate.isString(body.query)) return notEmulated("query must be a string");
|
|
211
|
+
const filter = exactObject(body.filter, "filter", ["property", "value"]);
|
|
212
|
+
if (isNotEmulated(filter)) return filter;
|
|
213
|
+
if (filter.property !== "object" || filter.value !== "page") return notEmulated("filters other than { property: \"object\", value: \"page\" } are not emulated");
|
|
214
|
+
const pageSize = integerIn(body.page_size, "page_size", 1, 100);
|
|
215
|
+
if (isNotEmulated(pageSize)) return pageSize;
|
|
216
|
+
if (body.start_cursor !== void 0 && (!Predicate.isString(body.start_cursor) || body.start_cursor.length === 0)) return notEmulated("start_cursor must be a non-empty string");
|
|
217
|
+
return {
|
|
218
|
+
query: body.query,
|
|
219
|
+
list: {
|
|
220
|
+
pageSize,
|
|
221
|
+
startCursor: Predicate.isString(body.start_cursor) ? body.start_cursor : void 0
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}, (state, input, { env }) => {
|
|
225
|
+
const query = input.query.toLowerCase();
|
|
226
|
+
const matches = state.pages.filter((page) => pageTitle(page).toLowerCase().includes(query));
|
|
227
|
+
if (matches.length === 0) return notEmulated("a search without matches is not emulated (no fixture records one)");
|
|
228
|
+
if (matches.some(isTrashed)) return notEmulated("a search matching a trashed page is not emulated (no fixture records one)");
|
|
229
|
+
if (matches.some((page) => page.times === null)) return notEmulated("a search matching a page shown only as a query row is not emulated (no fixture records one)");
|
|
230
|
+
return idListPage(env, `search\u0000${input.query}`, matches.map((page) => ({
|
|
231
|
+
id: page.id,
|
|
232
|
+
rendered: renderPage(env, page, "list")
|
|
233
|
+
})), input.list, env.drills.searchRepeatsResults, "page_or_data_source");
|
|
234
|
+
});
|
|
235
|
+
const botUser = statefulRoute(evidence("GET", "/users/me", false, [versionCase]), "none", (request) => withoutQuery(request) ?? {}, (state, _input, { env, seq }) => answer(() => {
|
|
236
|
+
const user = state.botUser;
|
|
237
|
+
const kind = env.drills.botUserAsPerson ? {
|
|
238
|
+
type: "person",
|
|
239
|
+
person: {}
|
|
240
|
+
} : {
|
|
241
|
+
type: "bot",
|
|
242
|
+
bot: {
|
|
243
|
+
owner: {
|
|
244
|
+
type: "workspace",
|
|
245
|
+
workspace: true
|
|
246
|
+
},
|
|
247
|
+
workspace_name: user.workspaceName
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
return ok({
|
|
251
|
+
object: "user",
|
|
252
|
+
id: user.id,
|
|
253
|
+
name: user.name,
|
|
254
|
+
avatar_url: user.avatarUrl,
|
|
255
|
+
...kind,
|
|
256
|
+
request_id: requestId(seq)
|
|
257
|
+
});
|
|
258
|
+
}));
|
|
259
|
+
const getPage = statefulRoute(evidence("GET", "/pages/{pageId}", false, [
|
|
260
|
+
errorCase,
|
|
261
|
+
titleCase,
|
|
262
|
+
propertyCase,
|
|
263
|
+
archiveCase
|
|
264
|
+
]), "none", (request) => {
|
|
265
|
+
const raw = request.params.pageId ?? "";
|
|
266
|
+
return withoutQuery(request) ?? {
|
|
267
|
+
raw,
|
|
268
|
+
id: normalizeId(raw)
|
|
269
|
+
};
|
|
270
|
+
}, (state, input, { env, seq }) => {
|
|
271
|
+
const id = input.id;
|
|
272
|
+
if (id === void 0) return answer(() => malformedPageId(env, seq, input.raw));
|
|
273
|
+
if (isImplied(state, id)) return impliedRefusal();
|
|
274
|
+
const page = state.pages.find((candidate) => candidate.id === id);
|
|
275
|
+
if (page === void 0 || env.drills.trashedPageNotFound && isTrashed(page)) return answer(() => pageNotFound(env, seq, id));
|
|
276
|
+
if (page.times === null) return notEmulated("a page read of a page the fixtures show only as a query row is not emulated");
|
|
277
|
+
return answer(() => ok(renderPage(env, page, "page")));
|
|
278
|
+
});
|
|
279
|
+
const createPage = statefulRoute(evidence("POST", "/pages", true, [archiveCase]), "json", (request) => {
|
|
280
|
+
const body = jsonBody(request, "the create page body", ["parent", "properties"]);
|
|
281
|
+
if (isNotEmulated(body)) return body;
|
|
282
|
+
const parent = exactObject(body.parent, "parent", ["page_id"]);
|
|
283
|
+
if (isNotEmulated(parent)) return parent;
|
|
284
|
+
const parentId = Predicate.isString(parent.page_id) ? normalizeId(parent.page_id) : void 0;
|
|
285
|
+
if (parentId === void 0) return notEmulated("parent.page_id must be a Notion id");
|
|
286
|
+
const properties = exactObject(body.properties, "properties", ["title"]);
|
|
287
|
+
if (isNotEmulated(properties)) return properties;
|
|
288
|
+
const title = exactObject(properties.title, "properties.title", ["title"]);
|
|
289
|
+
if (isNotEmulated(title)) return title;
|
|
290
|
+
const items = title.title;
|
|
291
|
+
if (!Array.isArray(items) || items.length !== 1) return notEmulated("a title of other than one text item is not emulated");
|
|
292
|
+
const item = exactObject(items[0], "the title item", ["text"]);
|
|
293
|
+
if (isNotEmulated(item)) return item;
|
|
294
|
+
const text = exactObject(item.text, "the title item text", ["content"]);
|
|
295
|
+
if (isNotEmulated(text)) return text;
|
|
296
|
+
return text.content === recordedCreateTitle ? parentId : notEmulated(`a page title other than "${recordedCreateTitle}" is not emulated`);
|
|
297
|
+
}, (state, parentId, { env }) => {
|
|
298
|
+
const parent = state.pages.find((page) => page.id === parentId);
|
|
299
|
+
if (!isImplied(state, parentId) && (parent === void 0 || isTrashed(parent))) return notEmulated("creating a page under a missing or trashed page is not emulated");
|
|
300
|
+
return () => {
|
|
301
|
+
const at = new Date(env.now()).toISOString();
|
|
302
|
+
const number = state.counters.nextPageNumber;
|
|
303
|
+
const id = `1f0000e0-0000-4000-8000-${pad(number, 12)}`;
|
|
304
|
+
const page = {
|
|
305
|
+
id,
|
|
306
|
+
times: {
|
|
307
|
+
created: at,
|
|
308
|
+
lastEdited: at
|
|
309
|
+
},
|
|
310
|
+
parent: {
|
|
311
|
+
type: "page_id",
|
|
312
|
+
page_id: parentId
|
|
313
|
+
},
|
|
314
|
+
archived: false,
|
|
315
|
+
inTrash: false,
|
|
316
|
+
properties: { title: titleProperty([plainRichText(recordedCreateTitle)]) },
|
|
317
|
+
url: `https://www.notion.so/${recordedCreateSlug}-${id.replaceAll("-", "")}`
|
|
318
|
+
};
|
|
319
|
+
state.counters = {
|
|
320
|
+
...state.counters,
|
|
321
|
+
nextPageNumber: number + 1
|
|
322
|
+
};
|
|
323
|
+
state.pages = [...state.pages, page];
|
|
324
|
+
return ok(renderPage(env, page, "page"));
|
|
325
|
+
};
|
|
326
|
+
});
|
|
327
|
+
const archivePage = statefulRoute(evidence("PATCH", "/pages/{pageId}", true, [archiveCase]), "json", (request) => {
|
|
328
|
+
const id = idParam(request, "pageId");
|
|
329
|
+
if (isNotEmulated(id)) return id;
|
|
330
|
+
const body = jsonBody(request, "the update page body", ["archived"]);
|
|
331
|
+
if (isNotEmulated(body)) return body;
|
|
332
|
+
return body.archived === true ? id : notEmulated("page updates other than { archived: true } are not emulated");
|
|
333
|
+
}, (state, id, { env }) => {
|
|
334
|
+
if (isImplied(state, id)) return impliedRefusal();
|
|
335
|
+
const page = state.pages.find((candidate) => candidate.id === id);
|
|
336
|
+
if (page === void 0 || isTrashed(page) || page.times === null) return notEmulated("archiving a missing, already trashed, or row page is not emulated");
|
|
337
|
+
return () => {
|
|
338
|
+
const archived = {
|
|
339
|
+
...page,
|
|
340
|
+
archived: true,
|
|
341
|
+
inTrash: true
|
|
342
|
+
};
|
|
343
|
+
state.pages = state.pages.map((candidate) => candidate.id === id ? archived : candidate);
|
|
344
|
+
return ok(renderPage(env, archived, "page"));
|
|
345
|
+
};
|
|
346
|
+
});
|
|
347
|
+
const blockChildren = statefulRoute(evidence("GET", "/blocks/{blockId}/children", false, [blocksCase]), "none", (request) => {
|
|
348
|
+
const blockId = idParam(request, "blockId");
|
|
349
|
+
if (isNotEmulated(blockId)) return blockId;
|
|
350
|
+
const query = listQuery(request);
|
|
351
|
+
return isNotEmulated(query) ? query : {
|
|
352
|
+
blockId,
|
|
353
|
+
list: query
|
|
354
|
+
};
|
|
355
|
+
}, (state, input, { env }) => {
|
|
356
|
+
if (!isImplied(state, input.blockId) && !state.pages.some((page) => page.id === input.blockId)) return notEmulated("children of anything but a page are not emulated");
|
|
357
|
+
const blocks = state.blocks.filter((block) => block.pageId === input.blockId);
|
|
358
|
+
if (blocks.length === 0) return notEmulated("a page without recorded child blocks is not emulated");
|
|
359
|
+
return idListPage(env, `blocks\u0000${input.blockId}`, blocks.map((block) => ({
|
|
360
|
+
id: block.id,
|
|
361
|
+
rendered: {
|
|
362
|
+
object: "block",
|
|
363
|
+
id: block.id,
|
|
364
|
+
parent: {
|
|
365
|
+
type: "page_id",
|
|
366
|
+
page_id: block.pageId
|
|
367
|
+
},
|
|
368
|
+
created_time: block.createdTime,
|
|
369
|
+
last_edited_time: block.lastEditedTime,
|
|
370
|
+
has_children: block.hasChildren,
|
|
371
|
+
archived: block.archived,
|
|
372
|
+
in_trash: block.inTrash,
|
|
373
|
+
type: block.type,
|
|
374
|
+
[block.type]: block.value
|
|
375
|
+
}
|
|
376
|
+
})), input.list, env.drills.blockCursorRepeats, "block");
|
|
377
|
+
});
|
|
378
|
+
const base64url = (text) => btoa(text).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
379
|
+
const mintPropertyCursor = (env) => {
|
|
380
|
+
const number = env.propertyCursorCounter.next;
|
|
381
|
+
env.propertyCursorCounter.next = number + 1;
|
|
382
|
+
return base64url(number === 1 ? "synthetic-property-cursor" : `synthetic-property-cursor-${number}`);
|
|
383
|
+
};
|
|
384
|
+
const decodeAgain = (value) => {
|
|
385
|
+
try {
|
|
386
|
+
return decodeURIComponent(value);
|
|
387
|
+
} catch {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
/** The route table: evidence plus handlers. `notionEmulatorRoutes` is its evidence part. */
|
|
392
|
+
const notionApiRoutes = [
|
|
393
|
+
search,
|
|
394
|
+
botUser,
|
|
395
|
+
getPage,
|
|
396
|
+
createPage,
|
|
397
|
+
archivePage,
|
|
398
|
+
blockChildren,
|
|
399
|
+
statefulRoute(evidence("GET", "/pages/{pageId}/properties/{propertyId}", false, [propertyCase]), "none", (request, env) => {
|
|
400
|
+
const pageId = idParam(request, "pageId");
|
|
401
|
+
if (isNotEmulated(pageId)) return pageId;
|
|
402
|
+
const once = request.params.propertyId ?? "";
|
|
403
|
+
const propertyId = env.drills.rejectDoubleEncodedPropertyId ? decodeAgain(once) : once;
|
|
404
|
+
if (propertyId === void 0) return notEmulated("a malformed property id is not emulated");
|
|
405
|
+
const query = listQuery(request);
|
|
406
|
+
return isNotEmulated(query) ? query : {
|
|
407
|
+
pageId,
|
|
408
|
+
propertyId,
|
|
409
|
+
list: query
|
|
410
|
+
};
|
|
411
|
+
}, (state, input, { env }) => {
|
|
412
|
+
const items = state.propertyItems.find((candidate) => candidate.pageId === input.pageId && candidate.propertyId === input.propertyId);
|
|
413
|
+
if (items === void 0 || !state.pages.some((page) => page.id === input.pageId)) return notEmulated("items of a property with no seeded item list are not emulated");
|
|
414
|
+
const listKey = `property\u0000${items.pageId}\u0000${items.propertyId}`;
|
|
415
|
+
const fingerprint = JSON.stringify(items.values);
|
|
416
|
+
const offset = startOf(env, listKey, fingerprint, input.list.startCursor);
|
|
417
|
+
if (isNotEmulated(offset)) return offset;
|
|
418
|
+
return () => {
|
|
419
|
+
const end = Math.min(offset + input.list.pageSize, items.values.length);
|
|
420
|
+
const nextCursor = end < items.values.length ? mintPropertyCursor(env) : null;
|
|
421
|
+
if (nextCursor !== null) issue(env, listKey, nextCursor, end, fingerprint);
|
|
422
|
+
return ok({
|
|
423
|
+
object: "list",
|
|
424
|
+
results: items.values.slice(offset, end).map((value) => ({
|
|
425
|
+
object: "property_item",
|
|
426
|
+
id: items.propertyId,
|
|
427
|
+
type: items.type,
|
|
428
|
+
[items.type]: value
|
|
429
|
+
})),
|
|
430
|
+
next_cursor: nextCursor,
|
|
431
|
+
has_more: nextCursor !== null,
|
|
432
|
+
type: "property_item",
|
|
433
|
+
property_item: {
|
|
434
|
+
id: items.propertyId,
|
|
435
|
+
next_url: nextCursor === null ? null : `${notionEmulatorOrigin}/v1/pages/${items.pageId}/properties/${items.propertyId}?start_cursor=${nextCursor}`,
|
|
436
|
+
type: items.type,
|
|
437
|
+
[items.type]: {}
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
};
|
|
441
|
+
}),
|
|
442
|
+
statefulRoute(evidence("GET", "/databases/{databaseId}", false, [dataSourceCase]), "none", (request) => withoutQuery(request) ?? idParam(request, "databaseId"), (state, id) => {
|
|
443
|
+
const database = state.databases.find((candidate) => candidate.id === id);
|
|
444
|
+
if (database === void 0) return notEmulated("a missing database is not emulated");
|
|
445
|
+
return answer(() => ok({
|
|
446
|
+
object: "database",
|
|
447
|
+
id: database.id,
|
|
448
|
+
title: [...database.title],
|
|
449
|
+
description: [...database.description],
|
|
450
|
+
parent: database.parent,
|
|
451
|
+
is_inline: database.isInline,
|
|
452
|
+
in_trash: database.inTrash,
|
|
453
|
+
archived: database.archived,
|
|
454
|
+
created_time: database.createdTime,
|
|
455
|
+
last_edited_time: database.lastEditedTime,
|
|
456
|
+
data_sources: state.dataSources.filter((source) => source.databaseId === database.id).map((source) => ({
|
|
457
|
+
id: source.id,
|
|
458
|
+
name: source.title.map((item) => isJsonObject(item) && Predicate.isString(item.plain_text) ? item.plain_text : "").join("")
|
|
459
|
+
})),
|
|
460
|
+
url: database.url
|
|
461
|
+
}));
|
|
462
|
+
}),
|
|
463
|
+
statefulRoute(evidence("GET", "/data_sources/{dataSourceId}", false, [dataSourceCase]), "none", (request) => withoutQuery(request) ?? idParam(request, "dataSourceId"), (state, id) => {
|
|
464
|
+
const source = state.dataSources.find((candidate) => candidate.id === id);
|
|
465
|
+
const database = state.databases.find((candidate) => candidate.id === source?.databaseId);
|
|
466
|
+
if (source === void 0 || database === void 0) return notEmulated("a missing data source is not emulated");
|
|
467
|
+
return answer(() => ok({
|
|
468
|
+
object: "data_source",
|
|
469
|
+
id: source.id,
|
|
470
|
+
parent: {
|
|
471
|
+
type: "database_id",
|
|
472
|
+
database_id: database.id
|
|
473
|
+
},
|
|
474
|
+
database_parent: database.parent,
|
|
475
|
+
title: [...source.title],
|
|
476
|
+
properties: source.properties,
|
|
477
|
+
archived: source.archived,
|
|
478
|
+
in_trash: source.inTrash
|
|
479
|
+
}));
|
|
480
|
+
}),
|
|
481
|
+
statefulRoute(evidence("POST", "/data_sources/{dataSourceId}/query", false, [dataSourceCase]), "json", (request) => {
|
|
482
|
+
const dataSourceId = idParam(request, "dataSourceId");
|
|
483
|
+
if (isNotEmulated(dataSourceId)) return dataSourceId;
|
|
484
|
+
const body = jsonBody(request, "the query body", ["page_size"]);
|
|
485
|
+
if (isNotEmulated(body)) return body;
|
|
486
|
+
const pageSize = integerIn(body.page_size, "page_size", 1, 100);
|
|
487
|
+
return isNotEmulated(pageSize) ? pageSize : {
|
|
488
|
+
dataSourceId,
|
|
489
|
+
pageSize
|
|
490
|
+
};
|
|
491
|
+
}, (state, input, { env }) => {
|
|
492
|
+
if (!state.dataSources.some((source) => source.id === input.dataSourceId)) return notEmulated("a missing data source is not emulated");
|
|
493
|
+
const inSource = (parent) => parent?.type === "data_source_id" && parent.data_source_id === input.dataSourceId;
|
|
494
|
+
const rows = state.pages.filter((page) => inSource(page.parent));
|
|
495
|
+
if (rows.some(isTrashed)) return notEmulated("a query of a data source with a trashed row is not emulated");
|
|
496
|
+
const implied = state.impliedPages.filter((row) => inSource(row.parent)).map((row) => row.id);
|
|
497
|
+
const next = [...rows.map((row) => row.id), ...implied][input.pageSize];
|
|
498
|
+
if (rows.length < input.pageSize) return notEmulated("a query page showing a row no fixture shows is not emulated");
|
|
499
|
+
const page = rows.slice(0, input.pageSize);
|
|
500
|
+
if (next === void 0) return notEmulated("a last query page (no further row) is not emulated");
|
|
501
|
+
return answer(() => ok(list(page.map((row) => renderPage(env, row, "list")), next, "page_or_data_source")));
|
|
502
|
+
})
|
|
503
|
+
];
|
|
504
|
+
//#endregion
|
|
505
|
+
export { notionApiRoutes, notionEmulatorBasePath, notionEmulatorDrillKnobs, notionEmulatorOrigin, notionEmulatorVersion };
|
|
506
|
+
|
|
507
|
+
//# sourceMappingURL=api.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.mjs","names":[],"sources":["../../src/notion/api.ts"],"sourcesContent":["/**\n * Notion emulator API: the route table (evidence, request-shape checks, and stateful handlers),\n * the fixture error envelopes, object rendering, and cursor paging (internal; re-exported by\n * `src/notion.ts`).\n *\n * Only the `/v1` routes the eight Notion conformance cases (and their cleanup) send are emulated,\n * at API version 2025-09-03, with the wire shapes of their synthetic fixtures:\n * search, the bot user, page read, create, and archive, block children, paginated property items,\n * and the database / data source split (the database lists its data sources; the data source\n * holds the schema and the rows). List cursors are the next result's id, as the search, block, and\n * query fixtures show; property item cursors are opaque. A cursor is accepted only when this\n * emulator issued it for the same list since the last reset and the list is unchanged. Every\n * route plans first (reading the state, writing nothing) and commits only an eligible request:\n * anything no fixture shows is not emulated (400, nothing written, no fault used up).\n *\n * @experimental\n */\nimport { Predicate } from 'effect'\nimport type * as Schema from 'effect/Schema'\nimport {\n exactObject,\n integerIn,\n isJsonObject,\n isNotEmulated,\n notEmulated,\n statefulRoute,\n type Commit,\n type EmulatedRequest,\n type NotEmulated,\n type StatefulRoute\n} from '../stateful-emulator.ts'\nimport {\n plainRichText,\n titleProperty,\n type NotionEmulatorPage,\n type NotionEmulatorState\n} from './state.ts'\n\n/** API version prefix of every Notion route. */\nexport const notionEmulatorBasePath = '/v1'\n\n/** The only `Notion-Version` the emulator answers (every fixture sends it). */\nexport const notionEmulatorVersion = '2025-09-03'\n\n/** Content type of every Notion response, as the fixtures record it. */\nconst notionContentType = 'application/json; charset=utf-8'\n\n/** The only page title a fixture records a create with, and the URL slug it records for it. */\nconst recordedCreateTitle = 'yolk-conformance page: safe to delete'\n\nconst recordedCreateSlug = 'yolk-conformance-page-safe-to-delete'\n\n/** Drill knobs (tests only): each makes the emulator disagree with one conformance claim. */\nexport type NotionEmulatorDrills = {\n /** A later search page starts one result early, repeating the previous page's last result. */\n readonly searchRepeatsResults?: boolean\n /** `GET /v1/users/me` answers a person instead of the bot user. */\n readonly botUserAsPerson?: boolean\n /** Error envelopes carry a `status` other than the HTTP status. */\n readonly envelopeStatusMismatch?: boolean\n /** Page reads answer title rich text without `plain_text`. */\n readonly omitTitlePlainText?: boolean\n /** A later block children page starts one block early, repeating a block. */\n readonly blockCursorRepeats?: boolean\n /** The property id path segment is decoded twice, so a re-encoded id is not found. */\n readonly rejectDoubleEncodedPropertyId?: boolean\n /** Data source rows answer a `database_id` parent instead of their data source. */\n readonly rowParentAsDatabase?: boolean\n /** A trashed page reads back as 404 `object_not_found`. */\n readonly trashedPageNotFound?: boolean\n}\n\nexport const notionEmulatorDrillKnobs: ReadonlyArray<keyof NotionEmulatorDrills> = [\n 'searchRepeatsResults',\n 'botUserAsPerson',\n 'envelopeStatusMismatch',\n 'omitTitlePlainText',\n 'blockCursorRepeats',\n 'rejectDoubleEncodedPropertyId',\n 'rowParentAsDatabase',\n 'trashedPageNotFound'\n]\n\n/** A cursor this emulator issued: where its list continues, and the list it was issued for. */\nexport type NotionIssuedCursor = {\n readonly offset: number\n /** The whole list as rendered when the cursor was issued; any change is not emulated. */\n readonly fingerprint: string\n}\n\nexport type NotionApiEnv = {\n /** Clock in epoch milliseconds (created page timestamps only). */\n readonly now: () => number\n readonly drills: Readonly<Record<keyof NotionEmulatorDrills, boolean>>\n /** Issued cursors by list and value (runtime-only; cleared by reset and seed). */\n readonly cursors: Map<string, NotionIssuedCursor>\n /** Property cursor numbers; never reset, so a cursor issued before a reset is never reissued. */\n readonly propertyCursorCounter: { next: number }\n /**\n * The cursor generation: advanced by every reset and seed (never rewound). Search and block\n * cursors are the next result's id only in the generation that first issued that id for that\n * list; a later generation issues a distinct value, so a cursor from before a reset or seed is\n * never accepted again, even after the same first-page request.\n */\n readonly generation: { current: number }\n /** The generation that first issued each list's raw id cursor (never cleared). */\n readonly firstIssued: Map<string, number>\n}\n\n/** The origin every Notion fixture records; another origin is not emulated. */\nexport const notionEmulatorOrigin = 'https://api.notion.com'\n\ntype Route = StatefulRoute<NotionEmulatorState, NotionApiEnv>\n\nconst searchCase = 'notion.search.cursor-paging'\n\nconst versionCase = 'notion.api.pinned-version-accepted'\n\nconst errorCase = 'notion.errors.error-envelope'\n\nconst titleCase = 'notion.pages.title-plain-text'\n\nconst blocksCase = 'notion.blocks.children-cursor-paging'\n\nconst propertyCase = 'notion.pages.property-item-paging'\n\nconst dataSourceCase = 'notion.data-sources.database-split'\n\nconst archiveCase = 'notion.pages.archive-in-trash'\n\nconst evidence = (\n method: string,\n path: string,\n write: boolean,\n caseIds: ReadonlyArray<string>\n) => ({\n method,\n path: `${notionEmulatorBasePath}${path}`,\n kind: 'connector' as const,\n write,\n caseIds,\n evidence: 'unverified' as const,\n origin: notionEmulatorOrigin\n})\n\n// Responses.\n\nconst ok = (body: Schema.JsonObject): Response =>\n new Response(JSON.stringify(body), {\n status: 200,\n headers: { 'content-type': notionContentType }\n })\n\n/** A read-only commit. */\nconst answer =\n (response: () => Response): Commit =>\n () =>\n response()\n\nconst pad = (value: number, width: number): string => String(value).padStart(width, '0')\n\n/** A synthetic request id for ledger sequence `seq`. */\nconst requestId = (seq: number): string =>\n `00000000-0000-4000-8000-${pad(seq % 1_000_000_000_000, 12)}`\n\n/** The fixture error envelope `{ object: \"error\", status, code, message, request_id }`. */\nconst errorEnvelope = (\n env: NotionApiEnv,\n seq: number,\n status: number,\n code: string,\n message: string\n): Response =>\n new Response(\n JSON.stringify({\n object: 'error',\n status: env.drills.envelopeStatusMismatch ? 500 : status,\n code,\n message,\n request_id: requestId(seq)\n }),\n { status, headers: { 'content-type': notionContentType } }\n )\n\n/** The error-envelope fixture's 404 for a well-formed id that addresses no page. */\nconst pageNotFound = (env: NotionApiEnv, seq: number, id: string): Response =>\n errorEnvelope(\n env,\n seq,\n 404,\n 'object_not_found',\n `Could not find page with ID: ${id}. Make sure the relevant pages and databases are shared with your integration.`\n )\n\n/** The error-envelope fixture's 400 for a malformed page id. */\nconst malformedPageId = (env: NotionApiEnv, seq: number, raw: string): Response =>\n errorEnvelope(\n env,\n seq,\n 400,\n 'validation_error',\n `path failed validation: path.page_id should be a valid uuid, instead was \\`${JSON.stringify(raw)}\\`.`\n )\n\n// Ids, queries, and bodies.\n\nconst compactIdPattern = /^[0-9a-f]{32}$/i\n\nconst dashedIdPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\n\n/** A Notion id with or without dashes, in any case, as stored (dashed, lower case). */\nconst normalizeId = (raw: string): string | undefined => {\n if (dashedIdPattern.test(raw)) return raw.toLowerCase()\n\n if (!compactIdPattern.test(raw)) return undefined\n\n const hex = raw.toLowerCase()\n\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`\n}\n\nconst idParam = (request: EmulatedRequest, name: string): string | NotEmulated =>\n normalizeId(request.params[name] ?? '') ??\n notEmulated(`a malformed ${name} is not emulated on this route`)\n\nconst withoutQuery = (request: EmulatedRequest): NotEmulated | undefined =>\n request.query.size > 0\n ? notEmulated('query parameters are not emulated on this route')\n : undefined\n\ntype ListQuery = { readonly pageSize: number; readonly startCursor: string | undefined }\n\n/** `page_size` (required, 1-100) and an optional `start_cursor`, as the list fixtures send them. */\nconst listQuery = (request: EmulatedRequest): ListQuery | NotEmulated => {\n const keys = [...request.query.keys()]\n const unknown = keys.find(key => key !== 'page_size' && key !== 'start_cursor')\n\n if (unknown !== undefined) return notEmulated(`query parameter ${unknown} is not emulated`)\n\n if (keys.length !== new Set(keys).size) {\n return notEmulated('repeated query parameters are not emulated')\n }\n\n const raw = request.query.get('page_size')\n\n if (raw === null) return notEmulated('requests without page_size are not emulated')\n\n const pageSize = integerIn(/^\\d+$/.test(raw) ? Number(raw) : null, 'page_size', 1, 100)\n\n if (isNotEmulated(pageSize)) return pageSize\n\n const startCursor = request.query.get('start_cursor') ?? undefined\n\n return startCursor === ''\n ? notEmulated('an empty start_cursor is not emulated')\n : { pageSize, startCursor }\n}\n\nconst jsonBody = (\n request: EmulatedRequest,\n label: string,\n required: ReadonlyArray<string>,\n optional: ReadonlyArray<string> = []\n): Schema.JsonObject | NotEmulated =>\n withoutQuery(request) ?? exactObject(request.json, label, required, optional)\n\n// Rendering, in the fixture key order.\n\n/** The plain text of a page's `title` property (empty without one). */\nconst pageTitle = (page: NotionEmulatorPage): string => {\n const title = Object.values(page.properties).find(\n property => isJsonObject(property) && property.type === 'title'\n )\n\n const items = isJsonObject(title) && Array.isArray(title.title) ? title.title : []\n\n return items\n .map(item => (isJsonObject(item) && Predicate.isString(item.plain_text) ? item.plain_text : ''))\n .join('')\n}\n\n/** Title items without `plain_text` (the `omitTitlePlainText` drill). */\nconst withoutTitlePlainText = (properties: Schema.JsonObject): Schema.JsonObject =>\n Object.fromEntries(\n Object.entries(properties).map(([name, property]) => [\n name,\n isJsonObject(property) && property.type === 'title' && Array.isArray(property.title)\n ? {\n ...property,\n title: property.title.map(item =>\n isJsonObject(item)\n ? Object.fromEntries(Object.entries(item).filter(([key]) => key !== 'plain_text'))\n : item\n )\n }\n : property\n ])\n )\n\nconst renderPage = (\n env: NotionApiEnv,\n page: NotionEmulatorPage,\n read: 'page' | 'list'\n): Schema.JsonObject => {\n const parent: Schema.JsonObject =\n page.parent.type === 'data_source_id' && env.drills.rowParentAsDatabase\n ? { type: 'database_id', database_id: page.parent.database_id }\n : page.parent\n\n const properties =\n read === 'page' && env.drills.omitTitlePlainText\n ? withoutTitlePlainText(page.properties)\n : page.properties\n\n const head = { object: 'page', id: page.id }\n\n const tail = {\n parent,\n archived: page.archived,\n in_trash: page.inTrash,\n properties,\n url: page.url\n }\n\n // In fixture key order; data source rows carry no timestamps, as their fixture records.\n return page.times === null\n ? { ...head, ...tail }\n : {\n ...head,\n created_time: page.times.created,\n last_edited_time: page.times.lastEdited,\n ...tail\n }\n}\n\nconst isTrashed = (page: NotionEmulatorPage): boolean => page.archived || page.inTrash\n\nconst isImplied = (state: NotionEmulatorState, id: string): boolean =>\n state.impliedPages.some(page => page.id === id)\n\nconst impliedRefusal = (): NotEmulated =>\n notEmulated('an answer showing a page a fixture only names by id is not emulated')\n\nconst list = (\n results: ReadonlyArray<Schema.Json>,\n nextCursor: string | null,\n type: 'page_or_data_source' | 'block'\n): Schema.JsonObject => ({\n object: 'list',\n results: [...results],\n next_cursor: nextCursor,\n has_more: nextCursor !== null,\n type,\n [type]: {}\n})\n\n/** Where a page of a list starts: 0, or an issued cursor's offset; not emulated otherwise. */\nconst startOf = (\n env: NotionApiEnv,\n listKey: string,\n fingerprint: string,\n cursor: string | undefined\n): number | NotEmulated => {\n if (cursor === undefined) return 0\n\n const issued = env.cursors.get(`${listKey}\\u0000${cursor}`)\n\n if (issued === undefined) {\n return notEmulated(\n 'a start_cursor this emulator did not issue for this list since the last reset is not emulated'\n )\n }\n\n return issued.fingerprint === fingerprint\n ? issued.offset\n : notEmulated('continuing a list that changed since its cursor was issued is not emulated')\n}\n\n/** Register a cursor (in a commit). */\nconst issue = (\n env: NotionApiEnv,\n listKey: string,\n cursor: string,\n offset: number,\n fingerprint: string\n): void => {\n env.cursors.set(`${listKey}\\u0000${cursor}`, { offset, fingerprint })\n}\n\n/**\n * The cursor value for the next item `id` of a list (in a commit): the id itself, as the fixtures\n * record, in the generation that first issued it for this list; a distinct value in any later\n * generation (after a reset or seed), so no cursor value crosses a reset or seed.\n */\nconst idCursor = (env: NotionApiEnv, listKey: string, id: string): string => {\n const key = `${listKey}\\u0000${id}`\n const first = env.firstIssued.get(key)\n const generation = env.generation.current\n\n if (first === undefined) env.firstIssued.set(key, generation)\n\n return first === undefined || first === generation ? id : `${id}.g${generation}`\n}\n\n/**\n * The commit answering one page of an id-cursor list (search results, blocks): the next cursor is\n * the next item's id, as the fixtures record (see `idCursor`), issued for this list.\n */\nconst idListPage = (\n env: NotionApiEnv,\n listKey: string,\n items: ReadonlyArray<{ readonly id: string; readonly rendered: Schema.JsonObject }>,\n query: ListQuery,\n repeats: boolean,\n type: 'page_or_data_source' | 'block'\n): Commit | NotEmulated => {\n const fingerprint = JSON.stringify(items.map(item => item.rendered))\n const offset = startOf(env, listKey, fingerprint, query.startCursor)\n\n if (isNotEmulated(offset)) return offset\n\n const start = repeats && offset > 0 ? offset - 1 : offset\n const end = start + query.pageSize\n const next = items[end]\n\n return () => {\n const cursor = next === undefined ? null : idCursor(env, listKey, next.id)\n\n if (cursor !== null) issue(env, listKey, cursor, end, fingerprint)\n\n return ok(\n list(\n items.slice(start, end).map(item => item.rendered),\n cursor,\n type\n )\n )\n }\n}\n\n// Routes.\n\ntype SearchInput = { readonly query: string; readonly list: ListQuery }\n\nconst search: Route = statefulRoute(\n evidence('POST', '/search', false, [searchCase]),\n 'json',\n (request): SearchInput | NotEmulated => {\n const body = jsonBody(\n request,\n 'the search body',\n ['query', 'filter', 'page_size'],\n ['start_cursor']\n )\n\n if (isNotEmulated(body)) return body\n\n if (!Predicate.isString(body.query)) return notEmulated('query must be a string')\n\n const filter = exactObject(body.filter, 'filter', ['property', 'value'])\n\n if (isNotEmulated(filter)) return filter\n\n if (filter.property !== 'object' || filter.value !== 'page') {\n return notEmulated(\n 'filters other than { property: \"object\", value: \"page\" } are not emulated'\n )\n }\n\n const pageSize = integerIn(body.page_size, 'page_size', 1, 100)\n\n if (isNotEmulated(pageSize)) return pageSize\n\n if (\n body.start_cursor !== undefined &&\n (!Predicate.isString(body.start_cursor) || body.start_cursor.length === 0)\n ) {\n return notEmulated('start_cursor must be a non-empty string')\n }\n\n return {\n query: body.query,\n list: {\n pageSize,\n startCursor: Predicate.isString(body.start_cursor) ? body.start_cursor : undefined\n }\n }\n },\n (state, input, { env }) => {\n const query = input.query.toLowerCase()\n const matches = state.pages.filter(page => pageTitle(page).toLowerCase().includes(query))\n\n // Every recorded search answer has results; an empty one (for example the leftover lookup's\n // search of a clean workspace) is no fixture's answer.\n if (matches.length === 0) {\n return notEmulated('a search without matches is not emulated (no fixture records one)')\n }\n\n if (matches.some(isTrashed)) {\n return notEmulated(\n 'a search matching a trashed page is not emulated (no fixture records one)'\n )\n }\n\n // The search fixture's results all carry timestamps; a page shown only as a query row has\n // none, so no fixture records it as a search result.\n if (matches.some(page => page.times === null)) {\n return notEmulated(\n 'a search matching a page shown only as a query row is not emulated (no fixture records one)'\n )\n }\n\n return idListPage(\n env,\n `search\\u0000${input.query}`,\n matches.map(page => ({ id: page.id, rendered: renderPage(env, page, 'list') })),\n input.list,\n env.drills.searchRepeatsResults,\n 'page_or_data_source'\n )\n }\n)\n\nconst botUser: Route = statefulRoute(\n evidence('GET', '/users/me', false, [versionCase]),\n 'none',\n request => withoutQuery(request) ?? {},\n (state, _input, { env, seq }) =>\n answer(() => {\n const user = state.botUser\n\n const kind: Schema.JsonObject = env.drills.botUserAsPerson\n ? { type: 'person', person: {} }\n : {\n type: 'bot',\n bot: {\n owner: { type: 'workspace', workspace: true },\n workspace_name: user.workspaceName\n }\n }\n\n return ok({\n object: 'user',\n id: user.id,\n name: user.name,\n avatar_url: user.avatarUrl,\n ...kind,\n request_id: requestId(seq)\n })\n })\n)\n\ntype PageRead = { readonly raw: string; readonly id: string | undefined }\n\nconst getPage: Route = statefulRoute(\n evidence('GET', '/pages/{pageId}', false, [errorCase, titleCase, propertyCase, archiveCase]),\n 'none',\n (request): PageRead | NotEmulated => {\n const raw = request.params.pageId ?? ''\n\n return withoutQuery(request) ?? { raw, id: normalizeId(raw) }\n },\n (state, input, { env, seq }) => {\n const id = input.id\n\n if (id === undefined) return answer(() => malformedPageId(env, seq, input.raw))\n\n if (isImplied(state, id)) return impliedRefusal()\n\n const page = state.pages.find(candidate => candidate.id === id)\n\n if (page === undefined || (env.drills.trashedPageNotFound && isTrashed(page))) {\n return answer(() => pageNotFound(env, seq, id))\n }\n\n if (page.times === null) {\n return notEmulated(\n 'a page read of a page the fixtures show only as a query row is not emulated'\n )\n }\n\n return answer(() => ok(renderPage(env, page, 'page')))\n }\n)\n\nconst createPage: Route = statefulRoute(\n evidence('POST', '/pages', true, [archiveCase]),\n 'json',\n (request): string | NotEmulated => {\n const body = jsonBody(request, 'the create page body', ['parent', 'properties'])\n\n if (isNotEmulated(body)) return body\n\n const parent = exactObject(body.parent, 'parent', ['page_id'])\n\n if (isNotEmulated(parent)) return parent\n\n const parentId = Predicate.isString(parent.page_id) ? normalizeId(parent.page_id) : undefined\n\n if (parentId === undefined) return notEmulated('parent.page_id must be a Notion id')\n\n const properties = exactObject(body.properties, 'properties', ['title'])\n\n if (isNotEmulated(properties)) return properties\n\n const title = exactObject(properties.title, 'properties.title', ['title'])\n\n if (isNotEmulated(title)) return title\n\n const items = title.title\n\n if (!Array.isArray(items) || items.length !== 1) {\n return notEmulated('a title of other than one text item is not emulated')\n }\n\n const item = exactObject(items[0], 'the title item', ['text'])\n\n if (isNotEmulated(item)) return item\n\n const text = exactObject(item.text, 'the title item text', ['content'])\n\n if (isNotEmulated(text)) return text\n\n // The page URL derives from the title; only the recorded title's URL is recorded.\n return text.content === recordedCreateTitle\n ? parentId\n : notEmulated(`a page title other than \"${recordedCreateTitle}\" is not emulated`)\n },\n (state, parentId, { env }) => {\n const parent = state.pages.find(page => page.id === parentId)\n\n if (!isImplied(state, parentId) && (parent === undefined || isTrashed(parent))) {\n return notEmulated('creating a page under a missing or trashed page is not emulated')\n }\n\n return () => {\n // Read the clock before any write: a failing clock writes nothing.\n const at = new Date(env.now()).toISOString()\n const number = state.counters.nextPageNumber\n const id = `1f0000e0-0000-4000-8000-${pad(number, 12)}`\n\n const page: NotionEmulatorPage = {\n id,\n times: { created: at, lastEdited: at },\n parent: { type: 'page_id', page_id: parentId },\n archived: false,\n inTrash: false,\n properties: { title: titleProperty([plainRichText(recordedCreateTitle)]) },\n url: `https://www.notion.so/${recordedCreateSlug}-${id.replaceAll('-', '')}`\n }\n\n state.counters = { ...state.counters, nextPageNumber: number + 1 }\n state.pages = [...state.pages, page]\n\n return ok(renderPage(env, page, 'page'))\n }\n }\n)\n\nconst archivePage: Route = statefulRoute(\n evidence('PATCH', '/pages/{pageId}', true, [archiveCase]),\n 'json',\n (request): string | NotEmulated => {\n const id = idParam(request, 'pageId')\n\n if (isNotEmulated(id)) return id\n\n const body = jsonBody(request, 'the update page body', ['archived'])\n\n if (isNotEmulated(body)) return body\n\n return body.archived === true\n ? id\n : notEmulated('page updates other than { archived: true } are not emulated')\n },\n (state, id, { env }) => {\n if (isImplied(state, id)) return impliedRefusal()\n\n const page = state.pages.find(candidate => candidate.id === id)\n\n if (page === undefined || isTrashed(page) || page.times === null) {\n return notEmulated('archiving a missing, already trashed, or row page is not emulated')\n }\n\n return () => {\n // The archive fixture keeps `last_edited_time` unchanged.\n const archived: NotionEmulatorPage = { ...page, archived: true, inTrash: true }\n\n state.pages = state.pages.map(candidate => (candidate.id === id ? archived : candidate))\n\n return ok(renderPage(env, archived, 'page'))\n }\n }\n)\n\ntype ChildrenInput = { readonly blockId: string; readonly list: ListQuery }\n\nconst blockChildren: Route = statefulRoute(\n evidence('GET', '/blocks/{blockId}/children', false, [blocksCase]),\n 'none',\n (request): ChildrenInput | NotEmulated => {\n const blockId = idParam(request, 'blockId')\n\n if (isNotEmulated(blockId)) return blockId\n\n const query = listQuery(request)\n\n return isNotEmulated(query) ? query : { blockId, list: query }\n },\n (state, input, { env }) => {\n if (!isImplied(state, input.blockId) && !state.pages.some(page => page.id === input.blockId)) {\n return notEmulated('children of anything but a page are not emulated')\n }\n\n const blocks = state.blocks.filter(block => block.pageId === input.blockId)\n\n // The block fixture records only nonempty pages of children.\n if (blocks.length === 0) {\n return notEmulated('a page without recorded child blocks is not emulated')\n }\n\n return idListPage(\n env,\n `blocks\\u0000${input.blockId}`,\n blocks.map(block => ({\n id: block.id,\n rendered: {\n object: 'block',\n id: block.id,\n parent: { type: 'page_id', page_id: block.pageId },\n created_time: block.createdTime,\n last_edited_time: block.lastEditedTime,\n has_children: block.hasChildren,\n archived: block.archived,\n in_trash: block.inTrash,\n type: block.type,\n [block.type]: block.value\n }\n })),\n input.list,\n env.drills.blockCursorRepeats,\n 'block'\n )\n }\n)\n\n// Property item cursors: opaque, minted from a counter that never resets (the first one is the\n// value the fixture records), and accepted only for the list they were issued for.\n\nconst base64url = (text: string): string =>\n btoa(text).replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/, '')\n\nconst mintPropertyCursor = (env: NotionApiEnv): string => {\n const number = env.propertyCursorCounter.next\n\n env.propertyCursorCounter.next = number + 1\n\n return base64url(\n number === 1 ? 'synthetic-property-cursor' : `synthetic-property-cursor-${number}`\n )\n}\n\ntype PropertyInput = {\n readonly pageId: string\n readonly propertyId: string\n readonly list: ListQuery\n}\n\nconst decodeAgain = (value: string): string | undefined => {\n try {\n return decodeURIComponent(value)\n } catch {\n return undefined\n }\n}\n\nconst propertyItems: Route = statefulRoute(\n evidence('GET', '/pages/{pageId}/properties/{propertyId}', false, [propertyCase]),\n 'none',\n (request, env): PropertyInput | NotEmulated => {\n const pageId = idParam(request, 'pageId')\n\n if (isNotEmulated(pageId)) return pageId\n\n const once = request.params.propertyId ?? ''\n const propertyId = env.drills.rejectDoubleEncodedPropertyId ? decodeAgain(once) : once\n\n if (propertyId === undefined) return notEmulated('a malformed property id is not emulated')\n\n const query = listQuery(request)\n\n return isNotEmulated(query) ? query : { pageId, propertyId, list: query }\n },\n (state, input, { env }) => {\n const items = state.propertyItems.find(\n candidate => candidate.pageId === input.pageId && candidate.propertyId === input.propertyId\n )\n\n if (items === undefined || !state.pages.some(page => page.id === input.pageId)) {\n return notEmulated('items of a property with no seeded item list are not emulated')\n }\n\n const listKey = `property\\u0000${items.pageId}\\u0000${items.propertyId}`\n const fingerprint = JSON.stringify(items.values)\n const offset = startOf(env, listKey, fingerprint, input.list.startCursor)\n\n if (isNotEmulated(offset)) return offset\n\n return () => {\n const end = Math.min(offset + input.list.pageSize, items.values.length)\n const nextCursor = end < items.values.length ? mintPropertyCursor(env) : null\n\n if (nextCursor !== null) issue(env, listKey, nextCursor, end, fingerprint)\n\n return ok({\n object: 'list',\n results: items.values.slice(offset, end).map(value => ({\n object: 'property_item',\n id: items.propertyId,\n type: items.type,\n [items.type]: value\n })),\n next_cursor: nextCursor,\n has_more: nextCursor !== null,\n type: 'property_item',\n property_item: {\n id: items.propertyId,\n // The fixture's next_url names the property id as the page object returns it.\n next_url:\n nextCursor === null\n ? null\n : `${notionEmulatorOrigin}${notionEmulatorBasePath}/pages/${items.pageId}/properties/${items.propertyId}?start_cursor=${nextCursor}`,\n type: items.type,\n [items.type]: {}\n }\n })\n }\n }\n)\n\nconst getDatabase: Route = statefulRoute(\n evidence('GET', '/databases/{databaseId}', false, [dataSourceCase]),\n 'none',\n request => withoutQuery(request) ?? idParam(request, 'databaseId'),\n (state, id) => {\n const database = state.databases.find(candidate => candidate.id === id)\n\n if (database === undefined) return notEmulated('a missing database is not emulated')\n\n return answer(() =>\n ok({\n object: 'database',\n id: database.id,\n title: [...database.title],\n description: [...database.description],\n parent: database.parent,\n is_inline: database.isInline,\n in_trash: database.inTrash,\n archived: database.archived,\n created_time: database.createdTime,\n last_edited_time: database.lastEditedTime,\n data_sources: state.dataSources\n .filter(source => source.databaseId === database.id)\n .map(source => ({\n id: source.id,\n name: source.title\n .map(item =>\n isJsonObject(item) && Predicate.isString(item.plain_text) ? item.plain_text : ''\n )\n .join('')\n })),\n url: database.url\n })\n )\n }\n)\n\nconst getDataSource: Route = statefulRoute(\n evidence('GET', '/data_sources/{dataSourceId}', false, [dataSourceCase]),\n 'none',\n request => withoutQuery(request) ?? idParam(request, 'dataSourceId'),\n (state, id) => {\n const source = state.dataSources.find(candidate => candidate.id === id)\n const database = state.databases.find(candidate => candidate.id === source?.databaseId)\n\n if (source === undefined || database === undefined) {\n return notEmulated('a missing data source is not emulated')\n }\n\n return answer(() =>\n ok({\n object: 'data_source',\n id: source.id,\n parent: { type: 'database_id', database_id: database.id },\n database_parent: database.parent,\n title: [...source.title],\n properties: source.properties,\n archived: source.archived,\n in_trash: source.inTrash\n })\n )\n }\n)\n\ntype QueryInput = { readonly dataSourceId: string; readonly pageSize: number }\n\nconst queryDataSource: Route = statefulRoute(\n evidence('POST', '/data_sources/{dataSourceId}/query', false, [dataSourceCase]),\n 'json',\n (request): QueryInput | NotEmulated => {\n const dataSourceId = idParam(request, 'dataSourceId')\n\n if (isNotEmulated(dataSourceId)) return dataSourceId\n\n const body = jsonBody(request, 'the query body', ['page_size'])\n\n if (isNotEmulated(body)) return body\n\n const pageSize = integerIn(body.page_size, 'page_size', 1, 100)\n\n return isNotEmulated(pageSize) ? pageSize : { dataSourceId, pageSize }\n },\n (state, input, { env }) => {\n if (!state.dataSources.some(source => source.id === input.dataSourceId)) {\n return notEmulated('a missing data source is not emulated')\n }\n\n const inSource = (parent: NotionEmulatorPage['parent'] | null): boolean =>\n parent?.type === 'data_source_id' && parent.data_source_id === input.dataSourceId\n\n const rows = state.pages.filter(page => inSource(page.parent))\n\n if (rows.some(isTrashed)) {\n return notEmulated('a query of a data source with a trashed row is not emulated')\n }\n\n const implied = state.impliedPages.filter(row => inSource(row.parent)).map(row => row.id)\n const next = [...rows.map(row => row.id), ...implied][input.pageSize]\n\n // The fixture records a first page whose next row it only names: a page that would show an\n // implied row, or a last page, is not recorded.\n if (rows.length < input.pageSize) {\n return notEmulated('a query page showing a row no fixture shows is not emulated')\n }\n\n const page = rows.slice(0, input.pageSize)\n\n if (next === undefined) {\n return notEmulated('a last query page (no further row) is not emulated')\n }\n\n return answer(() =>\n ok(\n list(\n page.map(row => renderPage(env, row, 'list')),\n next,\n 'page_or_data_source'\n )\n )\n )\n }\n)\n\n/** The route table: evidence plus handlers. `notionEmulatorRoutes` is its evidence part. */\nexport const notionApiRoutes: ReadonlyArray<Route> = [\n search,\n botUser,\n getPage,\n createPage,\n archivePage,\n blockChildren,\n propertyItems,\n getDatabase,\n getDataSource,\n queryDataSource\n]\n"],"mappings":"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"}
|