@zapier/kitcore 0.11.0 → 0.12.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/dist/index.mjs CHANGED
@@ -924,7 +924,8 @@ function isSdkPage(value) {
924
924
  }
925
925
  function createPageFunction(coreFn, {
926
926
  sdk,
927
- adaptPage
927
+ adaptPage,
928
+ finalizePage
928
929
  }) {
929
930
  const functionName = coreFn.name + "Page";
930
931
  const namedFunctions = {
@@ -937,7 +938,7 @@ function createPageFunction(coreFn, {
937
938
  `${functionName}: paginated result must be exactly { data: TItem[], nextCursor? } (produced by the handler or its \`adaptPage\`); got keys [${page && typeof page === "object" ? Object.keys(page).join(", ") : typeof page}]. If the handler returns a raw shape, set \`adaptPage\` to translate it; if \`adaptPage\` already runs, it must return only \`data\`/\`nextCursor\`.`
938
939
  );
939
940
  }
940
- return page;
941
+ return finalizePage ? finalizePage(page) : page;
941
942
  } catch (error) {
942
943
  throw normalizeError(
943
944
  error,
@@ -956,9 +957,14 @@ function createPaginatedFunction(coreFn, options) {
956
957
  defaultPageSize,
957
958
  adaptPage,
958
959
  annotator,
960
+ finalizePage,
959
961
  getDeprecation
960
962
  } = options;
961
- const pageFunction = createPageFunction(coreFn, { sdk, adaptPage });
963
+ const pageFunction = createPageFunction(coreFn, {
964
+ sdk,
965
+ adaptPage,
966
+ finalizePage
967
+ });
962
968
  const functionName = name || coreFn.name;
963
969
  const namedFunctions = {
964
970
  [functionName]: function(callOptions) {
@@ -1484,6 +1490,7 @@ function defineMethod(config) {
1484
1490
  importBindings: deps.bindings,
1485
1491
  inputSchema: config.inputSchema,
1486
1492
  skipInputValidation: config.skipInputValidation,
1493
+ skipOutputValidation: config.skipOutputValidation,
1487
1494
  meta: collectLeafMeta(config),
1488
1495
  resolvers: config.resolvers,
1489
1496
  formatter: config.formatter,
@@ -1590,6 +1597,29 @@ function declareMethod(config) {
1590
1597
  }
1591
1598
  };
1592
1599
  }
1600
+ function declareOptionalMethod(config) {
1601
+ const { name, namespace } = parseId(config.id);
1602
+ const id = makeId(name, namespace);
1603
+ return {
1604
+ pluginType: "method",
1605
+ name,
1606
+ namespace,
1607
+ id,
1608
+ standIn: true,
1609
+ optional: true,
1610
+ imports: [],
1611
+ importBindings: [],
1612
+ run: () => {
1613
+ throw new Error(
1614
+ `Plugin "${id}" is an optional stand-in (declareOptionalMethod) with no implementation. Its binding is \`undefined\` unless a real plugin is registered under this id.`
1615
+ );
1616
+ }
1617
+ // Requires nothing (phantom carrier `<never, never>`): a consumer that
1618
+ // imports it still passes `createSdk`'s completeness check unprovided. The
1619
+ // `optional: true` literal drives `PluginSurface` to type the binding
1620
+ // `| undefined`.
1621
+ };
1622
+ }
1593
1623
  function defineProperty(config) {
1594
1624
  const deps = normalizeImports(config.imports);
1595
1625
  return {
@@ -1635,6 +1665,11 @@ function declareOptionalProperty(config) {
1635
1665
  // import binding is still typed `TValue | undefined` from the descriptor.
1636
1666
  };
1637
1667
  }
1668
+ function declareDefault({
1669
+ plugin
1670
+ }) {
1671
+ return { ...plugin, defaultSource: plugin };
1672
+ }
1638
1673
  function defineHook(config) {
1639
1674
  const deps = normalizeImports(config.imports);
1640
1675
  return {
@@ -1936,6 +1971,97 @@ var getRegistryPlugin = defineMethod({
1936
1971
  run: ({ imports, input }) => buildSurfaceRegistry(imports.context, input?.package)
1937
1972
  });
1938
1973
 
1974
+ // src/utils/output-policy.ts
1975
+ function isRecord(value) {
1976
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1977
+ }
1978
+ function diffDroppedPaths(raw, parsed, prefix = "") {
1979
+ const paths = [];
1980
+ walkDroppedPaths(raw, parsed, prefix, paths);
1981
+ return paths;
1982
+ }
1983
+ function walkDroppedPaths(raw, parsed, prefix, out) {
1984
+ if (Array.isArray(raw) && Array.isArray(parsed)) {
1985
+ const seen = /* @__PURE__ */ new Set();
1986
+ const length = Math.min(raw.length, parsed.length);
1987
+ for (let index = 0; index < length; index++) {
1988
+ const elementPaths = [];
1989
+ walkDroppedPaths(raw[index], parsed[index], `${prefix}[]`, elementPaths);
1990
+ for (const path of elementPaths) {
1991
+ if (seen.has(path)) continue;
1992
+ seen.add(path);
1993
+ out.push(path);
1994
+ }
1995
+ }
1996
+ return;
1997
+ }
1998
+ if (isRecord(raw) && isRecord(parsed)) {
1999
+ for (const key of Object.keys(raw)) {
2000
+ const path = prefix ? `${prefix}.${key}` : key;
2001
+ if (!(key in parsed)) {
2002
+ out.push(path);
2003
+ continue;
2004
+ }
2005
+ walkDroppedPaths(raw[key], parsed[key], path, out);
2006
+ }
2007
+ return;
2008
+ }
2009
+ }
2010
+ function parseOutput(schema, value, policy, locator) {
2011
+ const result = schema.safeParse(value);
2012
+ if (result.success) return result.data;
2013
+ const issues = result.error.issues.map((issue) => {
2014
+ const path = issue.path.length > 0 ? issue.path.join(".") : "data";
2015
+ return `${path}: ${issue.message}`;
2016
+ });
2017
+ const subject = policy.methodName ? ` for "${policy.methodName}"` : "";
2018
+ const at = locator ? ` at ${locator}` : "";
2019
+ throw createCoreError(
2020
+ {
2021
+ code: CoreErrorCode.Validation,
2022
+ message: `Output validation failed${subject}${at}:
2023
+ ${issues.join("\n ")}
2024
+
2025
+ The response does not match the method's \`outputSchema\`. Correct the schema, or set \`skipOutputValidation: true\` on the method to pass the response through unvalidated.`,
2026
+ details: { zodErrors: result.error.issues, output: value }
2027
+ },
2028
+ policy.adaptError
2029
+ );
2030
+ }
2031
+ function applyItemOutputPolicy(result, policy) {
2032
+ const schema = policy.outputSchema;
2033
+ if (!schema || policy.skipOutputValidation) return result;
2034
+ if (!isRecord(result) || !("data" in result)) return result;
2035
+ const data = parseOutput(schema, result.data, policy);
2036
+ const next = { ...result, data };
2037
+ if (policy.includeOutputValidationDroppedPaths) {
2038
+ const droppedPaths = diffDroppedPaths(result.data, data);
2039
+ if (droppedPaths.length > 0) {
2040
+ next.meta = withOutputValidation(result.meta, droppedPaths);
2041
+ }
2042
+ }
2043
+ return next;
2044
+ }
2045
+ function withOutputValidation(existing, droppedPaths) {
2046
+ const base = isRecord(existing) ? existing : {};
2047
+ return { ...base, outputValidation: { droppedPaths } };
2048
+ }
2049
+ function applyListOutputPolicy(page, policy) {
2050
+ const schema = policy.outputSchema;
2051
+ if (!schema || policy.skipOutputValidation) return page;
2052
+ const data = page.data.map(
2053
+ (item, index) => parseOutput(schema, item, policy, `data[${index}]`)
2054
+ );
2055
+ const next = { ...page, data };
2056
+ if (policy.includeOutputValidationDroppedPaths) {
2057
+ const droppedPaths = diffDroppedPaths(page.data, data);
2058
+ if (droppedPaths.length > 0) {
2059
+ next.meta = { ...page.meta, outputValidation: { droppedPaths } };
2060
+ }
2061
+ }
2062
+ return next;
2063
+ }
2064
+
1939
2065
  // src/model/materialize.ts
1940
2066
  var FRAMEWORK_CONFIGURATION_IDS = /* @__PURE__ */ new Set([
1941
2067
  CORE_OPTIONS_ID
@@ -1999,6 +2125,9 @@ function edgesOf(plugin) {
1999
2125
  function isStandIn(plugin) {
2000
2126
  return (plugin.pluginType === "method" || plugin.pluginType === "property" || plugin.pluginType === "aggregate") && plugin.standIn === true;
2001
2127
  }
2128
+ function isDefault(plugin) {
2129
+ return (plugin.pluginType === "method" || plugin.pluginType === "property") && plugin.defaultSource !== void 0;
2130
+ }
2002
2131
  function topoOrder(descriptors) {
2003
2132
  const order = [];
2004
2133
  const visited = /* @__PURE__ */ new Set();
@@ -2013,28 +2142,89 @@ function topoOrder(descriptors) {
2013
2142
  return order;
2014
2143
  }
2015
2144
  function collectPlugins(root, materialized = /* @__PURE__ */ new Set(), configuration) {
2145
+ const rank = (plugin) => isStandIn(plugin) ? 0 : isDefault(plugin) ? 1 : 2;
2146
+ const allNodes = [];
2147
+ const seen = /* @__PURE__ */ new Set();
2148
+ const collect = (plugin) => {
2149
+ if (materialized.has(plugin.id) || seen.has(plugin)) return;
2150
+ seen.add(plugin);
2151
+ allNodes.push(plugin);
2152
+ for (const edge of edgesOf(plugin)) collect(edge);
2153
+ };
2154
+ collect(root);
2155
+ const childrenOf = /* @__PURE__ */ new Map();
2156
+ const candidatesById = /* @__PURE__ */ new Map();
2157
+ for (const node of allNodes) {
2158
+ childrenOf.set(
2159
+ node,
2160
+ edgesOf(node).filter((edge) => seen.has(edge))
2161
+ );
2162
+ const candidates = candidatesById.get(node.id);
2163
+ if (candidates) candidates.push(node);
2164
+ else candidatesById.set(node.id, [node]);
2165
+ }
2166
+ const live = new Set(allNodes);
2167
+ for (; ; ) {
2168
+ const reachable = /* @__PURE__ */ new Set();
2169
+ if (live.has(root)) reachable.add(root);
2170
+ const queue = reachable.has(root) ? [root] : [];
2171
+ while (queue.length) {
2172
+ const node = queue.pop();
2173
+ for (const child of childrenOf.get(node) ?? []) {
2174
+ if (reachable.has(child)) continue;
2175
+ reachable.add(child);
2176
+ if (live.has(child)) queue.push(child);
2177
+ }
2178
+ }
2179
+ let changed = false;
2180
+ for (const node of live) {
2181
+ if (!reachable.has(node)) {
2182
+ live.delete(node);
2183
+ changed = true;
2184
+ }
2185
+ }
2186
+ for (const candidates of candidatesById.values()) {
2187
+ let maxRank = -1;
2188
+ for (const candidate of candidates) {
2189
+ if (live.has(candidate)) maxRank = Math.max(maxRank, rank(candidate));
2190
+ }
2191
+ if (maxRank < 0) continue;
2192
+ for (const candidate of candidates) {
2193
+ if (live.has(candidate) && rank(candidate) < maxRank) {
2194
+ live.delete(candidate);
2195
+ changed = true;
2196
+ }
2197
+ }
2198
+ }
2199
+ if (!changed) break;
2200
+ }
2016
2201
  const byId = /* @__PURE__ */ new Map();
2017
- const visit = (plugin) => {
2018
- if (materialized.has(plugin.id)) return;
2019
- const existing = byId.get(plugin.id);
2020
- if (existing === plugin) return;
2021
- if (existing) {
2022
- const bothReal = !isStandIn(existing) && !isStandIn(plugin);
2023
- if (bothReal) {
2202
+ const conflictedDefaults = /* @__PURE__ */ new Set();
2203
+ const isOptional = (plugin) => "optional" in plugin && plugin.optional === true;
2204
+ for (const [id, candidates] of candidatesById) {
2205
+ const liveCandidates = candidates.filter(
2206
+ (candidate) => live.has(candidate)
2207
+ );
2208
+ const winner = liveCandidates.find((candidate) => !isOptional(candidate)) ?? liveCandidates[0];
2209
+ if (!winner) continue;
2210
+ if (liveCandidates.length > 1) {
2211
+ const winnerRank = rank(winner);
2212
+ if (winnerRank === 2) {
2024
2213
  throw new Error(
2025
- `createSdk: duplicate plugin id "${plugin.id}". Two different plugins registered under the same id.`
2214
+ `createSdk: duplicate plugin id "${id}". Two different plugins registered under the same id.`
2026
2215
  );
2027
2216
  }
2028
- if (isStandIn(existing) && !isStandIn(plugin)) {
2029
- byId.set(plugin.id, plugin);
2030
- for (const edge of edgesOf(plugin)) visit(edge);
2217
+ if (winnerRank === 1) {
2218
+ const sources = new Set(
2219
+ liveCandidates.map(
2220
+ (candidate) => candidate.defaultSource
2221
+ )
2222
+ );
2223
+ if (sources.size > 1) conflictedDefaults.add(id);
2031
2224
  }
2032
- return;
2033
2225
  }
2034
- byId.set(plugin.id, plugin);
2035
- for (const edge of edgesOf(plugin)) visit(edge);
2036
- };
2037
- visit(root);
2226
+ byId.set(id, winner);
2227
+ }
2038
2228
  if (configuration) {
2039
2229
  for (const [id, value] of Object.entries(configuration)) {
2040
2230
  const existing = byId.get(id);
@@ -2085,6 +2275,14 @@ function collectPlugins(root, materialized = /* @__PURE__ */ new Set(), configur
2085
2275
  );
2086
2276
  }
2087
2277
  }
2278
+ for (const id of conflictedDefaults) {
2279
+ const winner = byId.get(id);
2280
+ if (winner && isDefault(winner)) {
2281
+ throw new Error(
2282
+ `createSdk: conflicting defaults for "${id}". Two different plugins were declared as defaults for the same id and nothing else provides it. Register an explicit (non-default) plugin for this id to choose the winner, or give the implementations distinct ids if they are meant to coexist.`
2283
+ );
2284
+ }
2285
+ }
2088
2286
  return byId;
2089
2287
  }
2090
2288
  function bindValue({
@@ -2408,6 +2606,7 @@ function buildMethodEntries(descriptors, context, states) {
2408
2606
  const plugins = context.plugins;
2409
2607
  for (const [id, descriptor] of descriptors) {
2410
2608
  if (descriptor.pluginType !== "method") continue;
2609
+ if (isStandIn(descriptor)) continue;
2411
2610
  const out = normalizeOutput(descriptor.output);
2412
2611
  const entry = {
2413
2612
  pluginType: "method",
@@ -2460,6 +2659,16 @@ function buildMethodEntries(descriptors, context, states) {
2460
2659
  const sdk = { context };
2461
2660
  const methodAnnotator = descriptor.annotator;
2462
2661
  const boundAnnotator = methodAnnotator ? (input) => methodAnnotator({ input }) : void 0;
2662
+ const outputPolicy = () => {
2663
+ const core = resolveCoreOptions(context);
2664
+ return {
2665
+ outputSchema: descriptor.meta?.outputSchema,
2666
+ skipOutputValidation: descriptor.skipOutputValidation,
2667
+ includeOutputValidationDroppedPaths: core?.includeOutputValidationDroppedPaths,
2668
+ methodName: descriptor.name,
2669
+ adaptError: core?.adaptError
2670
+ };
2671
+ };
2463
2672
  if (out.type === "list") {
2464
2673
  entry.value = createPaginatedFunction(
2465
2674
  fold(callRun),
@@ -2470,11 +2679,15 @@ function buildMethodEntries(descriptors, context, states) {
2470
2679
  defaultPageSize: out.defaultPageSize,
2471
2680
  adaptPage: out.adaptPage,
2472
2681
  annotator: boundAnnotator,
2682
+ // Validate + strip each item against the item `outputSchema`
2683
+ // (item mode's sibling); dropped paths surface as `[].x` in the page's
2684
+ // `meta`, unioned across items.
2685
+ finalizePage: (page) => applyListOutputPolicy(page, outputPolicy()),
2473
2686
  getDeprecation: () => entry.meta?.deprecation
2474
2687
  }
2475
2688
  );
2476
2689
  } else if (out.type === "item") {
2477
- const itemCore = async (input, ctx) => callRun(input, ctx);
2690
+ const itemCore = async (input, ctx) => applyItemOutputPolicy(await callRun(input, ctx), outputPolicy());
2478
2691
  entry.value = createFunction(
2479
2692
  fold(itemCore),
2480
2693
  {
@@ -4190,7 +4403,9 @@ export {
4190
4403
  createSdk,
4191
4404
  createValidator,
4192
4405
  dangerousContextPlugin,
4406
+ declareDefault,
4193
4407
  declareMethod,
4408
+ declareOptionalMethod,
4194
4409
  declareOptionalProperty,
4195
4410
  declarePlugin,
4196
4411
  declareProperty,