Integrations.constants.tsx653 lines · main
| 1 | import { getEnableWebhooksSQL } from '@supabase/pg-meta' |
| 2 | import type { Tables } from 'common/marketplace.types' |
| 3 | import { Clock5, Code2, Layers, Timer, Vault, Webhook } from 'lucide-react' |
| 4 | import dynamic from 'next/dynamic' |
| 5 | import Image from 'next/image' |
| 6 | import { ComponentType, ReactNode } from 'react' |
| 7 | import { cn } from 'ui' |
| 8 | import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' |
| 9 | |
| 10 | import { UpgradeDatabaseAlert } from '../Queues/UpgradeDatabaseAlert' |
| 11 | import { getStripeSyncSchemaComment } from '../templates/StripeSyncEngine/useStripeSyncStatus' |
| 12 | import { WRAPPERS } from '../Wrappers/Wrappers.constants' |
| 13 | import { WrapperMeta } from '../Wrappers/Wrappers.types' |
| 14 | import { stripeSyncKeys } from '@/data/database-integrations/stripe/keys' |
| 15 | import { installStripeSync } from '@/data/database-integrations/stripe/stripe-sync-install-mutation' |
| 16 | import { enableDatabaseWebhooks } from '@/data/database/hooks-enable-mutation' |
| 17 | import { databaseKeys } from '@/data/database/keys' |
| 18 | import { getSchemas, invalidateSchemasQuery } from '@/data/database/schemas-query' |
| 19 | import { getQueryClient } from '@/data/query-client' |
| 20 | import { BASE_PATH, DOCS_URL } from '@/lib/constants' |
| 21 | import { useTrack } from '@/lib/telemetry/track' |
| 22 | |
| 23 | export type NavigationContentLayout = 'constrained' | 'full' |
| 24 | |
| 25 | export type Navigation = { |
| 26 | route: string |
| 27 | label: string |
| 28 | hasChild?: boolean |
| 29 | childIcon?: React.ReactNode |
| 30 | children?: Navigation[] |
| 31 | layout?: NavigationContentLayout // applies only to the new marketplace |
| 32 | } |
| 33 | |
| 34 | // [Joshen] Basing this on template.json for now |
| 35 | export type IntegrationInputs = { |
| 36 | [key: string]: { |
| 37 | label: string |
| 38 | type: 'text' | 'number' | 'password' |
| 39 | description?: string |
| 40 | required: boolean |
| 41 | actions: { |
| 42 | label: string |
| 43 | href: string |
| 44 | }[] |
| 45 | } |
| 46 | } |
| 47 | |
| 48 | type IntegrationStep = { |
| 49 | label: string |
| 50 | description?: string |
| 51 | } |
| 52 | |
| 53 | type Listing = Tables<'listings'> |
| 54 | type InstallUrlType = NonNullable<Listing['installation_url_type']> |
| 55 | type InstallIdentificationMethod = NonNullable<Listing['installation_identification_method']> |
| 56 | |
| 57 | export type MarketplaceSource = 'Official' | 'Partner' | 'Community' |
| 58 | |
| 59 | /** |
| 60 | * [Joshen] For marketplace, we probably need to revisit this definition |
| 61 | * What properties are obsolete, what properties we need from remote source |
| 62 | */ |
| 63 | export type IntegrationDefinition = { |
| 64 | id: string |
| 65 | name: string |
| 66 | status?: 'alpha' | 'beta' |
| 67 | categories?: string[] |
| 68 | featured?: boolean |
| 69 | icon: (props?: { className?: string; style?: Record<string, string | number> }) => ReactNode |
| 70 | description: string | null |
| 71 | content?: string | null |
| 72 | files?: string[] |
| 73 | docsUrl: string | null |
| 74 | siteUrl?: string | null |
| 75 | author: { |
| 76 | name: string |
| 77 | websiteUrl: string |
| 78 | } |
| 79 | /** Provenance of the integration — Official (built by Briven), Partner (formal third-party listing), Community (open-source, not officially endorsed). */ |
| 80 | source: MarketplaceSource |
| 81 | requiredExtensions: Array<string> |
| 82 | /** Optional component to render if the integration requires extensions that are not available on the current database image */ |
| 83 | missingExtensionsAlert?: ReactNode |
| 84 | navigation?: Array<Navigation> |
| 85 | navigate: (props: { |
| 86 | id: string | undefined |
| 87 | pageId: string | undefined |
| 88 | childId: string | undefined |
| 89 | }) => ComponentType<{}> | null |
| 90 | |
| 91 | /** For showing the SQL query in the installation sheet */ |
| 92 | installationSql?: string |
| 93 | /** Custom command to install the integration (if any - none atm) */ |
| 94 | installationCommand?: (props: { |
| 95 | ref: string |
| 96 | track?: ReturnType<typeof useTrack> |
| 97 | [key: string]: unknown |
| 98 | }) => Promise<void> |
| 99 | /** |
| 100 | * Used for long polling to track the progress of the integration installation if async |
| 101 | * The component calling this handles the polling logic, and should terminate the poll depending on the returned value |
| 102 | * Depending on how we want this to work, this method will thereafter also call any RQ invalidation if required |
| 103 | * */ |
| 104 | checkInstallationStatus?: (props: { |
| 105 | ref?: string |
| 106 | connectionString?: string | null |
| 107 | [key: string]: unknown |
| 108 | }) => Promise<'installed' | 'installing'> |
| 109 | /** User inputs for template integrations */ |
| 110 | inputs?: IntegrationInputs |
| 111 | /** Purely visual, just to show what are the changes on the project from installing the integration */ |
| 112 | steps?: IntegrationStep[] |
| 113 | |
| 114 | /** These are for OAuth Integrations */ |
| 115 | installUrl?: string | null |
| 116 | installUrlType?: InstallUrlType |
| 117 | installIdentificationMethod?: InstallIdentificationMethod |
| 118 | secretKeyPrefix?: string |
| 119 | edgeFunctionSecretName?: string |
| 120 | listingId?: string |
| 121 | } & ( |
| 122 | | { type: 'wrapper'; meta: WrapperMeta } |
| 123 | | { type: 'postgres_extension' | 'custom' | 'oauth' | 'template' } |
| 124 | ) |
| 125 | |
| 126 | const authorBriven = { |
| 127 | name: 'Briven', |
| 128 | websiteUrl: 'https://supabase.com', |
| 129 | } |
| 130 | |
| 131 | const BRIVEN_INTEGRATIONS: Array<IntegrationDefinition> = [ |
| 132 | { |
| 133 | id: 'queues', |
| 134 | type: 'postgres_extension' as const, |
| 135 | source: 'Community' as const, |
| 136 | requiredExtensions: ['pgmq'], |
| 137 | missingExtensionsAlert: <UpgradeDatabaseAlert minimumVersion="15.6.1.143" />, |
| 138 | name: `Queues`, |
| 139 | icon: ({ className, ...props } = {}) => ( |
| 140 | <Layers className={cn('inset-0 p-2 text-black w-full h-full', className)} {...props} /> |
| 141 | ), |
| 142 | description: 'Lightweight message queue in Postgres', |
| 143 | docsUrl: 'https://github.com/tembo-io/pgmq', |
| 144 | author: { |
| 145 | name: 'pgmq', |
| 146 | websiteUrl: 'https://github.com/tembo-io/pgmq', |
| 147 | }, |
| 148 | navigation: [ |
| 149 | { |
| 150 | route: 'overview', |
| 151 | label: 'Overview', |
| 152 | }, |
| 153 | { |
| 154 | route: 'queues', |
| 155 | label: 'Queues', |
| 156 | hasChild: true, |
| 157 | childIcon: ( |
| 158 | <Layers size={12} strokeWidth={1.5} className={cn('text-foreground w-full h-full')} /> |
| 159 | ), |
| 160 | }, |
| 161 | { |
| 162 | route: 'settings', |
| 163 | label: 'Settings', |
| 164 | layout: 'constrained', |
| 165 | }, |
| 166 | ], |
| 167 | navigate: ({ pageId = 'overview', childId }) => { |
| 168 | if (childId) { |
| 169 | return dynamic(() => import('../Queues/QueuePage').then((mod) => mod.QueuePage), { |
| 170 | loading: Loading, |
| 171 | }) |
| 172 | } |
| 173 | switch (pageId) { |
| 174 | case 'overview': |
| 175 | return dynamic( |
| 176 | () => |
| 177 | import('@/components/interfaces/Integrations/Queues/OverviewTab').then( |
| 178 | (mod) => mod.QueuesOverviewTab |
| 179 | ), |
| 180 | { loading: Loading } |
| 181 | ) |
| 182 | case 'queues': |
| 183 | return dynamic(() => import('../Queues/QueuesTab').then((mod) => mod.QueuesTab), { |
| 184 | loading: Loading, |
| 185 | }) |
| 186 | case 'settings': |
| 187 | return dynamic( |
| 188 | () => import('../Queues/QueuesSettings').then((mod) => mod.QueuesSettings), |
| 189 | { loading: Loading } |
| 190 | ) |
| 191 | } |
| 192 | return null |
| 193 | }, |
| 194 | }, |
| 195 | { |
| 196 | id: 'cron', |
| 197 | type: 'postgres_extension' as const, |
| 198 | source: 'Community' as const, |
| 199 | requiredExtensions: ['pg_cron'], |
| 200 | name: `Cron`, |
| 201 | icon: ({ className, ...props } = {}) => ( |
| 202 | <Clock5 className={cn('inset-0 p-2 text-black w-full h-full', className)} {...props} /> |
| 203 | ), |
| 204 | description: 'Schedule recurring Jobs in Postgres', |
| 205 | docsUrl: 'https://github.com/citusdata/pg_cron', |
| 206 | author: { |
| 207 | name: 'Citus Data', |
| 208 | websiteUrl: 'https://github.com/citusdata/pg_cron', |
| 209 | }, |
| 210 | navigation: [ |
| 211 | { |
| 212 | route: 'overview', |
| 213 | label: 'Overview', |
| 214 | }, |
| 215 | { |
| 216 | route: 'jobs', |
| 217 | label: 'Jobs', |
| 218 | hasChild: true, |
| 219 | childIcon: ( |
| 220 | <Timer size={12} strokeWidth={1.5} className={cn('text-foreground w-full h-full')} /> |
| 221 | ), |
| 222 | }, |
| 223 | ], |
| 224 | navigate: ({ pageId = 'overview', childId }) => { |
| 225 | if (childId) { |
| 226 | return dynamic(() => import('../CronJobs/CronJobPage').then((mod) => mod.CronJobPage), { |
| 227 | loading: Loading, |
| 228 | }) |
| 229 | } |
| 230 | switch (pageId) { |
| 231 | case 'overview': |
| 232 | return dynamic( |
| 233 | () => |
| 234 | import('@/components/interfaces/Integrations/Integration/IntegrationOverviewTabWrapper').then( |
| 235 | (mod) => mod.IntegrationOverviewTabWrapper |
| 236 | ), |
| 237 | { |
| 238 | loading: Loading, |
| 239 | } |
| 240 | ) |
| 241 | case 'jobs': |
| 242 | return dynamic(() => import('../CronJobs/CronJobsTab').then((mod) => mod.CronjobsTab), { |
| 243 | loading: Loading, |
| 244 | }) |
| 245 | } |
| 246 | return null |
| 247 | }, |
| 248 | }, |
| 249 | { |
| 250 | id: 'vault', |
| 251 | type: 'postgres_extension' as const, |
| 252 | source: 'Official' as const, |
| 253 | requiredExtensions: ['briven_vault'], |
| 254 | missingExtensionsAlert: <UpgradeDatabaseAlert />, |
| 255 | name: `Vault`, |
| 256 | status: 'beta', |
| 257 | icon: ({ className, ...props } = {}) => ( |
| 258 | <Vault className={cn('inset-0 p-2 text-black w-full h-full', className)} {...props} /> |
| 259 | ), |
| 260 | description: 'Application level encryption for your project', |
| 261 | docsUrl: `${DOCS_URL}/guides/database/vault`, |
| 262 | author: authorBriven, |
| 263 | navigation: [ |
| 264 | { |
| 265 | route: 'overview', |
| 266 | label: 'Overview', |
| 267 | }, |
| 268 | { |
| 269 | route: 'secrets', |
| 270 | label: 'Secrets', |
| 271 | }, |
| 272 | ], |
| 273 | navigate: ({ pageId = 'overview' }) => { |
| 274 | switch (pageId) { |
| 275 | case 'overview': |
| 276 | return dynamic( |
| 277 | () => |
| 278 | import('@/components/interfaces/Integrations/Integration/IntegrationOverviewTabWrapper').then( |
| 279 | (mod) => mod.IntegrationOverviewTabWrapper |
| 280 | ), |
| 281 | { |
| 282 | loading: Loading, |
| 283 | } |
| 284 | ) |
| 285 | case 'secrets': |
| 286 | return dynamic( |
| 287 | () => import('../Vault/Secrets/SecretsManagement').then((mod) => mod.SecretsManagement), |
| 288 | { |
| 289 | loading: Loading, |
| 290 | } |
| 291 | ) |
| 292 | } |
| 293 | return null |
| 294 | }, |
| 295 | }, |
| 296 | { |
| 297 | id: 'webhooks', |
| 298 | type: 'postgres_extension' as const, |
| 299 | source: 'Official' as const, |
| 300 | name: `Database Webhooks`, |
| 301 | icon: ({ className, ...props } = {}) => ( |
| 302 | <Webhook className={cn('inset-0 p-2 text-black w-full h-full', className)} {...props} /> |
| 303 | ), |
| 304 | description: |
| 305 | 'Send real-time data from your database to another system when a table event occurs', |
| 306 | docsUrl: `${DOCS_URL}/guides/database/webhooks`, |
| 307 | author: authorBriven, |
| 308 | requiredExtensions: ['pg_net'], |
| 309 | navigation: [ |
| 310 | { |
| 311 | route: 'overview', |
| 312 | label: 'Overview', |
| 313 | }, |
| 314 | { |
| 315 | route: 'webhooks', |
| 316 | label: 'Webhooks', |
| 317 | layout: 'constrained', |
| 318 | }, |
| 319 | ], |
| 320 | navigate: ({ pageId = 'overview' }) => { |
| 321 | switch (pageId) { |
| 322 | case 'overview': |
| 323 | return dynamic( |
| 324 | () => |
| 325 | import('@/components/interfaces/Integrations/Webhooks/OverviewTab').then( |
| 326 | (mod) => mod.WebhooksOverviewTab |
| 327 | ), |
| 328 | { |
| 329 | loading: Loading, |
| 330 | } |
| 331 | ) |
| 332 | case 'webhooks': |
| 333 | return dynamic( |
| 334 | () => |
| 335 | import('@/components/interfaces/Integrations/Webhooks/ListTab').then( |
| 336 | (mod) => mod.WebhooksListTab |
| 337 | ), |
| 338 | { |
| 339 | loading: Loading, |
| 340 | } |
| 341 | ) |
| 342 | } |
| 343 | return null |
| 344 | }, |
| 345 | installationSql: getEnableWebhooksSQL(), |
| 346 | installationCommand: async ({ ref }: { ref: string }) => { |
| 347 | const queryClient = getQueryClient() |
| 348 | await enableDatabaseWebhooks({ ref }) |
| 349 | await invalidateSchemasQuery(queryClient, ref) |
| 350 | }, |
| 351 | }, |
| 352 | { |
| 353 | id: 'data_api', |
| 354 | type: 'custom' as const, |
| 355 | source: 'Official' as const, |
| 356 | requiredExtensions: [], |
| 357 | name: `Data API`, |
| 358 | icon: ({ className, ...props } = {}) => ( |
| 359 | <Code2 className={cn('inset-0 p-2 text-black w-full h-full', className)} {...props} /> |
| 360 | ), |
| 361 | description: 'Auto-generate an API directly from your database schema', |
| 362 | docsUrl: `${DOCS_URL}/guides/api`, |
| 363 | author: authorBriven, |
| 364 | navigation: [ |
| 365 | { |
| 366 | route: 'overview', |
| 367 | label: 'Overview', |
| 368 | }, |
| 369 | { |
| 370 | route: 'settings', |
| 371 | label: 'Settings', |
| 372 | layout: 'constrained', |
| 373 | }, |
| 374 | { |
| 375 | route: 'docs', |
| 376 | label: 'Docs', |
| 377 | }, |
| 378 | ], |
| 379 | navigate: ({ pageId = 'overview' }) => { |
| 380 | switch (pageId) { |
| 381 | case 'overview': |
| 382 | return dynamic( |
| 383 | () => |
| 384 | import('@/components/interfaces/Integrations/DataApi/OverviewTab').then( |
| 385 | (mod) => mod.DataApiOverviewTab |
| 386 | ), |
| 387 | { |
| 388 | loading: Loading, |
| 389 | } |
| 390 | ) |
| 391 | case 'settings': |
| 392 | return dynamic( |
| 393 | () => |
| 394 | import('@/components/interfaces/Integrations/DataApi/SettingsTab').then( |
| 395 | (mod) => mod.DataApiSettingsTab |
| 396 | ), |
| 397 | { |
| 398 | loading: Loading, |
| 399 | } |
| 400 | ) |
| 401 | case 'docs': |
| 402 | return dynamic( |
| 403 | () => |
| 404 | import('@/components/interfaces/Integrations/DataApi/DocsTab').then( |
| 405 | (mod) => mod.DataApiDocsTab |
| 406 | ), |
| 407 | { |
| 408 | loading: Loading, |
| 409 | } |
| 410 | ) |
| 411 | } |
| 412 | return null |
| 413 | }, |
| 414 | }, |
| 415 | { |
| 416 | id: 'graphiql', |
| 417 | type: 'postgres_extension' as const, |
| 418 | source: 'Official' as const, |
| 419 | requiredExtensions: ['pg_graphql'], |
| 420 | name: `GraphQL`, |
| 421 | icon: ({ className, ...props } = {}) => ( |
| 422 | <Image |
| 423 | fill |
| 424 | src={`${BASE_PATH}/img/graphql.svg`} |
| 425 | alt="GraphiQL" |
| 426 | className={cn('p-2', className)} |
| 427 | {...props} |
| 428 | /> |
| 429 | ), |
| 430 | description: 'Run GraphQL queries through our interactive in-browser IDE', |
| 431 | docsUrl: `${DOCS_URL}/guides/database/extensions/pg_graphql`, |
| 432 | author: authorBriven, |
| 433 | navigation: [ |
| 434 | { |
| 435 | route: 'overview', |
| 436 | label: 'Overview', |
| 437 | }, |
| 438 | { |
| 439 | route: 'graphiql', |
| 440 | label: 'GraphiQL', |
| 441 | }, |
| 442 | ], |
| 443 | navigate: ({ pageId = 'overview' }) => { |
| 444 | switch (pageId) { |
| 445 | case 'overview': |
| 446 | return dynamic( |
| 447 | () => |
| 448 | import('@/components/interfaces/Integrations/Integration/IntegrationOverviewTabWrapper').then( |
| 449 | (mod) => mod.IntegrationOverviewTabWrapper |
| 450 | ), |
| 451 | { |
| 452 | loading: Loading, |
| 453 | } |
| 454 | ) |
| 455 | case 'graphiql': |
| 456 | return dynamic( |
| 457 | () => |
| 458 | import('@/components/interfaces/Integrations/GraphQL/GraphiQLTab').then( |
| 459 | (mod) => mod.GraphiQLTab |
| 460 | ), |
| 461 | { |
| 462 | loading: Loading, |
| 463 | } |
| 464 | ) |
| 465 | } |
| 466 | return null |
| 467 | }, |
| 468 | }, |
| 469 | ] as const |
| 470 | |
| 471 | const WRAPPER_INTEGRATIONS: Array<IntegrationDefinition> = WRAPPERS.map((w) => { |
| 472 | return { |
| 473 | id: w.name, |
| 474 | type: 'wrapper' as const, |
| 475 | source: 'Official' as const, |
| 476 | name: `${w.label} Wrapper`, |
| 477 | icon: ({ className, ...props } = {}) => ( |
| 478 | <Image fill src={w.icon} alt={w.name} className={cn('p-2', className)} {...props} /> |
| 479 | ), |
| 480 | requiredExtensions: ['wrappers', 'briven_vault'], |
| 481 | description: w.description, |
| 482 | docsUrl: w.docsUrl, |
| 483 | meta: w, |
| 484 | author: authorBriven, |
| 485 | navigation: [ |
| 486 | { |
| 487 | route: 'overview', |
| 488 | label: 'Overview', |
| 489 | }, |
| 490 | { |
| 491 | route: 'wrappers', |
| 492 | label: 'Wrappers', |
| 493 | }, |
| 494 | ], |
| 495 | navigate: ({ pageId = 'overview' }) => { |
| 496 | switch (pageId) { |
| 497 | case 'overview': |
| 498 | return dynamic( |
| 499 | () => |
| 500 | import('@/components/interfaces/Integrations/Wrappers/OverviewTab').then( |
| 501 | (mod) => mod.WrapperOverviewTab |
| 502 | ), |
| 503 | { |
| 504 | loading: Loading, |
| 505 | } |
| 506 | ) |
| 507 | case 'wrappers': |
| 508 | return dynamic( |
| 509 | () => |
| 510 | import('@/components/interfaces/Integrations/Wrappers/WrappersTab').then( |
| 511 | (mod) => mod.WrappersTab |
| 512 | ), |
| 513 | { |
| 514 | loading: Loading, |
| 515 | } |
| 516 | ) |
| 517 | } |
| 518 | return null |
| 519 | }, |
| 520 | } |
| 521 | }) |
| 522 | |
| 523 | const TEMPLATE_INTEGRATIONS: Array<IntegrationDefinition> = [ |
| 524 | { |
| 525 | id: 'stripe_sync_engine', |
| 526 | type: 'template' as const, |
| 527 | source: 'Partner' as const, |
| 528 | requiredExtensions: ['pgmq', 'briven_vault', 'pg_cron', 'pg_net'], |
| 529 | missingExtensionsAlert: <UpgradeDatabaseAlert minimumVersion="15.6.1.143" />, |
| 530 | name: `Stripe Sync Engine`, |
| 531 | status: 'alpha', |
| 532 | icon: ({ className, ...props } = {}) => ( |
| 533 | <Image |
| 534 | fill |
| 535 | src={`${BASE_PATH}/img/icons/stripe-icon.svg`} |
| 536 | alt={'Stripe Logo'} |
| 537 | className={cn('p-2', className)} |
| 538 | {...props} |
| 539 | /> |
| 540 | ), |
| 541 | description: |
| 542 | 'Continuously sync your payments, customer, and other data from Stripe to your Postgres database', |
| 543 | docsUrl: 'https://github.com/stripe-experiments/sync-engine/', |
| 544 | author: { |
| 545 | name: 'Stripe', |
| 546 | websiteUrl: 'https://www.stripe.com', |
| 547 | }, |
| 548 | navigation: [ |
| 549 | { |
| 550 | route: 'overview', |
| 551 | label: 'Overview', |
| 552 | }, |
| 553 | { |
| 554 | route: 'settings', |
| 555 | label: 'Settings', |
| 556 | layout: 'constrained', |
| 557 | }, |
| 558 | ], |
| 559 | navigate: ({ pageId = 'overview' }) => { |
| 560 | switch (pageId) { |
| 561 | case 'overview': |
| 562 | return dynamic( |
| 563 | () => |
| 564 | import('@/components/interfaces/Integrations/templates/StripeSyncEngine/OverviewTab').then( |
| 565 | (mod) => mod.StripeSyncEngineOverviewTab |
| 566 | ), |
| 567 | { loading: Loading } |
| 568 | ) |
| 569 | case 'settings': |
| 570 | return dynamic( |
| 571 | () => |
| 572 | import('@/components/interfaces/Integrations/templates/StripeSyncEngine/StripeSyncSettingsPage').then( |
| 573 | (mod) => mod.StripeSyncSettingsPage |
| 574 | ), |
| 575 | { loading: Loading } |
| 576 | ) |
| 577 | } |
| 578 | return null |
| 579 | }, |
| 580 | inputs: { |
| 581 | stripe_api_key: { |
| 582 | type: 'password', |
| 583 | required: true, |
| 584 | label: 'Stripe API secret key', |
| 585 | description: |
| 586 | 'Requires write access to Webhook Endpoints and read-only access to all other categories.', |
| 587 | actions: [ |
| 588 | { |
| 589 | label: 'Get API key', |
| 590 | href: 'https://dashboard.stripe.com/apikeys', |
| 591 | }, |
| 592 | { |
| 593 | label: 'What are Stripe API keys?', |
| 594 | href: 'https://support.stripe.com/questions/what-are-stripe-api-keys-and-how-to-find-them', |
| 595 | }, |
| 596 | ], |
| 597 | }, |
| 598 | }, |
| 599 | steps: [ |
| 600 | { label: 'Creates a new database schema named `stripe`' }, |
| 601 | { label: 'Creates tables and views in the `stripe` schema for synced Stripe data' }, |
| 602 | { label: 'Deploys Edge Functions to handle incoming webhooks from Stripe' }, |
| 603 | { label: 'Schedules automatic Stripe data syncs using Briven Queues' }, |
| 604 | ], |
| 605 | installationCommand: async ({ ref: projectRef, track, stripe_api_key }) => { |
| 606 | const startTime = Date.now() |
| 607 | await installStripeSync({ projectRef, startTime, stripeSecretKey: stripe_api_key as string }) |
| 608 | |
| 609 | if (track) |
| 610 | track('integration_install_submitted', { |
| 611 | integrationName: 'stripe_sync_engine', |
| 612 | method: 'template', |
| 613 | }) |
| 614 | |
| 615 | const queryClient = getQueryClient() |
| 616 | await queryClient.invalidateQueries({ queryKey: stripeSyncKeys.all }) |
| 617 | }, |
| 618 | checkInstallationStatus: async (props) => { |
| 619 | const queryClient = getQueryClient() |
| 620 | const { projectRef, connectionString } = props || {} |
| 621 | |
| 622 | const schemas = await getSchemas({ |
| 623 | projectRef: projectRef as string, |
| 624 | connectionString: connectionString as string, |
| 625 | }) |
| 626 | |
| 627 | const { status, errorMessage } = getStripeSyncSchemaComment(schemas) |
| 628 | |
| 629 | if (status === 'install error') { |
| 630 | throw new Error(errorMessage ?? 'Stripe Sync installation failed') |
| 631 | } |
| 632 | |
| 633 | if (status === 'installed') { |
| 634 | await queryClient.invalidateQueries({ |
| 635 | queryKey: databaseKeys.schemas(projectRef as string), |
| 636 | }) |
| 637 | } |
| 638 | return status === 'installed' ? 'installed' : 'installing' |
| 639 | }, |
| 640 | }, |
| 641 | ] |
| 642 | |
| 643 | export const INTEGRATIONS: Array<IntegrationDefinition> = [ |
| 644 | ...WRAPPER_INTEGRATIONS, |
| 645 | ...BRIVEN_INTEGRATIONS, |
| 646 | ...TEMPLATE_INTEGRATIONS, |
| 647 | ] |
| 648 | |
| 649 | export const Loading = () => ( |
| 650 | <div className="p-10"> |
| 651 | <GenericSkeletonLoader /> |
| 652 | </div> |
| 653 | ) |