@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,565 @@
|
|
|
1
|
+
import { exactObject, integerIn, isNotEmulated, jsonResponse, mediaType, notEmulated, parseJsonText, statefulRoute } from "../stateful-emulator.mjs";
|
|
2
|
+
import { syntheticContentHash } from "./state.mjs";
|
|
3
|
+
import { Predicate } from "effect";
|
|
4
|
+
//#region src/dropbox/api.ts
|
|
5
|
+
/**
|
|
6
|
+
* Dropbox emulator API: the route table (evidence, request-shape checks, and stateful handlers),
|
|
7
|
+
* the fixture error envelopes, metadata rendering, and list and search cursors (internal;
|
|
8
|
+
* re-exported by `src/dropbox.ts`).
|
|
9
|
+
*
|
|
10
|
+
* Only the RPC and upload routes the eight Dropbox conformance cases (and their cleanup) send are
|
|
11
|
+
* emulated, with the wire shapes of their synthetic fixtures. Every route is a `POST` under `/2`: the RPC routes on `api.dropboxapi.com` with a JSON body, the
|
|
12
|
+
* upload on `content.dropboxapi.com` with an `application/octet-stream` body and a
|
|
13
|
+
* `Dropbox-API-Arg` header, each only on the origin its fixtures record. Route errors are the
|
|
14
|
+
* fixtures' HTTP 409 `error_summary` envelopes, copied byte for byte. Every route plans first
|
|
15
|
+
* (reading the state, writing nothing) and commits only an eligible request: anything no fixture
|
|
16
|
+
* shows is not emulated (400, nothing written, no fault used up).
|
|
17
|
+
*
|
|
18
|
+
* @experimental
|
|
19
|
+
*/
|
|
20
|
+
/** API version prefix of every Dropbox route. */
|
|
21
|
+
const dropboxEmulatorBasePath = "/2";
|
|
22
|
+
/**
|
|
23
|
+
* The fixture error envelopes, byte for byte (`not-found-envelope.ts`, `create-folder-conflict.ts`,
|
|
24
|
+
* and `upload-rev-precondition.ts`), each answered with HTTP 409.
|
|
25
|
+
*/
|
|
26
|
+
const dropboxEmulatorErrorBodies = {
|
|
27
|
+
notFound: "{\"error_summary\": \"path/not_found/.\", \"error\": {\".tag\": \"path\", \"path\": {\".tag\": \"not_found\"}}}",
|
|
28
|
+
folderConflict: "{\"error_summary\": \"path/conflict/folder/..\", \"error\": {\".tag\": \"path\", \"path\": {\".tag\": \"conflict\", \"conflict\": {\".tag\": \"folder\"}}}}",
|
|
29
|
+
uploadConflict: "{\"error_summary\": \"path/conflict/file/..\", \"error\": {\".tag\": \"path\", \"path\": {\"reason\": {\".tag\": \"conflict\", \"conflict\": {\".tag\": \"file\"}}}}}"
|
|
30
|
+
};
|
|
31
|
+
const drillErrorBodies = {
|
|
32
|
+
notFoundAsPathLookup: "{\"error_summary\": \"path_lookup/not_found/.\", \"error\": {\".tag\": \"path_lookup\", \"path_lookup\": {\".tag\": \"not_found\"}}}",
|
|
33
|
+
folderConflictAsFile: "{\"error_summary\": \"path/conflict/file/..\", \"error\": {\".tag\": \"path\", \"path\": {\".tag\": \"conflict\", \"conflict\": {\".tag\": \"file\"}}}}"
|
|
34
|
+
};
|
|
35
|
+
const dropboxEmulatorDrillKnobs = [
|
|
36
|
+
"listFolderSinglePage",
|
|
37
|
+
"getMetadataCaseSensitive",
|
|
38
|
+
"searchRepeatsMatches",
|
|
39
|
+
"notFoundAsPathLookup",
|
|
40
|
+
"folderConflictAsFile",
|
|
41
|
+
"deleteLeavesNoTombstone",
|
|
42
|
+
"moveMintsNewId",
|
|
43
|
+
"uploadIgnoresRev"
|
|
44
|
+
];
|
|
45
|
+
/** Origin of the RPC routes. */
|
|
46
|
+
const dropboxEmulatorApiOrigin = "https://api.dropboxapi.com";
|
|
47
|
+
/** Origin of the upload route. */
|
|
48
|
+
const dropboxEmulatorContentOrigin = "https://content.dropboxapi.com";
|
|
49
|
+
const pagingCase = "dropbox.files.list-folder-cursor-paging";
|
|
50
|
+
const pathLowerCase = "dropbox.files.path-lower-lookup";
|
|
51
|
+
const searchCase = "dropbox.files.search-continue";
|
|
52
|
+
const notFoundCase = "dropbox.errors.not-found-409-envelope";
|
|
53
|
+
const conflictCase = "dropbox.files.create-folder-conflict";
|
|
54
|
+
const deleteCase = "dropbox.files.delete-then-not-found";
|
|
55
|
+
const copyCase = "dropbox.files.copy-move-metadata";
|
|
56
|
+
const uploadCase = "dropbox.files.upload-rev-precondition";
|
|
57
|
+
const evidence = (path, write, caseIds, origin = dropboxEmulatorApiOrigin) => ({
|
|
58
|
+
method: "POST",
|
|
59
|
+
path: `/2${path}`,
|
|
60
|
+
kind: "connector",
|
|
61
|
+
write,
|
|
62
|
+
caseIds,
|
|
63
|
+
evidence: "unverified",
|
|
64
|
+
origin
|
|
65
|
+
});
|
|
66
|
+
const json = (status, text) => new Response(text, {
|
|
67
|
+
status,
|
|
68
|
+
headers: { "content-type": "application/json" }
|
|
69
|
+
});
|
|
70
|
+
const ok = (body) => jsonResponse(200, body);
|
|
71
|
+
const notFound = (env) => json(409, env.drills.notFoundAsPathLookup ? drillErrorBodies.notFoundAsPathLookup : dropboxEmulatorErrorBodies.notFound);
|
|
72
|
+
const pathPattern = /^(?:\/[^/]+)+$/;
|
|
73
|
+
const parsePath = (value, label) => {
|
|
74
|
+
if (!Predicate.isString(value) || !pathPattern.test(value)) return notEmulated(`${label} must be an absolute /path (the root, ids, and revs are not emulated)`);
|
|
75
|
+
const components = value.split("/").slice(1);
|
|
76
|
+
return components.some((component) => component === "." || component === "..") ? notEmulated(`${label} with . or .. components is not emulated`) : { components };
|
|
77
|
+
};
|
|
78
|
+
const idPattern = /^id:\S+$/;
|
|
79
|
+
const childrenOf = (state, folderId) => state.entries.filter((entry) => entry.parentId === folderId);
|
|
80
|
+
const sameName = (left, right, caseSensitive) => caseSensitive ? left === right : left.toLowerCase() === right.toLowerCase();
|
|
81
|
+
/** The entry at `components` (case-insensitive, like Dropbox, unless `caseSensitive`). */
|
|
82
|
+
const lookup = (state, components, caseSensitive = false) => {
|
|
83
|
+
let current;
|
|
84
|
+
let parentId = null;
|
|
85
|
+
for (const component of components) {
|
|
86
|
+
current = childrenOf(state, parentId).find((entry) => sameName(entry.name, component, caseSensitive));
|
|
87
|
+
if (current === void 0) return void 0;
|
|
88
|
+
parentId = current.id;
|
|
89
|
+
}
|
|
90
|
+
return current;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* The folder holding `components`: `null` for the root, the folder, or not emulated when it is
|
|
94
|
+
* missing or a file (every fixture names a path whose parent folder exists).
|
|
95
|
+
*/
|
|
96
|
+
const parentFolder = (state, components, label) => {
|
|
97
|
+
if (components.length === 1) return null;
|
|
98
|
+
const parent = lookup(state, components.slice(0, -1));
|
|
99
|
+
return parent !== void 0 && parent.file === null ? parent : notEmulated(`${label} under a missing parent folder (or a file) is not emulated`);
|
|
100
|
+
};
|
|
101
|
+
/** Stored names from the root down to the entry. */
|
|
102
|
+
const storedNames = (state, entry) => {
|
|
103
|
+
const names = [];
|
|
104
|
+
let current = entry;
|
|
105
|
+
while (current !== void 0) {
|
|
106
|
+
names.unshift(current.name);
|
|
107
|
+
const parentId = current.parentId;
|
|
108
|
+
current = parentId === null ? void 0 : state.entries.find((candidate) => candidate.id === parentId);
|
|
109
|
+
}
|
|
110
|
+
return names;
|
|
111
|
+
};
|
|
112
|
+
const pathOf = (names) => names.map((name) => `/${name}`).join("");
|
|
113
|
+
const fileMetadata = (entry, file, display) => ({
|
|
114
|
+
".tag": "file",
|
|
115
|
+
name: entry.name,
|
|
116
|
+
path_lower: display.toLowerCase(),
|
|
117
|
+
path_display: display,
|
|
118
|
+
id: entry.id,
|
|
119
|
+
client_modified: file.clientModified,
|
|
120
|
+
server_modified: file.serverModified,
|
|
121
|
+
rev: file.rev,
|
|
122
|
+
size: file.size,
|
|
123
|
+
is_downloadable: true,
|
|
124
|
+
content_hash: file.contentHash
|
|
125
|
+
});
|
|
126
|
+
/** Folder metadata; `create_folder_v2` answers it without `.tag`, as its fixtures record. */
|
|
127
|
+
const folderMetadata = (entry, display, tagged) => {
|
|
128
|
+
const body = {
|
|
129
|
+
name: entry.name,
|
|
130
|
+
path_lower: display.toLowerCase(),
|
|
131
|
+
path_display: display,
|
|
132
|
+
id: entry.id
|
|
133
|
+
};
|
|
134
|
+
return tagged ? {
|
|
135
|
+
".tag": "folder",
|
|
136
|
+
...body
|
|
137
|
+
} : body;
|
|
138
|
+
};
|
|
139
|
+
const impliedRefusal = () => notEmulated("an answer showing a folder no fixture shows (an implied seed folder) is not emulated");
|
|
140
|
+
/** Metadata at the stored display path, or not emulated for an implied entry. */
|
|
141
|
+
const metadataOf = (state, entry) => {
|
|
142
|
+
if (entry.implied) return impliedRefusal();
|
|
143
|
+
const display = pathOf(storedNames(state, entry));
|
|
144
|
+
return entry.file === null ? folderMetadata(entry, display, true) : fileMetadata(entry, entry.file, display);
|
|
145
|
+
};
|
|
146
|
+
/** Metadata of every entry, or not emulated when any is implied. */
|
|
147
|
+
const metadataOfAll = (state, entries) => {
|
|
148
|
+
const rendered = [];
|
|
149
|
+
for (const entry of entries) {
|
|
150
|
+
const metadata = metadataOf(state, entry);
|
|
151
|
+
if (isNotEmulated(metadata)) return metadata;
|
|
152
|
+
rendered.push(metadata);
|
|
153
|
+
}
|
|
154
|
+
return rendered;
|
|
155
|
+
};
|
|
156
|
+
const deletedMetadata = (deleted) => ({
|
|
157
|
+
".tag": "deleted",
|
|
158
|
+
name: deleted.name,
|
|
159
|
+
path_lower: deleted.pathLower,
|
|
160
|
+
path_display: deleted.pathDisplay
|
|
161
|
+
});
|
|
162
|
+
const pad = (value, width) => String(value).padStart(width, "0");
|
|
163
|
+
const nextId = (state) => {
|
|
164
|
+
const number = state.counters.nextIdNumber;
|
|
165
|
+
state.counters = {
|
|
166
|
+
...state.counters,
|
|
167
|
+
nextIdNumber: number + 1
|
|
168
|
+
};
|
|
169
|
+
return `id:SyntheticEntry${pad(number, 8)}`;
|
|
170
|
+
};
|
|
171
|
+
const nextRev = (state) => {
|
|
172
|
+
const number = state.counters.nextRevNumber;
|
|
173
|
+
state.counters = {
|
|
174
|
+
...state.counters,
|
|
175
|
+
nextRevNumber: number + 1
|
|
176
|
+
};
|
|
177
|
+
return `a1b2c3d4e5f6${pad(number, 4)}`;
|
|
178
|
+
};
|
|
179
|
+
/** A Dropbox timestamp (`2026-09-29T14:00:00Z`) from the emulator clock. */
|
|
180
|
+
const nowTimestamp = (env) => (/* @__PURE__ */ new Date(Math.floor(env.now() / 1e3) * 1e3)).toISOString().replace(".000Z", "Z");
|
|
181
|
+
/** Issue a cursor (in a commit): a value never issued before, registered until reset or seed. */
|
|
182
|
+
const issueCursor = (env, cursor) => {
|
|
183
|
+
const number = env.cursorCounters[cursor.kind];
|
|
184
|
+
env.cursorCounters[cursor.kind] = number + 1;
|
|
185
|
+
const value = `AAHsynthetic${cursor.kind === "list" ? "List" : "Search"}Cursor${pad(number, 4)}`;
|
|
186
|
+
env.cursors.set(value, cursor);
|
|
187
|
+
return value;
|
|
188
|
+
};
|
|
189
|
+
const withoutQuery = (request) => request.query.size > 0 ? notEmulated("query parameters are not emulated on Dropbox routes") : void 0;
|
|
190
|
+
const rpcBody = (request, route, required, optional = []) => withoutQuery(request) ?? exactObject(request.json, `the ${route} body`, required, optional);
|
|
191
|
+
const falseFlag = (value, label) => value === false ? void 0 : notEmulated(`${label} other than false is not emulated`);
|
|
192
|
+
const cursorValue = (request, route) => {
|
|
193
|
+
const body = rpcBody(request, route, ["cursor"]);
|
|
194
|
+
if (isNotEmulated(body)) return body;
|
|
195
|
+
return Predicate.isString(body.cursor) && body.cursor.length > 0 ? body.cursor : notEmulated("cursor must be a non-empty string");
|
|
196
|
+
};
|
|
197
|
+
/** A read-only commit. */
|
|
198
|
+
const answer = (response) => () => response();
|
|
199
|
+
const getMetadata = statefulRoute(evidence("/files/get_metadata", false, [
|
|
200
|
+
pathLowerCase,
|
|
201
|
+
notFoundCase,
|
|
202
|
+
conflictCase,
|
|
203
|
+
deleteCase,
|
|
204
|
+
copyCase,
|
|
205
|
+
uploadCase
|
|
206
|
+
]), "json", (request) => {
|
|
207
|
+
const body = rpcBody(request, "get_metadata", ["path"], ["include_deleted"]);
|
|
208
|
+
if (isNotEmulated(body)) return body;
|
|
209
|
+
if (body.include_deleted !== void 0 && body.include_deleted !== true) return notEmulated("include_deleted other than true is not emulated");
|
|
210
|
+
const path = parsePath(body.path, "path");
|
|
211
|
+
return isNotEmulated(path) ? path : {
|
|
212
|
+
path,
|
|
213
|
+
includeDeleted: body.include_deleted === true
|
|
214
|
+
};
|
|
215
|
+
}, (state, input, { env }) => {
|
|
216
|
+
const entry = lookup(state, input.path.components, env.drills.getMetadataCaseSensitive);
|
|
217
|
+
if (entry === void 0) {
|
|
218
|
+
const parent = parentFolder(state, input.path.components, "get_metadata of a path");
|
|
219
|
+
if (isNotEmulated(parent)) return parent;
|
|
220
|
+
if (!input.includeDeleted) return answer(() => notFound(env));
|
|
221
|
+
const lower = pathOf(input.path.components).toLowerCase();
|
|
222
|
+
const deleted = state.deleted.find((candidate) => candidate.pathLower === lower);
|
|
223
|
+
return deleted === void 0 ? notEmulated("include_deleted for a path without a recorded delete is not emulated") : answer(() => ok(deletedMetadata(deleted)));
|
|
224
|
+
}
|
|
225
|
+
if (input.includeDeleted) return notEmulated("include_deleted for an existing entry is not emulated");
|
|
226
|
+
if (entry.file === null) return notEmulated("get_metadata of a folder is not emulated (no fixture records it)");
|
|
227
|
+
if (entry.implied) return impliedRefusal();
|
|
228
|
+
const display = pathOf([...input.path.components.slice(0, -1), entry.name]);
|
|
229
|
+
const file = entry.file;
|
|
230
|
+
return answer(() => ok(fileMetadata(entry, file, display)));
|
|
231
|
+
});
|
|
232
|
+
/** The whole listing of a folder, rendered, or not emulated (an implied child). */
|
|
233
|
+
const listingOf = (state, folderId) => metadataOfAll(state, childrenOf(state, folderId));
|
|
234
|
+
/** A commit answering one listing page and issuing its cursor. */
|
|
235
|
+
const listPage = (env, folderId, listing, offset, limit) => () => {
|
|
236
|
+
const page = listing.slice(offset, offset + limit);
|
|
237
|
+
const end = offset + page.length;
|
|
238
|
+
const cursor = issueCursor(env, {
|
|
239
|
+
kind: "list",
|
|
240
|
+
folderId,
|
|
241
|
+
fingerprint: JSON.stringify(listing),
|
|
242
|
+
offset: end,
|
|
243
|
+
limit
|
|
244
|
+
});
|
|
245
|
+
return ok({
|
|
246
|
+
entries: [...page],
|
|
247
|
+
cursor,
|
|
248
|
+
has_more: end < listing.length
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
const listFolder = statefulRoute(evidence("/files/list_folder", false, [pagingCase]), "json", (request) => {
|
|
252
|
+
const body = rpcBody(request, "list_folder", ["path", "limit"]);
|
|
253
|
+
if (isNotEmulated(body)) return body;
|
|
254
|
+
const path = parsePath(body.path, "path");
|
|
255
|
+
if (isNotEmulated(path)) return path;
|
|
256
|
+
const limit = integerIn(body.limit, "limit", 1, 2e3);
|
|
257
|
+
return isNotEmulated(limit) ? limit : {
|
|
258
|
+
path,
|
|
259
|
+
limit
|
|
260
|
+
};
|
|
261
|
+
}, (state, input, { env }) => {
|
|
262
|
+
const folder = lookup(state, input.path.components);
|
|
263
|
+
if (folder === void 0 || folder.file !== null) return notEmulated("list_folder of a missing path or a file is not emulated");
|
|
264
|
+
const listing = listingOf(state, folder.id);
|
|
265
|
+
if (isNotEmulated(listing)) return listing;
|
|
266
|
+
if (listing.length === 0) return notEmulated("a listing of an empty folder is not emulated (no fixture records one)");
|
|
267
|
+
const limit = env.drills.listFolderSinglePage ? Math.max(listing.length, 1) : input.limit;
|
|
268
|
+
return listPage(env, folder.id, listing, 0, limit);
|
|
269
|
+
});
|
|
270
|
+
const listFolderContinue = statefulRoute(evidence("/files/list_folder/continue", false, [pagingCase]), "json", (request) => cursorValue(request, "list_folder/continue"), (state, value, { env }) => {
|
|
271
|
+
const cursor = env.cursors.get(value);
|
|
272
|
+
if (cursor === void 0 || cursor.kind !== "list") return notEmulated("a list cursor this emulator did not issue since the last reset is not emulated");
|
|
273
|
+
const folder = state.entries.find((entry) => entry.id === cursor.folderId);
|
|
274
|
+
const listing = folder === void 0 ? void 0 : listingOf(state, folder.id);
|
|
275
|
+
if (folder === void 0 || listing === void 0 || isNotEmulated(listing) || JSON.stringify(listing) !== cursor.fingerprint) return notEmulated("continuing a listing whose folder changed since its cursor is not emulated");
|
|
276
|
+
if (cursor.offset >= listing.length) return notEmulated("continuing a finished listing (changes since it) is not emulated");
|
|
277
|
+
return listPage(env, folder.id, listing, cursor.offset, cursor.limit);
|
|
278
|
+
});
|
|
279
|
+
/** Every match of `query`, rendered as the fixture's match objects, or not emulated. */
|
|
280
|
+
const searchMatches = (state, query) => {
|
|
281
|
+
const lower = query.toLowerCase();
|
|
282
|
+
const matches = state.entries.filter((entry) => entry.name.toLowerCase().includes(lower));
|
|
283
|
+
if (matches.length === 0) return notEmulated("a search without matches is not emulated");
|
|
284
|
+
if (matches.some((entry) => entry.file === null)) return notEmulated("a search matching a folder is not emulated (the fixture matches files)");
|
|
285
|
+
const rendered = metadataOfAll(state, matches);
|
|
286
|
+
return isNotEmulated(rendered) ? rendered : rendered.map((metadata) => ({
|
|
287
|
+
match_type: { ".tag": "filename" },
|
|
288
|
+
metadata: {
|
|
289
|
+
".tag": "metadata",
|
|
290
|
+
metadata
|
|
291
|
+
}
|
|
292
|
+
}));
|
|
293
|
+
};
|
|
294
|
+
/** A commit answering one search page (the fixtures' last page carries no cursor). */
|
|
295
|
+
const searchPage = (env, query, matches, start, offset, maxResults) => () => {
|
|
296
|
+
const end = Math.min(offset + maxResults, matches.length);
|
|
297
|
+
const hasMore = end < matches.length;
|
|
298
|
+
const body = {
|
|
299
|
+
matches: matches.slice(start, end),
|
|
300
|
+
has_more: hasMore
|
|
301
|
+
};
|
|
302
|
+
if (!hasMore) return ok(body);
|
|
303
|
+
const cursor = issueCursor(env, {
|
|
304
|
+
kind: "search",
|
|
305
|
+
query,
|
|
306
|
+
fingerprint: JSON.stringify(matches),
|
|
307
|
+
offset: end,
|
|
308
|
+
maxResults
|
|
309
|
+
});
|
|
310
|
+
return ok({
|
|
311
|
+
...body,
|
|
312
|
+
cursor
|
|
313
|
+
});
|
|
314
|
+
};
|
|
315
|
+
const search = statefulRoute(evidence("/files/search_v2", false, [searchCase]), "json", (request) => {
|
|
316
|
+
const body = rpcBody(request, "search_v2", ["query", "options"]);
|
|
317
|
+
if (isNotEmulated(body)) return body;
|
|
318
|
+
if (!Predicate.isString(body.query) || body.query.trim().length === 0) return notEmulated("query must be a non-empty string");
|
|
319
|
+
const options = exactObject(body.options, "options", ["max_results", "filename_only"]);
|
|
320
|
+
if (isNotEmulated(options)) return options;
|
|
321
|
+
if (options.filename_only !== true) return notEmulated("options.filename_only other than true is not emulated");
|
|
322
|
+
const maxResults = integerIn(options.max_results, "options.max_results", 1, 1e3);
|
|
323
|
+
return isNotEmulated(maxResults) ? maxResults : {
|
|
324
|
+
query: body.query,
|
|
325
|
+
maxResults
|
|
326
|
+
};
|
|
327
|
+
}, (state, input, { env }) => {
|
|
328
|
+
const matches = searchMatches(state, input.query);
|
|
329
|
+
return isNotEmulated(matches) ? matches : searchPage(env, input.query, matches, 0, 0, input.maxResults);
|
|
330
|
+
});
|
|
331
|
+
const searchContinue = statefulRoute(evidence("/files/search/continue_v2", false, [searchCase]), "json", (request) => cursorValue(request, "search/continue_v2"), (state, value, { env }) => {
|
|
332
|
+
const cursor = env.cursors.get(value);
|
|
333
|
+
if (cursor === void 0 || cursor.kind !== "search") return notEmulated("a search cursor this emulator did not issue since the last reset is not emulated");
|
|
334
|
+
const matches = searchMatches(state, cursor.query);
|
|
335
|
+
if (isNotEmulated(matches) || JSON.stringify(matches) !== cursor.fingerprint) return notEmulated("continuing a search whose matches changed since its cursor is not emulated");
|
|
336
|
+
const start = env.drills.searchRepeatsMatches ? 0 : cursor.offset;
|
|
337
|
+
return searchPage(env, cursor.query, matches, start, cursor.offset, cursor.maxResults);
|
|
338
|
+
});
|
|
339
|
+
const createFolder = statefulRoute(evidence("/files/create_folder_v2", true, [
|
|
340
|
+
conflictCase,
|
|
341
|
+
deleteCase,
|
|
342
|
+
copyCase,
|
|
343
|
+
uploadCase
|
|
344
|
+
]), "json", (request) => {
|
|
345
|
+
const body = rpcBody(request, "create_folder_v2", ["path", "autorename"]);
|
|
346
|
+
if (isNotEmulated(body)) return body;
|
|
347
|
+
return falseFlag(body.autorename, "autorename") ?? parsePath(body.path, "path");
|
|
348
|
+
}, (state, path, { env }) => {
|
|
349
|
+
const parent = parentFolder(state, path.components, "create_folder_v2");
|
|
350
|
+
if (isNotEmulated(parent)) return parent;
|
|
351
|
+
const existing = lookup(state, path.components);
|
|
352
|
+
if (existing !== void 0) {
|
|
353
|
+
if (existing.file !== null) return notEmulated("create_folder_v2 over a file is not emulated");
|
|
354
|
+
return answer(() => json(409, env.drills.folderConflictAsFile ? drillErrorBodies.folderConflictAsFile : dropboxEmulatorErrorBodies.folderConflict));
|
|
355
|
+
}
|
|
356
|
+
return () => {
|
|
357
|
+
const created = {
|
|
358
|
+
id: nextId(state),
|
|
359
|
+
parentId: parent?.id ?? null,
|
|
360
|
+
name: path.components.at(-1) ?? "",
|
|
361
|
+
file: null,
|
|
362
|
+
implied: false
|
|
363
|
+
};
|
|
364
|
+
state.entries = [...state.entries, created];
|
|
365
|
+
return ok({ metadata: folderMetadata(created, pathOf(storedNames(state, created)), false) });
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
/** The entry and everything under it. */
|
|
369
|
+
const subtreeOf = (state, root) => [root, ...childrenOf(state, root.id).flatMap((child) => subtreeOf(state, child))];
|
|
370
|
+
const deleteEntry = statefulRoute(evidence("/files/delete_v2", true, [
|
|
371
|
+
conflictCase,
|
|
372
|
+
deleteCase,
|
|
373
|
+
copyCase,
|
|
374
|
+
uploadCase
|
|
375
|
+
]), "json", (request) => {
|
|
376
|
+
const body = rpcBody(request, "delete_v2", ["path"]);
|
|
377
|
+
if (isNotEmulated(body)) return body;
|
|
378
|
+
if (Predicate.isString(body.path) && idPattern.test(body.path)) return { id: body.path };
|
|
379
|
+
const path = parsePath(body.path, "path");
|
|
380
|
+
return isNotEmulated(path) ? path : { path };
|
|
381
|
+
}, (state, target, { env }) => {
|
|
382
|
+
const entry = "id" in target ? state.entries.find((candidate) => candidate.id === target.id) : lookup(state, target.path.components);
|
|
383
|
+
if (entry === void 0) return notEmulated("delete_v2 of a missing entry is not emulated (no fixture records it)");
|
|
384
|
+
if (entry.file !== null) return notEmulated("delete_v2 of a file is not emulated (the fixtures delete folders)");
|
|
385
|
+
const removed = subtreeOf(state, entry);
|
|
386
|
+
if (removed.some((candidate) => candidate.implied)) return impliedRefusal();
|
|
387
|
+
const metadata = metadataOf(state, entry);
|
|
388
|
+
if (isNotEmulated(metadata)) return metadata;
|
|
389
|
+
return () => {
|
|
390
|
+
const removedIds = new Set(removed.map((candidate) => candidate.id));
|
|
391
|
+
if (removed.length === 1 && !env.drills.deleteLeavesNoTombstone) {
|
|
392
|
+
const display = pathOf(storedNames(state, entry));
|
|
393
|
+
const lower = display.toLowerCase();
|
|
394
|
+
state.deleted = [...state.deleted.filter((record) => record.pathLower !== lower), {
|
|
395
|
+
name: entry.name,
|
|
396
|
+
pathLower: lower,
|
|
397
|
+
pathDisplay: display
|
|
398
|
+
}];
|
|
399
|
+
}
|
|
400
|
+
state.entries = state.entries.filter((candidate) => !removedIds.has(candidate.id));
|
|
401
|
+
return ok({ metadata });
|
|
402
|
+
};
|
|
403
|
+
});
|
|
404
|
+
const relocationInput = (request, route) => {
|
|
405
|
+
const body = rpcBody(request, route, [
|
|
406
|
+
"from_path",
|
|
407
|
+
"to_path",
|
|
408
|
+
"autorename"
|
|
409
|
+
]);
|
|
410
|
+
if (isNotEmulated(body)) return body;
|
|
411
|
+
const autorename = falseFlag(body.autorename, "autorename");
|
|
412
|
+
if (autorename !== void 0) return autorename;
|
|
413
|
+
const from = parsePath(body.from_path, "from_path");
|
|
414
|
+
if (isNotEmulated(from)) return from;
|
|
415
|
+
const to = parsePath(body.to_path, "to_path");
|
|
416
|
+
return isNotEmulated(to) ? to : {
|
|
417
|
+
from,
|
|
418
|
+
to
|
|
419
|
+
};
|
|
420
|
+
};
|
|
421
|
+
/** The source file and destination folder of a copy or move, or not emulated. */
|
|
422
|
+
const relocation = (state, input, route) => {
|
|
423
|
+
const source = lookup(state, input.from.components);
|
|
424
|
+
if (source === void 0 || source.file === null || source.implied) return notEmulated(`${route} of a missing entry or a folder is not emulated (files only)`);
|
|
425
|
+
const parent = parentFolder(state, input.to.components, route);
|
|
426
|
+
if (isNotEmulated(parent)) return parent;
|
|
427
|
+
return lookup(state, input.to.components) === void 0 ? {
|
|
428
|
+
source,
|
|
429
|
+
file: source.file,
|
|
430
|
+
parent
|
|
431
|
+
} : notEmulated(`${route} onto an existing entry is not emulated`);
|
|
432
|
+
};
|
|
433
|
+
const copy = statefulRoute(evidence("/files/copy_v2", true, [copyCase]), "json", (request) => relocationInput(request, "copy_v2"), (state, input) => {
|
|
434
|
+
const found = relocation(state, input, "copy_v2");
|
|
435
|
+
if (isNotEmulated(found)) return found;
|
|
436
|
+
return () => {
|
|
437
|
+
const file = {
|
|
438
|
+
...found.file,
|
|
439
|
+
rev: nextRev(state)
|
|
440
|
+
};
|
|
441
|
+
const created = {
|
|
442
|
+
id: nextId(state),
|
|
443
|
+
parentId: found.parent?.id ?? null,
|
|
444
|
+
name: input.to.components.at(-1) ?? "",
|
|
445
|
+
file,
|
|
446
|
+
implied: false
|
|
447
|
+
};
|
|
448
|
+
state.entries = [...state.entries, created];
|
|
449
|
+
return ok({ metadata: fileMetadata(created, file, pathOf(storedNames(state, created))) });
|
|
450
|
+
};
|
|
451
|
+
});
|
|
452
|
+
const move = statefulRoute(evidence("/files/move_v2", true, [copyCase]), "json", (request) => relocationInput(request, "move_v2"), (state, input, { env }) => {
|
|
453
|
+
const found = relocation(state, input, "move_v2");
|
|
454
|
+
if (isNotEmulated(found)) return found;
|
|
455
|
+
return () => {
|
|
456
|
+
const file = {
|
|
457
|
+
...found.file,
|
|
458
|
+
rev: nextRev(state)
|
|
459
|
+
};
|
|
460
|
+
const moved = {
|
|
461
|
+
id: env.drills.moveMintsNewId ? nextId(state) : found.source.id,
|
|
462
|
+
parentId: found.parent?.id ?? null,
|
|
463
|
+
name: input.to.components.at(-1) ?? "",
|
|
464
|
+
file,
|
|
465
|
+
implied: false
|
|
466
|
+
};
|
|
467
|
+
state.entries = state.entries.map((entry) => entry.id === found.source.id ? moved : entry);
|
|
468
|
+
return ok({ metadata: fileMetadata(moved, file, pathOf(storedNames(state, moved))) });
|
|
469
|
+
};
|
|
470
|
+
});
|
|
471
|
+
const revPattern = /^[0-9a-f]{9,}$/;
|
|
472
|
+
/** A file written by an upload (in a commit): a new rev, its size, hash, and clock timestamps. */
|
|
473
|
+
const uploadedFile = (state, at, bytes) => {
|
|
474
|
+
const rev = nextRev(state);
|
|
475
|
+
return {
|
|
476
|
+
rev,
|
|
477
|
+
size: bytes.byteLength,
|
|
478
|
+
contentHash: syntheticContentHash(rev.slice(-2)),
|
|
479
|
+
clientModified: at,
|
|
480
|
+
serverModified: at
|
|
481
|
+
};
|
|
482
|
+
};
|
|
483
|
+
/** The route table: evidence plus handlers. `dropboxEmulatorRoutes` is its evidence part. */
|
|
484
|
+
const dropboxApiRoutes = [
|
|
485
|
+
listFolder,
|
|
486
|
+
listFolderContinue,
|
|
487
|
+
getMetadata,
|
|
488
|
+
search,
|
|
489
|
+
searchContinue,
|
|
490
|
+
createFolder,
|
|
491
|
+
deleteEntry,
|
|
492
|
+
copy,
|
|
493
|
+
move,
|
|
494
|
+
statefulRoute(evidence("/files/upload", true, [uploadCase], dropboxEmulatorContentOrigin), "bytes", (request) => {
|
|
495
|
+
const noQuery = withoutQuery(request);
|
|
496
|
+
if (noQuery !== void 0) return noQuery;
|
|
497
|
+
if (mediaType(request.header("content-type")) !== "application/octet-stream") return notEmulated("the upload takes a content-type: application/octet-stream body");
|
|
498
|
+
const header = request.header("dropbox-api-arg");
|
|
499
|
+
const arg = header === void 0 ? void 0 : parseJsonText(header);
|
|
500
|
+
if (arg === void 0) return notEmulated("the upload needs a JSON Dropbox-API-Arg header");
|
|
501
|
+
const fields = exactObject(arg, "Dropbox-API-Arg", [
|
|
502
|
+
"path",
|
|
503
|
+
"mode",
|
|
504
|
+
"autorename",
|
|
505
|
+
"strict_conflict"
|
|
506
|
+
]);
|
|
507
|
+
if (isNotEmulated(fields)) return fields;
|
|
508
|
+
const autorename = falseFlag(fields.autorename, "autorename");
|
|
509
|
+
if (autorename !== void 0) return autorename;
|
|
510
|
+
if (fields.strict_conflict !== true) return notEmulated("strict_conflict other than true is not emulated");
|
|
511
|
+
const bytes = request.bytes ?? new Uint8Array();
|
|
512
|
+
if (fields.mode === "add") {
|
|
513
|
+
const path = parsePath(fields.path, "path");
|
|
514
|
+
return isNotEmulated(path) ? path : {
|
|
515
|
+
mode: "add",
|
|
516
|
+
path,
|
|
517
|
+
bytes
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
const mode = exactObject(fields.mode, "mode", [".tag", "update"]);
|
|
521
|
+
if (isNotEmulated(mode)) return mode;
|
|
522
|
+
if (mode[".tag"] !== "update" || !Predicate.isString(mode.update) || !revPattern.test(mode.update)) return notEmulated("mode other than add or { \".tag\": \"update\", update: <rev> } is not emulated");
|
|
523
|
+
return Predicate.isString(fields.path) && idPattern.test(fields.path) ? {
|
|
524
|
+
mode: "update",
|
|
525
|
+
id: fields.path,
|
|
526
|
+
rev: mode.update,
|
|
527
|
+
bytes
|
|
528
|
+
} : notEmulated("an update upload addresses the file by its id: form only");
|
|
529
|
+
}, (state, input, { env }) => {
|
|
530
|
+
if (input.mode === "add") {
|
|
531
|
+
const parent = parentFolder(state, input.path.components, "the upload");
|
|
532
|
+
if (isNotEmulated(parent)) return parent;
|
|
533
|
+
const existing = lookup(state, input.path.components);
|
|
534
|
+
if (existing !== void 0) return existing.file === null ? notEmulated("an add upload onto a folder is not emulated") : answer(() => json(409, dropboxEmulatorErrorBodies.uploadConflict));
|
|
535
|
+
return () => {
|
|
536
|
+
const file = uploadedFile(state, nowTimestamp(env), input.bytes);
|
|
537
|
+
const created = {
|
|
538
|
+
id: nextId(state),
|
|
539
|
+
parentId: parent?.id ?? null,
|
|
540
|
+
name: input.path.components.at(-1) ?? "",
|
|
541
|
+
file,
|
|
542
|
+
implied: false
|
|
543
|
+
};
|
|
544
|
+
state.entries = [...state.entries, created];
|
|
545
|
+
return ok(fileMetadata(created, file, pathOf(storedNames(state, created))));
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
const existing = state.entries.find((entry) => entry.id === input.id);
|
|
549
|
+
if (existing === void 0 || existing.file === null || existing.implied) return notEmulated("an update upload of a missing file or a folder is not emulated");
|
|
550
|
+
if (existing.file.rev !== input.rev && !env.drills.uploadIgnoresRev) return answer(() => json(409, dropboxEmulatorErrorBodies.uploadConflict));
|
|
551
|
+
return () => {
|
|
552
|
+
const file = uploadedFile(state, nowTimestamp(env), input.bytes);
|
|
553
|
+
const updated = {
|
|
554
|
+
...existing,
|
|
555
|
+
file
|
|
556
|
+
};
|
|
557
|
+
state.entries = state.entries.map((entry) => entry.id === existing.id ? updated : entry);
|
|
558
|
+
return ok(fileMetadata(updated, file, pathOf(storedNames(state, updated))));
|
|
559
|
+
};
|
|
560
|
+
})
|
|
561
|
+
];
|
|
562
|
+
//#endregion
|
|
563
|
+
export { dropboxApiRoutes, dropboxEmulatorApiOrigin, dropboxEmulatorBasePath, dropboxEmulatorContentOrigin, dropboxEmulatorDrillKnobs, dropboxEmulatorErrorBodies };
|
|
564
|
+
|
|
565
|
+
//# sourceMappingURL=api.mjs.map
|