diff --git a/.gitignore b/.gitignore index 21fd20c..35d5ed2 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,6 @@ coverage/ *.temp .direnv .env + +# Embedded repo (nested clone — not a submodule) +jam-nodes/ diff --git a/package-lock.json b/package-lock.json index 36b2561..888c928 100644 --- a/package-lock.json +++ b/package-lock.json @@ -925,6 +925,7 @@ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -1233,6 +1234,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -1908,6 +1910,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -2247,6 +2250,7 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=16.9.0" } @@ -2589,6 +2593,7 @@ "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "dev": true, "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -2613,6 +2618,7 @@ "resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz", "integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==", "license": "MIT", + "peer": true, "engines": { "node": ">= 10.16.0" } @@ -3208,6 +3214,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3429,6 +3436,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -3441,6 +3449,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -3454,6 +3463,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -4015,6 +4025,7 @@ "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -4121,6 +4132,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -4364,6 +4376,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -4379,7 +4392,7 @@ }, "packages/core": { "name": "@jam-nodes/core", - "version": "0.2.4", + "version": "0.2.5", "license": "MIT", "dependencies": { "jsonpath-plus": "^10.0.0" @@ -4433,7 +4446,7 @@ }, "packages/nodes": { "name": "@jam-nodes/nodes", - "version": "0.2.4", + "version": "0.2.5", "license": "MIT", "dependencies": { "@jam-nodes/core": "^0.2.1" @@ -4448,7 +4461,7 @@ }, "packages/playground": { "name": "@jam-nodes/playground", - "version": "0.1.1", + "version": "0.1.2", "license": "MIT", "dependencies": { "@jam-nodes/core": "*", diff --git a/packages/nodes/src/integrations/dataforseo/seo-audit.ts b/packages/nodes/src/integrations/dataforseo/seo-audit.ts index d00a899..7611757 100644 --- a/packages/nodes/src/integrations/dataforseo/seo-audit.ts +++ b/packages/nodes/src/integrations/dataforseo/seo-audit.ts @@ -339,9 +339,12 @@ export const seoAuditNode = defineNode({ let isPassed: boolean; if (POSITIVE_CHECKS.has(checkId)) { + // Positive checks: passed=true means the good thing IS present (e.g. is_https) isPassed = checkData.passed; } else { - isPassed = checkData.passed; + // Normal checks: passed=true means the problem is ABSENT (i.e. the page is clean) + // DataForSEO sets passed=true to mean "no issue found", so invert for our tracking + isPassed = !checkData.passed; } if (isPassed) { diff --git a/packages/nodes/src/transform/filter.ts b/packages/nodes/src/transform/filter.ts index cb3e7aa..24ae9a1 100644 --- a/packages/nodes/src/transform/filter.ts +++ b/packages/nodes/src/transform/filter.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { defineNode } from '@jam-nodes/core'; +import { resolvePath } from '../utils/resolve-path.js'; /** * Filter operator schema @@ -44,28 +45,6 @@ export const FilterOutputSchema = z.object({ export type FilterOutput = z.infer; -/** - * Resolve a nested path on an object - */ -function resolvePath(obj: unknown, path: string): unknown { - // Empty path means use the item itself - if (!path) { - return obj; - } - - const parts = path.split('.'); - let current: unknown = obj; - - for (const part of parts) { - if (current === null || current === undefined) { - return undefined; - } - current = (current as Record)[part]; - } - - return current; -} - /** * Evaluate filter condition */ diff --git a/packages/nodes/src/transform/map.ts b/packages/nodes/src/transform/map.ts index c169fad..37592ef 100644 --- a/packages/nodes/src/transform/map.ts +++ b/packages/nodes/src/transform/map.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; import { defineNode } from '@jam-nodes/core'; +import { resolvePath } from '../utils/resolve-path.js'; /** * Input schema for map node @@ -23,34 +24,6 @@ export const MapOutputSchema = z.object({ export type MapOutput = z.infer; -/** - * Resolve a nested path on an object - */ -function resolvePath(obj: unknown, path: string): unknown { - const parts = path.split('.'); - let current: unknown = obj; - - for (const part of parts) { - if (current === null || current === undefined) { - return undefined; - } - - // Handle array access like "[0]" - const arrayMatch = part.match(/^\[(\d+)\]$/); - if (arrayMatch) { - if (Array.isArray(current)) { - current = current[parseInt(arrayMatch[1]!, 10)]; - } else { - return undefined; - } - } else { - current = (current as Record)[part]; - } - } - - return current; -} - /** * Map node - extract a property from each item in an array. * diff --git a/packages/nodes/src/utils/http.ts b/packages/nodes/src/utils/http.ts index 702dcb6..e389c07 100644 --- a/packages/nodes/src/utils/http.ts +++ b/packages/nodes/src/utils/http.ts @@ -105,6 +105,14 @@ export async function fetchWithRetry( await sleep(delayMs); continue; } + + // All retries exhausted — throw instead of returning the 429 response + const errorText = await response.text(); + throw new FetchRetryError( + `Rate limit exceeded after ${maxRetries} attempts`, + 429, + errorText + ); } // Retry on server errors diff --git a/packages/nodes/src/utils/index.ts b/packages/nodes/src/utils/index.ts index 3064450..9aa470e 100644 --- a/packages/nodes/src/utils/index.ts +++ b/packages/nodes/src/utils/index.ts @@ -5,3 +5,5 @@ export { FetchRetryError, type FetchWithRetryOptions, } from './http.js'; + +export { resolvePath } from './resolve-path.js'; diff --git a/packages/nodes/src/utils/resolve-path.ts b/packages/nodes/src/utils/resolve-path.ts new file mode 100644 index 0000000..b4b4d7c --- /dev/null +++ b/packages/nodes/src/utils/resolve-path.ts @@ -0,0 +1,61 @@ +/** + * Shared path resolution utility for transform nodes. + * + * Resolves a dot-notation path string against an arbitrary object. + * This mirrors the same logic as `ExecutionContext.resolveNestedPath`, + * but operates on any object rather than the workflow variable store. + * + * Supported syntax: + * - Dot notation: "contact.email" + * - Keyed array access: "contacts[0].name" + * - Standalone array index: "[0].name" (when current value is an array) + * - Empty path returns the object itself + * + * @example + * resolvePath({ a: { b: [1, 2] } }, 'a.b[1]') // → 2 + * resolvePath([{ id: 1 }, { id: 2 }], '[0].id') // → 1 + * resolvePath({ score: 42 }, '') // → { score: 42 } + */ +export function resolvePath(obj: unknown, path: string): unknown { + // Empty path means use the value itself + if (!path) { + return obj; + } + + const parts = path.split('.'); + let current: unknown = obj; + + for (const part of parts) { + if (current === null || current === undefined) { + return undefined; + } + + // Handle keyed array access like "contacts[0]" + const keyedArrayMatch = part.match(/^(\w+)\[(\d+)\]$/); + if (keyedArrayMatch) { + const [, key, index] = keyedArrayMatch; + current = (current as Record)[key!]; + if (Array.isArray(current)) { + current = current[parseInt(index!, 10)]; + } else { + return undefined; + } + continue; + } + + // Handle standalone array index like "[0]" + const standaloneIndexMatch = part.match(/^\[(\d+)\]$/); + if (standaloneIndexMatch) { + if (Array.isArray(current)) { + current = current[parseInt(standaloneIndexMatch[1]!, 10)]; + } else { + return undefined; + } + continue; + } + + current = (current as Record)[part]; + } + + return current; +}