@hydrafetch/node-sdk 0.1.1 → 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/README.md CHANGED
@@ -82,6 +82,35 @@ out.results.forEach((item) => console.log(item.url, item.data?.name));
82
82
 
83
83
  Pass `prompt` instead of, or alongside, `schema` to describe the fields in plain language.
84
84
 
85
+ ### Zod schemas
86
+
87
+ If you already use Zod, pass the schema directly. Zod stays optional and is imported only when you
88
+ pass one, so this package still installs with no dependencies of its own.
89
+
90
+ Zod 4 converts its own schemas and needs nothing else. On Zod 3, install `zod-to-json-schema`
91
+ alongside it and that is used instead. Which one is used is decided by the schema you pass, not by
92
+ what is installed, so a workspace holding both majors still works.
93
+
94
+ Nested schemas are flattened before sending, including one reused in two places. A schema that
95
+ refers to itself cannot be flattened and is refused rather than sent.
96
+
97
+ ```ts
98
+ import { z } from 'zod';
99
+
100
+ const Product = z.object({
101
+ name: z.string().describe("the name of the product this page is selling, not a recommended one"),
102
+ priceUsd: z.number().describe('its current price in US dollars'),
103
+ });
104
+
105
+ const out = await hf.extract<z.infer<typeof Product>>(['https://example.com/product/1'], {
106
+ schema: Product,
107
+ });
108
+ ```
109
+
110
+ `.describe()` is the reason to bother. The description travels with the field to the model doing the
111
+ extraction, and on a page carrying several products, several dates or several addresses it is what
112
+ tells the model which one you meant. The same works in a plain JSON Schema with `description`.
113
+
85
114
  ## Discovery and bulk work
86
115
 
87
116
  `map` lists a site's URLs for one credit without fetching any page.
package/dist/index.cjs CHANGED
@@ -67,6 +67,62 @@ var DEFAULT_MAX_RETRIES = 2;
67
67
  function sleep(ms) {
68
68
  return new Promise((resolve) => setTimeout(resolve, ms));
69
69
  }
