@pipedream/rendex 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/rendex.app.mjs CHANGED
@@ -4,6 +4,12 @@ export default {
4
4
  type: "app",
5
5
  app: "rendex",
6
6
  propDefinitions: {
7
+ markdown: {
8
+ type: "string",
9
+ label: "Markdown",
10
+ description: "Markdown to render. Rendex applies clean default typography (no CSS needed) and converts it to an image or PDF.",
11
+ optional: true,
12
+ },
7
13
  html: {
8
14
  type: "string",
9
15
  label: "HTML",
@@ -29,6 +35,356 @@ export default {
29
35
  "pdf",
30
36
  ],
31
37
  },
38
+ watchId: {
39
+ type: "string",
40
+ label: "Watch",
41
+ description: "The watch to act on.",
42
+ async options({ prevContext }) {
43
+ // Cursor-based load-more: GET /v1/watches returns `nextCursor` (null on the
44
+ // last page). A null cursor in prevContext means we already paged to the end.
45
+ const cursor = prevContext?.cursor;
46
+ if (cursor === null) {
47
+ return [];
48
+ }
49
+ const { data } = await this.listWatches({
50
+ params: {
51
+ status: "all",
52
+ limit: 100,
53
+ cursor,
54
+ },
55
+ });
56
+ return {
57
+ options: (data?.items ?? []).map((watch) => ({
58
+ label: watch.name
59
+ ? `${watch.name} (${watch.url})`
60
+ : watch.url,
61
+ value: watch.id,
62
+ })),
63
+ context: {
64
+ cursor: data?.nextCursor ?? null,
65
+ },
66
+ };
67
+ },
68
+ },
69
+ name: {
70
+ type: "string",
71
+ label: "Name",
72
+ description: "A human-friendly label for the watch.",
73
+ optional: true,
74
+ },
75
+ intervalMinutes: {
76
+ type: "integer",
77
+ label: "Interval (minutes)",
78
+ description: "Minutes between automatic checks. Per-plan floors apply (Free 1440, Starter 180, Pro 30, Enterprise 5).",
79
+ optional: true,
80
+ min: 5,
81
+ max: 43200,
82
+ },
83
+ diffMode: {
84
+ type: "string",
85
+ label: "Diff Mode",
86
+ description: "How a change is detected: `visual` (pixel diff), `text` (extracted text), or `both`.",
87
+ optional: true,
88
+ options: [
89
+ "visual",
90
+ "text",
91
+ "both",
92
+ ],
93
+ },
94
+ threshold: {
95
+ type: "string",
96
+ label: "Threshold",
97
+ description: "Fraction of the page (between `0` and `1`) that must change to count as a change. Defaults to `0.01`.",
98
+ optional: true,
99
+ },
100
+ webhookUrl: {
101
+ type: "string",
102
+ label: "Webhook URL",
103
+ description: "URL that receives an HMAC-signed POST when a change is detected. Requires a Starter plan or higher.",
104
+ optional: true,
105
+ },
106
+ notifyEmail: {
107
+ type: "string",
108
+ label: "Notify Email",
109
+ description: "Email to alert on a change. Must match the email on your Rendex account.",
110
+ optional: true,
111
+ },
112
+ paused: {
113
+ type: "boolean",
114
+ label: "Paused",
115
+ description: "When `true`, the watch is created/left paused and is not scheduled (no credits charged).",
116
+ optional: true,
117
+ },
118
+ limit: {
119
+ type: "integer",
120
+ label: "Limit",
121
+ description: "Maximum number of items to return (1–100).",
122
+ optional: true,
123
+ min: 1,
124
+ max: 100,
125
+ },
126
+ cursor: {
127
+ type: "string",
128
+ label: "Cursor",
129
+ description: "Pagination cursor returned as `nextCursor` by a previous response.",
130
+ optional: true,
131
+ },
132
+ extractFormat: {
133
+ type: "string",
134
+ label: "Extract Format",
135
+ description: "Format of the extracted content.",
136
+ optional: true,
137
+ default: "markdown",
138
+ options: [
139
+ "markdown",
140
+ "json",
141
+ "html",
142
+ ],
143
+ },
144
+ expiresIn: {
145
+ type: "integer",
146
+ label: "Expires In (seconds)",
147
+ description: "How long the hosted render link stays valid (3600–2592000). Defaults to 30 days.",
148
+ optional: true,
149
+ min: 3600,
150
+ max: 2592000,
151
+ },
152
+ width: {
153
+ type: "integer",
154
+ label: "Width",
155
+ description: "Viewport width in pixels (320–3840). Defaults to `1280`.",
156
+ optional: true,
157
+ min: 320,
158
+ max: 3840,
159
+ },
160
+ height: {
161
+ type: "integer",
162
+ label: "Height",
163
+ description: "Viewport height in pixels (240–2160). Defaults to `800`.",
164
+ optional: true,
165
+ min: 240,
166
+ max: 2160,
167
+ },
168
+ fullPage: {
169
+ type: "boolean",
170
+ label: "Full Page",
171
+ description: "Capture the entire scrollable page instead of just the viewport.",
172
+ optional: true,
173
+ },
174
+ quality: {
175
+ type: "integer",
176
+ label: "Quality",
177
+ description: "Image quality from 1–100 (JPEG/WebP only).",
178
+ optional: true,
179
+ min: 1,
180
+ max: 100,
181
+ },
182
+ delay: {
183
+ type: "integer",
184
+ label: "Delay (ms)",
185
+ description: "Milliseconds to wait after the page settles before capturing (0–10000).",
186
+ optional: true,
187
+ min: 0,
188
+ max: 10000,
189
+ },
190
+ darkMode: {
191
+ type: "boolean",
192
+ label: "Dark Mode",
193
+ description: "Emulate `prefers-color-scheme: dark` when rendering.",
194
+ optional: true,
195
+ },
196
+ deviceScaleFactor: {
197
+ type: "string",
198
+ label: "Device Scale Factor",
199
+ description: "Pixel density multiplier from `1` to `3` (e.g. `2` for retina). Defaults to `2`.",
200
+ optional: true,
201
+ },
202
+ device: {
203
+ type: "string",
204
+ label: "Device",
205
+ description: "Device preset that sets viewport size, scale, and user agent in one shot. Explicit width/height/user agent still win.",
206
+ optional: true,
207
+ options: [
208
+ "desktop",
209
+ "iphone_15",
210
+ "iphone_se",
211
+ "pixel_8",
212
+ "ipad",
213
+ "ipad_pro",
214
+ ],
215
+ },
216
+ selector: {
217
+ type: "string",
218
+ label: "Selector",
219
+ description: "Capture only the element matching this CSS selector instead of the whole page.",
220
+ optional: true,
221
+ },
222
+ hideSelectors: {
223
+ type: "string[]",
224
+ label: "Hide Selectors",
225
+ description: "CSS selectors for elements to hide (`display: none`) before capture — e.g. cookie banners, popups, sticky nav.",
226
+ optional: true,
227
+ },
228
+ blockAds: {
229
+ type: "boolean",
230
+ label: "Block Ads",
231
+ description: "Block known ad/tracker network requests. Enabled by default.",
232
+ optional: true,
233
+ },
234
+ blockCookieBanners: {
235
+ type: "boolean",
236
+ label: "Block Cookie Banners",
237
+ description: "Hide common cookie/consent walls (OneTrust, Cookiebot, Quantcast, …) via a curated CSS list.",
238
+ optional: true,
239
+ },
240
+ timeout: {
241
+ type: "integer",
242
+ label: "Timeout (seconds)",
243
+ description: "Maximum time to wait for the page to load (5–60). Defaults to `30`.",
244
+ optional: true,
245
+ min: 5,
246
+ max: 60,
247
+ },
248
+ waitUntil: {
249
+ type: "string",
250
+ label: "Wait Until",
251
+ description: "Navigation lifecycle event to wait for before capturing.",
252
+ optional: true,
253
+ options: [
254
+ "load",
255
+ "domcontentloaded",
256
+ "networkidle0",
257
+ "networkidle2",
258
+ ],
259
+ },
260
+ bestAttempt: {
261
+ type: "boolean",
262
+ label: "Best Attempt",
263
+ description: "Return a partial capture on timeout instead of erroring. Enabled by default.",
264
+ optional: true,
265
+ },
266
+ css: {
267
+ type: "string",
268
+ label: "Custom CSS",
269
+ description: "CSS injected into the page before capture (up to 50KB).",
270
+ optional: true,
271
+ },
272
+ js: {
273
+ type: "string",
274
+ label: "Custom JavaScript",
275
+ description: "JavaScript executed in the page before capture (up to 50KB).",
276
+ optional: true,
277
+ },
278
+ userAgent: {
279
+ type: "string",
280
+ label: "User Agent",
281
+ description: "Override the browser User-Agent header.",
282
+ optional: true,
283
+ },
284
+ cookies: {
285
+ type: "string[]",
286
+ label: "Cookies",
287
+ description: "Cookies to set before loading the page. Each item is a JSON object, e.g. `{\"name\":\"session\",\"value\":\"abc\",\"domain\":\"example.com\"}`. Requires a Starter plan or higher.",
288
+ optional: true,
289
+ },
290
+ headers: {
291
+ type: "object",
292
+ label: "Headers",
293
+ description: "Extra HTTP request headers to send with the page load, as a JSON object of key–value pairs, e.g. `{ \"Accept-Language\": \"de-DE\", \"X-Debug\": \"1\" }`. Requires a Starter plan or higher.",
294
+ optional: true,
295
+ },
296
+ pdfFormat: {
297
+ type: "string",
298
+ label: "PDF Format",
299
+ description: "Paper size for PDF output (ignored for image formats).",
300
+ optional: true,
301
+ options: [
302
+ "A4",
303
+ "Letter",
304
+ "Legal",
305
+ "Tabloid",
306
+ "A3",
307
+ ],
308
+ },
309
+ pdfLandscape: {
310
+ type: "boolean",
311
+ label: "PDF Landscape",
312
+ description: "Render the PDF in landscape orientation.",
313
+ optional: true,
314
+ },
315
+ pdfPrintBackground: {
316
+ type: "boolean",
317
+ label: "PDF Print Background",
318
+ description: "Include background graphics and colors in the PDF.",
319
+ optional: true,
320
+ },
321
+ pdfScale: {
322
+ type: "string",
323
+ label: "PDF Scale",
324
+ description: "PDF render scale from `0.1` to `2`. Defaults to `1`.",
325
+ optional: true,
326
+ },
327
+ geo: {
328
+ type: "string",
329
+ label: "Geo Country",
330
+ description: "Two-letter country code (e.g. `US`, `DE`) to capture from via a geo-targeted proxy. Pro/Enterprise only.",
331
+ optional: true,
332
+ },
333
+ geoCity: {
334
+ type: "string",
335
+ label: "Geo City",
336
+ description: "City to target for the geo proxy. Pro/Enterprise only.",
337
+ optional: true,
338
+ },
339
+ geoState: {
340
+ type: "string",
341
+ label: "Geo State",
342
+ description: "State/region to target for the geo proxy. Pro/Enterprise only.",
343
+ optional: true,
344
+ },
345
+ renderFullPage: {
346
+ type: "boolean",
347
+ label: "Render Full Page",
348
+ description: "Capture (and diff) the entire scrollable page. Defaults to `true` for watches.",
349
+ optional: true,
350
+ },
351
+ renderSelector: {
352
+ type: "string",
353
+ label: "Render Selector",
354
+ description: "Monitor only the element matching this CSS selector instead of the whole page.",
355
+ optional: true,
356
+ },
357
+ ignoreText: {
358
+ type: "string[]",
359
+ label: "Ignore Text",
360
+ description: "Text patterns stripped from both captures before text-diffing so volatile substrings (timestamps, rotating prices, CSRF tokens) don't trigger a change. Wrap a pattern in `/…/flags` for a regex; anything else is a case-insensitive substring.",
361
+ optional: true,
362
+ },
363
+ minTextChars: {
364
+ type: "integer",
365
+ label: "Minimum Text Change (chars)",
366
+ description: "A text change must add and remove at least this many characters to count — drops micro-edits.",
367
+ optional: true,
368
+ min: 0,
369
+ max: 100000,
370
+ },
371
+ suppressWhilePresent: {
372
+ type: "string[]",
373
+ label: "Suppress While Present",
374
+ description: "While the page text contains any of these strings (e.g. `Out of stock`, `Loading`), the run is treated as unchanged — no alert, no baseline drift.",
375
+ optional: true,
376
+ },
377
+ uaMode: {
378
+ type: "string",
379
+ label: "User-Agent Mode",
380
+ description: "Fetch identity for the watch: `auto` (lead with the RendexWatch UA, retry once with a browser identity if blocked), `identify` (always the RendexWatch UA), or `stealth` (always a standard browser identity).",
381
+ optional: true,
382
+ options: [
383
+ "auto",
384
+ "identify",
385
+ "stealth",
386
+ ],
387
+ },
32
388
  },
33
389
  methods: {
34
390
  _baseUrl() {
@@ -64,5 +420,78 @@ export default {
64
420
  ...opts,
65
421
  });
66
422
  },
423
+ createWatch(opts = {}) {
424
+ return this._makeRequest({
425
+ method: "POST",
426
+ path: "/v1/watches",
427
+ ...opts,
428
+ });
429
+ },
430
+ listWatches(opts = {}) {
431
+ return this._makeRequest({
432
+ path: "/v1/watches",
433
+ ...opts,
434
+ });
435
+ },
436
+ getWatch(watchId, opts = {}) {
437
+ return this._makeRequest({
438
+ path: `/v1/watches/${watchId}`,
439
+ ...opts,
440
+ });
441
+ },
442
+ listRuns(watchId, opts = {}) {
443
+ return this._makeRequest({
444
+ path: `/v1/watches/${watchId}/runs`,
445
+ ...opts,
446
+ });
447
+ },
448
+ updateWatch(watchId, opts = {}) {
449
+ return this._makeRequest({
450
+ method: "PATCH",
451
+ path: `/v1/watches/${watchId}`,
452
+ ...opts,
453
+ });
454
+ },
455
+ deleteWatch(watchId, opts = {}) {
456
+ return this._makeRequest({
457
+ method: "DELETE",
458
+ path: `/v1/watches/${watchId}`,
459
+ ...opts,
460
+ });
461
+ },
462
+ runWatch(watchId, opts = {}) {
463
+ return this._makeRequest({
464
+ method: "POST",
465
+ path: `/v1/watches/${watchId}/run`,
466
+ ...opts,
467
+ });
468
+ },
469
+ extract(opts = {}) {
470
+ return this._makeRequest({
471
+ method: "POST",
472
+ path: "/v1/extract",
473
+ ...opts,
474
+ });
475
+ },
476
+ createRenderLink(opts = {}) {
477
+ return this._makeRequest({
478
+ method: "POST",
479
+ path: "/v1/render/link",
480
+ ...opts,
481
+ });
482
+ },
483
+ getAccount(opts = {}) {
484
+ return this._makeRequest({
485
+ path: "/v1/account",
486
+ ...opts,
487
+ });
488
+ },
489
+ createArtifact(opts = {}) {
490
+ return this._makeRequest({
491
+ method: "POST",
492
+ path: "/v1/artifact",
493
+ ...opts,
494
+ });
495
+ },
67
496
  },
