@zapier/kitcore 0.0.0 → 0.4.0
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/CHANGELOG.md +37 -0
- package/LICENSE +2 -0
- package/README.md +382 -2
- package/dist/index.cjs +3050 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.mts +2907 -0
- package/dist/index.d.ts +2907 -0
- package/dist/index.mjs +2962 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +53 -5
- package/index.mjs +0 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# @zapier/kitcore
|
|
2
|
+
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 5e2cf0d: Add the resolution controller: a sibling layer over a built SDK that completes a method's partial input into a fully validated one, interacting with a host only when needed. `createController(sdk)` exposes a wizard face (`resolve` / `start` / `step` over a serializable protocol) and a serializable reflection face (`listMethods` / `getMethod` / `listChoices`) that projects the registry to plain JSON (JSON Schema types, positional, output) without leaking zod. Adds the `defineResolver` authoring API and the signal hierarchy (`CoreSignal`, `CoreCancelledSignal`, `isCoreSignal`).
|
|
8
|
+
|
|
9
|
+
## 0.3.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- f5466d7: Publish kitcore publicly as `@zapier/kitcore`.
|
|
14
|
+
|
|
15
|
+
The package was previously private and internal-only. It is now published under the `@zapier` scope. It remains bundled into `@zapier/zapier-sdk` (unchanged); this additionally makes it available as a standalone package.
|
|
16
|
+
|
|
17
|
+
## 0.2.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- b8ff1dd: Refactor the SDKs onto kitcore's new internal plugin model.
|
|
22
|
+
|
|
23
|
+
kitcore (an internal building block) gained a new plugin model, and the SDKs were rebuilt on it behind a compatibility bridge, so existing behavior is unchanged. The only changes to the published packages' surface:
|
|
24
|
+
- Removed the deprecated `sdk.addPlugin(...)` chain method (and the `WithAddPlugin` type / chain-style no-arg `createSdk()` doorway). Extend a built SDK with the top-level `addPlugin(sdk, plugin)` instead.
|
|
25
|
+
- `@zapier/zapier-sdk` now re-exports the plugin-authoring helpers (`createSdk`, `defineMethod` / `definePlugin` / `declareMethod` / etc., `selectExports`, `addPlugin`). Note `createSdk` reuses the old doorway's name but now takes a root plugin, so a stale no-arg `createSdk()` call is a compile error rather than silent misbehavior.
|
|
26
|
+
|
|
27
|
+
## 0.1.0
|
|
28
|
+
|
|
29
|
+
### Minor Changes
|
|
30
|
+
|
|
31
|
+
- 571f90d: Add zapier-sdk-package-operation header to identify the caller operation
|
|
32
|
+
|
|
33
|
+
## 0.0.1
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- da80c77: Fix paginated list results silently truncating to page 1 when the same result is consumed twice (iterating pages then `.items()`, `.items()` then pages, or iterating either one twice). Pages and items are now two views over one page stream, so consuming either drains the other: the second view yields nothing instead of silently replaying page 1. A paginated result is consumed once; call the method again for a fresh result. Awaiting the result to read just the first page is unaffected: it is a repeatable peek that does not start the stream, so awaiting and then iterating still works.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
Copyright (c) Zapier, Inc.
|
|
2
|
+
The Zapier SDK is part of Zapier's services. By downloading, installing, accessing, or using any part of the Zapier SDK you agree to the Zapier Terms of Service, which can be found at: https://zapier.com/tos, or such other agreement between you and Zapier governing Zapier services (as applicable). If you do not agree to the Zapier Terms of Service (or do not have another governing agreement in place with Zapier), you may not download, install, access, or use the Zapier SDK.
|
package/README.md
CHANGED
|
@@ -1,5 +1,385 @@
|
|
|
1
1
|
# @zapier/kitcore
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A TypeScript framework for building plugin-driven SDKs.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
kitcore lets you assemble an SDK out of small, independent units called **plugins**. A plugin is shaped like an ES module: it has an identity, declares the other plugins it **imports**, and **exports** named members. You compose plugins by importing and re-exporting them, and `createSdk` materializes the whole graph into a finished, fully typed SDK object.
|
|
6
|
+
|
|
7
|
+
The design goals:
|
|
8
|
+
|
|
9
|
+
- **Fully typed authoring.** Inside a method, both its private `state` and its `imports` are inferred with no manual annotations.
|
|
10
|
+
- **Introspectable without running anything.** A plugin's contract (its methods, input schemas, output modes, metadata) is plain data, so a CLI, MCP server, or docs generator can walk it without invoking your code.
|
|
11
|
+
- **Low ceremony.** No base classes, no decorators, no DI container. You write functions and arrays.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install @zapier/kitcore zod
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`zod` is a peer dependency (used for input validation and schema-driven typing).
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
The smallest SDK is a single method. `defineMethod` describes it; `createSdk` builds it.
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
import { defineMethod, createSdk } from "@zapier/kitcore";
|
|
28
|
+
|
|
29
|
+
const greet = defineMethod({
|
|
30
|
+
name: "greet",
|
|
31
|
+
inputSchema: z.object({ name: z.string() }),
|
|
32
|
+
run: ({ input }) => `Hi ${input.name}`,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const sdk = createSdk(greet);
|
|
36
|
+
sdk.greet({ name: "Ada" }); // "Hi Ada"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`inputSchema` is optional. When present it validates the call at the boundary and types `input` for you, so `run` needs no annotations.
|
|
40
|
+
|
|
41
|
+
## Plugins are modules
|
|
42
|
+
|
|
43
|
+
There are three kinds of plugin, and an SDK is just a materialized plugin:
|
|
44
|
+
|
|
45
|
+
- **Method** (`defineMethod`) — a leaf that _is_ a function.
|
|
46
|
+
- **Property** (`defineProperty`) — a leaf that _is_ a value.
|
|
47
|
+
- **Aggregate** (`definePlugin`) — re-exports other plugins under binding names.
|
|
48
|
+
|
|
49
|
+
A plugin declares `imports` (an array of the plugins it depends on) and receives them as a flat `imports` bag, with each import bound under its own name. The two ends share the word the way ES modules do: `import { x } from "y"` is the declaration, `x` is the binding.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
const transport = defineMethod({
|
|
53
|
+
name: "transport",
|
|
54
|
+
run: ({ input }) => fetch(input.url),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const getUser = defineMethod({
|
|
58
|
+
name: "getUser",
|
|
59
|
+
imports: [transport], // depend on transport
|
|
60
|
+
run: ({ imports, input }) => imports.transport({ url: `/users/${input.id}` }),
|
|
61
|
+
});
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Importing a plugin pulls it into the graph but keeps it private. It only becomes part of the SDK surface if an aggregate **exports** it.
|
|
65
|
+
|
|
66
|
+
## Composing an SDK
|
|
67
|
+
|
|
68
|
+
`definePlugin` builds an aggregate. Its `exports` array decides the public surface: a leaf binds under its own name, and a nested aggregate spreads its bindings.
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { definePlugin, createSdk } from "@zapier/kitcore";
|
|
72
|
+
|
|
73
|
+
const api = definePlugin({
|
|
74
|
+
name: "api",
|
|
75
|
+
exports: [getUser, fetch], // surface getUser + fetch; transport stays private
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const sdk = createSdk(api);
|
|
79
|
+
sdk.getUser({ id: 1 });
|
|
80
|
+
sdk.fetch(url);
|
|
81
|
+
sdk.transport; // undefined — imported by getUser, never exported
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Subsetting and renaming with `selectExports`
|
|
85
|
+
|
|
86
|
+
`selectExports` is the `{ a, b as c }` clause. It picks a subset of a module's exports and optionally renames them. It works the same on the `exports` side (what you re-export) and the `imports` side (what a body sees).
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { selectExports } from "@zapier/kitcore";
|
|
90
|
+
|
|
91
|
+
const sdk = createSdk(
|
|
92
|
+
definePlugin({
|
|
93
|
+
name: "users",
|
|
94
|
+
// re-export getUser under its own name, and listUsers as `listAll`
|
|
95
|
+
exports: [selectExports(users, "getUser", { listAll: "listUsers" })],
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
sdk.getUser({ id: 1 });
|
|
99
|
+
sdk.listAll();
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
If two different plugins try to bind the same name, kitcore throws and points you at `selectExports` to rename one. Composition is dependency-based, so registration order never matters.
|
|
103
|
+
|
|
104
|
+
## State and effects: `setup`
|
|
105
|
+
|
|
106
|
+
`setup` is a per-build constructor. It runs once when `createSdk` materializes the plugin (dependencies first), may perform side effects, and returns private state handed to `run` as `bag.state`.
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
const counter = defineMethod({
|
|
110
|
+
name: "next",
|
|
111
|
+
setup: () => ({ n: 0 }), // runs once at createSdk
|
|
112
|
+
run: ({ state }) => ++state.n, // same state across calls
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const sdk = createSdk(counter);
|
|
116
|
+
sdk.next(); // 1
|
|
117
|
+
sdk.next(); // 2
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`setup` can read `imports`, so state can be built from dependencies. Because state lives behind `setup`, several small "state plugins" can each own a slice instead of one monolith.
|
|
121
|
+
|
|
122
|
+
## Properties
|
|
123
|
+
|
|
124
|
+
A property is a value rather than a function. It can be a static `value`, or a **live** `get` re-derived on every read, with an optional `setup` building the state `get` returns. So `setup` + `get` mirrors a method's `setup` + `run`.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
import { defineProperty } from "@zapier/kitcore";
|
|
128
|
+
|
|
129
|
+
// static value
|
|
130
|
+
const version = defineProperty({ name: "version", value: "1.0.0" });
|
|
131
|
+
|
|
132
|
+
// built once, returned live
|
|
133
|
+
const apps = defineProperty({
|
|
134
|
+
name: "apps",
|
|
135
|
+
imports: [runAction, fetch],
|
|
136
|
+
setup: ({ imports }) => buildAppsProxy(imports), // once at createSdk
|
|
137
|
+
get: ({ state }) => state, // returned per read
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Use `setup` + `get` when construction is expensive and should happen once; compute directly in `get` (no `setup`) when the value is cheap and you want it fresh on each access.
|
|
142
|
+
|
|
143
|
+
## Sharing state between plugins
|
|
144
|
+
|
|
145
|
+
State doesn't have to live inside one plugin. A property whose `setup` builds a value once becomes a small **state plugin**: every plugin that imports it receives the same instance, so state is shared without a global or a monolithic context object.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
// A state plugin: one Map, built once at createSdk.
|
|
149
|
+
const cache = defineProperty({
|
|
150
|
+
name: "cache",
|
|
151
|
+
setup: () => new Map<string, unknown>(),
|
|
152
|
+
get: ({ state }) => state, // every importer gets the same Map
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const write = defineMethod({
|
|
156
|
+
name: "write",
|
|
157
|
+
imports: [cache],
|
|
158
|
+
run: ({ imports, input }) => imports.cache.set(input.key, input.value),
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const read = defineMethod({
|
|
162
|
+
name: "read",
|
|
163
|
+
imports: [cache],
|
|
164
|
+
run: ({ imports, input }) => imports.cache.get(input.key),
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const sdk = createSdk(definePlugin({ name: "store", exports: [write, read] }));
|
|
168
|
+
sdk.write({ key: "a", value: 1 });
|
|
169
|
+
sdk.read({ key: "a" }); // 1 — the same Map, shared across both methods
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Prefer several focused state plugins over one big shared object: each owns a slice, dependents declare exactly the state they touch, and you can swap or mock one slice in tests without disturbing the rest.
|
|
173
|
+
|
|
174
|
+
## Output modes
|
|
175
|
+
|
|
176
|
+
A method's `output` mode shapes what `run` returns into the public surface:
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
// raw (default): the surface is whatever run returns
|
|
180
|
+
const transport = defineMethod({
|
|
181
|
+
name: "transport",
|
|
182
|
+
run: ({ input }) => fetch(input.url),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// item: run returns the value; the framework wraps it in { data }
|
|
186
|
+
const getApp = defineMethod({ name: "getApp", output: "item", run: () => app });
|
|
187
|
+
const { data } = await sdk.getApp({ app: "slack" });
|
|
188
|
+
|
|
189
|
+
// list: run returns one page; the surface is a paginated iterable
|
|
190
|
+
const listApps = defineMethod({
|
|
191
|
+
name: "listApps",
|
|
192
|
+
output: { type: "list", adaptPage, defaultPageSize: 100 },
|
|
193
|
+
run: ({ input }) => api.get("/apps", { offset: input.cursor }),
|
|
194
|
+
});
|
|
195
|
+
for await (const app of sdk.listApps().items()) {
|
|
196
|
+
/* every app across pages */
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Positional methods
|
|
201
|
+
|
|
202
|
+
By default a method takes a single options object. `positional` projects named input keys onto an ordered argument list for the public call, while validation, middleware, and `run` still see the canonical `{ input }`.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
const fetchMethod = defineMethod({
|
|
206
|
+
name: "fetch",
|
|
207
|
+
inputSchema: z.object({ url: z.string(), init: z.object({}).optional() }),
|
|
208
|
+
positional: ["url", "init"],
|
|
209
|
+
run: ({ input }) => doFetch(input.url, input.init),
|
|
210
|
+
});
|
|
211
|
+
sdk.fetch("https://example.com", { method: "GET" });
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Middleware
|
|
215
|
+
|
|
216
|
+
An aggregate can wrap the methods it imports. A `middleware` entry is keyed by the import binding it wraps and receives `{ imports, next, input }`. It must preserve the target's contract (enforced by the types), so it cannot change the public signature.
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
const auth = definePlugin({
|
|
220
|
+
name: "auth",
|
|
221
|
+
imports: [transport, credentials],
|
|
222
|
+
middleware: {
|
|
223
|
+
transport: ({ imports, next, input }) =>
|
|
224
|
+
next({
|
|
225
|
+
...input,
|
|
226
|
+
headers: withAuth(input.headers, imports.credentials()),
|
|
227
|
+
}),
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const retry = definePlugin({
|
|
232
|
+
name: "retry",
|
|
233
|
+
imports: [transport, auth], // import auth so retry nests around it
|
|
234
|
+
middleware: { transport: ({ next, input }) => withRetry(() => next(input)) },
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
A method that imports `transport` knows nothing about `auth` or `retry`; it just calls `imports.transport(...)` and the chain runs automatically. Nesting follows the dependency edges (`retry` imports `auth`, so it wraps around it), not registration order, and `next(input)` runs the next layer.
|
|
239
|
+
|
|
240
|
+
## Dependency injection
|
|
241
|
+
|
|
242
|
+
A plugin "names what it needs and receives it" without knowing who supplied it. That makes test doubles and configured providers trivial: register a real implementation under the same id and dependents get it transparently. This is the normal way one plugin reaches another, import the plugin, read it from the `imports` bag.
|
|
243
|
+
|
|
244
|
+
### The context escape hatch (avoid)
|
|
245
|
+
|
|
246
|
+
There is a built-in `dangerousContextPlugin` that hands you the SDK's raw internal context (the live plugin graph plus legacy compatibility fields):
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import { dangerousContextPlugin } from "@zapier/kitcore";
|
|
250
|
+
|
|
251
|
+
const whoami = defineMethod({
|
|
252
|
+
name: "whoami",
|
|
253
|
+
imports: [dangerousContextPlugin],
|
|
254
|
+
run: ({ imports }) => Object.keys(imports.context.plugins),
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Steer clear of it.** The context shape is an internal implementation detail and may change without notice, so anything built on it is fragile. Import the specific plugins you need instead, and use `getRegistryPlugin` (below) for surface introspection. The `dangerous` prefix is there to make the risk loud at every import site; it exists mainly so a not-yet-migrated plugin can reach legacy state during a transition.
|
|
259
|
+
|
|
260
|
+
## Extending a built SDK
|
|
261
|
+
|
|
262
|
+
`addPlugin` materializes a plugin into an already-built SDK in place. A TypeScript assertion-function signature widens the existing binding to include the new plugin's contributions, so you don't need a new variable.
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
import { addPlugin } from "@zapier/kitcore";
|
|
266
|
+
|
|
267
|
+
addPlugin(sdk, defineMethod({ name: "ping", run: () => "pong" }));
|
|
268
|
+
sdk.ping(); // "pong", typed
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Introspection
|
|
272
|
+
|
|
273
|
+
Re-export the built-in `getRegistryPlugin` to put a `getRegistry()` method on the SDK. It reports the live surface as plain data (one entry per binding, with the leaf's metadata), which is what drives generated docs, CLI commands, and MCP tools.
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
import { getRegistryPlugin } from "@zapier/kitcore";
|
|
277
|
+
|
|
278
|
+
const sdk = createSdk(
|
|
279
|
+
definePlugin({ name: "api", exports: [greet, getRegistryPlugin] }),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const registry = sdk.getRegistry();
|
|
283
|
+
registry.functions; // [{ name, description, inputSchema, ... }]
|
|
284
|
+
registry.categories; // grouped for menus / docs
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Because the registry reads the surface at call time, it also reflects anything added later with `addPlugin`.
|
|
288
|
+
|
|
289
|
+
## Resolving inputs: controllers
|
|
290
|
+
|
|
291
|
+
A method's `inputSchema` says what a complete call looks like, but a caller often starts with only part of it. A **resolution controller** is a sibling layer over a built SDK that turns a partial input into a complete, validated one, asking a host (a CLI prompt, a web form, an agent) for each missing piece only when it has to. The SDK surface is untouched; the controller reads its schemas and resolvers and drives them.
|
|
292
|
+
|
|
293
|
+
You make a parameter resolvable by attaching a `defineResolver` to it on the method. A resolver can offer a static enum (derived from the schema), fetch a dynamic list (with pagination and search), resolve without a prompt (a configured default), or describe an object/array to fill in field by field.
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
import { defineMethod, defineResolver, createSdk } from "@zapier/kitcore";
|
|
297
|
+
|
|
298
|
+
const appResolver = defineResolver({
|
|
299
|
+
// a paginated, searchable picker
|
|
300
|
+
listItems: ({ input, search, cursor }) => api.listApps({ search, cursor }),
|
|
301
|
+
prompt: ({ items }) => ({
|
|
302
|
+
message: "Which app?",
|
|
303
|
+
choices: items.map((a) => ({ label: a.title, value: a.key })),
|
|
304
|
+
}),
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const runAction = defineMethod({
|
|
308
|
+
name: "runAction",
|
|
309
|
+
inputSchema: z.object({ app: z.string(), action: z.string() }),
|
|
310
|
+
resolvers: { app: appResolver },
|
|
311
|
+
run: ({ input }) => api.run(input),
|
|
312
|
+
});
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
`createController(sdk)` exposes two faces over the same engine.
|
|
316
|
+
|
|
317
|
+
### Wizard face: `resolve`
|
|
318
|
+
|
|
319
|
+
`resolve` is in-process sugar: it loops the resolution against an `answer` callback and returns the finished input. The callback's only job is to render a question and return the chosen action; all resolution logic (fetching, pagination, search, validation, nesting) stays in the engine.
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
const controller = createController(sdk);
|
|
323
|
+
|
|
324
|
+
const input = await controller.resolve({
|
|
325
|
+
method: "runAction",
|
|
326
|
+
input: { action: "send_message" }, // seed what you already have
|
|
327
|
+
answer: async ({ state, result }) => {
|
|
328
|
+
// render result.question (a select / input / collection) however you like,
|
|
329
|
+
// then map the user's choice back to an action:
|
|
330
|
+
return { type: "choose", value: "slack" };
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
await sdk.runAction(input); // complete + validated
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Serializable protocol: `start` / `step`
|
|
338
|
+
|
|
339
|
+
For a host that spans a client/server boundary (web, agent, MCP), drive the protocol directly. `start` returns the first `{ state, result }`; you render `result`, then send the user's action plus the `state` back through `step`. `state` is plain JSON, so it round-trips over the wire with no live objects to keep alive between turns.
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
let { state, result } = await controller.start({ method: "runAction" });
|
|
343
|
+
while (result.status === "ask") {
|
|
344
|
+
const action = await renderAndCollect(result.question); // your host
|
|
345
|
+
({ state, result } = await controller.step({ state, action }));
|
|
346
|
+
}
|
|
347
|
+
// result.status is now "done" | "invalid" | "cancelled"
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Reflection face: `describe` / `listChoices`
|
|
351
|
+
|
|
352
|
+
For a form-style host that renders every field up front instead of one question at a time, `describe` returns the static shape of a method's inputs (required-ness, value type, which are dynamic, their dependencies), and `listChoices` enumerates the legal values for one dynamic parameter.
|
|
353
|
+
|
|
354
|
+
```ts
|
|
355
|
+
controller.describe({ method: "runAction" });
|
|
356
|
+
// { app: { required: true, dynamic: true }, action: { required: true, ... } }
|
|
357
|
+
|
|
358
|
+
await controller.listChoices({
|
|
359
|
+
method: "runAction",
|
|
360
|
+
parameter: "app",
|
|
361
|
+
search: "sl",
|
|
362
|
+
});
|
|
363
|
+
// { data: [{ label: "Slack", value: "slack" }], nextCursor }
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Stand-ins
|
|
367
|
+
|
|
368
|
+
When a plugin depends on something supplied elsewhere (a configured client, a test double), declare a typed stand-in with `declareMethod` / `declareProperty` (a single leaf) or `declarePlugin` (a whole module, with its export surface declared as leaf stand-ins). Dependents reference it for typing; at `createSdk` it is satisfied by whatever real plugin is registered under the same id, and an unsatisfied stand-in is a **compile-time** error (with a runtime backstop). You reference a stand-in by `id` (`namespace/name`, or a bare name), and the contract is provided as explicit type arguments (`declareMethod<"fetch", Input, Output>({ id: "fetch" })`), because the id must be a literal the dependency ledger can read. The binding is the id's last segment.
|
|
369
|
+
|
|
370
|
+
## Namespaces
|
|
371
|
+
|
|
372
|
+
Plugin ids are `name` by default, or `namespace/name` when you set the optional `namespace` field. Namespacing lets two packages define a plugin with the same bare name without their ids colliding; the surface and imports stay bare-named, so consumers are unaffected.
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
const greet = defineMethod({
|
|
376
|
+
name: "greet",
|
|
377
|
+
namespace: "acme",
|
|
378
|
+
run: () => "hi",
|
|
379
|
+
});
|
|
380
|
+
// id is "acme/greet"; sdk.greet() is still the call
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
## Status
|
|
384
|
+
|
|
385
|
+
Pre-release. The public surface is being stabilized as part of an extraction from [`@zapier/zapier-sdk`](https://www.npmjs.com/package/@zapier/zapier-sdk). Until a `1.0.0` release, expect breaking changes between minor versions.
|