diff --git a/package-lock.json b/package-lock.json index e26db0f2d56..7c2aae86323 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24782,16 +24782,6 @@ "dev": true, "license": "0BSD" }, - "node_modules/lru-cache": { - "version": "6.0.0", - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/ltgt": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ltgt/-/ltgt-2.2.1.tgz", @@ -29279,11 +29269,9 @@ } }, "node_modules/semver": { - "version": "7.6.0", - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -35585,6 +35573,7 @@ "passport-local": "1.0.0", "rate-limiter-flexible": "5.0.3", "redis": "4.6.13", + "semver": "7.6.3", "simple-oauth2": "5.1.0", "uuid": "9.0.0", "ws": "8.18.0", diff --git a/packages/cli/lib/services/__snapshots__/config.service.unit.test.ts.snap b/packages/cli/lib/services/__snapshots__/config.service.unit.test.ts.snap index 22f89281543..6c33d3cd9b0 100644 --- a/packages/cli/lib/services/__snapshots__/config.service.unit.test.ts.snap +++ b/packages/cli/lib/services/__snapshots__/config.service.unit.test.ts.snap @@ -156,7 +156,8 @@ exports[`load > should parse a nango.yaml file that is version 2 as expected 1`] { "description": "Get a GitHub issue.", "endpoint": { - "GET": "/ticketing/tickets/{GithubCreateIssueInput:id}", + "method": "GET", + "path": "/ticketing/tickets/{GithubCreateIssueInput:id}", }, "input": null, "name": "github-get-issue", @@ -175,7 +176,8 @@ exports[`load > should parse a nango.yaml file that is version 2 as expected 1`] { "description": "Creates a GitHub issue.", "endpoint": { - "POST": "/ticketing/tickets", + "method": "POST", + "path": "/ticketing/tickets", }, "input": "GithubCreateIssueInput", "name": "github-create-issue", @@ -196,7 +198,8 @@ exports[`load > should parse a nango.yaml file that is version 2 as expected 1`] { "description": "Deletes a GitHub issue.", "endpoint": { - "DELETE": "/ticketing/tickets/{GithubIssue:id}", + "method": "DELETE", + "path": "/ticketing/tickets/{GithubIssue:id}", }, "input": null, "name": "github-delete-issue", @@ -221,7 +224,8 @@ exports[`load > should parse a nango.yaml file that is version 2 as expected 1`] "description": "Sync github issues continuously from public repos", "endpoints": [ { - "GET": "/ticketing/tickets", + "method": "GET", + "path": "/ticketing/tickets", }, ], "input": null, @@ -249,7 +253,8 @@ exports[`load > should parse a nango.yaml file that is version 2 as expected 1`] "description": "Sync github issues continuously from public repos example two", "endpoints": [ { - "GET": "/ticketing/tickets-two", + "method": "GET", + "path": "/ticketing/tickets-two", }, ], "input": null, @@ -275,10 +280,12 @@ exports[`load > should parse a nango.yaml file that is version 2 as expected 1`] "description": "Sync github issues to multiple models", "endpoints": [ { - "GET": "/ticketing/ticket", + "method": "GET", + "path": "/ticketing/ticket", }, { - "GET": "/ticketing/pr", + "method": "GET", + "path": "/ticketing/pr", }, ], "input": null, diff --git a/packages/cli/lib/services/__snapshots__/deploy.service.unit.cli-test.ts.snap b/packages/cli/lib/services/__snapshots__/deploy.service.unit.cli-test.ts.snap index 15de9dddb0d..0f65bc669d7 100644 --- a/packages/cli/lib/services/__snapshots__/deploy.service.unit.cli-test.ts.snap +++ b/packages/cli/lib/services/__snapshots__/deploy.service.unit.cli-test.ts.snap @@ -40,7 +40,8 @@ exports[`package > should package correctly 2`] = ` "auto_start": true, "endpoints": [ { - "GET": "/hubspot/contacts", + "method": "GET", + "path": "/hubspot/contacts", }, ], "fileBody": { @@ -113,7 +114,8 @@ export default async function fetchData(nango: NangoSync): Promise { { "endpoints": [ { - "POST": "/hubspot/contact", + "method": "POST", + "path": "/hubspot/contact", }, ], "fileBody": { @@ -171,7 +173,8 @@ export default async function runAction(nango: NangoAction): Promise { "auto_start": true, "endpoints": [ { - "GET": "/github/issues", + "method": "GET", + "path": "/github/issues", }, ], "fileBody": { @@ -244,7 +247,8 @@ export default async function fetchData(nango: NangoSync): Promise { { "endpoints": [ { - "POST": "/github/issue", + "method": "POST", + "path": "/github/issue", }, ], "fileBody": { diff --git a/packages/nango-yaml/lib/helpers.ts b/packages/nango-yaml/lib/helpers.ts index 568b3e08774..20ee29f604b 100644 --- a/packages/nango-yaml/lib/helpers.ts +++ b/packages/nango-yaml/lib/helpers.ts @@ -1,7 +1,7 @@ import path from 'node:path'; import ms from 'ms'; import type { StringValue } from 'ms'; -import type { NangoYaml, NangoYamlParsed, NangoYamlParsedIntegration } from '@nangohq/types'; +import type { HTTP_METHOD, NangoSyncEndpointV2, NangoYaml, NangoYamlParsed, NangoYamlParsedIntegration } from '@nangohq/types'; interface IntervalResponse { interval: StringValue; @@ -194,3 +194,16 @@ export function getProviderConfigurationFromPath({ filePath, parsed }: { filePat return providerConfiguration; } + +export function parseEndpoint(rawEndpoint: string | NangoSyncEndpointV2, defaultMethod: HTTP_METHOD): NangoSyncEndpointV2 { + if (typeof rawEndpoint === 'string') { + const endpoint = rawEndpoint.split(' '); + if (endpoint.length > 1) { + return { method: endpoint[0] as HTTP_METHOD, path: endpoint[1] as string }; + } + + return { method: defaultMethod, path: endpoint[0] as string }; + } + + return rawEndpoint; +} diff --git a/packages/nango-yaml/lib/parser.ts b/packages/nango-yaml/lib/parser.ts index 5025790420d..9cfa9fdbc65 100644 --- a/packages/nango-yaml/lib/parser.ts +++ b/packages/nango-yaml/lib/parser.ts @@ -120,27 +120,19 @@ export abstract class NangoYamlParser { const find = getRecursiveModelNames(this.modelsParser, sync.input); find.forEach((name) => usedModelsSync.add(name)); } - for (const endpointByVerb of sync.endpoints) { - for (const [verb, endpoint] of Object.entries(endpointByVerb)) { - if (!endpoint) { - continue; // TS pleasing - } - - const str = `${verb} ${endpoint}`; - if (endpoints.has(str)) { - this.errors.push(new ParserErrorDuplicateEndpoint({ endpoint: str, path: [integrationName, 'syncs', sync.name, '[endpoints]'] })); - continue; - } + for (const endpoint of sync.endpoints) { + const str = `${endpoint.method} ${endpoint.path}`; + if (endpoints.has(str)) { + this.errors.push(new ParserErrorDuplicateEndpoint({ endpoint: str, path: [integrationName, 'syncs', sync.name, '[endpoints]'] })); + continue; + } - endpoints.add(str); - const modelInUrl = endpoint.match(/{([^}]+)}/); - if (modelInUrl) { - const modelName = modelInUrl[1]!; - if (!this.modelsParser.get(modelName)) { - this.errors.push( - new ParserErrorModelNotFound({ model: modelName, path: [integrationName, 'syncs', sync.name, '[endpoints]'] }) - ); - } + endpoints.add(str); + const modelInUrl = endpoint.path.match(/{([^}]+)}/); + if (modelInUrl) { + const modelName = modelInUrl[1]!; + if (!this.modelsParser.get(modelName)) { + this.errors.push(new ParserErrorModelNotFound({ model: modelName, path: [integrationName, 'syncs', sync.name, '[endpoints]'] })); } } } @@ -195,27 +187,19 @@ export abstract class NangoYamlParser { find.forEach((name) => usedModelsAction.add(name)); } if (action.endpoint) { - for (const [verb, endpoint] of Object.entries(action.endpoint)) { - if (!endpoint) { - continue; // TS pleasing - } + const endpoint = action.endpoint; - const str = `${verb} ${endpoint}`; - if (endpoints.has(str)) { - this.errors.push( - new ParserErrorDuplicateEndpoint({ endpoint: str, path: [integrationName, 'actions', action.name, '[endpoint]'] }) - ); - continue; - } - endpoints.add(str); - const modelInUrl = endpoint.match(/{([^}]+)}/); - if (modelInUrl) { - const modelName = modelInUrl[1]!.split(':')[0]!; - if (!this.modelsParser.get(modelName)) { - this.errors.push( - new ParserErrorModelNotFound({ model: modelName, path: [integrationName, 'syncs', action.name, '[endpoint]'] }) - ); - } + const str = `${endpoint.method} ${endpoint.path}`; + if (endpoints.has(str)) { + this.errors.push(new ParserErrorDuplicateEndpoint({ endpoint: str, path: [integrationName, 'actions', action.name, '[endpoint]'] })); + continue; + } + endpoints.add(str); + const modelInUrl = endpoint.path.match(/{([^}]+)}/); + if (modelInUrl) { + const modelName = modelInUrl[1]!.split(':')[0]!; + if (!this.modelsParser.get(modelName)) { + this.errors.push(new ParserErrorModelNotFound({ model: modelName, path: [integrationName, 'syncs', action.name, '[endpoint]'] })); } } } diff --git a/packages/nango-yaml/lib/parser.v2.ts b/packages/nango-yaml/lib/parser.v2.ts index f0d5b193666..41a312e7562 100644 --- a/packages/nango-yaml/lib/parser.v2.ts +++ b/packages/nango-yaml/lib/parser.v2.ts @@ -1,7 +1,6 @@ import type { - HTTP_VERB, NangoModel, - NangoSyncEndpoint, + NangoSyncEndpointV2, NangoYamlParsedIntegration, NangoYamlV2, NangoYamlV2Integration, @@ -13,7 +12,7 @@ import type { } from '@nangohq/types'; import { NangoYamlParser } from './parser.js'; import { ParserErrorEndpointsMismatch, ParserErrorInvalidRuns } from './errors.js'; -import { getInterval } from './helpers.js'; +import { getInterval, parseEndpoint } from './helpers.js'; export class NangoYamlParserV2 extends NangoYamlParser { parse(): boolean { @@ -85,7 +84,7 @@ export class NangoYamlParserV2 extends NangoYamlParser { modelNames.add(modelInput.name); } - const endpoints: NangoSyncEndpoint[] = []; + const endpoints: NangoSyncEndpointV2[] = []; if (sync.endpoint) { const tmp = Array.isArray(sync.endpoint) ? sync.endpoint : [sync.endpoint]; @@ -95,12 +94,7 @@ export class NangoYamlParserV2 extends NangoYamlParser { } for (const endpoint of tmp) { - const split = endpoint.split(' '); - if (split.length === 2) { - endpoints.push({ [split[0] as HTTP_VERB]: split[1] }); - } else { - endpoints.push({ GET: split[0]! }); - } + endpoints.push(parseEndpoint(endpoint, 'GET')); } } @@ -163,14 +157,9 @@ export class NangoYamlParserV2 extends NangoYamlParser { modelNames.add(modelInput.name); } - const endpoint: NangoSyncEndpoint = {}; + let endpoint: NangoSyncEndpointV2 | null = null; if (action.endpoint) { - const split = action.endpoint.split(' '); - if (split.length === 2) { - endpoint[split[0]! as HTTP_VERB] = split[1]!; - } else { - endpoint['POST'] = split[0]!; - } + endpoint = parseEndpoint(action.endpoint, 'POST'); } const parsedAction: ParsedNangoAction = { diff --git a/packages/nango-yaml/lib/parser.v2.unit.test.ts b/packages/nango-yaml/lib/parser.v2.unit.test.ts index 6c5ab66b009..9e5a3b5973d 100644 --- a/packages/nango-yaml/lib/parser.v2.unit.test.ts +++ b/packages/nango-yaml/lib/parser.v2.unit.test.ts @@ -25,7 +25,7 @@ describe('parse', () => { { auto_start: true, description: '', - endpoints: [{ GET: '/provider/top' }], + endpoints: [{ method: 'GET', path: '/provider/top' }], input: 'GithubIssue', name: 'top', output: ['GithubIssue'], @@ -44,7 +44,7 @@ describe('parse', () => { { description: '', input: 'Anonymous_provider_action_createIssue_input', - endpoint: { POST: '/test' }, + endpoint: { method: 'POST', path: '/test' }, name: 'createIssue', output: ['GithubIssue'], scopes: [], @@ -92,7 +92,7 @@ describe('parse', () => { { description: '', input: null, - endpoint: { POST: '/test' }, + endpoint: { method: 'POST', path: '/test' }, name: 'createIssue', output: ['Start'], scopes: [], @@ -131,7 +131,7 @@ describe('parse', () => { { auto_start: true, description: '', - endpoints: [{ GET: '/provider/top' }], + endpoints: [{ method: 'GET', path: '/provider/top' }], input: 'Anonymous_provider_sync_top_input', name: 'top', output: ['Anonymous_provider_sync_top_output'], @@ -235,7 +235,7 @@ describe('parse', () => { expect(parser.warnings).toStrictEqual([]); expect(parser.parsed?.integrations[0]?.actions).toMatchObject([ { - endpoint: { GET: '/ticketing/tickets/{Found:id}' } + endpoint: { method: 'GET', path: '/ticketing/tickets/{Found:id}' } } ]); }); diff --git a/packages/node-client/lib/types.ts b/packages/node-client/lib/types.ts index b72d249dcc2..84b8732683c 100644 --- a/packages/node-client/lib/types.ts +++ b/packages/node-client/lib/types.ts @@ -11,8 +11,8 @@ import type { AuthOperationType, AuthModeType, AuthModes, - HTTP_VERB, - NangoSyncEndpoint, + HTTP_METHOD, + NangoSyncEndpointV2, AllAuthCredentials, OAuth1Credentials, OAuth2Credentials, @@ -72,7 +72,7 @@ export type { JwtCredentials, TwoStepCredentials }; -export type { HTTP_VERB, NangoSyncEndpoint }; +export type { HTTP_METHOD, NangoSyncEndpointV2 }; export type { RecordMetadata, RecordLastAction, NangoRecord }; export type { @@ -269,7 +269,7 @@ export interface NangoSyncConfig { track_deletes?: boolean; returns: string[]; models: NangoSyncModel[]; - endpoints: NangoSyncEndpoint[]; + endpoints: NangoSyncEndpointV2[]; is_public?: boolean; pre_built?: boolean; version?: string | null; diff --git a/packages/server/lib/controllers/proxy.controller.ts b/packages/server/lib/controllers/proxy.controller.ts index 82aac8f9225..03a1a095d91 100644 --- a/packages/server/lib/controllers/proxy.controller.ts +++ b/packages/server/lib/controllers/proxy.controller.ts @@ -8,7 +8,7 @@ import url from 'url'; import querystring from 'querystring'; import type { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios'; import { backOff } from 'exponential-backoff'; -import type { HTTP_VERB, UserProvidedProxyConfiguration, InternalProxyConfiguration, ApplicationConstructedProxyConfiguration, File } from '@nangohq/shared'; +import type { HTTP_METHOD, UserProvidedProxyConfiguration, InternalProxyConfiguration, ApplicationConstructedProxyConfiguration, File } from '@nangohq/shared'; import { LogActionEnum, errorManager, ErrorSourceEnum, proxyService, connectionService, configService, featureFlags } from '@nangohq/shared'; import { metrics, getLogger, axiosInstance as axios, getHeaders } from '@nangohq/utils'; import { logContextGetter } from '@nangohq/logs'; @@ -80,7 +80,7 @@ class ProxyController { headers, baseUrlOverride, decompress: decompress === 'true' ? true : false, - method: method.toUpperCase() as HTTP_VERB, + method: method.toUpperCase() as HTTP_METHOD, retryOn }; @@ -164,7 +164,7 @@ class ProxyController { return; } - await this.sendToHttpMethod({ res, method: method as HTTP_VERB, configBody: proxyConfig, logCtx, isDebug }); + await this.sendToHttpMethod({ res, method: method as HTTP_METHOD, configBody: proxyConfig, logCtx, isDebug }); } catch (err) { const connectionId = req.get('Connection-Id') as string; const providerConfigKey = req.get('Provider-Config-Key') as string; @@ -212,7 +212,7 @@ class ProxyController { isDebug }: { res: Response; - method: HTTP_VERB; + method: HTTP_METHOD; configBody: ApplicationConstructedProxyConfiguration; logCtx: LogContext; isDebug: boolean; @@ -392,7 +392,7 @@ class ProxyController { isDebug }: { res: Response; - method: HTTP_VERB; + method: HTTP_METHOD; url: string; config: ApplicationConstructedProxyConfiguration; decompress: boolean; diff --git a/packages/server/lib/controllers/sync.controller.ts b/packages/server/lib/controllers/sync.controller.ts index 209a0e35968..3b12702fc8f 100644 --- a/packages/server/lib/controllers/sync.controller.ts +++ b/packages/server/lib/controllers/sync.controller.ts @@ -1,5 +1,5 @@ import type { Request, Response, NextFunction } from 'express'; -import type { NangoConnection, HTTP_VERB, Connection, Sync } from '@nangohq/shared'; +import type { NangoConnection, HTTP_METHOD, Connection, Sync } from '@nangohq/shared'; import tracer from 'dd-trace'; import type { Span } from 'dd-trace'; import { @@ -231,7 +231,7 @@ class SyncController { return; } - const { action, model } = await getActionOrModelByEndpoint(connection as NangoConnection, req.method as HTTP_VERB, path); + const { action, model } = await getActionOrModelByEndpoint(connection as NangoConnection, req.method as HTTP_METHOD, path); if (action) { const input = req.body || req.params[1]; req.body = {}; diff --git a/packages/server/lib/controllers/sync/deploy/postConfirmation.ts b/packages/server/lib/controllers/sync/deploy/postConfirmation.ts index 6e862d4cbed..cd30e73c95e 100644 --- a/packages/server/lib/controllers/sync/deploy/postConfirmation.ts +++ b/packages/server/lib/controllers/sync/deploy/postConfirmation.ts @@ -57,15 +57,23 @@ export const flowConfig = z input: z.union([z.string().max(255), z.any()]).optional(), endpoints: z .array( - z - .object({ - GET: z.string().optional(), - POST: z.string().optional(), - PATCH: z.string().optional(), - PUT: z.string().optional(), - DELETE: z.string().optional() - }) - .strict() + z.union([ + z + .object({ + method: z.enum(['GET', 'POST', 'PUT', 'PATCH', 'DELETE']), + path: z.string() + }) + .strict(), + z + .object({ + GET: z.string().optional(), + POST: z.string().optional(), + PATCH: z.string().optional(), + PUT: z.string().optional(), + DELETE: z.string().optional() + }) + .strict() + ]) ) .optional(), syncName: z.string(), diff --git a/packages/server/lib/controllers/sync/deploy/postDeploy.integration.test.ts b/packages/server/lib/controllers/sync/deploy/postDeploy.integration.test.ts index b658f5737b5..97d066d54f7 100644 --- a/packages/server/lib/controllers/sync/deploy/postDeploy.integration.test.ts +++ b/packages/server/lib/controllers/sync/deploy/postDeploy.integration.test.ts @@ -104,7 +104,7 @@ describe(`POST ${endpoint}`, () => { syncName: 'test', fileBody: { js: 'js file', ts: 'ts file' }, providerConfigKey: 'unauthenticated', - endpoints: [{ GET: '/path' }], + endpoints: [{ method: 'GET', path: '/path' }], runs: 'every day', type: 'sync', attributes: {}, @@ -151,7 +151,7 @@ describe(`POST ${endpoint}`, () => { auto_start: false, description: 'a', enabled: true, - endpoints: [{ GET: '/path' }], + endpoints: [{ method: 'GET', path: '/path' }], input: { fields: [{ array: false, name: 'id', optional: false, tsType: true, value: 'number' }], name: 'Input' diff --git a/packages/server/lib/controllers/sync/deploy/postDeploy.ts b/packages/server/lib/controllers/sync/deploy/postDeploy.ts index 3c0dfec1cdc..4bb06bbb994 100644 --- a/packages/server/lib/controllers/sync/deploy/postDeploy.ts +++ b/packages/server/lib/controllers/sync/deploy/postDeploy.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { requireEmptyQuery, zodErrorToHTTP } from '@nangohq/utils'; import type { PostDeploy } from '@nangohq/types'; import { asyncWrapper } from '../../../utils/asyncWrapper.js'; -import { AnalyticsTypes, analytics, deploy, errorManager, getAndReconcileDifferences } from '@nangohq/shared'; +import { AnalyticsTypes, analytics, cleanIncomingFlow, deploy, errorManager, getAndReconcileDifferences } from '@nangohq/shared'; import { getOrchestrator } from '../../../utils/utils.js'; import { logContextGetter } from '@nangohq/logs'; import { flowConfigs, jsonSchema, postConnectionScriptsByProvider } from './postConfirmation.js'; @@ -44,7 +44,7 @@ export const postDeploy = asyncWrapper(async (req, res) => { } = await deploy({ environment, account, - flows: body.flowConfigs, + flows: cleanIncomingFlow(body.flowConfigs), nangoYamlBody: body.nangoYamlBody, postConnectionScriptsByProvider: body.postConnectionScriptsByProvider, debug: body.debug, diff --git a/packages/server/lib/controllers/sync/deploy/postDeployInternal.ts b/packages/server/lib/controllers/sync/deploy/postDeployInternal.ts index deff124ba01..150fbe6d713 100644 --- a/packages/server/lib/controllers/sync/deploy/postDeployInternal.ts +++ b/packages/server/lib/controllers/sync/deploy/postDeployInternal.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { zodErrorToHTTP } from '@nangohq/utils'; import type { PostDeployInternal } from '@nangohq/types'; import { asyncWrapper } from '../../../utils/asyncWrapper.js'; -import { deploy, errorManager, getAndReconcileDifferences, environmentService, configService, connectionService } from '@nangohq/shared'; +import { deploy, errorManager, getAndReconcileDifferences, environmentService, configService, connectionService, cleanIncomingFlow } from '@nangohq/shared'; import { getOrchestrator } from '../../../utils/utils.js'; import { logContextGetter } from '@nangohq/logs'; import { flowConfigs, jsonSchema, postConnectionScriptsByProvider } from './postConfirmation.js'; @@ -95,7 +95,7 @@ export const postDeployInternal = asyncWrapper(async (req, r } = await deploy({ environment, account, - flows: body.flowConfigs, + flows: cleanIncomingFlow(body.flowConfigs), nangoYamlBody: body.nangoYamlBody, postConnectionScriptsByProvider: body.postConnectionScriptsByProvider, debug: body.debug, diff --git a/packages/server/lib/controllers/v1/integrations/providerConfigKey/flows/getFlows.ts b/packages/server/lib/controllers/v1/integrations/providerConfigKey/flows/getFlows.ts index 629aa907dc9..d0a82b21343 100644 --- a/packages/server/lib/controllers/v1/integrations/providerConfigKey/flows/getFlows.ts +++ b/packages/server/lib/controllers/v1/integrations/providerConfigKey/flows/getFlows.ts @@ -1,6 +1,6 @@ import { asyncWrapper } from '../../../../../utils/asyncWrapper.js'; import { requireEmptyQuery, zodErrorToHTTP } from '@nangohq/utils'; -import type { GetIntegration, GetIntegrationFlows, HTTP_VERB } from '@nangohq/types'; +import type { GetIntegration, GetIntegrationFlows } from '@nangohq/types'; import type { NangoSyncConfig } from '@nangohq/shared'; import { configService, flowService, getSyncConfigsAsStandardConfig } from '@nangohq/shared'; import { validationParams } from '../getIntegration.js'; @@ -59,10 +59,8 @@ export const getIntegrationFlows = asyncWrapper(async (req, function containsSameEndpoint(flowA: NangoSyncConfig, flowB: NangoSyncConfig) { for (const endpointObjA of flowA.endpoints) { - const endpointA = Object.entries(endpointObjA) as unknown as [HTTP_VERB, string]; - for (const endpointObjB of flowB.endpoints) { - if (endpointObjB[endpointA[0]] && endpointObjB[endpointA[0]] === endpointA[1]) { + if (endpointObjB.method && endpointObjA.method && endpointObjB.path === endpointObjA.path) { return true; } } diff --git a/packages/server/lib/middleware/cliVersionCheck.ts b/packages/server/lib/middleware/cliVersionCheck.ts new file mode 100644 index 00000000000..c9bea6e69fe --- /dev/null +++ b/packages/server/lib/middleware/cliVersionCheck.ts @@ -0,0 +1,35 @@ +import { getLogger } from '@nangohq/utils'; +import type { Request, Response, NextFunction } from 'express'; + +import semver from 'semver'; + +const logger = getLogger('CliVersionCheck'); + +const VERSION_REGEX = /nango-cli\/([0-9.]+)/; +export function cliMinVersion(minVersion: string) { + return (req: Request, _: Response, next: NextFunction) => { + const userAgent = req.headers['user-agent']; + if (!userAgent) { + // Could be strictly enforced + next(); + return; + } + + const match = userAgent.match(VERSION_REGEX); + if (!match || match.length <= 1 || !match[1]) { + // Could be strictly enforced + next(); + return; + } + + if (semver.gt(minVersion, match[1])) { + logger.info(`This endpoint requires a CLI version >= ${minVersion} (current: ${match[0]})`); + // res.status(400).send({ + // error: { code: 'invalid_cli_version', message: `This endpoint requires a CLI version >= ${minVersion} (current: ${match[0]})` } + // }); + // return; + } + + next(); + }; +} diff --git a/packages/server/lib/routes.ts b/packages/server/lib/routes.ts index 6180569bca0..0cf0065acd1 100644 --- a/packages/server/lib/routes.ts +++ b/packages/server/lib/routes.ts @@ -103,6 +103,7 @@ import { getConnections } from './controllers/v1/connections/getConnections.js'; import { getPublicConnections } from './controllers/connection/getConnections.js'; import { getConnectionsCount } from './controllers/v1/connections/getConnectionsCount.js'; import { getConnectionRefresh } from './controllers/v1/connections/connectionId/postRefresh.js'; +import { cliMinVersion } from './middleware/cliVersionCheck.js'; export const router = express.Router(); @@ -219,8 +220,8 @@ publicAPI.route('/connection/metadata').post(apiAuth, postPublicMetadata); publicAPI.route('/connection/metadata').patch(apiAuth, patchPublicMetadata); publicAPI.route('/connection').post(apiAuth, connectionController.createConnection.bind(connectionController)); publicAPI.route('/environment-variables').get(apiAuth, environmentController.getEnvironmentVariables.bind(connectionController)); -publicAPI.route('/sync/deploy').post(apiAuth, postDeploy); -publicAPI.route('/sync/deploy/confirmation').post(apiAuth, postDeployConfirmation); +publicAPI.route('/sync/deploy').post(apiAuth, cliMinVersion('0.39.25'), postDeploy); +publicAPI.route('/sync/deploy/confirmation').post(apiAuth, cliMinVersion('0.39.25'), postDeployConfirmation); publicAPI.route('/sync/deploy/internal').post(apiAuth, postDeployInternal); publicAPI.route('/sync/update-connection-frequency').put(apiAuth, syncController.updateFrequencyForConnection.bind(syncController)); publicAPI.route('/records').get(apiAuth, syncController.getAllRecords.bind(syncController)); diff --git a/packages/server/package.json b/packages/server/package.json index cbb82bb68f7..1a65baa6198 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -57,6 +57,7 @@ "passport-local": "1.0.0", "rate-limiter-flexible": "5.0.3", "redis": "4.6.13", + "semver": "7.6.3", "simple-oauth2": "5.1.0", "uuid": "9.0.0", "ws": "8.18.0", diff --git a/packages/shared/lib/models/Generic.ts b/packages/shared/lib/models/Generic.ts index be8b190b35c..7625734851f 100644 --- a/packages/shared/lib/models/Generic.ts +++ b/packages/shared/lib/models/Generic.ts @@ -1,6 +1,6 @@ import type { NangoError } from '../utils/error.js'; -export type HTTP_VERB = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'; +export type HTTP_METHOD = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'; export interface DBConfig { encryption_key_hash?: string | null; diff --git a/packages/shared/lib/models/NangoConfig.ts b/packages/shared/lib/models/NangoConfig.ts index f6a699e2f76..7c7afdf3fee 100644 --- a/packages/shared/lib/models/NangoConfig.ts +++ b/packages/shared/lib/models/NangoConfig.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/consistent-indexed-object-style */ -import type { NangoConfigMetadata, NangoSyncEndpoint, ScriptTypeLiteral } from '@nangohq/types'; +import type { NangoConfigMetadata, NangoSyncEndpointV2, ScriptTypeLiteral } from '@nangohq/types'; import type { SyncType } from './Sync.js'; import type { JSONSchema7 } from 'json-schema'; @@ -17,7 +17,7 @@ export interface NangoIntegrationDataV1 { sync_config_id?: number; pre_built?: boolean; is_public?: boolean; - endpoint?: string | string[]; + endpoint?: string | string[] | NangoSyncEndpointV2 | NangoSyncEndpointV2[]; nango_yaml_version?: string; enabled?: boolean; } @@ -120,7 +120,7 @@ export interface NangoSyncConfig { track_deletes?: boolean; returns: string[]; models: NangoSyncModel[]; - endpoints: NangoSyncEndpoint[]; + endpoints: NangoSyncEndpointV2[]; is_public?: boolean | null; pre_built?: boolean | null; version?: string | null; diff --git a/packages/shared/lib/models/Proxy.ts b/packages/shared/lib/models/Proxy.ts index d687877276f..148711e51b1 100644 --- a/packages/shared/lib/models/Proxy.ts +++ b/packages/shared/lib/models/Proxy.ts @@ -1,5 +1,5 @@ import type { ParamsSerializerOptions } from 'axios'; -import type { HTTP_VERB } from './Generic.js'; +import type { HTTP_METHOD } from './Generic.js'; import type { BasicApiCredentials, ApiKeyCredentials, AppCredentials, TbaCredentials, TableauCredentials, JwtCredentials } from './Auth.js'; import type { Connection } from './Connection.js'; import type { Provider, TwoStepCredentials } from '@nangohq/types'; @@ -40,7 +40,7 @@ export interface UserProvidedProxyConfiguration extends BaseProxyConfiguration { export interface ApplicationConstructedProxyConfiguration extends BaseProxyConfiguration { decompress?: boolean; - method: HTTP_VERB; + method: HTTP_METHOD; providerName: string; token: string | BasicApiCredentials | ApiKeyCredentials | AppCredentials | TbaCredentials | TableauCredentials | JwtCredentials | TwoStepCredentials; provider: Provider; diff --git a/packages/shared/lib/models/Sync.ts b/packages/shared/lib/models/Sync.ts index 1659fa09e75..d2d43960205 100644 --- a/packages/shared/lib/models/Sync.ts +++ b/packages/shared/lib/models/Sync.ts @@ -1,6 +1,6 @@ import type { JSONSchema7 } from 'json-schema'; -import type { HTTP_VERB, Timestamps, TimestampsAndDeleted } from './Generic.js'; -import type { NangoConfigMetadata, NangoModel, NangoSyncEndpoint, ScriptTypeLiteral } from '@nangohq/types'; +import type { HTTP_METHOD, Timestamps, TimestampsAndDeleted } from './Generic.js'; +import type { NangoConfigMetadata, NangoModel, NangoSyncEndpointV2, ScriptTypeLiteral } from '@nangohq/types'; import type { LogContext } from '@nangohq/logs'; export enum SyncStatus { @@ -97,7 +97,7 @@ export interface SyncConfig extends TimestampsAndDeleted { version?: string; pre_built?: boolean | null; is_public?: boolean | null; - endpoints?: NangoSyncEndpoint[]; + endpoints?: NangoSyncEndpointV2[]; input?: string | undefined; sync_type?: SyncType | undefined; webhook_subscriptions: string[] | null; @@ -108,7 +108,7 @@ export interface SyncConfig extends TimestampsAndDeleted { export interface SyncEndpoint extends Timestamps { id?: number; sync_config_id: number; - method: HTTP_VERB; + method: HTTP_METHOD; path: string; model?: string; } diff --git a/packages/shared/lib/services/file/local.service.ts b/packages/shared/lib/services/file/local.service.ts index e5799763c92..cd8907c8970 100644 --- a/packages/shared/lib/services/file/local.service.ts +++ b/packages/shared/lib/services/file/local.service.ts @@ -7,11 +7,13 @@ import errorManager, { ErrorSourceEnum } from '../../utils/error.manager.js'; import { NangoError } from '../../utils/error.js'; import { LogActionEnum } from '../../models/Telemetry.js'; import type { LayoutMode } from '../../models/NangoConfig.js'; -import { nangoConfigFile, SYNC_FILE_EXTENSION } from '../nango-config.service.js'; +import { nangoConfigFile } from '@nangohq/nango-yaml'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); +const SYNC_FILE_EXTENSION = 'js'; + class LocalFileService { public readFile(rawFilePath: string) { try { diff --git a/packages/shared/lib/services/file/remote.service.ts b/packages/shared/lib/services/file/remote.service.ts index c10291dcc42..0af27c11a8c 100644 --- a/packages/shared/lib/services/file/remote.service.ts +++ b/packages/shared/lib/services/file/remote.service.ts @@ -8,8 +8,8 @@ import { NangoError } from '../../utils/error.js'; import errorManager, { ErrorSourceEnum } from '../../utils/error.manager.js'; import { LogActionEnum } from '../../models/Telemetry.js'; import type { ServiceResponse } from '../../models/Generic.js'; -import { nangoConfigFile } from '../nango-config.service.js'; import localFileService from './local.service.js'; +import { nangoConfigFile } from '@nangohq/nango-yaml'; let client: S3Client | null = null; let useS3 = !isLocal && !isTest; diff --git a/packages/shared/lib/services/flow.service.ts b/packages/shared/lib/services/flow.service.ts index df0638ee98e..91e47a5635b 100644 --- a/packages/shared/lib/services/flow.service.ts +++ b/packages/shared/lib/services/flow.service.ts @@ -4,9 +4,7 @@ import fs from 'fs'; import { dirname } from '../utils/utils.js'; import { getPublicConfig } from './sync/config/config.service.js'; import { loadStandardConfig } from './nango-config.service.js'; -import remoteFileService from './file/remote.service.js'; -import type { NangoConfig, NangoIntegration, NangoSyncConfig, NangoModelV1, StandardNangoConfig } from '../models/NangoConfig.js'; -import type { HTTP_VERB } from '../models/Generic.js'; +import type { NangoConfig, NangoIntegration, NangoModelV1, StandardNangoConfig } from '../models/NangoConfig.js'; import { errorManager } from '../index.js'; import { stringifyError } from '@nangohq/utils'; import type { ScriptTypeLiteral } from '@nangohq/types'; @@ -50,7 +48,7 @@ class FlowService { models: models as NangoModelV1 }; - const { success, response } = loadStandardConfig(nangoConfig, false, true); + const { success, response } = loadStandardConfig(nangoConfig); if (success && response) { if (rawName) { @@ -126,76 +124,6 @@ class FlowService { return null; } - public getActionAsNangoConfig(provider: string, name: string): NangoConfig | null { - const integrations = this.getAllAvailableFlowsAsStandardConfig(); - - let foundAction: NangoSyncConfig | null = null; - let foundProvider = ''; - - for (const integration of integrations) { - if (integration.providerConfigKey === provider) { - foundProvider = integration.rawName || provider; - for (const action of integration.actions) { - if (action.name === name) { - foundAction = action; - } - } - } - } - - if (!foundAction) { - return null; - } - - const nangoConfig = { - integrations: { - [foundProvider]: { - [foundAction.name]: { - sync_config_id: foundAction.id, - runs: '', - type: foundAction.type, - returns: foundAction.returns, - input: foundAction.input, - track_deletes: false, - auto_start: false, - attributes: foundAction.attributes, - fileLocation: remoteFileService.getRemoteFileLocationForPublicTemplate(foundProvider, foundAction.name), - version: '1', - pre_built: true, - is_public: true, - metadata: { - description: foundAction.description, - scopes: foundAction.scopes - } - } - } - }, - models: {} - } as NangoConfig; - - return nangoConfig; - } - - public getPublicActionByPathAndMethod(provider: string, path: string, method: string): string | null { - let foundAction = null; - const integrations = this.getAllAvailableFlowsAsStandardConfig(); - - for (const integration of integrations) { - if (integration.providerConfigKey === provider) { - for (const action of integration.actions) { - const endpoints = Array.isArray(action.endpoints) ? action.endpoints : [action.endpoints]; - for (const endpoint of endpoints) { - if (endpoint[method as HTTP_VERB] && endpoint[method as HTTP_VERB] === path) { - foundAction = action.name; - } - } - } - } - } - - return foundAction; - } - public async getAddedPublicFlows(environmentId: number) { return getPublicConfig(environmentId); } diff --git a/packages/shared/lib/services/nango-config.service.ts b/packages/shared/lib/services/nango-config.service.ts index 95c73f9aadb..48f69a616bb 100644 --- a/packages/shared/lib/services/nango-config.service.ts +++ b/packages/shared/lib/services/nango-config.service.ts @@ -9,17 +9,18 @@ import type { NangoIntegrationDataV2, LayoutMode } from '../models/NangoConfig.js'; -import type { HTTP_VERB, ServiceResponse } from '../models/Generic.js'; +import type { ServiceResponse } from '../models/Generic.js'; import { SyncType } from '../models/Sync.js'; import localFileService from './file/local.service.js'; import { NangoError } from '../utils/error.js'; -import { determineVersion, getInterval, isJsOrTsType } from '@nangohq/nango-yaml'; -import type { NangoSyncEndpoint, ScriptTypeLiteral } from '@nangohq/types'; - -export const nangoConfigFile = 'nango.yaml'; -export const SYNC_FILE_EXTENSION = 'js'; - -export function loadStandardConfig(configData: NangoConfig, showMessages = false, isPublic?: boolean | null): ServiceResponse { +import { determineVersion, getInterval, isJsOrTsType, parseEndpoint } from '@nangohq/nango-yaml'; +import type { NangoSyncEndpointV2 } from '@nangohq/types'; + +/** + * Legacy parser only used for flows.yaml + * TODO: kill this in favor of nango-yaml + transformation to StandardNangoConfig + */ +export function loadStandardConfig(configData: NangoConfig): ServiceResponse { try { if (!configData) { return { success: false, error: new NangoError('no_config_found'), response: null }; @@ -30,12 +31,11 @@ export function loadStandardConfig(configData: NangoConfig, showMessages = false return { success: true, error: null, response: [] }; } - const configServiceResponse = - version === 'v1' ? convertConfigObject(configData as NangoConfigV1) : convertV2ConfigObject(configData as NangoConfigV2, showMessages, isPublic); + const configServiceResponse = version === 'v1' ? convertConfigObject(configData as NangoConfigV1) : convertV2ConfigObject(configData as NangoConfigV2); return configServiceResponse; - } catch (error: any) { - return { success: false, error: new NangoError('error_loading_nango_config', error?.message), response: null }; + } catch (error) { + return { success: false, error: new NangoError('error_loading_nango_config', error instanceof Error ? error.message : {}), response: null }; } } @@ -78,7 +78,7 @@ function getFieldsForModel(modelName: string, config: NangoConfig): { name: stri return modelFields; } -export function convertConfigObject(config: NangoConfigV1): ServiceResponse { +function convertConfigObject(config: NangoConfigV1): ServiceResponse { const output = []; for (const providerConfigKey in config.integrations) { @@ -149,36 +149,6 @@ export function convertConfigObject(config: NangoConfigV1): ServiceResponse { - let endpoints: NangoSyncEndpoint[] = []; - const endpoint = rawEndpoint.split(' '); - - if (endpoint.length > 1) { - const method = singleAllowedMethod ? defaultMethod : (endpoint[0]?.toUpperCase() as HTTP_VERB); - - if (singleAllowedMethod && showMessages && endpoint[0]?.toUpperCase() !== defaultMethod) { - console.log(`A sync only allows for a ${defaultMethod} method. The provided ${endpoint[0]?.toUpperCase()} method will be ignored.`); - } - - endpoints = [ - { - [method]: endpoint[1] as string - } - ]; - } else { - if (showMessages && !singleAllowedMethod) { - console.log(`No HTTP method provided for endpoint ${endpoint[0]}. Defaulting to ${defaultMethod}.`); - } - endpoints = [ - { - [defaultMethod]: endpoint[0] as string - } - ]; - } - - return endpoints; -}; - const parseModelInEndpoint = (endpoint: string, allModelNames: string[], inputModel: NangoSyncModel, config: NangoConfig): ServiceResponse => { if (Object.keys(inputModel).length > 0) { return { success: false, error: new NangoError('conflicting_model_and_input'), response: null }; @@ -221,7 +191,7 @@ const isEnabled = (script: NangoIntegrationDataV2): boolean => { return false; }; -export function convertV2ConfigObject(config: NangoConfigV2, showMessages = false, isPublic?: boolean | null): ServiceResponse { +function convertV2ConfigObject(config: NangoConfigV2): ServiceResponse { const output: StandardNangoConfig[] = []; const allModelNames = config.models ? Object.keys(config.models) : []; @@ -235,19 +205,11 @@ export function convertV2ConfigObject(config: NangoConfigV2, showMessages = fals delete integration['provider']; } - // check that every endpoint is unique across syncs and actions - const allEndpoints: string[] = []; - const allModels: string[] = []; - const syncs = integration['syncs'] as NangoV2Integration; const actions = integration['actions'] as NangoV2Integration; const postConnectionScripts: string[] = (integration['post-connection-scripts'] || []) as string[]; - const { - success: builtSyncSuccess, - error: builtSyncError, - response: builtSyncs - } = buildSyncs({ syncs, allModelNames, config, providerConfigKey, showMessages, isPublic, allModels, allEndpoints }); + const { success: builtSyncSuccess, error: builtSyncError, response: builtSyncs } = buildSyncs({ syncs, allModelNames, config, providerConfigKey }); if (!builtSyncSuccess || !builtSyncs) { return { success: builtSyncSuccess, error: builtSyncError, response: null }; @@ -257,7 +219,7 @@ export function convertV2ConfigObject(config: NangoConfigV2, showMessages = fals success: builtActionSuccess, error: builtActionError, response: builtActions - } = buildActions({ actions, allModelNames, config, providerConfigKey, showMessages, isPublic, allModels, allEndpoints }); + } = buildActions({ actions, allModelNames, config, providerConfigKey }); if (!builtActionSuccess || !builtActions) { return { success: builtActionSuccess, error: builtActionError, response: null }; @@ -281,32 +243,17 @@ export function convertV2ConfigObject(config: NangoConfigV2, showMessages = fals function formModelOutput({ integrationData, - allModels, allModelNames, - config, - name, - type + config }: { integrationData: NangoIntegrationDataV2; - allModels: string[]; allModelNames: string[]; config: NangoConfigV2; - name: string; - type: ScriptTypeLiteral; }): ServiceResponse { const models: NangoSyncModel[] = []; if (integrationData.output) { const integrationDataReturns = Array.isArray(integrationData.output) ? integrationData.output : [integrationData.output]; for (const model of integrationDataReturns) { - if (allModels.includes(model) && type === 'sync') { - const error = new NangoError('duplicate_model', { model, name, type: 'sync' }); - return { success: false, error, response: null }; - } - - if (!allModels.includes(model) && !isJsOrTsType(model)) { - allModels.push(model); - } - const modelFields = getFieldsForModel(model, config) as { name: string; type: string }[]; if (modelFields) { @@ -338,30 +285,18 @@ function buildSyncs({ syncs, allModelNames, config, - providerConfigKey, - showMessages, - isPublic, - allModels, - allEndpoints + providerConfigKey }: { syncs: NangoV2Integration; allModelNames: string[]; config: NangoConfigV2; providerConfigKey: string; - showMessages: boolean; - isPublic: boolean | null | undefined; - allModels: string[]; - allEndpoints: string[]; }): ServiceResponse { const builtSyncs: NangoSyncConfig[] = []; for (const syncName in syncs) { const sync: NangoIntegrationDataV2 = syncs[syncName] as NangoIntegrationDataV2; - const { - success: modelSuccess, - error: modelError, - response: models - } = formModelOutput({ integrationData: sync, allModels, allModelNames, config, name: syncName, type: 'sync' }); + const { success: modelSuccess, error: modelError, response: models } = formModelOutput({ integrationData: sync, allModelNames, config }); if (!modelSuccess || !models) { return { success: false, error: modelError, response: null }; @@ -379,46 +314,21 @@ function buildSyncs({ } } - let endpoints: NangoSyncEndpoint[] = []; + const endpoints: NangoSyncEndpointV2[] = []; if (sync?.endpoint) { if (Array.isArray(sync.endpoint)) { - if (sync.endpoint?.length !== sync.output?.length) { - const error = new NangoError('endpoint_output_mismatch', syncName); - return { success: false, error, response: null }; - } for (const endpoint of sync.endpoint) { - endpoints.push(...assignEndpoints(endpoint, 'GET', true, showMessages)); - - if (!allEndpoints.includes(endpoint)) { - allEndpoints.push(endpoint); - } else { - const error = new NangoError('duplicate_endpoint', endpoint); - return { success: false, error, response: null }; - } + const parsed = parseEndpoint(endpoint, 'GET'); + endpoints.push(parsed); } } else { - endpoints = assignEndpoints(sync.endpoint, 'GET', true, showMessages); - - if (sync.output && Array.isArray(sync.output) && sync.output?.length > 1) { - const error = new NangoError('endpoint_output_mismatch', syncName); - return { success: false, error, response: null }; - } - - if (!allEndpoints.includes(sync.endpoint)) { - allEndpoints.push(sync.endpoint); - } else { - const error = new NangoError('duplicate_endpoint', sync.endpoint); - return { success: false, error, response: null }; - } + const parsed = parseEndpoint(sync.endpoint, 'GET'); + endpoints.push(parsed); } } const scopes = sync?.scopes || sync?.metadata?.scopes || []; - if (!sync?.runs && showMessages) { - console.log(`No runs property found for sync "${syncName}". Defaulting to every day.`); - } - const runs = sync?.runs || 'every day'; const interval = getInterval(runs, new Date()); @@ -435,8 +345,6 @@ function buildSyncs({ webhookSubscriptions = [sync['webhook-subscriptions'] as string]; } } - const is_public = isPublic !== undefined ? isPublic : sync.is_public === true; - const pre_built = isPublic !== undefined ? isPublic : sync.pre_built === true; const enabled = isEnabled(sync); const syncObject: NangoSyncConfig = { @@ -448,8 +356,8 @@ function buildSyncs({ track_deletes: sync.track_deletes || false, auto_start: sync.auto_start === false ? false : true, last_deployed: sync.updated_at || null, - is_public, - pre_built, + is_public: true, + pre_built: true, version: sync.version || null, attributes: sync.attributes || {}, input: inputModel, @@ -479,30 +387,18 @@ function buildActions({ actions, allModelNames, config, - providerConfigKey, - showMessages, - isPublic, - allModels, - allEndpoints + providerConfigKey }: { actions: NangoV2Integration; allModelNames: string[]; config: NangoConfigV2; providerConfigKey: string; - showMessages: boolean; - isPublic: boolean | null | undefined; - allModels: string[]; - allEndpoints: string[]; }): ServiceResponse { const builtActions: NangoSyncConfig[] = []; for (const actionName in actions) { const action: NangoIntegrationDataV2 = actions[actionName] as NangoIntegrationDataV2; - const { - success: modelSuccess, - error: modelError, - response: models - } = formModelOutput({ integrationData: action, allModels, allModelNames, config, name: actionName, type: 'action' }); + const { success: modelSuccess, error: modelError, response: models } = formModelOutput({ integrationData: action, allModelNames, config }); if (!modelSuccess || !models) { return { success: false, error: modelError, response: null }; @@ -511,14 +407,6 @@ function buildActions({ let inputModel: NangoSyncModel | undefined = undefined; if (action.input) { - if (action.input.includes('{') && action.input.includes('}')) { - // find which model is in between the braces - const modelName = action.input.match(/{([^}]+)}/)?.[1]; - - if (!allModelNames.includes(modelName as string)) { - throw new Error(`Model ${modelName} not found included in models definition`); - } - } const modelFields = getFieldsForModel(action.input, config) as { name: string; type: string }[]; if (modelFields) { inputModel = { @@ -528,41 +416,20 @@ function buildActions({ } } - let endpoints: NangoSyncEndpoint[] = []; - let actionEndpoint: string; + let endpoint: NangoSyncEndpointV2 | undefined; if (action?.endpoint) { - if (Array.isArray(action?.endpoint)) { - if (action?.endpoint?.length > 1) { - const error = new NangoError('action_single_endpoint', actionName); - - return { success: false, error, response: null }; - } - actionEndpoint = action?.endpoint[0] as string; - } else { - actionEndpoint = action?.endpoint; - } - - endpoints = assignEndpoints(actionEndpoint, 'POST', false, showMessages); - if (actionEndpoint?.includes('{') && actionEndpoint.includes('}')) { - const { success, error, response } = parseModelInEndpoint(actionEndpoint, allModelNames, inputModel!, config); + endpoint = parseEndpoint(action.endpoint as string | NangoSyncEndpointV2, 'POST'); + if (endpoint.path?.includes('{') && endpoint.path.includes('}')) { + const { success, error, response } = parseModelInEndpoint(endpoint.path, allModelNames, inputModel!, config); if (!success || !response) { return { success, error, response: null }; } inputModel = response; } - - if (!allEndpoints.includes(actionEndpoint)) { - allEndpoints.push(actionEndpoint); - } else { - const error = new NangoError('duplicate_endpoint', actionEndpoint); - return { success: false, error, response: null }; - } } const scopes = action?.scopes || action?.metadata?.scopes || []; - const is_public = isPublic !== undefined ? isPublic : action.is_public === true; - const pre_built = isPublic !== undefined ? isPublic : action.pre_built === true; const enabled = isEnabled(action); @@ -571,8 +438,8 @@ function buildActions({ type: 'action', models: models || [], runs: '', - is_public, - pre_built, + is_public: true, + pre_built: true, version: action.version || null, last_deployed: action.updated_at || null, attributes: action.attributes || {}, @@ -580,7 +447,7 @@ function buildActions({ description: action?.description || action?.metadata?.description || '', scopes: Array.isArray(scopes) ? scopes : String(scopes)?.split(','), input: inputModel, - endpoints, + endpoints: endpoint ? [endpoint] : [], nango_yaml_version: action.nango_yaml_version || 'v2', enabled, layout_mode: localFileService.getLayoutMode(actionName, providerConfigKey, 'action'), diff --git a/packages/shared/lib/services/proxy.service.ts b/packages/shared/lib/services/proxy.service.ts index fbfb502cc9f..726ccb7b415 100644 --- a/packages/shared/lib/services/proxy.service.ts +++ b/packages/shared/lib/services/proxy.service.ts @@ -6,7 +6,7 @@ import { axiosInstance as axios, SIGNATURE_METHOD } from '@nangohq/utils'; import { backOff } from 'exponential-backoff'; import FormData from 'form-data'; import type { TbaCredentials, ApiKeyCredentials, BasicApiCredentials, TableauCredentials } from '../models/Auth.js'; -import type { HTTP_VERB, ServiceResponse } from '../models/Generic.js'; +import type { HTTP_METHOD, ServiceResponse } from '../models/Generic.js'; import type { ResponseType, ApplicationConstructedProxyConfiguration, UserProvidedProxyConfiguration, InternalProxyConfiguration } from '../models/Proxy.js'; import { interpolateIfNeeded, connectionCopyWithParsedConnectionConfig, mapProxyBaseUrlInterpolationFormat } from '../utils/utils.js'; @@ -185,7 +185,7 @@ class ProxyService { const configBody: ApplicationConstructedProxyConfiguration = { endpoint, - method: method?.toUpperCase() as HTTP_VERB, + method: method?.toUpperCase() as HTTP_METHOD, provider, token: token || '', providerName, @@ -325,7 +325,7 @@ class ProxyService { * @param {Request} req Express request object * @param {Response} res Express response object * @param {NextFuncion} next callback function to pass control to the next middleware function in the pipeline. - * @param {HTTP_VERB} method + * @param {HTTP_METHOD} method * @param {ApplicationConstructedProxyConfiguration} configBody */ private sendToHttpMethod(configBody: ApplicationConstructedProxyConfiguration): Promise { @@ -446,9 +446,8 @@ class ProxyService { /** * Construct Headers - * @param {ApplicationConstructedProxyConfiguration} config */ - public constructHeaders(config: ApplicationConstructedProxyConfiguration, method: HTTP_VERB, url: string): Record { + public constructHeaders(config: ApplicationConstructedProxyConfiguration, method: HTTP_METHOD, url: string): Record { let headers = {}; switch (config.provider.auth_mode) { diff --git a/packages/shared/lib/services/proxy.service.unit.test.ts b/packages/shared/lib/services/proxy.service.unit.test.ts index 8cd19c12b33..d1b2180a520 100644 --- a/packages/shared/lib/services/proxy.service.unit.test.ts +++ b/packages/shared/lib/services/proxy.service.unit.test.ts @@ -1,6 +1,6 @@ import { expect, describe, it } from 'vitest'; import proxyService from './proxy.service.js'; -import type { HTTP_VERB, UserProvidedProxyConfiguration, InternalProxyConfiguration, OAuth2Credentials } from '../models/index.js'; +import type { UserProvidedProxyConfiguration, InternalProxyConfiguration, OAuth2Credentials } from '../models/index.js'; import type { ApplicationConstructedProxyConfiguration } from '../models/Proxy.js'; import type { AxiosError, AxiosResponse, InternalAxiosRequestConfig } from 'axios'; import type { MessageRowInsert } from '@nangohq/types'; @@ -13,7 +13,7 @@ describe('Proxy service Construct Header Tests', () => { providerConfigKey: 'test', connectionId: 'test', token: { apiKey: 'sweet-secret-token' }, - method: 'GET' as HTTP_VERB, + method: 'GET', provider: { auth_mode: 'API_KEY', authorization_url: 'https://api.nangostarter.com', diff --git a/packages/shared/lib/services/sync/config/config.service.ts b/packages/shared/lib/services/sync/config/config.service.ts index e2deef798f9..709b288c810 100644 --- a/packages/shared/lib/services/sync/config/config.service.ts +++ b/packages/shared/lib/services/sync/config/config.service.ts @@ -9,11 +9,11 @@ import type { NangoConnection } from '../../../models/Connection.js'; import type { Config as ProviderConfig } from '../../../models/Provider.js'; import type { NangoConfigV1, StandardNangoConfig, NangoSyncConfig } from '../../../models/NangoConfig.js'; import errorManager, { ErrorSourceEnum } from '../../../utils/error.manager.js'; -import type { DBSyncConfig, SlimSync } from '@nangohq/types'; +import type { DBSyncConfig, NangoSyncEndpointV2, SlimSync } from '@nangohq/types'; const TABLE = dbNamespace + 'sync_configs'; -type ExtendedSyncConfig = SyncConfig & { provider: string; unique_key: string; endpoints_object: { method: string; path: string }[] }; +type ExtendedSyncConfig = SyncConfig & { provider: string; unique_key: string; endpoints_object: NangoSyncEndpointV2[] }; function convertSyncConfigToStandardConfig(syncConfigs: ExtendedSyncConfig[]): StandardNangoConfig[] { const tmp: Record = {}; @@ -46,11 +46,7 @@ function convertSyncConfigToStandardConfig(syncConfigs: ExtendedSyncConfig[]): S version: syncConfig.version as string, is_public: syncConfig.is_public || false, pre_built: syncConfig.pre_built || false, - endpoints: syncConfig.endpoints_object - ? syncConfig.endpoints_object.map((endpoint) => { - return { [endpoint.method]: endpoint.path }; - }) - : [], + endpoints: syncConfig.endpoints_object, input: input as any, nango_yaml_version: 'v2', enabled: syncConfig.enabled, diff --git a/packages/shared/lib/services/sync/config/deploy.service.ts b/packages/shared/lib/services/sync/config/deploy.service.ts index 5fb6256fd73..16dd5b64731 100644 --- a/packages/shared/lib/services/sync/config/deploy.service.ts +++ b/packages/shared/lib/services/sync/config/deploy.service.ts @@ -5,21 +5,31 @@ import { getSyncsByProviderConfigAndSyncName } from '../sync.service.js'; import { getSyncAndActionConfigByParams, increment, getSyncAndActionConfigsBySyncNameAndConfigId } from './config.service.js'; import connectionService from '../../connection.service.js'; import { LogActionEnum } from '../../../models/Telemetry.js'; -import type { HTTP_VERB, ServiceResponse } from '../../../models/Generic.js'; +import type { ServiceResponse } from '../../../models/Generic.js'; import type { SyncModelSchema, SyncConfig, SyncDeploymentResult, SyncConfigResult, SyncEndpoint, SyncType, Sync } from '../../../models/Sync.js'; -import type { DBEnvironment, DBTeam, IncomingFlowConfig, IncomingPreBuiltFlowConfig, NangoModel, PostConnectionScriptByProvider } from '@nangohq/types'; +import type { + DBEnvironment, + DBTeam, + CleanedIncomingFlowConfig, + IncomingPreBuiltFlowConfig, + NangoModel, + PostConnectionScriptByProvider, + NangoSyncEndpointV2, + IncomingFlowConfig, + HTTP_METHOD +} from '@nangohq/types'; import { postConnectionScriptService } from '../post-connection.service.js'; import { NangoError } from '../../../utils/error.js'; import telemetry, { LogTypes } from '../../../utils/telemetry.js'; import { env, Ok } from '@nangohq/utils'; import type { Result } from '@nangohq/utils'; -import { nangoConfigFile } from '../../nango-config.service.js'; import type { LogContext, LogContextGetter } from '@nangohq/logs'; import type { Orchestrator } from '../../../clients/orchestrator.js'; import type { Merge } from 'type-fest'; import type { JSONSchema7 } from 'json-schema'; import type { Config } from '../../../models/Provider.js'; import type { NangoSyncConfig } from '../../../models/NangoConfig.js'; +import { nangoConfigFile } from '@nangohq/nango-yaml'; const TABLE = dbNamespace + 'sync_configs'; const SYNC_TABLE = dbNamespace + 'syncs'; @@ -27,9 +37,29 @@ const ENDPOINT_TABLE = dbNamespace + 'sync_endpoints'; const nameOfType = 'sync/action'; -type FlowParsed = Merge; +type FlowParsed = Merge; type FlowWithoutScript = Omit; +/** + * Transform received incoming flow from the CLI to an internally standard object + */ +export function cleanIncomingFlow(flowConfigs: IncomingFlowConfig[]): CleanedIncomingFlowConfig[] { + const cleaned: CleanedIncomingFlowConfig[] = []; + for (const flow of flowConfigs) { + const parsedEndpoints = flow.endpoints + ? flow.endpoints.map((endpoint) => { + if ('path' in endpoint) { + return endpoint; + } + const entries = Object.entries(endpoint) as [HTTP_METHOD, string][]; + return { method: entries[0]![0], path: entries[0]![1] }; + }) + : []; + cleaned.push({ ...flow, endpoints: parsedEndpoints }); + } + return cleaned; +} + export async function deploy({ environment, account, @@ -43,7 +73,7 @@ export async function deploy({ }: { environment: DBEnvironment; account: DBTeam; - flows: IncomingFlowConfig[]; + flows: CleanedIncomingFlowConfig[]; jsonSchema?: JSONSchema7 | undefined; postConnectionScriptsByProvider: PostConnectionScriptByProvider[]; nangoYamlBody: string; @@ -128,29 +158,14 @@ export async function deploy({ .returning('id'); const endpoints: SyncEndpoint[] = []; - - // TODO: fix this - flowIds.forEach((row, index) => { - const flow = flows[index] as IncomingFlowConfig; - if (flow.endpoints && row.id) { - flow.endpoints.forEach((endpoint, endpointIndex: number) => { - const method = Object.keys(endpoint)[0] as HTTP_VERB; - const path = endpoint[method] as string; - const res: SyncEndpoint = { - sync_config_id: row.id as number, - method, - path, - created_at: new Date(), - updated_at: new Date() - }; - const model = flow.models[endpointIndex] as string; - if (model) { - res.model = model; - } - endpoints.push(res); - }); + for (const [index, row] of flowIds.entries()) { + const flow = flows[index]; + if (!flow) { + continue; } - }); + + endpoints.push(...endpointToSyncEndpoint(flow, row.id!)); + } if (endpoints.length > 0) { await db.knex.from(ENDPOINT_TABLE).insert(endpoints); @@ -234,13 +249,13 @@ export async function upgradePreBuilt({ const { sync_name: name, is_public, type } = syncConfig; const { unique_key: provider_config_key, provider } = config; - const file_location = (await remoteFileService.copy( + const file_location = await remoteFileService.copy( `${provider}/dist`, `${name}-${provider}.js`, `${env}/account/${account.id}/environment/${environment.id}/config/${syncConfig.nango_config_id}/${name}-v${flow.version}.js`, environment.id, `${name}-${provider_config_key}.js` - )) as string; + ); if (!file_location) { await logCtx.error('There was an error uploading the template', { isPublic: is_public, syncName: name, version: flow.version }); @@ -287,9 +302,7 @@ export async function upgradePreBuilt({ // update endpoints if (flow.endpoints) { - flow.endpoints.forEach((endpoint, endpointIndex) => { - const method = Object.keys(endpoint)[0] as HTTP_VERB; - const path = endpoint[method] as string; + flow.endpoints.forEach(({ method, path }, endpointIndex) => { const res: SyncEndpoint = { sync_config_id: newSyncConfigId, method, @@ -297,7 +310,7 @@ export async function upgradePreBuilt({ created_at: now, updated_at: now }; - const model = flowData.models[endpointIndex] as string; + const model = flowData.models[endpointIndex]; if (model) { res.model = model; } @@ -465,21 +478,21 @@ export async function deployPreBuilt({ const version = bumpedVersion || '0.0.1'; const jsFile = typeof config.fileBody === 'string' ? config.fileBody : config.fileBody?.js; - let file_location = ''; + let file_location: string | null = null; if (is_public) { - file_location = (await remoteFileService.copy( + file_location = await remoteFileService.copy( `${config.public_route}/dist`, `${sync_name}-${config.provider}.js`, `${env}/account/${account.id}/environment/${environment.id}/config/${nango_config_id}/${sync_name}-v${version}.js`, environment.id, `${sync_name}-${provider_config_key}.js` - )) as string; + ); } else { - file_location = (await remoteFileService.upload( + file_location = await remoteFileService.upload( jsFile as string, `${env}/account/${account.id}/environment/${environment.id}/config/${nango_config_id}/${sync_name}-v${version}.js`, environment.id - )) as string; + ); } if (!file_location) { @@ -576,27 +589,14 @@ export async function deployPreBuilt({ }); const endpoints: SyncEndpoint[] = []; - syncConfigs.forEach((row, index) => { - const sync = configs[index] as IncomingPreBuiltFlowConfig; - if (sync.endpoints && row.id) { - sync.endpoints.forEach((endpoint, endpointIndex) => { - const method = Object.keys(endpoint)[0] as HTTP_VERB; - const path = endpoint[method] as string; - const res: SyncEndpoint = { - sync_config_id: row.id as number, - method, - path, - created_at: new Date(), - updated_at: new Date() - }; - const model = sync.models[endpointIndex] as string; - if (model) { - res.model = model; - } - endpoints.push(res); - }); + for (const [index, row] of syncConfigs.entries()) { + const flow = configs[index]; + if (!flow) { + continue; } - }); + + endpoints.push(...endpointToSyncEndpoint(flow, row.id!)); + } if (endpoints.length > 0) { await db.knex.from(ENDPOINT_TABLE).insert(endpoints); @@ -891,3 +891,23 @@ function findModelInModelSchema(fields: NangoModel['fields']) { return models; } + +function endpointToSyncEndpoint(flow: Pick, sync_config_id: number) { + const endpoints: SyncEndpoint[] = []; + for (const [endpointIndex, endpoint] of flow.endpoints.entries()) { + const res: SyncEndpoint = { + sync_config_id, + method: endpoint.method, + path: endpoint.path, + created_at: new Date(), + updated_at: new Date() + }; + const model = flow.models[endpointIndex]; + if (model) { + res.model = model; + } + endpoints.push(res); + } + + return endpoints; +} diff --git a/packages/shared/lib/services/sync/config/deploy.service.unit.test.ts b/packages/shared/lib/services/sync/config/deploy.service.unit.test.ts index 392e2bc127f..c78644dbe2f 100644 --- a/packages/shared/lib/services/sync/config/deploy.service.unit.test.ts +++ b/packages/shared/lib/services/sync/config/deploy.service.unit.test.ts @@ -9,7 +9,7 @@ import { mockErrorManagerReport } from '../../../utils/error.manager.mocks.js'; import { logContextGetter } from '@nangohq/logs'; import { Orchestrator } from '../../../clients/orchestrator.js'; import type { OrchestratorClientInterface } from '../../../clients/orchestrator.js'; -import type { DBTeam, IncomingFlowConfig, DBEnvironment } from '@nangohq/types'; +import type { DBTeam, DBEnvironment, CleanedIncomingFlowConfig } from '@nangohq/types'; import type { SyncConfig } from '../../../models/Sync.js'; const orchestratorClientNoop: OrchestratorClientInterface = { @@ -33,7 +33,7 @@ describe('Sync config create', () => { const debug = true; it('Create sync configs correctly', async () => { - const syncs: IncomingFlowConfig[] = []; + const syncs: CleanedIncomingFlowConfig[] = []; const debug = true; vi.spyOn(environmentService, 'getAccountFromEnvironment').mockImplementation(() => { @@ -56,7 +56,7 @@ describe('Sync config create', () => { }); it('Throws a provider not found error', async () => { - const syncs: IncomingFlowConfig[] = [ + const syncs: CleanedIncomingFlowConfig[] = [ { syncName: 'test-sync', type: 'sync', @@ -69,7 +69,11 @@ describe('Sync config create', () => { runs: 'every 6h', version: '1', model_schema: '[{ "name": "model", "fields": [{ "name": "some", "type": "value" }] }]', - track_deletes: true + track_deletes: true, + endpoints: [ + { method: 'GET', path: '/model1' }, + { method: 'GET', path: '/model2' } + ] } ]; @@ -95,7 +99,7 @@ describe('Sync config create', () => { }); it('Throws an error at the end of the create sync process', async () => { - const syncs: IncomingFlowConfig[] = [ + const syncs: CleanedIncomingFlowConfig[] = [ { syncName: 'test-sync', type: 'sync', @@ -108,7 +112,11 @@ describe('Sync config create', () => { runs: 'every 6h', version: '1', model_schema: '[{ "name": "model", "fields": [{ "name": "some", "type": "value" }] }]', - track_deletes: true + track_deletes: true, + endpoints: [ + { method: 'GET', path: '/model1' }, + { method: 'GET', path: '/model2' } + ] } ]; diff --git a/packages/shared/lib/services/sync/config/endpoint.service.ts b/packages/shared/lib/services/sync/config/endpoint.service.ts index e753f27298a..68c6ef992b4 100644 --- a/packages/shared/lib/services/sync/config/endpoint.service.ts +++ b/packages/shared/lib/services/sync/config/endpoint.service.ts @@ -2,7 +2,7 @@ import { schema, dbNamespace } from '@nangohq/database'; import configService from '../../config.service.js'; import type { SyncConfig } from '../../../models/Sync.js'; import type { NangoConnection } from '../../../models/Connection.js'; -import type { HTTP_VERB } from '../../../models/Generic.js'; +import type { HTTP_METHOD } from '../../../models/Generic.js'; const ENDPOINT_TABLE = dbNamespace + 'sync_endpoints'; const SYNC_CONFIG_TABLE = dbNamespace + 'sync_configs'; @@ -12,7 +12,7 @@ interface ActionOrModel { model?: string; } -export async function getActionOrModelByEndpoint(nangoConnection: NangoConnection, method: HTTP_VERB, path: string): Promise { +export async function getActionOrModelByEndpoint(nangoConnection: NangoConnection, method: HTTP_METHOD, path: string): Promise { const config = await configService.getProviderConfig(nangoConnection.provider_config_key, nangoConnection.environment_id); if (!config) { throw new Error('Provider config not found'); diff --git a/packages/types/lib/api.ts b/packages/types/lib/api.ts index 313f4c900b9..71136053acf 100644 --- a/packages/types/lib/api.ts +++ b/packages/types/lib/api.ts @@ -24,7 +24,8 @@ export type ResDefaultErrors = | ApiError<'missing_auth_header'> | ApiError<'malformed_auth_header'> | ApiError<'unknown_account'> - | ApiError<'unknown_connect_session_token'>; + | ApiError<'unknown_connect_session_token'> + | ApiError<'invalid_cli_version'>; export type EndpointMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'; /** diff --git a/packages/types/lib/deploy/incomingFlow.ts b/packages/types/lib/deploy/incomingFlow.ts index d7a2b4cf0bd..e2d6a6e33e3 100644 --- a/packages/types/lib/deploy/incomingFlow.ts +++ b/packages/types/lib/deploy/incomingFlow.ts @@ -1,4 +1,5 @@ -import type { NangoModel, NangoSyncEndpoint, ScriptTypeLiteral, SyncTypeLiteral } from '../nangoYaml'; +import type { Merge } from 'type-fest'; +import type { NangoModel, NangoSyncEndpointOld, NangoSyncEndpointV2, ScriptTypeLiteral, SyncTypeLiteral } from '../nangoYaml'; export interface IncomingScriptFiles { js: string; @@ -38,7 +39,7 @@ interface InternalIncomingPreBuiltFlowConfig { metadata?: NangoConfigMetadata | undefined; model_schema: string | NangoModel[]; input?: string | LegacySyncModelSchema | undefined; - endpoints?: NangoSyncEndpoint[] | undefined; + endpoints?: (NangoSyncEndpointV2 | NangoSyncEndpointOld)[] | undefined; track_deletes: boolean; providerConfigKey: string; } @@ -51,6 +52,7 @@ export interface IncomingPreBuiltFlowConfig extends InternalIncomingPreBuiltFlow syncName?: string; // legacy nango_config_id?: number; fileBody?: IncomingScriptFiles; + endpoints: NangoSyncEndpointV2[]; } export interface IncomingFlowConfig extends InternalIncomingPreBuiltFlowConfig { @@ -60,3 +62,5 @@ export interface IncomingFlowConfig extends InternalIncomingPreBuiltFlowConfig { sync_type?: SyncTypeLiteral | undefined; webhookSubscriptions?: string[] | undefined; } + +export type CleanedIncomingFlowConfig = Merge; diff --git a/packages/types/lib/integration/api.ts b/packages/types/lib/integration/api.ts index ccd02a3a56e..046c59bd84c 100644 --- a/packages/types/lib/integration/api.ts +++ b/packages/types/lib/integration/api.ts @@ -3,7 +3,7 @@ import type { ApiTimestamps, Endpoint } from '../api'; import type { IntegrationConfig } from './db'; import type { Provider } from '../providers/provider'; import type { AuthModeType } from '../auth/api'; -import type { NangoModel, NangoSyncEndpoint, ScriptTypeLiteral } from '../nangoYaml'; +import type { NangoModel, NangoSyncEndpointV2, ScriptTypeLiteral } from '../nangoYaml'; import type { LegacySyncModelSchema, NangoConfigMetadata } from '../deploy/incomingFlow'; import type { JSONSchema7 } from 'json-schema'; import type { SyncType } from '../scripts/syncs/api'; @@ -129,7 +129,7 @@ export interface NangoSyncConfig { track_deletes?: boolean; returns: string[] | string; models: any[]; - endpoints: NangoSyncEndpoint[]; + endpoints: NangoSyncEndpointV2[]; is_public?: boolean | null; pre_built?: boolean | null; version?: string | null; diff --git a/packages/types/lib/nangoYaml/index.ts b/packages/types/lib/nangoYaml/index.ts index 1181f68da0d..b867453f662 100644 --- a/packages/types/lib/nangoYaml/index.ts +++ b/packages/types/lib/nangoYaml/index.ts @@ -1,4 +1,4 @@ -export type HTTP_VERB = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'; +export type HTTP_METHOD = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'; export type SyncTypeLiteral = 'incremental' | 'full'; export type ScriptFileType = 'actions' | 'syncs' | 'post-connection-scripts'; export type ScriptTypeLiteral = 'action' | 'sync'; @@ -34,7 +34,7 @@ export interface NangoYamlV2Integration { 'post-connection-scripts'?: string[]; } export interface NangoYamlV2IntegrationSync { - endpoint: string | string[]; + endpoint: string | string[] | NangoSyncEndpointV2 | NangoSyncEndpointV2[]; output: string | string[]; description?: string; sync_type?: SyncTypeLiteral; @@ -85,7 +85,7 @@ export interface NangoYamlParsedIntegration { export interface ParsedNangoSync { name: string; type: 'sync'; - endpoints: NangoSyncEndpoint[]; + endpoints: NangoSyncEndpointV2[]; description: string; sync_type: SyncTypeLiteral; track_deletes: boolean; @@ -105,7 +105,7 @@ export interface ParsedNangoAction { description: string; input: string | null; output: string[] | null; - endpoint: NangoSyncEndpoint | null; + endpoint: NangoSyncEndpointV2 | null; scopes: string[]; usedModels: string[]; version: string; @@ -129,6 +129,11 @@ export interface NangoModelField { optional?: boolean | undefined; } -export type NangoSyncEndpoint = { - [key in HTTP_VERB]?: string | undefined; +export type NangoSyncEndpointOld = { + [key in HTTP_METHOD]?: string | undefined; }; + +export interface NangoSyncEndpointV2 { + method: HTTP_METHOD; + path: string; +} diff --git a/packages/webapp/src/components/HttpLabel.tsx b/packages/webapp/src/components/HttpLabel.tsx index 71037775772..1751b448c67 100644 --- a/packages/webapp/src/components/HttpLabel.tsx +++ b/packages/webapp/src/components/HttpLabel.tsx @@ -1,7 +1,7 @@ import type { VariantProps } from 'class-variance-authority'; import { cva } from 'class-variance-authority'; import { cn } from '../utils/utils'; -import type { HTTP_VERB } from '@nangohq/types'; +import type { NangoSyncEndpointV2 } from '@nangohq/types'; const styles = cva('', { variants: { @@ -36,7 +36,7 @@ const sizesText = cva('text-[13px]', { } }); -export const HttpLabel: React.FC<{ method: HTTP_VERB; path: string; size?: Sizes }> = ({ method, path, size }) => { +export const HttpLabel: React.FC = ({ method, path, size }) => { return (
diff --git a/packages/webapp/src/pages/Integrations/components/FlowCard.tsx b/packages/webapp/src/pages/Integrations/components/FlowCard.tsx deleted file mode 100644 index ee9e39f55f5..00000000000 --- a/packages/webapp/src/pages/Integrations/components/FlowCard.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { ArrowPathRoundedSquareIcon, BoltIcon } from '@heroicons/react/24/outline'; -import { formatDateToShortUSFormat } from '../../../utils/utils'; -import type { Flow } from '../../../types'; - -export interface FlowProps { - flow: Flow; -} - -export default function FlowCard({ flow }: FlowProps) { - return ( -
-
- {flow.type === 'sync' && } - {flow.type === 'action' && } -
{flow.type === 'sync' ? 'Sync' : 'Action'} Information
-
-
- -
SCRIPT NAME
-
{flow.name}
-
-
- {('version' in flow || 'last_deployed' in flow) && ( -
- {flow.version && ( - -
VERSION
-
{flow.version}
-
- )} - {flow.last_deployed && ( - -
LAST DEPLOYED
-
{formatDateToShortUSFormat(flow.last_deployed)}
-
- )} -
- )} -
- -
SOURCE
-
{'is_public' in flow && flow.is_public ? 'Template' : 'Custom'}
-
- {flow.type === 'sync' && 'sync_type' in flow && ( - -
TYPE
-
{flow.sync_type ?? '-'}
-
- )} -
- {flow.type === 'sync' && ( - <> -
- -
FREQUENCY
-
{flow.runs ?? '-'}
-
- -
TRACK DELETES
-
{flow.track_deletes ? 'Yes' : 'No'}
-
-
-
- {'input' in flow && ( - -
METADATA
-
{Object.keys(flow.input as object).length > 0 ? 'Yes' : 'No'}
-
- )} - -
AUTO STARTS
-
{flow.auto_start === false ? 'No' : 'Yes'}
-
-
- - )} -
- -
ENABLED
-
{'version' in flow && flow.version !== null ? 'Yes' : 'No'}
-
-
-
- ); -} diff --git a/packages/webapp/src/pages/Integrations/providerConfigKey/Endpoints/Show.tsx b/packages/webapp/src/pages/Integrations/providerConfigKey/Endpoints/Show.tsx index abf89c0a99b..4f215e41118 100644 --- a/packages/webapp/src/pages/Integrations/providerConfigKey/Endpoints/Show.tsx +++ b/packages/webapp/src/pages/Integrations/providerConfigKey/Endpoints/Show.tsx @@ -3,7 +3,7 @@ import { Skeleton } from '../../../../components/ui/Skeleton'; import { useGetIntegrationFlows } from '../../../../hooks/useIntegration'; import { useStore } from '../../../../store'; import { useMemo } from 'react'; -import type { GetIntegration, HTTP_VERB, NangoSyncConfig } from '@nangohq/types'; +import type { GetIntegration, NangoSyncConfig } from '@nangohq/types'; import type { FlowGroup, NangoSyncConfigWithEndpoint } from './components/List'; import { EndpointsList } from './components/List'; import { EndpointOne } from './components/One'; @@ -39,14 +39,12 @@ export const EndpointsShow: React.FC<{ integration: GetIntegration['Success']['d const tmp: Record = {}; for (const flow of data.flows) { for (const endpoint of flow.endpoints) { - const entries = Object.entries(endpoint)[0]; - const paths = entries[1].split('/'); - - const path = paths[1]; - if (!path) { + const paths = endpoint.path.split('/'); + const firstPath = paths[1]; + if (!firstPath) { continue; } - const groupName = allowedGroup.includes(path) ? path : 'others'; + const groupName = allowedGroup.includes(firstPath) ? firstPath : 'others'; let group = tmp[groupName]; if (!group) { @@ -54,7 +52,7 @@ export const EndpointsShow: React.FC<{ integration: GetIntegration['Success']['d tmp[groupName] = group; } - group.push({ ...flow, endpoint: { method: entries[0] as HTTP_VERB, path: entries[1] } }); + group.push({ ...flow, endpoint }); } if (flow.endpoints.length <= 0) { @@ -107,10 +105,9 @@ export const EndpointsShow: React.FC<{ integration: GetIntegration['Success']['d } for (const flow of data.flows) { - for (const endpointObj of flow.endpoints) { - const endpoint = Object.entries(endpointObj)[0]; - if (endpoint[0] === method && endpoint[1] === path) { - return { ...flow, endpoint: { method: method as HTTP_VERB, path } }; + for (const endpoint of flow.endpoints) { + if (endpoint.method === method && endpoint.path === path) { + return { ...flow, endpoint }; } } } diff --git a/packages/webapp/src/pages/Integrations/providerConfigKey/Endpoints/components/List.tsx b/packages/webapp/src/pages/Integrations/providerConfigKey/Endpoints/components/List.tsx index e46b996cb37..ca8c43cd18d 100644 --- a/packages/webapp/src/pages/Integrations/providerConfigKey/Endpoints/components/List.tsx +++ b/packages/webapp/src/pages/Integrations/providerConfigKey/Endpoints/components/List.tsx @@ -1,4 +1,4 @@ -import type { GetIntegration, HTTP_VERB, NangoSyncConfig } from '@nangohq/types'; +import type { GetIntegration, NangoSyncConfig, NangoSyncEndpointV2 } from '@nangohq/types'; import * as Table from '../../../../../components/ui/Table'; import { HttpLabel } from '../../../../../components/HttpLabel'; import { QuestionMarkCircledIcon } from '@radix-ui/react-icons'; @@ -11,7 +11,7 @@ import { Info } from '../../../../../components/Info'; import { Tooltip, TooltipContent, TooltipTrigger } from '../../../../../components/ui/Tooltip'; import { Prism } from '@mantine/prism'; -export type NangoSyncConfigWithEndpoint = NangoSyncConfig & { endpoint: { method: HTTP_VERB; path: string } }; +export type NangoSyncConfigWithEndpoint = NangoSyncConfig & { endpoint: NangoSyncEndpointV2 }; export interface FlowGroup { name: string; flows: NangoSyncConfigWithEndpoint[]; @@ -70,7 +70,7 @@ export const EndpointsList: React.FC<{ integration: GetIntegration['Success']['d {flows.map((flow) => { - const usp = new URLSearchParams(flow.endpoint); + const usp = new URLSearchParams(flow.endpoint as unknown as Record); return ( { - const obj = Object.entries(endpoint)[0]; - return obj[0] === flow.endpoint.method && obj[1] === flow.endpoint.path; + return endpoint.method === flow.endpoint.method && endpoint.path === flow.endpoint.path; }); const outputModelName = Array.isArray(flow.returns) ? flow.returns[activeEndpointIndex] : flow.returns; // This code is completely valid but webpack is complaining for some obscure reason diff --git a/packages/webapp/src/pages/InteractiveDemo/ActionBloc.tsx b/packages/webapp/src/pages/InteractiveDemo/ActionBloc.tsx index 55ff7744589..6ed6e455e78 100644 --- a/packages/webapp/src/pages/InteractiveDemo/ActionBloc.tsx +++ b/packages/webapp/src/pages/InteractiveDemo/ActionBloc.tsx @@ -51,7 +51,7 @@ export const ActionBloc: React.FC<{ step: Steps; providerConfigKey: string; conn setSnippet( await httpSnippet({ baseUrl, - endpoint: { POST: endpointAction }, + endpoint: { method: 'POST', path: endpointAction }, secretKey, connectionId, providerConfigKey, diff --git a/packages/webapp/src/pages/InteractiveDemo/FetchBloc.tsx b/packages/webapp/src/pages/InteractiveDemo/FetchBloc.tsx index 2011185d198..0c444570d38 100644 --- a/packages/webapp/src/pages/InteractiveDemo/FetchBloc.tsx +++ b/packages/webapp/src/pages/InteractiveDemo/FetchBloc.tsx @@ -38,7 +38,16 @@ export const FetchBloc: React.FC<{ if (language === Language.Node) { setSnippet(nodeSyncSnippet({ modelName: model, secretKey, connectionId, providerConfigKey })); } else if (language === Language.cURL) { - setSnippet(await httpSnippet({ baseUrl, endpoint: { GET: endpointSync }, secretKey, connectionId, providerConfigKey, language: 'shell' })); + setSnippet( + await httpSnippet({ + baseUrl, + endpoint: { method: 'GET', path: endpointSync }, + secretKey, + connectionId, + providerConfigKey, + language: 'shell' + }) + ); } })(); }, [language, baseUrl, secretKey, connectionId, providerConfigKey]); diff --git a/packages/webapp/src/types.ts b/packages/webapp/src/types.ts index ce93a471efb..a3ab70437bc 100644 --- a/packages/webapp/src/types.ts +++ b/packages/webapp/src/types.ts @@ -1,4 +1,4 @@ -import type { NangoModel, SyncTypeLiteral, AuthModeType, NangoSyncEndpoint, ActiveLog } from '@nangohq/types'; +import type { SyncTypeLiteral, ActiveLog } from '@nangohq/types'; export type SyncResult = Record; @@ -8,31 +8,6 @@ export interface Result { deleted: number; } -export interface Sync { - id: string; - sync_name: string; - type: string; - provider: string; - runs: string; - auto_start: boolean; - unique_key: string; - models: string[]; - updated_at: string; - version: string; - pre_built: boolean; - is_public: boolean; - connections: - | { - connection_id: string; - metadata?: Record>; - }[] - | null; - metadata?: { - description?: string; - scopes?: string[]; - }; -} - export interface SyncResponse { id: string; created_at: string; @@ -72,56 +47,6 @@ export const UserFacingSyncCommand = { CANCEL: 'cancelled' }; -export interface Connection { - id: number; - connection_id: string; - provider: string; - providerConfigKey: string; - creationDate: string; - oauthType: string; - connectionConfig: Record; - connectionMetadata: Record; - accessToken: string | null; - refreshToken: string | null; - expiresAt: string | null; - oauthToken: string | null; - oauthTokenSecret: string | null; - rawCredentials: object; - credentials: BasicApiCredentials | ApiKeyCredentials | OAuthOverride | OAuth2ClientCredentials | null; -} - -export interface BasicApiCredentials { - username: string; - password: string; -} - -export interface ApiKeyCredentials { - apiKey: string; -} - -export interface OAuthOverride { - config_override: { - client_id: string; - client_secret: string; - }; -} - -export interface OAuth2ClientCredentials { - token: string; - access_token: string; - expires_at: string; - client_id: string; - client_secret: string; -} - -export interface FlowEndpoint { - GET?: string; - POST?: string; - PUT?: string; - PATCH?: string; - DELETE?: string; -} - interface NangoSyncModelField { name: string; type: string; @@ -133,48 +58,3 @@ export interface NangoSyncModel { description?: string; fields: NangoSyncModelField[]; } - -export interface Flow { - id?: number; - attributes: Record; - endpoints: NangoSyncEndpoint[]; - scopes: string[]; - enabled: boolean; - sync_type?: 'FULL' | 'INCREMENTAL'; - is_public: boolean; - pre_built: boolean; - version?: string; - upgrade_version?: string; - last_deployed?: string; - input?: NangoSyncModel; - description: string; - name: string; - returns: string | string[]; - output?: string; - type: 'sync' | 'action'; - runs?: string; - track_deletes: boolean; - auto_start?: boolean; - endpoint?: string; - models: NangoSyncModel[] | NangoModel[]; - nango_yaml_version: 'v1' | 'v2'; - webhookSubscriptions: string[]; -} - -export interface IntegrationConfig { - unique_key: string; - provider: string; - client_id: string; - client_secret: string; - app_link?: string; - has_webhook: boolean; - has_webhook_user_defined_secret?: boolean; - scopes: string; - auth_mode: AuthModeType; - created_at: string; - webhook_secret?: string; - custom?: Record; - connection_count: number; - connections: Connection[]; - docs: string; -} diff --git a/packages/webapp/src/utils/language-snippets.tsx b/packages/webapp/src/utils/language-snippets.tsx index 682e9ae3313..2790b002100 100644 --- a/packages/webapp/src/utils/language-snippets.tsx +++ b/packages/webapp/src/utils/language-snippets.tsx @@ -1,4 +1,4 @@ -import type { NangoModel, NangoSyncEndpoint } from '@nangohq/types'; +import type { NangoModel, NangoSyncEndpointV2 } from '@nangohq/types'; import type { TargetId } from 'httpsnippet-lite'; import { HTTPSnippet } from 'httpsnippet-lite'; import type { NangoSyncModel } from '../types'; @@ -73,19 +73,18 @@ export async function httpSnippet({ input }: { baseUrl: string; - endpoint: NangoSyncEndpoint; + endpoint: NangoSyncEndpointV2; secretKey: string; connectionId: string; providerConfigKey: string; language: TargetId; input?: NangoModel | NangoSyncModel | undefined; }) { - const [method, path] = Object.entries(endpoint)[0]; const secretKeyDisplay = isProd() ? maskedKey : secretKey; const snippet = new HTTPSnippet({ - method, - url: `${baseUrl}/v1${path}`, + method: endpoint.method, + url: `${baseUrl}/v1${endpoint.path}`, headers: [ { name: 'Authorization', value: `Bearer ${secretKeyDisplay}` }, { name: 'Content-Type', value: 'application/json' },