Files
probo/contrib/claude/n8n.md
Bryan Frimin ab52dc0a34 Clean agent rules
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
2026-04-19 11:42:50 +02:00

222 lines
6.7 KiB
Markdown

# n8n Node (`packages/n8n-node`)
Community node package `@probo/n8n-nodes-probo` exposing the Probo API as n8n operations. One `Probo` node with many resources; each resource maps to a set of GraphQL operations against the Console or Connect API.
## Directory structure
```
packages/n8n-node/
credentials/ProboApi.credentials.ts # API key credential (Bearer token)
nodes/Probo/
Probo.node.ts # Node class — resource picker, dispatch
Probo.node.json # n8n codex metadata
GenericFunctions.ts # GraphQL request helpers, pagination
actions/
index.ts # Resource registry, dispatch, field aggregators
<resource>/
index.ts # Operation dropdown + spread descriptions + re-exports
create.operation.ts # One file per operation
get.operation.ts
getAll.operation.ts
update.operation.ts
delete.operation.ts
...
```
## Resource registration
Two places must be updated when adding a resource:
**1. `actions/index.ts`** — import the module and add it to the `resources` map:
```typescript
import * as myresource from './myresource';
export const resources: Record<string, ResourceModule> = {
// ... existing resources ...
myresource: myresource as ResourceModule,
};
```
**2. `Probo.node.ts`** — add a Resource dropdown entry in the `properties` array:
```typescript
{
name: 'My Resource',
value: 'myresource',
description: 'Manage my resources',
},
```
The `value` must match the key in `resources` and the `displayOptions.show.resource` in every operation file.
## Per-resource file pattern
### `<resource>/index.ts`
1. Import each `*.operation.ts` as a namespace.
2. Export `description` — the operation dropdown (gated with `displayOptions.show.resource`) plus all spread operation descriptions.
3. Re-export each operation module with a name matching its `operation` value.
```typescript
import * as createOp from './create.operation';
import * as getOp from './get.operation';
import * as getAllOp from './getAll.operation';
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['myresource'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new resource',
action: 'Create a resource',
},
// ... more operations ...
],
default: 'create',
},
...createOp.description,
...getOp.description,
...getAllOp.description,
];
export {
createOp as create,
getOp as get,
getAllOp as getAll,
};
```
Export names (`create`, `get`, `getAll`, etc.) **must match** the operation `value` strings — `getExecuteFunction` uses them as keys.
### `<resource>/<verb>.operation.ts`
Each file exports `description` (field definitions) and `execute` (the handler):
```typescript
export const description: INodeProperties[] = [
{
displayName: 'Organization ID',
name: 'organizationId',
type: 'string',
displayOptions: {
show: {
resource: ['myresource'],
operation: ['create'],
},
},
default: '',
required: true,
},
// ... more fields ...
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['myresource'],
operation: ['create'],
},
},
options: [
// optional field definitions
],
},
];
export async function execute(
this: IExecuteFunctions,
itemIndex: number,
): Promise<INodeExecutionData> {
const organizationId = this.getNodeParameter('organizationId', itemIndex) as string;
const name = this.getNodeParameter('name', itemIndex) as string;
const query = `
mutation CreateMyResource($input: CreateMyResourceInput!) {
createMyResource(input: $input) {
myResourceEdge {
node {
id
name
}
}
}
}
`;
const responseData = await proboApiRequest.call(this, query, {
input: { organizationId, name },
});
return {
json: responseData,
pairedItem: { item: itemIndex },
};
}
```
## GraphQL helpers
All helpers live in `GenericFunctions.ts`.
| Helper | API endpoint | Use case |
|--------|-------------|----------|
| `proboApiRequest` | `/api/console/v1/graphql` | Single mutations and queries |
| `proboConnectApiRequest` | `/api/connect/v1/graphql` | Organization/user operations (IAM) |
| `proboApiRequestAllItems` | Console API | Cursor-paginated list queries |
| `proboConnectApiRequestAllItems` | Connect API | Cursor-paginated list queries (IAM) |
| `proboApiMultipartRequest` | Console API | File upload mutations (multipart/form-data) |
### Pagination (`proboApiRequestAllItems`)
Caller supplies a `getConnection` function that navigates from the raw GraphQL response to the Relay connection object (must have `edges` and `pageInfo`):
```typescript
const items = await proboApiRequestAllItems.call(
this,
query,
{ organizationId },
(response) => {
const data = response?.data as IDataObject | undefined;
const node = data?.node as IDataObject | undefined;
return node?.myResources as IDataObject | undefined;
},
returnAll,
limit,
);
```
Internal page size is 100. When `returnAll` is false, stops at `limit`.
### Update pattern
For nullable fields, empty string means "clear the value":
```typescript
if (additionalFields.description !== undefined) {
input.description = additionalFields.description === '' ? null : additionalFields.description;
}
```
## Adding a new resource — checklist
1. **Directory** — create `nodes/Probo/actions/<resource>/` with `index.ts` and one `*.operation.ts` per operation
2. **Operations** — each file exports `description` (fields gated with `displayOptions`) and `execute` (reads params, calls GraphQL, returns `{ json, pairedItem }`)
3. **Index** — `<resource>/index.ts` defines the operation dropdown, spreads all descriptions, re-exports ops with matching value names
4. **Register** — import and add to `resources` map in `actions/index.ts`
5. **Node** — add Resource dropdown entry in `Probo.node.ts` properties
6. **Verify** — `npx n8n-node lint` must pass