first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,22 @@
|
||||
# @n8n/rest-api-client
|
||||
|
||||
This package contains the REST API calls for n8n.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Features](#features)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
|
||||
## Features
|
||||
|
||||
- Provides a REST API for n8n
|
||||
- Supports authentication and authorization
|
||||
|
||||
## Contributing
|
||||
|
||||
For more details, please read our [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
For more details, please read our [LICENSE.md](LICENSE.md).
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "../../../../node_modules/@biomejs/biome/configuration_schema.json",
|
||||
"extends": ["../../../../biome.jsonc"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { frontendConfig } from '@n8n/eslint-config/frontend';
|
||||
|
||||
export default defineConfig(frontendConfig, {
|
||||
rules: {
|
||||
// TODO: Remove these
|
||||
'@typescript-eslint/naming-convention': 'warn',
|
||||
'@typescript-eslint/no-empty-object-type': 'warn',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'warn',
|
||||
'@typescript-eslint/no-unsafe-return': 'warn',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'warn',
|
||||
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "@n8n/rest-api-client",
|
||||
"type": "module",
|
||||
"version": "2.11.0",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.mjs",
|
||||
"types": "./dist/index.d.mts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.mts",
|
||||
"import": "./dist/index.mjs",
|
||||
"require": "./dist/index.cjs"
|
||||
},
|
||||
"./*": {
|
||||
"types": "./dist/*.d.mts",
|
||||
"import": "./dist/*.mjs",
|
||||
"require": "./dist/*.cjs"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "tsdown --watch",
|
||||
"build": "tsdown",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"test": "vitest run",
|
||||
"test:dev": "vitest --silent=false",
|
||||
"lint": "eslint src --quiet",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"format": "biome format --write . && prettier --write . --ignore-path ../../../../.prettierignore",
|
||||
"format:check": "biome ci . && prettier --check . --ignore-path ../../../../.prettierignore"
|
||||
},
|
||||
"dependencies": {
|
||||
"@n8n/api-types": "workspace:*",
|
||||
"@n8n/constants": "workspace:*",
|
||||
"@n8n/permissions": "workspace:*",
|
||||
"@n8n/utils": "workspace:*",
|
||||
"js-base64": "catalog:",
|
||||
"n8n-workflow": "workspace:*",
|
||||
"axios": "catalog:",
|
||||
"flatted": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@n8n/eslint-config": "workspace:*",
|
||||
"@n8n/i18n": "workspace:*",
|
||||
"@n8n/typescript-config": "workspace:*",
|
||||
"@n8n/vitest-config": "workspace:*",
|
||||
"@testing-library/jest-dom": "catalog:frontend",
|
||||
"@testing-library/user-event": "catalog:frontend",
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"vite": "catalog:",
|
||||
"vitest": "catalog:"
|
||||
},
|
||||
"license": "See LICENSE.md file in the root of the repository"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { AiUsageSettingsRequestDto } from '@n8n/api-types';
|
||||
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
export async function updateAiUsageSettings(
|
||||
context: IRestApiContext,
|
||||
data: AiUsageSettingsRequestDto,
|
||||
): Promise<void> {
|
||||
return await makeRestApiRequest(context, 'POST', '/ai/usage-settings', data);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import type {
|
||||
CreateApiKeyRequestDto,
|
||||
UpdateApiKeyRequestDto,
|
||||
ApiKey,
|
||||
ApiKeyWithRawValue,
|
||||
} from '@n8n/api-types';
|
||||
import type { ApiKeyScope } from '@n8n/permissions';
|
||||
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
export async function getApiKeys(context: IRestApiContext): Promise<ApiKey[]> {
|
||||
return await makeRestApiRequest(context, 'GET', '/api-keys');
|
||||
}
|
||||
|
||||
export async function getApiKeyScopes(context: IRestApiContext): Promise<ApiKeyScope[]> {
|
||||
return await makeRestApiRequest(context, 'GET', '/api-keys/scopes');
|
||||
}
|
||||
|
||||
export async function createApiKey(
|
||||
context: IRestApiContext,
|
||||
payload: CreateApiKeyRequestDto,
|
||||
): Promise<ApiKeyWithRawValue> {
|
||||
return await makeRestApiRequest(context, 'POST', '/api-keys', payload);
|
||||
}
|
||||
|
||||
export async function deleteApiKey(
|
||||
context: IRestApiContext,
|
||||
id: string,
|
||||
): Promise<{ success: boolean }> {
|
||||
return await makeRestApiRequest(context, 'DELETE', `/api-keys/${id}`);
|
||||
}
|
||||
|
||||
export async function updateApiKey(
|
||||
context: IRestApiContext,
|
||||
id: string,
|
||||
payload: UpdateApiKeyRequestDto,
|
||||
): Promise<{ success: boolean }> {
|
||||
return await makeRestApiRequest(context, 'PATCH', `/api-keys/${id}`, payload);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type {
|
||||
BreakingChangeLightReportResult,
|
||||
BreakingChangeWorkflowRuleResult,
|
||||
BreakingChangeVersion,
|
||||
} from '@n8n/api-types';
|
||||
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest, get } from '../utils';
|
||||
|
||||
type BreakingChangeQuery = {
|
||||
version?: BreakingChangeVersion;
|
||||
};
|
||||
|
||||
export async function getReport(
|
||||
context: IRestApiContext,
|
||||
query?: BreakingChangeQuery,
|
||||
): Promise<BreakingChangeLightReportResult> {
|
||||
return (await get(context.baseUrl, '/breaking-changes/report', query)).data;
|
||||
}
|
||||
|
||||
export async function refreshReport(
|
||||
context: IRestApiContext,
|
||||
query?: BreakingChangeQuery,
|
||||
): Promise<BreakingChangeLightReportResult> {
|
||||
const refreshUrl = query?.version
|
||||
? `/breaking-changes/report/refresh?version=${query.version}`
|
||||
: '/breaking-changes/report/refresh';
|
||||
|
||||
return await makeRestApiRequest(context, 'POST', refreshUrl);
|
||||
}
|
||||
|
||||
export async function getReportForRule(
|
||||
context: IRestApiContext,
|
||||
ruleId: string,
|
||||
): Promise<BreakingChangeWorkflowRuleResult> {
|
||||
return (await get(context.baseUrl, `/breaking-changes/report/${ruleId}`)).data;
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { get, post } from '../utils';
|
||||
|
||||
export declare namespace Cloud {
|
||||
export interface PlanData {
|
||||
planId: number;
|
||||
monthlyExecutionsLimit: number;
|
||||
activeWorkflowsLimit: number;
|
||||
credentialsLimit: number;
|
||||
isActive: boolean;
|
||||
displayName: string;
|
||||
expirationDate: string;
|
||||
metadata: PlanMetadata;
|
||||
userIsTrialing?: boolean;
|
||||
bannerConfig?: BannerConfig;
|
||||
}
|
||||
|
||||
export interface PlanMetadata {
|
||||
version: 'v1';
|
||||
group: 'opt-out' | 'opt-in' | 'trial';
|
||||
slug: 'pro-1' | 'pro-2' | 'starter' | 'trial-1';
|
||||
trial?: Trial;
|
||||
}
|
||||
|
||||
interface Trial {
|
||||
length: number;
|
||||
gracePeriod: number;
|
||||
}
|
||||
|
||||
export interface BannerConfig {
|
||||
// If set, show time left section
|
||||
// - If text provided, use it
|
||||
// - If text not provided, compute from expirationDate
|
||||
timeLeft?: {
|
||||
text?: string;
|
||||
};
|
||||
|
||||
// If true, show executions section with progress bar and x/y text
|
||||
showExecutions?: boolean;
|
||||
|
||||
// CTA button configuration
|
||||
cta?: {
|
||||
text?: string;
|
||||
icon?: string;
|
||||
size?: 'small' | 'medium';
|
||||
/** @deprecated Use variant instead */
|
||||
style?: 'primary' | 'success' | 'warning' | 'danger';
|
||||
variant?: 'solid' | 'subtle' | 'ghost' | 'outline' | 'destructive' | 'success';
|
||||
href?: string; // If provided, navigate to this URL; otherwise use upgrade flow
|
||||
};
|
||||
|
||||
// Banner icon (left side) - if not set, no icon shown
|
||||
icon?: string;
|
||||
|
||||
dismissible?: boolean;
|
||||
forceShow?: boolean; // Override localStorage dismissal
|
||||
}
|
||||
|
||||
export type UserAccount = {
|
||||
confirmed: boolean;
|
||||
username: string;
|
||||
email: string;
|
||||
hasEarlyAccess?: boolean;
|
||||
role?: string;
|
||||
selectedApps?: string[];
|
||||
information?: {
|
||||
[key: string]: string | string[];
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface InstanceUsage {
|
||||
timeframe?: string;
|
||||
executions: number;
|
||||
activeWorkflows: number;
|
||||
}
|
||||
|
||||
export async function getCurrentPlan(context: IRestApiContext): Promise<Cloud.PlanData> {
|
||||
return await get(context.baseUrl, '/admin/cloud-plan');
|
||||
}
|
||||
|
||||
export async function getCurrentUsage(context: IRestApiContext): Promise<InstanceUsage> {
|
||||
return await get(context.baseUrl, '/cloud/limits');
|
||||
}
|
||||
|
||||
export async function getCloudUserInfo(context: IRestApiContext): Promise<Cloud.UserAccount> {
|
||||
return await get(context.baseUrl, '/cloud/proxy/user/me');
|
||||
}
|
||||
|
||||
export async function sendConfirmationEmail(context: IRestApiContext): Promise<Cloud.UserAccount> {
|
||||
return await post(context.baseUrl, '/cloud/proxy/user/resend-confirmation-email');
|
||||
}
|
||||
|
||||
export async function getAdminPanelLoginCode(context: IRestApiContext): Promise<{ code: string }> {
|
||||
return await get(context.baseUrl, '/cloud/proxy/login/code');
|
||||
}
|
||||
|
||||
export interface DynamicNotification {
|
||||
title?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export async function sendUserEvent(
|
||||
context: IRestApiContext,
|
||||
eventData: { eventType: string; metadata?: Record<string, unknown> },
|
||||
): Promise<DynamicNotification> {
|
||||
return await post(context.baseUrl, '/cloud/proxy/user/event', eventData);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { NPM_COMMUNITY_NODE_SEARCH_API_URL } from '@n8n/constants';
|
||||
import type { PublicInstalledPackage } from 'n8n-workflow';
|
||||
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { get, post, makeRestApiRequest } from '../utils';
|
||||
|
||||
export async function getInstalledCommunityNodes(
|
||||
context: IRestApiContext,
|
||||
): Promise<PublicInstalledPackage[]> {
|
||||
const response = await get(context.baseUrl, '/community-packages');
|
||||
return response.data || [];
|
||||
}
|
||||
|
||||
export async function installNewPackage(
|
||||
context: IRestApiContext,
|
||||
name: string,
|
||||
verify?: boolean,
|
||||
version?: string,
|
||||
): Promise<PublicInstalledPackage> {
|
||||
return await post(context.baseUrl, '/community-packages', { name, verify, version });
|
||||
}
|
||||
|
||||
export async function uninstallPackage(context: IRestApiContext, name: string): Promise<void> {
|
||||
return await makeRestApiRequest(context, 'DELETE', '/community-packages', { name });
|
||||
}
|
||||
|
||||
export async function updatePackage(
|
||||
context: IRestApiContext,
|
||||
name: string,
|
||||
version?: string,
|
||||
checksum?: string,
|
||||
): Promise<PublicInstalledPackage> {
|
||||
return await makeRestApiRequest(context, 'PATCH', '/community-packages', {
|
||||
name,
|
||||
version,
|
||||
checksum,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAvailableCommunityPackageCount(): Promise<number> {
|
||||
const response = await get(
|
||||
NPM_COMMUNITY_NODE_SEARCH_API_URL,
|
||||
'search?q=keywords:n8n-community-node-package',
|
||||
);
|
||||
|
||||
return response.total || 0;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
export interface ConsentDetails {
|
||||
clientName: string;
|
||||
clientId: string;
|
||||
}
|
||||
|
||||
export interface ConsentApprovalResponse {
|
||||
status: string;
|
||||
redirectUrl: string;
|
||||
}
|
||||
|
||||
export async function getConsentDetails(context: IRestApiContext): Promise<ConsentDetails> {
|
||||
return await makeRestApiRequest(context, 'GET', '/consent/details');
|
||||
}
|
||||
|
||||
export async function approveConsent(
|
||||
context: IRestApiContext,
|
||||
approved: boolean,
|
||||
): Promise<ConsentApprovalResponse> {
|
||||
return await makeRestApiRequest(context, 'POST', '/consent/approve', { approved });
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { CredentialResolver, CredentialResolverType } from '@n8n/api-types';
|
||||
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
export async function getCredentialResolvers(
|
||||
context: IRestApiContext,
|
||||
): Promise<CredentialResolver[]> {
|
||||
return await makeRestApiRequest(context, 'GET', '/credential-resolvers');
|
||||
}
|
||||
|
||||
export async function getCredentialResolverTypes(
|
||||
context: IRestApiContext,
|
||||
): Promise<CredentialResolverType[]> {
|
||||
return await makeRestApiRequest(context, 'GET', '/credential-resolvers/types');
|
||||
}
|
||||
|
||||
export async function getCredentialResolver(
|
||||
context: IRestApiContext,
|
||||
resolverId: string,
|
||||
): Promise<CredentialResolver> {
|
||||
return await makeRestApiRequest(context, 'GET', `/credential-resolvers/${resolverId}`);
|
||||
}
|
||||
|
||||
export async function createCredentialResolver(
|
||||
context: IRestApiContext,
|
||||
payload: { name: string; type: string; config: Record<string, unknown> },
|
||||
): Promise<CredentialResolver> {
|
||||
return await makeRestApiRequest(context, 'POST', '/credential-resolvers', payload);
|
||||
}
|
||||
|
||||
export async function updateCredentialResolver(
|
||||
context: IRestApiContext,
|
||||
resolverId: string,
|
||||
payload: {
|
||||
name: string;
|
||||
type: string;
|
||||
config: Record<string, unknown>;
|
||||
clearCredentials?: boolean;
|
||||
},
|
||||
): Promise<CredentialResolver> {
|
||||
return await makeRestApiRequest(context, 'PATCH', `/credential-resolvers/${resolverId}`, payload);
|
||||
}
|
||||
|
||||
export async function deleteCredentialResolver(
|
||||
context: IRestApiContext,
|
||||
resolverId: string,
|
||||
): Promise<void> {
|
||||
return await makeRestApiRequest(context, 'DELETE', `/credential-resolvers/${resolverId}`);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { get } from '../utils';
|
||||
|
||||
export async function getBecomeCreatorCta(context: IRestApiContext): Promise<boolean> {
|
||||
const response = await get(context.baseUrl, '/cta/become-creator');
|
||||
|
||||
return response;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { BannerName } from '@n8n/api-types';
|
||||
import type { Role } from '@n8n/api-types/dist/schemas/user.schema';
|
||||
|
||||
import { get } from '../utils';
|
||||
|
||||
export type DynamicBanner = {
|
||||
id: BannerName;
|
||||
content: string;
|
||||
isDismissible: boolean;
|
||||
dismissPermanently: boolean | null;
|
||||
theme: 'info' | 'warning' | 'danger';
|
||||
priority: number;
|
||||
};
|
||||
|
||||
type DynamicBannerFilters = {
|
||||
version: string;
|
||||
deploymentType: string;
|
||||
planName?: string;
|
||||
instanceId: string;
|
||||
userCreatedAt?: string;
|
||||
isOwner?: boolean;
|
||||
role?: Role;
|
||||
};
|
||||
|
||||
export async function getDynamicBanners(
|
||||
endpoint: string,
|
||||
filters: DynamicBannerFilters,
|
||||
): Promise<DynamicBanner[]> {
|
||||
return await get(endpoint, '', filters);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { IDataObject, MessageEventBusDestinationOptions } from 'n8n-workflow';
|
||||
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
export type ApiMessageEventBusDestinationOptions = MessageEventBusDestinationOptions & {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export function hasDestinationId(
|
||||
destination: MessageEventBusDestinationOptions,
|
||||
): destination is ApiMessageEventBusDestinationOptions {
|
||||
return destination.id !== undefined;
|
||||
}
|
||||
|
||||
export async function saveDestinationToDb(
|
||||
context: IRestApiContext,
|
||||
destination: ApiMessageEventBusDestinationOptions,
|
||||
subscribedEvents: string[] = [],
|
||||
) {
|
||||
const data: IDataObject = {
|
||||
...destination,
|
||||
subscribedEvents,
|
||||
};
|
||||
return await makeRestApiRequest(context, 'POST', '/eventbus/destination', data);
|
||||
}
|
||||
|
||||
export async function deleteDestinationFromDb(context: IRestApiContext, destinationId: string) {
|
||||
return await makeRestApiRequest(context, 'DELETE', `/eventbus/destination?id=${destinationId}`);
|
||||
}
|
||||
|
||||
export async function sendTestMessageToDestination(
|
||||
context: IRestApiContext,
|
||||
destination: ApiMessageEventBusDestinationOptions,
|
||||
): Promise<boolean> {
|
||||
const data: IDataObject = {
|
||||
...destination,
|
||||
};
|
||||
return await makeRestApiRequest(context, 'GET', '/eventbus/testmessage', data);
|
||||
}
|
||||
|
||||
export async function getEventNamesFromBackend(context: IRestApiContext): Promise<string[]> {
|
||||
return await makeRestApiRequest(context, 'GET', '/eventbus/eventnames');
|
||||
}
|
||||
|
||||
export async function getDestinationsFromBackend(
|
||||
context: IRestApiContext,
|
||||
): Promise<MessageEventBusDestinationOptions[]> {
|
||||
return await makeRestApiRequest(context, 'GET', '/eventbus/destination');
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
export async function sessionStarted(context: IRestApiContext): Promise<void> {
|
||||
return await makeRestApiRequest(context, 'GET', '/events/session-started');
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { ExternalSecretsProvider } from '@n8n/api-types';
|
||||
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
export const getExternalSecrets = async (
|
||||
context: IRestApiContext,
|
||||
): Promise<Record<string, string[]>> => {
|
||||
return await makeRestApiRequest(context, 'GET', '/external-secrets/secrets');
|
||||
};
|
||||
|
||||
/**
|
||||
* @beta still under development
|
||||
*/
|
||||
export const getGlobalExternalSecrets = async (
|
||||
context: IRestApiContext,
|
||||
): Promise<Record<string, string[]>> => {
|
||||
return await makeRestApiRequest(context, 'GET', '/secret-providers/completions/secrets/global');
|
||||
};
|
||||
|
||||
/**
|
||||
* @beta still under development
|
||||
*/
|
||||
export const getProjectExternalSecrets = async (
|
||||
context: IRestApiContext,
|
||||
projectId: string,
|
||||
): Promise<Record<string, string[]>> => {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'GET',
|
||||
`/secret-providers/completions/secrets/project/${projectId}`,
|
||||
);
|
||||
};
|
||||
|
||||
export const getExternalSecretsProviders = async (
|
||||
context: IRestApiContext,
|
||||
): Promise<ExternalSecretsProvider[]> => {
|
||||
return await makeRestApiRequest(context, 'GET', '/external-secrets/providers');
|
||||
};
|
||||
|
||||
export const getExternalSecretsProvider = async (
|
||||
context: IRestApiContext,
|
||||
id: string,
|
||||
): Promise<ExternalSecretsProvider> => {
|
||||
return await makeRestApiRequest(context, 'GET', `/external-secrets/providers/${id}`);
|
||||
};
|
||||
|
||||
export const testExternalSecretsProviderConnection = async (
|
||||
context: IRestApiContext,
|
||||
id: string,
|
||||
data: ExternalSecretsProvider['data'],
|
||||
): Promise<{ testState: ExternalSecretsProvider['state'] }> => {
|
||||
return await makeRestApiRequest(context, 'POST', `/external-secrets/providers/${id}/test`, data);
|
||||
};
|
||||
|
||||
export const updateProvider = async (
|
||||
context: IRestApiContext,
|
||||
id: string,
|
||||
data: ExternalSecretsProvider['data'],
|
||||
): Promise<boolean> => {
|
||||
return await makeRestApiRequest(context, 'POST', `/external-secrets/providers/${id}`, data);
|
||||
};
|
||||
|
||||
export const reloadProvider = async (
|
||||
context: IRestApiContext,
|
||||
id: string,
|
||||
): Promise<{ updated: boolean }> => {
|
||||
return await makeRestApiRequest(context, 'POST', `/external-secrets/providers/${id}/update`);
|
||||
};
|
||||
|
||||
export const connectProvider = async (
|
||||
context: IRestApiContext,
|
||||
id: string,
|
||||
connected: boolean,
|
||||
): Promise<boolean> => {
|
||||
return await makeRestApiRequest(context, 'POST', `/external-secrets/providers/${id}/connect`, {
|
||||
connected,
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
export * from './ai-usage';
|
||||
export * from './api-keys';
|
||||
export * from './cloudPlans';
|
||||
export * from './communityNodes';
|
||||
export * from './credentialResolvers';
|
||||
export * from './ctas';
|
||||
export * from './eventbus.ee';
|
||||
export * from './events';
|
||||
export * from './externalSecrets.ee';
|
||||
export * from './secretsProvider.ee';
|
||||
export * from './ldap';
|
||||
export * from './mfa';
|
||||
export * from './nodeTypes';
|
||||
export * from './npsSurvey';
|
||||
export * from './orchestration';
|
||||
export * from './provisioning';
|
||||
export * from './roles';
|
||||
export * from './security-settings';
|
||||
export * from './settings';
|
||||
export * from './module-settings';
|
||||
export * from './sso';
|
||||
export type * from './tags';
|
||||
export * from './templates';
|
||||
export * from './third-party-licenses';
|
||||
export * from './ui';
|
||||
export * from './usage';
|
||||
export * from './users';
|
||||
export * from './versions';
|
||||
export * from './webhooks';
|
||||
export * from './workflowHistory';
|
||||
export type * from './workflows';
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
export interface LdapSyncData {
|
||||
id: number;
|
||||
startedAt: string;
|
||||
endedAt: string;
|
||||
created: number;
|
||||
updated: number;
|
||||
disabled: number;
|
||||
scanned: number;
|
||||
status: string;
|
||||
error: string;
|
||||
runMode: string;
|
||||
}
|
||||
|
||||
export interface LdapSyncTable {
|
||||
status: string;
|
||||
endedAt: string;
|
||||
runTime: string;
|
||||
runMode: string;
|
||||
details: string;
|
||||
}
|
||||
|
||||
export interface LdapConfig {
|
||||
loginEnabled: boolean;
|
||||
loginLabel: string;
|
||||
connectionUrl: string;
|
||||
allowUnauthorizedCerts: boolean;
|
||||
connectionSecurity: string;
|
||||
connectionPort: number;
|
||||
baseDn: string;
|
||||
bindingAdminDn: string;
|
||||
bindingAdminPassword: string;
|
||||
firstNameAttribute: string;
|
||||
lastNameAttribute: string;
|
||||
emailAttribute: string;
|
||||
loginIdAttribute: string;
|
||||
ldapIdAttribute: string;
|
||||
userFilter: string;
|
||||
synchronizationEnabled: boolean;
|
||||
synchronizationInterval: number; // minutes
|
||||
searchPageSize: number;
|
||||
searchTimeout: number;
|
||||
enforceEmailUniqueness: boolean;
|
||||
}
|
||||
|
||||
export async function getLdapConfig(context: IRestApiContext): Promise<LdapConfig> {
|
||||
return await makeRestApiRequest(context, 'GET', '/ldap/config');
|
||||
}
|
||||
|
||||
export async function testLdapConnection(context: IRestApiContext): Promise<{}> {
|
||||
return await makeRestApiRequest(context, 'POST', '/ldap/test-connection');
|
||||
}
|
||||
|
||||
export async function updateLdapConfig(
|
||||
context: IRestApiContext,
|
||||
adConfig: LdapConfig,
|
||||
): Promise<LdapConfig> {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'PUT',
|
||||
'/ldap/config',
|
||||
adConfig as unknown as IDataObject,
|
||||
);
|
||||
}
|
||||
|
||||
export async function runLdapSync(context: IRestApiContext, data: IDataObject): Promise<{}> {
|
||||
return await makeRestApiRequest(context, 'POST', '/ldap/sync', data as unknown as IDataObject);
|
||||
}
|
||||
|
||||
export async function getLdapSynchronizations(
|
||||
context: IRestApiContext,
|
||||
pagination: { page: number },
|
||||
): Promise<LdapSyncData[]> {
|
||||
return await makeRestApiRequest(context, 'GET', '/ldap/sync', pagination);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
export async function canEnableMFA(context: IRestApiContext) {
|
||||
return await makeRestApiRequest(context, 'POST', '/mfa/can-enable');
|
||||
}
|
||||
|
||||
export async function getMfaQR(
|
||||
context: IRestApiContext,
|
||||
): Promise<{ qrCode: string; secret: string; recoveryCodes: string[] }> {
|
||||
return await makeRestApiRequest(context, 'GET', '/mfa/qr');
|
||||
}
|
||||
|
||||
export async function enableMfa(
|
||||
context: IRestApiContext,
|
||||
data: { mfaCode: string },
|
||||
): Promise<void> {
|
||||
return await makeRestApiRequest(context, 'POST', '/mfa/enable', data);
|
||||
}
|
||||
|
||||
export async function verifyMfaCode(
|
||||
context: IRestApiContext,
|
||||
data: { mfaCode: string },
|
||||
): Promise<void> {
|
||||
return await makeRestApiRequest(context, 'POST', '/mfa/verify', data);
|
||||
}
|
||||
|
||||
export type DisableMfaParams = {
|
||||
mfaCode?: string;
|
||||
mfaRecoveryCode?: string;
|
||||
};
|
||||
|
||||
export async function disableMfa(context: IRestApiContext, data: DisableMfaParams): Promise<void> {
|
||||
return await makeRestApiRequest(context, 'POST', '/mfa/disable', data);
|
||||
}
|
||||
|
||||
export async function updateEnforceMfa(context: IRestApiContext, enforce: boolean): Promise<void> {
|
||||
return await makeRestApiRequest(context, 'POST', '/mfa/enforce-mfa', {
|
||||
enforce,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { FrontendModuleSettings } from '@n8n/api-types';
|
||||
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
export async function getModuleSettings(context: IRestApiContext): Promise<FrontendModuleSettings> {
|
||||
return await makeRestApiRequest(context, 'GET', '/module-settings');
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import type {
|
||||
ActionResultRequestDto,
|
||||
CommunityNodeType,
|
||||
GetNodeTypesByIdentifierRequestDto,
|
||||
OptionsRequestDto,
|
||||
ResourceLocatorRequestDto,
|
||||
ResourceMapperFieldsRequestDto,
|
||||
} from '@n8n/api-types';
|
||||
import type { INodeTranslationHeaders } from '@n8n/i18n';
|
||||
import axios from 'axios';
|
||||
import type {
|
||||
INodeListSearchResult,
|
||||
INodePropertyOptions,
|
||||
INodeTypeDescription,
|
||||
INodeTypeNameVersion,
|
||||
NodeParameterValueType,
|
||||
ResourceMapperFields,
|
||||
} from 'n8n-workflow';
|
||||
import { sleep } from 'n8n-workflow';
|
||||
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
async function fetchNodeTypesJsonWithRetry(url: string, retries = 5, delay = 500) {
|
||||
for (let attempt = 0; attempt < retries; attempt++) {
|
||||
const response = await axios.get(url, { withCredentials: true });
|
||||
|
||||
if (typeof response.data === 'object' && response.data !== null) {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
await sleep(delay * attempt);
|
||||
}
|
||||
|
||||
throw new Error('Could not fetch node types');
|
||||
}
|
||||
|
||||
export async function getNodeTypes(baseUrl: string) {
|
||||
return await fetchNodeTypesJsonWithRetry(baseUrl + 'types/nodes.json');
|
||||
}
|
||||
|
||||
export async function getNodeTypeVersions(baseUrl: string): Promise<string[]> {
|
||||
return await fetchNodeTypesJsonWithRetry(baseUrl + 'types/node-versions.json');
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch specific node types by their identifier (name@version format)
|
||||
* This is useful for incremental syncs where only missing node types need to be fetched
|
||||
*
|
||||
* @param context - The REST API context containing base URL and auth info
|
||||
* @param identifiers - Array of node type identifiers in "name@version" format
|
||||
* @returns Array of node type descriptions for the requested identifiers
|
||||
*/
|
||||
export async function getNodeTypesByIdentifier(
|
||||
context: IRestApiContext,
|
||||
identifiers: string[],
|
||||
): Promise<INodeTypeDescription[]> {
|
||||
const body: GetNodeTypesByIdentifierRequestDto = { identifiers };
|
||||
return await makeRestApiRequest(context, 'POST', '/node-types/by-identifier', body);
|
||||
}
|
||||
|
||||
export async function fetchCommunityNodeTypes(
|
||||
context: IRestApiContext,
|
||||
): Promise<CommunityNodeType[]> {
|
||||
return await makeRestApiRequest(context, 'GET', '/community-node-types');
|
||||
}
|
||||
|
||||
export async function fetchCommunityNodeAttributes(
|
||||
context: IRestApiContext,
|
||||
type: string,
|
||||
): Promise<CommunityNodeType | null> {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'GET',
|
||||
`/community-node-types/${encodeURIComponent(type)}`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getNodeTranslationHeaders(
|
||||
context: IRestApiContext,
|
||||
): Promise<INodeTranslationHeaders | undefined> {
|
||||
return await makeRestApiRequest(context, 'GET', '/node-translation-headers');
|
||||
}
|
||||
|
||||
export async function getNodesInformation(
|
||||
context: IRestApiContext,
|
||||
nodeInfos: INodeTypeNameVersion[],
|
||||
): Promise<INodeTypeDescription[]> {
|
||||
return await makeRestApiRequest(context, 'POST', '/node-types', { nodeInfos });
|
||||
}
|
||||
|
||||
export async function getNodeParameterOptions(
|
||||
context: IRestApiContext,
|
||||
sendData: OptionsRequestDto,
|
||||
): Promise<INodePropertyOptions[]> {
|
||||
return await makeRestApiRequest(context, 'POST', '/dynamic-node-parameters/options', sendData);
|
||||
}
|
||||
|
||||
export async function getResourceLocatorResults(
|
||||
context: IRestApiContext,
|
||||
sendData: ResourceLocatorRequestDto,
|
||||
): Promise<INodeListSearchResult> {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'POST',
|
||||
'/dynamic-node-parameters/resource-locator-results',
|
||||
sendData,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getResourceMapperFields(
|
||||
context: IRestApiContext,
|
||||
sendData: ResourceMapperFieldsRequestDto,
|
||||
): Promise<ResourceMapperFields> {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'POST',
|
||||
'/dynamic-node-parameters/resource-mapper-fields',
|
||||
sendData,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getLocalResourceMapperFields(
|
||||
context: IRestApiContext,
|
||||
sendData: ResourceMapperFieldsRequestDto,
|
||||
): Promise<ResourceMapperFields> {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'POST',
|
||||
'/dynamic-node-parameters/local-resource-mapper-fields',
|
||||
sendData,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getNodeParameterActionResult(
|
||||
context: IRestApiContext,
|
||||
sendData: ActionResultRequestDto,
|
||||
): Promise<NodeParameterValueType> {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'POST',
|
||||
'/dynamic-node-parameters/action-result',
|
||||
sendData,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { NpsSurveyState } from 'n8n-workflow';
|
||||
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
export async function updateNpsSurveyState(context: IRestApiContext, state: NpsSurveyState) {
|
||||
await makeRestApiRequest(context, 'PATCH', '/user-settings/nps-survey', state);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
const GET_STATUS_ENDPOINT = '/orchestration/worker/status';
|
||||
|
||||
export const sendGetWorkerStatus = async (context: IRestApiContext): Promise<void> => {
|
||||
await makeRestApiRequest(context, 'POST', GET_STATUS_ENDPOINT);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
export interface ProvisioningConfig {
|
||||
scopesInstanceRoleClaimName: string;
|
||||
scopesName: string;
|
||||
scopesProjectsRolesClaimName: string;
|
||||
scopesProvisionInstanceRole: boolean;
|
||||
scopesProvisionProjectRoles: boolean;
|
||||
}
|
||||
|
||||
export const getProvisioningConfig = async (
|
||||
context: IRestApiContext,
|
||||
): Promise<ProvisioningConfig> => {
|
||||
return await makeRestApiRequest(context, 'GET', '/sso/provisioning/config');
|
||||
};
|
||||
|
||||
export const saveProvisioningConfig = async (
|
||||
context: IRestApiContext,
|
||||
config: Partial<ProvisioningConfig>,
|
||||
): Promise<ProvisioningConfig> => {
|
||||
return await makeRestApiRequest(context, 'PATCH', '/sso/provisioning/config', config);
|
||||
};
|
||||
@@ -0,0 +1,59 @@
|
||||
import type {
|
||||
CreateRoleDto,
|
||||
RoleAssignmentsResponse,
|
||||
RoleProjectMembersResponse,
|
||||
UpdateRoleDto,
|
||||
} from '@n8n/api-types';
|
||||
import type { AllRolesMap, Role } from '@n8n/permissions';
|
||||
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
export const getRoles = async (context: IRestApiContext): Promise<AllRolesMap> => {
|
||||
return await makeRestApiRequest(context, 'GET', '/roles?withUsageCount=true');
|
||||
};
|
||||
|
||||
export const createProjectRole = async (
|
||||
context: IRestApiContext,
|
||||
body: CreateRoleDto,
|
||||
): Promise<Role> => {
|
||||
return await makeRestApiRequest(context, 'POST', '/roles', body);
|
||||
};
|
||||
|
||||
export const getRoleBySlug = async (
|
||||
context: IRestApiContext,
|
||||
body: { slug: string },
|
||||
): Promise<Role> => {
|
||||
return await makeRestApiRequest(context, 'GET', `/roles/${body.slug}?withUsageCount=true`);
|
||||
};
|
||||
|
||||
export const updateProjectRole = async (
|
||||
context: IRestApiContext,
|
||||
slug: string,
|
||||
body: UpdateRoleDto,
|
||||
): Promise<Role> => {
|
||||
return await makeRestApiRequest(context, 'PATCH', `/roles/${slug}`, body);
|
||||
};
|
||||
|
||||
export const deleteProjectRole = async (context: IRestApiContext, slug: string): Promise<Role> => {
|
||||
return await makeRestApiRequest(context, 'DELETE', `/roles/${slug}`);
|
||||
};
|
||||
|
||||
export const getRoleAssignments = async (
|
||||
context: IRestApiContext,
|
||||
slug: string,
|
||||
): Promise<RoleAssignmentsResponse> => {
|
||||
return await makeRestApiRequest(context, 'GET', `/roles/${slug}/assignments`);
|
||||
};
|
||||
|
||||
export const getRoleProjectMembers = async (
|
||||
context: IRestApiContext,
|
||||
slug: string,
|
||||
projectId: string,
|
||||
): Promise<RoleProjectMembersResponse> => {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'GET',
|
||||
`/roles/${slug}/assignments/${projectId}/members`,
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,172 @@
|
||||
import type {
|
||||
ReloadSecretProviderConnectionResponse,
|
||||
SecretProviderConnection,
|
||||
SecretProviderTypeResponse,
|
||||
TestSecretProviderConnectionResponse,
|
||||
} from '@n8n/api-types';
|
||||
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
export const getSecretProviderTypes = async (
|
||||
context: IRestApiContext,
|
||||
): Promise<SecretProviderTypeResponse[]> => {
|
||||
return await makeRestApiRequest(context, 'GET', '/secret-providers/types');
|
||||
};
|
||||
|
||||
export const getSecretProviderConnections = async (
|
||||
context: IRestApiContext,
|
||||
): Promise<SecretProviderConnection[]> => {
|
||||
return await makeRestApiRequest(context, 'GET', '/secret-providers/connections');
|
||||
};
|
||||
|
||||
export const getSecretProviderConnectionByKey = async (
|
||||
context: IRestApiContext,
|
||||
providerKey: string,
|
||||
): Promise<SecretProviderConnection> => {
|
||||
return await makeRestApiRequest(context, 'GET', `/secret-providers/connections/${providerKey}`);
|
||||
};
|
||||
|
||||
export const createSecretProviderConnection = async (
|
||||
context: IRestApiContext,
|
||||
data: {
|
||||
providerKey: string;
|
||||
type: string;
|
||||
isGlobal: boolean;
|
||||
projectIds: string[];
|
||||
settings: Record<string, unknown>;
|
||||
},
|
||||
): Promise<SecretProviderConnection> => {
|
||||
return await makeRestApiRequest(context, 'POST', '/secret-providers/connections', data);
|
||||
};
|
||||
|
||||
export const updateSecretProviderConnection = async (
|
||||
context: IRestApiContext,
|
||||
providerKey: string,
|
||||
data: {
|
||||
isGlobal: boolean;
|
||||
projectIds: string[];
|
||||
settings: Record<string, unknown>;
|
||||
},
|
||||
): Promise<SecretProviderConnection> => {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'PATCH',
|
||||
`/secret-providers/connections/${providerKey}`,
|
||||
data,
|
||||
);
|
||||
};
|
||||
|
||||
export const testSecretProviderConnection = async (
|
||||
context: IRestApiContext,
|
||||
providerKey: string,
|
||||
): Promise<TestSecretProviderConnectionResponse> => {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'POST',
|
||||
`/secret-providers/connections/${providerKey}/test`,
|
||||
);
|
||||
};
|
||||
|
||||
export const reloadSecretProviderConnection = async (
|
||||
context: IRestApiContext,
|
||||
providerKey: string,
|
||||
): Promise<ReloadSecretProviderConnectionResponse> => {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'POST',
|
||||
`/secret-providers/connections/${providerKey}/reload`,
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteSecretProviderConnection = async (
|
||||
context: IRestApiContext,
|
||||
providerKey: string,
|
||||
): Promise<void> => {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'DELETE',
|
||||
`/secret-providers/connections/${providerKey}`,
|
||||
);
|
||||
};
|
||||
|
||||
export const getProjectSecretProviderConnectionsByProjectId = async (
|
||||
context: IRestApiContext,
|
||||
projectId: string,
|
||||
): Promise<SecretProviderConnection[]> => {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'GET',
|
||||
`/secret-providers/projects/${projectId}/connections`,
|
||||
);
|
||||
};
|
||||
|
||||
export const getProjectSecretProviderConnectionByKey = async (
|
||||
context: IRestApiContext,
|
||||
projectId: string,
|
||||
providerKey: string,
|
||||
): Promise<SecretProviderConnection> => {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'GET',
|
||||
`/secret-providers/projects/${projectId}/connections/${providerKey}`,
|
||||
);
|
||||
};
|
||||
|
||||
export const createProjectSecretProviderConnection = async (
|
||||
context: IRestApiContext,
|
||||
projectId: string,
|
||||
data: {
|
||||
providerKey: string;
|
||||
type: string;
|
||||
projectIds: string[];
|
||||
settings: Record<string, unknown>;
|
||||
},
|
||||
): Promise<SecretProviderConnection> => {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'POST',
|
||||
`/secret-providers/projects/${projectId}/connections`,
|
||||
data,
|
||||
);
|
||||
};
|
||||
|
||||
export const updateProjectSecretProviderConnection = async (
|
||||
context: IRestApiContext,
|
||||
projectId: string,
|
||||
providerKey: string,
|
||||
data: {
|
||||
settings: Record<string, unknown>;
|
||||
},
|
||||
): Promise<SecretProviderConnection> => {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'PATCH',
|
||||
`/secret-providers/projects/${projectId}/connections/${providerKey}`,
|
||||
data,
|
||||
);
|
||||
};
|
||||
|
||||
export const testProjectSecretProviderConnection = async (
|
||||
context: IRestApiContext,
|
||||
projectId: string,
|
||||
providerKey: string,
|
||||
): Promise<TestSecretProviderConnectionResponse> => {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'POST',
|
||||
`/secret-providers/projects/${projectId}/connections/${providerKey}/test`,
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteProjectSecretProviderConnection = async (
|
||||
context: IRestApiContext,
|
||||
projectId: string,
|
||||
providerKey: string,
|
||||
): Promise<void> => {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'DELETE',
|
||||
`/secret-providers/projects/${projectId}/connections/${providerKey}`,
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { SecuritySettingsDto, UpdateSecuritySettingsDto } from '@n8n/api-types';
|
||||
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
export async function getSecuritySettings(context: IRestApiContext): Promise<SecuritySettingsDto> {
|
||||
return await makeRestApiRequest(context, 'GET', '/settings/security');
|
||||
}
|
||||
|
||||
export async function updateSecuritySettings(
|
||||
context: IRestApiContext,
|
||||
data: UpdateSecuritySettingsDto,
|
||||
): Promise<SecuritySettingsDto> {
|
||||
return await makeRestApiRequest(context, 'POST', '/settings/security', data);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import type { FrontendSettings } from '@n8n/api-types';
|
||||
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
export async function getSettings(context: IRestApiContext): Promise<FrontendSettings> {
|
||||
return await makeRestApiRequest(context, 'GET', '/settings');
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { OidcConfigDto, SamlPreferences, SamlToggleDto } from '@n8n/api-types';
|
||||
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
export type SamlPreferencesExtractedData = {
|
||||
entityID: string;
|
||||
returnUrl: string;
|
||||
};
|
||||
|
||||
export const initSSO = async (context: IRestApiContext, redirectUrl = ''): Promise<string> => {
|
||||
return await makeRestApiRequest(context, 'GET', `/sso/saml/initsso?redirect=${redirectUrl}`);
|
||||
};
|
||||
|
||||
export const getSamlMetadata = async (context: IRestApiContext): Promise<SamlPreferences> => {
|
||||
return await makeRestApiRequest(context, 'GET', '/sso/saml/metadata');
|
||||
};
|
||||
|
||||
export const getSamlConfig = async (
|
||||
context: IRestApiContext,
|
||||
): Promise<SamlPreferences & SamlPreferencesExtractedData> => {
|
||||
return await makeRestApiRequest(context, 'GET', '/sso/saml/config');
|
||||
};
|
||||
|
||||
export const saveSamlConfig = async (
|
||||
context: IRestApiContext,
|
||||
data: Partial<SamlPreferences>,
|
||||
): Promise<SamlPreferences | undefined> => {
|
||||
return await makeRestApiRequest(context, 'POST', '/sso/saml/config', data);
|
||||
};
|
||||
|
||||
export const toggleSamlConfig = async (
|
||||
context: IRestApiContext,
|
||||
data: SamlToggleDto,
|
||||
): Promise<void> => {
|
||||
return await makeRestApiRequest(context, 'POST', '/sso/saml/config/toggle', data);
|
||||
};
|
||||
|
||||
export const testSamlConfig = async (
|
||||
context: IRestApiContext,
|
||||
data: Partial<SamlPreferences>,
|
||||
): Promise<string> => {
|
||||
return await makeRestApiRequest(context, 'POST', '/sso/saml/config/test', data);
|
||||
};
|
||||
|
||||
export const getOidcConfig = async (context: IRestApiContext): Promise<OidcConfigDto> => {
|
||||
return await makeRestApiRequest(context, 'GET', '/sso/oidc/config');
|
||||
};
|
||||
|
||||
export const saveOidcConfig = async (
|
||||
context: IRestApiContext,
|
||||
data: OidcConfigDto,
|
||||
): Promise<OidcConfigDto> => {
|
||||
return await makeRestApiRequest(context, 'POST', '/sso/oidc/config', data);
|
||||
};
|
||||
|
||||
export const initOidcLogin = async (context: IRestApiContext): Promise<string> => {
|
||||
return await makeRestApiRequest(context, 'GET', '/sso/oidc/login');
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
export interface ITag {
|
||||
id: string;
|
||||
name: string;
|
||||
usageCount?: number;
|
||||
createdAt?: string;
|
||||
updatedAt?: string;
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
import type { RawAxiosRequestHeaders } from 'axios';
|
||||
import type { INode, INodeCredentialsDetails } from 'n8n-workflow';
|
||||
|
||||
import type { VersionNode } from './versions';
|
||||
import type { WorkflowData } from './workflows';
|
||||
import { get } from '../utils';
|
||||
|
||||
export interface IWorkflowTemplateNode
|
||||
extends Pick<
|
||||
INode,
|
||||
'name' | 'type' | 'position' | 'parameters' | 'typeVersion' | 'webhookId' | 'id' | 'disabled'
|
||||
> {
|
||||
// The credentials in a template workflow have a different type than in a regular workflow
|
||||
credentials?: IWorkflowTemplateNodeCredentials;
|
||||
}
|
||||
|
||||
export interface IWorkflowTemplateNodeCredentials {
|
||||
[key: string]: string | INodeCredentialsDetails;
|
||||
}
|
||||
|
||||
export interface IWorkflowTemplate {
|
||||
id: number;
|
||||
name: string;
|
||||
workflow: Pick<WorkflowData, 'connections' | 'settings' | 'pinData'> & {
|
||||
nodes: IWorkflowTemplateNode[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ITemplatesNode extends VersionNode {
|
||||
id: number;
|
||||
categories?: ITemplatesCategory[];
|
||||
}
|
||||
|
||||
export interface ITemplatesCollection {
|
||||
id: number;
|
||||
name: string;
|
||||
nodes: ITemplatesNode[];
|
||||
workflows: Array<{ id: number }>;
|
||||
}
|
||||
|
||||
interface ITemplatesImage {
|
||||
id: number;
|
||||
url: string;
|
||||
}
|
||||
|
||||
interface ITemplatesCollectionExtended extends ITemplatesCollection {
|
||||
description: string | null;
|
||||
image: ITemplatesImage[];
|
||||
categories: ITemplatesCategory[];
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface ITemplatesCollectionFull extends ITemplatesCollectionExtended {
|
||||
full: true;
|
||||
}
|
||||
|
||||
export interface ITemplatesCollectionResponse extends ITemplatesCollectionExtended {
|
||||
workflows: ITemplatesWorkflow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A template without the actual workflow definition
|
||||
*/
|
||||
|
||||
export interface ITemplatesWorkflow {
|
||||
id: number;
|
||||
createdAt: string;
|
||||
name: string;
|
||||
nodes: ITemplatesNode[];
|
||||
totalViews: number;
|
||||
user: {
|
||||
username: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
verified: boolean;
|
||||
};
|
||||
readyToDemo?: boolean | null;
|
||||
}
|
||||
|
||||
export interface ITemplatesWorkflowInfo {
|
||||
nodeCount: number;
|
||||
nodeTypes: {
|
||||
[key: string]: {
|
||||
count: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export type TemplateSearchFacet = {
|
||||
field_name: string;
|
||||
sampled: boolean;
|
||||
stats: {
|
||||
total_values: number;
|
||||
};
|
||||
counts: Array<{
|
||||
count: number;
|
||||
highlighted: string;
|
||||
value: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export interface ITemplatesWorkflowResponse extends ITemplatesWorkflow, IWorkflowTemplate {
|
||||
description: string | null;
|
||||
image: ITemplatesImage[];
|
||||
categories: ITemplatesCategory[];
|
||||
workflowInfo: ITemplatesWorkflowInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* A template with also the full workflow definition
|
||||
*/
|
||||
|
||||
export interface ITemplatesWorkflowFull extends ITemplatesWorkflowResponse {
|
||||
full: true;
|
||||
}
|
||||
|
||||
export interface ITemplatesQuery {
|
||||
categories: string[];
|
||||
search: string;
|
||||
apps?: string[];
|
||||
nodes?: string[];
|
||||
sort?: string;
|
||||
combineWith?: string;
|
||||
}
|
||||
|
||||
export interface ITemplatesCategory {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
function stringifyArray(arr: string[]) {
|
||||
return arr.join(',');
|
||||
}
|
||||
|
||||
export async function testHealthEndpoint(apiEndpoint: string) {
|
||||
return await get(apiEndpoint, '/health');
|
||||
}
|
||||
|
||||
export async function getCategories(
|
||||
apiEndpoint: string,
|
||||
headers?: RawAxiosRequestHeaders,
|
||||
): Promise<{ categories: ITemplatesCategory[] }> {
|
||||
return await get(apiEndpoint, '/templates/categories', undefined, headers);
|
||||
}
|
||||
|
||||
export async function getCollections(
|
||||
apiEndpoint: string,
|
||||
query: ITemplatesQuery,
|
||||
headers?: RawAxiosRequestHeaders,
|
||||
): Promise<{ collections: ITemplatesCollection[] }> {
|
||||
return await get(
|
||||
apiEndpoint,
|
||||
'/templates/collections',
|
||||
{ category: query.categories, search: query.search },
|
||||
headers,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getWorkflows(
|
||||
apiEndpoint: string,
|
||||
query: {
|
||||
page: number;
|
||||
limit: number;
|
||||
categories: string[];
|
||||
search: string;
|
||||
sort?: string;
|
||||
apps?: string[];
|
||||
nodes?: string[];
|
||||
combineWith?: string;
|
||||
},
|
||||
headers?: RawAxiosRequestHeaders,
|
||||
): Promise<{
|
||||
totalWorkflows: number;
|
||||
workflows: ITemplatesWorkflow[];
|
||||
filters: TemplateSearchFacet[];
|
||||
}> {
|
||||
const { apps, sort, combineWith, categories, nodes, ...restQuery } = query;
|
||||
const finalQuery = {
|
||||
...restQuery,
|
||||
category: stringifyArray(categories),
|
||||
...(apps && { apps: stringifyArray(apps) }),
|
||||
...(nodes && { nodes: stringifyArray(nodes) }),
|
||||
...(sort && { sort }),
|
||||
...(combineWith && { combineWith }),
|
||||
};
|
||||
|
||||
return await get(apiEndpoint, '/templates/search', finalQuery, headers);
|
||||
}
|
||||
|
||||
export async function getCollectionById(
|
||||
apiEndpoint: string,
|
||||
collectionId: string,
|
||||
headers?: RawAxiosRequestHeaders,
|
||||
): Promise<{ collection: ITemplatesCollectionResponse }> {
|
||||
return await get(apiEndpoint, `/templates/collections/${collectionId}`, undefined, headers);
|
||||
}
|
||||
|
||||
export async function getTemplateById(
|
||||
apiEndpoint: string,
|
||||
templateId: string,
|
||||
headers?: RawAxiosRequestHeaders,
|
||||
): Promise<{ workflow: ITemplatesWorkflowResponse }> {
|
||||
return await get(apiEndpoint, `/templates/workflows/${templateId}`, undefined, headers);
|
||||
}
|
||||
|
||||
export async function getWorkflowTemplate(
|
||||
apiEndpoint: string,
|
||||
templateId: string,
|
||||
headers?: RawAxiosRequestHeaders,
|
||||
): Promise<IWorkflowTemplate> {
|
||||
return await get(apiEndpoint, `/workflows/templates/${templateId}`, undefined, headers);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { request } from '../utils';
|
||||
|
||||
export async function getThirdPartyLicenses(context: IRestApiContext): Promise<string> {
|
||||
return await request({
|
||||
method: 'GET',
|
||||
baseURL: context.baseUrl,
|
||||
endpoint: '/third-party-licenses',
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { BannerName } from '@n8n/api-types';
|
||||
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
export async function dismissBannerPermanently(
|
||||
context: IRestApiContext,
|
||||
data: { bannerName: BannerName; dismissedBanners: string[] },
|
||||
): Promise<void> {
|
||||
return await makeRestApiRequest(context, 'POST', '/owner/dismiss-banner', {
|
||||
banner: data.bannerName,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { CommunityRegisteredRequestDto, UsageState } from '@n8n/api-types';
|
||||
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
export const getLicense = async (context: IRestApiContext): Promise<UsageState['data']> => {
|
||||
return await makeRestApiRequest(context, 'GET', '/license');
|
||||
};
|
||||
|
||||
export const activateLicenseKey = async (
|
||||
context: IRestApiContext,
|
||||
data: {
|
||||
activationKey: string;
|
||||
eulaUri?: string;
|
||||
},
|
||||
): Promise<UsageState['data']> => {
|
||||
return await makeRestApiRequest(context, 'POST', '/license/activate', data);
|
||||
};
|
||||
|
||||
export const renewLicense = async (context: IRestApiContext): Promise<UsageState['data']> => {
|
||||
return await makeRestApiRequest(context, 'POST', '/license/renew');
|
||||
};
|
||||
|
||||
export const requestLicenseTrial = async (
|
||||
context: IRestApiContext,
|
||||
): Promise<UsageState['data']> => {
|
||||
return await makeRestApiRequest(context, 'POST', '/license/enterprise/request_trial');
|
||||
};
|
||||
|
||||
export const registerCommunityEdition = async (
|
||||
context: IRestApiContext,
|
||||
params: CommunityRegisteredRequestDto,
|
||||
): Promise<{ title: string; text: string }> => {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'POST',
|
||||
'/license/enterprise/community-registered',
|
||||
params,
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,245 @@
|
||||
import type {
|
||||
LoginRequestDto,
|
||||
PasswordUpdateRequestDto,
|
||||
SettingsUpdateRequestDto,
|
||||
UserSelfSettingsUpdateRequestDto,
|
||||
UsersListFilterDto,
|
||||
UserUpdateRequestDto,
|
||||
Role,
|
||||
UsersList,
|
||||
User,
|
||||
} from '@n8n/api-types';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
import type {
|
||||
FeatureFlags,
|
||||
IDataObject,
|
||||
IPersonalizationSurveyAnswersV4,
|
||||
IUserSettings,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
export type IPersonalizationSurveyAnswersV1 = {
|
||||
codingSkill?: string | null;
|
||||
companyIndustry?: string[] | null;
|
||||
companySize?: string | null;
|
||||
otherCompanyIndustry?: string | null;
|
||||
otherWorkArea?: string | null;
|
||||
workArea?: string[] | string | null;
|
||||
};
|
||||
|
||||
export type IPersonalizationSurveyAnswersV2 = {
|
||||
version: 'v2';
|
||||
automationGoal?: string | null;
|
||||
codingSkill?: string | null;
|
||||
companyIndustryExtended?: string[] | null;
|
||||
companySize?: string | null;
|
||||
companyType?: string | null;
|
||||
customerType?: string | null;
|
||||
mspFocus?: string[] | null;
|
||||
mspFocusOther?: string | null;
|
||||
otherAutomationGoal?: string | null;
|
||||
otherCompanyIndustryExtended?: string[] | null;
|
||||
};
|
||||
|
||||
export type IPersonalizationSurveyAnswersV3 = {
|
||||
version: 'v3';
|
||||
automationGoal?: string | null;
|
||||
otherAutomationGoal?: string | null;
|
||||
companyIndustryExtended?: string[] | null;
|
||||
otherCompanyIndustryExtended?: string[] | null;
|
||||
companySize?: string | null;
|
||||
companyType?: string | null;
|
||||
automationGoalSm?: string[] | null;
|
||||
automationGoalSmOther?: string | null;
|
||||
usageModes?: string[] | null;
|
||||
email?: string | null;
|
||||
};
|
||||
|
||||
export type IPersonalizationLatestVersion = IPersonalizationSurveyAnswersV4;
|
||||
|
||||
export type IPersonalizationSurveyVersions =
|
||||
| IPersonalizationSurveyAnswersV1
|
||||
| IPersonalizationSurveyAnswersV2
|
||||
| IPersonalizationSurveyAnswersV3
|
||||
| IPersonalizationSurveyAnswersV4;
|
||||
|
||||
export interface IUserResponse extends User {
|
||||
globalScopes?: Scope[];
|
||||
personalizationAnswers?: IPersonalizationSurveyVersions | null;
|
||||
settings?: IUserSettings | null;
|
||||
}
|
||||
|
||||
export interface CurrentUserResponse extends IUserResponse {
|
||||
featureFlags?: FeatureFlags;
|
||||
}
|
||||
|
||||
export interface IUser extends IUserResponse {
|
||||
isDefaultUser: boolean;
|
||||
isPendingUser: boolean;
|
||||
inviteAcceptUrl?: string;
|
||||
fullName?: string;
|
||||
createdAt?: string;
|
||||
mfaEnabled: boolean;
|
||||
mfaAuthenticated?: boolean;
|
||||
}
|
||||
|
||||
export async function loginCurrentUser(
|
||||
context: IRestApiContext,
|
||||
): Promise<CurrentUserResponse | null> {
|
||||
return await makeRestApiRequest(context, 'GET', '/login');
|
||||
}
|
||||
|
||||
export async function login(
|
||||
context: IRestApiContext,
|
||||
params: LoginRequestDto,
|
||||
): Promise<CurrentUserResponse> {
|
||||
return await makeRestApiRequest(context, 'POST', '/login', params);
|
||||
}
|
||||
|
||||
export async function logout(context: IRestApiContext): Promise<void> {
|
||||
await makeRestApiRequest(context, 'POST', '/logout');
|
||||
}
|
||||
|
||||
export async function setupOwner(
|
||||
context: IRestApiContext,
|
||||
params: { firstName: string; lastName: string; email: string; password: string },
|
||||
): Promise<CurrentUserResponse> {
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'POST',
|
||||
'/owner/setup',
|
||||
params as unknown as IDataObject,
|
||||
);
|
||||
}
|
||||
|
||||
export async function validateSignupToken(
|
||||
context: IRestApiContext,
|
||||
params: { token?: string } | { inviterId?: string; inviteeId?: string },
|
||||
): Promise<{ inviter: { firstName: string; lastName: string } }> {
|
||||
return await makeRestApiRequest(context, 'GET', '/resolve-signup-token', params);
|
||||
}
|
||||
|
||||
export async function signup(
|
||||
context: IRestApiContext,
|
||||
params: {
|
||||
inviterId: string;
|
||||
inviteeId: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
password: string;
|
||||
},
|
||||
): Promise<CurrentUserResponse> {
|
||||
const { inviteeId, ...props } = params;
|
||||
return await makeRestApiRequest(
|
||||
context,
|
||||
'POST',
|
||||
`/users/${params.inviteeId}`,
|
||||
props as unknown as IDataObject,
|
||||
);
|
||||
}
|
||||
|
||||
export async function sendForgotPasswordEmail(
|
||||
context: IRestApiContext,
|
||||
params: { email: string },
|
||||
): Promise<void> {
|
||||
await makeRestApiRequest(context, 'POST', '/forgot-password', params);
|
||||
}
|
||||
|
||||
export async function validatePasswordToken(
|
||||
context: IRestApiContext,
|
||||
params: { token: string },
|
||||
): Promise<void> {
|
||||
await makeRestApiRequest(context, 'GET', '/resolve-password-token', params);
|
||||
}
|
||||
|
||||
export async function changePassword(
|
||||
context: IRestApiContext,
|
||||
params: { token: string; password: string; mfaCode?: string },
|
||||
): Promise<void> {
|
||||
await makeRestApiRequest(context, 'POST', '/change-password', params);
|
||||
}
|
||||
|
||||
export async function updateCurrentUser(
|
||||
context: IRestApiContext,
|
||||
params: UserUpdateRequestDto,
|
||||
): Promise<IUserResponse> {
|
||||
return await makeRestApiRequest(context, 'PATCH', '/me', params);
|
||||
}
|
||||
|
||||
export async function updateCurrentUserSettings(
|
||||
context: IRestApiContext,
|
||||
settings: UserSelfSettingsUpdateRequestDto,
|
||||
): Promise<IUserSettings> {
|
||||
return await makeRestApiRequest(context, 'PATCH', '/me/settings', settings);
|
||||
}
|
||||
|
||||
export async function updateOtherUserSettings(
|
||||
context: IRestApiContext,
|
||||
userId: string,
|
||||
settings: SettingsUpdateRequestDto,
|
||||
): Promise<IUserSettings> {
|
||||
return await makeRestApiRequest(context, 'PATCH', `/users/${userId}/settings`, settings);
|
||||
}
|
||||
|
||||
export async function updateCurrentUserPassword(
|
||||
context: IRestApiContext,
|
||||
params: PasswordUpdateRequestDto,
|
||||
): Promise<void> {
|
||||
return await makeRestApiRequest(context, 'PATCH', '/me/password', params);
|
||||
}
|
||||
|
||||
export async function deleteUser(
|
||||
context: IRestApiContext,
|
||||
{ id, transferId }: { id: string; transferId?: string },
|
||||
): Promise<void> {
|
||||
await makeRestApiRequest(context, 'DELETE', `/users/${id}`, transferId ? { transferId } : {});
|
||||
}
|
||||
|
||||
export async function getUsers(
|
||||
context: IRestApiContext,
|
||||
filter?: UsersListFilterDto,
|
||||
): Promise<UsersList> {
|
||||
return await makeRestApiRequest(context, 'GET', '/users', filter);
|
||||
}
|
||||
|
||||
export async function getInviteLink(
|
||||
context: IRestApiContext,
|
||||
{ id }: { id: string },
|
||||
): Promise<{ link: string }> {
|
||||
return await makeRestApiRequest(context, 'GET', `/users/${id}/invite-link`);
|
||||
}
|
||||
|
||||
export async function generateInviteLink(
|
||||
context: IRestApiContext,
|
||||
{ id }: { id: string },
|
||||
): Promise<{ link: string }> {
|
||||
return await makeRestApiRequest(context, 'POST', `/users/${id}/invite-link`);
|
||||
}
|
||||
|
||||
export async function getPasswordResetLink(
|
||||
context: IRestApiContext,
|
||||
{ id }: { id: string },
|
||||
): Promise<{ link: string }> {
|
||||
return await makeRestApiRequest(context, 'GET', `/users/${id}/password-reset-link`);
|
||||
}
|
||||
|
||||
export async function submitPersonalizationSurvey(
|
||||
context: IRestApiContext,
|
||||
params: IPersonalizationLatestVersion,
|
||||
): Promise<void> {
|
||||
await makeRestApiRequest(context, 'POST', '/me/survey', params as unknown as IDataObject);
|
||||
}
|
||||
|
||||
export interface UpdateGlobalRolePayload {
|
||||
id: string;
|
||||
newRoleName: Role;
|
||||
}
|
||||
|
||||
export async function updateGlobalRole(
|
||||
context: IRestApiContext,
|
||||
{ id, newRoleName }: UpdateGlobalRolePayload,
|
||||
): Promise<IUserResponse> {
|
||||
return await makeRestApiRequest(context, 'PATCH', `/users/${id}/role`, { newRoleName });
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { INSTANCE_ID_HEADER, INSTANCE_VERSION_HEADER } from '@n8n/constants';
|
||||
import type { INodeParameters } from 'n8n-workflow';
|
||||
|
||||
import { get } from '../utils';
|
||||
|
||||
export interface VersionNode {
|
||||
name: string;
|
||||
displayName: string;
|
||||
icon: string;
|
||||
iconUrl?: string;
|
||||
defaults: INodeParameters;
|
||||
iconData: {
|
||||
type: string;
|
||||
icon?: string;
|
||||
fileBuffer?: string;
|
||||
};
|
||||
typeVersion?: number;
|
||||
}
|
||||
|
||||
export interface Version {
|
||||
name: string;
|
||||
nodes: VersionNode[];
|
||||
createdAt: string;
|
||||
description: string;
|
||||
documentationUrl: string;
|
||||
hasBreakingChange: boolean;
|
||||
hasSecurityFix: boolean;
|
||||
hasSecurityIssue: boolean;
|
||||
securityIssueFixVersion: string;
|
||||
}
|
||||
|
||||
export interface WhatsNewSection {
|
||||
title: string;
|
||||
calloutText: string;
|
||||
footer: string;
|
||||
items: WhatsNewArticle[];
|
||||
createdAt: string;
|
||||
updatedAt: string | null;
|
||||
}
|
||||
|
||||
export interface WhatsNewArticle {
|
||||
id: number;
|
||||
createdAt: string;
|
||||
updatedAt: string | null;
|
||||
publishedAt: string;
|
||||
title: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
export async function getNextVersions(
|
||||
endpoint: string,
|
||||
currentVersion: string,
|
||||
instanceId: string,
|
||||
): Promise<Version[]> {
|
||||
const headers = { [INSTANCE_ID_HEADER as string]: instanceId };
|
||||
return await get(endpoint, currentVersion, {}, headers);
|
||||
}
|
||||
|
||||
export async function getWhatsNewSection(
|
||||
endpoint: string,
|
||||
currentVersion: string,
|
||||
instanceId: string,
|
||||
): Promise<WhatsNewSection> {
|
||||
const headers = {
|
||||
[INSTANCE_ID_HEADER as string]: instanceId,
|
||||
[INSTANCE_VERSION_HEADER as string]: currentVersion,
|
||||
};
|
||||
return await get(endpoint, '', {}, headers);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { IHttpRequestMethods } from 'n8n-workflow';
|
||||
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { makeRestApiRequest } from '../utils';
|
||||
|
||||
type WebhookData = {
|
||||
workflowId: string;
|
||||
webhookPath: string;
|
||||
method: IHttpRequestMethods;
|
||||
node: string;
|
||||
};
|
||||
|
||||
export const findWebhook = async (
|
||||
context: IRestApiContext,
|
||||
data: { path: string; method: string },
|
||||
): Promise<WebhookData | null> => {
|
||||
return await makeRestApiRequest(context, 'POST', '/webhooks/find', data);
|
||||
};
|
||||
@@ -0,0 +1,98 @@
|
||||
import type { IConnections, INode } from 'n8n-workflow';
|
||||
|
||||
import type { IRestApiContext } from '../types';
|
||||
import { get, patch, post } from '../utils';
|
||||
|
||||
export type WorkflowHistory = {
|
||||
versionId: string;
|
||||
authors: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
workflowPublishHistory: WorkflowPublishHistory[];
|
||||
name: string | null;
|
||||
description: string | null;
|
||||
};
|
||||
|
||||
export type WorkflowVersionData = Pick<WorkflowHistory, 'versionId' | 'name' | 'description'>;
|
||||
|
||||
export type WorkflowPublishHistory = {
|
||||
createdAt: string;
|
||||
id: number;
|
||||
event: 'activated' | 'deactivated';
|
||||
userId: string | null;
|
||||
versionId: string;
|
||||
workflowId: string;
|
||||
};
|
||||
|
||||
export type WorkflowVersionId = WorkflowHistory['versionId'];
|
||||
|
||||
export type WorkflowVersion = WorkflowHistory & {
|
||||
workflowId: string;
|
||||
nodes: INode[];
|
||||
connections: IConnections;
|
||||
};
|
||||
|
||||
export type WorkflowHistoryActionTypes = Array<
|
||||
'restore' | 'publish' | 'unpublish' | 'clone' | 'open' | 'download' | 'name'
|
||||
>;
|
||||
|
||||
export type WorkflowHistoryRequestParams = { take: number; skip?: number };
|
||||
|
||||
export type UpdateWorkflowHistoryVersion = {
|
||||
nodes?: INode[];
|
||||
connections?: IConnections;
|
||||
authors?: string;
|
||||
name?: string | null;
|
||||
description?: string | null;
|
||||
};
|
||||
|
||||
export const getWorkflowHistory = async (
|
||||
context: IRestApiContext,
|
||||
workflowId: string,
|
||||
queryParams: WorkflowHistoryRequestParams,
|
||||
): Promise<WorkflowHistory[]> => {
|
||||
const { data } = await get(
|
||||
context.baseUrl,
|
||||
`/workflow-history/workflow/${workflowId}`,
|
||||
queryParams,
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getWorkflowVersion = async (
|
||||
context: IRestApiContext,
|
||||
workflowId: string,
|
||||
versionId: string,
|
||||
): Promise<WorkflowVersion> => {
|
||||
const { data } = await get(
|
||||
context.baseUrl,
|
||||
`/workflow-history/workflow/${workflowId}/version/${versionId}`,
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const getWorkflowVersionsByIds = async (
|
||||
context: IRestApiContext,
|
||||
workflowId: string,
|
||||
versionIds: string[],
|
||||
): Promise<{ versions: Array<{ versionId: string; createdAt: string }> }> => {
|
||||
const { data } = await post(
|
||||
context.baseUrl,
|
||||
`/workflow-history/workflow/${workflowId}/versions`,
|
||||
{ versionIds },
|
||||
);
|
||||
return data;
|
||||
};
|
||||
|
||||
export const updateWorkflowHistoryVersion = async (
|
||||
context: IRestApiContext,
|
||||
workflowId: string,
|
||||
versionId: string,
|
||||
data: UpdateWorkflowHistoryVersion,
|
||||
): Promise<void> => {
|
||||
await patch(
|
||||
context.baseUrl,
|
||||
`/workflow-history/workflow/${workflowId}/versions/${versionId}`,
|
||||
data,
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { IWorkflowSettings, IConnections, INode, IPinData } from 'n8n-workflow';
|
||||
|
||||
import type { ITag } from './tags';
|
||||
|
||||
export interface WorkflowMetadata {
|
||||
onboardingId?: string;
|
||||
templateId?: string;
|
||||
instanceId?: string;
|
||||
templateCredsSetupCompleted?: boolean;
|
||||
}
|
||||
|
||||
// Simple version of n8n-workflow.Workflow
|
||||
export interface WorkflowData {
|
||||
id?: string;
|
||||
name?: string;
|
||||
active?: boolean;
|
||||
nodes: INode[];
|
||||
connections: IConnections;
|
||||
settings?: IWorkflowSettings;
|
||||
tags?: string[];
|
||||
pinData?: IPinData;
|
||||
versionId?: string;
|
||||
activeVersionId?: string | null;
|
||||
meta?: WorkflowMetadata;
|
||||
}
|
||||
|
||||
export interface WorkflowDataUpdate {
|
||||
id?: string;
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
nodes?: INode[];
|
||||
connections?: IConnections;
|
||||
settings?: IWorkflowSettings;
|
||||
active?: boolean;
|
||||
tags?: ITag[] | string[]; // string[] when store or requested, ITag[] from API response
|
||||
pinData?: IPinData;
|
||||
versionId?: string;
|
||||
meta?: WorkflowMetadata;
|
||||
parentFolderId?: string;
|
||||
uiContext?: string;
|
||||
// checksum of workflow snapshot for conflict detection
|
||||
expectedChecksum?: string;
|
||||
aiBuilderAssisted?: boolean;
|
||||
autosaved?: boolean;
|
||||
}
|
||||
|
||||
export interface WorkflowDataCreate extends WorkflowDataUpdate {
|
||||
projectId?: string;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './api';
|
||||
export type * from './types';
|
||||
export * from './utils';
|
||||
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface IRestApiContext {
|
||||
baseUrl: string;
|
||||
pushRef: string;
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import { ResponseError, STREAM_SEPARATOR, streamRequest } from './utils';
|
||||
|
||||
describe('streamRequest', () => {
|
||||
it('should stream data from the API endpoint', async () => {
|
||||
const encoder = new TextEncoder();
|
||||
const mockResponse = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(encoder.encode(`${JSON.stringify({ chunk: 1 })}${STREAM_SEPARATOR}`));
|
||||
controller.enqueue(encoder.encode(`${JSON.stringify({ chunk: 2 })}${STREAM_SEPARATOR}`));
|
||||
controller.enqueue(encoder.encode(`${JSON.stringify({ chunk: 3 })}${STREAM_SEPARATOR}`));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
body: mockResponse,
|
||||
});
|
||||
|
||||
global.fetch = mockFetch;
|
||||
|
||||
const onChunkMock = vi.fn();
|
||||
const onDoneMock = vi.fn();
|
||||
const onErrorMock = vi.fn();
|
||||
|
||||
await streamRequest(
|
||||
{
|
||||
baseUrl: 'https://api.example.com',
|
||||
pushRef: '',
|
||||
},
|
||||
'/data',
|
||||
{ key: 'value' },
|
||||
onChunkMock,
|
||||
onDoneMock,
|
||||
onErrorMock,
|
||||
);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/data', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ key: 'value' }),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'browser-id': expect.stringContaining('-'),
|
||||
},
|
||||
});
|
||||
|
||||
expect(onChunkMock).toHaveBeenCalledTimes(3);
|
||||
expect(onChunkMock).toHaveBeenNthCalledWith(1, { chunk: 1 });
|
||||
expect(onChunkMock).toHaveBeenNthCalledWith(2, { chunk: 2 });
|
||||
expect(onChunkMock).toHaveBeenNthCalledWith(3, { chunk: 3 });
|
||||
|
||||
expect(onDoneMock).toHaveBeenCalledTimes(1);
|
||||
expect(onErrorMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should stream error response with error data from the API endpoint', async () => {
|
||||
const testError = { code: 500, message: 'Error happened' };
|
||||
const encoder = new TextEncoder();
|
||||
const mockResponse = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(encoder.encode(JSON.stringify(testError)));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 500,
|
||||
statusText: 'Internal Server Error',
|
||||
body: mockResponse,
|
||||
});
|
||||
|
||||
global.fetch = mockFetch;
|
||||
|
||||
const onChunkMock = vi.fn();
|
||||
const onDoneMock = vi.fn();
|
||||
const onErrorMock = vi.fn();
|
||||
|
||||
await streamRequest(
|
||||
{
|
||||
baseUrl: 'https://api.example.com',
|
||||
pushRef: '',
|
||||
},
|
||||
'/data',
|
||||
{ key: 'value' },
|
||||
onChunkMock,
|
||||
onDoneMock,
|
||||
onErrorMock,
|
||||
);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/data', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ key: 'value' }),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'browser-id': expect.stringContaining('-'),
|
||||
},
|
||||
});
|
||||
|
||||
expect(onChunkMock).not.toHaveBeenCalled();
|
||||
expect(onDoneMock).not.toHaveBeenCalled();
|
||||
expect(onErrorMock).toHaveBeenCalledExactlyOnceWith(
|
||||
new ResponseError(testError.message, { httpStatusCode: 500 }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should call onError when stream ends immediately with non-ok status and no chunks', async () => {
|
||||
const mockResponse = new ReadableStream({
|
||||
start(controller) {
|
||||
// Empty stream that just closes without sending any chunks
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status: 403,
|
||||
statusText: 'Forbidden',
|
||||
body: mockResponse,
|
||||
});
|
||||
|
||||
global.fetch = mockFetch;
|
||||
|
||||
const onChunkMock = vi.fn();
|
||||
const onDoneMock = vi.fn();
|
||||
const onErrorMock = vi.fn();
|
||||
|
||||
await streamRequest(
|
||||
{
|
||||
baseUrl: 'https://api.example.com',
|
||||
pushRef: '',
|
||||
},
|
||||
'/data',
|
||||
{ key: 'value' },
|
||||
onChunkMock,
|
||||
onDoneMock,
|
||||
onErrorMock,
|
||||
);
|
||||
|
||||
expect(onChunkMock).not.toHaveBeenCalled();
|
||||
expect(onDoneMock).not.toHaveBeenCalled();
|
||||
expect(onErrorMock).toHaveBeenCalledTimes(1);
|
||||
expect(onErrorMock).toHaveBeenCalledExactlyOnceWith(
|
||||
new ResponseError('Forbidden', { httpStatusCode: 403 }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle broken stream data', async () => {
|
||||
const encoder = new TextEncoder();
|
||||
const mockResponse = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(
|
||||
encoder.encode(`${JSON.stringify({ chunk: 1 })}${STREAM_SEPARATOR}{"chunk": `),
|
||||
);
|
||||
controller.enqueue(encoder.encode(`2}${STREAM_SEPARATOR}{"ch`));
|
||||
controller.enqueue(encoder.encode('unk":'));
|
||||
controller.enqueue(encoder.encode(`3}${STREAM_SEPARATOR}`));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
body: mockResponse,
|
||||
});
|
||||
|
||||
global.fetch = mockFetch;
|
||||
|
||||
const onChunkMock = vi.fn();
|
||||
const onDoneMock = vi.fn();
|
||||
const onErrorMock = vi.fn();
|
||||
|
||||
await streamRequest(
|
||||
{
|
||||
baseUrl: 'https://api.example.com',
|
||||
pushRef: '',
|
||||
},
|
||||
'/data',
|
||||
{ key: 'value' },
|
||||
onChunkMock,
|
||||
onDoneMock,
|
||||
onErrorMock,
|
||||
);
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/data', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ key: 'value' }),
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'browser-id': expect.stringContaining('-'),
|
||||
},
|
||||
});
|
||||
|
||||
expect(onChunkMock).toHaveBeenCalledTimes(3);
|
||||
expect(onChunkMock).toHaveBeenNthCalledWith(1, { chunk: 1 });
|
||||
expect(onChunkMock).toHaveBeenNthCalledWith(2, { chunk: 2 });
|
||||
expect(onChunkMock).toHaveBeenNthCalledWith(3, { chunk: 3 });
|
||||
|
||||
expect(onDoneMock).toHaveBeenCalledTimes(1);
|
||||
expect(onErrorMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,335 @@
|
||||
import { BROWSER_ID_STORAGE_KEY } from '@n8n/constants';
|
||||
import { assert } from '@n8n/utils/assert';
|
||||
import type { AxiosRequestConfig, Method, RawAxiosRequestHeaders } from 'axios';
|
||||
import axios from 'axios';
|
||||
import { ApplicationError, jsonParse } from 'n8n-workflow';
|
||||
import type { GenericValue, IDataObject } from 'n8n-workflow';
|
||||
|
||||
import type { IRestApiContext } from './types';
|
||||
|
||||
const getBrowserId = () => {
|
||||
let browserId = localStorage.getItem(BROWSER_ID_STORAGE_KEY);
|
||||
if (!browserId) {
|
||||
browserId = crypto.randomUUID();
|
||||
localStorage.setItem(BROWSER_ID_STORAGE_KEY, browserId);
|
||||
}
|
||||
return browserId;
|
||||
};
|
||||
|
||||
export const NO_NETWORK_ERROR_CODE = 999;
|
||||
export const STREAM_SEPARATOR = '⧉⇋⇋➽⌑⧉§§\n';
|
||||
|
||||
export class MfaRequiredError extends ApplicationError {
|
||||
constructor() {
|
||||
super('MFA is required to access this resource. Please set up MFA in your user settings.');
|
||||
this.name = 'MfaRequiredError';
|
||||
}
|
||||
}
|
||||
|
||||
export class ResponseError extends ApplicationError {
|
||||
// The HTTP status code of response
|
||||
httpStatusCode?: number;
|
||||
|
||||
// The error code in the response
|
||||
errorCode?: number;
|
||||
|
||||
// The stack trace of the server
|
||||
serverStackTrace?: string;
|
||||
|
||||
// Additional metadata from the server (e.g., EULA URL)
|
||||
meta?: Record<string, unknown>;
|
||||
|
||||
// Additional hint from the server
|
||||
hint?: string;
|
||||
|
||||
/**
|
||||
* Creates an instance of ResponseError.
|
||||
* @param {string} message The error message
|
||||
* @param {number} [errorCode] The error code which can be used by frontend to identify the actual error
|
||||
* @param {number} [httpStatusCode] The HTTP status code the response should have
|
||||
* @param {string} [stack] The stack trace
|
||||
* @param {Record<string, unknown>} [meta] Additional metadata from the server
|
||||
* @param {string} [hint] Additional hint from the server
|
||||
*/
|
||||
constructor(
|
||||
message: string,
|
||||
options: {
|
||||
errorCode?: number;
|
||||
httpStatusCode?: number;
|
||||
stack?: string;
|
||||
meta?: Record<string, unknown>;
|
||||
hint?: ResponseError['hint'];
|
||||
} = {},
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'ResponseError';
|
||||
|
||||
const { errorCode, httpStatusCode, stack, meta, hint } = options;
|
||||
if (errorCode) {
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
if (httpStatusCode) {
|
||||
this.httpStatusCode = httpStatusCode;
|
||||
}
|
||||
if (stack) {
|
||||
this.serverStackTrace = stack;
|
||||
}
|
||||
if (meta) {
|
||||
this.meta = meta;
|
||||
}
|
||||
if (hint) {
|
||||
this.hint = hint;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const legacyParamSerializer = (params: Record<string, any>) =>
|
||||
Object.keys(params)
|
||||
.filter((key) => params[key] !== undefined)
|
||||
.map((key) => {
|
||||
if (Array.isArray(params[key])) {
|
||||
return params[key].map((v: string) => `${key}[]=${encodeURIComponent(v)}`).join('&');
|
||||
}
|
||||
if (typeof params[key] === 'object') {
|
||||
params[key] = JSON.stringify(params[key]);
|
||||
}
|
||||
return `${key}=${encodeURIComponent(params[key])}`;
|
||||
})
|
||||
.join('&');
|
||||
|
||||
export async function request(config: {
|
||||
method: Method;
|
||||
baseURL: string;
|
||||
endpoint: string;
|
||||
headers?: RawAxiosRequestHeaders;
|
||||
data?: GenericValue | GenericValue[];
|
||||
withCredentials?: boolean;
|
||||
}) {
|
||||
const { method, baseURL, endpoint, headers, data } = config;
|
||||
const options: AxiosRequestConfig = {
|
||||
method,
|
||||
url: endpoint,
|
||||
baseURL,
|
||||
headers: headers ?? {},
|
||||
};
|
||||
if (baseURL.startsWith('/')) {
|
||||
options.headers!['browser-id'] = getBrowserId();
|
||||
}
|
||||
if (
|
||||
import.meta.env.NODE_ENV !== 'production' &&
|
||||
!baseURL.includes('api.n8n.io') &&
|
||||
!baseURL.includes('n8n.cloud')
|
||||
) {
|
||||
options.withCredentials = options.withCredentials ?? true;
|
||||
}
|
||||
if (['POST', 'PATCH', 'PUT'].includes(method)) {
|
||||
options.data = data;
|
||||
} else if (data) {
|
||||
options.params = data;
|
||||
options.paramsSerializer = legacyParamSerializer;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.request(options);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (error.message === 'Network Error') {
|
||||
throw new ResponseError("Can't connect to n8n.", {
|
||||
errorCode: NO_NETWORK_ERROR_CODE,
|
||||
});
|
||||
}
|
||||
|
||||
const errorResponseData = error.response?.data;
|
||||
if (errorResponseData?.mfaRequired === true) {
|
||||
throw new MfaRequiredError();
|
||||
}
|
||||
if (errorResponseData?.message !== undefined) {
|
||||
if (errorResponseData.name === 'NodeApiError') {
|
||||
errorResponseData.httpStatusCode = error.response.status;
|
||||
throw errorResponseData;
|
||||
}
|
||||
|
||||
throw new ResponseError(errorResponseData.message, {
|
||||
errorCode: errorResponseData.code,
|
||||
httpStatusCode: error.response.status,
|
||||
stack: errorResponseData.stack,
|
||||
meta: errorResponseData.meta,
|
||||
hint: errorResponseData.hint,
|
||||
});
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a request to the API and returns the response without extracting the data key.
|
||||
* @param context Rest API context
|
||||
* @param method HTTP method
|
||||
* @param endpoint relative path to the API endpoint
|
||||
* @param data request data
|
||||
* @returns data and total count
|
||||
*/
|
||||
export async function getFullApiResponse<T>(
|
||||
context: IRestApiContext,
|
||||
method: Method,
|
||||
endpoint: string,
|
||||
data?: GenericValue | GenericValue[],
|
||||
) {
|
||||
const response = await request({
|
||||
method,
|
||||
baseURL: context.baseUrl,
|
||||
endpoint,
|
||||
headers: { 'push-ref': context.pushRef },
|
||||
data,
|
||||
});
|
||||
|
||||
return response as { count: number; data: T };
|
||||
}
|
||||
|
||||
export async function makeRestApiRequest<T>(
|
||||
context: IRestApiContext,
|
||||
method: Method,
|
||||
endpoint: string,
|
||||
data?: GenericValue | GenericValue[],
|
||||
) {
|
||||
const response = await request({
|
||||
method,
|
||||
baseURL: context.baseUrl,
|
||||
endpoint,
|
||||
headers: { 'push-ref': context.pushRef },
|
||||
data,
|
||||
});
|
||||
|
||||
// All cli rest api endpoints return data wrapped in `data` key
|
||||
return response.data as T;
|
||||
}
|
||||
|
||||
export async function get(
|
||||
baseURL: string,
|
||||
endpoint: string,
|
||||
params?: IDataObject,
|
||||
headers?: RawAxiosRequestHeaders,
|
||||
) {
|
||||
return await request({ method: 'GET', baseURL, endpoint, headers, data: params });
|
||||
}
|
||||
|
||||
export async function post(
|
||||
baseURL: string,
|
||||
endpoint: string,
|
||||
params?: IDataObject,
|
||||
headers?: RawAxiosRequestHeaders,
|
||||
) {
|
||||
return await request({ method: 'POST', baseURL, endpoint, headers, data: params });
|
||||
}
|
||||
|
||||
export async function patch(
|
||||
baseURL: string,
|
||||
endpoint: string,
|
||||
params?: IDataObject,
|
||||
headers?: RawAxiosRequestHeaders,
|
||||
) {
|
||||
return await request({ method: 'PATCH', baseURL, endpoint, headers, data: params });
|
||||
}
|
||||
|
||||
export async function streamRequest<T extends object>(
|
||||
context: IRestApiContext,
|
||||
apiEndpoint: string,
|
||||
payload: object,
|
||||
onChunk?: (chunk: T) => void,
|
||||
onDone?: () => void,
|
||||
onError?: (e: Error) => void,
|
||||
separator = STREAM_SEPARATOR,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<void> {
|
||||
let onErrorOnce: ((e: Error) => void) | undefined = (e: Error) => {
|
||||
onErrorOnce = undefined;
|
||||
onError?.(e);
|
||||
};
|
||||
const headers: Record<string, string> = {
|
||||
'browser-id': getBrowserId(),
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
const assistantRequest: RequestInit = {
|
||||
headers,
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(payload),
|
||||
signal: abortSignal,
|
||||
};
|
||||
try {
|
||||
const response = await fetch(`${context.baseUrl}${apiEndpoint}`, assistantRequest);
|
||||
|
||||
if (response.body) {
|
||||
// Handle the streaming response
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
|
||||
let buffer = '';
|
||||
|
||||
async function readStream() {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
if (response.ok) {
|
||||
onDone?.();
|
||||
} else {
|
||||
onErrorOnce?.(
|
||||
new ResponseError(response.statusText, {
|
||||
httpStatusCode: response.status,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const chunk = decoder.decode(value);
|
||||
buffer += chunk;
|
||||
|
||||
const splitChunks = buffer.split(separator);
|
||||
|
||||
buffer = '';
|
||||
for (const splitChunk of splitChunks) {
|
||||
if (splitChunk) {
|
||||
let data: T;
|
||||
try {
|
||||
data = jsonParse<T>(splitChunk, { errorMessage: 'Invalid json' });
|
||||
} catch (e) {
|
||||
// incomplete json. append to buffer to complete
|
||||
buffer += splitChunk;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
if (response.ok) {
|
||||
// Call chunk callback if request was successful
|
||||
onChunk?.(data);
|
||||
} else {
|
||||
// Otherwise, call error callback
|
||||
const message = 'message' in data ? data.message : response.statusText;
|
||||
onErrorOnce?.(
|
||||
new ResponseError(String(message), {
|
||||
httpStatusCode: response.status,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
onErrorOnce?.(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
await readStream();
|
||||
}
|
||||
|
||||
// Start reading the stream
|
||||
await readStream();
|
||||
} else if (onErrorOnce) {
|
||||
onErrorOnce(new Error(response.statusText));
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
assert(e instanceof Error);
|
||||
onErrorOnce?.(e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "@n8n/typescript-config/tsconfig.frontend.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "dist",
|
||||
"useUnknownInCatchVariables": false,
|
||||
"types": ["vite/client", "vitest/globals"],
|
||||
"isolatedModules": true,
|
||||
"paths": {
|
||||
"@n8n/utils/*": ["../../../@n8n/utils/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*.ts", "vite.config.ts", "tsdown.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'tsdown';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/**/*.ts', '!src/**/*.test.ts', '!src/**/*.d.ts', '!src/__tests__/**/*'],
|
||||
format: ['cjs', 'esm'],
|
||||
clean: true,
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
hash: false,
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
import { defineConfig, mergeConfig } from 'vite';
|
||||
import { createVitestConfig } from '@n8n/vitest-config/frontend';
|
||||
|
||||
export default mergeConfig(defineConfig({}), createVitestConfig({ setupFiles: [] }));
|
||||
Reference in New Issue
Block a user