68
497
  };
@@ -0,0 +1,151 @@
1
+ import rendex from "../../rendex.app.mjs";
2
+ import { DEFAULT_POLLING_SOURCE_TIMER_INTERVAL } from "@pipedream/platform";
3
+
4
+ export default {
5
+ key: "rendex-new-change-detected",
6
+ name: "New Change Detected",
7
+ description: "Emit new event each time a Rendex watch run detects a change. [See the documentation](https://rendex.dev/docs/watch#how-you-get-alerted).",
8
+ version: "0.0.1",
9
+ type: "source",
10
+ dedupe: "unique",
11
+ props: {
12
+ rendex,
13
+ db: "$.service.db",
14
+ timer: {
15
+ type: "$.interface.timer",
16
+ static: {
17
+ intervalSeconds: DEFAULT_POLLING_SOURCE_TIMER_INTERVAL,
18
+ },
19
+ },
20
+ watchId: {
21
+ propDefinition: [
22
+ rendex,
23
+ "watchId",
24
+ ],
25
+ optional: true,
26
+ description: "Limit to a single watch. Leave blank to watch every watch on the account.",
27
+ },
28
+ },
29
+ methods: {
30
+ _getLastTs() {
31
+ return this.db.get("lastTs") || 0;
32
+ },
33
+ _setLastTs(ts) {
34
+ this.db.set("lastTs", ts);
35
+ },
36
+ _runTs(run) {
37
+ return Date.parse(run.completedAt || run.createdAt) || 0;
38
+ },
39
+ async _watchIds() {
40
+ if (this.watchId) {
41
+ return [
42
+ this.watchId,
43
+ ];
44
+ }
45
+ const ids = [];
46
+ let cursor;
47
+ do {
48
+ const { data } = await this.rendex.listWatches({
49
+ params: {
50
+ status: "all",
51
+ limit: 100,
52
+ cursor,
53
+ },
54
+ });
55
+ for (const watch of data?.items ?? []) {
56
+ ids.push(watch.id);
57
+ }
58
+ cursor = data?.nextCursor;
59
+ } while (cursor);
60
+ return ids;
61
+ },
62
+ async _scanWatch(watchId, since, maxPages) {
63
+ const runs = [];
64
+ let maxTs = since;
65
+ let cursor;
66
+ let pages = 0;
67
+ let reachedOld = false;
68
+ do {
69
+ const { data } = await this.rendex.listRuns(watchId, {
70
+ params: {
71
+ limit: 100,
72
+ cursor,
73
+ },
74
+ });
75
+ for (const run of data?.items ?? []) {
76
+ const ts = this._runTs(run);
77
+ if (ts <= since) {
78
+ reachedOld = true;
79
+ break;
80
+ }
81
+ if (ts > maxTs) {
82
+ maxTs = ts;
83
+ }
84
+ if (run.status === "completed" && run.changed === true) {
85
+ runs.push({
86
+ ...run,
87
+ watchId,
88
+ });
89
+ }
90
+ }
91
+ pages += 1;
92
+ cursor = reachedOld || (maxPages && pages >= maxPages)
93
+ ? null
94
+ : data?.nextCursor;
95
+ } while (cursor);
96
+ return {
97
+ runs,
98
+ maxTs,
99
+ };
100
+ },
101
+ async _scanChanges(since, maxPages) {
102
+ const watchIds = await this._watchIds();
103
+ const runs = [];
104
+ let maxTs = since;
105
+ for (const watchId of watchIds) {
106
+ const scanned = await this._scanWatch(watchId, since, maxPages);
107
+ runs.push(...scanned.runs);
108
+ if (scanned.maxTs > maxTs) {
109
+ maxTs = scanned.maxTs;
110
+ }
111
+ }
112
+ return {
113
+ runs,
114
+ maxTs,
115
+ };
116
+ },
117
+ emitRun(run) {
118
+ const detail = run.diffScore != null
119
+ ? ` (diff ${run.diffScore})`
120
+ : "";
121
+ this.$emit(run, {
122
+ id: run.id,
123
+ summary: `Change detected on watch ${run.watchId}${detail}`,
124
+ ts: this._runTs(run),
125
+ });
126
+ },
127
+ },
128
+ hooks: {
129
+ async deploy() {
130
+ const {
131
+ runs, maxTs,
132
+ } = await this._scanChanges(0, 1);
133
+ runs
134
+ .sort((a, b) => this._runTs(b) - this._runTs(a))
135
+ .slice(0, 5)
136
+ .reverse()
137
+ .forEach((run) => this.emitRun(run));
138
+ this._setLastTs(maxTs);
139
+ },
140
+ },
141
+ async run() {
142
+ const lastTs = this._getLastTs();
143
+ const {
144
+ runs, maxTs,
145
+ } = await this._scanChanges(lastTs);
146
+ runs
147
+ .sort((a, b) => this._runTs(a) - this._runTs(b))
148
+ .forEach((run) => this.emitRun(run));
149
+ this._setLastTs(maxTs);
150
+ },
151
+ };