@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,541 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Microsoft Graph emulator OneDrive routes (internal): item read, children listing, folder
|
|
3
|
+
* create, delete, and the asynchronous copy with its monitor URL. Wire shapes follow the
|
|
4
|
+
* synthetic OneDrive conformance fixtures.
|
|
5
|
+
*
|
|
6
|
+
* Deleted items are removed with their subtree (no recycle bin is emulated). Folder create and
|
|
7
|
+
* copy take `@microsoft.graph.conflictBehavior` `fail` only, as the fixtures send it, and only for
|
|
8
|
+
* a name that is free: no fixture records a name conflict, so one fails closed (400). Children
|
|
9
|
+
* listings need `$top` and answer one page (no `$skip`, no `@odata.nextLink`), as the fixtures
|
|
10
|
+
* record them. A copy takes what the copy fixture sends (a file, `parentReference { driveId, id }`
|
|
11
|
+
* on the same drive, no new `name`; anything else is not emulated) and answers 202 with one
|
|
12
|
+
* monitor `Location` on the SharePoint origin; the monitor needs no credentials, answers `inProgress` (202) for `copyInProgressPolls` polls (default 0), then runs
|
|
13
|
+
* the copy and answers `completed` (200) with the new item's `resourceId`, as the fixture's first
|
|
14
|
+
* poll does. No fixture records a failed copy: when the copy can no longer run (source or
|
|
15
|
+
* destination gone, or the name taken), the monitor answers 400 not emulated.
|
|
16
|
+
*
|
|
17
|
+
* @experimental
|
|
18
|
+
*/
|
|
19
|
+
import { Predicate } from 'effect'
|
|
20
|
+
import type * as Schema from 'effect/Schema'
|
|
21
|
+
import {
|
|
22
|
+
bodyObject,
|
|
23
|
+
codes,
|
|
24
|
+
collection,
|
|
25
|
+
emptyResponse,
|
|
26
|
+
entity,
|
|
27
|
+
invalidValue,
|
|
28
|
+
isJsonObject,
|
|
29
|
+
jsonResponse,
|
|
30
|
+
metadataContext,
|
|
31
|
+
nestedObject,
|
|
32
|
+
notEmulated,
|
|
33
|
+
nowTimestamp,
|
|
34
|
+
odataKey,
|
|
35
|
+
padded,
|
|
36
|
+
personalSite,
|
|
37
|
+
project,
|
|
38
|
+
selectSuffix,
|
|
39
|
+
selectedFields,
|
|
40
|
+
singlePage,
|
|
41
|
+
type CopyMonitor,
|
|
42
|
+
type MicrosoftApiEnv,
|
|
43
|
+
type RouteHandler,
|
|
44
|
+
type RouteRequest
|
|
45
|
+
} from './graph.ts'
|
|
46
|
+
import type { MicrosoftEmulatorDriveItem, MicrosoftEmulatorState } from './state.ts'
|
|
47
|
+
|
|
48
|
+
const itemFields: ReadonlyArray<string> = [
|
|
49
|
+
'id',
|
|
50
|
+
'name',
|
|
51
|
+
'size',
|
|
52
|
+
'webUrl',
|
|
53
|
+
'createdDateTime',
|
|
54
|
+
'lastModifiedDateTime',
|
|
55
|
+
'eTag',
|
|
56
|
+
'cTag',
|
|
57
|
+
'parentReference',
|
|
58
|
+
'file',
|
|
59
|
+
'folder',
|
|
60
|
+
'package',
|
|
61
|
+
'remoteItem',
|
|
62
|
+
'shared',
|
|
63
|
+
'deleted'
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
const notFound = (request: RouteRequest): Response =>
|
|
67
|
+
request.error(404, codes.driveItemNotFound, 'The resource could not be found.')
|
|
68
|
+
|
|
69
|
+
const childrenOf = (state: MicrosoftEmulatorState, id: string) =>
|
|
70
|
+
state.driveItems.filter(item => item.parentId === id)
|
|
71
|
+
|
|
72
|
+
/** The item and its descendants. */
|
|
73
|
+
const subtreeOf = (
|
|
74
|
+
state: MicrosoftEmulatorState,
|
|
75
|
+
root: MicrosoftEmulatorDriveItem
|
|
76
|
+
): ReadonlyArray<MicrosoftEmulatorDriveItem> => [
|
|
77
|
+
root,
|
|
78
|
+
...childrenOf(state, root.id).flatMap(child => subtreeOf(state, child))
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
/** Folders report the size of their contents. */
|
|
82
|
+
const sizeOf = (state: MicrosoftEmulatorState, item: MicrosoftEmulatorDriveItem): number =>
|
|
83
|
+
item.kind === 'file'
|
|
84
|
+
? item.size
|
|
85
|
+
: childrenOf(state, item.id).reduce((sum, child) => sum + sizeOf(state, child), 0)
|
|
86
|
+
|
|
87
|
+
/** Names from below the root down to the item (empty for the root). */
|
|
88
|
+
const pathNames = (
|
|
89
|
+
state: MicrosoftEmulatorState,
|
|
90
|
+
item: MicrosoftEmulatorDriveItem
|
|
91
|
+
): ReadonlyArray<string> => {
|
|
92
|
+
const parent =
|
|
93
|
+
item.parentId === null
|
|
94
|
+
? undefined
|
|
95
|
+
: state.driveItems.find(candidate => candidate.id === item.parentId)
|
|
96
|
+
|
|
97
|
+
return parent === undefined ? [] : [...pathNames(state, parent), item.name]
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** A stable synthetic GUID for an item's `eTag`/`cTag` (FNV-1a over the id). */
|
|
101
|
+
const itemGuid = (id: string): string => {
|
|
102
|
+
let hash = 0x811c9dc5
|
|
103
|
+
|
|
104
|
+
for (const character of id) {
|
|
105
|
+
hash = Math.imul(hash ^ (character.codePointAt(0) ?? 0), 0x01000193) >>> 0
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return `${hash.toString(16).padStart(8, '0')}-0000-4000-8000-000000000000`
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The full item, in the fixture key order. */
|
|
112
|
+
const renderItem = (
|
|
113
|
+
state: MicrosoftEmulatorState,
|
|
114
|
+
env: MicrosoftApiEnv,
|
|
115
|
+
item: MicrosoftEmulatorDriveItem
|
|
116
|
+
): Schema.JsonObject => {
|
|
117
|
+
const site = personalSite(state.user)
|
|
118
|
+
const names = pathNames(state, item)
|
|
119
|
+
const parentPath = names.slice(0, -1)
|
|
120
|
+
|
|
121
|
+
const parentReference: Schema.JsonObject =
|
|
122
|
+
item.parentId === null
|
|
123
|
+
? { driveType: state.drive.driveType, driveId: state.drive.id }
|
|
124
|
+
: {
|
|
125
|
+
driveType: state.drive.driveType,
|
|
126
|
+
driveId: state.drive.id,
|
|
127
|
+
id: item.parentId,
|
|
128
|
+
path: `/drive/root:${parentPath.map(name => `/${name}`).join('')}`
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const base = {
|
|
132
|
+
id: item.id,
|
|
133
|
+
name: item.name,
|
|
134
|
+
size: sizeOf(state, item),
|
|
135
|
+
webUrl: `${env.sharePointOrigin}/personal/${site}/Documents${names.map(name => `/${encodeURIComponent(name)}`).join('')}`,
|
|
136
|
+
createdDateTime: item.createdDateTime,
|
|
137
|
+
lastModifiedDateTime: item.lastModifiedDateTime,
|
|
138
|
+
eTag: `"{${itemGuid(item.id)}},1"`,
|
|
139
|
+
cTag: `"c:{${itemGuid(item.id)}},0"`,
|
|
140
|
+
parentReference
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (item.kind === 'folder') {
|
|
144
|
+
return { ...base, folder: { childCount: childrenOf(state, item.id).length } }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
...base,
|
|
149
|
+
file: {
|
|
150
|
+
mimeType: item.mimeType ?? 'application/octet-stream',
|
|
151
|
+
hashes: item.quickXorHash === null ? {} : { quickXorHash: item.quickXorHash }
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** `drives('{driveId}')`, the context prefix of every OneDrive route. */
|
|
157
|
+
const driveContext = (request: RouteRequest): string =>
|
|
158
|
+
`drives${odataKey(request.params.driveId ?? '')}`
|
|
159
|
+
|
|
160
|
+
/** `drives('{driveId}')/items('{itemId}')/children`. */
|
|
161
|
+
const childrenContext = (request: RouteRequest): string =>
|
|
162
|
+
`${driveContext(request)}/items${odataKey(request.params.itemId ?? '')}/children`
|
|
163
|
+
|
|
164
|
+
/** The drive of the `{driveId}` segment, or 404. */
|
|
165
|
+
const driveProblem = (state: MicrosoftEmulatorState, request: RouteRequest) =>
|
|
166
|
+
request.params.driveId === state.drive.id ? undefined : notFound(request)
|
|
167
|
+
|
|
168
|
+
/** The item of an id segment (`root` names the drive root). */
|
|
169
|
+
const findItem = (
|
|
170
|
+
state: MicrosoftEmulatorState,
|
|
171
|
+
id: string
|
|
172
|
+
): MicrosoftEmulatorDriveItem | undefined => {
|
|
173
|
+
const resolved = id.toLowerCase() === 'root' ? state.drive.rootId : id
|
|
174
|
+
|
|
175
|
+
return state.driveItems.find(item => item.id === resolved)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** `GET /drives/{driveId}/items/{itemId}` (`$select`). */
|
|
179
|
+
export const getItem: RouteHandler = (state, request, env) => {
|
|
180
|
+
const drive = driveProblem(state, request)
|
|
181
|
+
|
|
182
|
+
if (drive !== undefined) return drive
|
|
183
|
+
|
|
184
|
+
const fields = selectedFields(request, itemFields)
|
|
185
|
+
|
|
186
|
+
if (fields instanceof Response) return fields
|
|
187
|
+
|
|
188
|
+
const item = findItem(state, request.params.itemId ?? '')
|
|
189
|
+
|
|
190
|
+
const context = metadataContext(
|
|
191
|
+
env,
|
|
192
|
+
`${driveContext(request)}/items${selectSuffix(fields)}/$entity`
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
return item === undefined
|
|
196
|
+
? notFound(request)
|
|
197
|
+
: jsonResponse(200, entity(context, project(renderItem(state, env, item), fields)))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const byName = (left: MicrosoftEmulatorDriveItem, right: MicrosoftEmulatorDriveItem) =>
|
|
201
|
+
left.name.localeCompare(right.name, 'en', { sensitivity: 'base' }) ||
|
|
202
|
+
left.id.localeCompare(right.id)
|
|
203
|
+
|
|
204
|
+
/** The largest children `$top` a fixture sends (`$top=200`, create-folder and copy cases). */
|
|
205
|
+
const childrenMaxTop = 200
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* `GET /drives/{driveId}/items/{itemId}/children`: by name, at most `$top` (one page only;
|
|
209
|
+
* `$top` required, at most `childrenMaxTop`).
|
|
210
|
+
*/
|
|
211
|
+
export const listChildren: RouteHandler = (state, request, env) => {
|
|
212
|
+
const drive = driveProblem(state, request)
|
|
213
|
+
|
|
214
|
+
if (drive !== undefined) return drive
|
|
215
|
+
|
|
216
|
+
const fields = selectedFields(request, itemFields)
|
|
217
|
+
|
|
218
|
+
if (fields instanceof Response) return fields
|
|
219
|
+
|
|
220
|
+
const parent = findItem(state, request.params.itemId ?? '')
|
|
221
|
+
|
|
222
|
+
if (parent === undefined) return notFound(request)
|
|
223
|
+
|
|
224
|
+
if (parent.kind !== 'folder') return notEmulated(request, 'children of a file are not emulated.')
|
|
225
|
+
|
|
226
|
+
const page = singlePage([...childrenOf(state, parent.id)].sort(byName), request, childrenMaxTop)
|
|
227
|
+
|
|
228
|
+
if (page instanceof Response) return page
|
|
229
|
+
|
|
230
|
+
return jsonResponse(
|
|
231
|
+
200,
|
|
232
|
+
collection(
|
|
233
|
+
metadataContext(env, `${childrenContext(request)}${selectSuffix(fields)}`),
|
|
234
|
+
page.map(item => project(renderItem(state, env, item), fields)),
|
|
235
|
+
undefined
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const invalidNamePattern = /["*:<>?/\\|]/
|
|
241
|
+
|
|
242
|
+
const nameProblem = (request: RouteRequest, name: unknown): string | Response =>
|
|
243
|
+
Predicate.isString(name) &&
|
|
244
|
+
name.trim() !== '' &&
|
|
245
|
+
name.trim() === name &&
|
|
246
|
+
!invalidNamePattern.test(name) &&
|
|
247
|
+
name !== '.' &&
|
|
248
|
+
name !== '..' &&
|
|
249
|
+
!name.endsWith('.')
|
|
250
|
+
? name
|
|
251
|
+
: invalidValue(request, 'the item name is empty or has characters OneDrive does not allow.')
|
|
252
|
+
|
|
253
|
+
const nameTaken = (state: MicrosoftEmulatorState, parentId: string, name: string): boolean =>
|
|
254
|
+
childrenOf(state, parentId).some(child => child.name.toLowerCase() === name.toLowerCase())
|
|
255
|
+
|
|
256
|
+
const nextItemId = (state: MicrosoftEmulatorState): string => {
|
|
257
|
+
let number = state.counters.nextItemNumber
|
|
258
|
+
let id = `01SYNTHETICITEM${padded(number, 17)}`
|
|
259
|
+
|
|
260
|
+
while (state.driveItems.some(item => item.id === id)) {
|
|
261
|
+
number += 1
|
|
262
|
+
id = `01SYNTHETICITEM${padded(number, 17)}`
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
state.counters = { ...state.counters, nextItemNumber: number + 1 }
|
|
266
|
+
|
|
267
|
+
return id
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const nameConflict = (request: RouteRequest): Response =>
|
|
271
|
+
notEmulated(request, 'name conflicts are not emulated (the name is already taken).')
|
|
272
|
+
|
|
273
|
+
/** Only `fail`, as the fixtures send it; anything else (or none) is not emulated. */
|
|
274
|
+
const conflictBehaviorProblem = (request: RouteRequest, value: unknown): Response | undefined =>
|
|
275
|
+
value === 'fail'
|
|
276
|
+
? undefined
|
|
277
|
+
: notEmulated(
|
|
278
|
+
request,
|
|
279
|
+
'@microsoft.graph.conflictBehavior must be fail (other or missing values are not emulated).'
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* `POST /drives/{driveId}/items/{itemId}/children` with a `folder` facet and
|
|
284
|
+
* `@microsoft.graph.conflictBehavior: fail`: 201 with the folder. A taken name, and creating
|
|
285
|
+
* files here, are not emulated.
|
|
286
|
+
*/
|
|
287
|
+
export const createFolder: RouteHandler = (state, request, env) => {
|
|
288
|
+
const drive = driveProblem(state, request)
|
|
289
|
+
|
|
290
|
+
if (drive !== undefined) return drive
|
|
291
|
+
|
|
292
|
+
const parent = findItem(state, request.params.itemId ?? '')
|
|
293
|
+
|
|
294
|
+
if (parent === undefined) return notFound(request)
|
|
295
|
+
|
|
296
|
+
if (parent.kind !== 'folder') return invalidValue(request, 'the parent item is not a folder.')
|
|
297
|
+
|
|
298
|
+
const fields = bodyObject(request, ['name', 'folder', '@microsoft.graph.conflictBehavior'])
|
|
299
|
+
|
|
300
|
+
if (fields instanceof Response) return fields
|
|
301
|
+
|
|
302
|
+
if (fields.folder === undefined) {
|
|
303
|
+
return notEmulated(request, 'only folder creation (a folder facet) is emulated here.')
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const folderFacet = fields.folder
|
|
307
|
+
|
|
308
|
+
if (!isJsonObject(folderFacet) || Object.keys(folderFacet).length > 0) {
|
|
309
|
+
return notEmulated(request, 'the folder facet must be {}.')
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const name = nameProblem(request, fields.name)
|
|
313
|
+
|
|
314
|
+
if (name instanceof Response) return name
|
|
315
|
+
|
|
316
|
+
const behavior = conflictBehaviorProblem(request, fields['@microsoft.graph.conflictBehavior'])
|
|
317
|
+
|
|
318
|
+
if (behavior !== undefined) return behavior
|
|
319
|
+
|
|
320
|
+
if (nameTaken(state, parent.id, name)) return nameConflict(request)
|
|
321
|
+
|
|
322
|
+
const now = nowTimestamp(env)
|
|
323
|
+
|
|
324
|
+
const folder: MicrosoftEmulatorDriveItem = {
|
|
325
|
+
id: nextItemId(state),
|
|
326
|
+
parentId: parent.id,
|
|
327
|
+
name,
|
|
328
|
+
kind: 'folder',
|
|
329
|
+
size: 0,
|
|
330
|
+
mimeType: null,
|
|
331
|
+
quickXorHash: null,
|
|
332
|
+
createdDateTime: now,
|
|
333
|
+
lastModifiedDateTime: now
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
state.driveItems = [...state.driveItems, folder]
|
|
337
|
+
|
|
338
|
+
return jsonResponse(
|
|
339
|
+
201,
|
|
340
|
+
entity(
|
|
341
|
+
metadataContext(env, `${childrenContext(request)}/$entity`),
|
|
342
|
+
renderItem(state, env, folder)
|
|
343
|
+
)
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** `DELETE /drives/{driveId}/items/{itemId}`: 204; the item and its subtree are removed. */
|
|
348
|
+
export const deleteItem: RouteHandler = (state, request) => {
|
|
349
|
+
const drive = driveProblem(state, request)
|
|
350
|
+
|
|
351
|
+
if (drive !== undefined) return drive
|
|
352
|
+
|
|
353
|
+
const item = findItem(state, request.params.itemId ?? '')
|
|
354
|
+
|
|
355
|
+
if (item === undefined) return notFound(request)
|
|
356
|
+
|
|
357
|
+
if (item.parentId === null)
|
|
358
|
+
return notEmulated(request, 'deleting the drive root is not emulated.')
|
|
359
|
+
|
|
360
|
+
const removed = new Set(subtreeOf(state, item).map(entry => entry.id))
|
|
361
|
+
|
|
362
|
+
state.driveItems = state.driveItems.filter(entry => !removed.has(entry.id))
|
|
363
|
+
|
|
364
|
+
return emptyResponse(204)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const monitorPath = (state: MicrosoftEmulatorState, id: string) =>
|
|
368
|
+
`/personal/${personalSite(state.user)}/_api/v2.0/monitor/${id}`
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* `POST /drives/{driveId}/items/{itemId}/copy` (`{ parentReference: { driveId, id } }`, query
|
|
372
|
+
* `@microsoft.graph.conflictBehavior=fail`): 202 with an empty body and one monitor `Location`.
|
|
373
|
+
* The copy itself runs when the monitor reports completion. Only what the copy fixture sends is
|
|
374
|
+
* emulated: a file source, a destination on the same drive named by `driveId` and `id`, and the
|
|
375
|
+
* source's name (a new `name`, a folder source, or a missing `driveId` fail closed).
|
|
376
|
+
*/
|
|
377
|
+
export const copyItem: RouteHandler = (state, request, env) => {
|
|
378
|
+
const drive = driveProblem(state, request)
|
|
379
|
+
|
|
380
|
+
if (drive !== undefined) return drive
|
|
381
|
+
|
|
382
|
+
const source = findItem(state, request.params.itemId ?? '')
|
|
383
|
+
|
|
384
|
+
if (source === undefined) return notFound(request)
|
|
385
|
+
|
|
386
|
+
if (source.kind !== 'file') {
|
|
387
|
+
return notEmulated(request, 'copying a folder is not emulated (the fixture copies a file).')
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const behavior = conflictBehaviorProblem(
|
|
391
|
+
request,
|
|
392
|
+
request.query.get('@microsoft.graph.conflictBehavior') ?? undefined
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
if (behavior !== undefined) return behavior
|
|
396
|
+
|
|
397
|
+
// A new `name` fails here too: the fixture's copy keeps the source's name.
|
|
398
|
+
const fields = bodyObject(request, ['parentReference'])
|
|
399
|
+
|
|
400
|
+
if (fields instanceof Response) return fields
|
|
401
|
+
|
|
402
|
+
const reference = nestedObject(
|
|
403
|
+
request,
|
|
404
|
+
fields.parentReference,
|
|
405
|
+
['driveId', 'id'],
|
|
406
|
+
'parentReference { driveId, id }'
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
if (reference instanceof Response) return reference
|
|
410
|
+
|
|
411
|
+
if (reference.driveId === undefined) {
|
|
412
|
+
return notEmulated(request, 'copies without parentReference.driveId are not emulated.')
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (!Predicate.isString(reference.id) || !Predicate.isString(reference.driveId)) {
|
|
416
|
+
return invalidValue(request, 'parentReference must be { driveId, id } with string values.')
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (reference.driveId !== state.drive.id) {
|
|
420
|
+
return notEmulated(request, 'copies to another drive are not emulated.')
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const destination = findItem(state, reference.id)
|
|
424
|
+
|
|
425
|
+
if (destination === undefined) return notFound(request)
|
|
426
|
+
|
|
427
|
+
if (destination.kind !== 'folder') {
|
|
428
|
+
return invalidValue(request, 'the destination is not a folder.')
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const number = env.monitorCounter.next
|
|
432
|
+
|
|
433
|
+
env.monitorCounter.next += 1
|
|
434
|
+
|
|
435
|
+
const monitor: CopyMonitor = {
|
|
436
|
+
id: `00000000-0000-4000-8000-${padded(number, 12)}`,
|
|
437
|
+
sourceId: source.id,
|
|
438
|
+
destinationParentId: destination.id,
|
|
439
|
+
pollsLeft: env.copyInProgressPolls,
|
|
440
|
+
resourceId: undefined
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
env.monitors.set(monitor.id, monitor)
|
|
444
|
+
|
|
445
|
+
return emptyResponse(202, {
|
|
446
|
+
location: `${env.sharePointOrigin}${monitorPath(state, monitor.id)}`
|
|
447
|
+
})
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Copy the file `item` under `parentId` with a fresh id and its name; returns the new id. */
|
|
451
|
+
const copyFile = (
|
|
452
|
+
state: MicrosoftEmulatorState,
|
|
453
|
+
env: MicrosoftApiEnv,
|
|
454
|
+
item: MicrosoftEmulatorDriveItem,
|
|
455
|
+
parentId: string
|
|
456
|
+
): string => {
|
|
457
|
+
const now = nowTimestamp(env)
|
|
458
|
+
|
|
459
|
+
const copy: MicrosoftEmulatorDriveItem = {
|
|
460
|
+
...item,
|
|
461
|
+
id: nextItemId(state),
|
|
462
|
+
parentId,
|
|
463
|
+
createdDateTime: now,
|
|
464
|
+
lastModifiedDateTime: now
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
state.driveItems = [...state.driveItems, copy]
|
|
468
|
+
|
|
469
|
+
return copy.id
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Run a monitored copy: its `resourceId`, or why it cannot run (the source or destination is
|
|
474
|
+
* gone, or the name is taken; no fixture records a failed copy).
|
|
475
|
+
*/
|
|
476
|
+
const runCopy = (
|
|
477
|
+
state: MicrosoftEmulatorState,
|
|
478
|
+
env: MicrosoftApiEnv,
|
|
479
|
+
monitor: CopyMonitor
|
|
480
|
+
): string | { readonly problem: string } => {
|
|
481
|
+
const source = findItem(state, monitor.sourceId)
|
|
482
|
+
const destination = findItem(state, monitor.destinationParentId)
|
|
483
|
+
|
|
484
|
+
if (source === undefined || destination === undefined || destination.kind !== 'folder') {
|
|
485
|
+
return { problem: 'the copy source or destination no longer exists' }
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (nameTaken(state, destination.id, source.name)) {
|
|
489
|
+
return { problem: 'the copy name is already taken in the destination' }
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return copyFile(state, env, source, destination.id)
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const monitorContentType = 'application/json;odata.metadata=minimal;odata.streaming=true'
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* `GET /personal/{site}/_api/v2.0/monitor/{monitorId}` on the SharePoint origin (no credentials
|
|
499
|
+
* needed, like the real capability URL): `inProgress` (202) while polls remain, then the copy runs
|
|
500
|
+
* once and every later poll answers `completed` (200, `resourceId`). A copy that cannot run
|
|
501
|
+
* answers 400 not emulated (failed copies are not emulated) and stays pending.
|
|
502
|
+
*/
|
|
503
|
+
export const copyMonitor: RouteHandler = (state, request, env) => {
|
|
504
|
+
const monitor = env.monitors.get(request.params.monitorId ?? '')
|
|
505
|
+
|
|
506
|
+
if (monitor === undefined || request.params.site !== personalSite(state.user)) {
|
|
507
|
+
return notFound(request)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const context = `${env.sharePointOrigin}/personal/${personalSite(state.user)}/_api/v2.0/$metadata#oneDrive.asynchronousOperationStatus`
|
|
511
|
+
const headers = { 'content-type': monitorContentType }
|
|
512
|
+
|
|
513
|
+
if (monitor.resourceId === undefined && monitor.pollsLeft > 0) {
|
|
514
|
+
monitor.pollsLeft -= 1
|
|
515
|
+
|
|
516
|
+
return jsonResponse(
|
|
517
|
+
202,
|
|
518
|
+
{ '@odata.context': context, percentageComplete: 0, status: 'inProgress' },
|
|
519
|
+
headers
|
|
520
|
+
)
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const resourceId = monitor.resourceId ?? runCopy(state, env, monitor)
|
|
524
|
+
|
|
525
|
+
if (!Predicate.isString(resourceId)) {
|
|
526
|
+
return notEmulated(request, `${resourceId.problem} (failed copies are not emulated).`)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
monitor.resourceId = resourceId
|
|
530
|
+
|
|
531
|
+
return jsonResponse(
|
|
532
|
+
200,
|
|
533
|
+
{
|
|
534
|
+
'@odata.context': context,
|
|
535
|
+
percentageComplete: 100,
|
|
536
|
+
resourceId,
|
|
537
|
+
status: 'completed'
|
|
538
|
+
},
|
|
539
|
+
headers
|
|
540
|
+
)
|
|
541
|
+
}
|