feat: add sanitizeSearchParams

pull/643/head^2
Travis Fischer 2024-06-03 12:26:46 -05:00
rodzic 94198f318b
commit ed85b708fd
8 zmienionych plików z 258 dodań i 255 usunięć

Wyświetl plik

@ -62,6 +62,7 @@
- provide a converter for langchain `DynamicStructuredTool` - provide a converter for langchain `DynamicStructuredTool`
- pull from other libs - pull from other libs
- pull from [nango](https://docs.nango.dev/integrations/overview) - pull from [nango](https://docs.nango.dev/integrations/overview)
- https://github.com/causaly/zod-validation-error
## License ## License

Wyświetl plik

@ -0,0 +1,15 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`sanitizeSearchParams 1`] = `"a=1&c=13"`;
exports[`sanitizeSearchParams 2`] = `"a=1&a=2&a=3"`;
exports[`sanitizeSearchParams 3`] = `"b=a&b=b&foo=true"`;
exports[`sanitizeSearchParams 4`] = `"b=false&b=true&b=false"`;
exports[`sanitizeSearchParams 5`] = `"flag=foo&flag=bar&flag=baz&token=test"`;
exports[`sanitizeSearchParams 6`] = `""`;
exports[`sanitizeSearchParams 7`] = `""`;

Wyświetl plik

