-
Notifications
You must be signed in to change notification settings - Fork 372
feat: appdrag drag and drop #1007
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import type { ApiRequestOptions, OpenAPIConfig } from 'corsair/http'; | ||
| import { request } from 'corsair/http'; | ||
|
|
||
| export class AppdragAPIError extends Error { | ||
| constructor( | ||
| message: string, | ||
| public readonly code?: string, | ||
| ) { | ||
| super(message); | ||
| this.name = 'AppdragAPIError'; | ||
| } | ||
| } | ||
|
|
||
| // TODO: Update with your API base URL | ||
| const APPDRAG_API_BASE = 'https://api.example.com'; | ||
|
Comment on lines
+14
to
+15
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Replace the placeholder API base URLs. Both clients use 📍 Affects 2 files
🤖 Prompt for AI Agents |
||
|
|
||
| export async function makeAppdragRequest<T>( | ||
| endpoint: string, | ||
| apiKey: string, | ||
| options: { | ||
| method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; | ||
| body?: Record<string, unknown>; | ||
| query?: Record<string, string | number | boolean | undefined>; | ||
| } = {}, | ||
| ): Promise<T> { | ||
| const { method = 'GET', body, query } = options; | ||
|
|
||
| const config: OpenAPIConfig = { | ||
| BASE: APPDRAG_API_BASE, | ||
| VERSION: '1.0.0', | ||
| WITH_CREDENTIALS: false, | ||
| CREDENTIALS: 'omit', | ||
| TOKEN: apiKey, | ||
| HEADERS: { | ||
| 'Content-Type': 'application/json', | ||
| // TODO: Add authentication headers | ||
| // 'Authorization': \`Bearer \${apiKey}\` | ||
| }, | ||
| }; | ||
|
|
||
| const requestOptions: ApiRequestOptions = { | ||
| method, | ||
| url: endpoint, | ||
| body: | ||
| method === 'POST' || method === 'PUT' || method === 'PATCH' | ||
| ? body | ||
| : undefined, | ||
| mediaType: 'application/json; charset=utf-8', | ||
| query: method === 'GET' ? query : undefined, | ||
| }; | ||
|
|
||
| try { | ||
| return await request<T>(config, requestOptions); | ||
| } catch (error) { | ||
| if (error instanceof Error) { | ||
| throw new AppdragAPIError(error.message); | ||
| } | ||
| throw new AppdragAPIError('Unknown error'); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import { z } from 'zod'; | ||
|
|
||
| export const dragUploadEndpoint = { | ||
| method: 'POST' as const, | ||
| path: '/appdrag/upload', | ||
| input: z.object({ | ||
| fileName: z.string(), | ||
| fileSize: z.number(), | ||
| fileType: z.string(), | ||
| draggedAt: z.number().optional(), | ||
| }), | ||
| handler: async ({ input }: { input: any }) => { | ||
| // This is called when user drags a file into Corsair | ||
| console.log('File dragged:', input.fileName); | ||
|
|
||
| return { | ||
| success: true, | ||
| message: `File ${input.fileName} received via drag`, | ||
| received: input, | ||
| }; | ||
| }, | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { dragUploadEndpoint } from './example.js'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { z } from 'zod'; | ||
|
|
||
| const ExampleGetInputSchema = z.object({ | ||
| id: z.string(), | ||
| }); | ||
|
|
||
| export type ExampleGetInput = z.infer<typeof ExampleGetInputSchema>; | ||
|
|
||
| const ExampleGetResponseSchema = z.object({ | ||
| id: z.string(), | ||
| }); | ||
|
|
||
| export type ExampleGetResponse = z.infer<typeof ExampleGetResponseSchema>; | ||
|
|
||
| export type AppdragEndpointInputs = { | ||
| exampleGet: ExampleGetInput; | ||
| }; | ||
|
|
||
| export type AppdragEndpointOutputs = { | ||
| exampleGet: ExampleGetResponse; | ||
| }; | ||
|
|
||
| export const AppdragEndpointInputSchemas = { | ||
| exampleGet: ExampleGetInputSchema, | ||
| } as const; | ||
|
|
||
| export const AppdragEndpointOutputSchemas = { | ||
| exampleGet: ExampleGetResponseSchema, | ||
| } as const; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import type { CorsairErrorHandler } from 'corsair/core'; | ||
| import { ApiError } from 'corsair/http'; | ||
|
|
||
| export const errorHandlers = { | ||
| RATE_LIMIT_ERROR: { | ||
| match: (error: Error) => { | ||
| if (error instanceof ApiError && error.status === 429) return true; | ||
| const msg = error.message.toLowerCase(); | ||
| return msg.includes('rate_limited') || msg.includes('429'); | ||
| }, | ||
| handler: async (error: Error) => { | ||
| let retryAfterMs: number | undefined; | ||
| if (error instanceof ApiError && error.retryAfter !== undefined) { | ||
| retryAfterMs = error.retryAfter; | ||
| } | ||
| return { maxRetries: 5, headersRetryAfterMs: retryAfterMs }; | ||
| }, | ||
| }, | ||
| AUTH_ERROR: { | ||
| match: (error: Error) => { | ||
| if (error instanceof ApiError && error.status === 401) return true; | ||
| const msg = error.message.toLowerCase(); | ||
| return msg.includes('unauthorized') || msg.includes('invalid_auth'); | ||
| }, | ||
| handler: async () => ({ maxRetries: 0 }), | ||
| }, | ||
| DEFAULT: { | ||
| match: () => true, | ||
| handler: async () => ({ maxRetries: 0 }), | ||
| }, | ||
| } satisfies CorsairErrorHandler; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,206 @@ | ||
| import type { | ||
| AuthTypes, | ||
| BindEndpoints, | ||
| BindWebhooks, | ||
| CorsairEndpoint, | ||
| CorsairErrorHandler, | ||
| CorsairPlugin, | ||
| CorsairPluginContext, | ||
| CorsairWebhook, | ||
| KeyBuilderContext, | ||
| PickAuth, | ||
| PluginAuthConfig, | ||
| PluginPermissionsConfig, | ||
| RequiredPluginEndpointMeta, | ||
| RequiredPluginEndpointSchemas, | ||
| RequiredPluginWebhookSchemas, | ||
| } from 'corsair/core'; | ||
| import { Example } from './endpoints'; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When Appdrag is typechecked, this imports Knowledge Base Used: Provider plugin implementation conventions
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win Fix the Appdrag endpoint export before merging.
📍 Affects 2 files
🤖 Prompt for AI AgentsSource: Pipeline failures |
||
| import type { | ||
| AppdragEndpointInputs, | ||
| AppdragEndpointOutputs, | ||
| } from './endpoints/types'; | ||
| import { | ||
| AppdragEndpointInputSchemas, | ||
| AppdragEndpointOutputSchemas, | ||
| } from './endpoints/types'; | ||
| import { errorHandlers } from './error-handlers'; | ||
| import { AppdragSchema } from './schema'; | ||
| import { ExampleWebhooks } from './webhooks'; | ||
| import { resolveAppdragOAuthWebhookTenantLink } from './webhooks/oauth-tenant-link'; | ||
| import { matchAppdragTenantWebhook } from './webhooks/tenant-matcher'; | ||
| import type { AppdragWebhookOutputs, ExampleEvent } from './webhooks/types'; | ||
| import { ExampleEventSchema } from './webhooks/types'; | ||
|
|
||
| export type AppdragPluginOptions = { | ||
| authType?: PickAuth<'api_key' | 'oauth_2'>; | ||
| key?: string; | ||
| webhookSecret?: string; | ||
| hooks?: InternalAppdragPlugin['hooks']; | ||
| webhookHooks?: InternalAppdragPlugin['webhookHooks']; | ||
| errorHandlers?: CorsairErrorHandler; | ||
| permissions?: PluginPermissionsConfig<typeof appdragEndpointsNested>; | ||
| }; | ||
|
|
||
| export type AppdragContext = CorsairPluginContext< | ||
| typeof AppdragSchema, | ||
| AppdragPluginOptions | ||
| >; | ||
|
|
||
| export type AppdragKeyBuilderContext = KeyBuilderContext<AppdragPluginOptions>; | ||
|
|
||
| export type AppdragBoundEndpoints = BindEndpoints< | ||
| typeof appdragEndpointsNested | ||
| >; | ||
|
|
||
| type AppdragEndpoint<K extends keyof AppdragEndpointOutputs> = CorsairEndpoint< | ||
| AppdragContext, | ||
| AppdragEndpointInputs[K], | ||
| AppdragEndpointOutputs[K] | ||
| >; | ||
|
|
||
| export type AppdragEndpoints = { | ||
| exampleGet: AppdragEndpoint<'exampleGet'>; | ||
| }; | ||
|
|
||
| type AppdragWebhook< | ||
| K extends keyof AppdragWebhookOutputs, | ||
| TEvent, | ||
| > = CorsairWebhook<AppdragContext, TEvent, AppdragWebhookOutputs[K]>; | ||
|
|
||
| export type AppdragWebhooks = { | ||
| example: AppdragWebhook<'example', ExampleEvent>; | ||
| }; | ||
|
|
||
| export type AppdragBoundWebhooks = BindWebhooks<AppdragWebhooks>; | ||
|
|
||
| const appdragEndpointsNested = { | ||
| example: { | ||
| get: Example.get, | ||
| }, | ||
| } as const; | ||
|
|
||
| const appdragWebhooksNested = { | ||
| example: { | ||
| example: ExampleWebhooks.example, | ||
| }, | ||
| } as const; | ||
|
|
||
| export const appdragEndpointSchemas = { | ||
| 'example.get': { | ||
| input: AppdragEndpointInputSchemas.exampleGet, | ||
| output: AppdragEndpointOutputSchemas.exampleGet, | ||
| }, | ||
| } as const satisfies RequiredPluginEndpointSchemas< | ||
| typeof appdragEndpointsNested | ||
| >; | ||
|
|
||
| const appdragWebhookSchemas = { | ||
| 'example.example': { | ||
| description: 'An example webhook event', | ||
| payload: ExampleEventSchema, | ||
| response: ExampleEventSchema, | ||
| }, | ||
| } as const satisfies RequiredPluginWebhookSchemas<typeof appdragWebhooksNested>; | ||
|
|
||
| const defaultAuthType: AuthTypes = 'api_key' as const; | ||
|
|
||
| const appdragEndpointMeta = { | ||
| 'example.get': { | ||
| riskLevel: 'read', | ||
| description: 'Get an example resource by ID', | ||
| }, | ||
| } as const satisfies RequiredPluginEndpointMeta<typeof appdragEndpointsNested>; | ||
|
|
||
| export const appdragAuthConfig = { | ||
| api_key: { | ||
| account: ['tenant_external_id'] as const, | ||
| }, | ||
| oauth_2: { | ||
| account: ['tenant_external_id'] as const, | ||
| }, | ||
| } as const satisfies PluginAuthConfig; | ||
|
|
||
| export type BaseAppdragPlugin<T extends AppdragPluginOptions> = CorsairPlugin< | ||
| 'appdrag', | ||
| typeof AppdragSchema, | ||
| typeof appdragEndpointsNested, | ||
| typeof appdragWebhooksNested, | ||
| T, | ||
| typeof defaultAuthType | ||
| >; | ||
|
|
||
| export type InternalAppdragPlugin = BaseAppdragPlugin<AppdragPluginOptions>; | ||
|
|
||
| export type ExternalAppdragPlugin<T extends AppdragPluginOptions> = | ||
| BaseAppdragPlugin<T>; | ||
|
|
||
| export function appdrag<const T extends AppdragPluginOptions>( | ||
| incomingOptions: AppdragPluginOptions & T = {} as AppdragPluginOptions & T, | ||
| ): ExternalAppdragPlugin<T> { | ||
| const options = { | ||
| ...incomingOptions, | ||
| authType: incomingOptions.authType ?? defaultAuthType, | ||
| }; | ||
| return { | ||
| id: 'appdrag', | ||
| authConfig: appdragAuthConfig, | ||
| schema: AppdragSchema, | ||
| options: options, | ||
| hooks: options.hooks, | ||
| webhookHooks: options.webhookHooks, | ||
| endpoints: appdragEndpointsNested, | ||
| webhooks: appdragWebhooksNested, | ||
| endpointMeta: appdragEndpointMeta, | ||
| endpointSchemas: appdragEndpointSchemas, | ||
| webhookSchemas: appdragWebhookSchemas, | ||
| pluginWebhookMatcher: (request) => { | ||
| const headers = request.headers; | ||
| // TODO: Update to match your webhook signature headers | ||
| return 'x-appdrag-signature' in headers; | ||
| }, | ||
| pluginTenantWebhookMatcher: matchAppdragTenantWebhook, | ||
| oauthWebhookTenantLinkResolver: resolveAppdragOAuthWebhookTenantLink, | ||
| errorHandlers: { | ||
| ...errorHandlers, | ||
| ...options.errorHandlers, | ||
| }, | ||
| keyBuilder: async (ctx: AppdragKeyBuilderContext, source) => { | ||
| if (source === 'webhook' && options.webhookSecret) { | ||
| return options.webhookSecret; | ||
| } | ||
|
|
||
| if (source === 'webhook') { | ||
| const res = await ctx.keys.get_webhook_signature(); | ||
| return res ?? ''; | ||
| } | ||
|
|
||
| if (source === 'endpoint' && options.key) { | ||
| return options.key; | ||
| } | ||
|
|
||
| if (source === 'endpoint' && ctx.authType === 'api_key') { | ||
| const res = await ctx.keys.get_api_key(); | ||
| return res ?? ''; | ||
| } | ||
|
|
||
| if (source === 'endpoint' && ctx.authType === 'oauth_2') { | ||
| const res = await ctx.keys.get_access_token(); | ||
| return res ?? ''; | ||
| } | ||
|
|
||
| return ''; | ||
| }, | ||
| } satisfies InternalAppdragPlugin; | ||
| } | ||
|
|
||
| export type { | ||
| AppdragEndpointInputs, | ||
| AppdragEndpointOutputs, | ||
| ExampleGetInput, | ||
| ExampleGetResponse, | ||
| } from './endpoints/types'; | ||
| export type { | ||
| AppdragWebhookOutputs, | ||
| ExampleEvent, | ||
| } from './webhooks/types'; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When either generated client is invoked, it sends the request to
https://api.example.comwhile the provider-specific authentication setup remains unfinished, causing endpoint calls to target the placeholder service instead of the intended provider.Rule Used: Flag boilerplate residue from the plugin generator... (source)
Knowledge Base Used: Provider plugin implementation conventions