@jskit-ai/connectors-catalog 0.1.1

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.
Files changed (399) hide show
  1. package/README.md +192 -0
  2. package/docs/ai-upstream-notices.md +51 -0
  3. package/docs/ai.md +191 -0
  4. package/docs/airtable.md +130 -0
  5. package/docs/algolia.md +190 -0
  6. package/docs/amazon-redshift.md +229 -0
  7. package/docs/amplitude.md +210 -0
  8. package/docs/apify.md +162 -0
  9. package/docs/apollo-io.md +200 -0
  10. package/docs/asana.md +126 -0
  11. package/docs/ashby.md +188 -0
  12. package/docs/atlassian.md +224 -0
  13. package/docs/attention.md +162 -0
  14. package/docs/aws-athena.md +150 -0
  15. package/docs/aws-credentials.md +75 -0
  16. package/docs/aws-s3.md +193 -0
  17. package/docs/bigquery.md +146 -0
  18. package/docs/brevo.md +122 -0
  19. package/docs/calendly.md +158 -0
  20. package/docs/canva.md +191 -0
  21. package/docs/chargebee.md +192 -0
  22. package/docs/clay.md +194 -0
  23. package/docs/clickhouse.md +242 -0
  24. package/docs/confidence-exp.md +74 -0
  25. package/docs/confidence-flags.md +75 -0
  26. package/docs/confidence-oauth.md +137 -0
  27. package/docs/contentful.md +123 -0
  28. package/docs/databricks.md +180 -0
  29. package/docs/dbt-semantic-layer.md +175 -0
  30. package/docs/elevenlabs.md +102 -0
  31. package/docs/figma.md +132 -0
  32. package/docs/firebase-cloud-messaging.md +262 -0
  33. package/docs/firecrawl.md +137 -0
  34. package/docs/fireflies.md +95 -0
  35. package/docs/fireworks-ai.md +53 -0
  36. package/docs/gatewayapi.md +113 -0
  37. package/docs/gemini-enterprise.md +224 -0
  38. package/docs/github-api.md +133 -0
  39. package/docs/gitlab-api.md +138 -0
  40. package/docs/gmail.md +133 -0
  41. package/docs/gong.md +205 -0
  42. package/docs/google-ads.md +156 -0
  43. package/docs/google-analytics.md +202 -0
  44. package/docs/google-docs.md +105 -0
  45. package/docs/google-drive.md +108 -0
  46. package/docs/google-maps-platform.md +208 -0
  47. package/docs/google-oauth.md +123 -0
  48. package/docs/google-search-console.md +124 -0
  49. package/docs/google-sheets.md +94 -0
  50. package/docs/google-slides.md +95 -0
  51. package/docs/granola.md +178 -0
  52. package/docs/hex.md +161 -0
  53. package/docs/heygen.md +124 -0
  54. package/docs/hubspot.md +142 -0
  55. package/docs/incident-io.md +118 -0
  56. package/docs/inngest.md +178 -0
  57. package/docs/klipy.md +149 -0
  58. package/docs/lexware.md +144 -0
  59. package/docs/lightspeed.md +193 -0
  60. package/docs/linear.md +137 -0
  61. package/docs/linkedin.md +196 -0
  62. package/docs/logo-dev.md +152 -0
  63. package/docs/mailgun.md +153 -0
  64. package/docs/mapbox.md +174 -0
  65. package/docs/microsoft-excel.md +97 -0
  66. package/docs/microsoft-fabric.md +249 -0
  67. package/docs/microsoft-oauth.md +143 -0
  68. package/docs/microsoft-onedrive.md +76 -0
  69. package/docs/microsoft-onenote.md +75 -0
  70. package/docs/microsoft-outlook.md +90 -0
  71. package/docs/microsoft-powerpoint.md +119 -0
  72. package/docs/microsoft-sharepoint.md +75 -0
  73. package/docs/microsoft-teams.md +72 -0
  74. package/docs/microsoft-word.md +119 -0
  75. package/docs/miro.md +117 -0
  76. package/docs/n8n.md +169 -0
  77. package/docs/notion.md +137 -0
  78. package/docs/oura.md +161 -0
  79. package/docs/paddle.md +168 -0
  80. package/docs/perplexity.md +85 -0
  81. package/docs/pipedrive.md +120 -0
  82. package/docs/polar.md +63 -0
  83. package/docs/posthog.md +227 -0
  84. package/docs/prestashop.md +151 -0
  85. package/docs/replicate.md +60 -0
  86. package/docs/resend.md +153 -0
  87. package/docs/salesforce.md +234 -0
  88. package/docs/sanity.md +122 -0
  89. package/docs/semrush.md +276 -0
  90. package/docs/sentry.md +112 -0
  91. package/docs/sevdesk.md +107 -0
  92. package/docs/shopify.md +206 -0
  93. package/docs/slack.md +257 -0
  94. package/docs/snowflake.md +271 -0
  95. package/docs/storyblok.md +130 -0
  96. package/docs/stripe.md +77 -0
  97. package/docs/supabase.md +51 -0
  98. package/docs/tally.md +105 -0
  99. package/docs/telegram.md +165 -0
  100. package/docs/tiktok.md +201 -0
  101. package/docs/twilio.md +284 -0
  102. package/docs/twitch.md +428 -0
  103. package/docs/wave.md +440 -0
  104. package/docs/wix.md +1249 -0
  105. package/docs/wiz.md +177 -0
  106. package/docs/woocommerce.md +400 -0
  107. package/docs/wordpress-com.md +358 -0
  108. package/docs/wordpress-self-hosted.md +252 -0
  109. package/docs/workday.md +292 -0
  110. package/docs/x-twitter.md +188 -0
  111. package/docs/xero.md +429 -0
  112. package/docs/zoho-books.md +214 -0
  113. package/docs/zoho-crm.md +192 -0
  114. package/package.json +153 -0
  115. package/patterns/ai-connections/PATTERN.md +84 -0
  116. package/patterns/ai-connections/example/ai-model.js +11 -0
  117. package/patterns/ai-connections/example/perplexity-answer.js +29 -0
  118. package/patterns/api-key-connection/PATTERN.md +368 -0
  119. package/patterns/api-key-connection/example/connections.js +18 -0
  120. package/patterns/api-key-connection/example/integrations.json +19 -0
  121. package/patterns/assistant-mcp/PATTERN.md +110 -0
  122. package/patterns/assistant-mcp/example/integrations.json +13 -0
  123. package/patterns/assistant-mcp-oauth/PATTERN.md +235 -0
  124. package/patterns/assistant-mcp-oauth/example/integrations.json +102 -0
  125. package/patterns/aws-storage-queries/PATTERN.md +169 -0
  126. package/patterns/aws-storage-queries/example/formats/data-formats.js +35 -0
  127. package/patterns/aws-storage-queries/example/formats/package-lock.json +49 -0
  128. package/patterns/aws-storage-queries/example/formats/package.json +12 -0
  129. package/patterns/aws-storage-queries/example/formats/verify-formats.mjs +37 -0
  130. package/patterns/aws-storage-queries/example/integrations.json +38 -0
  131. package/patterns/aws-storage-queries/example/s3-transfer.js +34 -0
  132. package/patterns/event-delivery/PATTERN.md +151 -0
  133. package/patterns/event-delivery/example/integrations.json +17 -0
  134. package/patterns/firebase-messaging/PATTERN.md +156 -0
  135. package/patterns/firebase-messaging/example/server/notifications.js +23 -0
  136. package/patterns/google-ads-search/PATTERN.md +92 -0
  137. package/patterns/google-ads-search/example/ads-setup.js +25 -0
  138. package/patterns/oauth-connection/PATTERN.md +724 -0
  139. package/patterns/oauth-connection/example/integrations.json +20 -0
  140. package/patterns/paddle-catalogue/PATTERN.md +78 -0
  141. package/patterns/paddle-catalogue/example/create-products.js +38 -0
  142. package/patterns/public-image/PATTERN.md +84 -0
  143. package/patterns/public-image/example/integrations.json +13 -0
  144. package/patterns/public-image/example/logo-url.js +16 -0
  145. package/patterns/redshift-queries/PATTERN.md +150 -0
  146. package/patterns/redshift-queries/example/integrations.json +33 -0
  147. package/patterns/source-scanning/PATTERN.md +83 -0
  148. package/patterns/source-scanning/example/source-scanner.js +13 -0
  149. package/src/client/firebase-cloud-messaging.js +10 -0
  150. package/src/client/google-analytics.js +72 -0
  151. package/src/client/google-maps-platform.js +39 -0
  152. package/src/client/logo-dev.js +36 -0
  153. package/src/server/ai.js +46 -0
  154. package/src/server/airtable.js +67 -0
  155. package/src/server/algolia.js +63 -0
  156. package/src/server/amazon-redshift.js +96 -0
  157. package/src/server/amplitude.js +31 -0
  158. package/src/server/apify.js +68 -0
  159. package/src/server/apollo-io.js +85 -0
  160. package/src/server/asana.js +64 -0
  161. package/src/server/ashby.js +61 -0
  162. package/src/server/atlassian.js +32 -0
  163. package/src/server/attention.js +93 -0
  164. package/src/server/aws-athena.js +82 -0
  165. package/src/server/aws-s3.js +49 -0
  166. package/src/server/awsTransport.js +75 -0
  167. package/src/server/bigquery.js +97 -0
  168. package/src/server/brevo.js +97 -0
  169. package/src/server/calendly.js +77 -0
  170. package/src/server/canva.js +13 -0
  171. package/src/server/chargebee.js +88 -0
  172. package/src/server/clay.js +78 -0
  173. package/src/server/clickhouse.js +66 -0
  174. package/src/server/confidence.js +29 -0
  175. package/src/server/contentful.js +40 -0
  176. package/src/server/databricks.js +55 -0
  177. package/src/server/dbt-semantic-layer.js +95 -0
  178. package/src/server/elevenlabs.js +81 -0
  179. package/src/server/figma.js +24 -0
  180. package/src/server/firebase-cloud-messaging.js +111 -0
  181. package/src/server/firecrawl.js +70 -0
  182. package/src/server/fireflies.js +31 -0
  183. package/src/server/fireworks-ai.js +16 -0
  184. package/src/server/gatewayapi.js +48 -0
  185. package/src/server/gemini-enterprise.js +47 -0
  186. package/src/server/github-api.js +90 -0
  187. package/src/server/gitlab-api.js +67 -0
  188. package/src/server/gmail.js +60 -0
  189. package/src/server/gong.js +52 -0
  190. package/src/server/google-ads-search.js +134 -0
  191. package/src/server/google-ads.js +80 -0
  192. package/src/server/google-docs.js +36 -0
  193. package/src/server/google-drive.js +71 -0
  194. package/src/server/google-maps-platform.js +90 -0
  195. package/src/server/google-search-console.js +31 -0
  196. package/src/server/google-sheets.js +60 -0
  197. package/src/server/google-slides.js +36 -0
  198. package/src/server/google.js +61 -0
  199. package/src/server/granola.js +115 -0
  200. package/src/server/graphqlOperation.js +15 -0
  201. package/src/server/hex.js +29 -0
  202. package/src/server/heygen.js +54 -0
  203. package/src/server/hubspot.js +85 -0
  204. package/src/server/incident-io.js +68 -0
  205. package/src/server/inngest.js +86 -0
  206. package/src/server/jsonOperation.js +34 -0
  207. package/src/server/klipy.js +34 -0
  208. package/src/server/lexware.js +59 -0
  209. package/src/server/lightspeed.js +82 -0
  210. package/src/server/linear.js +52 -0
  211. package/src/server/linkedin.js +74 -0
  212. package/src/server/mailgun.js +76 -0
  213. package/src/server/mapbox.js +46 -0
  214. package/src/server/mcpRegistration.js +41 -0
  215. package/src/server/mcpTransport.js +60 -0
  216. package/src/server/microsoft-excel.js +67 -0
  217. package/src/server/microsoft-fabric.js +81 -0
  218. package/src/server/microsoft-onedrive.js +58 -0
  219. package/src/server/microsoft-onenote.js +52 -0
  220. package/src/server/microsoft-outlook.js +58 -0
  221. package/src/server/microsoft-powerpoint.js +5 -0
  222. package/src/server/microsoft-sharepoint.js +46 -0
  223. package/src/server/microsoft-teams.js +29 -0
  224. package/src/server/microsoft-word.js +5 -0
  225. package/src/server/microsoft.js +88 -0
  226. package/src/server/miro.js +29 -0
  227. package/src/server/n8n.js +78 -0
  228. package/src/server/notion.js +76 -0
  229. package/src/server/oura.js +48 -0
  230. package/src/server/paddle.js +83 -0
  231. package/src/server/perplexity.js +15 -0
  232. package/src/server/pipedrive.js +95 -0
  233. package/src/server/polar.js +20 -0
  234. package/src/server/posthog.js +44 -0
  235. package/src/server/prestashop.js +42 -0
  236. package/src/server/replicate.js +42 -0
  237. package/src/server/resend.js +93 -0
  238. package/src/server/salesforce.js +84 -0
  239. package/src/server/sanity.js +31 -0
  240. package/src/server/semrush.js +359 -0
  241. package/src/server/sentry.js +30 -0
  242. package/src/server/sevdesk.js +20 -0
  243. package/src/server/shopify.js +99 -0
  244. package/src/server/slack.js +143 -0
  245. package/src/server/snowflake.js +139 -0
  246. package/src/server/storyblok.js +49 -0
  247. package/src/server/stripe.js +13 -0
  248. package/src/server/supabase.js +13 -0
  249. package/src/server/tally.js +78 -0
  250. package/src/server/telegram.js +55 -0
  251. package/src/server/tiktok.js +86 -0
  252. package/src/server/twilio.js +90 -0
  253. package/src/server/twitch.js +409 -0
  254. package/src/server/wave.js +520 -0
  255. package/src/server/wix.js +1048 -0
  256. package/src/server/wiz.js +117 -0
  257. package/src/server/woocommerce.js +412 -0
  258. package/src/server/wordpress-com.js +236 -0
  259. package/src/server/wordpress-self-hosted.js +134 -0
  260. package/src/server/workday.js +136 -0
  261. package/src/server/x-twitter.js +110 -0
  262. package/src/server/xero.js +397 -0
  263. package/src/server/zoho-books.js +90 -0
  264. package/src/server/zoho-crm.js +89 -0
  265. package/src/server/zohoOAuth.js +21 -0
  266. package/src/shared/ai-models.json +8049 -0
  267. package/src/shared/ai.js +63 -0
  268. package/src/shared/algolia.js +29 -0
  269. package/src/shared/amazon-redshift.js +51 -0
  270. package/src/shared/amplitude.js +28 -0
  271. package/src/shared/atlassian.js +43 -0
  272. package/src/shared/aws.js +72 -0
  273. package/src/shared/canva.js +66 -0
  274. package/src/shared/chargebee.js +26 -0
  275. package/src/shared/clickhouse.js +38 -0
  276. package/src/shared/confidence.js +28 -0
  277. package/src/shared/databricks.js +33 -0
  278. package/src/shared/dbt-semantic-layer.js +32 -0
  279. package/src/shared/definitions.js +140 -0
  280. package/src/shared/figma.js +21 -0
  281. package/src/shared/firebase-cloud-messaging.js +45 -0
  282. package/src/shared/gemini-enterprise.js +33 -0
  283. package/src/shared/gong.js +33 -0
  284. package/src/shared/google-ads-search.js +31 -0
  285. package/src/shared/google-ads.js +28 -0
  286. package/src/shared/google-maps-platform.js +28 -0
  287. package/src/shared/google.js +133 -0
  288. package/src/shared/granola.js +25 -0
  289. package/src/shared/hex.js +28 -0
  290. package/src/shared/inngest.js +35 -0
  291. package/src/shared/lightspeed.js +75 -0
  292. package/src/shared/linkedin.js +27 -0
  293. package/src/shared/logo-dev.js +21 -0
  294. package/src/shared/mapbox.js +38 -0
  295. package/src/shared/mcp.js +92 -0
  296. package/src/shared/microsoft-fabric.js +47 -0
  297. package/src/shared/microsoft.js +231 -0
  298. package/src/shared/miro.js +27 -0
  299. package/src/shared/oura.js +23 -0
  300. package/src/shared/posthog.js +33 -0
  301. package/src/shared/prestashop.js +23 -0
  302. package/src/shared/salesforce.js +36 -0
  303. package/src/shared/semrush.js +24 -0
  304. package/src/shared/shopify.js +51 -0
  305. package/src/shared/siteUrl.js +12 -0
  306. package/src/shared/slack.js +88 -0
  307. package/src/shared/snowflake.js +55 -0
  308. package/src/shared/tiktok.js +22 -0
  309. package/src/shared/tokens.js +747 -0
  310. package/src/shared/twilio.js +41 -0
  311. package/src/shared/twitch.js +49 -0
  312. package/src/shared/wave.js +52 -0
  313. package/src/shared/wix.js +40 -0
  314. package/src/shared/wiz.js +41 -0
  315. package/src/shared/wordpress-com.js +30 -0
  316. package/src/shared/wordpress.js +69 -0
  317. package/src/shared/workday.js +50 -0
  318. package/src/shared/x-twitter.js +19 -0
  319. package/src/shared/xero.js +46 -0
  320. package/src/shared/zoho-books.js +39 -0
  321. package/src/shared/zoho-crm.js +43 -0
  322. package/src/shared/zoho-regions.js +12 -0
  323. package/test/ai.test.js +136 -0
  324. package/test/airtable.test.js +125 -0
  325. package/test/algolia.test.js +207 -0
  326. package/test/amazon-redshift.test.js +272 -0
  327. package/test/amplitude.test.js +239 -0
  328. package/test/apify.test.js +93 -0
  329. package/test/apollo-io.test.js +125 -0
  330. package/test/asana.test.js +99 -0
  331. package/test/ashby.test.js +97 -0
  332. package/test/atlassian.test.js +249 -0
  333. package/test/attention.test.js +79 -0
  334. package/test/aws.test.js +405 -0
  335. package/test/brevo.test.js +71 -0
  336. package/test/calendly.test.js +136 -0
  337. package/test/canva.test.js +258 -0
  338. package/test/chargebee.test.js +195 -0
  339. package/test/clickhouse.test.js +278 -0
  340. package/test/confidence.test.js +205 -0
  341. package/test/databricks.test.js +253 -0
  342. package/test/dbt-semantic-layer.test.js +221 -0
  343. package/test/designMcp.test.js +336 -0
  344. package/test/elevenlabs.test.js +52 -0
  345. package/test/firebase-cloud-messaging.test.js +242 -0
  346. package/test/firecrawl.test.js +66 -0
  347. package/test/gemini-enterprise.test.js +218 -0
  348. package/test/github-api.test.js +167 -0
  349. package/test/gitlab-api.test.js +160 -0
  350. package/test/gong.test.js +192 -0
  351. package/test/google-ads.test.js +350 -0
  352. package/test/google-maps-platform.test.js +191 -0
  353. package/test/google.test.js +710 -0
  354. package/test/granola.test.js +240 -0
  355. package/test/hex.test.js +253 -0
  356. package/test/heygen.test.js +121 -0
  357. package/test/hubspot.test.js +103 -0
  358. package/test/inngest.test.js +219 -0
  359. package/test/lightspeed.test.js +250 -0
  360. package/test/linear.test.js +146 -0
  361. package/test/linkedin.test.js +243 -0
  362. package/test/logo-dev.test.js +79 -0
  363. package/test/mailgun.test.js +107 -0
  364. package/test/mapbox.test.js +181 -0
  365. package/test/mcp.test.js +227 -0
  366. package/test/microsoft-fabric.test.js +258 -0
  367. package/test/microsoft.test.js +554 -0
  368. package/test/n8nOAuth.test.js +188 -0
  369. package/test/notionOAuth.test.js +183 -0
  370. package/test/oura.test.js +211 -0
  371. package/test/paddleCatalogue.test.js +51 -0
  372. package/test/paddlePermissions.test.js +101 -0
  373. package/test/perplexityComposition.test.js +39 -0
  374. package/test/pipedriveOAuth.test.js +140 -0
  375. package/test/posthog.test.js +200 -0
  376. package/test/prestashop.test.js +207 -0
  377. package/test/resend.test.js +163 -0
  378. package/test/salesforce.test.js +276 -0
  379. package/test/semrush.test.js +479 -0
  380. package/test/sentry.test.js +35 -0
  381. package/test/shopify.test.js +247 -0
  382. package/test/slack.test.js +320 -0
  383. package/test/snowflake.test.js +391 -0
  384. package/test/tally.test.js +75 -0
  385. package/test/telegram.test.js +58 -0
  386. package/test/tiktok.test.js +243 -0
  387. package/test/tokens.test.js +1079 -0
  388. package/test/twilio.test.js +221 -0
  389. package/test/twitch.test.js +573 -0
  390. package/test/wave.test.js +678 -0
  391. package/test/wix.test.js +1099 -0
  392. package/test/wiz.test.js +185 -0
  393. package/test/wordpress-com.test.js +469 -0
  394. package/test/wordpress.test.js +660 -0
  395. package/test/workday.test.js +314 -0
  396. package/test/x-twitter.test.js +256 -0
  397. package/test/xero.test.js +700 -0
  398. package/test/zoho-books.test.js +253 -0
  399. package/test/zoho-crm.test.js +223 -0