@ -12,9 +12,6 @@ export interface PrivateAIFunctionMetadata {
description: string description: string
inputSchema: z.AnyZodObject inputSchema: z.AnyZodObject
methodName: string methodName: string
// TODO
// pre and post
} }
export abstract class AIFunctionsProvider { export abstract class AIFunctionsProvider {

Wyświetl plik

@ -3,7 +3,13 @@ import pThrottle from 'p-throttle'
import { z } from 'zod' import { z } from 'zod'
import { aiFunction, AIFunctionsProvider } from '../fns.js' import { aiFunction, AIFunctionsProvider } from '../fns.js'
import { assert, getEnv, throttleKy } from '../utils.js' import {
assert,
getEnv,
omit,
sanitizeSearchParams,
throttleKy
} from '../utils.js'
export namespace diffbot { export namespace diffbot {
export const API_BASE_URL = 'https://api.diffbot.com' export const API_BASE_URL = 'https://api.diffbot.com'
@ -373,17 +379,17 @@ export namespace diffbot {
allUris?: string[] allUris?: string[]
// extra metadata // extra metadata
nbOrigins?: number
nbIncomingEdges?: number
nbFollowers?: number
educations?: Education[] educations?: Education[]
nationalities?: Nationality[] nationalities?: Nationality[]
allNames?: string[] allNames?: string[]
skills?: Skill[] skills?: Skill[]
children?: Children[] children?: Children[]
nbOrigins?: number
height?: number height?: number
image?: string image?: string
images?: Image[] images?: Image[]
nbIncomingEdges?: number
nbFollowers?: number
allOriginHashes?: string[] allOriginHashes?: string[]
nameDetail?: NameDetail nameDetail?: NameDetail
parents?: Parent[] parents?: Parent[]
@ -638,6 +644,18 @@ export namespace diffbot {
name: string name: string
type: string type: string
} }
export function pruneEntity(entity: diffbot.Entity) {
return omit(
entity,
'allOriginHashes',
'locations',
'images',
'nationalities',
'awards',
'interests'
)
}
} }
export class DiffbotClient extends AIFunctionsProvider { export class DiffbotClient extends AIFunctionsProvider {
@ -713,7 +731,7 @@ export class DiffbotClient extends AIFunctionsProvider {
@aiFunction({ @aiFunction({
name: 'diffbot_enhance_entity', name: 'diffbot_enhance_entity',
description: description:
'Enriches a person or organization entity given partial data. Enhance is an enrichment API to find a person or organization using partial data as input. Enhance scores several candidates against the submitted query and returns the best match. More information in the query helps Enhance models estimate with more confidence and will typically result in better matches and a higher score for the matches.', 'Resolves and enriches a partial person or organization entity.',
inputSchema: diffbot.EnhanceEntityOptionsSchema.omit({ inputSchema: diffbot.EnhanceEntityOptionsSchema.omit({
refresh: true, refresh: true,
search: true, search: true,
@ -722,25 +740,16 @@ export class DiffbotClient extends AIFunctionsProvider {
}) })
}) })
async enhanceEntity(opts: diffbot.EnhanceEntityOptions) { async enhanceEntity(opts: diffbot.EnhanceEntityOptions) {
const { name, url, ...params } = opts const res = await this.kyKnowledgeGraph
// TODO: clean this array handling up...
const arraySearchParams = [
name ? (Array.isArray(name) ? name : [name]).map((v) => ['name', v]) : [],
url?.map((v) => ['url', v])
]
.filter(Boolean)
.flat()
return this.kyKnowledgeGraph
.get('kg/v3/enhance', { .get('kg/v3/enhance', {
searchParams: new URLSearchParams([ searchParams: sanitizeSearchParams({
...arraySearchParams, ...opts,
...Object.entries(params).map(([key, value]) => [key, String(value)]), token: this.apiKey
['token', this.apiKey] })
])
}) })
.json<diffbot.EnhanceEntityResponse>() .json<diffbot.EnhanceEntityResponse>()
return res.data.map((datum) => diffbot.pruneEntity(datum.entity))
} }
async searchKnowledgeGraph(options: diffbot.KnowledgeGraphSearchOptions) { async searchKnowledgeGraph(options: diffbot.KnowledgeGraphSearchOptions) {
@ -769,10 +778,10 @@ export class DiffbotClient extends AIFunctionsProvider {
T extends diffbot.ExtractResponse = diffbot.ExtractResponse T extends diffbot.ExtractResponse = diffbot.ExtractResponse
>(endpoint: string, options: diffbot.ExtractOptions): Promise<T> { >(endpoint: string, options: diffbot.ExtractOptions): Promise<T> {
const { customJs, customHeaders, ...rest } = options const { customJs, customHeaders, ...rest } = options
const searchParams: Record<string, any> = { const searchParams = sanitizeSearchParams({
...rest, ...rest,
token: this.apiKey token: this.apiKey
} })
const headers = { const headers = {
...Object.fromEntries( ...Object.fromEntries(
[['X-Forward-X-Evaluate', customJs]].filter(([, value]) => value) [['X-Forward-X-Evaluate', customJs]].filter(([, value]) => value)
@ -780,12 +789,6 @@ export class DiffbotClient extends AIFunctionsProvider {
...customHeaders ...customHeaders
} }
for (const [key, value] of Object.entries(rest)) {
if (Array.isArray(value)) {
searchParams[key] = value.join(',')
}
}
// console.log(`DiffbotClient._extract: ${endpoint}`, searchParams) // console.log(`DiffbotClient._extract: ${endpoint}`, searchParams)
return this.ky return this.ky

Wyświetl plik

@ -3,7 +3,7 @@ import pThrottle from 'p-throttle'
import { z } from 'zod' import { z } from 'zod'
import { aiFunction, AIFunctionsProvider } from '../fns.js' import { aiFunction, AIFunctionsProvider } from '../fns.js'
import { assert, getEnv, pruneUndefined, throttleKy } from '../utils.js' import { assert, getEnv, sanitizeSearchParams, throttleKy } from '../utils.js'
// TODO: https://docs.goperigon.com/docs/searching-sources // TODO: https://docs.goperigon.com/docs/searching-sources
// TODO: https://docs.goperigon.com/docs/journalist-data // TODO: https://docs.goperigon.com/docs/journalist-data
@ -683,28 +683,10 @@ export class PerigonClient extends AIFunctionsProvider {
}) })
}) })
async searchArticles(opts: perigon.ArticlesSearchOptions) { async searchArticles(opts: perigon.ArticlesSearchOptions) {
const {
personWikidataId,
personName,
companyId,
companyDomain,
companySymbol,
...params
} = opts
const arrayParams = pruneUndefined({
personWikidataId: personWikidataId?.join(','),
personName: personName?.join(','),
companyId: companyId?.join(','),
companyDomain: companyDomain?.join(','),
companySymbol: companySymbol?.join(',')
})
return this.ky return this.ky
.get('all', { .get('all', {
searchParams: { searchParams: sanitizeSearchParams({
...arrayParams, ...opts,
...params,
apiKey: this.apiKey, apiKey: this.apiKey,
size: Math.max( size: Math.max(
1, 1,
@ -713,7 +695,7 @@ export class PerigonClient extends AIFunctionsProvider {
opts.size || perigon.DEFAULT_PAGE_SIZE opts.size || perigon.DEFAULT_PAGE_SIZE
) )
) )
} })
}) })
.json<perigon.ArticlesSearchResponse>() .json<perigon.ArticlesSearchResponse>()
} }
@ -740,28 +722,10 @@ export class PerigonClient extends AIFunctionsProvider {
}) })
}) })
async searchStories(opts: perigon.StoriesSearchOptions) { async searchStories(opts: perigon.StoriesSearchOptions) {
const {
personWikidataId,
personName,
companyId,
companyDomain,
companySymbol,
...params
} = opts
const arrayParams = pruneUndefined({
personWikidataId: personWikidataId?.join(','),
personName: personName?.join(','),
companyId: companyId?.join(','),
companyDomain: companyDomain?.join(','),
companySymbol: companySymbol?.join(',')
})
return this.ky return this.ky
.get('stories/all', { .get('stories/all', {
searchParams: { searchParams: sanitizeSearchParams({
...arrayParams, ...opts,
...params,
apiKey: this.apiKey, apiKey: this.apiKey,
size: Math.max( size: Math.max(
1, 1,
@ -770,7 +734,7 @@ export class PerigonClient extends AIFunctionsProvider {
opts.size || perigon.DEFAULT_PAGE_SIZE opts.size || perigon.DEFAULT_PAGE_SIZE
) )
) )
} })
}) })
.json<perigon.StoriesSearchResponse>() .json<perigon.StoriesSearchResponse>()
} }
@ -785,18 +749,10 @@ export class PerigonClient extends AIFunctionsProvider {
inputSchema: perigon.PeopleSearchOptionsSchema inputSchema: perigon.PeopleSearchOptionsSchema
}) })
async searchPeople(opts: perigon.PeopleSearchOptions) { async searchPeople(opts: perigon.PeopleSearchOptions) {
const { wikidataId, occupationId, ...params } = opts
const arrayParams = pruneUndefined({
wikidataId: wikidataId?.join(','),
occupationId: occupationId?.join(',')
})
return this.ky return this.ky
.get('people/all', { .get('people/all', {
searchParams: { searchParams: sanitizeSearchParams({
...arrayParams, ...opts,
...params,
apiKey: this.apiKey, apiKey: this.apiKey,
size: Math.max( size: Math.max(
1, 1,
@ -805,7 +761,7 @@ export class PerigonClient extends AIFunctionsProvider {
opts.size || perigon.DEFAULT_PAGE_SIZE opts.size || perigon.DEFAULT_PAGE_SIZE
) )
) )
} })
}) })
.json<perigon.PeopleSearchResponse>() .json<perigon.PeopleSearchResponse>()
} }
@ -821,19 +777,10 @@ export class PerigonClient extends AIFunctionsProvider {
inputSchema: perigon.CompanySearchOptionsSchema inputSchema: perigon.CompanySearchOptionsSchema
}) })
async searchCompanies(opts: perigon.CompanySearchOptions) { async searchCompanies(opts: perigon.CompanySearchOptions) {
const { id, symbol, domain, ...params } = opts
const arrayParams = pruneUndefined({
id: id?.join(','),
domain: domain?.join(','),
symbol: symbol?.join(',')
})
return this.ky return this.ky
.get('companies/all', { .get('companies/all', {
searchParams: { searchParams: sanitizeSearchParams({
...arrayParams, ...opts,
...params,
apiKey: this.apiKey, apiKey: this.apiKey,
size: Math.max( size: Math.max(
1, 1,
@ -842,7 +789,7 @@ export class PerigonClient extends AIFunctionsProvider {
opts.size || perigon.DEFAULT_PAGE_SIZE opts.size || perigon.DEFAULT_PAGE_SIZE
) )
) )
} })
}) })
.json<perigon.CompanySearchResponse>() .json<perigon.CompanySearchResponse>()
} }

