Skip to content

Commit

Permalink
UBERF-9575/UBERF-9560 (#8170)
Browse files Browse the repository at this point in the history
Signed-off-by: Andrey Sobolev <[email protected]>
  • Loading branch information
haiodo authored Mar 7, 2025
1 parent 427ef59 commit e8f32c4
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,6 @@
const hierarchy = client.getHierarchy()
const dispatch = createEventDispatcher()
let maxIndex = 1
function onChange (e: Filter | undefined) {
if (e === undefined) return
updateFilter(e)
Expand All @@ -52,7 +50,7 @@
_class,
target,
space,
index: ++maxIndex,
index: $filterStore.map((it) => it.index).reduce((a, b) => Math.max(a, b), 0) + 1,
onChange
},
target
Expand Down
46 changes: 45 additions & 1 deletion server/postgres/src/__tests__/conversion.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import core, {
type WorkspaceUuid
} from '@hcengineering/core'
import { PostgresAdapter } from '../storage'
import { convertArrayParams, decodeArray } from '../utils'
import { convertArrayParams, decodeArray, filterProjection } from '../utils'
import { genMinModel, test, type ComplexClass } from './minmodel'
import { createDummyClient, type TypedQuery } from './utils'

Expand Down Expand Up @@ -160,3 +160,47 @@ function createTestContext (): { adapter: PostgresAdapter, ctx: MeasureMetricsCo
)
return { adapter, ctx, queries }
}

