@zapier/kitcore 0.11.0 → 0.13.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
@@ -76,6 +76,15 @@ function isPositional(schema) {
76
76
  }
77
77
  return false;
78
78
  }
79
+ function getNegatable(schema) {
80
+ const negatable = schema.meta?.()?.negatable;
81
+ if (negatable === true) return true;
82
+ if (typeof negatable === "string" && negatable.length > 0) return negatable;
83
+ if (schema instanceof z.ZodOptional || schema instanceof z.ZodDefault) {
84
+ return getNegatable(schema._zod.def.innerType);
85
+ }
86
+ return void 0;
87
+ }
79
88
  function openEnum(values, description) {
80
89
  return z.union([z.enum(values), z.string()]).describe(description);
81
90
  }
@@ -924,7 +933,8 @@ function isSdkPage(value) {
924
933
  }
925
934
  function createPageFunction(coreFn, {
926
935
  sdk,
927
- adaptPage
936
+ adaptPage,
937
+ finalizePage
928
938
  }) {
929
939
  const functionName = coreFn.name + "Page";
930
940
  const namedFunctions = {
@@ -937,7 +947,7 @@ function createPageFunction(coreFn, {
937
947
  `${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
948
  );
939
949
  }
940
- return page;
950
+ return finalizePage ? finalizePage(page) : page;
941
951
  } catch (error) {
942
952
  throw normalizeError(
943
953
  error,
@@ -956,9 +966,14 @@ function createPaginatedFunction(coreFn, options) {
956
966
  defaultPageSize,
957
967
  adaptPage,
958
968
  annotator,
969
+ finalizePage,
959
970
  getDeprecation
960
971
  } = options;
961
- const pageFunction = createPageFunction(coreFn, { sdk, adaptPage });
972
+ const pageFunction = createPageFunction(coreFn, {
973
+ sdk,
974
+ adaptPage,
975
+ finalizePage
976
+ });
962
977
  const functionName = name || coreFn.name;
963
978
  const namedFunctions = {
964
979
  [functionName]: function(callOptions) {
@@ -1484,6 +1499,7 @@ function defineMethod(config) {
1484
1499
  importBindings: deps.bindings,
1485
1500
  inputSchema: config.inputSchema,
1486
1501
  skipInputValidation: config.skipInputValidation,
1502
+ skipOutputValidation: config.skipOutputValidation,
1487
1503
  meta: collectLeafMeta(config),
1488
1504
  resolvers: config.resolvers,
1489
1505
  formatter: config.formatter,
@@ -1590,6 +1606,29 @@ function declareMethod(config) {
1590
1606
  }
1591
1607
  };
1592
1608
  }
1609
+ function declareOptionalMethod(config) {
1610
+ const { name, namespace } = parseId(config.id);
1611
+ const id = makeId(name, namespace);
1612
+ return {
1613
+ pluginType: "method",
1614
+ name,
1615
+ namespace,
1616
+ id,
1617
+ standIn: true,
1618
+ optional: true,
1619
+ imports: [],
1620
+ importBindings: [],
1621
+ run: () => {
1622
+ throw new Error(
1623
+ `Plugin "${id}" is an optional stand-in (declareOptionalMethod) with no implementation. Its binding is \`undefined\` unless a real plugin is registered under this id.`
1624
+ );
1625
+ }
1626
+ // Requires nothing (phantom carrier `<never, never>`): a consumer that
1627
+ // imports it still passes `createSdk`'s completeness check unprovided. The
1628
+ // `optional: true` literal drives `PluginSurface` to type the binding
1629
+ // `| undefined`.
1630
+ };
1631
+ }
1593
1632
  function defineProperty(config) {
1594
1633
  const deps = normalizeImports(config.imports);
1595
1634
  return {
@@ -1635,6 +1674,11 @@ function declareOptionalProperty(config) {
1635
1674
  // import binding is still typed `TValue | undefined` from the descriptor.
1636
1675
  };
1637
1676
  }
1677
+ function declareDefault({
1678
+ plugin
1679
+ }) {
1680
+ return { ...plugin, defaultSource: plugin };
1681
+ }
1638
1682
  function defineHook(config) {
1639
1683
  const deps = normalizeImports(config.imports);
1640
1684
  return {
@@ -1936,6 +1980,97 @@ var getRegistryPlugin = defineMethod({
1936
1980
  run: ({ imports, input }) => buildSurfaceRegistry(imports.context, input?.package)
1937
1981
  });
1938
1982
 
1983
+ // src/utils/output-policy.ts
1984
+ function isRecord(value) {
1985
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1986
+ }
1987
+ function diffDroppedPaths(raw, parsed, prefix = "") {
1988
+ const paths = [];
1989
+ walkDroppedPaths(raw, parsed, prefix, paths);
1990
+ return paths;
1991
+ }
1992
+ function walkDroppedPaths(raw, parsed, prefix, out) {
1993
+ if (Array.isArray(raw) && Array.isArray(parsed)) {
1994
+ const seen = /* @__PURE__ */ new Set();
1995
+ const length = Math.min(raw.length, parsed.length);
1996
+ for (let index = 0; index < length; index++) {
1997
+ const elementPaths = [];
1998
+ walkDroppedPaths(raw[index], parsed[index], `${prefix}[]`, elementPaths);
1999
+ for (const path of elementPaths) {
2000
+ if (seen.has(path)) continue;
2001
+ seen.add(path);
2002
+ out.push(path);
2003
+ }
2004
+ }
2005
+ return;
2006
+ }
2007
+ if (isRecord(raw) && isRecord(parsed)) {
2008
+ for (const key of Object.keys(raw)) {
2009
+ const path = prefix ? `${prefix}.${key}` : key;
2010
+ if (!(key in parsed)) {
2011
+ out.push(path);
2012
+ continue;
2013
+ }
2014
+ walkDroppedPaths(raw[key], parsed[key], path, out);
2015
+ }
2016
+ return;
2017
+ }
2018
+ }
2019
+ function parseOutput(schema, value, policy, locator) {
2020
+ const result = schema.safeParse(value);
2021
+ if (result.success) return result.data;
2022
+ const issues = result.error.issues.map((issue) => {
2023
+ const path = issue.path.length > 0 ? issue.path.join(".") : "data";
2024
+ return `${path}: ${issue.message}`;
2025
+ });
2026
+ const subject = policy.methodName ? ` for "${policy.methodName}"` : "";
2027
+ const at = locator ? ` at ${locator}` : "";
2028
+ throw createCoreError(
2029
+ {
2030
+ code: CoreErrorCode.Validation,
2031
+ message: `Output validation failed${subject}${at}:
2032
+ ${issues.join("\n ")}
2033
+
2034
+ 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.`,
2035
+ details: { zodErrors: result.error.issues, output: value }
2036
+ },
2037
+ policy.adaptError
2038
+ );
2039
+ }
2040
+ function applyItemOutputPolicy(result, policy) {
2041
+ const schema = policy.outputSchema;
2042
+ if (!schema || policy.skipOutputValidation) return result;
2043
+ if (!isRecord(result) || !("data" in result)) return result;
2044
+ const data = parseOutput(schema, result.data, policy);
2045
+ const next = { ...result, data };
2046
+ if (policy.includeOutputValidationDroppedPaths) {
2047
+ const droppedPaths = diffDroppedPaths(result.data, data);
2048
+ if (droppedPaths.length > 0) {
2049
+ next.meta = withOutputValidation(result.meta, droppedPaths);
2050
+ }
2051
+ }
2052
+ return next;
2053
+ }
2054
+ function withOutputValidation(existing, droppedPaths) {
2055
+ const base = isRecord(existing) ? existing : {};
2056
+ return { ...base, outputValidation: { droppedPaths } };
2057
+ }
2058
+ function applyListOutputPolicy(page, policy) {
2059
+ const schema = policy.outputSchema;
2060
+ if (!schema || policy.skipOutputValidation) return page;
2061
+ const data = page.data.map(
2062
+ (item, index) => parseOutput(schema, item, policy, `data[${index}]`)
2063
+ );
2064
+ const next = { ...page, data };
2065
+ if (policy.includeOutputValidationDroppedPaths) {
2066
+ const droppedPaths = diffDroppedPaths(page.data, data);
2067
+ if (droppedPaths.length > 0) {
2068
+ next.meta = { ...page.meta, outputValidation: { droppedPaths } };
2069
+ }
2070
+ }
2071
+ return next;
2072
+ }
2073
+
1939
2074
  // src/model/materialize.ts
1940
2075
  var FRAMEWORK_CONFIGURATION_IDS = /* @__PURE__ */ new Set([
1941
2076
  CORE_OPTIONS_ID
@@ -1999,6 +2134,9 @@ function edgesOf(plugin) {
1999
2134
  function isStandIn(plugin) {
2000
2135
  return (plugin.pluginType === "method" || plugin.pluginType === "property" || plugin.pluginType === "aggregate") && plugin.standIn === true;
2001
2136
  }
2137
+ function isDefault(plugin) {
2138
+ return (plugin.pluginType === "method" || plugin.pluginType === "property") && plugin.defaultSource !== void 0;
2139
+ }
2002
2140
  function topoOrder(descriptors) {
2003
2141
  const order = [];
2004
2142
  const visited = /* @__PURE__ */ new Set();
@@ -2013,28 +2151,89 @@ function topoOrder(descriptors) {
2013
2151
  return order;
2014
2152
  }
2015
2153
  function collectPlugins(root, materialized = /* @__PURE__ */ new Set(), configuration) {
2154
+ const rank = (plugin) => isStandIn(plugin) ? 0 : isDefault(plugin) ? 1 : 2;
2155
+ const allNodes = [];
2156
+ const seen = /* @__PURE__ */ new Set();
2157
+ const collect = (plugin) => {
2158
+ if (materialized.has(plugin.id) || seen.has(plugin)) return;
2159
+ seen.add(plugin);
2160
+ allNodes.push(plugin);
2161
+ for (const edge of edgesOf(plugin)) collect(edge);
2162
+ };
2163
+ collect(root);
2164
+ const childrenOf = /* @__PURE__ */ new Map();
2165
+ const candidatesById = /* @__PURE__ */ new Map();
2166
+ for (const node of allNodes) {
2167
+ childrenOf.set(
2168
+ node,
2169
+ edgesOf(node).filter((edge) => seen.has(edge))
2170
+ );
2171
+ const candidates = candidatesById.get(node.id);
2172
+ if (candidates) candidates.push(node);
2173
+ else candidatesById.set(node.id, [node]);
2174
+ }
2175
+ const live = new Set(allNodes);
2176
+ for (; ; ) {
2177
+ const reachable = /* @__PURE__ */ new Set();
2178
+ if (live.has(root)) reachable.add(root);
2179
+ const queue = reachable.has(root) ? [root] : [];
2180
+ while (queue.length) {
2181
+ const node = queue.pop();
2182
+ for (const child of childrenOf.get(node) ?? []) {
2183
+ if (reachable.has(child)) continue;
2184
+ reachable.add(child);
2185
+ if (live.has(child)) queue.push(child);
2186
+ }
2187
+ }
2188
+ let changed = false;
2189
+ for (const node of live) {
2190
+ if (!reachable.has(node)) {
2191
+ live.delete(node);
2192
+ changed = true;
2193
+ }
2194
+ }
2195
+ for (const candidates of candidatesById.values()) {
2196
+ let maxRank = -1;
2197
+ for (const candidate of candidates) {
2198
+ if (live.has(candidate)) maxRank = Math.max(maxRank, rank(candidate));
2199
+ }
2200
+ if (maxRank < 0) continue;
2201
+ for (const candidate of candidates) {
2202
+ if (live.has(candidate) && rank(candidate) < maxRank) {
2203
+ live.delete(candidate);
2204
+ changed = true;
2205
+ }
2206
+ }
2207
+ }
2208
+ if (!changed) break;
2209
+ }
2016
2210
  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) {
2211
+ const conflictedDefaults = /* @__PURE__ */ new Set();
2212
+ const isOptional = (plugin) => "optional" in plugin && plugin.optional === true;
2213
+ for (const [id, candidates] of candidatesById) {
2214
+ const liveCandidates = candidates.filter(
2215
+ (candidate) => live.has(candidate)
2216
+ );
2217
+ const winner = liveCandidates.find((candidate) => !isOptional(candidate)) ?? liveCandidates[0];
2218
+ if (!winner) continue;
2219
+ if (liveCandidates.length > 1) {
2220
+ const winnerRank = rank(winner);
2221
+ if (winnerRank === 2) {
2024
2222
  throw new Error(
2025
- `createSdk: duplicate plugin id "${plugin.id}". Two different plugins registered under the same id.`
2223
+ `createSdk: duplicate plugin id "${id}". Two different plugins registered under the same id.`
2026
2224
  );
2027
2225
  }
2028
- if (isStandIn(existing) && !isStandIn(plugin)) {
2029
- byId.set(plugin.id, plugin);
2030
- for (const edge of edgesOf(plugin)) visit(edge);
2226
+ if (winnerRank === 1) {
2227
+ const sources = new Set(
2228
+ liveCandidates.map(
2229
+ (candidate) => candidate.defaultSource
2230
+ )
2231
+ );
2232
+ if (sources.size > 1) conflictedDefaults.add(id);
2031
2233
  }
2032
- return;
2033
2234
  }
2034
- byId.set(plugin.id, plugin);
2035
- for (const edge of edgesOf(plugin)) visit(edge);
2036
- };
2037
- visit(root);
2235
+ byId.set(id, winner);
2236
+ }
2038
2237
  if (configuration) {
2039
2238
  for (const [id, value] of Object.entries(configuration)) {
2040
2239
  const existing = byId.get(id);
@@ -2085,6 +2284,14 @@ function collectPlugins(root, materialized = /* @__PURE__ */ new Set(), configur
2085
2284
  );
2086
2285
  }
2087
2286
  }
2287
+ for (const id of conflictedDefaults) {
2288
+ const winner = byId.get(id);
2289
+ if (winner && isDefault(winner)) {
2290
+ throw new Error(
2291
+ `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.`
2292
+ );
2293
+ }
2294
+ }
2088
2295
  return byId;
2089
2296
  }
2090
2297
  function bindValue({
@@ -2408,6 +2615,7 @@ function buildMethodEntries(descriptors, context, states) {
2408
2615
  const plugins = context.plugins;
2409
2616
  for (const [id, descriptor] of descriptors) {
2410
2617
  if (descriptor.pluginType !== "method") continue;
2618
+ if (isStandIn(descriptor)) continue;
2411
2619
  const out = normalizeOutput(descriptor.output);
2412
2620
  const entry = {
2413
2621
  pluginType: "method",
@@ -2460,6 +2668,16 @@ function buildMethodEntries(descriptors, context, states) {
2460
2668
  const sdk = { context };
2461
2669
  const methodAnnotator = descriptor.annotator;
2462
2670
  const boundAnnotator = methodAnnotator ? (input) => methodAnnotator({ input }) : void 0;
2671
+ const outputPolicy = () => {
2672
+ const core = resolveCoreOptions(context);
2673
+ return {
2674
+ outputSchema: descriptor.meta?.outputSchema,
2675
+ skipOutputValidation: descriptor.skipOutputValidation,
2676
+ includeOutputValidationDroppedPaths: core?.includeOutputValidationDroppedPaths,
2677
+ methodName: descriptor.name,
2678
+ adaptError: core?.adaptError
2679
+ };
2680
+ };
2463
2681
  if (out.type === "list") {
2464
2682
  entry.value = createPaginatedFunction(
2465
2683
  fold(callRun),
@@ -2470,11 +2688,15 @@ function buildMethodEntries(descriptors, context, states) {
2470
2688
  defaultPageSize: out.defaultPageSize,
2471
2689
  adaptPage: out.adaptPage,
2472
2690
  annotator: boundAnnotator,
2691
+ // Validate + strip each item against the item `outputSchema`
2692
+ // (item mode's sibling); dropped paths surface as `[].x` in the page's
2693
+ // `meta`, unioned across items.
2694
+ finalizePage: (page) => applyListOutputPolicy(page, outputPolicy()),
2473
2695
  getDeprecation: () => entry.meta?.deprecation
2474
2696
  }
2475
2697
  );
2476
2698
  } else if (out.type === "item") {
2477
- const itemCore = async (input, ctx) => callRun(input, ctx);
2699
+ const itemCore = async (input, ctx) => applyItemOutputPolicy(await callRun(input, ctx), outputPolicy());
2478
2700
  entry.value = createFunction(
2479
2701
  fold(itemCore),
2480
2702
  {
@@ -4190,7 +4412,9 @@ export {
4190
4412
  createSdk,
4191
4413
  createValidator,
4192
4414
  dangerousContextPlugin,
4415
+ declareDefault,
4193
4416
  declareMethod,
4417
+ declareOptionalMethod,
4194
4418
  declareOptionalProperty,
4195
4419
  declarePlugin,
4196
4420
  declareProperty,
@@ -4212,6 +4436,7 @@ export {
4212
4436
  getCurrentDepth,
4213
4437
  getCurrentScope,
4214
4438
  getFieldDescriptions,
4439
+ getNegatable,
4215
4440
  getOutputSchema,
4216
4441
  getRegistryPlugin,
4217
4442
  getSchemaDescription,