Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
9f4b15a
Refactor node into per-operation handlers and add asset search
eitanp461 May 27, 2026
30479f7
Regenerate package-lock.json against public npm registry
eitanp461 May 28, 2026
e282690
test: unblock vitest on current toolchain
eitanp461 May 28, 2026
8f0f6cc
chore: align node descriptor, peer dep, CI, and docs
eitanp461 May 28, 2026
8d706e6
refactor: relabel resources as Asset/Library and move Search to Asset
eitanp461 May 28, 2026
55f9b1b
Add Codex to gitignore
eitanp461 May 28, 2026
8133d06
feat: add Get Asset operation (asset_id-based)
eitanp461 May 28, 2026
7d9076e
feat: add Delete Assets operation (bulk by public_id)
eitanp461 May 28, 2026
0e52a34
feat: add Update Asset Display Name operation (asset_id-based)
eitanp461 May 28, 2026
8612955
feat: add Asset resource (asset_id-based) and Append-mode tag updates
eitanp461 May 28, 2026
239e175
fix: correct codex node identifier to community package namespace
eitanp461 May 31, 2026
a40ce72
feat: surface video alongside images in node search and docs
eitanp461 May 31, 2026
d1d5107
feat: add Transform resource with 8 image/video delivery-URL operations
eitanp461 Jun 1, 2026
918ba6a
feat: add Video Player op, Multi-Step aspect-ratio crop, and thumbnai…
eitanp461 Jun 2, 2026
3c1f40c
docs: split agent guidance into lean AGENTS.md core + on-demand docs
eitanp461 Jun 2, 2026
5d3d1a1
fix: keep $json lowercase in thumbnail base-transformation description
eitanp461 Jun 2, 2026
e3d0730
fix: harden Transform delivery URLs and align release validation
eitanp461 Jun 2, 2026
4aa32d5
feat: improve agentic affordances in node and operation descriptions
eitanp461 Jun 2, 2026
7779c8a
feat: build delivery URLs via @cloudinary/url-gen with analytics slug
eitanp461 Jun 2, 2026
c96cff9
fix: make secureDistribution conditional on privateCdn in credentials
eitanp461 Jun 2, 2026
51140c2
docs: remove stale zero-dep constraint from agent instructions
eitanp461 Jun 2, 2026
af32123
fix: address PR #7 review feedback
eitanp461 Jun 3, 2026
5bff9a1
feat: categorize and reorder Transform operations and resources
eitanp461 Jun 4, 2026
035f15f
chore: add backward-compat contract check tooling
eitanp461 Jun 4, 2026
e0c7ef3
ci: gate PRs on backward-compat contract check
eitanp461 Jun 4, 2026
4ffd88c
refactor: rename compat-check to backward-compatibility-check
eitanp461 Jun 4, 2026
b0c9d69
feat: shorten Video Player action label
eitanp461 Jun 4, 2026
9c0e24e
fix: drop empty params from Cloudinary signature
eitanp461 Jun 4, 2026
6d50dc7
feat: add pad fit modes and pad background to Resize
eitanp461 Jun 4, 2026
4d44aa0
chore: ignore .DS_Store
eitanp461 Jun 4, 2026
afe5830
feat: add Video Crop and Resize transform operations
eitanp461 Jun 4, 2026
ab6fc1d
feat: chain transforms across nodes via Continue From Transformation
eitanp461 Jun 7, 2026
08b9a1a
fix: apply Video Player transformation to the video stream
eitanp461 Jun 7, 2026
d6c025d
docs: explain transform chaining model and property mapping
eitanp461 Jun 7, 2026
7e2f50f
feat: add Video Player Crop Mode and guard the aspect-ratio crop conf…
eitanp461 Jun 7, 2026
ff65225
ci: surface baseline build output in backward-compat check
eitanp461 Jun 7, 2026
aec05d7
fix: build compat-check baseline against public npm, not CodeArtifact
eitanp461 Jun 7, 2026
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
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ module.exports = {
extraFileExtensions: ['.json'],
},

ignorePatterns: ['.eslintrc.js', '**/*.js', '**/node_modules/**', '**/dist/**'],
ignorePatterns: ['.eslintrc.js', '**/*.js', '**/node_modules/**', '**/dist/**', '**/*.test.ts', '**/testHelpers.ts'],