70
+ async function toJsonSchema(schema) {
71
+ const zodVersion = zodMajorOf(schema);
72
+ if (zodVersion === null) return schema;
73
+ const converter = await zodConverter(zodVersion);
74
+ let converted;
75
+ try {
76
+ converted = converter(schema);
77
+ } catch (cause) {
78
+ throw new HydrafetchError({
79
+ code: "SCHEMA_CONVERSION_FAILED",
80
+ message: `The Zod schema could not be converted: ${cause?.message ?? cause}`,
81
+ status: 0
82
+ });
83
+ }
84
+ if (JSON.stringify(converted).includes('"$ref":"#')) {
85
+ throw new HydrafetchError({
86
+ code: "SCHEMA_CONVERSION_FAILED",
87
+ message: "The Zod schema refers to itself, so it cannot be expressed as a flat JSON Schema. Flatten the schema or pass JSON Schema directly.",
88
+ status: 0
89
+ });
90
+ }
91
+ return converted;
92
+ }
93
+ function zodMajorOf(schema) {
94
+ if (typeof schema !== "object" || schema === null) return null;
95
+ if (typeof schema.parse !== "function") return null;
96
+ if ("_zod" in schema) return 4;
97
+ return "_def" in schema ? 3 : null;
98
+ }
99
+ async function zodConverter(version) {
100
+ if (version === 4) {
101
+ const zod = await optionalImport(
102
+ "zod"
103
+ );
104
+ if (zod && typeof zod.toJSONSchema === "function") return zod.toJSONSchema;
105
+ }
106
+ const legacy = await optionalImport("zod-to-json-schema");
107
+ if (legacy && typeof legacy.zodToJsonSchema === "function") {
108
+ return (schema) => legacy.zodToJsonSchema(schema, { $refStrategy: "none" });
109
+ }
110
+ throw new HydrafetchError({
111
+ code: "SCHEMA_CONVERSION_FAILED",
112
+ message: `A Zod ${version} schema was passed but nothing here can convert it. Use zod 4, which converts its own schemas, or install zod-to-json-schema alongside zod 3, or pass a JSON Schema.`,
113
+ status: 0
114
+ });
115
+ }
116
+ async function optionalImport(name) {
117
+ try {
118
+ return await import(
119
+ /* @vite-ignore */
120
+ name
121
+ );
122
+ } catch {
123
+ return null;
124
+ }
125
+ }
70
126
  var Hydrafetch = class {
71
127
  apiKey;
72
128
  baseUrl;
@@ -171,10 +227,12 @@ var Hydrafetch = class {
171
227
  search(query, options = {}) {
172
228
  return this.request("POST", "/v1/web/search", { query, ...options });
173
229
  }
174
- extract(urls, options = {}) {
230
+ async extract(urls, options = {}) {
231
+ const { schema, ...rest } = options;
175
232
  return this.request("POST", "/v1/web/extract", {
176
233
  urls: Array.isArray(urls) ? urls : [urls],
177
- ...options
234
+ ...rest,
235
+ ...schema === void 0 ? {} : { schema: await toJsonSchema(schema) }
178
236
  });
179
237
  }
180
238
  brand(domain) {
package/dist/index.d.cts CHANGED
@@ -90,8 +90,23 @@ interface SearchResult {
90
90
  status?: string;
91
91
  results: SearchResultItem[];
92
92
  }
93
+ /**
94
+ * A standards-compliant JSON Schema, or a Zod schema you already have.
95
+ *
96
+ * Zod is not a dependency of this package and never will be: it is loaded only if you actually
97
+ * pass one. The reason to pass one is `.describe()`. A field description reaches the model that
98
+ * does the extraction, and on a page carrying several products or several dates it is what tells
99
+ * the model which one you meant.
100
+ */
101
+ type ExtractSchema = Record<string, unknown> | ZodLike;
102
+ /** Structurally a Zod schema, without importing Zod to say so. */
103
+ interface ZodLike {
104
+ _zod?: unknown;
105
+ _def?: unknown;
106
+ parse?: (value: unknown) => unknown;
107
+ }
93
108
  interface ExtractOptions {
94
- schema?: Record<string, unknown>;
109
+ schema?: ExtractSchema;
95
110
  prompt?: string;
96
111
  preferStructure?: boolean;
97
112
  enableWebSearch?: boolean;
@@ -271,4 +286,4 @@ declare class HydrafetchTimeoutError extends HydrafetchError {
271
286
  constructor(message: string);
272
287
  }
273
288
 
274
- export { type BatchOptions, type BrandOptions, type BrandResult, type CrawlOptions, type ExtractItem, type ExtractOptions, type ExtractResult, type Format, Hydrafetch, HydrafetchError, type HydrafetchOptions, HydrafetchTimeoutError, type ImagesResult, type JobPage, type JobResult, type JobStatus, type LogoResult, type MapOptions, type MapResult, type PageImage, type PageMetadata, type ScrapeBase, type ScrapeContent, type ScrapeOptions, type ScrapeResult, type ScreenshotOptions, type ScreenshotResult, type SearchOptions, type SearchResult, type SearchResultItem, type StyleguideResult, type Usage, type WaitOptions };
289
+ export { type BatchOptions, type BrandOptions, type BrandResult, type CrawlOptions, type ExtractItem, type ExtractOptions, type ExtractResult, type ExtractSchema, type Format, Hydrafetch, HydrafetchError, type HydrafetchOptions, HydrafetchTimeoutError, type ImagesResult, type JobPage, type JobResult, type JobStatus, type LogoResult, type MapOptions, type MapResult, type PageImage, type PageMetadata, type ScrapeBase, type ScrapeContent, type ScrapeOptions, type ScrapeResult, type ScreenshotOptions, type ScreenshotResult, type SearchOptions, type SearchResult, type SearchResultItem, type StyleguideResult, type Usage, type WaitOptions, type ZodLike };
package/dist/index.d.ts CHANGED
@@ -90,8 +90,23 @@ interface SearchResult {
90
90
  status?: string;
91
91
  results: SearchResultItem[];
92
92
  }
93
+ /**
94
+ * A standards-compliant JSON Schema, or a Zod schema you already have.
95
+ *
96
+ * Zod is not a dependency of this package and never will be: it is loaded only if you actually
97
+ * pass one. The reason to pass one is `.describe()`. A field description reaches the model that
98
+ * does the extraction, and on a page carrying several products or several dates it is what tells
99
+ * the model which one you meant.
100
+ */
101
+ type ExtractSchema = Record<string, unknown> | ZodLike;
102
+ /** Structurally a Zod schema, without importing Zod to say so. */
103
+ interface ZodLike {
104
+ _zod?: unknown;
105
+ _def?: unknown;
106
+ parse?: (value: unknown) => unknown;
107
+ }
93
108
  interface ExtractOptions {
94
- schema?: Record<string, unknown>;
109
+ schema?: ExtractSchema;
95
110
  prompt?: string;
96
111
  preferStructure?: boolean;
97
112
  enableWebSearch?: boolean;
@@ -271,4 +286,4 @@ declare class HydrafetchTimeoutError extends HydrafetchError {
271
286
  constructor(message: string);
272
287
  }
273
288
 
274
- export { type BatchOptions, type BrandOptions, type BrandResult, type CrawlOptions, type ExtractItem, type ExtractOptions, type ExtractResult, type Format, Hydrafetch, HydrafetchError, type HydrafetchOptions, HydrafetchTimeoutError, type ImagesResult, type JobPage, type JobResult, type JobStatus, type LogoResult, type MapOptions, type MapResult, type PageImage, type PageMetadata, type ScrapeBase, type ScrapeContent, type ScrapeOptions, type ScrapeResult, type ScreenshotOptions, type ScreenshotResult, type SearchOptions, type SearchResult, type SearchResultItem, type StyleguideResult, type Usage, type WaitOptions };
289
+ export { type BatchOptions, type BrandOptions, type BrandResult, type CrawlOptions, type ExtractItem, type ExtractOptions, type ExtractResult, type ExtractSchema, type Format, Hydrafetch, HydrafetchError, type HydrafetchOptions, HydrafetchTimeoutError, type ImagesResult, type JobPage, type JobResult, type JobStatus, type LogoResult, type MapOptions, type MapResult, type PageImage, type PageMetadata, type ScrapeBase, type ScrapeContent, type ScrapeOptions, type ScrapeResult, type ScreenshotOptions, type ScreenshotResult, type SearchOptions, type SearchResult, type SearchResultItem, type StyleguideResult, type Usage, type WaitOptions, type ZodLike };
package/dist/index.js CHANGED
@@ -39,6 +39,62 @@ var DEFAULT_MAX_RETRIES = 2;
39
39
  function sleep(ms) {
40
40
  return new Promise((resolve) => setTimeout(resolve, ms));
41
41
  }
42
+ async function toJsonSchema(schema) {
43
+ const zodVersion = zodMajorOf(schema);
44
+ if (zodVersion === null) return schema;
45
+ const converter = await zodConverter(zodVersion);
46
+ let converted;
47
+ try {
48
+ converted = converter(schema);
49
+ } catch (cause) {
50
+ throw new HydrafetchError({
51
+ code: "SCHEMA_CONVERSION_FAILED",
52
+ message: `The Zod schema could not be converted: ${cause?.message ?? cause}`,
53
+ status: 0
54
+ });
55
+ }
56
+ if (JSON.stringify(converted).includes('"$ref":"#')) {
57
+ throw new HydrafetchError({
58
+ code: "SCHEMA_CONVERSION_FAILED",
59
+ message: "The Zod schema refers to itself, so it cannot be expressed as a flat JSON Schema. Flatten the schema or pass JSON Schema directly.",
60
+ status: 0
61
+ });
62
+ }
63
+ return converted;
64
+ }
65
+ function zodMajorOf(schema) {
66
+ if (typeof schema !== "object" || schema === null) return null;
67
+ if (typeof schema.parse !== "function") return null;
68
+ if ("_zod" in schema) return 4;
69
+ return "_def" in schema ? 3 : null;
70
+ }
71
+ async function zodConverter(version) {
72
+ if (version === 4) {
73
+ const zod = await optionalImport(
74
+ "zod"
75
+ );
76
+ if (zod && typeof zod.toJSONSchema === "function") return zod.toJSONSchema;
77
+ }
78
+ const legacy = await optionalImport("zod-to-json-schema");
79
+ if (legacy && typeof legacy.zodToJsonSchema === "function") {
80
+ return (schema) => legacy.zodToJsonSchema(schema, { $refStrategy: "none" });
81
+ }
82
+ throw new HydrafetchError({
83
+ code: "SCHEMA_CONVERSION_FAILED",
84
+ message: `A Zod ${version} schema was passed but nothing here can convert it. Use zod 4, which converts its own schemas, or install zod-to-json-schema alongside zod 3, or pass a JSON Schema.`,
85
+ status: 0
86
+ });
87
+ }
88
+ async function optionalImport(name) {
89
+ try {
90
+ return await import(
91
+ /* @vite-ignore */
92
+ name
93
+ );
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
42
98
  var Hydrafetch = class {
43
99
  apiKey;
44
100
  baseUrl;
@@ -143,10 +199,12 @@ var Hydrafetch = class {
143
199
  search(query, options = {}) {
144
200
  return this.request("POST", "/v1/web/search", { query, ...options });
145
201
  }
146
- extract(urls, options = {}) {
202
+ async extract(urls, options = {}) {
203
+ const { schema, ...rest } = options;
147
204
  return this.request("POST", "/v1/web/extract", {
148
205
  urls: Array.isArray(urls) ? urls : [urls],
149
- ...options
206
+ ...rest,
207
+ ...schema === void 0 ? {} : { schema: await toJsonSchema(schema) }
150
208
  });
151
209
  }
152
210
  brand(domain) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hydrafetch/node-sdk",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Official Node/TypeScript client for the Hydrafetch web data API. Turn any URL into clean Markdown and structured data.",
5
5
  "keywords": [
6
6
  "hydrafetch",
@@ -47,9 +47,22 @@
47
47
  "devDependencies": {
48
48
  "tsup": "^8.5.0",
49
49
  "typescript": "^5.9.2",
50
- "vitest": "^3.2.4"
50
+ "vitest": "^3.2.4",
51
+ "zod": "^4.5.4"
51
52
  },
52
53
  "publishConfig": {
53
54
  "access": "public"
55
+ },
56
+ "peerDependencies": {
57
+ "zod": ">=3",
58
+ "zod-to-json-schema": ">=3"
59
+ },
60
+ "peerDependenciesMeta": {
61
+ "zod": {
62
+ "optional": true
63
+ },
64
+ "zod-to-json-schema": {
65
+ "optional": true
66
+ }
54
67
  }
55
68
  }