Skip to content
This repository has been archived by the owner on Jul 6, 2022. It is now read-only.

Commit

Permalink
feat(client): support scalar list filters
Browse files Browse the repository at this point in the history
  • Loading branch information
aiji42 committed May 8, 2022
1 parent 41e77ea commit 4c51e67
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 32 deletions.
76 changes: 61 additions & 15 deletions packages/client/src/__tests__/makeWhere.test.ts
@@ -1,31 +1,77 @@
import { makeWhere } from '../helpers/makeWhere'
import { Args } from '../types'
import { Args, Models } from '../types'

const models = {
User: {
fields: {
id: { isList: false },
name: { isList: false },
labels: { isList: true },
languages: { isList: true },
tags1: { isList: true },
tags2: { isList: true },
tags3: { isList: true },
},
},
} as unknown as Models

test('make where statement', () => {
expect(
makeWhere({ where: { id: 1, name: { in: ['a', 'b', 'c'] } } }),
makeWhere({ where: { id: 1, name: { in: ['a', 'b', 'c'] } } }, 'User', {
models,
}),
).toEqual('id.eq.1,name.in.("a","b","c")')
})

test('make where statement with not operator', () => {
expect(makeWhere({ where: { name: { not: null } } })).toEqual(
'name.not.is.null',
)
expect(
makeWhere({ where: { name: { not: null } } }, 'User', {
models,
}),
).toEqual('name.not.is.null')
})

test('make where statement with OR and NOT operator', () => {
expect(
makeWhere({
where: {
OR: [
{ name: { startsWith: 'a' } },
{ name: { endsWith: 'b' } },
{ name: { contains: 'c', mode: 'insensitive' } },
],
NOT: [{ id: { gt: 10 } }, { id: { lt: 100 } }],
} as Args['where'],
}),
makeWhere(
{
where: {
OR: [
{ name: { startsWith: 'a' } },
{ name: { endsWith: 'b' } },
{ name: { contains: 'c', mode: 'insensitive' } },
],
NOT: [{ id: { gt: 10 } }, { id: { lt: 100 } }],
} as Args['where'],
},
'User',
{
models,
},
),
).toEqual(
'or(name.like.a*,name.like.*b,name.ilike.*c*),not.and(id.gt.10,id.lt.100)',
)
})

