Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
9 changes: 6 additions & 3 deletions packages/corsair/core/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ export const BaseProviders = [
'agenty',
'ahrefs',
'aimlapi',
'allimagesai',
'airtable',
'alchemy',
'algolia',
'allimagesai',
'alphavantage',
'altoviz',
'alttextai',
Expand Down Expand Up @@ -86,6 +86,7 @@ export const BaseProviders = [
'databricks',
'datadog',
'deepseek',
'diffbot',
'digitalocean',
'discord',
'dockerhub',
Expand Down Expand Up @@ -205,10 +206,10 @@ export const ProviderDisplayNames = {
agenty: 'Agenty',
ahrefs: 'Ahrefs',
aimlapi: 'AI/ML API',
allimagesai: 'All Images AI',
airtable: 'Airtable',
alchemy: 'Alchemy',
algolia: 'Algolia',
allimagesai: 'All Images AI',
alphavantage: 'Alpha Vantage',
altoviz: 'Altoviz',
alttextai: 'AltText.ai',
Expand Down Expand Up @@ -263,6 +264,7 @@ export const ProviderDisplayNames = {
databricks: 'Databricks',
datadog: 'Datadog',
deepseek: 'DeepSeek',
diffbot: 'Diffbot',
digitalocean: 'DigitalOcean',
discord: 'Discord',
dockerhub: 'Docker Hub',
Expand Down Expand Up @@ -389,10 +391,10 @@ export type AllProviders =
| 'agenty'
| 'ahrefs'
| 'aimlapi'
| 'allimagesai'
| 'airtable'
| 'alchemy'
| 'algolia'
| 'allimagesai'
| 'alphavantage'
| 'altoviz'
| 'alttextai'
Expand Down Expand Up @@ -447,6 +449,7 @@ export type AllProviders =
| 'databricks'
| 'datadog'
| 'deepseek'
| 'diffbot'
| 'digitalocean'
| 'discord'
| 'dockerhub'
Expand Down
242 changes: 242 additions & 0 deletions packages/diffbot/api.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import {
AnalyzeInputSchema,
AnalyzeResponseSchema,
DqlSearchInputSchema,
DqlSearchResponseSchema,
ExtractArticleInputSchema,
ExtractArticleResponseSchema,
ExtractProductInputSchema,
ExtractProductResponseSchema,
WebSearchInputSchema,
WebSearchResponseSchema,
} from './endpoints/types';

// ---------------------------------------------------------------------------
// Extract Article
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Outdated
// ---------------------------------------------------------------------------

describe('extract.article — input schema', () => {
it('accepts a valid URL', () => {
const result = ExtractArticleInputSchema.safeParse({
url: 'https://techcrunch.com/2024/01/01/example-article',
});
expect(result.success).toBe(true);
});

it('accepts optional fields param', () => {
const result = ExtractArticleInputSchema.safeParse({
url: 'https://example.com',
fields: 'links,meta,tags',
timeout: 30000,
});
expect(result.success).toBe(true);
});

it('rejects missing url', () => {
const result = ExtractArticleInputSchema.safeParse({});
expect(result.success).toBe(false);
});
});

describe('extract.article — response schema', () => {
it('parses a valid article response', () => {
const payload = {
request: { pageUrl: 'https://example.com', api: 'article', version: 3 },
objects: [
{
type: 'article' as const,
title: 'Test Article',
text: 'Article body text',
author: 'John Doe',
date: '2024-01-01T00:00:00.000Z',
pageUrl: 'https://example.com',
humanLanguage: 'en',
},
],
};
const result = ExtractArticleResponseSchema.safeParse(payload);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.objects[0]?.title).toBe('Test Article');
expect(result.data.objects[0]?.author).toBe('John Doe');
}
});

it('parses response with optional fields missing', () => {
const result = ExtractArticleResponseSchema.safeParse({
objects: [{ type: 'article' }],
});
expect(result.success).toBe(true);
});
});

// ---------------------------------------------------------------------------
// Extract Product
// ---------------------------------------------------------------------------

describe('extract.product — input schema', () => {
it('accepts a valid product URL', () => {
const result = ExtractProductInputSchema.safeParse({
url: 'https://www.amazon.com/dp/B08N5WRWNW',
});
expect(result.success).toBe(true);
});

it('rejects missing url', () => {
const result = ExtractProductInputSchema.safeParse({ timeout: 5000 });
expect(result.success).toBe(false);
});
});

describe('extract.product — response schema', () => {
it('parses a valid product response', () => {
const payload = {
objects: [
{
type: 'product' as const,
title: 'Example Product',
offerPrice: '$29.99',
availability: true,
brand: 'Acme',
pageUrl: 'https://example.com/product',
},
],
};
const result = ExtractProductResponseSchema.safeParse(payload);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.objects[0]?.offerPrice).toBe('$29.99');
expect(result.data.objects[0]?.availability).toBe(true);
}
});
});

// ---------------------------------------------------------------------------
// Analyze (auto-detect)
// ---------------------------------------------------------------------------

