feat: add WizPixelFlow node
This commit is contained in:
198
nodes/WizPixelFlow/WizPixelFlow.node.ts
Normal file
198
nodes/WizPixelFlow/WizPixelFlow.node.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import {
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
NodeOperationError,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
const OUTPUT_FORMATS = [
|
||||
{ name: 'Leaderboard 728x90', value: 'leaderboard_728x90' },
|
||||
{ name: 'Medium Rectangle 300x250', value: 'medium_rectangle_300x250' },
|
||||
{ name: 'Half Page 300x600', value: 'half_page_300x600' },
|
||||
{ name: 'Large Rectangle 336x280', value: 'large_rectangle_336x280' },
|
||||
{ name: 'Billboard 970x250', value: 'billboard_970x250' },
|
||||
{ name: 'Wide Skyscraper 160x600', value: 'wide_skyscraper_160x600' },
|
||||
{ name: 'Skyscraper 120x600', value: 'skyscraper_120x600' },
|
||||
{ name: 'Small Rectangle 300x100', value: 'small_rectangle_300x100' },
|
||||
{ name: 'Square 250x250', value: 'square_250x250' },
|
||||
{ name: 'Small Square 200x200', value: 'small_square_200x200' },
|
||||
{ name: 'Banner 468x60', value: 'banner_468x60' },
|
||||
{ name: 'Half Banner 234x60', value: 'half_banner_234x60' },
|
||||
{ name: 'Micro Bar 88x31', value: 'micro_bar_88x31' },
|
||||
{ name: 'Square Post 1080x1080', value: 'square_post_1080x1080' },
|
||||
{ name: 'Portrait Post 1080x1350', value: 'portrait_post_1080x1350' },
|
||||
{ name: 'Stories 1080x1920', value: 'stories_1080x1920' },
|
||||
{ name: 'Shorts 1080x1920', value: 'shorts_1080x1920' },
|
||||
{ name: 'Landscape 1280x720', value: 'landscape_1280x720' },
|
||||
{ name: 'LinkedIn Banner 1584x396', value: 'linkedin_banner_1584x396' },
|
||||
{ name: 'LinkedIn Square 1200x1200', value: 'linkedin_square_1200x1200' },
|
||||
{ name: 'Twitter Card 1200x628', value: 'twitter_card_1200x628' },
|
||||
{ name: 'Twitter Square 1080x1080', value: 'twitter_square_1080x1080' },
|
||||
];
|
||||
|
||||
export class WizPixelFlow implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'WizPixel Flow',
|
||||
name: 'wizPixelFlow',
|
||||
icon: 'file:wizpixel-flow.svg',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
subtitle: '={{$parameter["operation"]}}',
|
||||
description: 'Render HTML5 banners and MP4 videos with WizPixel Flow',
|
||||
defaults: { name: 'WizPixel Flow' },
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
credentials: [{ name: 'wizPixelFlowApi', required: true }],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
options: [
|
||||
{ name: 'Render', value: 'render', description: 'Submit a render job' },
|
||||
{ name: 'Get Job', value: 'getJob', description: 'Get status of a render job' },
|
||||
{ name: 'List Templates', value: 'listTemplates', description: 'List available templates' },
|
||||
],
|
||||
default: 'render',
|
||||
},
|
||||
{
|
||||
displayName: 'Template ID',
|
||||
name: 'templateId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: { show: { operation: ['render'] } },
|
||||
description: 'The ID of the Flow template to render',
|
||||
},
|
||||
{
|
||||
displayName: 'Output Formats',
|
||||
name: 'outputFormats',
|
||||
type: 'multiOptions',
|
||||
options: OUTPUT_FORMATS,
|
||||
default: ['leaderboard_728x90', 'medium_rectangle_300x250'],
|
||||
required: true,
|
||||
displayOptions: { show: { operation: ['render'] } },
|
||||
},
|
||||
{
|
||||
displayName: 'Locales',
|
||||
name: 'locales',
|
||||
type: 'string',
|
||||
default: 'en',
|
||||
displayOptions: { show: { operation: ['render'] } },
|
||||
description: 'Comma-separated locale codes, e.g. en,it,fr',
|
||||
},
|
||||
{
|
||||
displayName: 'Variables (JSON)',
|
||||
name: 'variables',
|
||||
type: 'json',
|
||||
default: '{}',
|
||||
displayOptions: { show: { operation: ['render'] } },
|
||||
description: 'Key/value pairs to substitute into {{variable}} placeholders',
|
||||
},
|
||||
{
|
||||
displayName: 'Include Video (MP4)',
|
||||
name: 'includeVideo',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
displayOptions: { show: { operation: ['render'] } },
|
||||
},
|
||||
{
|
||||
displayName: 'Wait for Completion',
|
||||
name: 'waitForCompletion',
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
displayOptions: { show: { operation: ['render'] } },
|
||||
description: 'Poll until the job is done and return the ZIP download URL',
|
||||
},
|
||||
{
|
||||
displayName: 'Poll Interval (seconds)',
|
||||
name: 'pollInterval',
|
||||
type: 'number',
|
||||
default: 5,
|
||||
displayOptions: { show: { operation: ['render'], waitForCompletion: [true] } },
|
||||
},
|
||||
{
|
||||
displayName: 'Max Wait (seconds)',
|
||||
name: 'maxWait',
|
||||
type: 'number',
|
||||
default: 300,
|
||||
displayOptions: { show: { operation: ['render'], waitForCompletion: [true] } },
|
||||
},
|
||||
{
|
||||
displayName: 'Job ID',
|
||||
name: 'jobId',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: true,
|
||||
displayOptions: { show: { operation: ['getJob'] } },
|
||||
},
|
||||
{
|
||||
displayName: 'Limit',
|
||||
name: 'limit',
|
||||
type: 'number',
|
||||
default: 50,
|
||||
typeOptions: { minValue: 1, maxValue: 200 },
|
||||
displayOptions: { show: { operation: ['listTemplates'] } },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const credentials = await this.getCredentials('wizPixelFlowApi');
|
||||
const baseUrl = (credentials.baseUrl as string).replace(/\/$/, '');
|
||||
const apiKey = credentials.apiKey as string;
|
||||
const headers = { 'X-WPF-API-Key': apiKey, 'Content-Type': 'application/json' };
|
||||
const items = this.getInputData();
|
||||
const returnData: INodeExecutionData[] = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const operation = this.getNodeParameter('operation', i) as string;
|
||||
try {
|
||||
if (operation === 'render') {
|
||||
const templateId = this.getNodeParameter('templateId', i) as string;
|
||||
const outputFormats = this.getNodeParameter('outputFormats', i) as string[];
|
||||
const localesRaw = this.getNodeParameter('locales', i) as string;
|
||||
const variablesRaw = this.getNodeParameter('variables', i) as string;
|
||||
const includeVideo = this.getNodeParameter('includeVideo', i) as boolean;
|
||||
const waitForCompletion = this.getNodeParameter('waitForCompletion', i) as boolean;
|
||||
const pollInterval = this.getNodeParameter('pollInterval', i) as number;
|
||||
const maxWait = this.getNodeParameter('maxWait', i) as number;
|
||||
const locales = localesRaw.split(',').map((l) => l.trim()).filter(Boolean);
|
||||
const variables = typeof variablesRaw === 'string' ? JSON.parse(variablesRaw) : variablesRaw;
|
||||
const body = { template_id: templateId, formats: outputFormats, locales, variables, include_video: includeVideo };
|
||||
const submitResp = await this.helpers.httpRequest({ method: 'POST', url: `${baseUrl}/render`, headers, body: JSON.stringify(body) });
|
||||
const parsed = typeof submitResp === 'string' ? JSON.parse(submitResp) : submitResp;
|
||||
const jobId = parsed.job_id;
|
||||
if (!waitForCompletion) { returnData.push({ json: parsed }); continue; }
|
||||
const deadline = Date.now() + maxWait * 1000;
|
||||
let jobData: Record<string, unknown> = {};
|
||||
while (Date.now() < deadline) {
|
||||
await new Promise((r) => setTimeout(r, pollInterval * 1000));
|
||||
const pollResp = await this.helpers.httpRequest({ method: 'GET', url: `${baseUrl}/jobs/${jobId}`, headers });
|
||||
jobData = typeof pollResp === 'string' ? JSON.parse(pollResp) : pollResp;
|
||||
const job = (jobData as { job?: { status?: string } }).job;
|
||||
if (job?.status === 'done' || job?.status === 'failed') break;
|
||||
}
|
||||
returnData.push({ json: jobData });
|
||||
} else if (operation === 'getJob') {
|
||||
const jobId = this.getNodeParameter('jobId', i) as string;
|
||||
const resp = await this.helpers.httpRequest({ method: 'GET', url: `${baseUrl}/jobs/${jobId}`, headers });
|
||||
returnData.push({ json: typeof resp === 'string' ? JSON.parse(resp) : resp });
|
||||
} else if (operation === 'listTemplates') {
|
||||
const limit = this.getNodeParameter('limit', i) as number;
|
||||
const resp = await this.helpers.httpRequest({ method: 'GET', url: `${baseUrl}/templates?limit=${limit}`, headers });
|
||||
returnData.push({ json: typeof resp === 'string' ? JSON.parse(resp) : resp });
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
returnData.push({ json: { error: (error as Error).message }, pairedItem: i });
|
||||
} else {
|
||||
throw new NodeOperationError(this.getNode(), error as Error, { itemIndex: i });
|
||||
}
|
||||
}
|
||||
}
|
||||
return [returnData];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user