test('make where statement with list field operator', () => {
expect(
makeWhere(
{
where: {
labels: { has: 'a' },
languages: { hasSome: ['a'] },
tags1: { hasEvery: ['a', 'b'] },
tags2: { isEmpty: true },
tags3: { equals: 'c' },
},
},
'User',
{
models,
},
),
).toEqual(
'labels.cs.{"a"},languages.ov.{"a"},tags1.cs.{"a","b"},tags2.eq.{},tags3.eq.{"c"}',
)
})
2 changes: 1 addition & 1 deletion packages/client/src/helpers/makeFetcher.ts
Expand Up @@ -20,7 +20,7 @@ export const makeFetcher = (
return (args, method, model, modelMap, headers) => {
const select = makeSelect(args, model, modelMap)
const orderBy = makeOrder(args)
const where = makeWhere(args)
const where = makeWhere(args, model, modelMap)

const url = new URL(endpoint)
url.pathname = `/rest/v1/${modelMap.models[model]?.dbName ?? model}`
Expand Down
97 changes: 82 additions & 15 deletions packages/client/src/helpers/makeWhere.ts
@@ -1,18 +1,31 @@
import { Args, NegativeOperators, Operators, Scalar, Where } from '../types'
import {
Args,
ModelMapping,
NegativeOperators,
Operators,
Scalar,
Where,
} from '../types'

// TODO: related table
export const makeWhere = (arg: Args) => {
export const makeWhere = (
arg: Args,
model: string,
{ models }: Pick<ModelMapping, 'models'>,
) => {
if (!arg.where) return ''
const { AND, OR, NOT, ...rest } = arg.where
let where = []
if (AND) where.push(_AND(AND))
if (OR) where.push(_OR(OR))
if (NOT) where.push(_NOT(NOT))
if (AND) where.push(_AND(AND, model, { models }))
if (OR) where.push(_OR(OR, model, { models }))
if (NOT) where.push(_NOT(NOT, model, { models }))
const restStatement = Object.entries(rest)
.flatMap(([col, cond]) => {
const s = []
if (cond === null || typeof cond !== 'object') return _equals(col, cond)
if (cond.equals !== undefined) s.push(_equals(col, cond.equals))
if (cond === null || typeof cond !== 'object')
return _equals(col, cond, false)
if (cond.equals !== undefined)
s.push(_equals(col, cond.equals, isListColumn(col, model, { models })))
if (cond.in !== undefined) s.push(_in(col, cond.in))
if (cond.notIn !== undefined) s.push(_notIn(col, cond.notIn))
if (cond.lt !== undefined) s.push(_lt(col, cond.lt))
Expand All @@ -25,6 +38,10 @@ export const makeWhere = (arg: Args) => {
s.push(_startsWith(col, cond.startsWith, cond.mode))
if (cond.endsWith !== undefined)
s.push(_endsWith(col, cond.endsWith, cond.mode))
if (cond.has !== undefined) s.push(_has(col, cond.has))
if (cond.hasEvery !== undefined) s.push(_hasEvery(col, cond.hasEvery))
if (cond.hasSome !== undefined) s.push(_hasSome(col, cond.hasSome))
if (cond.isEmpty !== undefined) s.push(_isEmpty(col, cond.isEmpty))
if (cond.not !== undefined) s.push(_not(col, cond.not, cond.mode))
return s
})
Expand All @@ -33,22 +50,45 @@ export const makeWhere = (arg: Args) => {
return where.join(',')
}

const _AND = (condition: Where[] | Where): string => {
const _AND = (
condition: Where[] | Where,
model: string,
{ models }: Pick<ModelMapping, 'models'>,
): string => {
const cond = Array.isArray(condition) ? condition : [condition]
return `and(${cond.map((where) => makeWhere({ where })).join(',')})`
return `and(${cond
.map((where) => makeWhere({ where }, model, { models }))
.join(',')})`
}

const _OR = (condition: Where[] | Where): string => {
const _OR = (
condition: Where[] | Where,
model: string,
{ models }: Pick<ModelMapping, 'models'>,
): string => {
const cond = Array.isArray(condition) ? condition : [condition]
return `or(${cond.map((where) => makeWhere({ where })).join(',')})`
return `or(${cond
.map((where) => makeWhere({ where }, model, { models }))
.join(',')})`
}

const _NOT = (condition: Where[] | Where): string => {
const _NOT = (
condition: Where[] | Where,
model: string,
{ models }: Pick<ModelMapping, 'models'>,
): string => {
const cond = Array.isArray(condition) ? condition : [condition]
return `not.and(${cond.map((where) => makeWhere({ where })).join(',')})`
return `not.and(${cond
.map((where) => makeWhere({ where }, model, { models }))
.join(',')})`
}

const _equals = (col: string, condition: Required<Operators['equals']>) => {
const _equals = (
col: string,
condition: Required<Operators['equals']>,
forList: boolean,
) => {
if (forList) return _equalsList(col, condition)
if (typeof condition === 'boolean' || condition === null)
return `${col}.is.${condition}`
return `${col}.eq.${condition}`
Expand Down Expand Up @@ -100,7 +140,6 @@ const _notGte = (col: string, condition: Required<Operators['gte']>) => {
return `${col}.not.gte.${condition}`
}

// TODO: Filters for list-type columns
const _contains = (
col: string,
condition: Required<Operators['contains']>,
Expand Down Expand Up @@ -175,3 +214,31 @@ const _not = (
if (condition.not !== undefined) s.push(_not(col, !condition.not, mode))
return s.join(',')
}

const _has = (col: string, condition: Required<Operators['has']>) =>
_hasEvery(col, condition)

const _hasEvery = (col: string, condition: Required<Operators['hasEvery']>) => {
const cond = Array.isArray(condition) ? condition : [condition]
return `${col}.cs.{${JSON.stringify(cond).replace(/^\[|]$/g, '')}}`
}

const _hasSome = (col: string, condition: Required<Operators['hasSome']>) => {
const cond = Array.isArray(condition) ? condition : [condition]
return `${col}.ov.{${JSON.stringify(cond).replace(/^\[|]$/g, '')}}`
}

const _isEmpty = (col: string, condition: Required<Operators['isEmpty']>) => {
return condition ? `${col}.eq.{}` : `${col}.neq.{}`
}

const _equalsList = (col: string, condition: Required<Operators['equals']>) => {
const cond = Array.isArray(condition) ? condition : [condition]
return `${col}.eq.{${JSON.stringify(cond).replace(/^\[|]$/g, '')}}`
}

const isListColumn = (
column: string,
model: string,
{ models }: Pick<ModelMapping, 'models'>,
) => models[model]?.fields[column]?.isList ?? false
7 changes: 6 additions & 1 deletion packages/client/src/types.d.ts
Expand Up @@ -3,7 +3,7 @@ import { DMMF } from '@prisma/generator-helper'
export type Scalar = number | string | boolean | null

export type Operators = {
equals?: Scalar
equals?: Scalar | Scalar[]
in?: Scalar[]
notIn?: Scalar[]
lt?: number
Expand All @@ -14,6 +14,11 @@ export type Operators = {
mode?: 'default' | 'insensitive'
startsWith?: string
endsWith?: string

has?: Scalar
hasEvery?: Scalar | Scalar[]
hasSome?: Scalar | Scalar[]
isEmpty?: boolean
}

export type NegativeOperators = {
Expand Down

0 comments on commit 4c51e67

Please sign in to comment.