@zapier/kitcore 0.5.1 → 0.7.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 +20 -0
- package/README.md +7 -2
- package/dist/index.cjs +200 -115
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +154 -39
- package/dist/index.d.ts +154 -39
- package/dist/index.mjs +200 -115
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @zapier/kitcore
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 615fda9: The resolution controller now paginates dynamic selects as a page window: each `select` ask carries ONE page of choices plus a `page: { generation, index }` descriptor, so payloads stay proportional to a page.
|
|
8
|
+
- `next_page` and `previous_page` actions replace `more`.
|
|
9
|
+
- State carries coordinates only: `ControllerPagination` holds the cursor trail, never items, so the host carries cursors back between steps instead of the items it was just sent. An accumulating host appends pages client-side keyed on the question's `path` + `generation`, which increments whenever a `search` restarts the listing.
|
|
10
|
+
- Every question carries `path` (which field it is for), so hosts never read the state.
|
|
11
|
+
- Resolver `validate` is now a top-level callback (`validate({ imports, value, input, context })`), async and able to verify against the source; a thrown validate surfaces as a `failed` result with retry, not a rejection. A `validate` inside the prompt config is a type error (`ResolverPromptConfig` omits it), so it can't silently never run.
|
|
12
|
+
- The `exhausted` flag is removed; end-of-list is the absence of `next_page`.
|
|
13
|
+
- A failed fetch records its position so `retry` replays exactly the page that failed.
|
|
14
|
+
|
|
15
|
+
- e6b22fb: `concatPaginated` now paginates with stateless cursors: each source is called with its own cursor per page (`({ cursor }) => ...`), outgoing cursors encode the source index plus that source's cursor, and a new `cursor` option resumes the stream from a previous page's cursor with no shared in-memory state. The `dedupe` option is removed; sources must be disjoint by construction.
|
|
16
|
+
|
|
17
|
+
## 0.6.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- 197a125: Breaking for plugin authors: an `output: "item"` method's `run` now returns the `{ data: item }` envelope itself (strictly `data`, no extra keys), symmetric with list's `SdkPage`, instead of returning the bare item for the framework to wrap. The public call surface is unchanged: callers still get `Promise<{ data: item }>`.
|
|
22
|
+
|
|
3
23
|
## 0.5.1
|
|
4
24
|
|
|
5
25
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -183,8 +183,13 @@ const transport = defineMethod({
|
|
|
183
183
|
run: ({ input }) => fetch(input.url),
|
|
184
184
|
});
|
|
185
185
|
|
|
186
|
-
// item: run returns the
|
|
187
|
-
|
|
186
|
+
// item: run returns the { data } envelope itself (data and nothing else),
|
|
187
|
+
// symmetric with list's page shape
|
|
188
|
+
const getApp = defineMethod({
|
|
189
|
+
name: "getApp",
|
|
190
|
+
output: "item",
|
|
191
|
+
run: () => ({ data: app }),
|
|
192
|
+
});
|
|
188
193
|
const { data } = await sdk.getApp({ app: "slack" });
|
|
189
194
|
|
|
190
195
|
// list: run returns one page; the surface is a paginated iterable
|
package/dist/index.cjs
CHANGED
|
@@ -301,7 +301,8 @@ function getCoreErrorCause(value) {
|
|
|
301
301
|
var CURSOR_VERSION = 1;
|
|
302
302
|
var CURSOR_SOURCE = {
|
|
303
303
|
API: "api",
|
|
304
|
-
SDK: "sdk"
|
|
304
|
+
SDK: "sdk",
|
|
305
|
+
CONCAT: "concat"
|
|
305
306
|
};
|
|
306
307
|
function encodeBase64(str) {
|
|
307
308
|
return btoa(
|
|
@@ -521,10 +522,32 @@ async function* paginateBuffered(pageFunction, pageOptions) {
|
|
|
521
522
|
}
|
|
522
523
|
}
|
|
523
524
|
var paginate = paginateBuffered;
|
|
525
|
+
function encodeConcatCursor(index, cursor) {
|
|
526
|
+
const envelope = {
|
|
527
|
+
v: CURSOR_VERSION,
|
|
528
|
+
source: CURSOR_SOURCE.CONCAT,
|
|
529
|
+
index,
|
|
530
|
+
cursor
|
|
531
|
+
};
|
|
532
|
+
return encodeBase64(JSON.stringify(envelope));
|
|
533
|
+
}
|
|
534
|
+
function decodeConcatCursor(incoming) {
|
|
535
|
+
if (!incoming) {
|
|
536
|
+
return { index: 0, cursor: void 0 };
|
|
537
|
+
}
|
|
538
|
+
try {
|
|
539
|
+
const envelope = JSON.parse(decodeBase64(incoming));
|
|
540
|
+
if (envelope.v === CURSOR_VERSION && envelope.source === CURSOR_SOURCE.CONCAT && typeof envelope.index === "number") {
|
|
541
|
+
return { index: envelope.index, cursor: envelope.cursor };
|
|
542
|
+
}
|
|
543
|
+
} catch {
|
|
544
|
+
}
|
|
545
|
+
return { index: 0, cursor: incoming };
|
|
546
|
+
}
|
|
524
547
|
function concatPaginated({
|
|
525
548
|
sources,
|
|
526
|
-
|
|
527
|
-
|
|
549
|
+
pageSize = 100,
|
|
550
|
+
cursor
|
|
528
551
|
}) {
|
|
529
552
|
if (sources.length === 0) {
|
|
530
553
|
const empty = { data: [] };
|
|
@@ -534,40 +557,24 @@ function concatPaginated({
|
|
|
534
557
|
}
|
|
535
558
|
});
|
|
536
559
|
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
if (!
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
}
|
|
546
|
-
const next = await currentIterator.next();
|
|
547
|
-
if (next.done) {
|
|
548
|
-
sourceIndex++;
|
|
549
|
-
currentIterator = null;
|
|
560
|
+
const pageFunction = async (options) => {
|
|
561
|
+
let { index, cursor: sourceCursor } = decodeConcatCursor(options.cursor);
|
|
562
|
+
while (index < sources.length) {
|
|
563
|
+
const page = await sources[index]({ cursor: sourceCursor });
|
|
564
|
+
const hasMoreInSource = page.nextCursor != null;
|
|
565
|
+
if (page.data.length === 0 && !hasMoreInSource) {
|
|
566
|
+
index++;
|
|
567
|
+
sourceCursor = void 0;
|
|
550
568
|
continue;
|
|
551
569
|
}
|
|
552
|
-
let items = next.value.data;
|
|
553
|
-
if (dedupe) {
|
|
554
|
-
if (sourceIndex > 0) {
|
|
555
|
-
items = items.filter((item) => !seen.has(dedupe(item)));
|
|
556
|
-
}
|
|
557
|
-
for (const item of items) {
|
|
558
|
-
seen.add(dedupe(item));
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
const hasMoreInSource = next.value.nextCursor != null;
|
|
562
|
-
const hasMoreSources = sourceIndex < sources.length - 1;
|
|
563
570
|
return {
|
|
564
|
-
data:
|
|
565
|
-
nextCursor: hasMoreInSource
|
|
571
|
+
data: page.data,
|
|
572
|
+
nextCursor: hasMoreInSource ? encodeConcatCursor(index, page.nextCursor) : index < sources.length - 1 ? encodeConcatCursor(index + 1, void 0) : void 0
|
|
566
573
|
};
|
|
567
574
|
}
|
|
568
575
|
return { data: [] };
|
|
569
576
|
};
|
|
570
|
-
const iterator = paginateBuffered(pageFunction, { pageSize });
|
|
577
|
+
const iterator = paginateBuffered(pageFunction, { pageSize, cursor });
|
|
571
578
|
const firstPagePromise = iterator.next().then((result) => {
|
|
572
579
|
if (result.done) {
|
|
573
580
|
return { data: [] };
|
|
@@ -1448,6 +1455,7 @@ function defineResolver(config) {
|
|
|
1448
1455
|
getContext: config.getContext,
|
|
1449
1456
|
listItems: config.listItems,
|
|
1450
1457
|
prompt: config.prompt,
|
|
1458
|
+
validate: config.validate,
|
|
1451
1459
|
tryResolveWithoutPrompt: config.tryResolveWithoutPrompt,
|
|
1452
1460
|
tryResolveFromSearch: config.tryResolveFromSearch
|
|
1453
1461
|
};
|
|
@@ -2106,6 +2114,7 @@ function bindResolver(resolver, plugins) {
|
|
|
2106
2114
|
const {
|
|
2107
2115
|
getContext: getContext2,
|
|
2108
2116
|
listItems,
|
|
2117
|
+
validate,
|
|
2109
2118
|
tryResolveWithoutPrompt,
|
|
2110
2119
|
tryResolveFromSearch
|
|
2111
2120
|
} = resolver;
|
|
@@ -2119,6 +2128,9 @@ function bindResolver(resolver, plugins) {
|
|
|
2119
2128
|
};
|
|
2120
2129
|
if (getContext2)
|
|
2121
2130
|
bound.getContext = ({ input }) => getContext2({ imports, input });
|
|
2131
|
+
if (validate) {
|
|
2132
|
+
bound.validate = ({ value, input, context }) => validate({ imports, value, input, context });
|
|
2133
|
+
}
|
|
2122
2134
|
if (tryResolveWithoutPrompt) {
|
|
2123
2135
|
bound.tryResolveWithoutPrompt = ({ input }) => tryResolveWithoutPrompt({ imports, input });
|
|
2124
2136
|
}
|
|
@@ -2274,9 +2286,7 @@ function buildMethodEntries(descriptors, context, states) {
|
|
|
2274
2286
|
}
|
|
2275
2287
|
);
|
|
2276
2288
|
} else if (out.type === "item") {
|
|
2277
|
-
const itemCore = async (input) => (
|
|
2278
|
-
data: await callRun(input)
|
|
2279
|
-
});
|
|
2289
|
+
const itemCore = async (input) => callRun(input);
|
|
2280
2290
|
entry.value = createFunction(
|
|
2281
2291
|
fold(itemCore),
|
|
2282
2292
|
{
|
|
@@ -2800,14 +2810,14 @@ function coerce(leaf, raw) {
|
|
|
2800
2810
|
}
|
|
2801
2811
|
async function validationError(leaf, value, state) {
|
|
2802
2812
|
if (leaf.resolver?.type !== "dynamic") return null;
|
|
2813
|
+
const validate = leaf.resolver.validate;
|
|
2814
|
+
if (!validate) return null;
|
|
2803
2815
|
const context = await resolveContext(leaf, state.resolved);
|
|
2804
|
-
const
|
|
2805
|
-
|
|
2816
|
+
const verdict = await validate({
|
|
2817
|
+
value,
|
|
2806
2818
|
input: mergeInput(state.resolved, leaf.extraInput),
|
|
2807
2819
|
context
|
|
2808
2820
|
});
|
|
2809
|
-
if (!config?.validate) return null;
|
|
2810
|
-
const verdict = config.validate(value);
|
|
2811
2821
|
if (verdict === true) return null;
|
|
2812
2822
|
return typeof verdict === "string" ? verdict : `${leaf.name}: invalid value`;
|
|
2813
2823
|
}
|
|
@@ -2933,20 +2943,36 @@ async function resolveContext(leaf, input) {
|
|
|
2933
2943
|
input: mergeInput(input, leaf.extraInput)
|
|
2934
2944
|
});
|
|
2935
2945
|
}
|
|
2936
|
-
|
|
2946
|
+
function firstPagePosition(opts = {}) {
|
|
2947
|
+
return {
|
|
2948
|
+
...opts.search !== void 0 ? { search: opts.search } : {},
|
|
2949
|
+
pageCursor: null,
|
|
2950
|
+
previousCursors: [],
|
|
2951
|
+
generation: opts.generation ?? 0
|
|
2952
|
+
};
|
|
2953
|
+
}
|
|
2954
|
+
async function fetchListing(leaf, input, position, context) {
|
|
2955
|
+
if (leaf.resolver?.type === "dynamic" && leaf.resolver.inputType === "search" && position.search === void 0) {
|
|
2956
|
+
return { position, items: [] };
|
|
2957
|
+
}
|
|
2937
2958
|
const page = await firstPage(
|
|
2938
2959
|
leaf.resolver?.type === "dynamic" ? leaf.resolver.listItems({
|
|
2939
2960
|
input: mergeInput(input, leaf.extraInput),
|
|
2940
|
-
context
|
|
2941
|
-
search:
|
|
2942
|
-
cursor:
|
|
2961
|
+
context,
|
|
2962
|
+
search: position.search,
|
|
2963
|
+
cursor: position.pageCursor ?? void 0
|
|
2943
2964
|
}) : void 0
|
|
2944
2965
|
);
|
|
2945
2966
|
return {
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2967
|
+
position,
|
|
2968
|
+
items: page.data,
|
|
2969
|
+
...page.nextCursor != null ? { nextCursor: page.nextCursor } : {}
|
|
2970
|
+
};
|
|
2971
|
+
}
|
|
2972
|
+
function toPagination(page) {
|
|
2973
|
+
return {
|
|
2974
|
+
position: page.position,
|
|
2975
|
+
...page.nextCursor !== void 0 ? { nextCursor: page.nextCursor } : {}
|
|
2950
2976
|
};
|
|
2951
2977
|
}
|
|
2952
2978
|
|
|
@@ -2968,23 +2994,32 @@ var AFFORDANCE = {
|
|
|
2968
2994
|
description: "Filter the options by a search term",
|
|
2969
2995
|
supply: "term"
|
|
2970
2996
|
},
|
|
2971
|
-
|
|
2997
|
+
nextPage: {
|
|
2998
|
+
action: "next_page",
|
|
2999
|
+
description: "Fetch the next page of options"
|
|
3000
|
+
},
|
|
3001
|
+
previousPage: {
|
|
3002
|
+
action: "previous_page",
|
|
3003
|
+
description: "Return to the previous page of options"
|
|
3004
|
+
},
|
|
2972
3005
|
skip: { action: "skip", description: "Omit this optional parameter" },
|
|
2973
3006
|
add: { action: "add", description: "Add another item" },
|
|
2974
3007
|
done: { action: "done", description: "Finish the list" },
|
|
2975
3008
|
retry: { action: "retry", description: "Retry loading the options" },
|
|
2976
3009
|
cancel: { action: "cancel", description: "Cancel resolution" }
|
|
2977
3010
|
};
|
|
2978
|
-
function selectActions(leaf,
|
|
3011
|
+
function selectActions(leaf, page, multiple) {
|
|
2979
3012
|
const searchMode = leaf.resolver?.type === "dynamic" && leaf.resolver.inputType === "search";
|
|
2980
|
-
if (searchMode &&
|
|
3013
|
+
if (searchMode && page.position.search === void 0 && page.items.length === 0) {
|
|
2981
3014
|
const actions2 = multiple ? [AFFORDANCE.search] : [AFFORDANCE.search, AFFORDANCE.custom];
|
|
2982
3015
|
if (!leaf.required) actions2.push(AFFORDANCE.skip);
|
|
2983
3016
|
return actions2;
|
|
2984
3017
|
}
|
|
2985
3018
|
const actions = multiple ? [AFFORDANCE.choose] : [AFFORDANCE.choose, AFFORDANCE.custom];
|
|
2986
3019
|
if (searchMode) actions.push(AFFORDANCE.search);
|
|
2987
|
-
if (
|
|
3020
|
+
if (page.nextCursor) actions.push(AFFORDANCE.nextPage);
|
|
3021
|
+
if (page.position.previousCursors.length > 0)
|
|
3022
|
+
actions.push(AFFORDANCE.previousPage);
|
|
2988
3023
|
if (!leaf.required) actions.push(AFFORDANCE.skip);
|
|
2989
3024
|
return actions;
|
|
2990
3025
|
}
|
|
@@ -2992,16 +3027,17 @@ function labeledMessage(leaf) {
|
|
|
2992
3027
|
if (!leaf.label) return void 0;
|
|
2993
3028
|
return `${leaf.label} (${leaf.required ? "required" : "optional"}):`;
|
|
2994
3029
|
}
|
|
2995
|
-
function selectQuestion(leaf, input,
|
|
3030
|
+
function selectQuestion(leaf, path, input, page, context) {
|
|
2996
3031
|
const resolver = leaf.resolver?.type === "dynamic" ? leaf.resolver : void 0;
|
|
2997
3032
|
const config = resolver?.prompt?.({
|
|
2998
|
-
items:
|
|
3033
|
+
items: page.items,
|
|
2999
3034
|
input: mergeInput(input, leaf.extraInput),
|
|
3000
3035
|
context
|
|
3001
3036
|
});
|
|
3002
3037
|
const multiple = config?.type === "checkbox";
|
|
3003
3038
|
return {
|
|
3004
3039
|
type: "select",
|
|
3040
|
+
path,
|
|
3005
3041
|
// A labeled field's title beats the resolver's message: per-field
|
|
3006
3042
|
// resolvers are shared across fields (one choices-fetcher for every
|
|
3007
3043
|
// field), so only the leaf knows which field is being asked.
|
|
@@ -3009,9 +3045,13 @@ function selectQuestion(leaf, input, listing, context) {
|
|
|
3009
3045
|
choices: (config?.choices ?? []).map(toChoice),
|
|
3010
3046
|
...multiple ? { multiple: true } : {},
|
|
3011
3047
|
...config?.notes?.length ? { notes: config.notes } : {},
|
|
3012
|
-
...
|
|
3048
|
+
...page.position.search !== void 0 ? { search: page.position.search } : {},
|
|
3013
3049
|
...resolver?.placeholder ? { placeholder: resolver.placeholder } : {},
|
|
3014
|
-
|
|
3050
|
+
page: {
|
|
3051
|
+
generation: page.position.generation,
|
|
3052
|
+
index: page.position.previousCursors.length
|
|
3053
|
+
},
|
|
3054
|
+
actions: selectActions(leaf, page, multiple)
|
|
3015
3055
|
};
|
|
3016
3056
|
}
|
|
3017
3057
|
function toControllerError(error) {
|
|
@@ -3033,6 +3073,7 @@ function failedResult(state, name, error) {
|
|
|
3033
3073
|
error: toControllerError(error),
|
|
3034
3074
|
question: {
|
|
3035
3075
|
type: "select",
|
|
3076
|
+
path: state.current ?? [],
|
|
3036
3077
|
message: `Could not load options for ${name}.`,
|
|
3037
3078
|
choices: [],
|
|
3038
3079
|
actions: [AFFORDANCE.retry, AFFORDANCE.cancel]
|
|
@@ -3040,27 +3081,15 @@ function failedResult(state, name, error) {
|
|
|
3040
3081
|
}
|
|
3041
3082
|
};
|
|
3042
3083
|
}
|
|
3043
|
-
async function buildQuestion(leaf, input) {
|
|
3084
|
+
async function buildQuestion(leaf, path, input) {
|
|
3044
3085
|
const optional = !leaf.required;
|
|
3045
3086
|
const resolver = leaf.resolver;
|
|
3046
3087
|
if (resolver?.type === "dynamic") {
|
|
3047
3088
|
const context = await resolveContext(leaf, input);
|
|
3048
|
-
|
|
3049
|
-
const listing2 = {
|
|
3050
|
-
items: [],
|
|
3051
|
-
cursor: void 0,
|
|
3052
|
-
search: void 0,
|
|
3053
|
-
exhausted: true
|
|
3054
|
-
};
|
|
3055
|
-
return {
|
|
3056
|
-
question: selectQuestion(leaf, input, listing2, context),
|
|
3057
|
-
listing: listing2
|
|
3058
|
-
};
|
|
3059
|
-
}
|
|
3060
|
-
const listing = await fetchListing(leaf, input, { context });
|
|
3089
|
+
const page = await fetchListing(leaf, input, firstPagePosition(), context);
|
|
3061
3090
|
return {
|
|
3062
|
-
question: selectQuestion(leaf, input,
|
|
3063
|
-
|
|
3091
|
+
question: selectQuestion(leaf, path, input, page, context),
|
|
3092
|
+
pagination: toPagination(page)
|
|
3064
3093
|
};
|
|
3065
3094
|
}
|
|
3066
3095
|
if (leaf.staticChoices) {
|
|
@@ -3069,6 +3098,7 @@ async function buildQuestion(leaf, input) {
|
|
|
3069
3098
|
return {
|
|
3070
3099
|
question: {
|
|
3071
3100
|
type: "select",
|
|
3101
|
+
path,
|
|
3072
3102
|
message: `Select ${leaf.name}:`,
|
|
3073
3103
|
choices: leaf.staticChoices,
|
|
3074
3104
|
actions: actions2
|
|
@@ -3082,6 +3112,7 @@ async function buildQuestion(leaf, input) {
|
|
|
3082
3112
|
return {
|
|
3083
3113
|
question: {
|
|
3084
3114
|
type: "input",
|
|
3115
|
+
path,
|
|
3085
3116
|
// The optional marker makes Enter-to-pass discoverable on a bare
|
|
3086
3117
|
// parameter; a labeled field carries its marker via labeledMessage.
|
|
3087
3118
|
message: labeledMessage(leaf) ?? `Enter ${leaf.name}${optional ? " (optional)" : ""}:`,
|
|
@@ -3096,6 +3127,7 @@ function collectionQuestion(t) {
|
|
|
3096
3127
|
if (t.count >= t.min) actions.push(AFFORDANCE.done);
|
|
3097
3128
|
return {
|
|
3098
3129
|
type: "collection",
|
|
3130
|
+
path: t.path,
|
|
3099
3131
|
message: `Add ${t.path[t.path.length - 1]}[${t.count}]?`,
|
|
3100
3132
|
container: "array",
|
|
3101
3133
|
count: t.count,
|
|
@@ -3109,6 +3141,7 @@ function collectionQuestion(t) {
|
|
|
3109
3141
|
function objectGateQuestion(path) {
|
|
3110
3142
|
return {
|
|
3111
3143
|
type: "collection",
|
|
3144
|
+
path,
|
|
3112
3145
|
message: `Add ${path[path.length - 1]}?`,
|
|
3113
3146
|
container: "object",
|
|
3114
3147
|
actions: [
|
|
@@ -3120,9 +3153,10 @@ function objectGateQuestion(path) {
|
|
|
3120
3153
|
]
|
|
3121
3154
|
};
|
|
3122
3155
|
}
|
|
3123
|
-
function optionalsGateQuestion(pending) {
|
|
3156
|
+
function optionalsGateQuestion(path, pending) {
|
|
3124
3157
|
return {
|
|
3125
3158
|
type: "collection",
|
|
3159
|
+
path,
|
|
3126
3160
|
// The prompt and its context ride separately so a host renders the info
|
|
3127
3161
|
// line above the confirm without composing any text of its own.
|
|
3128
3162
|
message: "Would you like to configure optional fields?",
|
|
@@ -3272,8 +3306,12 @@ async function askLeaf(state, path, leaf, opts = {}) {
|
|
|
3272
3306
|
state.current = path;
|
|
3273
3307
|
delete state.gate;
|
|
3274
3308
|
try {
|
|
3275
|
-
const { question,
|
|
3276
|
-
|
|
3309
|
+
const { question, pagination } = await buildQuestion(
|
|
3310
|
+
leaf,
|
|
3311
|
+
path,
|
|
3312
|
+
state.resolved
|
|
3313
|
+
);
|
|
3314
|
+
state.pagination = pagination;
|
|
3277
3315
|
return {
|
|
3278
3316
|
state,
|
|
3279
3317
|
result: {
|
|
@@ -3283,7 +3321,7 @@ async function askLeaf(state, path, leaf, opts = {}) {
|
|
|
3283
3321
|
}
|
|
3284
3322
|
};
|
|
3285
3323
|
} catch (error) {
|
|
3286
|
-
state.
|
|
3324
|
+
state.pagination = failedPagination(void 0, firstPagePosition());
|
|
3287
3325
|
return failedResult(state, leaf.name, error);
|
|
3288
3326
|
}
|
|
3289
3327
|
}
|
|
@@ -3293,13 +3331,13 @@ async function advance(ctx, state) {
|
|
|
3293
3331
|
if (!target) {
|
|
3294
3332
|
delete state.current;
|
|
3295
3333
|
delete state.gate;
|
|
3296
|
-
delete state.
|
|
3334
|
+
delete state.pagination;
|
|
3297
3335
|
return { state, result: finalize(ctx, state.resolved) };
|
|
3298
3336
|
}
|
|
3299
3337
|
if (target.kind === "array") {
|
|
3300
3338
|
state.current = target.path;
|
|
3301
3339
|
state.gate = "array";
|
|
3302
|
-
delete state.
|
|
3340
|
+
delete state.pagination;
|
|
3303
3341
|
return {
|
|
3304
3342
|
state,
|
|
3305
3343
|
result: { status: "ask", question: collectionQuestion(target) }
|
|
@@ -3308,7 +3346,7 @@ async function advance(ctx, state) {
|
|
|
3308
3346
|
if (target.kind === "object") {
|
|
3309
3347
|
state.current = target.path;
|
|
3310
3348
|
state.gate = "entry";
|
|
3311
|
-
delete state.
|
|
3349
|
+
delete state.pagination;
|
|
3312
3350
|
return {
|
|
3313
3351
|
state,
|
|
3314
3352
|
result: { status: "ask", question: objectGateQuestion(target.path) }
|
|
@@ -3317,12 +3355,12 @@ async function advance(ctx, state) {
|
|
|
3317
3355
|
if (target.kind === "optionals") {
|
|
3318
3356
|
state.current = target.path;
|
|
3319
3357
|
state.gate = "optionals";
|
|
3320
|
-
delete state.
|
|
3358
|
+
delete state.pagination;
|
|
3321
3359
|
return {
|
|
3322
3360
|
state,
|
|
3323
3361
|
result: {
|
|
3324
3362
|
status: "ask",
|
|
3325
|
-
question: optionalsGateQuestion(target.pending)
|
|
3363
|
+
question: optionalsGateQuestion(target.path, target.pending)
|
|
3326
3364
|
}
|
|
3327
3365
|
};
|
|
3328
3366
|
}
|
|
@@ -3373,13 +3411,13 @@ async function step(ctx, prior, action) {
|
|
|
3373
3411
|
if (action.type === "cancel") {
|
|
3374
3412
|
delete state.current;
|
|
3375
3413
|
delete state.gate;
|
|
3376
|
-
delete state.
|
|
3414
|
+
delete state.pagination;
|
|
3377
3415
|
return { state, result: { status: "cancelled" } };
|
|
3378
3416
|
}
|
|
3379
3417
|
const path = state.current;
|
|
3380
3418
|
if (!path) throw new Error("step called with no outstanding question");
|
|
3381
3419
|
const leaf = await leafAt(ctx, path, state.resolved);
|
|
3382
|
-
if (leaf && (action.type === "search" || action.type === "
|
|
3420
|
+
if (leaf && (action.type === "search" || action.type === "next_page" || action.type === "previous_page" || action.type === "retry")) {
|
|
3383
3421
|
return refine(ctx, state, leaf, path, action);
|
|
3384
3422
|
}
|
|
3385
3423
|
if (action.type === "add" || action.type === "done") {
|
|
@@ -3391,7 +3429,7 @@ async function step(ctx, prior, action) {
|
|
|
3391
3429
|
}
|
|
3392
3430
|
delete state.current;
|
|
3393
3431
|
delete state.gate;
|
|
3394
|
-
delete state.
|
|
3432
|
+
delete state.pagination;
|
|
3395
3433
|
if (action.type === "done") {
|
|
3396
3434
|
settle(state, path);
|
|
3397
3435
|
return advance(ctx, state);
|
|
@@ -3420,23 +3458,44 @@ async function step(ctx, prior, action) {
|
|
|
3420
3458
|
case "choose":
|
|
3421
3459
|
case "custom": {
|
|
3422
3460
|
if (leaf) {
|
|
3423
|
-
|
|
3461
|
+
let error;
|
|
3462
|
+
try {
|
|
3463
|
+
error = await validationError(leaf, action.value, state);
|
|
3464
|
+
} catch (thrown) {
|
|
3465
|
+
return failedResult(state, leaf.name, thrown);
|
|
3466
|
+
}
|
|
3424
3467
|
if (error) {
|
|
3425
|
-
if (state.
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3468
|
+
if (state.pagination && leaf.resolver?.type === "dynamic") {
|
|
3469
|
+
try {
|
|
3470
|
+
const context = await resolveContext(leaf, state.resolved);
|
|
3471
|
+
const page = await fetchListing(
|
|
3472
|
+
leaf,
|
|
3473
|
+
state.resolved,
|
|
3474
|
+
state.pagination.position,
|
|
3475
|
+
context
|
|
3476
|
+
);
|
|
3477
|
+
state.pagination = toPagination(page);
|
|
3478
|
+
return {
|
|
3479
|
+
state,
|
|
3480
|
+
result: {
|
|
3481
|
+
status: "ask",
|
|
3482
|
+
question: selectQuestion(
|
|
3483
|
+
leaf,
|
|
3484
|
+
path,
|
|
3485
|
+
state.resolved,
|
|
3486
|
+
page,
|
|
3487
|
+
context
|
|
3488
|
+
),
|
|
3489
|
+
error
|
|
3490
|
+
}
|
|
3491
|
+
};
|
|
3492
|
+
} catch (fetchError) {
|
|
3493
|
+
state.pagination = failedPagination(
|
|
3494
|
+
state.pagination,
|
|
3495
|
+
state.pagination.position
|
|
3496
|
+
);
|
|
3497
|
+
return failedResult(state, leaf.name, fetchError);
|
|
3498
|
+
}
|
|
3440
3499
|
}
|
|
3441
3500
|
return askLeaf(state, path, leaf, { error });
|
|
3442
3501
|
}
|
|
@@ -3455,15 +3514,11 @@ async function step(ctx, prior, action) {
|
|
|
3455
3514
|
throw new Error(`action "${action.type}" is not supported here`);
|
|
3456
3515
|
}
|
|
3457
3516
|
delete state.current;
|
|
3458
|
-
delete state.
|
|
3517
|
+
delete state.pagination;
|
|
3459
3518
|
return advance(ctx, state);
|
|
3460
3519
|
}
|
|
3461
3520
|
async function refine(ctx, state, leaf, path, action) {
|
|
3462
|
-
const
|
|
3463
|
-
search: state.listing?.search,
|
|
3464
|
-
cursor: state.listing?.cursor,
|
|
3465
|
-
priorItems: state.listing?.items ?? []
|
|
3466
|
-
};
|
|
3521
|
+
const position = positionAfter(state.pagination, action);
|
|
3467
3522
|
try {
|
|
3468
3523
|
if (action.type === "search") {
|
|
3469
3524
|
const exact = leaf.resolver?.type === "dynamic" ? await leaf.resolver.tryResolveFromSearch?.({
|
|
@@ -3473,32 +3528,62 @@ async function refine(ctx, state, leaf, path, action) {
|
|
|
3473
3528
|
if (exact) {
|
|
3474
3529
|
setAtPath(state.resolved, path, coerce(leaf, exact.resolvedValue));
|
|
3475
3530
|
delete state.current;
|
|
3476
|
-
delete state.
|
|
3531
|
+
delete state.pagination;
|
|
3477
3532
|
return advance(ctx, state);
|
|
3478
3533
|
}
|
|
3479
3534
|
}
|
|
3480
3535
|
const context = await resolveContext(leaf, state.resolved);
|
|
3481
|
-
|
|
3482
|
-
|
|
3483
|
-
context
|
|
3484
|
-
});
|
|
3536
|
+
const page = await fetchListing(leaf, state.resolved, position, context);
|
|
3537
|
+
state.pagination = toPagination(page);
|
|
3485
3538
|
return {
|
|
3486
3539
|
state,
|
|
3487
3540
|
result: {
|
|
3488
3541
|
status: "ask",
|
|
3489
|
-
question: selectQuestion(leaf, state.resolved,
|
|
3542
|
+
question: selectQuestion(leaf, path, state.resolved, page, context)
|
|
3490
3543
|
}
|
|
3491
3544
|
};
|
|
3492
3545
|
} catch (error) {
|
|
3493
|
-
state.
|
|
3494
|
-
items: attempt.priorItems,
|
|
3495
|
-
search: attempt.search,
|
|
3496
|
-
cursor: attempt.cursor,
|
|
3497
|
-
exhausted: false
|
|
3498
|
-
};
|
|
3546
|
+
state.pagination = failedPagination(state.pagination, position);
|
|
3499
3547
|
return failedResult(state, leaf.name, error);
|
|
3500
3548
|
}
|
|
3501
3549
|
}
|
|
3550
|
+
function failedPagination(pagination, retryPosition) {
|
|
3551
|
+
return {
|
|
3552
|
+
position: pagination?.position ?? firstPagePosition(),
|
|
3553
|
+
...pagination?.nextCursor !== void 0 ? { nextCursor: pagination.nextCursor } : {},
|
|
3554
|
+
retryPosition
|
|
3555
|
+
};
|
|
3556
|
+
}
|
|
3557
|
+
function positionAfter(pagination, action) {
|
|
3558
|
+
const current = pagination?.position ?? firstPagePosition();
|
|
3559
|
+
switch (action.type) {
|
|
3560
|
+
case "search":
|
|
3561
|
+
return firstPagePosition({
|
|
3562
|
+
search: action.term,
|
|
3563
|
+
generation: current.generation + 1
|
|
3564
|
+
});
|
|
3565
|
+
case "next_page": {
|
|
3566
|
+
if (pagination?.nextCursor == null) return current;
|
|
3567
|
+
return {
|
|
3568
|
+
...current.search !== void 0 ? { search: current.search } : {},
|
|
3569
|
+
pageCursor: pagination.nextCursor,
|
|
3570
|
+
previousCursors: [...current.previousCursors, current.pageCursor],
|
|
3571
|
+
generation: current.generation
|
|
3572
|
+
};
|
|
3573
|
+
}
|
|
3574
|
+
case "previous_page": {
|
|
3575
|
+
if (current.previousCursors.length === 0) return current;
|
|
3576
|
+
return {
|
|
3577
|
+
...current.search !== void 0 ? { search: current.search } : {},
|
|
3578
|
+
pageCursor: current.previousCursors[current.previousCursors.length - 1],
|
|
3579
|
+
previousCursors: current.previousCursors.slice(0, -1),
|
|
3580
|
+
generation: current.generation
|
|
3581
|
+
};
|
|
3582
|
+
}
|
|
3583
|
+
case "retry":
|
|
3584
|
+
return pagination?.retryPosition ?? current;
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3502
3587
|
|
|
3503
3588
|
// src/model/resolution/controller.ts
|
|
3504
3589
|
function toJsonSchema(schema) {
|