describe('projection', () => {
it('mixin query projection', () => {
const data = {
'638611f18894c91979399ef3': {
Источник_6386125d8894c91979399eff: 'Workable'
},
attachments: 1,
avatar: null,
avatarProps: null,
avatarType: 'color',
channels: 3,
city: 'Poland',
docUpdateMessages: 31,
name: 'Mulkuha,Muklyi',
'notification:mixin:Collaborators': {
collaborators: []
},
'recruit:mixin:Candidate': {
Title_63f38419efefd99805238bbd: 'Backend-RoR',
Trash_64493626f9b50e77bf82d231: 'Нет',
__mixin: 'true',
applications: 1,
onsite: null,
remote: null,
skills: 18,
title: '',
Опытработы_63860d5c8894c91979399e73: '2018',
Уровеньанглийского_63860d038894c91979399e6f: 'UPPER'
}
}
const projected = filterProjection<any>(data, {
'recruit:mixin:Candidate.Уровеньанглийского_63860d038894c91979399e6f': 1,
_class: 1,
space: 1,
modifiedOn: 1
})
expect(projected).toEqual({
'recruit:mixin:Candidate': {
Уровеньанглийского_63860d038894c91979399e6f: 'UPPER'
}
})
})
})
79 changes: 57 additions & 22 deletions server/postgres/src/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1305,7 +1305,11 @@ abstract class PostgresAdapterBase implements DbAdapter {
private translateQueryValue (vars: ValuesVariables, tkey: string, value: any, type: ValueType): string | undefined {
const tkeyData = tkey.includes('data') && (tkey.includes('->') || tkey.includes('#>>'))
if (tkeyData && (Array.isArray(value) || (typeof value !== 'object' && typeof value !== 'string'))) {
value = Array.isArray(value) ? value.map((it) => (it == null ? null : `${it}`)) : `${value}`
value = Array.isArray(value)
? value.map((it) => (it == null ? null : `${it}`))
: value == null
? null
: `${value}`
}

if (value === null) {
Expand All @@ -1316,76 +1320,87 @@ abstract class PostgresAdapterBase implements DbAdapter {
for (const operator in value) {
let val = value[operator]
if (tkeyData && (Array.isArray(val) || (typeof val !== 'object' && typeof val !== 'string'))) {
val = Array.isArray(val) ? val.map((it) => (it == null ? null : `${it}`)) : `${val}`
val = Array.isArray(val) ? val.map((it) => (it == null ? null : `${it}`)) : val == null ? null : `${val}`
}

let valType = inferType(val)
const { tlkey, arrowCount } = prepareJsonValue(tkey, valType)
if (arrowCount > 0 && valType === '::text') {
valType = ''
}

switch (operator) {
case '$ne':
if (val === null) {
res.push(`${tkey} IS NOT NULL`)
if (val == null) {
res.push(`${tlkey} IS NOT NULL`)
} else {
res.push(`${tkey} != ${vars.add(val, inferType(val))}`)
res.push(`${tlkey} != ${vars.add(val, valType)}`)
}
break
case '$gt':
res.push(`${tkey} > ${vars.add(val, inferType(val))}`)
res.push(`${tlkey} > ${vars.add(val, valType)}`)
break
case '$gte':
res.push(`${tkey} >= ${vars.add(val, inferType(val))}`)
res.push(`${tlkey} >= ${vars.add(val, valType)}`)
break
case '$lt':
res.push(`${tkey} < ${vars.add(val, inferType(val))}`)
res.push(`${tlkey} < ${vars.add(val, valType)}`)
break
case '$lte':
res.push(`${tkey} <= ${vars.add(val, inferType(val))}`)
res.push(`${tlkey} <= ${vars.add(val, valType)}`)
break
case '$in':
switch (type) {
case 'common':
if (Array.isArray(val) && val.includes(null)) {
const vv = vars.addArray(val, inferType(val))
res.push(`(${tkey} = ANY(${vv}) OR ${tkey} IS NULL)`)
const vv = vars.addArray(val, valType)
res.push(`(${tlkey} = ANY(${vv}) OR ${tkey} IS NULL)`)
} else {
if (val.length > 0) {
res.push(`${tkey} = ANY(${vars.addArray(val, inferType(val))})`)
res.push(`${tlkey} = ANY(${vars.addArray(val, valType)})`)
} else {
res.push(`${tkey} IN ('NULL')`)
res.push(`${tlkey} IN ('NULL')`)
}
}
break
case 'array':
{
const vv = vars.addArrayI(val, inferType(val))
const vv = vars.addArrayI(val, valType)
res.push(`${tkey} && ${vv}`)
}
break
case 'dataArray':
{
const vv = vars.addArrayI(val, inferType(val))
const vv = vars.addArrayI(val, valType)
res.push(`${tkey} ?| ${vv}`)
}
break
}
break
case '$nin':
if (Array.isArray(val) && val.includes(null)) {
res.push(`(${tkey} != ALL(${vars.addArray(val, inferType(val))}) AND ${tkey} IS NOT NULL)`)
res.push(`(${tlkey} != ALL(${vars.addArray(val, valType)}) AND ${tkey} IS NOT NULL)`)
} else if (Array.isArray(val) && val.length > 0) {
res.push(`${tkey} != ALL(${vars.addArray(val, inferType(val))})`)
res.push(`${tlkey} != ALL(${vars.addArray(val, valType)})`)
}
break
case '$like':
res.push(`${tkey} ILIKE ${vars.add(val, inferType(val))}`)
res.push(`${tlkey} ILIKE ${vars.add(val, valType)}`)
break
case '$exists':
res.push(`${tkey} IS ${val === true || val === 'true' ? 'NOT NULL' : 'NULL'}`)
res.push(`${tlkey} IS ${val === true || val === 'true' ? 'NOT NULL' : 'NULL'}`)
break
case '$regex':
res.push(`${tkey} SIMILAR TO ${vars.add(val, inferType(val))}`)
res.push(`${tlkey} SIMILAR TO ${vars.add(val, valType)}`)
break
case '$options':
break
case '$all':
res.push(`${tkey} @> ${vars.addArray(value, inferType(value))}`)
if (arrowCount > 0) {
res.push(`${tkey} @> '${JSON.stringify(val)}'::jsonb`)
} else {
res.push(`${tkey} @> ${vars.addArray(val, valType)}`)
}
break
default:
res.push(`${tkey} @> '[${JSON.stringify(value)}]'`)
Expand All @@ -1395,8 +1410,13 @@ abstract class PostgresAdapterBase implements DbAdapter {
return res.length === 0 ? undefined : res.join(' AND ')
}

let valType = inferType(value)
const { tlkey, arrowCount } = prepareJsonValue(tkey, valType)
if (arrowCount > 0 && valType === '::text') {
valType = ''
}
return type === 'common'
? `${tkey} = ${vars.add(value, inferType(value))}`
? `${tlkey} = ${vars.add(value, valType)}`
: type === 'array'
? `${tkey} @> '${typeof value === 'string' ? '{"' + value + '"}' : value}'`
: `${tkey} @> '${typeof value === 'string' ? '"' + value + '"' : value}'`
Expand Down Expand Up @@ -2083,6 +2103,21 @@ class PostgresTxAdapter extends PostgresAdapterBase implements TxAdapter {
return this.stripHash(systemTx.concat(userTx)) as Tx[]
}
}
function prepareJsonValue (tkey: string, valType: string): { tlkey: string, arrowCount: number } {
if (valType === '::string') {
valType = '' // No need to add a string conversion
}
const arrowCount = (tkey.match(/->/g) ?? []).length
// We need to convert to type without array if pressent
let tlkey = arrowCount > 0 ? `(${tkey})${valType.replace('[]', '')}` : tkey

if (arrowCount > 0) {
// We need to replace only the last -> to ->>
tlkey = arrowCount === 1 ? tlkey.replace('->', '->>') : tlkey.replace(/->(?!.*->)/, '->>')
}
return { tlkey, arrowCount }
}

/**
* @public
*/
Expand Down
34 changes: 27 additions & 7 deletions server/postgres/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,30 @@ export function convertArrayParams (parameters?: ParameterOrJSON<any>[]): any[]
})
}

export function filterProjection<T extends Doc> (data: any, projection: Projection<T> | undefined): any {
for (const key in data) {
if (!Object.prototype.hasOwnProperty.call(projection, key) || (projection as any)[key] === 0) {
// check nested projections in case of object
let value = data[key]
if (typeof value === 'object' && !Array.isArray(value) && value != null) {
// We need to filter projection for nested objects
const innerP = Object.entries(projection as any)
.filter((it) => it[0].startsWith(key))
.map((it) => [it[0].substring(key.length + 1), it[1]])
if (innerP.length > 0) {
value = filterProjection(value, Object.fromEntries(innerP))
data[key] = value
continue
}
}

// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete data[key]
}
}
return data
}

export function parseDocWithProjection<T extends Doc> (
doc: DBDoc,
domain: string,
Expand All @@ -574,16 +598,12 @@ export function parseDocWithProjection<T extends Doc> (
;(rest as any)[key] = decodeArray((rest as any)[key])
}
}
let resultData = data
if (projection !== undefined) {
for (const key in data) {
if (!Object.prototype.hasOwnProperty.call(projection, key) || (projection as any)[key] === 0) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete data[key]
}
}
resultData = filterProjection(data, projection)
}
const res = {
...data,
...resultData,
...rest
} as any as T

Expand Down

0 comments on commit e8f32c4

Please sign in to comment.