feat: per-operation refactor + Asset, Transform, and Admin operations#7
Open
eitanp461 wants to merge 21 commits into
Open
feat: per-operation refactor + Asset, Transform, and Admin operations#7eitanp461 wants to merge 21 commits into
eitanp461 wants to merge 21 commits into
Conversation
Restructure the Cloudinary node from a single monolithic execute() into a
declarative handler map: one file per operation grouped by resource, with
field definitions split into dedicated description modules. execute() is now
a thin dispatch loop over input items.
Add an Admin "Search Assets" operation that emits one item per matching
asset, auto-injects a resource_type clause from the selected types, supports
automatic pagination ("Return All"), and surfaces rate-limit and invalid
expression errors with actionable messages.
Harden structured-metadata serialization: multi-value fields render as a
bracketed list of quoted strings and delimiter characters are escaped in
every value, matching Cloudinary's documented metadata format. Sanitize the
multipart upload filename so it can't break request framing.
Introduce a Vitest test suite covering the metadata/signature/URL helpers
and the per-operation request contracts, and share common request headers
and auth across handlers.
- vitest.config.ts: update n8n-workflow alias from dist/index.js to dist/cjs/index.js. n8n-workflow@2.16 reorganised its build output and no longer ships a top-level dist/index.js, so the previous alias resolved to nothing and every test file failed at import time with "Cannot find package 'n8n-workflow'". - .nvmrc: bump from v22.11.0 to v24.16.0 (current Active LTS, Krypton). Vitest 4 transitively pulls in std-env@4 (ESM-only) and require()s it from its CJS config loader; unflagged require(ESM) needs Node >=20.19 or >=22.12, so v22.11.0 hit ERR_REQUIRE_ESM on `vitest run`. Moving to 24.16.0 also matches engines.node already declared in package.json. All 86 tests pass after these changes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Cloudinary.node.ts: switch inputs/outputs to the 'main' string literals and drop the NodeConnectionType import. Change group from the bogus ['Cloudinary'] to ['transform'] so the node lands in the right category in the n8n node picker. - package.json: pin the n8n-workflow peer range to ^2.0.0 instead of '*'. The previous wildcard let any host install try to load this node against incompatible majors; we already build and test against the 2.x API. - package-lock.json: regenerated to match. - .github/workflows/release.yml: drive setup-node from .nvmrc instead of hardcoding '22', and enable npm cache so releases install faster and stay in lockstep with the local toolchain. - CLAUDE.md: rewrite the backwards-compatibility section to spell out the frozen-by-string vs frozen-by-meaning vs free-to-change axes, call out option values and displayOptions narrowing as breaking, document the typeVersion escape hatch, and separate workflow-JSON compatibility from runtime-host compatibility. Minor tightening elsewhere. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Step 1 of the asset-crud reorganization. UI-only relabel — `value` strings (`updateAsset`, `admin`) are frozen-by-string per CLAUDE.md so saved workflows still resolve. `Search Assets` belongs with the entity it operates on; moved its operation entry, handler-map key, and field `displayOptions.show.resource` from `admin` to `updateAsset`. Search is not yet released, so no compatibility shim is needed. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the first new operation in the Asset CRUD effort. Identifies the asset by its immutable asset_id and hits `GET /resources/:asset_id` — a single-field form instead of the legacy three-field (resource_type, type, public_id) shape. Scope notes: - Deliberately asset_id-only. No `identifyBy` mode selector — for new ops, a one-field form is strictly cleaner UX than gating it behind a picker most users wouldn't need. - Existing `updateTags` / `updateMetadata` are not modified; their public_id surface stays exactly as it shipped. asset_id support for those ops is the job of the upcoming `asset` resource (see plan). Adds `buildResourceByAssetIdUrl` utility, the `getOptions` collection (colors, faces, image_metadata, pages, phash, coordinates, accessibility_analysis, derived_next_cursor), and per-handler + URL-builder tests. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds an Asset → Delete Assets op backed by the documented public_id
endpoint:
DELETE /v1_1/{cloud}/resources/{resource_type}/{type}?public_ids=...
Cloudinary's delete endpoint is public_id-based, not asset_id-based —
an earlier draft assumed otherwise and failed at runtime with
"Illegal ids given". The field accepts:
- a single public_id (no commas needed),
- a comma-separated list, or
- an array from an n8n expression (e.g. `{{ $items().map(...) }}`).
Arrays are pre-joined to a CSV before being placed in `qs` because
n8n's default query serializer turns JS arrays into `public_ids[0]=...`
(bracketed indices), which Cloudinary rejects with "public_ids must be
a list of strings or a comma separated string". Joining to CSV
side-steps the serializer entirely.
New helpers in cloudinary.utils.ts:
- buildResourceDeleteUrl(cloud, resourceType, type)
- splitCsvIds(csv) — trims and drops blanks
Adds 8 handler tests covering URL/method, single-string, CSV trimming,
array-from-expression, non-image resource routing, options merging,
and empty-input guards, plus unit tests for the new utils.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Splits the existing Update Asset surface into two resources: - Asset: asset_id-keyed Get/Update/Search/Delete plus tag and metadata operations. asset_id is a single global key, so callers do not need to juggle (resource_type, type) coordinates. - Asset (Legacy): kept at the bottom of the dropdown; only the public_id-keyed operations that shipped on master are exposed. Saved workflows continue to load unchanged. Update Asset Tags gains a Mode selector with backward-compatible default: - Set (default): existing behavior, replaces the tag list via the resource endpoint with Basic auth. - Append: new path hitting POST /:resource_type/tags with command=add and signed auth, preserving existing tags. Shared appendTags helper lives in operations/tagAppend.ts and is wired into both resources' updateTags handlers. The tag-action endpoint scopes lookups by (resource_type, type) and silently returns public_ids: [] for assets stored under a non-default type. Append mode now exposes a Type field (labeled to match the asset object's "type" property) and threads it into the signed body, so authenticated/private assets resolve correctly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The codex "node" field declared n8n-nodes-base.cloudinary, the reserved namespace for n8n's built-in core nodes. This package's real node type is n8n-nodes-cloudinary.cloudinary (package name + node "name"). n8n's loader binds a .node.json codex to its node by filename adjacency (Cloudinary.node.json <-> Cloudinary.node.js) and never reads this field, so the change is cosmetic — but the previous value was misleading, implying this community node lived in n8n-nodes-base. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The node already uploads and manages videos, but nothing advertised it: a "video" search in the n8n node panel didn't list Cloudinary, and the docs links and description only mentioned images. - Add a codex "alias" array (video, image, media, transform, transcode, optimize, upload, CDN, asset, DAM). The node creator's search keys only on displayName and codex.alias, so this is what makes Cloudinary match "video" (the same mechanism the Google Gemini and MiniMax nodes use). - Mention images and videos in the node description (tooltip + the AI-agent tool schema exposed via usableAsTool). - Add the video best practices guide to primaryDocumentation. All UI-only metadata, so no typeVersion bump. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduces transform:optimizeImage, resizeImage, cropImage, convertImage, optimizeVideo, trimVideo, videoThumbnail, customTransformation, and multiStep. All operations build a Cloudinary delivery URL (no API call); callers chain n8n's HTTP Request node to materialize bytes. Delivery host resolution mirrors the SDK: default res.cloudinary.com (shared), <cloud>-res.cloudinary.com (private CDN), or custom CNAME — driven by new optional privateCdn / secureDistribution credential fields (backwards-compat; default off reproduces today's behaviour. Component builders in operations/transform/shared.ts are shared between standalone ops and the multi-step chain; both consumers map to the same builder to prevent drift. Handlers carry 25 new Vitest cases (156 total) asserting returned JSON rather than a request spy. Also includes: trailingMediaFormat fix for public_ids that embed their own extension, codex alias additions (resize/crop/thumbnail), and CLAUDE.md documentation for the third flow + no-HTTP test pattern. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> EOF )
…l chaining Transform resource increment on top of the 8 delivery-URL operations: - Video Player: new operation that builds a player.cloudinary.com embed URL plus a player_config object (autoplay, loop, muted, source types, poster, colors, skin, fluid/aspect ratio, font, and advanced toggles). Emits no HTTP request. Embed params use camelCase, matching the embed-page docs (snake_case is also accepted by the embedder — verified in-browser). - Multi-Step: crop step now supports crop-by-aspect-ratio (cropAspectWidth + aspectRatio) alongside crop-by-dimensions (cropWidth/cropHeight), mapping each to the shared crop builder. - Video Thumbnail: optional base transformation (thumbnailBaseTransformation) prepended before the frame selector, so a thumbnail can chain from a previous Trim/Multi-Step transform. - USER_AGENT now reports n8n-nodes-cloudinary/<version> sourced from package.json instead of a hardcoded string. - Bump version 0.0.9 -> 0.1.0; gitignore examples/ (kept local-only) and drop the previously committed social-video POC; CLAUDE.md documents the two-surface field-definition split for transform fields. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Restructure the always-loaded agent context to cut per-turn token cost. CLAUDE.md becomes a one-line `@AGENTS.md` import stub (committed plain text — no symlink, so it survives Windows/CI), and AGENTS.md holds a lean core: commands, six one-line core rules, and a one-paragraph architecture map. The deep reference (three flows, transform builders, backwards-compat taxonomy, conventions) moves verbatim into docs/, linked with plain markdown so it loads only when a task touches that area. AGENTS.md is the cross-agent standard, read directly by Codex/Cursor/etc.; the CLAUDE.md stub keeps Claude Code working with no duplication. Always-loaded footprint: ~109 lines -> ~43. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
CI lint failed on node-param-description-miscased-json at the new thumbnailBaseTransformation field. The rule flags the word "json" in descriptions and wants "JSON" — but here it is the n8n expression variable $json, which is correctly lowercase. The autofix (and an earlier manual edit) rewrote it to $JSON, which is undefined in n8n and would hand users a broken chaining example. Keep $json and suppress the false positive on that single line with a documented eslint-disable, so the guidance stays runtime-correct. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address PR review findings on the image/video Transform feature: - fetch/social delivery URLs no longer get a spurious file extension. The format suffix (explicit Convert/Thumbnail format or one recovered from a dotted public_id) is now appended only for stored-asset types (upload/private/authenticated), whose public_id is opaque. For fetch and social sources the identifier is a remote URL / external id, so the conversion rides in the transformation (e.g. f_webp) instead. Keeps one invariant: result.format is set iff the URL carries a trailing .<format>. - fetch remote URLs are now smart-escaped before going into the path, so query strings and fragments (?sig=...#v1) are percent-encoded and reach Cloudinary as the source URL instead of being split off by the browser. Path-safe chars stay readable, mirroring the Cloudinary SDKs. - Multi-Step descriptor: step-specific fields are gated by displayOptions on stepType/cropMode, and the Quality field gets its QUALITY_OPTIONS so an Optimize step can pick a quality level. - Comments: drop internal/working-context detail (delivery-code claim and "see plan") in favor of public, observed behavior. - Release: publish with --provenance and set n8n-workflow peerDependency to "*" per the n8n community-node standard. Adds regression tests for stored-vs-fetch extension handling and for fetch URL escaping (query string, fragment, spaces). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Based on an agentic-POV review of what an AI agent actually sees when building a flow with this node: - Node description now advertises transform/optimize/deliver and video player capabilities, not just upload/manage — it is the agent's first filter when the node is offered as a tool (usableAsTool). - Each Transform operation description states its output (secure_url plus a reusable transformation string; embed_url plus player_config for the Video Player), so an agent can chain steps without running the node first. - The delivery type field leads with the recommended "Upload" and makes the signed-URL limitation prominent (those delivery URLs fail to load). Description only — no behavior change. - Resource selector options get one-line purpose descriptions, the legacy resource is relabeled "Asset (Legacy, by Public ID)" and its operations marked Deprecated, reducing wrong-resource picks (e.g. Library was easy to mistake for asset listing). - Resize/Crop field help notes they do not auto-optimize, and to chain Optimize Image or use Multi-Step for f_auto/q_auto. All changes are UI / tool-schema text — no saved-workflow contract impact. The Multi-Step Quality dropdown flagged during review was already fixed in e3d0730, so no change was needed there. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the hand-rolled buildDeliveryUrl() with @cloudinary/url-gen so the SDK owns URL path construction and appends the ?_a= analytics slug automatically (sdkCode I = n8n, product B = Integrations, registered in Appendix C of the Cloudinary SDK analytics spec). Key details: - forceVersion: false suppresses the SDK default v1 on nested public IDs - fixFetchUrl() post-processes fetch URLs to encode & and # that the SDK leaves bare, preventing CDN/browser misparse of the fetch source URL - Analytics is correctly suppressed when the fetch source URL contains a literal ? (per the Cloudinary analytics spec) - ? in non-fetch public IDs now encodes as %3F — correct since a bare ? in a delivery path starts the query string at the CDN level Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A Cloudinary CNAME (secure_distribution) is only available on private-CDN accounts, so the two fields always appear together in a real account. Hiding secureDistribution behind privateCdn: true in the credential UI prevents the impossible state and lets the SDK config be a direct translation of credentials with no inference needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
n8n does not mandate zero runtime dependencies for community nodes — the rule was a project-level choice that is now superseded. Future maintainers will see the dependencies section in package.json directly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Expands the Cloudinary community node from a single declarative class into a maintainable, multi-resource node with full test coverage. Adds Asset, Transform, and Admin operations on top of the existing Upload flow, and restructures the codebase around per-operation handlers so new operations are additive.
This is a large branch; it groups into four themes below.
1. Architecture refactor
Cloudinary.node.tsinto a thinexecute()loop plus a per-operation handler map keyed${resource}:${operation}(operations/index.ts). Adding an operation no longer touchesexecute().displayOptions.showonresource+operation.form-datadep), URL builders, metadata serialization, error extraction. Zero runtime deps beyond then8n-workflowpeer dep (pure-JS SHA-256 in sha256.utils.ts).2. Asset & Admin operations
asset_id-based): Get Asset, Update Display Name, plus Append-mode tag updates.public_id).type,public_id,resource_type,tags,context) so Search results pipe straight into later operations.3. Transform resource (delivery-URL construction)
buildDeliveryUrlresolves shared / private-CDN / custom-CNAME hosts from optionalprivateCdn/secureDistributioncredentials.player.cloudinary.com/embed/) and a self-hostedplayer_configJSON. Signed delivery (s--SIG--) is intentionally not implemented yet (disclaimed in the UI).4. Toolchain, CI & docs
IExecuteFunctionsasserting the request contract (URL / method / auth / body) — no network calls. Delivery-URL ops assert returned JSON instead.package-lock.jsonagainst the public npm registry; aligned node descriptor, peer dep range,.nvmrc, and CI.Testing
npm test— Vitest suite (utils + per-operation handler contracts).npm run lint/npm run buildclean.examples/.