diff --git a/packages/server/lib/controllers/config.controller.ts b/packages/server/lib/controllers/config.controller.ts index 6fa3cb85a3..24b738505d 100644 --- a/packages/server/lib/controllers/config.controller.ts +++ b/packages/server/lib/controllers/config.controller.ts @@ -12,7 +12,6 @@ import { isHosted } from '@nangohq/utils'; import type { Template as ProviderTemplate, AuthModeType } from '@nangohq/types'; import { flowService, - getConfigWithEndpointsByProviderConfigKey, errorManager, NangoError, analytics, @@ -22,7 +21,8 @@ import { getUniqueSyncsByProviderConfig, getActionsByProviderConfigKey, getFlowConfigsByParams, - getGlobalWebhookReceiveUrl + getGlobalWebhookReceiveUrl, + getSyncConfigsAsStandardConfig } from '@nangohq/shared'; import { getOrchestrator, parseConnectionConfigParamsFromTemplate } from '../utils/utils.js'; import type { RequestLocals } from '../utils/express.js'; @@ -386,7 +386,7 @@ class ConfigController { if (includeFlows && !isHosted) { const availablePublicFlows = flowService.getAllAvailableFlowsAsStandardConfig(); const [publicFlows] = availablePublicFlows.filter((flow) => flow.providerConfigKey === config.provider); - const allFlows = await getConfigWithEndpointsByProviderConfigKey(environmentId, providerConfigKey); + const allFlows = await getSyncConfigsAsStandardConfig(environmentId, providerConfigKey); if (availablePublicFlows.length && publicFlows && allFlows) { const { disabledFlows, flows } = getEnabledAndDisabledFlows(publicFlows, allFlows); diff --git a/packages/server/lib/controllers/flow.controller.ts b/packages/server/lib/controllers/flow.controller.ts index 7b7cac271f..0e37e9f4de 100644 --- a/packages/server/lib/controllers/flow.controller.ts +++ b/packages/server/lib/controllers/flow.controller.ts @@ -9,13 +9,12 @@ import { deployPreBuilt as deployPreBuiltSyncConfig, syncManager, remoteFileService, - getAllSyncsAndActions, getNangoConfigIdAndLocationFromId, - getConfigWithEndpointsByProviderConfigKeyAndName, getSyncsByConnectionIdsAndEnvironmentIdAndSyncName, enableScriptConfig as enableConfig, disableScriptConfig as disableConfig, - environmentService + environmentService, + getSyncConfigsAsStandardConfig } from '@nangohq/shared'; import { logContextGetter } from '@nangohq/logs'; import type { RequestLocals } from '../utils/express.js'; @@ -179,7 +178,7 @@ class FlowController { try { const environmentId = res.locals['environment'].id; - const nangoConfigs = await getAllSyncsAndActions(environmentId); + const nangoConfigs = await getSyncConfigsAsStandardConfig(environmentId); res.send(nangoConfigs); } catch (e) { @@ -277,7 +276,7 @@ class FlowController { const flow = flowService.getSingleFlowAsStandardConfig(flowName); const provider = await configService.getProviderName(providerConfigKey); - const flowConfig = await getConfigWithEndpointsByProviderConfigKeyAndName(environment.id, providerConfigKey, flowName); + const flowConfig = await getSyncConfigsAsStandardConfig(environment.id, providerConfigKey, flowName); res.send({ flowConfig, unEnabledFlow: flow, provider }); } catch (e) { diff --git a/packages/shared/lib/models/NangoConfig.ts b/packages/shared/lib/models/NangoConfig.ts index be5391e8ac..dd5251bb58 100644 --- a/packages/shared/lib/models/NangoConfig.ts +++ b/packages/shared/lib/models/NangoConfig.ts @@ -6,7 +6,7 @@ export interface NangoIntegrationDataV1 { type?: ScriptTypeLiteral; runs: string; returns: string[]; - input?: string; + input?: string | undefined; track_deletes?: boolean; auto_start?: boolean; attributes?: object; @@ -127,7 +127,7 @@ export interface NangoSyncConfig { id?: number; // v2 additions - input?: NangoSyncModel; + input?: NangoSyncModel | undefined; sync_type?: SyncType; nango_yaml_version?: string; webhookSubscriptions?: string[]; diff --git a/packages/shared/lib/models/Sync.ts b/packages/shared/lib/models/Sync.ts index 090725c64c..dd2eff64ec 100644 --- a/packages/shared/lib/models/Sync.ts +++ b/packages/shared/lib/models/Sync.ts @@ -3,7 +3,7 @@ import type { JSONSchema7 } from 'json-schema'; import type { HTTP_VERB, Timestamps, TimestampsAndDeleted } from './Generic.js'; import type { NangoProps } from '../sdk/sync.js'; import type { NangoIntegrationData } from './NangoConfig.js'; -import type { NangoConfigMetadata, NangoSyncEndpoint, ScriptTypeLiteral } from '@nangohq/types'; +import type { NangoConfigMetadata, NangoModel, NangoSyncEndpoint, ScriptTypeLiteral } from '@nangohq/types'; import type { LogContext } from '@nangohq/logs'; export enum SyncStatus { @@ -89,7 +89,7 @@ export interface SyncConfig extends TimestampsAndDeleted { file_location: string; nango_config_id: number; models: string[]; - model_schema: SyncModelSchema[]; + model_schema: SyncModelSchema[] | NangoModel[]; active: boolean; runs: string; track_deletes: boolean; @@ -100,7 +100,7 @@ export interface SyncConfig extends TimestampsAndDeleted { pre_built?: boolean | null; is_public?: boolean | null; endpoints?: NangoSyncEndpoint[]; - input?: string | SyncModelSchema | undefined; + input?: string | undefined; sync_type?: SyncType | undefined; webhook_subscriptions: string[] | null; enabled: boolean; diff --git a/packages/shared/lib/services/sync/config/config.service.ts b/packages/shared/lib/services/sync/config/config.service.ts index a8c129b9b4..6a6efcfe4b 100644 --- a/packages/shared/lib/services/sync/config/config.service.ts +++ b/packages/shared/lib/services/sync/config/config.service.ts @@ -1,149 +1,68 @@ import semver from 'semver'; import db, { schema, dbNamespace } from '@nangohq/database'; -import { getLogger } from '@nangohq/utils'; import configService from '../../config.service.js'; import remoteFileService from '../../file/remote.service.js'; +import { SyncType } from '../../../models/Sync.js'; +import type { Action, SyncConfigWithProvider, SyncConfig } from '../../../models/Sync.js'; import { LogActionEnum } from '../../../models/Telemetry.js'; -import type { Action, SyncConfigWithProvider, SyncType, SyncConfig } from '../../../models/Sync.js'; -import { convertV2ConfigObject } from '../../nango-config.service.js'; import type { NangoConnection } from '../../../models/Connection.js'; import type { Config as ProviderConfig } from '../../../models/Provider.js'; -import type { - NangoModelV1, - NangoSyncModelField, - NangoSyncModel, - NangoConfigV1, - NangoV2Integration, - StandardNangoConfig, - NangoIntegrationDataV2 -} from '../../../models/NangoConfig.js'; +import type { NangoConfigV1, StandardNangoConfig, NangoSyncConfig } from '../../../models/NangoConfig.js'; import errorManager, { ErrorSourceEnum } from '../../../utils/error.manager.js'; import type { SlimSync } from '@nangohq/types'; -const logger = getLogger('Sync.Config'); - 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: { method: string; path: string }[] }; -const convertSyncConfigToStandardConfig = (syncConfigs: extendedSyncConfig[]): StandardNangoConfig[] => { - const nangoConfig = { - integrations: {} as Record, - models: {} - }; - - let isV1 = false; +function convertSyncConfigToStandardConfig(syncConfigs: ExtendedSyncConfig[]): StandardNangoConfig[] { + const tmp: Record = {}; for (const syncConfig of syncConfigs) { - if (!syncConfig) { - continue; - } - - const uniqueKey = syncConfig.unique_key; - - if (!uniqueKey) { - continue; - } - - if (!nangoConfig['integrations'][uniqueKey]) { - nangoConfig['integrations'][uniqueKey] = { - provider: syncConfig.provider - } as NangoV2Integration; + if (!tmp[syncConfig.provider]) { + tmp[syncConfig.provider] = { actions: [], postConnectionScripts: [], providerConfigKey: syncConfig.provider, syncs: [] }; } - const syncName = syncConfig.sync_name; + const integration = tmp[syncConfig.provider]!; - const endpoint = - !syncConfig.endpoints_object || syncConfig?.endpoints_object?.length === 0 - ? null - : syncConfig.endpoints_object.map((endpoint) => `${endpoint.method} ${endpoint.path}`); - - if (!endpoint || endpoint.length === 0) { - isV1 = true; - } - - const flowObject = { - id: syncConfig.id, + const input = syncConfig.input ? syncConfig.model_schema.find((m) => m.name === syncConfig.input) : undefined; + const flowObject: NangoSyncConfig = { + id: syncConfig.id!, + name: syncConfig.sync_name, runs: syncConfig.runs, type: syncConfig.type, - output: syncConfig.models, returns: syncConfig.models, - description: syncConfig?.metadata?.description || '', + description: syncConfig.metadata?.description || '', track_deletes: syncConfig.track_deletes, auto_start: syncConfig.auto_start, attributes: syncConfig.attributes || {}, - scopes: syncConfig?.metadata?.scopes || [], + scopes: syncConfig.metadata?.scopes || [], version: syncConfig.version as string, - updated_at: syncConfig.updated_at?.toISOString(), - is_public: syncConfig?.is_public, - pre_built: syncConfig?.pre_built, - endpoint: - !syncConfig.endpoints_object || syncConfig?.endpoints_object?.length === 0 - ? null - : syncConfig.endpoints_object.map((endpoint) => `${endpoint.method} ${endpoint.path}`), - input: syncConfig.input, - 'webhook-subscriptions': syncConfig.webhook_subscriptions, - nango_yaml_version: isV1 ? 'v1' : 'v2', - enabled: syncConfig.enabled - } as NangoIntegrationDataV2; + 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 }; + }) + : [], + input: input as any, + nango_yaml_version: 'v2', + enabled: syncConfig.enabled, + layout_mode: 'nested', + models: syncConfig.model_schema as any, + last_deployed: syncConfig.updated_at!.toISOString() + }; if (syncConfig.type === 'sync') { - if (!nangoConfig['integrations'][uniqueKey]!['syncs']) { - nangoConfig['integrations'][uniqueKey]!['syncs'] = {} as Record; - } - flowObject['sync_type'] = syncConfig.sync_type as SyncType; - nangoConfig['integrations'][uniqueKey]!['syncs'] = { - ...nangoConfig['integrations'][uniqueKey]!['syncs'], - [syncName]: flowObject - }; + flowObject.sync_type = syncConfig.sync_type || SyncType.FULL; + integration['syncs'].push(flowObject); } else { - if (!nangoConfig['integrations'][uniqueKey]!['actions']) { - nangoConfig['integrations'][uniqueKey]!['actions'] = {} as Record; - } - nangoConfig['integrations'][uniqueKey]!['actions'] = { - ...nangoConfig['integrations'][uniqueKey]!['actions'], - [syncName]: flowObject - }; + integration['actions'].push(flowObject); } } - const { success, error, response: standardConfig } = convertV2ConfigObject(nangoConfig); - - if (error) { - logger.error(`Error in converting sync config to standard config: ${error}`); - } - - if (!success || !standardConfig) { - return []; - } - - const configWithModels = standardConfig.map((config: StandardNangoConfig) => { - const { providerConfigKey } = config; - for (const sync of [...config.syncs, ...config.actions]) { - const { name } = sync; - const syncObject = syncConfigs.find( - (syncConfig: extendedSyncConfig) => syncConfig.sync_name === name && syncConfig.unique_key === providerConfigKey - ); - - const { model_schema, input } = syncObject as SyncConfig; - - for (const model of model_schema) { - if (Array.isArray(model.fields) && Array.isArray(model.fields[0])) { - model.fields = model.fields.flat(); - } - - if (model.name === input) { - sync.input = model; - } - } - sync.models = model_schema; - } - - return config; - }); - - return configWithModels; -}; + return Object.values(tmp); +} export async function getSyncConfig({ nangoConnection, @@ -189,86 +108,29 @@ export async function getSyncConfig({ const fileLocation = syncConfig.file_location; providerConfig[configSyncName] = { - sync_config_id: syncConfig.id as number, + sync_config_id: syncConfig.id!, runs: syncConfig.runs, type: syncConfig.type, returns: syncConfig.models, - input: syncConfig.input as string, + input: syncConfig.input, track_deletes: syncConfig.track_deletes, auto_start: syncConfig.auto_start, attributes: syncConfig.attributes || {}, fileLocation, - version: syncConfig.version as string, - pre_built: syncConfig.pre_built as boolean, - is_public: syncConfig.is_public as boolean, + version: syncConfig.version || '', + pre_built: syncConfig.pre_built || false, + is_public: syncConfig.is_public || false, metadata: syncConfig.metadata!, enabled: syncConfig.enabled }; nangoConfig.integrations[key] = providerConfig; - - const models: NangoModelV1 = {}; - - syncConfig.model_schema.forEach((model: NangoSyncModel) => { - if (!models[model.name]) { - models[model.name] = {}; - } - model.fields.forEach((field: NangoSyncModelField) => { - models[model.name]![field.name] = field.type; - }); - }); - - nangoConfig.models = models; } } return nangoConfig; } -export async function getAllSyncsAndActions(environment_id: number): Promise { - const syncConfigs = await schema() - .select( - `${TABLE}.sync_name`, - `${TABLE}.runs`, - `${TABLE}.type`, - `${TABLE}.models`, - `${TABLE}.model_schema`, - `${TABLE}.track_deletes`, - `${TABLE}.auto_start`, - `${TABLE}.attributes`, - `${TABLE}.updated_at`, - `${TABLE}.version`, - `${TABLE}.sync_type`, - `${TABLE}.metadata`, - `${TABLE}.input`, - `${TABLE}.enabled`, - '_nango_configs.provider', - '_nango_configs.unique_key', - db.knex.raw( - `( - SELECT json_agg(json_build_object('method', method, 'path', path)) - FROM _nango_sync_endpoints - WHERE _nango_sync_endpoints.sync_config_id = ${TABLE}.id - ) as endpoints_object` - ) - ) - .from(TABLE) - .join('_nango_configs', `${TABLE}.nango_config_id`, '_nango_configs.id') - .where({ - [`${TABLE}.environment_id`]: environment_id, - [`${TABLE}.deleted`]: false, - active: true - }); - - if (!syncConfigs) { - return []; - } - - const standardConfig = convertSyncConfigToStandardConfig(syncConfigs); - - return standardConfig; -} - export async function getSyncConfigsByParams(environment_id: number, providerConfigKey: string, isAction?: boolean): Promise { const config = await configService.getProviderConfig(providerConfigKey, environment_id); @@ -804,27 +666,17 @@ export async function updateFrequency(sync_config_id: number, runs: string): Pro }); } -export async function getConfigWithEndpointsByProviderConfigKey(environment_id: number, provider_config_key: string): Promise { - const syncConfigs = await schema() +export function getSyncConfigsAsStandardConfig(environmentId: number): Promise; +export function getSyncConfigsAsStandardConfig(environmentId: number, providerConfigKey?: string, name?: string): Promise; +export async function getSyncConfigsAsStandardConfig( + environmentId: number, + providerConfigKey?: string, + name?: string +): Promise { + const query = db.knex .from(TABLE) - .select( - `${TABLE}.id`, - `${TABLE}.metadata`, - `${TABLE}.sync_name`, - `${TABLE}.pre_built`, - `${TABLE}.is_public`, - `${TABLE}.updated_at`, - `${TABLE}.version`, - `${TABLE}.runs`, - `${TABLE}.models`, - `${TABLE}.model_schema`, - `${TABLE}.input`, - `${TABLE}.type`, - `${TABLE}.sync_type`, - `${TABLE}.track_deletes`, - `${TABLE}.auto_start`, - `${TABLE}.webhook_subscriptions`, - `${TABLE}.enabled`, + .select( + `${TABLE}.*`, '_nango_configs.unique_key', '_nango_configs.provider', db.knex.raw( @@ -836,94 +688,32 @@ export async function getConfigWithEndpointsByProviderConfigKey(environment_id: ) ) .join('_nango_configs', `${TABLE}.nango_config_id`, '_nango_configs.id') - .leftJoin('_nango_sync_endpoints', `${TABLE}.id`, '_nango_sync_endpoints.sync_config_id') .where({ - '_nango_configs.environment_id': environment_id, - '_nango_configs.unique_key': provider_config_key, + '_nango_configs.environment_id': environmentId, '_nango_configs.deleted': false, [`${TABLE}.deleted`]: false, [`${TABLE}.active`]: true - }); + }) + .orderBy([{ column: 'sync_name', order: 'asc' }]); - if (syncConfigs.length === 0) { - return null; + if (providerConfigKey) { + query.where('_nango_configs.unique_key', providerConfigKey); + } + if (name) { + query.where(`${TABLE}.sync_name`, name); } - const standardConfig = convertSyncConfigToStandardConfig(syncConfigs); - - const [config] = standardConfig; - - return config as StandardNangoConfig; -} - -export async function getConfigWithEndpointsByProviderConfigKeyAndName( - environment_id: number, - provider_config_key: string, - name: string -): Promise { - const syncConfigs = await schema() - .from(TABLE) - .select( - `${TABLE}.id`, - `${TABLE}.metadata`, - `${TABLE}.sync_name`, - `${TABLE}.pre_built`, - `${TABLE}.is_public`, - `${TABLE}.updated_at`, - `${TABLE}.version`, - `${TABLE}.runs`, - `${TABLE}.models`, - `${TABLE}.model_schema`, - `${TABLE}.input`, - `${TABLE}.type`, - `${TABLE}.sync_type`, - `${TABLE}.track_deletes`, - `${TABLE}.auto_start`, - `${TABLE}.webhook_subscriptions`, - '_nango_configs.unique_key', - '_nango_configs.provider', - db.knex.raw( - `( - SELECT json_agg(json_build_object('method', method, 'path', path)) - FROM _nango_sync_endpoints - WHERE _nango_sync_endpoints.sync_config_id = ${TABLE}.id - ) as endpoints_object` - ) - ) - .join('_nango_configs', `${TABLE}.nango_config_id`, '_nango_configs.id') - .join('_nango_sync_endpoints', `${TABLE}.id`, '_nango_sync_endpoints.sync_config_id') - .where({ - '_nango_configs.environment_id': environment_id, - '_nango_configs.unique_key': provider_config_key, - '_nango_configs.deleted': false, - [`${TABLE}.deleted`]: false, - [`${TABLE}.sync_name`]: name, - [`${TABLE}.active`]: true - }); - + const syncConfigs = await query; if (syncConfigs.length === 0) { return null; } const standardConfig = convertSyncConfigToStandardConfig(syncConfigs); - - const [config] = standardConfig; - - return config as StandardNangoConfig; -} - -export async function getAllSyncAndActionNames(environmentId: number): Promise { - const result = await schema().from(TABLE).select(`${TABLE}.sync_name`).where({ - deleted: false, - environment_id: environmentId, - active: true - }); - - if (!result) { - return []; + if (!providerConfigKey) { + return standardConfig; } - return result.map((syncConfig: SyncConfig) => syncConfig.sync_name); + return standardConfig[0]!; } export async function getSyncConfigsByConfigIdForWebhook(environment_id: number, nango_config_id: number): Promise { diff --git a/packages/shared/lib/services/sync/config/deploy.service.ts b/packages/shared/lib/services/sync/config/deploy.service.ts index 9f8a179aef..b8afda77a7 100644 --- a/packages/shared/lib/services/sync/config/deploy.service.ts +++ b/packages/shared/lib/services/sync/config/deploy.service.ts @@ -50,7 +50,7 @@ export async function deploy({ const providers = flows.map((flow) => flow.providerConfigKey); - const idsToMarkAsInvactive: number[] = []; + const idsToMarkAsInactive: number[] = []; let flowsWithVersions: FlowWithVersion[] = flows.map((flow) => { const { fileBody: _fileBody, model_schema, ...rest } = flow; @@ -73,7 +73,7 @@ export async function deploy({ const { success, error, response } = await compileDeployInfo({ flow, flowsWithVersions, - idsToMarkAsInvactive, + idsToMarkAsInactive, insertData, flowReturnData, env, @@ -134,8 +134,8 @@ export async function deploy({ await postConnectionScriptService.update({ environment, account, postConnectionScriptsByProvider }); } - if (idsToMarkAsInvactive.length > 0) { - await schema().from(TABLE).update({ active: false }).whereIn('id', idsToMarkAsInvactive); + if (idsToMarkAsInactive.length > 0) { + await schema().from(TABLE).update({ active: false }).whereIn('id', idsToMarkAsInactive); } await logCtx.info(`Successfully deployed ${flowsWithVersions.length} script${flowsWithVersions.length > 1 ? 's' : ''}`, { @@ -494,7 +494,7 @@ export async function deployPreBuilt( async function compileDeployInfo({ flow, flowsWithVersions, - idsToMarkAsInvactive, + idsToMarkAsInactive, insertData, flowReturnData, env, @@ -506,7 +506,7 @@ async function compileDeployInfo({ }: { flow: IncomingFlowConfig; flowsWithVersions: FlowWithVersion[]; - idsToMarkAsInvactive: number[]; + idsToMarkAsInactive: number[]; insertData: SyncConfig[]; flowReturnData: SyncConfigResult['result']; env: string; @@ -625,7 +625,7 @@ async function compileDeployInfo({ if (oldConfigs.length > 0) { const ids = oldConfigs.map((oldConfig: SyncConfig) => oldConfig.id as number); - idsToMarkAsInvactive.push(...ids); + idsToMarkAsInactive.push(...ids); const lastConfig = oldConfigs[oldConfigs.length - 1]; if (lastConfig) { lastSyncWasEnabled = lastConfig.enabled; @@ -660,7 +660,7 @@ async function compileDeployInfo({ runs, active: true, model_schema: model_schema as unknown as SyncModelSchema[], - input: flow.input || '', + input: (flow.input as string) || '', sync_type: flow.sync_type as SyncType, webhook_subscriptions: flow.webhookSubscriptions || [], enabled: lastSyncWasEnabled && !shouldCap diff --git a/packages/webapp/src/pages/Integration/EndpointReference.tsx b/packages/webapp/src/pages/Integration/EndpointReference.tsx index 3a9444c99e..ea03e22bfa 100644 --- a/packages/webapp/src/pages/Integration/EndpointReference.tsx +++ b/packages/webapp/src/pages/Integration/EndpointReference.tsx @@ -6,11 +6,12 @@ import Button from '../../components/ui/button/Button'; import CopyButton from '../../components/ui/button/CopyButton'; import Info from '../../components/ui/Info'; import EndpointLabel from './components/EndpointLabel'; -import type { NangoSyncEndpoint, IntegrationConfig, FlowEndpoint, Flow } from '../../types'; -import { nodeSnippet, nodeActionSnippet, curlSnippet } from '../../utils/language-snippets'; -import { parseInput, generateResponseModel } from '../../utils/utils'; +import type { IntegrationConfig, FlowEndpoint, Flow } from '../../types'; +import { nodeSyncSnippet, nodeActionSnippet, curlSnippet } from '../../utils/language-snippets'; import { Tabs, SubTabs } from './Show'; import { useStore } from '../../store'; +import { getSyncResponse, modelToString } from '../../utils/scripts'; +import type { NangoModel } from '@nangohq/types'; enum Language { Node = 0, @@ -30,6 +31,7 @@ interface EndpointReferenceProps { setActiveTab: (tab: Tabs) => void; } +const connectionId = ''; export default function EndpointReference(props: EndpointReferenceProps) { const { environment, integration, activeFlow, setSubTab, setActiveTab, activeEndpoint } = props; @@ -38,34 +40,45 @@ export default function EndpointReference(props: EndpointReferenceProps) { const [syncSnippet, setSyncSnippet] = useState(''); const [jsonResponseSnippet, setJsonResponseSnippet] = useState(''); - const connectionId = ''; - const baseUrl = useStore((state) => state.baseUrl); useEffect(() => { - if (activeFlow) { - const model = activeFlow.models.length > 0 ? activeFlow.models : activeFlow.returns[0]; + if (!activeFlow) { + return; + } + + const activeEndpointIndex = activeFlow.endpoints.findIndex((endpoint) => endpoint === activeEndpoint); + const outputModelName = Array.isArray(activeFlow.returns) ? activeFlow.returns[activeEndpointIndex] : activeFlow.returns; + // This code is completely valid but webpack is complaining for some obscure reason + const outputModel = (activeFlow.models as unknown as NangoModel[]).find((m) => m.name === outputModelName); + + if (language === Language.Node) { setSyncSnippet( activeFlow.type === 'sync' - ? nodeSnippet(model, environment.secret_key, connectionId, integration.unique_key) - : nodeActionSnippet(activeFlow.name, environment.secret_key, connectionId, integration.unique_key, parseInput(activeFlow)) + ? nodeSyncSnippet({ + modelName: activeFlow.models[0].name, + secretKey: environment.secret_key, + connectionId, + providerConfigKey: integration.unique_key + }) + : nodeActionSnippet({ + actionName: activeFlow.name, + secretKey: environment.secret_key, + connectionId, + providerConfigKey: integration.unique_key, + input: activeFlow.input + }) ); + } else { + setSyncSnippet(curlSnippet(baseUrl, activeFlow?.endpoints[0], environment.secret_key, connectionId, integration.unique_key, activeFlow.input)); + } - const activeEndpointIndex = activeFlow.endpoints.findIndex((endpoint) => endpoint === activeEndpoint); - const jsonModel = generateResponseModel( - activeFlow.models, - Array.isArray(activeFlow.returns) ? activeFlow.returns[activeEndpointIndex] : activeFlow.returns, - activeFlow.type === 'sync' - ); - if (activeFlow.type === 'sync') { - setJsonResponseSnippet( - JSON.stringify({ records: [{ ...jsonModel }], next_cursor: 'MjAyMy0xMS0xN1QxMTo0NzoxNC40NDcrMDI6MDB8fDAz...' }, null, 2) - ); - } else { - setJsonResponseSnippet(JSON.stringify(jsonModel, null, 2)); - } + if (activeFlow.type === 'sync') { + setJsonResponseSnippet(outputModel ? getSyncResponse(outputModel) : 'no response'); + } else { + setJsonResponseSnippet(outputModel ? modelToString(outputModel) : 'no response'); } - }, [activeFlow, environment, integration.unique_key, activeEndpoint]); + }, [activeFlow, environment, integration.unique_key, activeEndpoint, language]); const routeToFlow = () => { setActiveTab(Tabs.Scripts); @@ -101,25 +114,7 @@ export default function EndpointReference(props: EndpointReferenceProps) { variant={language === Language.Node ? 'active' : 'hover'} className={`cursor-default ${language === Language.Node ? 'pointer-events-none' : 'cursor-pointer'}`} onClick={() => { - if (language !== Language.Node) { - setSyncSnippet( - activeFlow?.type === 'sync' - ? nodeSnippet( - activeFlow && activeFlow.models.length > 0 ? activeFlow.models : activeFlow.returns[0], - environment.secret_key, - connectionId, - integration.unique_key - ) - : nodeActionSnippet( - activeFlow?.name as string, - environment.secret_key, - connectionId, - integration.unique_key, - parseInput(activeFlow as Flow) - ) - ); - setLanguage(Language.Node); - } + setLanguage(Language.Node); }} > Node @@ -129,19 +124,7 @@ export default function EndpointReference(props: EndpointReferenceProps) { variant={language === Language.cURL ? 'active' : 'hover'} className={`cursor-default ${language === Language.cURL ? 'pointer-events-none' : 'cursor-pointer'}`} onClick={() => { - if (language !== Language.cURL) { - setSyncSnippet( - curlSnippet( - baseUrl, - activeFlow?.endpoints[0] as NangoSyncEndpoint, - environment.secret_key, - connectionId, - integration.unique_key, - parseInput(activeFlow as Flow) - ) - ); - setLanguage(Language.cURL); - } + setLanguage(Language.cURL); }} > cURL @@ -226,27 +209,6 @@ export default function EndpointReference(props: EndpointReferenceProps) { type="button" variant="active" className={`cursor-default ${language === Language.Node ? 'pointer-events-none' : 'cursor-pointer'}`} - onClick={() => { - if (language !== Language.Node) { - setSyncSnippet( - activeFlow?.type === 'sync' - ? nodeSnippet( - activeFlow.models || activeFlow.returns[0], - environment.secret_key, - connectionId, - integration.unique_key - ) - : nodeActionSnippet( - activeFlow?.name as string, - environment.secret_key, - connectionId, - integration.unique_key, - parseInput(activeFlow as Flow) - ) - ); - setLanguage(Language.Node); - } - }} > JSON diff --git a/packages/webapp/src/pages/Integration/FlowPage.tsx b/packages/webapp/src/pages/Integration/FlowPage.tsx index 74ff9c1dd2..d78eb942b9 100644 --- a/packages/webapp/src/pages/Integration/FlowPage.tsx +++ b/packages/webapp/src/pages/Integration/FlowPage.tsx @@ -16,10 +16,12 @@ import type { IntegrationConfig, Flow, Connection } from '../../types'; import EndpointLabel from './components/EndpointLabel'; import ActionModal from '../../components/ui/ActionModal'; import Info from '../../components/ui/Info'; -import { parseInput, generateResponseModel, formatDateToShortUSFormat } from '../../utils/utils'; +import { formatDateToShortUSFormat } from '../../utils/utils'; import EnableDisableSync from './components/EnableDisableSync'; -import { autoStartSnippet, setMetadaSnippet } from '../../utils/language-snippets'; +import { autoStartSnippet, setMetadataSnippet } from '../../utils/language-snippets'; import { useStore } from '../../store'; +import { getSyncResponse } from '../../utils/scripts'; +import type { NangoModel } from '@nangohq/types'; interface FlowPageProps { environment: EnvironmentAndAccount['environment']; @@ -405,7 +407,7 @@ export default function FlowPage(props: FlowPageProps) { {showMetadataCode && ( -
+
- )} + text={setMetadataSnippet(environment.secret_key, integration.unique_key, flow.input)} />
- {setMetadaSnippet( - environment.secret_key, - integration.unique_key, - parseInput(flow) as Record - )} + {setMetadataSnippet(environment.secret_key, integration.unique_key, flow.input)}
)} @@ -468,7 +462,7 @@ export default function FlowPage(props: FlowPageProps) {
{showAutoStartCode && ( -
+
-
- -
- - {JSON.stringify( - { - records: [generateResponseModel(flow.models, model, true)], - next_cursor: 'MjAyMy0xMS0xN1QxMTo0NzoxNC40NDcrMDI6MDB8fDAz...' - }, - null, - 2 - )} - -
+ {flow?.type === 'sync' && flow.returns && ( +
+
+ Output Models + {(Array.isArray(flow.returns) ? flow.returns : [flow.returns]).map((model: string, index: number) => ( +
+ {model} +
+
+
+
- ) - )} +
+ + {getSyncResponse((flow.models as unknown as NangoModel[]).find((m) => m.name === model)!)} + +
-
- )} - + ))} +
+
)} diff --git a/packages/webapp/src/pages/InteractiveDemo/ActionBloc.tsx b/packages/webapp/src/pages/InteractiveDemo/ActionBloc.tsx index 853660a4c8..b33dd10c03 100644 --- a/packages/webapp/src/pages/InteractiveDemo/ActionBloc.tsx +++ b/packages/webapp/src/pages/InteractiveDemo/ActionBloc.tsx @@ -11,6 +11,7 @@ import { curlSnippet, nodeActionSnippet } from '../../utils/language-snippets'; import { useStore } from '../../store'; import { useMeta } from '../../hooks/useMeta'; import { apiFetch } from '../../utils/api'; +import type { NangoModel } from '@nangohq/types'; export const ActionBloc: React.FC<{ step: Steps; providerConfigKey: string; connectionId: string; secretKey: string; onProgress: () => void }> = ({ step, @@ -31,10 +32,17 @@ export const ActionBloc: React.FC<{ step: Steps; providerConfigKey: string; conn const baseUrl = useStore((state) => state.baseUrl); const snippet = useMemo(() => { + const model: NangoModel = { name: 'CreateIssue', fields: [{ name: 'title', value: title }] }; if (language === Language.Node) { - return nodeActionSnippet(actionName, secretKey, connectionId, providerConfigKey, { title }, true); + return nodeActionSnippet({ + actionName, + secretKey, + connectionId, + providerConfigKey, + input: model + }); } else { - return curlSnippet(baseUrl, endpointAction, secretKey, connectionId, providerConfigKey, `{ title: ${JSON.stringify(title)} }`, 'POST'); + return curlSnippet(baseUrl, endpointAction, secretKey, connectionId, providerConfigKey, model, 'POST'); } }, [title, providerConfigKey, connectionId, secretKey, language, baseUrl]); @@ -100,7 +108,7 @@ export const ActionBloc: React.FC<{ step: Steps; providerConfigKey: string; conn )}
-
+
-
+
(() => { if (language === Language.Node) { - return nodeSnippet(model, secretKey, connectionId, providerConfigKey); + return nodeSyncSnippet({ modelName: model, secretKey, connectionId, providerConfigKey }); } else if (language === Language.cURL) { return curlSnippet(baseUrl, endpointSync, secretKey, connectionId, providerConfigKey); } @@ -155,7 +155,7 @@ export const FetchBloc: React.FC<{ >
-
+
; @@ -200,7 +200,7 @@ export interface Flow { track_deletes: boolean; auto_start?: boolean; endpoint?: string; - models: NangoSyncModel[]; + models: NangoSyncModel[] | NangoModel[]; nango_yaml_version: 'v1' | 'v2'; webhookSubscriptions: string[]; } diff --git a/packages/webapp/src/utils/language-snippets.tsx b/packages/webapp/src/utils/language-snippets.tsx index 988405a346..e74fe5aa20 100644 --- a/packages/webapp/src/utils/language-snippets.tsx +++ b/packages/webapp/src/utils/language-snippets.tsx @@ -1,10 +1,21 @@ +import type { NangoModel } from '@nangohq/types'; import type { NangoSyncEndpoint, NangoSyncModel, HTTP_VERB } from '../types'; import { isProd } from './utils'; +import { legacyModelToObject, modelToString } from './scripts'; const maskedKey = ''; -export const nodeSnippet = (models: string | NangoSyncModel[] | undefined, secretKey: string, connectionId: string, providerConfigKey: string) => { - const model = Array.isArray(models) ? models[0]?.name : models; +export function nodeSyncSnippet({ + modelName, + secretKey, + connectionId, + providerConfigKey +}: { + modelName: string; + secretKey: string; + connectionId: string; + providerConfigKey: string; +}) { const secretKeyDisplay = isProd() ? maskedKey : secretKey; return `import { Nango } from '@nangohq/node'; @@ -13,36 +24,25 @@ const nango = new Nango({ secretKey: '${secretKeyDisplay}' }); const records = await nango.listRecords({ providerConfigKey: '${providerConfigKey}', connectionId: '${connectionId}', - model: '${model}' + model: '${modelName}' }); `; -}; - -export const nodeActionSnippet = ( - actionName: string, - secretKey: string, - connectionId: string, - providerConfigKey: string, - input?: Record | string, - safeInput?: boolean -) => { - let formattedInput = ''; - if (!safeInput) { - if (typeof input === 'string') { - formattedInput = `'<${input}>'`; - } else if (input && typeof input === 'object') { - formattedInput = `{ -${JSON.stringify(input, null, 2) - .split('\n') - .slice(1) - .join('\n') - .replace(/^/gm, ' ') - .replace(/: "([^"]*)"/g, ': "<$1>"')}`; - } - } else { - formattedInput = `{ -${JSON.stringify(input, null, 2).split('\n').slice(1).join('\n').replace(/^/gm, ' ')}`; - } +} + +export function nodeActionSnippet({ + actionName, + secretKey, + connectionId, + providerConfigKey, + input +}: { + actionName: string; + secretKey: string; + connectionId: string; + providerConfigKey: string; + input?: NangoModel | NangoSyncModel; +}) { + const formattedInput = input ? modelToString(input) : ''; const secretKeyDisplay = isProd() ? maskedKey : secretKey; @@ -53,20 +53,20 @@ const response = await nango.triggerAction( '${providerConfigKey}', '${connectionId}', '${actionName}', - ${formattedInput} +${formattedInput} ); `; -}; +} -export const curlSnippet = ( +export function curlSnippet( baseUrl: string, endpoint: string | NangoSyncEndpoint | NangoSyncEndpoint[], secretKey: string, connectionId: string, providerConfigKey: string, - input?: Record | string, + input?: NangoModel | NangoSyncModel, method = 'GET' -) => { +) { let curlMethod: HTTP_VERB = method as HTTP_VERB; const secretKeyDisplay = isProd() ? maskedKey : secretKey; if (typeof endpoint !== 'string') { @@ -74,18 +74,7 @@ export const curlSnippet = ( endpoint = (Array.isArray(endpoint) ? endpoint[0][curlMethod] : endpoint[curlMethod]) as string; } - let formattedInput = ''; - if (typeof input === 'string' && input !== 'undefined') { - formattedInput = input; - } else if (input && typeof input === 'object') { - formattedInput = `{ -${JSON.stringify(input, null, 2) - .split('\n') - .slice(1) - .join('\n') - .replace(/^/gm, ' ') - .replace(/: "([^"]*)"/g, ': "<$1>"')}`; - } + const formattedInput = input ? modelToString(input) : ''; return ` curl --request ${curlMethod} \\ @@ -96,7 +85,7 @@ ${JSON.stringify(input, null, 2) --header 'Provider-Config-Key: ${providerConfigKey}' ${formattedInput ? '\\' : ''} ${formattedInput ? `--data '${formattedInput}'` : ''} `; -}; +} export const autoStartSnippet = (secretKey: string, provider: string, sync: string) => { const secretKeyDisplay = isProd() ? maskedKey : secretKey; @@ -108,7 +97,7 @@ await nango.startSync('${provider}', ['${sync}'], ''); `; }; -export const setMetadaSnippet = (secretKey: string, provider: string, input: Record) => { +export const setMetadataSnippet = (secretKey: string, provider: string, input?: NangoSyncModel) => { return `import Nango from '@nangohq/node'; const nango = new Nango({ secretKey: '${secretKey}' }); @@ -116,7 +105,7 @@ const nango = new Nango({ secretKey: '${secretKey}' }); await nango.setMetadata( '${provider}', '', - ${input ? `{\n${JSON.stringify(input, null, 2).split('\n').slice(1).join('\n').replace(/^/gm, ' ')}` : ''} + ${input ? `{\n${JSON.stringify(legacyModelToObject(input), null, 2).split('\n').slice(1).join('\n').replace(/^/gm, ' ')}` : ''} ); `; }; diff --git a/packages/webapp/src/utils/scripts.ts b/packages/webapp/src/utils/scripts.ts new file mode 100644 index 0000000000..1ded7dd4ee --- /dev/null +++ b/packages/webapp/src/utils/scripts.ts @@ -0,0 +1,108 @@ +import type { NangoModel, NangoModelField } from '@nangohq/types'; +import type { NangoSyncModel } from '../types'; + +export function isNewModel(model: NangoSyncModel | NangoModel): model is NangoModel { + return 'fields' in model && model.fields.length > 0 && 'value' in model.fields[0]; +} + +export function getSyncResponse(model: NangoSyncModel | NangoModel) { + let record = ''; + if (isNewModel(model)) { + record = fieldsToTypescript({ fields: model.fields }).join('\n '); + } else { + const tmp = JSON.stringify(legacyModelToObject(model), null, 2).slice(2); + record = tmp.substring(0, tmp.length - 2); + } + return `{ + "records": [ + { + ${record} + "_nango_metadata": { + "deleted_at": "", + "last_action": "ADDED | UPDATED | DELETED", + "first_seen_at": "", + "cursor": "", + "last_modified_at": "" + } + } + ], + "next_cursor": "MjAyMy0xMS0xN1QxMTo0NzoxNC40NDcrMDI6MDB8fDAz..." +}`; +} + +export function legacyModelToObject(model: NangoSyncModel) { + const obj: Record = {}; + for (const field of model.fields) { + obj[field.name] = field.type; + } + return obj; +} + +export function modelToString(model: NangoSyncModel | NangoModel) { + if (isNewModel(model)) { + return ` { +${fieldsToTypescript({ fields: model.fields }).join('\n').replace(/^/gm, ' ')} + }`; + } else { + return JSON.stringify(legacyModelToObject(model), null, 2).split('\n').join('\n').replace(/^/gm, ' '); + } +} + +/** + * Initial logic from from model.service.ts + */ + +const regQuote = /^[a-zA-Z0-9_]+$/; +export function shouldQuote(name: string) { + return !regQuote.test(name); +} + +export function fieldsToTypescript({ fields }: { fields: NangoModelField[] }) { + const output: string[] = []; + const dynamic = fields.find((field) => field.dynamic); + + // Insert dynamic key at the beginning + if (dynamic) { + output.push(` [key: string]: ${fieldToTypescript({ field: dynamic })};`); + } + + // Regular fields + for (const field of fields) { + if (field.dynamic) { + continue; + } + if (Array.isArray(field.value) && !field.union && !field.array) { + output.push(` "${field.name}"${field.optional ? '?' : ''}: ${fieldToTypescript({ field: field })},`); + } else { + output.push(` "${field.name}"${field.optional ? '?' : ''}: "<${fieldToTypescript({ field: field })}>",`); + } + } + + return output; +} + +/** + * Transform a field definition to its typescript equivalent + */ +export function fieldToTypescript({ field }: { field: NangoModelField }): string | boolean | null | undefined | number { + if (Array.isArray(field.value)) { + if (field.union) { + return field.value.map((f) => fieldToTypescript({ field: f })).join(' | '); + } + if (field.array) { + return `(${field.value.map((f) => fieldToTypescript({ field: f })).join(' | ')})[]`; + } + + return `{${fieldsToTypescript({ fields: field.value }).join('\n')} }`; + } + if (field.model || field.tsType) { + return `${field.value}${field.array ? '[]' : ''}`; + } + if (field.value === null) { + return 'null'; + } + if (typeof field.value === 'string') { + return `'${field.value}${field.array ? '[]' : ''}'`; + } + return `${field.value}${field.array ? '[]' : ''}`; +} diff --git a/packages/webapp/src/utils/utils.tsx b/packages/webapp/src/utils/utils.tsx index 3d29ba7c1b..f2e54d7ab8 100644 --- a/packages/webapp/src/utils/utils.tsx +++ b/packages/webapp/src/utils/utils.tsx @@ -2,7 +2,7 @@ import { clsx } from 'clsx'; import type { ClassValue } from 'clsx'; import { format } from 'date-fns'; import { twMerge } from 'tailwind-merge'; -import type { Flow, SyncResult, NangoSyncModel } from '../types'; +import type { SyncResult } from '../types'; export const localhostUrl: string = 'http://localhost:3003'; export const stagingUrl: string = 'https://api-staging.nango.dev'; @@ -200,64 +200,6 @@ export function getRunTime(created_at: string, updated_at: string): string { return runtime.trim() || '-'; } -export function createExampleForType(type: string): any { - if (typeof type !== 'string') { - return {}; - } - - return `<${type}>`; -} - -export function generateExampleValueForProperty(model: NangoSyncModel): Record { - if (!Array.isArray(model?.fields)) { - return createExampleForType(model?.name); - } - const example = {} as Record; - for (const field of model.fields) { - example[field.name] = createExampleForType(field.type); - } - return example; -} - -export const parseInput = (flow: Flow) => { - let input; - - if (flow?.input && Object.keys(flow?.input).length > 0 && !flow.input.fields) { - input = flow.input.name; - } else if (flow?.input && Object.keys(flow?.input).length > 0) { - const rawInput = {} as Record; - for (const field of flow.input.fields) { - rawInput[field.name] = field.type; - } - input = rawInput; - } else { - input = undefined; - } - - return input; -}; - -export function generateResponseModel(models: NangoSyncModel[], output: string | undefined, isSync: boolean): Record { - if (!output) { - return {}; - } - const model = models.find((model) => model.name === output); - const jsonResponse = generateExampleValueForProperty(model as NangoSyncModel); - if (!isSync) { - return model?.name?.includes('[]') ? [jsonResponse] : jsonResponse; - } - const metadata = { - _nango_metadata: { - deleted_at: '', - last_action: 'ADDED|UPDATED|DELETED', - first_seen_at: '', - cursor: '', - last_modified_at: '' - } - }; - return { ...jsonResponse, ...metadata }; -} - export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }