diff --git a/credentials/S4DSApi.credentials.ts b/credentials/S4DSApi.credentials.ts index 034abb2..1135de3 100644 --- a/credentials/S4DSApi.credentials.ts +++ b/credentials/S4DSApi.credentials.ts @@ -14,7 +14,7 @@ export class S4DSApi implements ICredentialType { type: 'options', options: [ { - name: 'Demo Test', + name: 'Demo TEST', value: 'https://demotest.s4ds.com/demoapi-test', }, { @@ -22,32 +22,20 @@ export class S4DSApi implements ICredentialType { value: 'https://demouat.s4ds.com/demoapi-uat', }, { - name: 'Demo Production', - value: 'https://demoprod.s4ds.com/demoapi-prod', + name: 'Demo CORE', + value: 'https://demo.s4ds.com/demoapi-core', }, { - name: 'Cliente 1 Test', - value: 'https://cliente1test.s4ds.com/cliente1api-test', + name: 'Aquasource TEST', + value: 'https://aquasourcetest.s4ds.com/aquasourceapi-test', }, { - name: 'Cliente 1 UAT', - value: 'https://cliente1uat.s4ds.com/cliente1api-uat', + name: 'Aquasource UAT', + value: 'https://aquasourceuat.s4ds.com/aquasourceapi-uat', }, { - name: 'Cliente 1 Production', - value: 'https://cliente1prod.s4ds.com/cliente1api-prod', - }, - { - name: 'Cliente 2 Test', - value: 'https://cliente2test.s4ds.com/cliente2api-test', - }, - { - name: 'Cliente 2 UAT', - value: 'https://cliente2uat.s4ds.com/cliente2api-uat', - }, - { - name: 'Cliente 2 Production', - value: 'https://cliente2prod.s4ds.com/cliente2api-prod', + name: 'Aquasource CORE', + value: 'https://aquasource.s4ds.com/aquasourceapi-core', }, { name: 'Custom URL', diff --git a/index.js b/index.js index fbb47e8..0cead78 100644 --- a/index.js +++ b/index.js @@ -1,9 +1,8 @@ -const { S4DSAuth } = require('./dist/nodes/S4DSAuth/S4DSAuth.node.js'); -const { S4DSExample } = require('./dist/nodes/S4DSExample/S4DSExample.node.js'); +const { S4DSMain } = require('./dist/nodes/S4DSMain/S4DSMain.node.js'); const { S4DSApi } = require('./dist/credentials/S4DSApi.credentials.js'); module.exports = { - nodes: [S4DSAuth, S4DSExample], + nodes: [S4DSMain], credentials: [S4DSApi], }; diff --git a/nodes/S4DSAuth/AuthDescription.ts b/nodes/S4DSAuth/AuthDescription.ts deleted file mode 100644 index 526b999..0000000 --- a/nodes/S4DSAuth/AuthDescription.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { INodeProperties } from 'n8n-workflow'; - -// Operaciones disponibles para el recurso de autenticación -export const authOperations: INodeProperties[] = [ - { - displayName: 'Operation', - name: 'operation', - type: 'options', - noDataExpression: true, - displayOptions: { - show: { - resource: ['auth'], - }, - }, - options: [ - { - name: 'Generate Token', - value: 'generateToken', - description: 'Generate authentication token using credentials', - action: 'Generate authentication token', - routing: { - request: { - method: 'POST', - url: '/login/generateToken', - body: { - username: '={{$credentials.s4dsApi.username}}', - password: '={{$credentials.s4dsApi.password}}', - }, - }, - }, - }, - { - name: 'Validate Token', - value: 'validateToken', - description: 'Validate existing token', - action: 'Validate existing token', - routing: { - request: { - method: 'GET', - url: '/auth/validate', - }, - }, - }, - ], - default: 'generateToken', - }, -]; - -// Campos para la operación de generación de token -const generateTokenOperation: INodeProperties[] = [ - { - displayName: 'Additional Fields', - name: 'additionalFields', - type: 'collection', - placeholder: 'Add Field', - default: {}, - displayOptions: { - show: { - resource: ['auth'], - operation: ['generateToken'], - }, - }, - options: [ - { - displayName: 'Store Token in Context', - name: 'storeInContext', - type: 'boolean', - default: true, - description: 'Whether to store the token in the workflow context for other nodes to use', - }, - { - displayName: 'Token Context Key', - name: 'tokenContextKey', - type: 'string', - default: 's4ds_token', - description: 'Key to store the token in the workflow context', - displayOptions: { - show: { - storeInContext: [true], - }, - }, - }, - ], - }, -]; - -// Campos para la operación de validación de token -const validateTokenOperation: INodeProperties[] = [ - { - displayName: 'Token', - name: 'token', - type: 'string', - required: true, - default: '', - displayOptions: { - show: { - resource: ['auth'], - operation: ['validateToken'], - }, - }, - description: 'Token to validate', - }, -]; - -// Exportar todos los campos -export const authFields: INodeProperties[] = [ - ...generateTokenOperation, - ...validateTokenOperation, -]; \ No newline at end of file diff --git a/nodes/S4DSAuth/S4DSAuth.node.ts b/nodes/S4DSAuth/S4DSAuth.node.ts deleted file mode 100644 index dac17aa..0000000 --- a/nodes/S4DSAuth/S4DSAuth.node.ts +++ /dev/null @@ -1,168 +0,0 @@ -import { INodeType, INodeTypeDescription, NodeConnectionType, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; -import { authOperations, authFields } from './AuthDescription'; - -export class S4DSAuth implements INodeType { - description: INodeTypeDescription = { - displayName: 'S4DS Authentication', - name: 's4dsAuth', - icon: { light: 'file:s4ds.svg', dark: 'file:s4ds.svg' }, - group: ['transform'], - version: 1, - subtitle: '={{$parameter["operation"]}}', - description: 'Authenticate with S4DS API and manage tokens', - defaults: { - name: 'S4DS Auth', - }, - inputs: [NodeConnectionType.Main], - outputs: [NodeConnectionType.Main], - usableAsTool: true, - credentials: [ - { - name: 's4dsApi', - required: true, - }, - ], - requestDefaults: { - baseURL: '={{$credentials.s4dsApi.baseUrl === "custom" ? $credentials.s4dsApi.customBaseUrl : $credentials.s4dsApi.baseUrl}}', - url: '', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - }, - properties: [ - { - displayName: 'Resource', - name: 'resource', - type: 'options', - noDataExpression: true, - options: [ - { - name: 'Authentication', - value: 'auth', - }, - ], - default: 'auth', - }, - ...authOperations, - ...authFields, - ], - }; - - async execute(this: IExecuteFunctions): Promise { - const items = this.getInputData(); - const returnData: INodeExecutionData[] = []; - - for (let i = 0; i < items.length; i++) { - try { - const resource = this.getNodeParameter('resource', i) as string; - const operation = this.getNodeParameter('operation', i) as string; - - if (resource === 'auth') { - if (operation === 'generateToken') { - // Obtener credenciales - const credentials = await this.getCredentials('s4dsApi'); - - // Determinar la URL base - let baseUrl = credentials.baseUrl; - if (baseUrl === 'custom') { - baseUrl = credentials.customBaseUrl; - } - - if (!baseUrl) { - throw new Error('Base URL is required. Please configure the S4DS API credentials.'); - } - - // Ejecutar la petición HTTP para generar token - const response = await this.helpers.httpRequest({ - method: 'POST', - url: `${baseUrl}/login/generateToken`, - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - }, - body: { - username: credentials.username, - password: credentials.password, - }, - }); - - // Almacenar token en contexto - const additionalFields = this.getNodeParameter('additionalFields', i) as { - storeInContext?: boolean; - tokenContextKey?: string; - }; - - const contextKey = additionalFields.tokenContextKey || 's4ds_token'; - const tokenData = { - token: response.token, - token_type: response.token_type, - expires_in: response.expires_in, - expires_at: new Date(Date.now() + response.expires_in * 1000).toISOString(), - authorization_header: `${response.token_type} ${response.token}`, - }; - - // Almacenar en contexto del workflow - this.getWorkflowStaticData('global')[contextKey] = tokenData; - - // Retornar respuesta sin el token por seguridad - const secureResponse = { - success: true, - token_type: response.token_type, - expires_in: response.expires_in, - expires_at: tokenData.expires_at, - message: 'Token generated successfully and stored in workflow context', - context_key: contextKey, - }; - - returnData.push({ - json: secureResponse, - }); - } else if (operation === 'validateToken') { - const token = this.getNodeParameter('token', i) as string; - - // Obtener credenciales - const credentials = await this.getCredentials('s4dsApi'); - - // Determinar la URL base - let baseUrl = credentials.baseUrl; - if (baseUrl === 'custom') { - baseUrl = credentials.customBaseUrl; - } - - if (!baseUrl) { - throw new Error('Base URL is required. Please configure the S4DS API credentials.'); - } - - // Ejecutar validación de token - const response = await this.helpers.httpRequest({ - method: 'GET', - url: `${baseUrl}/auth/validate`, - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}`, - }, - }); - - returnData.push({ - json: response, - }); - } - } - } catch (error) { - if (this.continueOnFail()) { - returnData.push({ - json: { - error: error.message, - }, - }); - continue; - } - throw error; - } - } - - return [returnData]; - } -} \ No newline at end of file diff --git a/nodes/S4DSAuth/S4DSHelper.ts b/nodes/S4DSAuth/S4DSHelper.ts deleted file mode 100644 index 56e5432..0000000 --- a/nodes/S4DSAuth/S4DSHelper.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { IExecuteFunctions } from 'n8n-workflow'; - -export interface S4DSTokenData { - token: string; - token_type: string; - expires_in: number; - expires_at: string; - authorization_header: string; -} - -export class S4DSHelper { - /** - * Obtiene el token de autorización del contexto del workflow - * @param this - Contexto de ejecución del nodo - * @param contextKey - Clave del contexto donde está almacenado el token (por defecto: 's4ds_token') - * @returns Objeto con datos del token incluyendo el header de autorización - */ - static getTokenFromContext(this: IExecuteFunctions, contextKey: string = 's4ds_token'): S4DSTokenData { - const tokenData = this.getWorkflowStaticData('global')[contextKey] as S4DSTokenData; - - if (!tokenData || !tokenData.token) { - throw new Error(`No S4DS token found in context with key: ${contextKey}. Please run S4DS Auth node first.`); - } - - // Verificar si el token ha expirado - if (tokenData.expires_at && new Date(tokenData.expires_at) <= new Date()) { - throw new Error('S4DS token has expired. Please regenerate token using S4DS Auth node.'); - } - - return tokenData; - } - - /** - * Obtiene el header de autorización listo para usar - * @param this - Contexto de ejecución del nodo - * @param contextKey - Clave del contexto donde está almacenado el token (por defecto: 's4ds_token') - * @returns Header de autorización completo (ej: "Bearer 8a6c71b3-fa62-434d-8b38-907de24c3176") - */ - static getAuthorizationHeader(this: IExecuteFunctions, contextKey: string = 's4ds_token'): string { - const tokenData = S4DSHelper.getTokenFromContext.call(this, contextKey); - return tokenData.authorization_header; - } - - /** - * Obtiene headers HTTP completos con autorización - * @param this - Contexto de ejecución del nodo - * @param contextKey - Clave del contexto donde está almacenado el token (por defecto: 's4ds_token') - * @param additionalHeaders - Headers adicionales opcionales - * @returns Objeto con headers HTTP completos - */ - static getHeadersWithAuth( - this: IExecuteFunctions, - contextKey: string = 's4ds_token', - additionalHeaders: Record = {} - ): Record { - const authHeader = S4DSHelper.getAuthorizationHeader.call(this, contextKey); - - return { - Authorization: authHeader, - Accept: 'application/json', - 'Content-Type': 'application/json', - ...additionalHeaders, - }; - } - - /** - * Verifica si existe un token válido en el contexto - * @param this - Contexto de ejecución del nodo - * @param contextKey - Clave del contexto donde está almacenado el token (por defecto: 's4ds_token') - * @returns true si existe un token válido, false en caso contrario - */ - static hasValidToken(this: IExecuteFunctions, contextKey: string = 's4ds_token'): boolean { - try { - S4DSHelper.getTokenFromContext.call(this, contextKey); - return true; - } catch { - return false; - } - } -} \ No newline at end of file diff --git a/nodes/S4DSExample/S4DSExample.node.ts b/nodes/S4DSExample/S4DSExample.node.ts deleted file mode 100644 index 012829b..0000000 --- a/nodes/S4DSExample/S4DSExample.node.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { INodeType, INodeTypeDescription, NodeConnectionType, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; -import { S4DSHelper } from '../S4DSAuth/S4DSHelper'; - -export class S4DSExample implements INodeType { - description: INodeTypeDescription = { - displayName: 'S4DS Example', - name: 's4dsExample', - icon: { light: 'file:s4ds.svg', dark: 'file:s4ds.svg' }, - group: ['transform'], - version: 1, - subtitle: '={{$parameter["operation"]}}', - description: 'Example node that uses S4DS authentication token', - defaults: { - name: 'S4DS Example', - }, - inputs: [NodeConnectionType.Main], - outputs: [NodeConnectionType.Main], - credentials: [ - { - name: 's4dsApi', - required: true, - }, - ], - requestDefaults: { - baseURL: '={{$credentials.s4dsApi.baseUrl}}', - url: '', - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - }, - }, - properties: [ - { - displayName: 'Operation', - name: 'operation', - type: 'options', - noDataExpression: true, - options: [ - { - name: 'Get Product Count', - value: 'getProductCount', - description: 'Get the total count of products', - action: 'Get product count', - routing: { - request: { - method: 'GET', - url: '/product/count', - }, - }, - }, - ], - default: 'getProductCount', - }, - { - displayName: 'Token Source', - name: 'tokenSource', - type: 'options', - required: true, - default: 'context', - options: [ - { - name: 'From Context', - value: 'context', - description: 'Use token stored in workflow context', - }, - { - name: 'Custom Token', - value: 'custom', - description: 'Provide custom token', - }, - ], - }, - { - displayName: 'Context Token Key', - name: 'contextTokenKey', - type: 'string', - default: 's4ds_token', - required: true, - displayOptions: { - show: { - tokenSource: ['context'], - }, - }, - description: 'Key used to store token in workflow context', - }, - { - displayName: 'Custom Token', - name: 'customToken', - type: 'string', - default: '', - required: true, - displayOptions: { - show: { - tokenSource: ['custom'], - }, - }, - description: 'Custom authentication token', - }, - ], - }; - - async execute(this: IExecuteFunctions): Promise { - const items = this.getInputData(); - const returnData: INodeExecutionData[] = []; - - for (let i = 0; i < items.length; i++) { - try { - const operation = this.getNodeParameter('operation', i) as string; - const tokenSource = this.getNodeParameter('tokenSource', i) as string; - - // Obtener headers según la fuente - let headers: Record; - - if (tokenSource === 'context') { - const contextKey = this.getNodeParameter('contextTokenKey', i) as string; - - // Debug: Verificar qué hay en el contexto - const contextData = this.getWorkflowStaticData('global'); - const availableKeys = Object.keys(contextData); - - // Usar el helper para obtener headers con autorización - try { - headers = S4DSHelper.getHeadersWithAuth.call(this, contextKey); - } catch (error) { - // Si falla, retornar información de debug - returnData.push({ - json: { - error: error.message, - debug_info: { - context_key_requested: contextKey, - available_context_keys: availableKeys, - context_data: contextData, - message: 'Please ensure S4DS Auth node runs before this node and generates a token successfully.' - } - }, - }); - continue; - } - } else { - const customToken = this.getNodeParameter('customToken', i) as string; - headers = { - Authorization: customToken, - Accept: 'application/json', - 'Content-Type': 'application/json', - }; - } - - // Ejecutar la operación - let response; - if (operation === 'getProductCount') { - // Obtener credenciales para construir la URL base - const credentials = await this.getCredentials('s4dsApi'); - - // Determinar la URL base - let baseUrl = credentials.baseUrl; - if (baseUrl === 'custom') { - baseUrl = credentials.customBaseUrl; - } - - if (!baseUrl) { - throw new Error('Base URL is required. Please configure the S4DS API credentials.'); - } - - response = await this.helpers.httpRequest({ - method: 'GET', - url: `${baseUrl}/product/count`, - headers, - }); - } - - returnData.push({ - json: { - ...response, - _token_info: { - source: tokenSource, - }, - }, - }); - } catch (error) { - if (this.continueOnFail()) { - returnData.push({ - json: { - error: error.message, - }, - }); - continue; - } - throw error; - } - } - - return [returnData]; - } -} \ No newline at end of file diff --git a/nodes/S4DSMain/ApiHelper.ts b/nodes/S4DSMain/ApiHelper.ts new file mode 100644 index 0000000..a1b732d --- /dev/null +++ b/nodes/S4DSMain/ApiHelper.ts @@ -0,0 +1,369 @@ +import { INodeProperties, NodePropertyTypes } from 'n8n-workflow'; +import * as apiDefinitions from './api-definitions.json'; +import * as dtoDefinitions from './dto-definitions.json'; + +export interface ApiDefinition { + method: string; + endpoint: string; + description: string; + parameters: ApiParameter[]; + requiresAuth: boolean; + requestBody?: { + schema: string; + required: boolean; + }; + response: { + type: string; + properties: Record; + }; +} + +export interface ApiParameter { + name: string; + type: string; + required: boolean; + description: string; + in: 'query' | 'body' | 'path'; +} + +export interface DtoProperty { + type: string; + description: string; + required: boolean; + schema?: string; + items?: { + type: string; + schema?: string; + }; +} + +export interface DtoDefinition { + type: string; + properties: Record; +} + +export class ApiHelper { + static getApiDefinitions(): Record> { + return apiDefinitions as any; + } + + static getResources(): INodeProperties[] { + const definitions = this.getApiDefinitions(); + const resources = Object.keys(definitions).map(resource => ({ + name: this.capitalizeFirst(resource), + value: resource, + description: `${resource} operations`, + })); + + return [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: resources, + default: resources[0]?.value || 'authentication', + }, + ]; + } + + static getOperations(): INodeProperties[] { + const definitions = this.getApiDefinitions(); + const operations: INodeProperties[] = []; + + // Crear operaciones para cada recurso + Object.keys(definitions).forEach(resource => { + const resourceOperations = Object.keys(definitions[resource]).map(operation => ({ + name: this.formatOperationName(operation), + value: operation, + description: definitions[resource][operation].description, + action: this.formatOperationName(operation), + })); + + operations.push({ + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: [resource], + }, + }, + options: resourceOperations, + default: resourceOperations[0]?.value || '', + }); + }); + + return operations; + } + + static getFields(): INodeProperties[] { + const definitions = this.getApiDefinitions(); + const fields: INodeProperties[] = []; + + // Ya no agregamos campos de autenticación + + Object.keys(definitions).forEach(resource => { + Object.keys(definitions[resource]).forEach(operation => { + const apiDef = definitions[resource][operation]; + + // Campos específicos de la operación + if (apiDef.parameters && apiDef.parameters.length > 0) { + fields.push(...this.getOperationFields(resource, operation, apiDef.parameters)); + } + + // Campos para request body con DTOs + if (apiDef.requestBody && apiDef.requestBody.schema) { + fields.push(...this.getRequestBodyFields(resource, operation, apiDef.requestBody.schema)); + } + }); + }); + + return fields; + } + + private static getOperationFields(resource: string, operation: string, parameters: ApiParameter[]): INodeProperties[] { + const fields: INodeProperties[] = []; + + // Agrupar parámetros por tipo + const queryParams = parameters.filter(p => p.in === 'query'); + const bodyParams = parameters.filter(p => p.in === 'body'); + + // Campos para parámetros de query + if (queryParams.length > 0) { + fields.push({ + displayName: 'Query Parameters', + name: 'queryParameters', + type: 'collection', + placeholder: 'Add Query Parameter', + default: {}, + displayOptions: { + show: { + resource: [resource], + operation: [operation], + }, + }, + options: queryParams.map(param => ({ + displayName: this.capitalizeFirst(param.name), + name: param.name, + type: this.mapParameterType(param.type), + default: '', + required: param.required, + description: param.description, + })), + }); + } + + // Campos para parámetros de body + if (bodyParams.length > 0) { + fields.push({ + displayName: 'Body Parameters', + name: 'bodyParameters', + type: 'collection', + placeholder: 'Add Body Parameter', + default: {}, + displayOptions: { + show: { + resource: [resource], + operation: [operation], + }, + }, + options: bodyParams.map(param => ({ + displayName: this.capitalizeFirst(param.name), + name: param.name, + type: this.mapParameterType(param.type), + default: '', + required: param.required, + description: param.description, + })), + }); + } + + return fields; + } + + private static getRequestBodyFields(resource: string, operation: string, dtoSchema: string): INodeProperties[] { + const example = this.generateDtoExample(dtoSchema); + return [ + { + displayName: 'Request Body (JSON)', + name: 'requestBody', + type: 'json', + default: JSON.stringify(example, null, 2), + required: false, + description: 'Pega aquí el JSON completo según el esquema del DTO.', + displayOptions: { + show: { + resource: [resource], + operation: [operation], + }, + }, + }, + ]; + } + + // Helper para generar un ejemplo de JSON para un DTO + private static generateDtoExample(dtoName: string): any { + const dtoDef = this.getDtoDefinition(dtoName); + if (!dtoDef) return {}; + const result: any = {}; + Object.keys(dtoDef.properties).forEach(propName => { + const prop = dtoDef.properties[propName]; + if (prop.type === 'object' && prop.schema) { + result[propName] = this.generateDtoExample(prop.schema); + } else if (prop.type === 'object') { + result[propName] = {}; + } else if (prop.type === 'array' && prop.items?.schema) { + result[propName] = [this.generateDtoExample(prop.items.schema)]; + } else if (prop.type === 'array') { + result[propName] = []; + } else if (prop.type === 'boolean') { + result[propName] = false; + } else if (prop.type === 'number' || prop.type === 'integer') { + result[propName] = 0; + } else { + result[propName] = ''; + } + }); + return result; + } + + private static mapParameterType(apiType: string): NodePropertyTypes { + switch (apiType.toLowerCase()) { + case 'string': + return 'string'; + case 'number': + case 'integer': + return 'number'; + case 'boolean': + return 'boolean'; + case 'array': + return 'fixedCollection'; + case 'object': + return 'json'; + default: + return 'string'; + } + } + + private static capitalizeFirst(str: string): string { + return str.charAt(0).toUpperCase() + str.slice(1); + } + + private static formatOperationName(operation: string): string { + // Convertir camelCase a palabras separadas + return operation + .replace(/([A-Z])/g, ' $1') + .replace(/^./, str => str.toUpperCase()) + .trim(); + } + + static getApiDefinition(resource: string, operation: string): ApiDefinition | null { + const definitions = this.getApiDefinitions(); + return definitions[resource]?.[operation] || null; + } + + static getDtoDefinitions(): Record { + return dtoDefinitions as any; + } + + static getDtoDefinition(dtoName: string): DtoDefinition | null { + const definitions = this.getDtoDefinitions(); + return definitions[dtoName] || null; + } + + static generateDtoFields(dtoName: string, prefix: string = ''): INodeProperties[] { + const dtoDef = this.getDtoDefinition(dtoName); + if (!dtoDef) return []; + + const fields: INodeProperties[] = []; + + Object.keys(dtoDef.properties).forEach(propName => { + const prop = dtoDef.properties[propName]; + const fieldName = prefix ? `${prefix}.${propName}` : propName; + const displayName = this.capitalizeFirst(propName); + + if (prop.type === 'object' && prop.schema) { + // Campo de objeto anidado + fields.push({ + displayName: `${displayName} (${prop.schema})`, + name: fieldName, + type: 'collection', + placeholder: `Add ${displayName}`, + default: {}, + required: prop.required, + description: prop.description, + options: this.generateDtoFields(prop.schema, fieldName), + }); + } else if (prop.type === 'array' && prop.items?.schema) { + // Campo de array de objetos + fields.push({ + displayName: `${displayName} (Array of ${prop.items.schema})`, + name: fieldName, + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + placeholder: `Add ${displayName} Item`, + default: {}, + required: prop.required, + description: prop.description, + options: [ + { + displayName: prop.items.schema, + name: 'item', + values: this.generateDtoFields(prop.items.schema, `${fieldName}.item`), + }, + ], + }); + } else { + // Campo simple + fields.push({ + displayName, + name: fieldName, + type: this.mapParameterType(prop.type), + default: '', + required: prop.required, + description: prop.description, + }); + } + }); + + return fields; + } + + static buildDtoObject(data: Record, dtoSchema: string): any { + const dtoDef = this.getDtoDefinition(dtoSchema); + if (!dtoDef) return data; + + const result: any = {}; + + Object.keys(dtoDef.properties).forEach(propName => { + const prop = dtoDef.properties[propName]; + const value = data[propName]; + + if (value !== undefined && value !== '') { + if (prop.type === 'object' && prop.schema) { + // Objeto anidado + result[propName] = this.buildDtoObject(value, prop.schema); + } else if (prop.type === 'array' && prop.items?.schema) { + // Array de objetos + if (Array.isArray(value)) { + result[propName] = value.map(item => this.buildDtoObject(item, prop.items!.schema!)); + } else if (value && typeof value === 'object') { + // Para fixedCollection de n8n + result[propName] = Object.values(value).map((item: any) => + this.buildDtoObject(item, prop.items!.schema!) + ); + } + } else { + // Campo simple + result[propName] = value; + } + } + }); + + return result; + } +} \ No newline at end of file diff --git a/nodes/S4DSMain/S4DSMain.node.ts b/nodes/S4DSMain/S4DSMain.node.ts new file mode 100644 index 0000000..02d4fcd --- /dev/null +++ b/nodes/S4DSMain/S4DSMain.node.ts @@ -0,0 +1,196 @@ +import { INodeType, INodeTypeDescription, NodeConnectionType, IExecuteFunctions, INodeExecutionData, IHttpRequestMethods } from 'n8n-workflow'; +import { ApiHelper } from './ApiHelper'; + +export class S4DSMain implements INodeType { + description: INodeTypeDescription = { + displayName: 'S4DS', + name: 's4ds', + icon: { light: 'file:logo_generic.png', dark: 'file:logo_generic.png' }, + group: ['transform'], + version: 1, + subtitle: '={{$parameter["resource"]}} - {{$parameter["operation"]}}', + description: 'S4DS API operations including authentication and product management', + defaults: { + name: 'S4DS', + }, + inputs: [NodeConnectionType.Main], + outputs: [NodeConnectionType.Main], + credentials: [ + { + name: 's4dsApi', + required: true, + }, + ], + requestDefaults: { + baseURL: '={{$credentials.s4dsApi.baseUrl === "custom" ? $credentials.s4dsApi.customBaseUrl : $credentials.s4dsApi.baseUrl}}', + url: '', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + }, + properties: [ + ...ApiHelper.getResources(), + ...ApiHelper.getOperations(), + ...ApiHelper.getFields(), + ], + }; + + async execute(this: IExecuteFunctions): Promise { + const items = this.getInputData(); + const returnData: INodeExecutionData[] = []; + + for (let i = 0; i < items.length; i++) { + try { + const resource = this.getNodeParameter('resource', i) as string; + const operation = this.getNodeParameter('operation', i) as string; + + // Obtener definición de la API + const apiDefinition = ApiHelper.getApiDefinition(resource, operation); + if (!apiDefinition) { + throw new Error(`API definition not found for resource: ${resource}, operation: ${operation}`); + } + + // Obtener credenciales + const credentials = await this.getCredentials('s4dsApi'); + + // Determinar la URL base + let baseUrl = credentials.baseUrl; + if (baseUrl === 'custom') { + baseUrl = credentials.customBaseUrl; + } + + if (!baseUrl) { + throw new Error('Base URL is required. Please configure the S4DS API credentials.'); + } + + // Preparar headers + let headers: Record = { + Accept: 'application/json', + 'Content-Type': 'application/json', + }; + + // Agregar autenticación si es requerida + if (apiDefinition.requiresAuth) { + // Siempre usar la key por defecto 's4ds_token' + const contextKey = 's4ds_token'; + const contextData = this.getWorkflowStaticData('global'); + const tokenData = contextData[contextKey] as any; + if (!tokenData || !tokenData.authorization_header) { + throw new Error(`Token not found in context with key 's4ds_token'. Please run an authentication operation first.`); + } + headers.Authorization = tokenData.authorization_header; + } + + // Preparar parámetros de query + const queryParams: Record = {}; + if (apiDefinition.parameters) { + const queryParameters = this.getNodeParameter('queryParameters', i, {}) as Record; + apiDefinition.parameters + .filter(p => p.in === 'query') + .forEach(param => { + if (queryParameters[param.name] !== undefined && queryParameters[param.name] !== '') { + queryParams[param.name] = queryParameters[param.name]; + } + }); + } + + // Preparar body si es necesario + let body: any = undefined; + if (apiDefinition.method === 'POST' && resource === 'authentication') { + body = { + username: credentials.username, + password: credentials.password, + }; + } else if (apiDefinition.requestBody && apiDefinition.requestBody.schema) { + // Usar DTO para request body + let requestBodyData = this.getNodeParameter('requestBody', i, {}); + if (typeof requestBodyData === 'string') { + try { + requestBodyData = JSON.parse(requestBodyData); + } catch (e) { + throw new Error('El JSON del body no es válido: ' + e.message); + } + } + body = requestBodyData; + } else if (apiDefinition.parameters) { + const bodyParameters = this.getNodeParameter('bodyParameters', i, {}) as Record; + const bodyParams = apiDefinition.parameters.filter(p => p.in === 'body'); + if (bodyParams.length > 0) { + body = {}; + bodyParams.forEach(param => { + if (bodyParameters[param.name] !== undefined && bodyParameters[param.name] !== '') { + body[param.name] = bodyParameters[param.name]; + } + }); + } + } + + // Construir URL con parámetros de query + let url = `${baseUrl}${apiDefinition.endpoint}`; + if (Object.keys(queryParams).length > 0) { + const queryString = Object.keys(queryParams) + .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`) + .join('&'); + url += `?${queryString}`; + } + + // Ejecutar la petición HTTP + const response = await this.helpers.httpRequest({ + method: apiDefinition.method as IHttpRequestMethods, + url, + headers, + body, + }); + + // Procesar respuesta según el tipo de operación + if (resource === 'authentication' && operation === 'generateToken') { + // Almacenar token en contexto + const contextKey = 's4ds_token'; + const tokenData = { + token: response.token, + token_type: response.token_type, + expires_in: response.expires_in, + expires_at: new Date(Date.now() + response.expires_in * 1000).toISOString(), + authorization_header: `${response.token_type} ${response.token}`, + }; + // Almacenar en contexto del workflow + this.getWorkflowStaticData('global')[contextKey] = tokenData; + + // Retornar respuesta sin el token por seguridad + const secureResponse = { + success: true, + token_type: response.token_type, + expires_in: response.expires_in, + expires_at: tokenData.expires_at, + message: 'Token generated successfully and stored in workflow context', + context_key: contextKey, + }; + + returnData.push({ + json: secureResponse, + }); + } else { + // Retornar respuesta normal + returnData.push({ + json: response, + }); + } + } catch (error) { + if (this.continueOnFail()) { + returnData.push({ + json: { + error: error.message, + }, + }); + continue; + } + throw error; + } + } + + return [returnData]; + } + + +} \ No newline at end of file diff --git a/nodes/S4DSMain/api-definitions.json b/nodes/S4DSMain/api-definitions.json new file mode 100644 index 0000000..eb9f71e --- /dev/null +++ b/nodes/S4DSMain/api-definitions.json @@ -0,0 +1,81 @@ +{ + "authentication": { + "generateToken": { + "method": "POST", + "endpoint": "/login/generateToken", + "description": "Generate authentication token using credentials", + "parameters": [], + "requiresAuth": false, + "response": { + "type": "object", + "properties": { + "token": "string", + "token_type": "string", + "expires_in": "number" + } + } + } + }, + "products": { + "getProductCount": { + "method": "GET", + "endpoint": "/product/count", + "description": "Get the total count of products", + "parameters": [], + "requiresAuth": true, + "response": { + "type": "object", + "properties": { + "count": "number" + } + } + } + }, + "customers": { + "getCustomerByDocument": { + "method": "GET", + "endpoint": "/customer/specificCustomer", + "description": "Get customer information by document ID and type", + "parameters": [ + { + "name": "documentId", + "type": "string", + "required": false, + "description": "Document ID of the customer", + "in": "query" + }, + { + "name": "documentType", + "type": "string", + "required": false, + "description": "Type of document (e.g., CC, CE, NIT)", + "in": "query" + } + ], + "requiresAuth": true, + "response": { + "type": "object", + "properties": { + "customer": "object" + } + } + }, + "createCustomer": { + "method": "POST", + "endpoint": "/customer", + "description": "Create a new customer", + "parameters": [], + "requiresAuth": true, + "requestBody": { + "schema": "SetNewCustomerDTO", + "required": true + }, + "response": { + "type": "object", + "properties": { + "customer": "object" + } + } + } + } +} \ No newline at end of file diff --git a/nodes/S4DSMain/dto-definitions.json b/nodes/S4DSMain/dto-definitions.json new file mode 100644 index 0000000..7dbd3e9 --- /dev/null +++ b/nodes/S4DSMain/dto-definitions.json @@ -0,0 +1,608 @@ +{ + "SetNewCustomerDTO": { + "type": "object", + "properties": { + "documentType": { + "type": "string", + "description": "Type of document", + "required": false + }, + "docType": { + "type": "string", + "description": "Document type code", + "required": false + }, + "docTypeDesc": { + "type": "string", + "description": "Document type description", + "required": false + }, + "documentId": { + "type": "string", + "description": "Document ID", + "required": false + }, + "newDocumentId": { + "type": "string", + "description": "New document ID", + "required": false + }, + "newDocumentType": { + "type": "string", + "description": "New document type", + "required": false + }, + "verificationDigit": { + "type": "string", + "description": "Verification digit", + "required": false + }, + "internalCode": { + "type": "string", + "description": "Internal code", + "required": false + }, + "customerType": { + "type": "string", + "description": "Customer type", + "required": false + }, + "status": { + "type": "string", + "description": "Status", + "required": false + }, + "customerStatus": { + "type": "string", + "description": "Customer status", + "required": false + }, + "name": { + "type": "string", + "description": "First name", + "required": false + }, + "secondName": { + "type": "string", + "description": "Second name", + "required": false + }, + "lastName": { + "type": "string", + "description": "Last name", + "required": false + }, + "lastName1": { + "type": "string", + "description": "First last name", + "required": false + }, + "lastName2": { + "type": "string", + "description": "Second last name", + "required": false + }, + "email": { + "type": "string", + "description": "Email address", + "required": false + }, + "gender": { + "type": "string", + "description": "Gender", + "required": false + }, + "genderDesc": { + "type": "string", + "description": "Gender description", + "required": false + }, + "language": { + "type": "string", + "description": "Language", + "required": false + }, + "languageCode": { + "type": "string", + "description": "Language code", + "required": false + }, + "username": { + "type": "string", + "description": "Username", + "required": false + }, + "password": { + "type": "string", + "description": "Password", + "required": false + }, + "loginType": { + "type": "string", + "description": "Login type", + "required": false + }, + "roles": { + "type": "array", + "description": "User roles", + "items": { + "type": "string" + }, + "required": false + }, + "metadata": { + "type": "object", + "description": "Metadata del cliente", + "required": false + }, + "priceList": { + "type": "string", + "description": "Price list", + "required": false + }, + "shippingAddress": { + "type": "object", + "description": "Shipping address", + "schema": "AddressDTO", + "required": false + }, + "homeAddress": { + "type": "object", + "description": "Home address", + "schema": "AddressDTO", + "required": false + }, + "phone": { + "type": "array", + "description": "Phone numbers", + "items": { + "type": "object", + "schema": "PhoneDTO" + }, + "required": false + }, + "birthdate": { + "type": "string", + "description": "Birth date", + "required": false + }, + "sellerId": { + "type": "string", + "description": "Seller ID", + "required": false + }, + "balanceDate": { + "type": "string", + "description": "Balance date", + "required": false + }, + "balance": { + "type": "string", + "description": "Balance", + "required": false + }, + "balanceMoneyFormat": { + "type": "string", + "description": "Balance money format", + "required": false + }, + "paymentTerms": { + "type": "string", + "description": "Payment terms", + "required": false + }, + "creditDays": { + "type": "string", + "description": "Credit days", + "required": false + }, + "creditQuota": { + "type": "string", + "description": "Credit quota", + "required": false + }, + "creditSegment": { + "type": "string", + "description": "Credit segment", + "required": false + }, + "creditStatus": { + "type": "string", + "description": "Credit status", + "required": false + }, + "territorialDivision": { + "type": "string", + "description": "Territorial division", + "required": false + }, + "territoryDescription": { + "type": "string", + "description": "Territory description", + "required": false + }, + "zoneDivisionId": { + "type": "string", + "description": "Zone division ID", + "required": false + }, + "zoneDivisionDesc": { + "type": "string", + "description": "Zone division description", + "required": false + }, + "zoneDivisionCode": { + "type": "string", + "description": "Zone division code", + "required": false + }, + "tdDivisionId": { + "type": "string", + "description": "TD division ID", + "required": false + }, + "tdDivisionCode": { + "type": "string", + "description": "TD division code", + "required": false + }, + "regionDivisionId": { + "type": "string", + "description": "Region division ID", + "required": false + }, + "regionDivisionDescription": { + "type": "string", + "description": "Region division description", + "required": false + }, + "regionDivisionCode": { + "type": "string", + "description": "Region division code", + "required": false + }, + "countryDivisionId": { + "type": "string", + "description": "Country division ID", + "required": false + }, + "countryDivisionCode": { + "type": "string", + "description": "Country division code", + "required": false + }, + "countryDivisionDesc": { + "type": "string", + "description": "Country division description", + "required": false + }, + "bankCode": { + "type": "string", + "description": "Bank code", + "required": false + }, + "bankDescription": { + "type": "string", + "description": "Bank description", + "required": false + }, + "accountType": { + "type": "string", + "description": "Account type", + "required": false + }, + "taxRegime": { + "type": "string", + "description": "Tax regime", + "required": false + }, + "taxRegimeCode": { + "type": "string", + "description": "Tax regime code", + "required": false + }, + "taxRegimeDescription": { + "type": "string", + "description": "Tax regime description", + "required": false + }, + "accountNumber": { + "type": "string", + "description": "Account number", + "required": false + }, + "segment": { + "type": "string", + "description": "Segment", + "required": false + }, + "registration": { + "type": "string", + "description": "Registration", + "required": false + }, + "reactivationDate": { + "type": "string", + "description": "Reactivation date", + "required": false + }, + "referent": { + "type": "object", + "description": "Referent information", + "schema": "ReferentDTO", + "required": false + }, + "sponsor": { + "type": "object", + "description": "Sponsor information", + "schema": "SponsorDTO", + "required": false + }, + "sync": { + "type": "string", + "description": "Sync status", + "required": false + }, + "currencyId": { + "type": "string", + "description": "Currency ID", + "required": false + }, + "continuity": { + "type": "string", + "description": "Continuity", + "required": false + }, + "accumulatedPoints": { + "type": "string", + "description": "Accumulated points", + "required": false + }, + "rankCode": { + "type": "string", + "description": "Rank code", + "required": false + }, + "rankDescription": { + "type": "string", + "description": "Rank description", + "required": false + }, + "honorificRankCode": { + "type": "string", + "description": "Honorific rank code", + "required": false + }, + "honorificRankDescription": { + "type": "string", + "description": "Honorific rank description", + "required": false + }, + "honorificDate": { + "type": "string", + "description": "Honorific date", + "required": false + }, + "compensationStructure": { + "type": "string", + "description": "Compensation structure", + "required": false + }, + "inactivityCampaigns": { + "type": "string", + "description": "Inactivity campaigns", + "required": false + }, + "commercialStatus": { + "type": "string", + "description": "Commercial status", + "required": false + }, + "desertionDate": { + "type": "string", + "description": "Desertion date", + "required": false + }, + "outstandingCreditBalanceSync": { + "type": "string", + "description": "Outstanding credit balance sync", + "required": false + }, + "outstandingCreditBalanceSyncFormatted": { + "type": "string", + "description": "Outstanding credit balance sync formatted", + "required": false + }, + "notificationType": { + "type": "string", + "description": "Notification type", + "required": false + }, + "needsAuditing": { + "type": "string", + "description": "Needs auditing", + "required": false + }, + "rut": { + "type": "string", + "description": "RUT", + "required": false + }, + "georeference": { + "type": "string", + "description": "Georeference", + "required": false + }, + "warehouseId": { + "type": "string", + "description": "Warehouse ID", + "required": false + }, + "warehouseCode": { + "type": "string", + "description": "Warehouse code", + "required": false + }, + "currency": { + "type": "string", + "description": "Currency", + "required": false + }, + "postalCode": { + "type": "string", + "description": "Postal code", + "required": false + }, + "shippingPostalCode": { + "type": "string", + "description": "Shipping postal code", + "required": false + }, + "tdivisionDescription": { + "type": "string", + "description": "T division description", + "required": false + } + } + }, + "AddressDTO": { + "type": "object", + "properties": { + "address": { + "type": "string", + "description": "Address line", + "required": false + }, + "address2": { + "type": "string", + "description": "Address line 2", + "required": false + }, + "address3": { + "type": "string", + "description": "Address line 3", + "required": false + }, + "postalCode": { + "type": "string", + "description": "Postal code", + "required": false + }, + "neighborhood": { + "type": "string", + "description": "Neighborhood", + "required": false + }, + "city": { + "type": "string", + "description": "City", + "required": false + }, + "cityCode": { + "type": "string", + "description": "City code", + "required": false + }, + "state": { + "type": "string", + "description": "State", + "required": false + }, + "stateCode": { + "type": "string", + "description": "State code", + "required": false + }, + "country": { + "type": "string", + "description": "Country", + "required": false + }, + "countryCode": { + "type": "string", + "description": "Country code", + "required": false + }, + "addressName": { + "type": "string", + "description": "Address name", + "required": false + }, + "addressDescription": { + "type": "string", + "description": "Address description", + "required": false + }, + "addressMetadata": { + "type": "object", + "description": "Address metadata", + "required": false + } + } + }, + "PhoneDTO": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Phone type", + "required": false + }, + "countryCode": { + "type": "string", + "description": "Country code", + "required": false + }, + "number": { + "type": "string", + "description": "Phone number", + "required": false + }, + "modifiedAt": { + "type": "string", + "description": "Modified at timestamp", + "required": false + } + } + }, + "ReferentDTO": { + "type": "object", + "properties": { + "documentId": { + "type": "string", + "description": "Document ID", + "required": false + }, + "docType": { + "type": "string", + "description": "Document type", + "required": false + }, + "docTypeDesc": { + "type": "string", + "description": "Document type description", + "required": false + } + } + }, + "SponsorDTO": { + "type": "object", + "properties": { + "documentId": { + "type": "string", + "description": "Document ID", + "required": false + }, + "docType": { + "type": "string", + "description": "Document type", + "required": false + }, + "docTypeDesc": { + "type": "string", + "description": "Document type description", + "required": false + } + } + } +} \ No newline at end of file diff --git a/nodes/S4DSMain/logo_generic.png b/nodes/S4DSMain/logo_generic.png new file mode 100644 index 0000000..38b9235 Binary files /dev/null and b/nodes/S4DSMain/logo_generic.png differ diff --git a/package.json b/package.json index 7bf00ee..e94201f 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,7 @@ "dist/credentials/S4DSApi.credentials.js" ], "nodes": [ - "dist/nodes/S4DSAuth/S4DSAuth.node.js", - "dist/nodes/S4DSExample/S4DSExample.node.js" + "dist/nodes/S4DSMain/S4DSMain.node.js" ] }, "devDependencies": {