Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,23 @@ export async function createAction(ctx: IExecuteFunctions, i: number): Promise<A
actorId: ctx.getNodeParameter('actorId', i) as string,
actorType: ctx.getNodeParameter('actorType', i) as ActionCreatePayload['actorType'],
actionType: ctx.getNodeParameter('actionType', i) as string,
callbackUrl: ctx.getNodeParameter('callbackUrl', i) as string,
payload: {},
};

const parsedPayload = safeParse(ctx.getNodeParameter('payload', i, '{}'));
if (parsedPayload) body.payload = parsedPayload as Record<string, unknown>;

const callbackMethod = ctx.getNodeParameter('callbackMethod', i) as ActionCreatePayload['callbackMethod'];
if (callbackMethod) body.callbackMethod = callbackMethod;

const callbackPayloadSpec = safeParse(ctx.getNodeParameter('callbackPayloadSpec', i, '{}'));
if (callbackPayloadSpec) body.callbackPayloadSpec = callbackPayloadSpec as Record<string, unknown>;
if (callbackMethod && callbackMethod !== 'none') {
body.callbackMethod = callbackMethod;
body.callbackUrl = ctx.getNodeParameter('callbackUrl', i) as string;

const callbackPayloadSpec = safeParse(ctx.getNodeParameter('callbackPayloadSpec', i, '{}'));
if (callbackPayloadSpec) body.callbackPayloadSpec = callbackPayloadSpec as Record<string, unknown>;
} else {
body.callbackMethod = 'none';
body.callbackUrl = '';
}

const dueDate = ctx.getNodeParameter('dueDate', i, '') as string;
if (dueDate) body.dueDate = dueDate;
Expand Down
300 changes: 300 additions & 0 deletions community-nodes/nodes/WorkflowInteractionLayer/shared/properties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
import type { INodeProperties } from 'n8n-workflow';

type DisplayOptions = INodeProperties['displayOptions'];

// ── Shared field factories ──

function actorIdField(displayOptions: DisplayOptions, options?: Partial<INodeProperties>): INodeProperties {
return {
displayName: 'Actor ID',
name: 'actorId',
type: 'string',
default: '',
required: true,
description: 'Identifier for the target actor (max 50 characters)',
displayOptions,
...options,
};
}

function actorTypeField(displayOptions: DisplayOptions): INodeProperties {
return {
displayName: 'Actor Type',
name: 'actorType',
type: 'options',
default: 'user',
required: true,
options: [
{ name: 'Group', value: 'group' },
{ name: 'Role', value: 'role' },
{ name: 'System', value: 'system' },
{ name: 'User', value: 'user' },
{ name: 'Other', value: 'other' },
],
displayOptions,
};
}

function sinceField(displayOptions: DisplayOptions, description: string): INodeProperties {
return {
displayName: 'Since',
name: 'since',
type: 'dateTime',
default: '',
description,
displayOptions,
};
}

function limitField(displayOptions: DisplayOptions): INodeProperties {
return {
displayName: 'Limit',
name: 'limit',
type: 'number',
typeOptions: { minValue: 1, maxValue: 200 },
default: 50,
description: 'Max number of results to return',
displayOptions,
};
}

function workflowInstanceIdField(displayOptions: DisplayOptions): INodeProperties {
return {
displayName: 'Workflow Instance ID',
name: 'workflowInstanceId',
type: 'string',
default: '',
description: 'Filter by workflow instance ID',
displayOptions,
};
}

function metadataField(displayOptions: DisplayOptions): INodeProperties {
return {
displayName: 'Metadata',
name: 'metadata',
type: 'json',
default: '{}',
description: 'Optional JSON metadata object',
displayOptions,
};
}

function returnAllField(displayOptions: DisplayOptions): INodeProperties {
return {
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
description: 'Whether to return all results or only up to a given limit',
displayOptions,
};
}

