diff --git a/packages/client/src/__tests__/makeWhere.test.ts b/packages/client/src/__tests__/makeWhere.test.ts index 8684e57..64df6c4 100644 --- a/packages/client/src/__tests__/makeWhere.test.ts +++ b/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"}', + ) +}) diff --git a/packages/client/src/helpers/makeFetcher.ts b/packages/client/src/helpers/makeFetcher.ts index a91ed1b..26dfdd8 100644 --- a/packages/client/src/helpers/makeFetcher.ts +++ b/packages/client/src/helpers/makeFetcher.ts @@ -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}` diff --git a/packages/client/src/helpers/makeWhere.ts b/packages/client/src/helpers/makeWhere.ts index e6b150e..2ff7989 100644 --- a/packages/client/src/helpers/makeWhere.ts +++ b/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, +) => { 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)) @@ -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 }) @@ -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, +): 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, +): 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, +): 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) => { +const _equals = ( + col: string, + condition: Required, + forList: boolean, +) => { + if (forList) return _equalsList(col, condition) if (typeof condition === 'boolean' || condition === null) return `${col}.is.${condition}` return `${col}.eq.${condition}` @@ -100,7 +140,6 @@ const _notGte = (col: string, condition: Required) => { return `${col}.not.gte.${condition}` } -// TODO: Filters for list-type columns const _contains = ( col: string, condition: Required, @@ -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) => + _hasEvery(col, condition) + +const _hasEvery = (col: string, condition: Required) => { + const cond = Array.isArray(condition) ? condition : [condition] + return `${col}.cs.{${JSON.stringify(cond).replace(/^\[|]$/g, '')}}` +} + +const _hasSome = (col: string, condition: Required) => { + const cond = Array.isArray(condition) ? condition : [condition] + return `${col}.ov.{${JSON.stringify(cond).replace(/^\[|]$/g, '')}}` +} + +const _isEmpty = (col: string, condition: Required) => { + return condition ? `${col}.eq.{}` : `${col}.neq.{}` +} + +const _equalsList = (col: string, condition: Required) => { + const cond = Array.isArray(condition) ? condition : [condition] + return `${col}.eq.{${JSON.stringify(cond).replace(/^\[|]$/g, '')}}` +} + +const isListColumn = ( + column: string, + model: string, + { models }: Pick, +) => models[model]?.fields[column]?.isList ?? false diff --git a/packages/client/src/types.d.ts b/packages/client/src/types.d.ts index 7b06dc3..39881df 100644 --- a/packages/client/src/types.d.ts +++ b/packages/client/src/types.d.ts @@ -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 @@ -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 = {