@zapier/kitcore 0.6.0 → 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 +14 -0
- package/dist/index.cjs +199 -112
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +134 -34
- package/dist/index.d.ts +134 -34
- package/dist/index.mjs +199 -112
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
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
|
+
|
|
3
17
|
## 0.6.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
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
|
}
|
|
@@ -2798,14 +2810,14 @@ function coerce(leaf, raw) {
|
|
|
2798
2810
|
}
|
|
2799
2811
|
async function validationError(leaf, value, state) {
|
|
2800
2812
|
if (leaf.resolver?.type !== "dynamic") return null;
|
|
2813
|
+
const validate = leaf.resolver.validate;
|
|
2814
|
+
if (!validate) return null;
|
|
2801
2815
|
const context = await resolveContext(leaf, state.resolved);
|
|
2802
|
-
const
|
|
2803
|
-
|
|
2816
|
+
const verdict = await validate({
|
|
2817
|
+
value,
|
|
2804
2818
|
input: mergeInput(state.resolved, leaf.extraInput),
|
|
2805
2819
|
context
|
|
2806
2820
|
});
|
|
2807
|
-
if (!config?.validate) return null;
|
|
2808
|
-
const verdict = config.validate(value);
|
|
2809
2821
|
if (verdict === true) return null;
|
|
2810
2822
|
return typeof verdict === "string" ? verdict : `${leaf.name}: invalid value`;
|
|
2811
2823
|
}
|
|
@@ -2931,20 +2943,36 @@ async function resolveContext(leaf, input) {
|
|
|
2931
2943
|
input: mergeInput(input, leaf.extraInput)
|
|
2932
2944
|
});
|
|
2933
2945
|
}
|
|
2934
|
-
|
|
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
|
+
}
|
|
2935
2958
|
const page = await firstPage(
|
|
2936
2959
|
leaf.resolver?.type === "dynamic" ? leaf.resolver.listItems({
|
|
2937
2960
|
input: mergeInput(input, leaf.extraInput),
|
|
2938
|
-
context
|
|
2939
|
-
search:
|
|
2940
|
-
cursor:
|
|
2961
|
+
context,
|
|
2962
|
+
search: position.search,
|
|
2963
|
+
cursor: position.pageCursor ?? void 0
|
|
2941
2964
|
}) : void 0
|
|
2942
2965
|
);
|
|
2943
2966
|
return {
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
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 } : {}
|
|
2948
2976
|
};
|
|
2949
2977
|
}
|
|
2950
2978
|
|
|
@@ -2966,23 +2994,32 @@ var AFFORDANCE = {
|
|
|
2966
2994
|
description: "Filter the options by a search term",
|
|
2967
2995
|
supply: "term"
|
|
2968
2996
|
},
|
|
2969
|
-
|
|
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
|
+
},
|
|
2970
3005
|
skip: { action: "skip", description: "Omit this optional parameter" },
|
|
2971
3006
|
add: { action: "add", description: "Add another item" },
|
|
2972
3007
|
done: { action: "done", description: "Finish the list" },
|
|
2973
3008
|
retry: { action: "retry", description: "Retry loading the options" },
|
|
2974
3009
|
cancel: { action: "cancel", description: "Cancel resolution" }
|
|
2975
3010
|
};
|
|
2976
|
-
function selectActions(leaf,
|
|
3011
|
+
function selectActions(leaf, page, multiple) {
|
|
2977
3012
|
const searchMode = leaf.resolver?.type === "dynamic" && leaf.resolver.inputType === "search";
|
|
2978
|
-
if (searchMode &&
|
|
3013
|
+
if (searchMode && page.position.search === void 0 && page.items.length === 0) {
|
|
2979
3014
|
const actions2 = multiple ? [AFFORDANCE.search] : [AFFORDANCE.search, AFFORDANCE.custom];
|
|
2980
3015
|
if (!leaf.required) actions2.push(AFFORDANCE.skip);
|
|
2981
3016
|
return actions2;
|
|
2982
3017
|
}
|
|
2983
3018
|
const actions = multiple ? [AFFORDANCE.choose] : [AFFORDANCE.choose, AFFORDANCE.custom];
|
|
2984
3019
|
if (searchMode) actions.push(AFFORDANCE.search);
|
|
2985
|
-
if (
|
|
3020
|
+
if (page.nextCursor) actions.push(AFFORDANCE.nextPage);
|
|
3021
|
+
if (page.position.previousCursors.length > 0)
|
|
3022
|
+
actions.push(AFFORDANCE.previousPage);
|
|
2986
3023
|
if (!leaf.required) actions.push(AFFORDANCE.skip);
|
|
2987
3024
|
return actions;
|
|
2988
3025
|
}
|
|
@@ -2990,16 +3027,17 @@ function labeledMessage(leaf) {
|
|
|
2990
3027
|
if (!leaf.label) return void 0;
|
|
2991
3028
|
return `${leaf.label} (${leaf.required ? "required" : "optional"}):`;
|
|
2992
3029
|
}
|
|
2993
|
-
function selectQuestion(leaf, input,
|
|
3030
|
+
function selectQuestion(leaf, path, input, page, context) {
|
|
2994
3031
|
const resolver = leaf.resolver?.type === "dynamic" ? leaf.resolver : void 0;
|
|
2995
3032
|
const config = resolver?.prompt?.({
|
|
2996
|
-
items:
|
|
3033
|
+
items: page.items,
|
|
2997
3034
|
input: mergeInput(input, leaf.extraInput),
|
|
2998
3035
|
context
|
|
2999
3036
|
});
|
|
3000
3037
|
const multiple = config?.type === "checkbox";
|
|
3001
3038
|
return {
|
|
3002
3039
|
type: "select",
|
|
3040
|
+
path,
|
|
3003
3041
|
// A labeled field's title beats the resolver's message: per-field
|
|
3004
3042
|
// resolvers are shared across fields (one choices-fetcher for every
|
|
3005
3043
|
// field), so only the leaf knows which field is being asked.
|
|
@@ -3007,9 +3045,13 @@ function selectQuestion(leaf, input, listing, context) {
|
|
|
3007
3045
|
choices: (config?.choices ?? []).map(toChoice),
|
|
3008
3046
|
...multiple ? { multiple: true } : {},
|
|
3009
3047
|
...config?.notes?.length ? { notes: config.notes } : {},
|
|
3010
|
-
...
|
|
3048
|
+
...page.position.search !== void 0 ? { search: page.position.search } : {},
|
|
3011
3049
|
...resolver?.placeholder ? { placeholder: resolver.placeholder } : {},
|
|
3012
|
-
|
|
3050
|
+
page: {
|
|
3051
|
+
generation: page.position.generation,
|
|
3052
|
+
index: page.position.previousCursors.length
|
|
3053
|
+
},
|
|
3054
|
+
actions: selectActions(leaf, page, multiple)
|
|
3013
3055
|
};
|
|
3014
3056
|
}
|
|
3015
3057
|
function toControllerError(error) {
|
|
@@ -3031,6 +3073,7 @@ function failedResult(state, name, error) {
|
|
|
3031
3073
|
error: toControllerError(error),
|
|
3032
3074
|
question: {
|
|
3033
3075
|
type: "select",
|
|
3076
|
+
path: state.current ?? [],
|
|
3034
3077
|
message: `Could not load options for ${name}.`,
|
|
3035
3078
|
choices: [],
|
|
3036
3079
|
actions: [AFFORDANCE.retry, AFFORDANCE.cancel]
|
|
@@ -3038,27 +3081,15 @@ function failedResult(state, name, error) {
|
|
|
3038
3081
|
}
|
|
3039
3082
|
};
|
|
3040
3083
|
}
|
|
3041
|
-
async function buildQuestion(leaf, input) {
|
|
3084
|
+
async function buildQuestion(leaf, path, input) {
|
|
3042
3085
|
const optional = !leaf.required;
|
|
3043
3086
|
const resolver = leaf.resolver;
|
|
3044
3087
|
if (resolver?.type === "dynamic") {
|
|
3045
3088
|
const context = await resolveContext(leaf, input);
|
|
3046
|
-
|
|
3047
|
-
const listing2 = {
|
|
3048
|
-
items: [],
|
|
3049
|
-
cursor: void 0,
|
|
3050
|
-
search: void 0,
|
|
3051
|
-
exhausted: true
|
|
3052
|
-
};
|
|
3053
|
-
return {
|
|
3054
|
-
question: selectQuestion(leaf, input, listing2, context),
|
|
3055
|
-
listing: listing2
|
|
3056
|
-
};
|
|
3057
|
-
}
|
|
3058
|
-
const listing = await fetchListing(leaf, input, { context });
|
|
3089
|
+
const page = await fetchListing(leaf, input, firstPagePosition(), context);
|
|
3059
3090
|
return {
|
|
3060
|
-
question: selectQuestion(leaf, input,
|
|
3061
|
-
|
|
3091
|
+
question: selectQuestion(leaf, path, input, page, context),
|
|
3092
|
+
pagination: toPagination(page)
|
|
3062
3093
|
};
|
|
3063
3094
|
}
|
|
3064
3095
|
if (leaf.staticChoices) {
|
|
@@ -3067,6 +3098,7 @@ async function buildQuestion(leaf, input) {
|
|
|
3067
3098
|
return {
|
|
3068
3099
|
question: {
|
|
3069
3100
|
type: "select",
|
|
3101
|
+
path,
|
|
3070
3102
|
message: `Select ${leaf.name}:`,
|
|
3071
3103
|
choices: leaf.staticChoices,
|
|
3072
3104
|
actions: actions2
|
|
@@ -3080,6 +3112,7 @@ async function buildQuestion(leaf, input) {
|
|
|
3080
3112
|
return {
|
|
3081
3113
|
question: {
|
|
3082
3114
|
type: "input",
|
|
3115
|
+
path,
|
|
3083
3116
|
// The optional marker makes Enter-to-pass discoverable on a bare
|
|
3084
3117
|
// parameter; a labeled field carries its marker via labeledMessage.
|
|
3085
3118
|
message: labeledMessage(leaf) ?? `Enter ${leaf.name}${optional ? " (optional)" : ""}:`,
|
|
@@ -3094,6 +3127,7 @@ function collectionQuestion(t) {
|
|
|
3094
3127
|
if (t.count >= t.min) actions.push(AFFORDANCE.done);
|
|
3095
3128
|
return {
|
|
3096
3129
|
type: "collection",
|
|
3130
|
+
path: t.path,
|
|
3097
3131
|
message: `Add ${t.path[t.path.length - 1]}[${t.count}]?`,
|
|
3098
3132
|
container: "array",
|
|
3099
3133
|
count: t.count,
|
|
@@ -3107,6 +3141,7 @@ function collectionQuestion(t) {
|
|
|
3107
3141
|
function objectGateQuestion(path) {
|
|
3108
3142
|
return {
|
|
3109
3143
|
type: "collection",
|
|
3144
|
+
path,
|
|
3110
3145
|
message: `Add ${path[path.length - 1]}?`,
|
|
3111
3146
|
container: "object",
|
|
3112
3147
|
actions: [
|
|
@@ -3118,9 +3153,10 @@ function objectGateQuestion(path) {
|
|
|
3118
3153
|
]
|
|
3119
3154
|
};
|
|
3120
3155
|
}
|
|
3121
|
-
function optionalsGateQuestion(pending) {
|
|
3156
|
+
function optionalsGateQuestion(path, pending) {
|
|
3122
3157
|
return {
|
|
3123
3158
|
type: "collection",
|
|
3159
|
+
path,
|
|
3124
3160
|
// The prompt and its context ride separately so a host renders the info
|
|
3125
3161
|
// line above the confirm without composing any text of its own.
|
|
3126
3162
|
message: "Would you like to configure optional fields?",
|
|
@@ -3270,8 +3306,12 @@ async function askLeaf(state, path, leaf, opts = {}) {
|
|
|
3270
3306
|
state.current = path;
|
|
3271
3307
|
delete state.gate;
|
|
3272
3308
|
try {
|
|
3273
|
-
const { question,
|
|
3274
|
-
|
|
3309
|
+
const { question, pagination } = await buildQuestion(
|
|
3310
|
+
leaf,
|
|
3311
|
+
path,
|
|
3312
|
+
state.resolved
|
|
3313
|
+
);
|
|
3314
|
+
state.pagination = pagination;
|
|
3275
3315
|
return {
|
|
3276
3316
|
state,
|
|
3277
3317
|
result: {
|
|
@@ -3281,7 +3321,7 @@ async function askLeaf(state, path, leaf, opts = {}) {
|
|
|
3281
3321
|
}
|
|
3282
3322
|
};
|
|
3283
3323
|
} catch (error) {
|
|
3284
|
-
state.
|
|
3324
|
+
state.pagination = failedPagination(void 0, firstPagePosition());
|
|
3285
3325
|
return failedResult(state, leaf.name, error);
|
|
3286
3326
|
}
|
|
3287
3327
|
}
|
|
@@ -3291,13 +3331,13 @@ async function advance(ctx, state) {
|
|
|
3291
3331
|
if (!target) {
|
|
3292
3332
|
delete state.current;
|
|
3293
3333
|
delete state.gate;
|
|
3294
|
-
delete state.
|
|
3334
|
+
delete state.pagination;
|
|
3295
3335
|
return { state, result: finalize(ctx, state.resolved) };
|
|
3296
3336
|
}
|
|
3297
3337
|
if (target.kind === "array") {
|
|
3298
3338
|
state.current = target.path;
|
|
3299
3339
|
state.gate = "array";
|
|
3300
|
-
delete state.
|
|
3340
|
+
delete state.pagination;
|
|
3301
3341
|
return {
|
|
3302
3342
|
state,
|
|
3303
3343
|
result: { status: "ask", question: collectionQuestion(target) }
|
|
@@ -3306,7 +3346,7 @@ async function advance(ctx, state) {
|
|
|
3306
3346
|
if (target.kind === "object") {
|
|
3307
3347
|
state.current = target.path;
|
|
3308
3348
|
state.gate = "entry";
|
|
3309
|
-
delete state.
|
|
3349
|
+
delete state.pagination;
|
|
3310
3350
|
return {
|
|
3311
3351
|
state,
|
|
3312
3352
|
result: { status: "ask", question: objectGateQuestion(target.path) }
|
|
@@ -3315,12 +3355,12 @@ async function advance(ctx, state) {
|
|
|
3315
3355
|
if (target.kind === "optionals") {
|
|
3316
3356
|
state.current = target.path;
|
|
3317
3357
|
state.gate = "optionals";
|
|
3318
|
-
delete state.
|
|
3358
|
+
delete state.pagination;
|
|
3319
3359
|
return {
|
|
3320
3360
|
state,
|
|
3321
3361
|
result: {
|
|
3322
3362
|
status: "ask",
|
|
3323
|
-
question: optionalsGateQuestion(target.pending)
|
|
3363
|
+
question: optionalsGateQuestion(target.path, target.pending)
|
|
3324
3364
|
}
|
|
3325
3365
|
};
|
|
3326
3366
|
}
|
|
@@ -3371,13 +3411,13 @@ async function step(ctx, prior, action) {
|
|
|
3371
3411
|
if (action.type === "cancel") {
|
|
3372
3412
|
delete state.current;
|
|
3373
3413
|
delete state.gate;
|
|
3374
|
-
delete state.
|
|
3414
|
+
delete state.pagination;
|
|
3375
3415
|
return { state, result: { status: "cancelled" } };
|
|
3376
3416
|
}
|
|
3377
3417
|
const path = state.current;
|
|
3378
3418
|
if (!path) throw new Error("step called with no outstanding question");
|
|
3379
3419
|
const leaf = await leafAt(ctx, path, state.resolved);
|
|
3380
|
-
if (leaf && (action.type === "search" || action.type === "
|
|
3420
|
+
if (leaf && (action.type === "search" || action.type === "next_page" || action.type === "previous_page" || action.type === "retry")) {
|
|
3381
3421
|
return refine(ctx, state, leaf, path, action);
|
|
3382
3422
|
}
|
|
3383
3423
|
if (action.type === "add" || action.type === "done") {
|
|
@@ -3389,7 +3429,7 @@ async function step(ctx, prior, action) {
|
|
|
3389
3429
|
}
|
|
3390
3430
|
delete state.current;
|
|
3391
3431
|
delete state.gate;
|
|
3392
|
-
delete state.
|
|
3432
|
+
delete state.pagination;
|
|
3393
3433
|
if (action.type === "done") {
|
|
3394
3434
|
settle(state, path);
|
|
3395
3435
|
return advance(ctx, state);
|
|
@@ -3418,23 +3458,44 @@ async function step(ctx, prior, action) {
|
|
|
3418
3458
|
case "choose":
|
|
3419
3459
|
case "custom": {
|
|
3420
3460
|
if (leaf) {
|
|
3421
|
-
|
|
3461
|
+
let error;
|
|
3462
|
+
try {
|
|
3463
|
+
error = await validationError(leaf, action.value, state);
|
|
3464
|
+
} catch (thrown) {
|
|
3465
|
+
return failedResult(state, leaf.name, thrown);
|
|
3466
|
+
}
|
|
3422
3467
|
if (error) {
|
|
3423
|
-
if (state.
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
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
|
+
}
|
|
3438
3499
|
}
|
|
3439
3500
|
return askLeaf(state, path, leaf, { error });
|
|
3440
3501
|
}
|
|
@@ -3453,15 +3514,11 @@ async function step(ctx, prior, action) {
|
|
|
3453
3514
|
throw new Error(`action "${action.type}" is not supported here`);
|
|
3454
3515
|
}
|
|
3455
3516
|
delete state.current;
|
|
3456
|
-
delete state.
|
|
3517
|
+
delete state.pagination;
|
|
3457
3518
|
return advance(ctx, state);
|
|
3458
3519
|
}
|
|
3459
3520
|
async function refine(ctx, state, leaf, path, action) {
|
|
3460
|
-
const
|
|
3461
|
-
search: state.listing?.search,
|
|
3462
|
-
cursor: state.listing?.cursor,
|
|
3463
|
-
priorItems: state.listing?.items ?? []
|
|
3464
|
-
};
|
|
3521
|
+
const position = positionAfter(state.pagination, action);
|
|
3465
3522
|
try {
|
|
3466
3523
|
if (action.type === "search") {
|
|
3467
3524
|
const exact = leaf.resolver?.type === "dynamic" ? await leaf.resolver.tryResolveFromSearch?.({
|
|
@@ -3471,32 +3528,62 @@ async function refine(ctx, state, leaf, path, action) {
|
|
|
3471
3528
|
if (exact) {
|
|
3472
3529
|
setAtPath(state.resolved, path, coerce(leaf, exact.resolvedValue));
|
|
3473
3530
|
delete state.current;
|
|
3474
|
-
delete state.
|
|
3531
|
+
delete state.pagination;
|
|
3475
3532
|
return advance(ctx, state);
|
|
3476
3533
|
}
|
|
3477
3534
|
}
|
|
3478
3535
|
const context = await resolveContext(leaf, state.resolved);
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
context
|
|
3482
|
-
});
|
|
3536
|
+
const page = await fetchListing(leaf, state.resolved, position, context);
|
|
3537
|
+
state.pagination = toPagination(page);
|
|
3483
3538
|
return {
|
|
3484
3539
|
state,
|
|
3485
3540
|
result: {
|
|
3486
3541
|
status: "ask",
|
|
3487
|
-
question: selectQuestion(leaf, state.resolved,
|
|
3542
|
+
question: selectQuestion(leaf, path, state.resolved, page, context)
|
|
3488
3543
|
}
|
|
3489
3544
|
};
|
|
3490
3545
|
} catch (error) {
|
|
3491
|
-
state.
|
|
3492
|
-
items: attempt.priorItems,
|
|
3493
|
-
search: attempt.search,
|
|
3494
|
-
cursor: attempt.cursor,
|
|
3495
|
-
exhausted: false
|
|
3496
|
-
};
|
|
3546
|
+
state.pagination = failedPagination(state.pagination, position);
|
|
3497
3547
|
return failedResult(state, leaf.name, error);
|
|
3498
3548
|
}
|
|
3499
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
|
+
}
|
|
3500
3587
|
|
|
3501
3588
|
// src/model/resolution/controller.ts
|
|
3502
3589
|
function toJsonSchema(schema) {
|