function autoFieldsNotice(name: string, displayOptions: DisplayOptions): INodeProperties {
return {
displayName:
'Workflow ID and Workflow Instance ID are automatically set from the current workflow and execution context',
name,
type: 'notice',
default: '',
displayOptions,
};
}

// ── Composed property groups ──

/** Fields shared by "getByActor" operations (message and action). */
function getByActorFields(resource: string): INodeProperties[] {
const show = { resource: [resource], operation: ['getByActor'] };
return [
actorIdField({ show }, { description: `ID of the actor to retrieve ${resource}s for` }),
sinceField({ show }, `Filter ${resource}s created after this RFC 3339 timestamp`),
limitField({ show }),
workflowInstanceIdField({ show }),
];
}

/** Fields shared by "list" operations (message and action). */
function listFields(resource: string): INodeProperties[] {
const show = { resource: [resource], operation: ['list'] };
const showWithLimit = { resource: [resource], operation: ['list'], returnAll: [false] };
return [
returnAllField({ show }),
limitField({ show: showWithLimit }),
actorIdField({ show }, { required: false, description: 'Filter by actor ID' }),
workflowInstanceIdField({ show }),
sinceField(
{ show },
`Filter ${resource}s created after this RFC 3339 timestamp${resource === 'message' ? ' (cursor for pagination)' : ''}`,
),
];
}

// ── Exported property arrays ──

export const messageCreateProperties: INodeProperties[] = [
autoFieldsNotice('autoFieldsNoticeMessage', { show: { resource: ['message'], operation: ['create'] } }),
actorIdField({ show: { resource: ['message'], operation: ['create'] } }),
actorTypeField({ show: { resource: ['message'], operation: ['create'] } }),
{
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
required: true,
description: 'Message title (max 255 characters)',
displayOptions: { show: { resource: ['message'], operation: ['create'] } },
},
{
displayName: 'Body',
name: 'body',
type: 'string',
typeOptions: { rows: 4 },
default: '',
required: true,
description: 'Message body text',
displayOptions: { show: { resource: ['message'], operation: ['create'] } },
},
metadataField({ show: { resource: ['message'], operation: ['create'] } }),
];

export const messageGetByActorProperties: INodeProperties[] = getByActorFields('message');

export const messageListProperties: INodeProperties[] = listFields('message');

export const actionCreateProperties: INodeProperties[] = [
autoFieldsNotice('autoFieldsNoticeAction', { show: { resource: ['action'], operation: ['create'] } }),
actorIdField({ show: { resource: ['action'], operation: ['create'] } }),
actorTypeField({ show: { resource: ['action'], operation: ['create'] } }),
{
displayName: 'Action Type',
name: 'actionType',
type: 'options',
default: 'getapproval',
required: true,
options: [
{ name: 'Get Approval', value: 'getapproval' },
{ name: 'Show Form', value: 'showform' },
{ name: 'Wait on Event', value: 'waitonevent' },
],
description: 'The type of action to create',
displayOptions: { show: { resource: ['action'], operation: ['create'] } },
},
{
displayName: 'Payload',
name: 'payload',
type: 'json',
default: '{}',
required: true,
description: 'For "showform": include formId, formVersion, returnUrl. For "getapproval": free-form JSON.',
displayOptions: { show: { resource: ['action'], operation: ['create'] } },
},
{
displayName: 'Callback Method',
name: 'callbackMethod',
type: 'options',
default: 'POST',
options: [
{ name: 'None', value: 'none' },
{ name: 'POST', value: 'POST' },
{ name: 'PUT', value: 'PUT' },
{ name: 'PATCH', value: 'PATCH' },
],
description: 'HTTP method for the callback. Select "None" if no callback is needed.',
displayOptions: { show: { resource: ['action'], operation: ['create'] } },
},
{
displayName: 'Callback URL',
name: 'callbackUrl',
type: 'string',
default: '',
required: true,
description: 'URL to call when the action is completed',
displayOptions: {
show: { resource: ['action'], operation: ['create'], callbackMethod: ['POST', 'PUT', 'PATCH'] },
},
},
{
displayName: 'Callback Payload Spec',
name: 'callbackPayloadSpec',
type: 'json',
default: '{}',
description: 'Optional template for expected callback body',
displayOptions: {
show: { resource: ['action'], operation: ['create'], callbackMethod: ['POST', 'PUT', 'PATCH'] },
},
},
{
displayName: 'Due Date',
name: 'dueDate',
type: 'dateTime',
default: '',
description: 'Optional due date in RFC 3339 format',
displayOptions: { show: { resource: ['action'], operation: ['create'] } },
},
{
displayName: 'Priority',
name: 'priority',
type: 'options',
default: 'normal',
options: [
{ name: 'Critical', value: 'critical' },
{ name: 'Normal', value: 'normal' },
],
displayOptions: { show: { resource: ['action'], operation: ['create'] } },
},
{
displayName: 'Check In',
name: 'checkIn',
type: 'dateTime',
default: '',
description: 'Optional reminder timestamp in RFC 3339 format',
displayOptions: { show: { resource: ['action'], operation: ['create'] } },
},
metadataField({ show: { resource: ['action'], operation: ['create'] } }),
];