overrides: [
{
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
# backward-compatibility-check builds a worktree at the deployed
# baseline commit, so the full history (not a shallow clone) must
# be available.
fetch-depth: 0

- name: Setup Node.js
uses: actions/setup-node@v4
Expand All @@ -29,3 +34,9 @@ jobs:
- name: Run build
run: npm run build

- name: Run tests
run: npm test

- name: Backward-compatibility contract check
run: npm run backward-compatibility-check

5 changes: 3 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ jobs:

- uses: actions/setup-node@v4
with:
node-version: '22'
node-version-file: '.nvmrc'
cache: 'npm'
- run: npm install -g npm@latest
- run: npm ci
- run: npm run build --if-present
- run: npm run lint
- run: npm publish
- run: npm publish --provenance
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,12 @@ vite.config.ts.timestamp-*

# IDE
.idea

# Codex
.codex

# Local scratch / temp files (not for commit)
.local/
examples/

.DS_Store
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v20.19.0
v24.16.0
39 changes: 39 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# AGENTS.md

Guidance for coding agents working in this repository. This is an n8n **community node** package — a single node (`n8n-nodes-cloudinary.cloudinary`) and a single credential type (`cloudinaryApi`). No service layer, no SDK; zero runtime deps beyond the `n8n-workflow` peer dep.

Deep references live in [docs/](docs/) and are linked inline below — read them when a task touches that area, so this file stays small.

## Commands

- `npm run build` — compile TypeScript and copy SVG/PNG icons into `dist/` via gulp.
- `npm run dev` — `tsc --watch`. Does not re-run the icon copy; if icons change, re-run `build`.
- `npm run lint` / `npm run lintfix` — ESLint (`eslint-plugin-n8n-nodes-base`) over `nodes`, `credentials`, `package.json`.
- `npm run format` — Prettier over `nodes` and `credentials`.
- `npm run n8n-validate` — `@n8n/scan-community-package`; required to pass before publishing.
- `npm run prepublishOnly` — build + stricter lint (`.eslintrc.prepublish.js`); runs automatically on `npm publish`.
- `npm test` — run the Vitest suite once. `npm run test:watch` for watch mode.

## Core rules

These bite often; the linked docs carry the full reasoning.

- **No runtime dependencies — ever.** n8n forbids verified community nodes from declaring any runtime `dependencies`; `package.json` must carry only `devDependencies` and the `n8n-workflow` peer dep. This is a hard publishing gate, not a style preference — don't reach for an SDK or helper library; hand-roll it (see the pure-JS SHA-256 and multipart builders) or it won't pass verification. → [n8n verification rules](https://docs.n8n.io/integrations/community-nodes/build-community-nodes/#submit-your-node-for-verification-by-n8n)
- **Tests use Vitest, not Jest.** `*.test.ts` is excluded from `tsconfig.json`, so any shared test util importing `vitest` must also be excluded or it leaks into `dist/`. → [docs/architecture.md#testing](docs/architecture.md#testing)
- **Three interaction flows** (signed Upload API / HTTP Basic auth / delivery-URL construction with no API call) — pick the right one when adding an op. → [docs/architecture.md#three-flows](docs/architecture.md#three-flows)
- **Transformation logic lives in the component builders**, not the handlers — Multi-Step is a second consumer and must not drift. Change the builder, not a handler. Transforms compose three ways (single op / in-node Multi-Step / cross-node `Continue From Transformation`); the cross-node bridge is a contract — an op's `transformation` output maps to the next op's `continueFromTransformation` input. The Widgets `videoPlayer` also consumes a `transformation` string (embed-URL + HLS caveats). → [docs/transforms.md](docs/transforms.md)
- **Mirror the Cloudinary API for field/option/output names** (`type`, `public_id`, `resource_type`, …) — don't invent or prefix when an API name exists. → [docs/conventions.md#naming-mirror-the-cloudinary-api](docs/conventions.md#naming-mirror-the-cloudinary-api)
- **Saved-workflow JSON is a public contract** — evolve additively or bump `typeVersion`; never rename a param `name` or option `value`. → [docs/backwards-compat.md](docs/backwards-compat.md)
- **Structured metadata is a pipe-separated string, not JSON** — always go through `metadataToPipeString`. → [docs/conventions.md#structured-metadata-format](docs/conventions.md#structured-metadata-format)

## Architecture at a glance

A declarative node class + a per-operation handler map + small util files:

- [Cloudinary.node.ts](nodes/Cloudinary/Cloudinary.node.ts) — `INodeTypeDescription`; fields from [descriptions/](nodes/Cloudinary/descriptions/) drive the UI via `displayOptions.show` on `resource` + `operation`. `execute()` is a thin loop: resolve creds → look up `operationHandlers[`${resource}:${operation}`]` → wrap the returned JSON into `INodeExecutionData`.
- [operations/](nodes/Cloudinary/operations/) — one file per operation, grouped by resource; each exports an `OperationHandler`. [operations/index.ts](nodes/Cloudinary/operations/index.ts) maps `${resource}:${operation}` → handler.
- [cloudinary.utils.ts](nodes/Cloudinary/cloudinary.utils.ts) — signing, multipart, URL/delivery builders, metadata serialization, error extraction.

**Adding an operation:** (1) add it to the matching `operation` options block + any fields it needs (with the right `displayOptions.show`) under [descriptions/](nodes/Cloudinary/descriptions/); (2) drop a handler file in `operations/<resource>/`; (3) register it in [operations/index.ts](nodes/Cloudinary/operations/index.ts). No change to `execute()` needed.

The full file map, the three interaction flows, the testing setup, n8n integration points, and the build-output contract are in **[docs/architecture.md](docs/architecture.md)**.
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# CLAUDE.md

Project guidance for coding agents lives in [AGENTS.md](AGENTS.md) (the cross-agent standard). The line below imports it so Claude Code loads it as memory; keep all content in `AGENTS.md` and the `docs/` it links.

@AGENTS.md
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,53 @@ On this page, you'll find a list of operations the Cloudinary node supports.
* Update asset metadata fields
* Get tags
* Get structured metadata definitions
* Search assets
Comment thread
eitanp461 marked this conversation as resolved.
* Transform (builds a delivery URL — no API call)
* Image: Optimize, Resize, Crop, Convert
* Video: Optimize, Resize, Crop, Trim, Thumbnail
* Custom Transformation (raw transformation string)
* Compose: Combine Transformations (multi-step, in one node)
* Widget
* Video Player (builds an embed URL + player config)

### Search assets

Uses the [Cloudinary Search API](https://cloudinary.com/documentation/search_api) to find assets by tag, folder, metadata, or any supported expression (e.g. `tags=cat AND uploaded_at>1d`, `folder:products/*`, `tags="back to school"`).

The node emits **one n8n item per matching asset**, so downstream nodes can map over results directly without a Split Out step.

- **Resource Types** — Cloudinary's Search API defaults to image-only when no `resource_type:` clause is in the expression. This node injects the clause automatically based on the Resource Types you select (defaults to `image`). To search across all types, select all three. If your expression already contains a `resource_type:` clause, the selection is ignored.
- **Return All** — when enabled, the node automatically paginates through Cloudinary's `next_cursor` until all matching assets have been returned. When disabled, only the first page (up to *Max Results*, capped at 500) is returned.
- **Rate limits** — the node surfaces `429`/`420` responses with a clear "rate limit exceeded" error including the server's `Retry-After`.
- **Eventual consistency** — newly uploaded assets may take a few seconds to appear in search results. Avoid searching for something you just uploaded in the same workflow without a delay.

### Transformations and chaining

A **transformation** is the instruction string Cloudinary applies when delivering an asset — for example `c_fill,w_400` (crop to 400px wide) or `f_auto/q_auto` (auto format and quality). Each Transform operation builds one and returns it on the output as **`transformation`**, alongside the finished `secure_url`.

Real edits usually need several transformations applied in sequence ("first crop, then optimize"). In Cloudinary's URL these are **components joined with `/`**, applied left to right — each acts on the output of the one before it. For example `c_fill,w_400/f_auto/q_auto` means *crop, then auto-format, then auto-quality*. There are three ways to build a chain with this node, from simplest to most flexible:

1. **One operation.** A single Transform op (e.g. *Image: Resize*) when one step is all you need.

2. **Combine Transformations (one node).** The *Compose → Combine Transformations* operation takes an ordered, reorderable list of steps and chains them inside a single node. Best when the whole recipe is known up front and lives in one place.

3. **Continue From Transformation (across nodes).** Every chainable Transform op has an optional **Continue From Transformation** field. This is the bridge that makes chaining work across separate nodes, and it relies on one property mapping:

> The upstream op's **`transformation`** output → the downstream op's **Continue From Transformation** input.

Wire it with an expression: set *Continue From Transformation* to `{{ $json.transformation }}`. The node prepends that incoming string before the current op's own transformation, so the two compound into one delivery URL. Example — a *Resize* node outputs `transformation: "c_fill,w_400"`; feed it into an *Optimize* node's *Continue From Transformation*, and the Optimize node delivers `c_fill,w_400/f_auto/q_auto`.

This field is available on all single-purpose Transform ops. It is intentionally **not** offered on *Custom Transformation* (you already control the entire string there) or *Combine Transformations* (which chains its own steps internally).

**Tip:** *Resize* and *Crop* don't auto-optimize. To also get `f_auto/q_auto`, either add an *Optimize* step in *Combine Transformations*, or chain an *Optimize* node after them via *Continue From Transformation*.

### Video Player (Widget)

The *Widget → Video Player* operation builds a [Cloudinary Video Player](https://cloudinary.com/documentation/video_player_quickstart_guide) embed URL plus a player config. Its **Transformation** field applies to the **played video stream** (it accepts the same `transformation` string the Transform ops emit, so you can wire `{{ $json.transformation }}` here too). A few constraints worth knowing:

- **Use video-capable transforms.** Image-only effects (e.g. `e_grayscale`) are silently ignored on video and have no visible effect. See [video effects](https://cloudinary.com/documentation/video_effects_and_enhancements).
- **Adaptive streaming vs. format selection.** If you select an adaptive-streaming **Source Type** (HLS or MPEG-DASH), the transformation must **not** pin a delivery format (an `f_` component such as `f_auto:video`, which an *Optimize* step adds). The two are incompatible — Cloudinary can't apply a streaming profile to a fixed non-streaming format. The node detects this and fails with a clear message; for adaptive streaming, keep the transformation to resize/crop/trim only.
- **Aspect Ratio crops on top of your Transformation.** Setting an **Aspect Ratio** makes the player re-crop the video to those proportions using the **Crop Mode** field (*Smart*, *Fill*, or *Pad* — default *Smart*). With the default *Smart* mode, that re-crop can't be combined with a Transformation that already crops the video, and the player rejects it. The node fails fast with a clear message; to render either set *Crop Mode* to *Fill* or *Pad*, or clear *Aspect Ratio* and let your Transformation define the framing.

## Supported authentication methods

Expand All @@ -34,6 +81,10 @@ If you're a user with a Master admin, Admin, or Technical admin role, you can fi
4. From the top of the page copy the **Cloud name**.
5. Enter the cloud name, api key and api secret to your n8n credential.

#### Private CDN / custom delivery hostname (advanced)

Most users can skip this. If your organization is on a **Advanced plan** that uses a [private CDN distribution or a custom delivery hostname (CNAME)](https://cloudinary.com/documentation/advanced_url_delivery_options#private_cdns_and_custom_delivery_hostnames_cnames), enable **Private CDN** in the credential and enter your **Custom Delivery Hostname** so the node builds delivery URLs against your private distribution instead of the default `res.cloudinary.com`. Leave these off if you're unsure — they don't apply to standard accounts.


## Related resources

Expand Down
18 changes: 18 additions & 0 deletions credentials/CloudinaryApi.credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,24 @@ export class CloudinaryApi implements ICredentialType {
default: '',
typeOptions: { password: true },
},
{
displayName: 'Private CDN',
name: 'privateCdn',
type: 'boolean',
default: false,
description:
'Whether your account delivers from a private CDN distribution (<cloud>-res.cloudinary.com). Only affects the delivery URLs built by Transform operations.',
},
{
displayName: 'Custom Delivery Hostname',
name: 'secureDistribution',
type: 'string',
default: '',
placeholder: 'assets.example.com',
description:
'Custom delivery hostname (CNAME) for your private CDN account. Leave empty to use the default <cloud>-res.cloudinary.com subdomain.',
displayOptions: { show: { privateCdn: [true] } },
},
];

// This tells how this credential is authenticated
Expand Down
Loading
Loading