Skip to content

Commit

Permalink
fix: convert multiple filter values to a string (#1776)
Browse files Browse the repository at this point in the history
  • Loading branch information
veu committed Mar 17, 2023
1 parent 045bd10 commit 4fa43a5
Show file tree
Hide file tree
Showing 28 changed files with 802 additions and 603 deletions.
9 changes: 5 additions & 4 deletions lib/create-contentful-api.ts
Expand Up @@ -28,7 +28,8 @@ import {
SyncOptions,
} from './types'
import { EntryQueries, LocaleOption, TagQueries } from './types/query/query'
import { FieldsType } from './types'
import { FieldsType } from './types/query/util'
import normalizeSearchParameters from './utils/normalize-search-parameters'
import normalizeSelect from './utils/normalize-select'
import resolveCircular from './utils/resolve-circular'
import validateTimestamp from './utils/validate-timestamp'
Expand Down Expand Up @@ -457,7 +458,7 @@ export default function createContentfulApi<OptionType extends ChainOptions>(
const entries = await get({
context: 'environment',
path: 'entries',
config: createRequestConfig({ query: normalizeSelect(query) }),
config: createRequestConfig({ query: normalizeSearchParameters(normalizeSelect(query)) }),
})

return resolveCircular(entries, {
Expand Down Expand Up @@ -534,7 +535,7 @@ export default function createContentfulApi<OptionType extends ChainOptions>(
return get({
context: 'environment',
path: 'assets',
config: createRequestConfig({ query: normalizeSelect(query) }),
config: createRequestConfig({ query: normalizeSearchParameters(normalizeSelect(query)) }),
})
} catch (error) {
errorHandler(error as AxiosError)
Expand All @@ -552,7 +553,7 @@ export default function createContentfulApi<OptionType extends ChainOptions>(
return get<TagCollection>({
context: 'environment',
path: 'tags',
config: createRequestConfig({ query: normalizeSelect(query) }),
config: createRequestConfig({ query: normalizeSearchParameters(normalizeSelect(query)) }),
})
}

Expand Down
16 changes: 11 additions & 5 deletions lib/types/query/equality.ts
@@ -1,5 +1,5 @@
import { EntryFields } from '../entry'
import { ConditionalQueries, NonEmpty } from './util'
import { ConditionalQueries } from './util'

type SupportedTypes =
| EntryFields.Symbol
Expand All @@ -14,14 +14,20 @@ type SupportedTypes =
* @desc equality - search for exact matches
* @see [Documentation]{@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/equality-operator}
*/
export type EqualityFilter<Fields, Prefix extends string> = NonEmpty<
ConditionalQueries<Fields, SupportedTypes, Prefix, ''>
export type EqualityFilter<Fields, Prefix extends string> = ConditionalQueries<
Fields,
SupportedTypes,
Prefix,
''
>

/**
* @desc inequality - exclude matching items
* @see [Documentation]{@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/inequality-operator}
*/
export type InequalityFilter<Fields, Prefix extends string> = NonEmpty<
ConditionalQueries<Fields, SupportedTypes, Prefix, '[ne]'>
export type InequalityFilter<Fields, Prefix extends string> = ConditionalQueries<
Fields,
SupportedTypes,
Prefix,
'[ne]'
>
46 changes: 16 additions & 30 deletions lib/types/query/location.ts
@@ -1,25 +1,22 @@
import { ConditionalPick } from 'type-fest'
import { EntryFields } from '../entry'
import { NonEmpty } from './util'

type Types = EntryFields.Location | undefined

export type ProximitySearchFilterInput = [number, number] | undefined
export type BoundingBoxSearchFilterInput = [number, number, number, number] | undefined
export type BoundingCircleSearchFilterInput = [number, number, number] | undefined
export type ProximitySearchFilterInput = [number, number]
export type BoundingBoxSearchFilterInput = [number, number, number, number]
export type BoundingCircleSearchFilterInput = [number, number, number]

type BaseLocationFilter<
Fields,
SupportedTypes,
ValueType,
Prefix extends string,
QueryFilter extends string = ''
> = NonEmpty<
NonNullable<{
[FieldName in keyof ConditionalPick<Fields, SupportedTypes> as `${Prefix}.${string &
FieldName}[${QueryFilter}]`]?: ValueType
}>
>
> = NonNullable<{
[FieldName in keyof ConditionalPick<Fields, SupportedTypes> as `${Prefix}.${string &
FieldName}[${QueryFilter}]`]?: ValueType
}>

/**
* @desc near - location proximity search
Expand All @@ -34,35 +31,24 @@ export type ProximitySearchFilter<Fields, Prefix extends string> = BaseLocationF
>

/**
* @desc within - location in a bounding rectangle
* @see [Documentation]{@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/locations-in-a-bounding-object}
*/
type BoundingBoxSearchFilter<Fields, Prefix extends string> = BaseLocationFilter<
Fields,
Types,
BoundingBoxSearchFilterInput,
Prefix,
'within'
>
/**
* @desc within - location in a bounding circle
* @desc within - location in a bounding object
* @see [Documentation]{@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/locations-in-a-bounding-object}
*/
type BoundingCircleSearchFilter<Fields, Prefix extends string> = BaseLocationFilter<
type BoundingObjectSearchFilter<Fields, Prefix extends string> = BaseLocationFilter<
Fields,
Types,
BoundingCircleSearchFilterInput,
BoundingCircleSearchFilterInput | BoundingBoxSearchFilterInput,
Prefix,
'within'
>

/**
* @desc location search
* @see [proximity]{@link ProximitySearchFilter}
* @see [bounding rectangle]{@link BoundingBoxSearchFilter}
* @see [bounding circle]{@link BoundingCircleSearchFilter}
* @see [bounding object]{@link BoundingObjectSearchFilter}
*/
export type LocationSearchFilters<Fields, Prefix extends string> =
| ProximitySearchFilter<Fields, Prefix>
| BoundingBoxSearchFilter<Fields, Prefix>
| BoundingCircleSearchFilter<Fields, Prefix>
export type LocationSearchFilters<Fields, Prefix extends string> = ProximitySearchFilter<
Fields,
Prefix
> &
BoundingObjectSearchFilter<Fields, Prefix>
19 changes: 9 additions & 10 deletions lib/types/query/query.ts
Expand Up @@ -57,14 +57,13 @@ export type EntriesQueries<Fields extends FieldsType> =

export type EntryQueries = Omit<FixedQueryOptions, 'query'>

export type AssetFieldsQueries<Fields extends FieldsType> =
| (ExistenceFilter<Fields, 'fields'> &
EqualityFilter<Fields, 'fields'> &
InequalityFilter<Fields, 'fields'> &
FullTextSearchFilters<Fields, 'fields'> &
AssetSelectFilter<Fields>)
| RangeFilters<Fields, 'fields'>
| SubsetFilters<Fields, 'fields'>
export type AssetFieldsQueries<Fields extends FieldsType> = ExistenceFilter<Fields, 'fields'> &
EqualityFilter<Fields, 'fields'> &
InequalityFilter<Fields, 'fields'> &
FullTextSearchFilters<Fields, 'fields'> &
AssetSelectFilter<Fields> &
RangeFilters<Fields, 'fields'> &
SubsetFilters<Fields, 'fields'>

export type AssetQueries<Fields extends FieldsType> = AssetFieldsQueries<Fields> &
SysQueries<Pick<AssetSys, 'createdAt' | 'updatedAt' | 'revision' | 'id' | 'type'>> &
Expand All @@ -76,8 +75,8 @@ export type TagNameFilters = {
name?: string
'name[ne]'?: string
'name[match]'?: string
'name[in]'?: string
'name[nin]'?: string
'name[in]'?: string[]
'name[nin]'?: string[]
}

export type TagQueries = TagNameFilters &
Expand Down
9 changes: 6 additions & 3 deletions lib/types/query/range.ts
@@ -1,5 +1,5 @@
import { EntryFields } from '../entry'
import { ConditionalQueries, NonEmpty } from './util'
import { ConditionalQueries } from './util'

type RangeFilterTypes = 'lt' | 'lte' | 'gt' | 'gte'

Expand All @@ -13,6 +13,9 @@ type SupportedTypes = EntryFields.Date | EntryFields.Number | EntryFields.Intege
* {string} gte: Greater than or equal to.
* @see [Documentation]{@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/select-operator}
*/
export type RangeFilters<Fields, Prefix extends string> = NonEmpty<
ConditionalQueries<Fields, SupportedTypes, Prefix, `[${RangeFilterTypes}]`>
export type RangeFilters<Fields, Prefix extends string> = ConditionalQueries<
Fields,
SupportedTypes,
Prefix,
`[${RangeFilterTypes}]`
>
10 changes: 7 additions & 3 deletions lib/types/query/search.ts
@@ -1,5 +1,5 @@
import { EntryFields } from '../entry'
import { ConditionalFixedQueries, NonEmpty } from './util'
import { ConditionalFixedQueries } from './util'

type SupportedTypes =
| EntryFields.Text
Expand All @@ -12,6 +12,10 @@ type SupportedTypes =
* @desc match - full text search
* @see [documentation]{@link https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/full-text-search}
*/
export type FullTextSearchFilters<Fields, Prefix extends string> = NonEmpty<
ConditionalFixedQueries<Fields, SupportedTypes, string, Prefix, '[match]'>
export type FullTextSearchFilters<Fields, Prefix extends string> = ConditionalFixedQueries<
Fields,
SupportedTypes,
string,
Prefix,
'[match]'
>
10 changes: 7 additions & 3 deletions lib/types/query/subset.ts
@@ -1,9 +1,10 @@
import { EntryFields } from '..'
import { ConditionalQueries, NonEmpty } from './util'
import { ConditionalListQueries } from './util'

type SubsetFilterTypes = 'in' | 'nin'
type SupportedTypes =
| EntryFields.Symbol
| EntryFields.Symbol[]
| EntryFields.Text
| EntryFields.Integer
| EntryFields.Number
Expand All @@ -19,6 +20,9 @@ type SupportedTypes =
* // {'fields.myField', 'singleValue'}
* // {'fields.myField', 'firstValue,secondValue'}
*/
export type SubsetFilters<Fields, Prefix extends string> = NonEmpty<
NonNullable<ConditionalQueries<Fields, SupportedTypes, Prefix, `[${SubsetFilterTypes}]`>>
export type SubsetFilters<Fields, Prefix extends string> = ConditionalListQueries<
Fields,
SupportedTypes,
Prefix,
`[${SubsetFilterTypes}]`
>
11 changes: 10 additions & 1 deletion lib/types/query/util.ts
Expand Up @@ -6,7 +6,6 @@ export type BaseOrArrayType<T> = T extends Array<infer U> ? U : T

export type NonEmpty<T> = T extends Record<string, never> ? never : T

//TODO: should we also allow ValueType[] for array types
export type ConditionalFixedQueries<
Fields,
SupportedTypes,
Expand All @@ -18,6 +17,16 @@ export type ConditionalFixedQueries<
FieldName}${QueryFilter}`]?: ValueType
}

export type ConditionalListQueries<
Fields,
SupportedTypes,
Prefix extends string,
QueryFilter extends string = ''
> = {
[FieldName in keyof ConditionalPick<Fields, SupportedTypes> as `${Prefix}.${string &
FieldName}${QueryFilter}`]?: NonNullable<BaseOrArrayType<Fields[FieldName]>>[]
}

export type ConditionalQueries<
Fields,
SupportedTypes,
Expand Down
18 changes: 18 additions & 0 deletions lib/utils/normalize-search-parameters.ts
@@ -0,0 +1,18 @@
export default function normalizeSearchParameters(query: Record<string, any>): Record<string, any> {
const convertedQuery = {}
let hasConverted = false
for (const key in query) {
// We allow multiple values to be passed as arrays
// which have to be converted to comma-separated strings before being sent to the API
if (Array.isArray(query[key])) {
convertedQuery[key] = query[key].join(',')
hasConverted = true
}
}

if (hasConverted) {
return { ...query, ...convertedQuery }
}

return query
}
4 changes: 2 additions & 2 deletions test/integration/getTags.test.ts
Expand Up @@ -38,14 +38,14 @@ describe('getTags', () => {
})

it('gets the tags with the name in the list of the provided value', async () => {
const response = await client.getTags({ 'name[in]': 'public tag,public tag 1' })
const response = await client.getTags({ 'name[in]': ['public tag', 'public tag 1'] })

expect(response.items).toHaveLength(1)
expect(response.items[0].name).toEqual('public tag 1')
})

it('gets the tags with the name not in the list of the provided value', async () => {
const response = await client.getTags({ 'name[nin]': 'public tag,public tag 1' })
const response = await client.getTags({ 'name[nin]': ['public tag', 'public tag 1'] })

expect(response.items).toHaveLength(0)
expect(response.items).toEqual([])
Expand Down
4 changes: 2 additions & 2 deletions test/integration/tests.test.ts
Expand Up @@ -217,7 +217,7 @@ test('Gets entries with array inequality query', async () => {
})

test('Gets entries with inclusion query', async () => {
const response = await client.getEntries({ 'sys.id[in]': 'finn,jake' })
const response = await client.getEntries({ 'sys.id[in]': ['finn', 'jake'] })

expect(response.total).toBe(2)
expect(response.items.filter((item) => item.sys.id === 'finn')).toHaveLength(1)
Expand Down Expand Up @@ -296,7 +296,7 @@ test('Gets entries with full text search query on field', async () => {
test('Gets entries with location proximity search', async () => {
const response = await client.getEntries({
content_type: '1t9IbcfdCk6m04uISSsaIK',
'fields.center[near]': '38,-122',
'fields.center[near]': [38, -122],
})

expect(response.items[0].fields.center.lat).toBeDefined()
Expand Down
21 changes: 19 additions & 2 deletions test/types/mocks.ts
Expand Up @@ -6,17 +6,34 @@ import {
AssetLink,
AssetSys,
BaseEntry,
EntryFields,
EntryLink,
EntrySys,
FieldsType,
} from '../../lib'
import {
BoundingBoxSearchFilterInput,
BoundingCircleSearchFilterInput,
ProximitySearchFilterInput,
} from '../../lib/types/query/location'

export const anyValue = '' as any
export const stringValue = ''
export const stringArrayValue = [stringValue]
export const numberValue = 123
export const booleanValue = true
export const dateValue = '2018-05-03T09:18:16.329Z'
export const numberArrayValue = [numberValue]

export const booleanValue = true as boolean
export const booleanArrayValue = [booleanValue]
export const dateValue: EntryFields.Date = '2018-05-03T09:18:16.329Z'
export const dateArrayValue = [dateValue]
export const locationValue = { lat: 55.01496234536782, lon: 38.75813066219786 }
export const jsonValue = {}

export const nearLocationValue: ProximitySearchFilterInput = [1, 0]
export const withinCircleLocationValue: BoundingCircleSearchFilterInput = [1, 0, 2]
export const withinBoxLocationValue: BoundingBoxSearchFilterInput = [1, 0, 2, 1]

export const metadataValue = { tags: [] }
export const entryLink: EntryLink = {
type: 'Link',
Expand Down

0 comments on commit 4fa43a5

Please sign in to comment.