export const actionGetProperties: INodeProperties[] = [
{
displayName: 'Action ID',
name: 'actionId',
type: 'string',
default: '',
required: true,
description: 'ID of the action to retrieve',
displayOptions: { show: { resource: ['action'], operation: ['get'] } },
},
];

export const actionGetByActorProperties: INodeProperties[] = getByActorFields('action');

export const actionListProperties: INodeProperties[] = listFields('action');

export const actionUpdateProperties: INodeProperties[] = [
{
displayName: 'Action ID',
name: 'actionId',
type: 'string',
default: '',
required: true,
description: 'ID of the action to update',
displayOptions: { show: { resource: ['action'], operation: ['update'] } },
},
{
displayName: 'Status',
name: 'status',
type: 'options',
default: 'pending',
options: [
{ name: 'Cancelled', value: 'cancelled' },
{ name: 'Completed', value: 'completed' },
{ name: 'Deleted', value: 'deleted' },
{ name: 'Expired', value: 'expired' },
{ name: 'In Progress', value: 'in_progress' },
{ name: 'Pending', value: 'pending' },
],
description: 'New status for the action',
displayOptions: { show: { resource: ['action'], operation: ['update'] } },
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ export interface ActionCreatePayload {
actorType: 'user' | 'group' | 'role' | 'system' | 'other';
actionType: string;
payload: Record<string, unknown>;
callbackUrl: string;
callbackMethod?: 'POST' | 'PUT' | 'PATCH';
callbackUrl?: string;
callbackMethod?: 'POST' | 'PUT' | 'PATCH' | 'none';
callbackPayloadSpec?: Record<string, unknown>;
workflowId: string;
dueDate?: string;
Expand Down
4 changes: 2 additions & 2 deletions docker-compose/sdg-mock-app/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ FROM node:24-alpine AS base
RUN corepack enable && corepack prepare pnpm@11.0.8 --activate

FROM base AS deps
RUN apk add --no-cache libc6-compat
RUN apk add --no-cache g++ libc6-compat make python3
WORKDIR /app

COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm i --frozen-lockfile --ignore-scripts
RUN pnpm i --frozen-lockfile

FROM base AS builder
WORKDIR /app
Expand Down
1 change: 1 addition & 0 deletions docker-compose/sdg-mock-app/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
outputFileTracingRoot: __dirname,
distDir: '.next',
serverExternalPackages: ['better-sqlite3'],
};

export default nextConfig;
2 changes: 1 addition & 1 deletion docker-compose/sdg-mock-app/pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# pnpm-workspace.yaml
allowBuilds:
better-sqlite3: false
better-sqlite3: true
sharp: false
unrs-resolver: false
confirmModulesPurge: false
Loading
Loading