@@ -0,0 +1,520 @@
1
+ import { createSchema } from "json-rest-schema";
2
+ import { ConnectorError } from "@jskit-ai/connectors-core/server";
3
+ import { waveDefinition } from "../shared/wave.js";
4
+ import { graphqlOperation } from "./graphqlOperation.js";
5
+
6
+ const endpoint = "https://gql.waveapps.com/graphql/public";
7
+ const identifier = (value) => typeof value === "string" && value.length > 0 && value.length <= 2048 && !/[\s\p{Cc}]/u.test(value);
8
+ const businessId = { type: "string", required: true, validator: (value) => identifier(value) || "Choose a business ID returned by Wave." };
9
+ const paging = {
10
+ page: { type: "integer", min: 1, max: 2_147_483_647, defaultTo: 1 },
11
+ pageSize: { type: "integer", min: 1, max: 100, defaultTo: 20 }
12
+ };
13
+ const pageFields = "pageInfo { currentPage totalPages totalCount }";
14
+ const validPage = (value, validNode) => Array.isArray(value?.edges) && value.edges.length <= 100 &&
15
+ value.edges.every((edge) => edge?.node === null || validNode(edge?.node)) &&
16
+ Number.isSafeInteger(value.pageInfo?.currentPage) && value.pageInfo.currentPage >= 1 &&
17
+ Number.isSafeInteger(value.pageInfo.totalPages) && value.pageInfo.totalPages >= 0 &&
18
+ Number.isSafeInteger(value.pageInfo.totalCount) && value.pageInfo.totalCount >= value.edges.length;
19
+
20
+ const decimal = value => typeof value === "string" && /^-?\d+(?:\.\d+)?$/u.test(value);
21
+ const namedRecord = value => identifier(value?.id) && typeof value.name === "string";
22
+ function businessPage(resource, selection, scopes, validateNode, sort = "") {
23
+ return {
24
+ ...graphqlOperation(endpoint,
25
+ `query ConnectorWave_${resource}($businessId: ID!, $page: Int!, $pageSize: Int!) { business(id: $businessId) { id ${resource}(page: $page, pageSize: $pageSize${sort}) { ${pageFields} edges { node { ${selection} } } } } }`,
26
+ { businessId, ...paging }, data => identifier(data?.business?.id) && validPage(data.business[resource], validateNode)),
27
+ scopes
28
+ };
29
+ }
30
+
31
+ const customerFields = {
32
+ name: { type: "string", minLength: 1, maxLength: 255 },
33
+ firstName: { type: "string", maxLength: 255 },
34
+ lastName: { type: "string", maxLength: 255 },
35
+ email: { type: "string", maxLength: 254 },
36
+ phone: { type: "string", maxLength: 100 },
37
+ mobile: { type: "string", maxLength: 100 },
38
+ displayId: { type: "string", maxLength: 255 },
39
+ internalNotes: { type: "string", maxLength: 10000 }
40
+ };
41
+
42
+ const decimalInput = { type: "none",
43
+ validator: value => typeof value === "string" && value.length <= 100 && /^-?\d+(?:\.\d{1,8})?$/u.test(value) || "Use a decimal string with at most eight decimal places." };
44
+ const dateInput = { type: "string", validator: value => /^\d{4}-\d{2}-\d{2}$/u.test(value) &&
45
+ !Number.isNaN(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value || "Use a valid YYYY-MM-DD date." };
46
+ const invoiceFields = {
47
+ customerId: businessId,
48
+ status: { type: "string", enum: ["DRAFT", "SAVED"] },
49
+ currency: { type: "string", validator: value => /^[A-Z]{3}$/u.test(value) || "Use a three-letter currency code." },
50
+ title: { type: "string", minLength: 1, maxLength: 255 },
51
+ invoiceNumber: { type: "string", minLength: 1, maxLength: 255 },
52
+ poNumber: { type: "string", maxLength: 255 },
53
+ invoiceDate: dateInput, dueDate: dateInput, exchangeRate: decimalInput,
54
+ memo: { type: "string", maxLength: 10000 }, footer: { type: "string", maxLength: 10000 },
55
+ items: { type: "array", required: true, validator: values => values.length >= 1 && values.length <= 100 || "Use 1–100 invoice items.",
56
+ items: { type: "object", schema: createSchema({
57
+ productId: businessId,
58
+ description: { type: "string", maxLength: 10000 },
59
+ quantity: decimalInput, unitPrice: decimalInput,
60
+ taxes: { type: "array", validator: values => values.length <= 20 || "Use at most 20 taxes per item.",
61
+ items: { type: "object", schema: createSchema({ salesTaxId: businessId }) } }
62
+ }) } }
63
+ };
64
+ const invoiceSelection = "id invoiceNumber invoiceDate status currency { code } customer { id } business { id } total { value currency { code } } amountDue { value currency { code } } pdfUrl viewUrl";
65
+ const validInvoice = value => identifier(value?.id) && typeof value.invoiceNumber === "string" && typeof value.status === "string" &&
66
+ identifier(value.business?.id) && identifier(value.customer?.id) && typeof value.currency?.code === "string" &&
67
+ decimal(value.total?.value) && decimal(value.amountDue?.value);
68
+ const productFields = {
69
+ name: { type: "string", minLength: 1, maxLength: 255 },
70
+ description: { type: "string", maxLength: 10000 },
71
+ unitPrice: { type: "none", validator: value => typeof value === "string" && value.length <= 100 && /^-?\d+(?:\.\d{1,5})?$/u.test(value) || "Use a decimal string with at most five decimal places." },
72
+ incomeAccountId: { ...businessId, required: false }, expenseAccountId: { ...businessId, required: false },
73
+ defaultSalesTaxIds: { type: "array", validator: values => values.length <= 20 && new Set(values).size === values.length || "Choose at most 20 distinct taxes.",
74
+ items: businessId }
75
+ };
76
+ function productMutation(action) {
77
+ return {
78
+ ...graphqlOperation(endpoint,
79
+ `mutation ConnectorWaveProduct${action}($input: Product${action}Input!) { product${action}(input: $input) { didSucceed inputErrors { code path } product { id name unitPrice business { id } } } }`,
80
+ { businessId, input: { type: "object", required: true, schema: createSchema(action === "Create" ? {
81
+ ...productFields, name: { ...productFields.name, required: true }, unitPrice: { ...productFields.unitPrice, required: true }
82
+ } : { ...productFields, id: businessId }) } }, data => {
83
+ const value = data?.[`product${action}`];
84
+ return value?.didSucceed === true && (value.inputErrors === null || (Array.isArray(value.inputErrors) && !value.inputErrors.length)) &&
85
+ namedRecord(value.product) && identifier(value.product.business?.id) && decimal(value.product.unitPrice);
86
+ }), scopes: ["product:write", "product:*"]
87
+ };
88
+ }
89
+ const transactionAmount = { type: "none", required: true, validator: value => typeof value === "string" && value.length <= 100 &&
90
+ /^\d+(?:\.\d{1,2})?$/u.test(value) && /[1-9]/u.test(value) || "Use a positive decimal-string amount with at most two places." };
91
+ const accountSubtypes = ["CASH_AND_BANK", "COST_OF_GOODS_SOLD", "CREDIT_CARD", "CUSTOMER_PREPAYMENTS_AND_CREDITS",
92
+ "DEPRECIATION_AND_AMORTIZATION", "DISCOUNTS", "DUE_FOR_PAYROLL", "DUE_TO_YOU_AND_OTHER_OWNERS", "EXPENSE", "INCOME",
93
+ "INVENTORY", "LOANS", "MONEY_IN_TRANSIT", "NON_RETAINED_EARNINGS", "OTHER_CURRENT_ASSETS", "OTHER_CURRENT_LIABILITY",
94
+ "OTHER_INCOME", "OTHER_LONG_TERM_ASSETS", "OTHER_LONG_TERM_LIABILITY", "PAYABLE", "PAYMENT_PROCESSING_FEES",
95
+ "PAYROLL_EXPENSES", "PROPERTY_PLANT_EQUIPMENT", "RECEIVABLE", "RETAINED_EARNINGS", "VENDOR_PREPAYMENTS_AND_CREDITS"];
96
+ const accountFields = { name: productFields.name, description: productFields.description, displayId: { type: "string", maxLength: 255 } };
97
+ const taxRate = { type: "none", required: true, validator: value => typeof value === "string" && value.length <= 100 && /^\d+(?:\.\d{1,6})?$/u.test(value) || "Use a nonnegative decimal-string rate with at most six places (0.15 means 15%)." };
98
+ const taxFields = { name: productFields.name, abbreviation: { type: "string", minLength: 1, maxLength: 10 },
99
+ description: productFields.description, taxNumber: { type: "string", maxLength: 255 }, showTaxNumberOnInvoices: { type: "boolean" } };
100
+ const accountSelection = "id name sequence currency { code } subtype { value } isArchived business { id }";
101
+ const taxSelection = "id name abbreviation rate rates { effective rate } isArchived business { id }";
102
+ const validAccount = value => namedRecord(value) && identifier(value.business?.id) && Number.isSafeInteger(value.sequence) && value.sequence >= 0 &&
103
+ typeof value.currency?.code === "string" && typeof value.subtype?.value === "string" && typeof value.isArchived === "boolean";
104
+ const validTax = value => namedRecord(value) && identifier(value.business?.id) && typeof value.abbreviation === "string" && decimal(value.rate) &&
105
+ typeof value.isArchived === "boolean" && Array.isArray(value.rates) && value.rates.every(rate => typeof rate?.effective === "string" && decimal(rate.rate));
106
+ function accountingMutation(kind, action, fields) {
107
+ const resource = kind === "Account" ? "account" : "salesTax";
108
+ const scope = kind === "Account" ? "account" : "sales_tax";
109
+ return {
110
+ ...graphqlOperation(endpoint,
111
+ `mutation ConnectorWave${kind}${action}($input: ${kind}${action}Input!) { ${resource}${action}(input: $input) { didSucceed inputErrors { code path } ${action === "Archive" ? "" : `${resource} { ${kind === "Account" ? accountSelection : taxSelection} }`} } }`,
112
+ { businessId, input: { type: "object", required: true, schema: createSchema(fields) } }, data => {
113
+ const outcome = data?.[`${resource}${action}`];
114
+ return outcome?.didSucceed === true && (outcome.inputErrors === null || (Array.isArray(outcome.inputErrors) && !outcome.inputErrors.length)) &&
115
+ (action === "Archive" || (kind === "Account" ? validAccount(outcome[resource]) : validTax(outcome[resource])));
116
+ }), scopes: [`${scope}:write`, `${scope}:*`]
117
+ };
118
+ }
119
+ const estimateStatuses = ["ACCEPTED", "APPROVED", "CONVERTED", "DELETED", "DRAFT", "EXPIRED", "REJECTED", "SENT", "VIEWED"];
120
+ const estimateFields = {
121
+ customerId: businessId, status: { type: "string", enum: ["DRAFT"], defaultTo: "DRAFT" },
122
+ currency: invoiceFields.currency, title: invoiceFields.title, subhead: { type: "string", maxLength: 255 },
123
+ estimateNumber: invoiceFields.invoiceNumber, poNumber: invoiceFields.poNumber,
124
+ estimateDate: dateInput, dueDate: dateInput, exchangeRate: decimalInput,
125
+ memo: invoiceFields.memo, footer: invoiceFields.footer,
126
+ items: { ...invoiceFields.items, items: { type: "object", schema: createSchema({
127
+ productId: businessId, name: { type: "string", maxLength: 255 },
128
+ description: { type: "string", maxLength: 10000 }, quantity: decimalInput,
129
+ unitPrice: { ...decimalInput, required: true },
130
+ taxes: { type: "array", validator: values => values.length <= 20 || "Use at most 20 taxes per item.",
131
+ items: { type: "object", schema: createSchema({ salesTaxId: businessId }) } }
132
+ }) } }
133
+ };
134
+ const estimateSelection = "id estimateNumber estimateDate dueDate title status exchangeRate currency { code } customer { id business { id } } total { value currency { code } } amountDue { value currency { code } } pdfUrl viewUrl";
135
+ const validEstimate = value => identifier(value?.id) && typeof value.estimateNumber === "string" && estimateStatuses.includes(value.status) &&
136
+ identifier(value.customer?.id) && identifier(value.customer.business?.id) && typeof value.currency?.code === "string" &&
137
+ decimal(value.total?.value) && decimal(value.amountDue?.value);
138
+ const sendFields = {
139
+ to: { type: "array", required: true, validator: values => values.length >= 1 && values.length <= 20 || "Use 1–20 recipients.",
140
+ items: { type: "string", maxLength: 254, validator: value => /^[^\s@]+@[^\s@]+\.[^\s@]+$/u.test(value) || "Enter an email address." } },
141
+ subject: { type: "string", maxLength: 255 }, message: { type: "string", maxLength: 10000 },
142
+ attachPDF: { type: "boolean", required: true }, ccMyself: { type: "boolean" }
143
+ };
144
+ function documentMutation(kind, action, fields, permission) {
145
+ const resource = kind.toLowerCase();
146
+ const key = `${resource}${action}`;
147
+ const selection = kind === "Invoice" ? invoiceSelection : estimateSelection;
148
+ const validate = kind === "Invoice" ? validInvoice : validEstimate;
149
+ return {
150
+ ...graphqlOperation(endpoint,
151
+ `mutation ConnectorWave${kind}${action}($input: ${kind}${action}Input!) { ${key}(input: $input) { didSucceed inputErrors { code path } ${action === "Send" ? "" : `${resource} { ${selection} }`} } }`,
152
+ { businessId, input: { type: "object", required: true, schema: createSchema(fields) } },
153
+ data => data?.[key]?.didSucceed === true && (data[key].inputErrors === null ||
154
+ (Array.isArray(data[key].inputErrors) && data[key].inputErrors.length === 0)) &&
155
+ (action === "Send" || validate(data[key][resource]))),
156
+ scopes: [permission, `${resource}:*`]
157
+ };
158
+ }
159
+
160
+ const waveProvider = Object.freeze({
161
+ ...waveDefinition,
162
+ oauth: { issuer: "https://api.waveapps.com", authorization_endpoint: "https://api.waveapps.com/oauth2/authorize/", token_endpoint: "https://api.waveapps.com/oauth2/token/" },
163
+ refreshRequiresRedirectUri: true,
164
+ apiOrigins: ["https://gql.waveapps.com"], checkOperation: "user.read",
165
+ async normalizeTokenResponse(response) {
166
+ if (response.status === 403) {
167
+ throw new ConnectorError("connector_permission_denied", "Wave denied token access. Check the business subscription and permissions.", { statusCode: 403 });
168
+ }
169
+ if (response.ok) {
170
+ let value;
171
+ try { value = await response.clone().json(); } catch { /* reported below */ }
172
+ if (typeof value?.scope !== "string" || !/^[^\s]+(?: [^\s]+)*$/u.test(value.scope)) {
173
+ throw new ConnectorError("connector_response_invalid", "Wave returned an invalid permission grant.", { statusCode: 502 });
174
+ }
175
+ }
176
+ return response;
177
+ },
178
+ operations: {
179
+ "user.read": {
180
+ ...graphqlOperation(endpoint, "query ConnectorWaveUser { user { id firstName lastName defaultEmail } }", {},
181
+ (data) => identifier(data?.user?.id) && typeof data.user.defaultEmail === "string"),
182
+ scopes: ["user:read", "user:*"]
183
+ },
184
+ "businesses.list": {
185
+ ...graphqlOperation(endpoint, `query ConnectorWaveBusinesses($page: Int!, $pageSize: Int!) { businesses(page: $page, pageSize: $pageSize) { ${pageFields} edges { node { id name isPersonal } } } }`, paging,
186
+ (data) => validPage(data?.businesses, (node) => identifier(node?.id) && typeof node.name === "string" && typeof node.isPersonal === "boolean")),
187
+ scopes: ["business:read", "business:*"]
188
+ },
189
+ "customers.list": {
190
+ ...graphqlOperation(endpoint, `query ConnectorWaveCustomers($businessId: ID!, $page: Int!, $pageSize: Int!) { business(id: $businessId) { id customers(page: $page, pageSize: $pageSize, sort: [NAME_ASC]) { ${pageFields} edges { node { id name email } } } } }`, { businessId, ...paging },
191
+ (data) => identifier(data?.business?.id) && validPage(data.business.customers, (node) => identifier(node?.id) && typeof node.name === "string" && (node.email === null || typeof node.email === "string"))),
192
+ scopes: ["customer:read", "customer:*"]
193
+ },
194
+ "accounts.list": businessPage("accounts", "id name currency { code } type { value } subtype { value } isArchived balance",
195
+ ["account:read", "account:*"], node => namedRecord(node) && typeof node.currency?.code === "string" &&
196
+ typeof node.type?.value === "string" && typeof node.subtype?.value === "string" && typeof node.isArchived === "boolean" &&
197
+ (node.balance === null || decimal(node.balance))),
198
+ "products.list": businessPage("products", "id name description unitPrice isSold isBought incomeAccount { id } expenseAccount { id }",
199
+ ["product:read", "product:*"], node => namedRecord(node) && decimal(node.unitPrice) && typeof node.isSold === "boolean" &&
200
+ typeof node.isBought === "boolean" && (node.incomeAccount === null || identifier(node.incomeAccount?.id)) &&
201
+ (node.expenseAccount === null || identifier(node.expenseAccount?.id)), ", sort: [NAME_ASC]"),
202
+ "salesTaxes.list": businessPage("salesTaxes", "id name abbreviation rate isCompound isRecoverable isArchived",
203
+ ["sales_tax:read", "sales_tax:*"], node => namedRecord(node) && typeof node.abbreviation === "string" && decimal(node.rate) &&
204
+ typeof node.isCompound === "boolean" && typeof node.isRecoverable === "boolean" && typeof node.isArchived === "boolean"),
205
+ "vendors.list": businessPage("vendors", "id name email phone",
206
+ ["vendor:read", "vendor:*"], node => namedRecord(node) && (node.email === null || typeof node.email === "string") &&
207
+ (node.phone === null || typeof node.phone === "string")),
208
+ "transactions.create": {
209
+ ...graphqlOperation(endpoint,
210
+ "mutation ConnectorWaveTransactionCreate($input: MoneyTransactionCreateInput!) { moneyTransactionCreate(input: $input) { didSucceed inputErrors { code path } transaction { id } } }",
211
+ { businessId, input: { type: "object", required: true, schema: createSchema({
212
+ externalId: { type: "string", required: true, minLength: 1, maxLength: 255 },
213
+ date: { ...dateInput, required: true }, description: { type: "string", required: true, minLength: 1, maxLength: 10000 },
214
+ notes: { type: "string", maxLength: 10000 },
215
+ anchor: { type: "object", required: true, schema: createSchema({ accountId: businessId, amount: transactionAmount,
216
+ direction: { type: "string", required: true, enum: ["DEPOSIT", "WITHDRAWAL"] } }) },
217
+ lineItems: { type: "array", required: true, validator: values => values.length >= 1 && values.length <= 100 || "Use 1–100 transaction lines.",
218
+ items: { type: "object", schema: createSchema({ accountId: businessId, amount: transactionAmount,
219
+ balance: { type: "string", required: true, enum: ["CREDIT", "DEBIT", "INCREASE", "DECREASE"] },
220
+ customerId: { ...businessId, required: false }, description: { type: "string", maxLength: 10000 },
221
+ taxes: { type: "array", validator: values => values.length <= 20 || "Use at most 20 taxes per line.",
222
+ items: { type: "object", schema: createSchema({ salesTaxId: businessId, amount: transactionAmount }) } }
223
+ }) } }
224
+ }) } }, data => data?.moneyTransactionCreate?.didSucceed === true && identifier(data.moneyTransactionCreate.transaction?.id) &&
225
+ (data.moneyTransactionCreate.inputErrors === null || (Array.isArray(data.moneyTransactionCreate.inputErrors) && !data.moneyTransactionCreate.inputErrors.length))),
226
+ scopes: ["transaction:write", "transaction:*"]
227
+ },
228
+ "accounts.get": {
229
+ ...graphqlOperation(endpoint, `query ConnectorWaveAccountGet($businessId: ID!, $accountId: ID!) { business(id: $businessId) { id account(id: $accountId) { ${accountSelection} } } }`,
230
+ { businessId, accountId: businessId }, data => validAccount(data?.business?.account)), scopes: ["account:read", "account:*"]
231
+ },
232
+ "accounts.create": accountingMutation("Account", "Create", { ...accountFields, name: { ...accountFields.name, required: true },
233
+ subtype: { type: "string", required: true, enum: accountSubtypes }, currency: invoiceFields.currency }),
234
+ "accounts.update": accountingMutation("Account", "Patch", { ...accountFields, id: businessId,
235
+ sequence: { type: "integer", required: true, min: 0, max: 2_147_483_647 } }),
236
+ "accounts.archive": accountingMutation("Account", "Archive", { id: businessId }),
237
+ "salesTaxes.get": {
238
+ ...graphqlOperation(endpoint, `query ConnectorWaveSalesTaxGet($businessId: ID!, $salesTaxId: ID!) { business(id: $businessId) { id salesTax(id: $salesTaxId) { ${taxSelection} } } }`,
239
+ { businessId, salesTaxId: businessId }, data => validTax(data?.business?.salesTax)), scopes: ["sales_tax:read", "sales_tax:*"]
240
+ },
241
+ "salesTaxes.create": accountingMutation("SalesTax", "Create", { ...taxFields, name: { ...taxFields.name, required: true },
242
+ abbreviation: { ...taxFields.abbreviation, required: true }, rate: taxRate, isCompound: { type: "boolean" }, isRecoverable: { type: "boolean" } }),
243
+ "salesTaxes.update": accountingMutation("SalesTax", "Patch", { ...taxFields, id: businessId,
244
+ rates: { type: "array", validator: rates => rates.length >= 1 && rates.length <= 100 && new Set(rates.map(rate => rate.effective)).size === rates.length || "Use 1–100 rates with distinct effective dates.",
245
+ items: { type: "object", schema: createSchema({ effective: { ...dateInput, required: true }, rate: taxRate }) } } }),
246
+ "products.create": productMutation("Create"),
247
+ "products.update": productMutation("Patch"),
248
+ "customers.update": {
249
+ ...graphqlOperation(endpoint,
250
+ "mutation ConnectorWaveCustomerPatch($input: CustomerPatchInput!) { customerPatch(input: $input) { didSucceed inputErrors { code path } customer { id name email business { id } } } }",
251
+ { businessId, input: { type: "object", required: true, schema: createSchema({ id: businessId, ...customerFields }) } },
252
+ data => data?.customerPatch?.didSucceed === true &&
253
+ (data.customerPatch.inputErrors === null || (Array.isArray(data.customerPatch.inputErrors) && !data.customerPatch.inputErrors.length)) &&
254
+ namedRecord(data.customerPatch.customer) && identifier(data.customerPatch.customer.business?.id)),
255
+ scopes: ["customer:write", "customer:*"]
256
+ },
257
+ "customers.create": {
258
+ ...graphqlOperation(endpoint,
259
+ "mutation ConnectorWaveCustomerCreate($input: CustomerCreateInput!) { customerCreate(input: $input) { didSucceed inputErrors { code path } customer { id name email business { id } } } }",
260
+ { input: { type: "object", required: true, schema: createSchema({
261
+ businessId,
262
+ ...customerFields, name: { ...customerFields.name, required: true }
263
+ }) } },
264
+ (data) => data?.customerCreate?.didSucceed === true &&
265
+ (data.customerCreate.inputErrors === null || (Array.isArray(data.customerCreate.inputErrors) && data.customerCreate.inputErrors.length === 0)) &&
266
+ identifier(data.customerCreate.customer?.id) && typeof data.customerCreate.customer.name === "string" &&
267
+ (data.customerCreate.customer.email === null || typeof data.customerCreate.customer.email === "string") &&
268
+ identifier(data.customerCreate.customer.business?.id)),
269
+ scopes: ["customer:write", "customer:*"]
270
+ },
271
+ "invoices.create": documentMutation("Invoice", "Create", { ...invoiceFields, status: { ...invoiceFields.status, defaultTo: "DRAFT" } }, "invoice:write"),
272
+ "invoices.update": documentMutation("Invoice", "Patch", { ...invoiceFields, customerId: { ...businessId, required: false },
273
+ items: { ...invoiceFields.items, required: false }, id: businessId }, "invoice:write"),
274
+ "invoices.approve": documentMutation("Invoice", "Approve", { invoiceId: businessId }, "invoice:write"),
275
+ "invoices.send": documentMutation("Invoice", "Send", {
276
+ invoiceId: businessId,
277
+ ...sendFields
278
+ }, "invoice:send"),
279
+ "estimates.create": documentMutation("Estimate", "Create", estimateFields, "estimate:write"),
280
+ "estimates.update": documentMutation("Estimate", "Patch", {
281
+ ...estimateFields, id: businessId,
282
+ status: { type: "string", required: true, enum: estimateStatuses },
283
+ title: { ...estimateFields.title, required: true }, currency: { ...estimateFields.currency, required: true },
284
+ estimateDate: { ...dateInput, required: true }, dueDate: { ...dateInput, required: true },
285
+ exchangeRate: { ...decimalInput, required: true }, items: { ...estimateFields.items, required: false }
286
+ }, "estimate:write"),
287
+ "estimates.approve": documentMutation("Estimate", "Approve", { estimateId: businessId }, "estimate:write"),
288
+ "estimates.send": documentMutation("Estimate", "Send", { estimateId: businessId, ...sendFields }, "estimate:send"),
289
+ "estimates.list": businessPage("estimates", estimateSelection, ["estimate:read", "estimate:*"], validEstimate, ", sort: CREATED_AT_DESC"),
290
+ "estimates.get": {
291
+ ...graphqlOperation(endpoint,
292
+ `query ConnectorWaveEstimateGet($businessId: ID!, $estimateId: ID!) { business(id: $businessId) { id estimate(id: $estimateId) { ${estimateSelection} } } }`,
293
+ { businessId, estimateId: businessId }, data => identifier(data?.business?.id) && validEstimate(data.business.estimate)),
294
+ scopes: ["estimate:read", "estimate:*"]
295
+ },
296
+ "invoices.get": {
297
+ ...graphqlOperation(endpoint,
298
+ `query ConnectorWaveInvoiceGet($businessId: ID!, $invoiceId: ID!) { business(id: $businessId) { id invoice(id: $invoiceId) { ${invoiceSelection} } } }`,
299
+ { businessId, invoiceId: businessId }, data => identifier(data?.business?.id) && validInvoice(data.business.invoice)),
300
+ scopes: ["invoice:read", "invoice:*"]
301
+ },
302
+ "invoices.list": {
303
+ ...graphqlOperation(endpoint, `query ConnectorWaveInvoices($businessId: ID!, $page: Int!, $pageSize: Int!) { business(id: $businessId) { id invoices(page: $page, pageSize: $pageSize, sort: [CREATED_AT_DESC]) { ${pageFields} edges { node { id invoiceNumber invoiceDate status currency { code } } } } } }`, { businessId, ...paging },
304
+ (data) => identifier(data?.business?.id) && validPage(data.business.invoices, (node) => identifier(node?.id) &&
305
+ typeof node.invoiceNumber === "string" && typeof node.invoiceDate === "string" && typeof node.status === "string" && typeof node.currency?.code === "string")),
306
+ scopes: ["invoice:read", "invoice:*"]
307
+ }
308
+ },
309
+ async exchange(address, options, { request: transport }) {
310
+ const request = async (url, init) => {
311
+ const result = await transport(url, init);
312
+ if (Array.isArray(result?.errors) && result.errors.length) {
313
+ const codes = result.errors.map((error) => error?.extensions?.code);
314
+ if (codes.includes("UNAUTHENTICATED")) throw new ConnectorError("connector_reconnect_required", "Connect this Wave account again.", { statusCode: 401 });
315
+ if (codes.includes("NOT_FOUND")) throw new ConnectorError("connector_resource_not_found", "This Wave resource is not available.", { statusCode: 404 });
316
+ throw new ConnectorError("connector_provider_failed", "Wave could not complete this query.", { statusCode: 502 });
317
+ }
318
+ return result;
319
+ };
320
+ const variables = options.body.variables;
321
+ const patchCustomer = options.body.query.startsWith("mutation ConnectorWaveCustomerPatch(");
322
+ const documentMatch = /^mutation ConnectorWave(Invoice|Estimate)(Create|Patch|Approve|Send)\(/u.exec(options.body.query);
323
+ const documentKind = documentMatch?.[1];
324
+ const documentAction = documentMatch?.[2];
325
+ const resource = documentKind?.toLowerCase();
326
+ const transactionCreate = options.body.query.startsWith("mutation ConnectorWaveTransactionCreate(");
327
+ if (transactionCreate) {
328
+ const values = variables.input;
329
+ const related = new Map();
330
+ related.set(`account:${values.anchor.accountId}`, { field: "account", id: values.anchor.accountId });
331
+ for (const line of values.lineItems) {
332
+ related.set(`account:${line.accountId}`, { field: "account", id: line.accountId });
333
+ if (line.customerId) related.set(`customer:${line.customerId}`, { field: "customer", id: line.customerId });
334
+ for (const tax of line.taxes || []) related.set(`salesTax:${tax.salesTaxId}`, { field: "salesTax", id: tax.salesTaxId });
335
+ }
336
+ if (related.size > 100) throw new ConnectorError("connector_input_invalid", "Use at most 100 distinct related records per transaction.", { statusCode: 422 });
337
+ for (const target of related.values()) {
338
+ const owner = await request(address, { ...options, body: {
339
+ query: `query ConnectorWaveTransactionOwner($businessId: ID!, $id: ID!) { business(id: $businessId) { id record: ${target.field}(id: $id) { id business { id } } } }`,
340
+ variables: { businessId: variables.businessId, id: target.id }
341
+ } });
342
+ if (owner?.data?.business?.id !== variables.businessId || owner.data.business.record?.id !== target.id ||
343
+ owner.data.business.record.business?.id !== variables.businessId) {
344
+ throw new ConnectorError("connector_resource_not_found", "A transaction account, customer or tax is not available in the selected business. Check read access.", { statusCode: 404 });
345
+ }
346
+ }
347
+ options = { ...options, body: { ...options.body, variables: { input: { ...values, businessId: variables.businessId } } } };
348
+ }
349
+ const accountingMatch = /^mutation ConnectorWave(Account|SalesTax)(Create|Patch|Archive)\(/u.exec(options.body.query);
350
+ const accountingResource = accountingMatch?.[1] === "Account" ? "account" : "salesTax";
351
+ if (accountingMatch) {
352
+ const values = variables.input;
353
+ if (accountingMatch[2] !== "Create") {
354
+ const changedFields = Object.keys(values).filter(key => key !== "id" && key !== "sequence");
355
+ if (accountingMatch[2] === "Patch" && !changedFields.length) throw new ConnectorError("connector_input_invalid", "Choose accounting fields to update.", { statusCode: 422 });
356
+ const existing = await request(address, { ...options, body: {
357
+ query: `query ConnectorWaveAccountingOwner($businessId: ID!, $id: ID!) { business(id: $businessId) { id record: ${accountingResource}(id: $id) { id business { id } ${accountingResource === "account" ? "sequence" : ""} } } }`,
358
+ variables: { businessId: variables.businessId, id: values.id }
359
+ } });
360
+ if (existing?.data?.business?.id !== variables.businessId || existing.data.business.record?.id !== values.id ||
361
+ existing.data.business.record.business?.id !== variables.businessId) {
362
+ throw new ConnectorError("connector_resource_not_found", "This accounting record is not available in the selected business. Check read access.", { statusCode: 404 });
363
+ }
364
+ if (accountingMatch[2] === "Patch" && accountingResource === "account" && existing.data.business.record.sequence !== values.sequence) {
365
+ throw new ConnectorError("connector_conflict", "The account changed. Reload it before applying your edit.", { statusCode: 409 });
366
+ }
367
+ }
368
+ options = { ...options, body: { ...options.body, variables: { input: accountingMatch[2] === "Create" ? { ...values, businessId: variables.businessId } : values } } };
369
+ }
370
+ const productAction = /^mutation ConnectorWaveProduct(Create|Patch)\(/u.exec(options.body.query)?.[1];
371
+ if (productAction) {
372
+ const values = variables.input;
373
+ if (productAction === "Patch" && Object.keys(values).length < 2) {
374
+ throw new ConnectorError("connector_input_invalid", "Choose at least one product field to update.", { statusCode: 422 });
375
+ }
376
+ const related = [];
377
+ if (productAction === "Patch") related.push({ field: "product", id: values.id });
378
+ for (const field of ["incomeAccountId", "expenseAccountId"]) {
379
+ if (values[field]) related.push({ field: "account", id: values[field] });
380
+ }
381
+ for (const id of values.defaultSalesTaxIds || []) related.push({ field: "salesTax", id });
382
+ for (const target of related) {
383
+ const owner = await request(address, { ...options, body: {
384
+ query: `query ConnectorWaveProductRelated($businessId: ID!, $id: ID!) { business(id: $businessId) { id record: ${target.field}(id: $id) { id business { id } } } }`,
385
+ variables: { businessId: variables.businessId, id: target.id }
386
+ } });
387
+ if (owner?.data?.business?.id !== variables.businessId ||
388
+ owner.data.business.record?.id !== target.id || owner.data.business.record.business?.id !== variables.businessId) {
389
+ throw new ConnectorError("connector_resource_not_found", "This product, account or tax is not available in the selected business. Check read access.", { statusCode: 404 });
390
+ }
391
+ }
392
+ options = { ...options, body: { ...options.body, variables: { input: productAction === "Create" ? { ...values, businessId: variables.businessId } : values } } };
393
+ }
394
+ if (documentAction) {
395
+ const values = variables.input;
396
+ if (documentAction === "Patch" && Object.keys(values).length < 2) {
397
+ throw new ConnectorError("connector_input_invalid", "Choose at least one document field to update.", { statusCode: 422 });
398
+ }
399
+ const targetId = values.id || values[`${resource}Id`];
400
+ if (targetId) {
401
+ const existing = await request(address, { ...options, body: {
402
+ query: `query ConnectorWave${documentKind}Owner($businessId: ID!, $id: ID!) { business(id: $businessId) { id emailSendEnabled ${resource}(id: $id) { id status ${resource === "invoice" ? "business { id }" : "customer { id business { id } }"} } } }`,
403
+ variables: { businessId: variables.businessId, id: targetId }
404
+ } });
405
+ if (existing?.data?.business?.id !== variables.businessId ||
406
+ existing.data.business[resource]?.id !== targetId || (resource === "invoice" ? existing.data.business[resource].business?.id : existing.data.business[resource].customer?.business?.id) !== variables.businessId) {
407
+ throw new ConnectorError("connector_resource_not_found", "This document is not available in the selected business. Check read access.", { statusCode: 404 });
408
+ }
409
+ const status = existing.data.business[resource].status;
410
+ if (documentAction === "Send" && ((resource === "invoice" && existing.data.business.emailSendEnabled !== true) ||
411
+ typeof status !== "string" || !status || status === "DRAFT" ||
412
+ (resource === "estimate" && !["APPROVED", "SENT", "VIEWED", "ACCEPTED"].includes(status)))) {
413
+ throw new ConnectorError("connector_input_invalid", "Approve the document and check Wave email eligibility before sending.", { statusCode: 422 });
414
+ }
415
+ }
416
+ if (values.customerId) {
417
+ const owner = await request(address, { ...options, body: {
418
+ query: "query ConnectorWaveInvoiceCustomer($businessId: ID!, $id: ID!) { business(id: $businessId) { id customer(id: $id) { id business { id } } } }",
419
+ variables: { businessId: variables.businessId, id: values.customerId }
420
+ } });
421
+ if (owner?.data?.business?.id !== variables.businessId || owner.data.business.customer?.id !== values.customerId ||
422
+ owner.data.business.customer.business?.id !== variables.businessId) {
423
+ throw new ConnectorError("connector_resource_not_found", "This customer is not available in the selected business. Check read access.", { statusCode: 404 });
424
+ }
425
+ }
426
+ options = { ...options, body: { ...options.body, variables: { input: documentAction === "Create" ? { ...values, businessId: variables.businessId } : values } } };
427
+ }
428
+ if (patchCustomer) {
429
+ if (Object.keys(variables.input).length < 2) {
430
+ throw new ConnectorError("connector_input_invalid", "Choose at least one customer field to update.", { statusCode: 422 });
431
+ }
432
+ const existing = await request(address, { ...options, body: {
433
+ query: "query ConnectorWaveCustomerOwner($businessId: ID!, $id: ID!) { business(id: $businessId) { id customer(id: $id) { id business { id } } } }",
434
+ variables: { businessId: variables.businessId, id: variables.input.id }
435
+ } });
436
+ if (existing?.data?.business?.id !== variables.businessId || existing.data.business.customer?.id !== variables.input.id ||
437
+ existing.data.business.customer.business?.id !== variables.businessId) {
438
+ throw new ConnectorError("connector_resource_not_found", "This customer is not available in the selected business.", { statusCode: 404 });
439
+ }
440
+ // Business selection is a local authorization input; CustomerPatchInput only takes its customer ID.
441
+ options = { ...options, body: { ...options.body, variables: { input: variables.input } } };
442
+ }
443
+ const result = await request(address, options);
444
+ if (transactionCreate) {
445
+ if (result?.data?.moneyTransactionCreate?.didSucceed === false) {
446
+ throw new ConnectorError("connector_input_invalid", "Wave rejected the transaction. Review its balance, accounts and details before submitting again.", { statusCode: 422 });
447
+ }
448
+ return result;
449
+ }
450
+ if (accountingMatch) {
451
+ const outcome = result?.data?.[`${accountingResource}${accountingMatch[2]}`];
452
+ if (outcome?.didSucceed === false) throw new ConnectorError("connector_input_invalid", "Wave rejected the accounting change. Check the values and current record.", { statusCode: 422 });
453
+ if (outcome?.didSucceed === true && accountingMatch[2] !== "Archive" && (outcome[accountingResource]?.business?.id !== variables.businessId ||
454
+ (variables.input.id && outcome[accountingResource]?.id !== variables.input.id))) {
455
+ throw new ConnectorError("connector_response_invalid", "Wave returned an unexpected accounting record. Check the outcome before retrying.", { statusCode: 502 });
456
+ }
457
+ return result;
458
+ }
459
+ if (productAction) {
460
+ const value = result?.data?.[`product${productAction}`];
461
+ if (value?.didSucceed === false) {
462
+ throw new ConnectorError("connector_input_invalid", "Wave rejected the product details. Check accounts, taxes and price.", { statusCode: 422 });
463
+ }
464
+ if (value?.didSucceed === true && (value.product?.business?.id !== variables.businessId ||
465
+ (productAction === "Patch" && value.product?.id !== variables.input.id))) {
466
+ throw new ConnectorError("connector_response_invalid", "Wave returned an unexpected product. Check the outcome before retrying.", { statusCode: 502 });
467
+ }
468
+ return result;
469
+ }
470
+ if (documentAction) {
471
+ const outcome = result?.data?.[`${resource}${documentAction}`];
472
+ if (outcome?.didSucceed === false) {
473
+ throw new ConnectorError("connector_input_invalid", "Wave rejected the document operation. Check its details and current state.", { statusCode: 422 });
474
+ }
475
+ if (outcome?.didSucceed === true && documentAction !== "Send" &&
476
+ ((resource === "invoice" ? outcome[resource]?.business?.id : outcome[resource]?.customer?.business?.id) !== variables.businessId ||
477
+ ((variables.input.id || variables.input[`${resource}Id`]) && outcome[resource]?.id !== (variables.input.id || variables.input[`${resource}Id`])) ||
478
+ (variables.input.customerId && outcome[resource]?.customer?.id !== variables.input.customerId))) {
479
+ throw new ConnectorError("connector_response_invalid", "Wave returned an unexpected document. Check the outcome before retrying.", { statusCode: 502 });
480
+ }
481
+ return result;
482
+ }
483
+ const mutation = patchCustomer ? result?.data?.customerPatch : result?.data?.customerCreate;
484
+ if (mutation?.didSucceed === false) {
485
+ throw new ConnectorError("connector_input_invalid", "Wave rejected the customer details. Check the selected business and customer fields.", { statusCode: 422 });
486
+ }
487
+ if (mutation?.didSucceed === true && mutation.customer?.business?.id !== (patchCustomer ? variables.businessId : variables.input?.businessId)) {
488
+ throw new ConnectorError("connector_response_invalid", "Wave returned a customer from an unexpected business. Check the outcome before retrying.", { statusCode: 502 });
489
+ }
490
+ if (!patchCustomer && variables.businessId && result?.data?.business?.id !== variables.businessId) {
491
+ throw new ConnectorError("connector_response_invalid", "Wave returned an unexpected business.", { statusCode: 502 });
492
+ }
493
+ if (patchCustomer && mutation?.customer?.id !== variables.input.id) {
494
+ throw new ConnectorError("connector_response_invalid", "Wave returned an unexpected customer. Check the outcome before retrying.", { statusCode: 502 });
495
+ }
496
+ if (variables.invoiceId && (result?.data?.business?.invoice?.id !== variables.invoiceId ||
497
+ result?.data?.business?.invoice?.business?.id !== variables.businessId)) {
498
+ throw new ConnectorError("connector_response_invalid", "Wave returned an unexpected invoice.", { statusCode: 502 });
499
+ }
500
+ if (variables.estimateId && (result?.data?.business?.estimate?.id !== variables.estimateId ||
501
+ result?.data?.business?.estimate?.customer?.business?.id !== variables.businessId)) {
502
+ throw new ConnectorError("connector_response_invalid", "Wave returned an unexpected estimate.", { statusCode: 502 });
503
+ }
504
+ if (result?.data?.business?.estimates?.edges?.some(edge => edge?.node?.customer?.business?.id !== variables.businessId)) {
505
+ throw new ConnectorError("connector_response_invalid", "Wave returned an estimate from another business.", { statusCode: 502 });
506
+ }
507
+ for (const field of ["account", "salesTax"]) {
508
+ if (variables[`${field}Id`] && (result?.data?.business?.[field]?.id !== variables[`${field}Id`] ||
509
+ result?.data?.business?.[field]?.business?.id !== variables.businessId)) {
510
+ throw new ConnectorError("connector_response_invalid", "Wave returned an unexpected accounting record.", { statusCode: 502 });
511
+ }
512
+ }
513
+ const page = result?.data?.businesses || Object.values(result?.data?.business || {}).find(value => value && typeof value === "object" && Object.hasOwn(value, "pageInfo"));
514
+ if (page && (page.pageInfo?.currentPage !== variables.page || page.edges?.length > variables.pageSize)) {
515
+ throw new ConnectorError("connector_response_invalid", "Wave returned an unexpected page.", { statusCode: 502 });
516
+ }
517
+ return result;
518
+ }
519
+ });
520
+ export { waveProvider };