Wyświetl plik

@ -4,7 +4,15 @@ import { z } from 'zod'
import type { DeepNullable } from '../types.js' import type { DeepNullable } from '../types.js'
import { aiFunction, AIFunctionsProvider } from '../fns.js' import { aiFunction, AIFunctionsProvider } from '../fns.js'
import { assert, getEnv, pruneUndefined, throttleKy } from '../utils.js' import {
assert,
getEnv,
pruneUndefined,
sanitizeSearchParams,
throttleKy
} from '../utils.js'
// TODO: improve `domain` validation for fast-fail
export namespace predictleads { export namespace predictleads {
// Allow up to 20 requests per minute by default. // Allow up to 20 requests per minute by default.
@ -188,118 +196,124 @@ export namespace predictleads {
export type JobOpeningByIdResponse = Omit<JobOpeningResponse, 'meta'> export type JobOpeningByIdResponse = Omit<JobOpeningResponse, 'meta'>
export const EventCategorySchema = z.union([ export const EventCategorySchema = z
z .union([
.literal('hires') z
.describe( .literal('hires')
'Company hired new executive or senior personnel. (leadership)' .describe(
), 'Company hired new executive or senior personnel. (leadership)'
z ),
.literal('promotes') z
.describe( .literal('promotes')
'Company promoted existing executive or senior personnel. (leadership)' .describe(
), 'Company promoted existing executive or senior personnel. (leadership)'
z ),
.literal('leaves') z
.describe('Executive or senior personnel left the company. (leadership)'), .literal('leaves')
z .describe(
.literal('retires') 'Executive or senior personnel left the company. (leadership)'
.describe( ),
'Executive or senior personnel retires from the company. (leadership)' z
), .literal('retires')
z .describe(
.literal('acquires') 'Executive or senior personnel retires from the company. (leadership)'
.describe('Company acquired other company. (acquisition)'), ),
z z
.literal('merges_with') .literal('acquires')
.describe('Company merges with other company. (acquisition)'), .describe('Company acquired other company. (acquisition)'),
z z
.literal('sells_assets_to') .literal('merges_with')
.describe( .describe('Company merges with other company. (acquisition)'),
'Company sells assets (like properties or warehouses) to other company. (acquisition)' z
), .literal('sells_assets_to')
z .describe(
.literal('expands_offices_to') 'Company sells assets (like properties or warehouses) to other company. (acquisition)'
.describe( ),
'Company opens new offices in another town, state, country or continent. (expansion)' z
), .literal('expands_offices_to')
z .describe(
.literal('expands_offices_in') 'Company opens new offices in another town, state, country or continent. (expansion)'
.describe('Company expands existing offices. (expansion)'), ),
z z
.literal('expands_facilities') .literal('expands_offices_in')
.describe( .describe('Company expands existing offices. (expansion)'),
'Company opens new or expands existing facilities like warehouses, data centers, manufacturing plants etc. (expansion)' z
), .literal('expands_facilities')
z .describe(
.literal('opens_new_location') 'Company opens new or expands existing facilities like warehouses, data centers, manufacturing plants etc. (expansion)'
.describe( ),
'Company opens new service location like hotels, restaurants, bars, hospitals etc. (expansion)' z
), .literal('opens_new_location')
z .describe(
.literal('increases_headcount_by') 'Company opens new service location like hotels, restaurants, bars, hospitals etc. (expansion)'
.describe('Company offers new job vacancies. (expansion)'), ),
z z
.literal('launches') .literal('increases_headcount_by')
.describe('Company launches new offering. (new_offering)'), .describe('Company offers new job vacancies. (expansion)'),
z z
.literal('integrates_with') .literal('launches')
.describe('Company integrates with other company. (new_offering)'), .describe('Company launches new offering. (new_offering)'),
z z
.literal('is_developing') .literal('integrates_with')
.describe('Company begins development of a new offering. (new_offering)'), .describe('Company integrates with other company. (new_offering)'),
z z
.literal('receives_financing') .literal('is_developing')
.describe( .describe(
'Company receives investment like venture funding, loan, grant etc. (investment)' 'Company begins development of a new offering. (new_offering)'
), ),
z z
.literal('invests_into') .literal('receives_financing')
.describe('Company invests into other company. (investment)'), .describe(
z 'Company receives investment like venture funding, loan, grant etc. (investment)'
.literal('invests_into_assets') ),
.describe( z
'Company invests into assets like property, trucks, facilities etc. (investment)' .literal('invests_into')
), .describe('Company invests into other company. (investment)'),
z z
.literal('goes_public') .literal('invests_into_assets')
.describe( .describe(
'Company issues shares to the public for the first time. (investment)' 'Company invests into assets like property, trucks, facilities etc. (investment)'
), ),
z z
.literal('closes_offices_in') .literal('goes_public')
.describe('Company closes existing offices. (cost_cutting)'), .describe(
z 'Company issues shares to the public for the first time. (investment)'
.literal('decreases_headcount_by') ),
.describe('Company lays off employees. (cost_cutting)'), z
z .literal('closes_offices_in')
.literal('partners_with') .describe('Company closes existing offices. (cost_cutting)'),
.describe('Company partners with other company. (partnership)'), z
z .literal('decreases_headcount_by')
.literal('receives_award') .describe('Company lays off employees. (cost_cutting)'),
.describe( z
'Company or person at the company receives an award. (recognition)' .literal('partners_with')
), .describe('Company partners with other company. (partnership)'),
z z
.literal('recognized_as') .literal('receives_award')
.describe( .describe(
'Company or person at the company receives recognition. (recognition)' 'Company or person at the company receives an award. (recognition)'
), ),
z z
.literal('signs_new_client') .literal('recognized_as')
.describe('Company signs new client. (contract)'), .describe(
z 'Company or person at the company receives recognition. (recognition)'
.literal('files_suit_against') ),
.describe( z
'Company files suit against other company. (corporate_challenges)' .literal('signs_new_client')
), .describe('Company signs new client. (contract)'),
z z
.literal('has_issues_with') .literal('files_suit_against')
.describe('Company has vulnerability problems. (corporate_challenges)'), .describe(
z 'Company files suit against other company. (corporate_challenges)'
.literal('identified_as_competitor_of') ),
.describe('New or existing competitor was identified. (relational)') z
]) .literal('has_issues_with')
.describe('Company has vulnerability problems. (corporate_challenges)'),
z
.literal('identified_as_competitor_of')
.describe('New or existing competitor was identified. (relational)')
])
.describe('Event category')
export type EventCategory = z.infer<typeof EventCategorySchema> export type EventCategory = z.infer<typeof EventCategorySchema>
export const CompanyParamsSchema = z.object({ export const CompanyParamsSchema = z.object({
@ -535,17 +549,15 @@ export class PredictLeadsClient extends AIFunctionsProvider {
domain, domain,
page = 1, page = 1,
limit = predictleads.DEFAULT_PAGE_SIZE, limit = predictleads.DEFAULT_PAGE_SIZE,
categories,
...params ...params
} = opts } = opts
assert(domain, 'Missing required company "domain"') assert(domain, 'Missing required company "domain"')
return this.ky return this.ky
.get(`v2/companies/${domain}/events`, { .get(`v2/companies/${domain}/events`, {
searchParams: pruneUndefined({ searchParams: sanitizeSearchParams({
page, page,
limit: String(limit), limit,
categories: categories?.join(','),
...params ...params
}) })
}) })
@ -586,19 +598,13 @@ export class PredictLeadsClient extends AIFunctionsProvider {
) { ) {
const opts = const opts =
typeof domainOrOpts === 'string' ? { domain: domainOrOpts } : domainOrOpts typeof domainOrOpts === 'string' ? { domain: domainOrOpts } : domainOrOpts
const { const { domain, limit = predictleads.DEFAULT_PAGE_SIZE, ...params } = opts
domain,
limit = predictleads.DEFAULT_PAGE_SIZE,
categories,
...params
} = opts
assert(domain, 'Missing required company "domain"') assert(domain, 'Missing required company "domain"')
return this.ky return this.ky
.get(`v2/companies/${domain}/job_openings`, { .get(`v2/companies/${domain}/job_openings`, {
searchParams: pruneUndefined({ searchParams: sanitizeSearchParams({
limit: String(limit), limit,
categories: categories?.join(','),
...params ...params
}) })
}) })
@ -621,19 +627,13 @@ export class PredictLeadsClient extends AIFunctionsProvider {
) { ) {
const opts = const opts =
typeof domainOrOpts === 'string' ? { domain: domainOrOpts } : domainOrOpts typeof domainOrOpts === 'string' ? { domain: domainOrOpts } : domainOrOpts
const { const { domain, limit = predictleads.DEFAULT_PAGE_SIZE, ...params } = opts
domain,
limit = predictleads.DEFAULT_PAGE_SIZE,
categories,
...params
} = opts
assert(domain, 'Missing required company "domain"') assert(domain, 'Missing required company "domain"')
return this.ky return this.ky
.get(`v2/companies/${domain}/technologies`, { .get(`v2/companies/${domain}/technologies`, {
searchParams: pruneUndefined({ searchParams: sanitizeSearchParams({
limit: String(limit), limit,
categories: categories?.join(','),
...params ...params
}) })
}) })
@ -651,19 +651,13 @@ export class PredictLeadsClient extends AIFunctionsProvider {
) { ) {
const opts = const opts =
typeof domainOrOpts === 'string' ? { domain: domainOrOpts } : domainOrOpts typeof domainOrOpts === 'string' ? { domain: domainOrOpts } : domainOrOpts
const { const { domain, limit = predictleads.DEFAULT_PAGE_SIZE, ...params } = opts
domain,
limit = predictleads.DEFAULT_PAGE_SIZE,
categories,
...params
} = opts
assert(domain, 'Missing required company "domain"') assert(domain, 'Missing required company "domain"')
return this.ky return this.ky
.get(`v2/companies/${domain}/connections`, { .get(`v2/companies/${domain}/connections`, {
searchParams: pruneUndefined({ searchParams: sanitizeSearchParams({
limit: String(limit), limit,
categories: categories?.join(','),
...params ...params
}) })
}) })
@ -686,7 +680,7 @@ export class PredictLeadsClient extends AIFunctionsProvider {
return this.ky return this.ky
.get(`v2/companies/${domain}/website_evolution`, { .get(`v2/companies/${domain}/website_evolution`, {
searchParams: pruneUndefined({ limit: String(limit), ...params }) searchParams: sanitizeSearchParams({ limit, ...params })
}) })
.json<predictleads.Response>() .json<predictleads.Response>()
} }
@ -707,7 +701,7 @@ export class PredictLeadsClient extends AIFunctionsProvider {
return this.ky return this.ky
.get(`v2/companies/${domain}/github_repositories`, { .get(`v2/companies/${domain}/github_repositories`, {
searchParams: pruneUndefined({ limit: String(limit), ...params }) searchParams: sanitizeSearchParams({ limit, ...params })
}) })
.json<predictleads.Response>() .json<predictleads.Response>()
} }
@ -723,19 +717,13 @@ export class PredictLeadsClient extends AIFunctionsProvider {
) { ) {
const opts = const opts =
typeof domainOrOpts === 'string' ? { domain: domainOrOpts } : domainOrOpts typeof domainOrOpts === 'string' ? { domain: domainOrOpts } : domainOrOpts
const { const { domain, limit = predictleads.DEFAULT_PAGE_SIZE, ...params } = opts
domain,
sources,
limit = predictleads.DEFAULT_PAGE_SIZE,
...params
} = opts
assert(domain, 'Missing required company "domain"') assert(domain, 'Missing required company "domain"')
return this.ky return this.ky
.get(`v2/companies/${domain}/products`, { .get(`v2/companies/${domain}/products`, {
searchParams: pruneUndefined({ searchParams: sanitizeSearchParams({
limit: String(limit), limit,
sources: sources?.join(','),
...params ...params
}) })
}) })
@ -783,7 +771,7 @@ export class PredictLeadsClient extends AIFunctionsProvider {
async getFollowingCompanies(limit: number = predictleads.DEFAULT_PAGE_SIZE) { async getFollowingCompanies(limit: number = predictleads.DEFAULT_PAGE_SIZE) {
return this.ky return this.ky
.get(`v2/followings`, { .get(`v2/followings`, {
searchParams: { limit: String(limit) } searchParams: sanitizeSearchParams({ limit })
}) })
.json<predictleads.FollowedCompaniesResponse>() .json<predictleads.FollowedCompaniesResponse>()
} }

Wyświetl plik

@ -3,7 +3,7 @@ import pThrottle from 'p-throttle'
import { expect, test } from 'vitest' import { expect, test } from 'vitest'
import { mockKyInstance } from './_utils.js' import { mockKyInstance } from './_utils.js'
import { omit, pick, throttleKy } from './utils.js' import { omit, pick, sanitizeSearchParams, throttleKy } from './utils.js'
test('pick', () => { test('pick', () => {
expect(pick({ a: 1, b: 2, c: 3 }, 'a', 'c')).toEqual({ a: 1, c: 3 }) expect(pick({ a: 1, b: 2, c: 3 }, 'a', 'c')).toEqual({ a: 1, c: 3 })
@ -19,6 +19,33 @@ test('omit', () => {
).toEqual({ a: { b: 'foo' }, d: -1 }) ).toEqual({ a: { b: 'foo' }, d: -1 })
}) })
test('sanitizeSearchParams', () => {
expect(
sanitizeSearchParams({ a: 1, b: undefined, c: 13 }).toString()
).toMatchSnapshot()
expect(sanitizeSearchParams({ a: [1, 2, 3] }).toString()).toMatchSnapshot()
expect(
sanitizeSearchParams({ b: ['a', 'b'], foo: true }).toString()
).toMatchSnapshot()
expect(
sanitizeSearchParams({ b: [false, true, false] }).toString()
).toMatchSnapshot()
expect(
sanitizeSearchParams({
flag: ['foo', 'bar', 'baz'],
token: 'test'
}).toString()
).toMatchSnapshot()
expect(sanitizeSearchParams({}).toString()).toMatchSnapshot()
expect(sanitizeSearchParams({ a: [] }).toString()).toMatchSnapshot()
})
test( test(
'throttleKy should rate-limit requests to ky properly', 'throttleKy should rate-limit requests to ky properly',
async () => { async () => {

Wyświetl plik

@ -86,3 +86,28 @@ export function throttleKy(
} }
}) })
} }
/**
* Creates a new `URLSearchParams` object with all values coerced to strings
* that correctly handles arrays of values as repeated keys.
*/
export function sanitizeSearchParams(
searchParams: Record<
string,
string | number | boolean | string[] | number[] | boolean[] | undefined
>
): URLSearchParams {
return new URLSearchParams(
Object.entries(searchParams).flatMap(([key, value]) => {
if (key === undefined || value === undefined) {
return []
}
if (Array.isArray(value)) {
return value.map((v) => [key, String(v)])
}
return [[key, String(value)]]
})
)
}