feat(engine): add private NDC nodes and ontology bridge

This commit is contained in:
Codex
2026-07-16 02:23:45 +03:00
parent 569b8762e6
commit 59e9c92415
25 changed files with 10450 additions and 12 deletions
@@ -0,0 +1,105 @@
import type {
IExecuteFunctions,
ILoadOptionsFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import {
DATA_PRODUCT_BASE_PATH,
DATA_PLANE_DEFAULT_BASE_URL,
DATA_PLANE_BASE_URL_ENV,
DATA_PRODUCT_WRITER_CATALOG_PATH,
NDC_DATA_PRODUCT_WRITER_CREDENTIAL,
NDC_NODE_ICON,
} from '../shared/constants';
import { buildPublishPayload, encodeIdentifierPath } from '../shared/contracts';
import { safeHttpError, safeInputError } from '../shared/errors';
import { loadDataProductOptions, ndcRequest, serviceBaseUrl } from '../shared/http';
export class NdcDataProductPublish implements INodeType {
description: INodeTypeDescription = {
displayName: 'NDC Data Product Publish',
name: 'ndcDataProductPublish',
icon: NDC_NODE_ICON,
group: ['output'],
version: 1,
subtitle: '={{$parameter["dataProductId"]}}',
description: 'Publish canonical facts through a scoped NDC Data Product grant',
defaults: { name: 'NDC Data Product Publish' },
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [{ name: NDC_DATA_PRODUCT_WRITER_CREDENTIAL, required: true }],
properties: [
{
displayName: 'Data Product Name or ID',
name: 'dataProductId',
type: 'options',
typeOptions: { loadOptionsMethod: 'getDataProducts' },
default: '',
required: true,
},
{
displayName: 'Batch Sequence',
name: 'sequence',
type: 'number',
typeOptions: { minValue: 0, numberStepSize: 1 },
default: 0,
required: true,
description: 'Sequence number when one execution publishes several batches',
},
],
};
methods = {
loadOptions: {
async getDataProducts(this: ILoadOptionsFunctions) {
return loadDataProductOptions(
this,
NDC_DATA_PRODUCT_WRITER_CREDENTIAL,
DATA_PLANE_BASE_URL_ENV,
DATA_PLANE_DEFAULT_BASE_URL,
DATA_PRODUCT_WRITER_CATALOG_PATH,
);
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const inputItems = this.getInputData();
const dataProductId = this.getNodeParameter('dataProductId', 0) as string;
const sequence = this.getNodeParameter('sequence', 0, 0) as number;
let body;
let encodedProductId;
try {
body = buildPublishPayload(
inputItems,
this.getExecutionId(),
String(this.getWorkflow().id ?? 'workflow'),
this.getNode().id,
dataProductId,
sequence,
);
encodedProductId = encodeIdentifierPath(dataProductId, 'dataProductId');
} catch (error) {
throw new NodeOperationError(this.getNode(), safeInputError(error, 'ndc_data_product_publish_input_invalid'));
}
try {
const baseUrl = serviceBaseUrl(DATA_PLANE_BASE_URL_ENV, DATA_PLANE_DEFAULT_BASE_URL);
const response = await ndcRequest(this, NDC_DATA_PRODUCT_WRITER_CREDENTIAL, {
method: 'POST',
url: `${baseUrl}${DATA_PRODUCT_BASE_PATH}/${encodedProductId}/publish`,
body,
});
return [[{
json: response as INodeExecutionData['json'],
pairedItem: inputItems.map((_item, index) => ({ item: index })),
}]];
} catch (error) {
throw new NodeOperationError(this.getNode(), safeHttpError(error, 'ndc_data_product_publish_failed'));
}
}
}
@@ -0,0 +1,99 @@
import type {
IExecuteFunctions,
ILoadOptionsFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import {
DATA_PRODUCT_BASE_PATH,
DATA_PRODUCT_READER_CATALOG_PATH,
DATA_PRODUCT_SNAPSHOT_SUFFIX,
DATA_PLANE_DEFAULT_BASE_URL,
DATA_PLANE_BASE_URL_ENV,
NDC_DATA_PRODUCT_READER_CREDENTIAL,
NDC_NODE_ICON,
} from '../shared/constants';
import { normalizeSnapshotFacts, snapshotReadParameters } from '../shared/contracts';
import { safeHttpError, safeInputError } from '../shared/errors';
import { loadDataProductOptions, ndcRequest, serviceBaseUrl } from '../shared/http';
export class NdcDataProductRead implements INodeType {
description: INodeTypeDescription = {
displayName: 'NDC Data Product Read',
name: 'ndcDataProductRead',
icon: NDC_NODE_ICON,
group: ['input'],
version: 1,
subtitle: '={{$parameter["dataProductId"]}}',
description: 'Read a scoped NDC Data Product snapshot',
defaults: { name: 'NDC Data Product Read' },
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [{ name: NDC_DATA_PRODUCT_READER_CREDENTIAL, required: true }],
properties: [
{
displayName: 'Data Product Name or ID',
name: 'dataProductId',
type: 'options',
typeOptions: { loadOptionsMethod: 'getDataProducts' },
default: '',
required: true,
},
{
displayName: 'Page Size',
name: 'pageSize',
type: 'number',
typeOptions: { minValue: 1, maxValue: 5000, numberStepSize: 1 },
default: 1000,
required: true,
description: 'Maximum number of snapshot facts to return',
},
],
};
methods = {
loadOptions: {
async getDataProducts(this: ILoadOptionsFunctions) {
return loadDataProductOptions(
this,
NDC_DATA_PRODUCT_READER_CREDENTIAL,
DATA_PLANE_BASE_URL_ENV,
DATA_PLANE_DEFAULT_BASE_URL,
DATA_PRODUCT_READER_CATALOG_PATH,
);
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const dataProductId = this.getNodeParameter('dataProductId', 0) as string;
const pageSize = this.getNodeParameter('pageSize', 0, 1000) as number;
let parameters;
try {
parameters = snapshotReadParameters(dataProductId, pageSize);
} catch (error) {
throw new NodeOperationError(this.getNode(), safeInputError(error, 'ndc_data_product_read_input_invalid'));
}
try {
const baseUrl = serviceBaseUrl(DATA_PLANE_BASE_URL_ENV, DATA_PLANE_DEFAULT_BASE_URL);
const response = await ndcRequest(this, NDC_DATA_PRODUCT_READER_CREDENTIAL, {
method: 'GET',
url: `${baseUrl}${DATA_PRODUCT_BASE_PATH}/${parameters.encodedProductId}${DATA_PRODUCT_SNAPSHOT_SUFFIX}`,
qs: {
limit: parameters.pageSize,
},
});
const facts = normalizeSnapshotFacts(response);
return [facts.map((fact) => ({ json: fact, pairedItem: { item: 0 } }))];
} catch (error) {
const safe = error instanceof Error && error.message === 'data_product_snapshot_invalid'
? safeInputError(error, 'ndc_data_product_snapshot_invalid')
: safeHttpError(error, 'ndc_data_product_read_failed');
throw new NodeOperationError(this.getNode(), safe);
}
}
}
@@ -0,0 +1,140 @@
import type {
IExecuteFunctions,
ILoadOptionsFunctions,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionTypes, NodeOperationError } from 'n8n-workflow';
import {
FOUNDRY_BINDING_PATH,
FOUNDRY_BASE_URL_ENV,
FOUNDRY_CATALOG_PATH,
FOUNDRY_DEFAULT_BASE_URL,
NDC_FOUNDRY_BINDING_CREDENTIAL,
NDC_NODE_ICON,
} from '../shared/constants';
import { buildFoundryBindingPayload } from '../shared/contracts';
import { safeHttpError, safeInputError } from '../shared/errors';
import { loadDataProductOptions, ndcRequest, serviceBaseUrl } from '../shared/http';
export class NdcFoundryBinding implements INodeType {
description: INodeTypeDescription = {
displayName: 'NDC Foundry Binding',
name: 'ndcFoundryBinding',
icon: NDC_NODE_ICON,
group: ['output'],
version: 1,
subtitle: '={{$parameter["dataProductId"]}}',
description: 'Bind an approved Data Product to a Foundry page slot through the control plane',
defaults: { name: 'NDC Foundry Binding' },
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
credentials: [{ name: NDC_FOUNDRY_BINDING_CREDENTIAL, required: true }],
properties: [
{
displayName: 'Uses the dedicated Foundry control-plane endpoint and a separate scoped workload grant. It fails closed if either is unavailable and never sends runtime facts or provider credentials to Foundry.',
name: 'availabilityNotice',
type: 'notice',
default: '',
},
{
displayName: 'Application ID',
name: 'applicationId',
type: 'string',
default: '',
required: true,
},
{
displayName: 'Page ID',
name: 'pageId',
type: 'string',
default: '',
required: true,
},
{
displayName: 'Binding ID',
name: 'bindingId',
type: 'string',
default: '',
required: true,
},
{
displayName: 'Data Product Name or ID',
name: 'dataProductId',
type: 'options',
typeOptions: { loadOptionsMethod: 'getDataProducts' },
default: '',
required: true,
},
{
displayName: 'Slot ID',
name: 'slotId',
type: 'string',
default: 'points',
required: true,
},
{
displayName: 'Semantic Types',
name: 'semanticTypes',
type: 'string',
typeOptions: { multipleValues: true, multipleValueButtonText: 'Add Semantic Type' },
default: [],
required: true,
},
{
displayName: 'Field Projection',
name: 'fieldProjection',
type: 'string',
typeOptions: { multipleValues: true, multipleValueButtonText: 'Add Field' },
default: [],
},
],
};
methods = {
loadOptions: {
async getDataProducts(this: ILoadOptionsFunctions) {
return loadDataProductOptions(
this,
NDC_FOUNDRY_BINDING_CREDENTIAL,
FOUNDRY_BASE_URL_ENV,
FOUNDRY_DEFAULT_BASE_URL,
FOUNDRY_CATALOG_PATH,
);
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
let body;
try {
body = buildFoundryBindingPayload({
applicationId: this.getNodeParameter('applicationId', 0) as string,
pageId: this.getNodeParameter('pageId', 0) as string,
binding: {
id: this.getNodeParameter('bindingId', 0) as string,
dataProductId: this.getNodeParameter('dataProductId', 0) as string,
slotId: this.getNodeParameter('slotId', 0) as string,
semanticTypes: this.getNodeParameter('semanticTypes', 0, []) as string[],
fieldProjection: this.getNodeParameter('fieldProjection', 0, []) as string[],
},
});
} catch (error) {
throw new NodeOperationError(this.getNode(), safeInputError(error, 'ndc_foundry_binding_input_invalid'));
}
try {
const baseUrl = serviceBaseUrl(FOUNDRY_BASE_URL_ENV, FOUNDRY_DEFAULT_BASE_URL);
const response = await ndcRequest(this, NDC_FOUNDRY_BINDING_CREDENTIAL, {
method: 'POST',
url: `${baseUrl}${FOUNDRY_BINDING_PATH}`,
body,
});
return [[{ json: response as INodeExecutionData['json'], pairedItem: { item: 0 } }]];
} catch (error) {
throw new NodeOperationError(this.getNode(), safeHttpError(error, 'ndc_foundry_binding_unavailable'));
}
}
}
@@ -0,0 +1,23 @@
export const DATA_PRODUCT_PUBLISH_SCHEMA_VERSION = 'nodedc.data-product.publish/v1';
export const FOUNDRY_BINDING_UPSERT_SCHEMA_VERSION = 'nodedc.foundry.binding-upsert/v1';
export const DATA_PRODUCT_WRITER_CATALOG_PATH = '/internal/data-plane/v1/writer/data-products';
export const DATA_PRODUCT_READER_CATALOG_PATH = '/internal/data-plane/v1/reader/data-products';
export const DATA_PRODUCT_BASE_PATH = '/internal/data-plane/v1/data-products';
export const DATA_PRODUCT_SNAPSHOT_SUFFIX = '/snapshot';
export const FOUNDRY_BINDING_PATH = '/internal/foundry/v1/data-product-bindings';
export const FOUNDRY_CATALOG_PATH = '/internal/foundry/v1/data-products';
export const DATA_PLANE_BASE_URL_ENV = 'NDC_DATA_PLANE_BASE_URL';
export const FOUNDRY_BASE_URL_ENV = 'NDC_FOUNDRY_BASE_URL';
export const DATA_PLANE_DEFAULT_BASE_URL = 'http://external-data-plane:18106';
export const FOUNDRY_DEFAULT_BASE_URL = 'http://nodedc-module-foundry:3333';
export const NDC_DATA_PRODUCT_WRITER_CREDENTIAL = 'ndcDataProductWriterApi';
export const NDC_DATA_PRODUCT_READER_CREDENTIAL = 'ndcDataProductReaderApi';
export const NDC_FOUNDRY_BINDING_CREDENTIAL = 'ndcFoundryBindingApi';
export const NDC_NODE_ICON = {
light: 'file:../../icons/ndc.svg',
dark: 'file:../../icons/ndc.dark.svg',
} as const;
@@ -0,0 +1,238 @@
import { createHash } from 'node:crypto';
import type { IDataObject, INodeExecutionData, INodeListSearchItems } from 'n8n-workflow';
import {
DATA_PRODUCT_PUBLISH_SCHEMA_VERSION,
FOUNDRY_BINDING_UPSERT_SCHEMA_VERSION,
} from './constants';
const IDENTIFIER = /^[a-z][a-z0-9._:-]{2,127}$/;
const MAX_FACTS = 5000;
const MAX_ATTRIBUTES_BYTES = 64 * 1024;
const SECRET_LIKE_KEY = /(token|secret|password|authorization|access[_-]?token|refresh[_-]?token|api[_-]?key)/i;
const SECRET_LIKE_VALUE = /(?:ndc_edp(?:wb|rb)_[A-Za-z0-9_-]*|(?:bearer|basic)\s+\S+|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)/i;
export interface NdcFact extends IDataObject {
sourceId: string;
semanticType: string;
observedAt: string;
attributes?: IDataObject;
geometry?: {
type: 'Point';
coordinates: [number, number];
};
}
export interface NdcPublishPayload extends IDataObject {
schemaVersion: typeof DATA_PRODUCT_PUBLISH_SCHEMA_VERSION;
batch: {
runId: string;
sequence: number;
idempotencyKey: string;
};
facts: NdcFact[];
}
export interface NdcFoundryBindingInput extends IDataObject {
applicationId: string;
pageId: string;
binding: {
id: string;
dataProductId: string;
slotId: string;
semanticTypes: string[];
fieldProjection: string[];
};
}
export function normalizeBaseUrl(value: unknown): string {
const normalized = String(value ?? '').trim().replace(/\/+$/, '');
if (!/^https?:\/\/[^\s]+$/i.test(normalized)) throw new Error('ndc_internal_api_base_url_invalid');
return normalized;
}
export function requireIdentifier(value: unknown, field: string): string {
const normalized = String(value ?? '').trim();
if (!IDENTIFIER.test(normalized)) throw new Error(`${field}_invalid`);
return normalized;
}
export function encodeIdentifierPath(value: unknown, field: string): string {
return encodeURIComponent(requireIdentifier(value, field));
}
export function factsFromItems(items: INodeExecutionData[]): NdcFact[] {
if (!items.length) throw new Error('facts_required');
if (items.length > MAX_FACTS) throw new Error('facts_limit_exceeded');
const entityKeys = new Set<string>();
return items.map((item, index) => {
const source = isObject(item.json.fact) ? item.json.fact : item.json;
const fact: NdcFact = {
sourceId: requireIdentifier(source.sourceId, `facts_${index}_sourceId`),
semanticType: requireIdentifier(source.semanticType, `facts_${index}_semanticType`),
observedAt: requireIsoTimestamp(source.observedAt, `facts_${index}_observedAt`),
};
const entityKey = `${fact.sourceId}\u0000${fact.semanticType}`;
if (entityKeys.has(entityKey)) throw new Error('facts_duplicate_entity_key');
entityKeys.add(entityKey);
if (source.attributes !== undefined) {
if (!isObject(source.attributes)) throw new Error(`facts_${index}_attributes_invalid`);
if (Buffer.byteLength(JSON.stringify(source.attributes)) > MAX_ATTRIBUTES_BYTES) {
throw new Error(`facts_${index}_attributes_size_exceeded`);
}
if (containsSecretLikeMaterial(source.attributes)) throw new Error(`facts_${index}_attributes_secret_material_forbidden`);
fact.attributes = source.attributes;
}
if (source.geometry !== undefined) fact.geometry = normalizePoint(source.geometry, index);
return fact;
});
}
export function buildPublishPayload(
items: INodeExecutionData[],
executionId: string,
workflowId: string,
nodeId: string,
dataProductId: string,
sequence: number,
): NdcPublishPayload {
if (!Number.isInteger(sequence) || sequence < 0 || sequence > 2_147_483_647) throw new Error('batch_sequence_invalid');
requireIdentifier(dataProductId, 'dataProductId');
const runId = `run-${sha256(`${workflowId}|${executionId}`).slice(0, 48)}`;
const idempotencyKey = `publish-${sha256(
`${workflowId}|${executionId}|${nodeId}|${dataProductId}|${sequence}`,
)}`;
return {
schemaVersion: DATA_PRODUCT_PUBLISH_SCHEMA_VERSION,
batch: { runId, sequence, idempotencyKey },
facts: factsFromItems(items),
};
}
export function normalizeSnapshotFacts(value: unknown): IDataObject[] {
if (!isObject(value) || !Array.isArray(value.facts)) throw new Error('data_product_snapshot_invalid');
return value.facts.map((fact, index) => {
if (!isObject(fact)) throw new Error(`snapshot_fact_${index}_invalid`);
return fact;
});
}
export function snapshotReadParameters(
dataProductId: unknown,
pageSize: unknown,
): { encodedProductId: string; pageSize: number } {
const normalizedPageSize = Number(pageSize);
if (!Number.isInteger(normalizedPageSize) || normalizedPageSize < 1 || normalizedPageSize > 5000) {
throw new Error('page_size_invalid');
}
return {
encodedProductId: encodeIdentifierPath(dataProductId, 'dataProductId'),
pageSize: normalizedPageSize,
};
}
export function dataProductOptions(value: unknown): INodeListSearchItems[] {
if (!isObject(value)) throw new Error('data_product_catalog_invalid');
const rows = Array.isArray(value.dataProducts)
? value.dataProducts
: Array.isArray(value.products)
? value.products
: [];
return rows.flatMap((row) => {
if (typeof row === 'string') {
return IDENTIFIER.test(row) ? [{ name: row, value: row }] : [];
}
if (!isObject(row)) return [];
const id = String(row.id ?? row.dataProductId ?? '').trim();
if (!IDENTIFIER.test(id)) return [];
const version = String(row.version ?? '').trim();
const label = String(row.label ?? row.name ?? id).trim() || id;
return [{ name: version ? `${label} · ${version}` : label, value: id }];
});
}
export function buildFoundryBindingPayload(input: NdcFoundryBindingInput): IDataObject {
const applicationId = requireFoundryApplicationId(input.applicationId);
const pageId = requireFoundryPageId(input.pageId);
const bindingId = requireIdentifier(input.binding.id, 'binding_id');
const dataProductId = requireIdentifier(input.binding.dataProductId, 'dataProductId');
const slotId = requireFoundrySlotId(input.binding.slotId);
const semanticTypes = uniqueIdentifiers(input.binding.semanticTypes, 'semanticTypes');
if (!semanticTypes.length) throw new Error('semanticTypes_required');
const fieldProjection = uniqueIdentifiers(input.binding.fieldProjection, 'fieldProjection', true);
const binding = { id: bindingId, dataProductId, slotId, semanticTypes, fieldProjection };
const digest = createHash('sha256')
.update(JSON.stringify({ applicationId, pageId, binding }))
.digest('hex')
.slice(0, 32);
return {
schemaVersion: FOUNDRY_BINDING_UPSERT_SCHEMA_VERSION,
applicationId,
pageId,
idempotencyKey: `foundry-binding-${digest}`,
binding,
};
}
function requireFoundryApplicationId(value: unknown): string {
const normalized = String(value ?? '').trim();
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(normalized)) {
throw new Error('applicationId_invalid');
}
return normalized;
}
function requireFoundryPageId(value: unknown): string {
const normalized = String(value ?? '').trim();
if (!/^[a-z0-9][a-z0-9-]{0,79}$/.test(normalized)) throw new Error('pageId_invalid');
return normalized;
}
function requireFoundrySlotId(value: unknown): string {
const normalized = String(value ?? '').trim();
if (!/^[A-Za-z0-9][A-Za-z0-9-]{0,79}$/.test(normalized)) throw new Error('slotId_invalid');
return normalized;
}
function normalizePoint(value: unknown, index: number): NdcFact['geometry'] {
if (!isObject(value) || value.type !== 'Point' || !Array.isArray(value.coordinates) || value.coordinates.length !== 2) {
throw new Error(`facts_${index}_geometry_invalid`);
}
const [longitude, latitude] = value.coordinates;
if (![longitude, latitude].every((coordinate) => typeof coordinate === 'number' && Number.isFinite(coordinate))) {
throw new Error(`facts_${index}_geometry_coordinates_invalid`);
}
if ((longitude as number) < -180 || (longitude as number) > 180 || (latitude as number) < -90 || (latitude as number) > 90) {
throw new Error(`facts_${index}_geometry_coordinates_out_of_range`);
}
return { type: 'Point', coordinates: [longitude as number, latitude as number] };
}
function requireIsoTimestamp(value: unknown, field: string): string {
const normalized = String(value ?? '').trim();
if (!normalized || Number.isNaN(Date.parse(normalized))) throw new Error(`${field}_invalid`);
return normalized;
}
function uniqueIdentifiers(value: unknown, field: string, allowEmpty = false): string[] {
if (!Array.isArray(value)) throw new Error(`${field}_invalid`);
const normalized = [...new Set(value.map((item) => requireIdentifier(item, field)))];
if (!allowEmpty && !normalized.length) throw new Error(`${field}_required`);
return normalized;
}
function sha256(value: string): string {
return createHash('sha256').update(value).digest('hex');
}
function containsSecretLikeMaterial(value: unknown): boolean {
if (typeof value === 'string') return SECRET_LIKE_VALUE.test(value);
if (Array.isArray(value)) return value.some(containsSecretLikeMaterial);
if (!isObject(value)) return false;
return Object.entries(value).some(([key, child]) => SECRET_LIKE_KEY.test(key) || containsSecretLikeMaterial(child));
}
function isObject(value: unknown): value is IDataObject {
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
}
@@ -0,0 +1,17 @@
export function safeInputError(error: unknown, fallback: string): Error {
const message = error instanceof Error ? error.message : '';
const normalized = /^[a-z0-9_.:-]{3,160}$/i.test(message) ? message : fallback;
return new Error(normalized);
}
export function safeHttpError(error: unknown, fallback: string): Error {
const source = error && typeof error === 'object' ? error as Record<string, unknown> : {};
const response = source.response && typeof source.response === 'object'
? source.response as Record<string, unknown>
: {};
const candidate = Number(source.statusCode ?? source.status ?? response.statusCode ?? response.status);
const suffix = Number.isInteger(candidate) && candidate >= 100 && candidate <= 599
? `_http_${candidate}`
: '';
return new Error(`${fallback}${suffix}`);
}
@@ -0,0 +1,40 @@
import type {
IExecuteFunctions,
ILoadOptionsFunctions,
IHttpRequestOptions,
INodeListSearchItems,
} from 'n8n-workflow';
import { dataProductOptions, normalizeBaseUrl } from './contracts';
type NdcHttpContext = IExecuteFunctions | ILoadOptionsFunctions;
export function serviceBaseUrl(environmentVariable: string, defaultBaseUrl: string): string {
return normalizeBaseUrl(process.env[environmentVariable] || defaultBaseUrl);
}
export async function ndcRequest(
context: NdcHttpContext,
credentialName: string,
options: IHttpRequestOptions,
): Promise<unknown> {
return context.helpers.httpRequestWithAuthentication.call(context, credentialName, {
...options,
json: true,
});
}
export async function loadDataProductOptions(
context: ILoadOptionsFunctions,
credentialName: string,
baseUrlEnvironmentVariable: string,
defaultBaseUrl: string,
catalogPath: string,
): Promise<INodeListSearchItems[]> {
const baseUrl = serviceBaseUrl(baseUrlEnvironmentVariable, defaultBaseUrl);
const response = await ndcRequest(context, credentialName, {
method: 'GET',
url: `${baseUrl}${catalogPath}`,
});
return dataProductOptions(response);
}