describe('extract.analyze — input schema', () => {
it('accepts a URL with fallback option', () => {
const result = AnalyzeInputSchema.safeParse({
url: 'https://example.com',
fallback: 'article',
});
expect(result.success).toBe(true);
});

it('rejects empty object', () => {
const result = AnalyzeInputSchema.safeParse({});
expect(result.success).toBe(false);
});
});

describe('extract.analyze — response schema', () => {
it('parses an analyze response with detected type', () => {
const result = AnalyzeResponseSchema.safeParse({
type: 'article',
humanLanguage: 'en',
objects: [{ type: 'article', title: 'Detected Article' }],
});
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.type).toBe('article');
}
});
});

// ---------------------------------------------------------------------------
// Web Search
// ---------------------------------------------------------------------------

describe('search.web — input schema', () => {
it('accepts a valid search query', () => {
const result = WebSearchInputSchema.safeParse({
query: 'artificial intelligence trends 2024',
num: 10,
});
expect(result.success).toBe(true);
});

it('rejects num > 25 (Diffbot max)', () => {
const result = WebSearchInputSchema.safeParse({
query: 'test',
num: 100,
});
expect(result.success).toBe(false);
});

it('rejects missing query', () => {
const result = WebSearchInputSchema.safeParse({ num: 5 });
expect(result.success).toBe(false);
});
});

describe('search.web — response schema', () => {
it('parses a valid search response', () => {
const payload = {
results: [
{
title: 'AI in 2024',
pageUrl: 'https://example.com/ai-2024',
text: 'Article summary...',
humanLanguage: 'en',
},
],
numResults: 1,
hits: 1,
};
const result = WebSearchResponseSchema.safeParse(payload);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.results?.[0]?.title).toBe('AI in 2024');
expect(result.data.numResults).toBe(1);
}
});
});

// ---------------------------------------------------------------------------
// DQL (Knowledge Graph Search)
// ---------------------------------------------------------------------------

describe('search.dql — input schema', () => {
it('accepts a DQL query', () => {
const result = DqlSearchInputSchema.safeParse({
query: 'name:"OpenAI"',
type: 'Organization',
size: 5,
});
expect(result.success).toBe(true);
});

it('accepts a query without optional type', () => {
const result = DqlSearchInputSchema.safeParse({
query: 'diffbot',
});
expect(result.success).toBe(true);
});

it('rejects missing query', () => {
const result = DqlSearchInputSchema.safeParse({ size: 5 });
expect(result.success).toBe(false);
});
});

describe('search.dql — response schema', () => {
it('parses a valid DQL response', () => {
const payload = {
data: [{ id: 'org-123', name: 'OpenAI', type: 'Organization' }],
hits: 1,
};
const result = DqlSearchResponseSchema.safeParse(payload);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.hits).toBe(1);
expect(result.data.data).toHaveLength(1);
}
});

it('parses an empty result set', () => {
const result = DqlSearchResponseSchema.safeParse({ data: [], hits: 0 });
expect(result.success).toBe(true);
});
});
71 changes: 71 additions & 0 deletions packages/diffbot/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { ApiRequestOptions, OpenAPIConfig } from 'corsair/http';
import { request } from 'corsair/http';

export class DiffbotAPIError extends Error {
constructor(
message: string,
public readonly code?: string,
) {
super(message);
this.name = 'DiffbotAPIError';
}
}

// Diffbot API v3 base URL
const DIFFBOT_API_BASE = 'https://api.diffbot.com/v3';

/**
* Make a request to the Diffbot API.
*
* Diffbot authenticates via `?token=<api-key>` as a query parameter —
* NOT via an Authorization header. The token is injected automatically here.
*/
export async function makeDiffbotRequest<T>(
endpoint: string,
token: string,
options: {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
body?: Record<string, unknown>;
query?: Record<string, string | number | boolean | undefined>;
} = {},
): Promise<T> {
const { method = 'GET', body, query } = options;

const config: OpenAPIConfig = {
BASE: DIFFBOT_API_BASE,
VERSION: '3',
Comment thread
coderabbitai[bot] marked this conversation as resolved.
WITH_CREDENTIALS: false,
CREDENTIALS: 'omit',
TOKEN: undefined,
HEADERS: {
Accept: 'application/json',
},
};

// Diffbot auth: token is a query parameter, not a header
const queryWithToken: Record<string, string | number | boolean | undefined> =
{
token,
...query,
};

const requestOptions: ApiRequestOptions = {
method,
url: endpoint,
body:
method === 'POST' || method === 'PUT' || method === 'PATCH'
? body
: undefined,
mediaType: 'application/json',
query: queryWithToken,
};

try {
return await request<T>(config, requestOptions);
} catch (error) {
if (error instanceof Error) {
throw new DiffbotAPIError(error.message);
}
Comment thread
greptile-apps[bot] marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
throw new DiffbotAPIError('Unknown error occurred');
}
}
Loading
Loading