@autofleet/sadot 0.7.7-beta-0ecad376.0 → 0.7.7-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -48,27 +48,43 @@ exports.beforeBulkCreate = beforeBulkCreate;
48
48
  const beforeCreate = (scopeAttributes, modelOptions = {}) => async (instance, options) => {
49
49
  logger_1.default.debug('sadot - before create hook');
50
50
  const { fields } = options;
51
+ const { include, useEntityIdFromInclude } = modelOptions;
51
52
  const modelType = instance.constructor.name;
52
53
  const identifiers = (0, scopeAttributes_1.default)(instance, scopeAttributes);
53
- // get all model's required definitions
54
- const requiredFieldsNames = await DefinitionRepo.getRequiredFields(modelType, instance.id, identifiers, modelOptions);
54
+ const where = {
55
+ modelType,
56
+ disabled: false,
57
+ ...(!useEntityIdFromInclude && { entityId: identifiers }),
58
+ };
59
+ const fieldDefinitions = await DefinitionRepo.findAll(where, { withDisabled: false, transaction: options.transaction, include: include?.(identifiers) });
60
+ const requiredFieldsNames = Array.from(new Set(fieldDefinitions.filter(({ required }) => required).map(({ name }) => name)));
55
61
  const customFieldsIdx = fields.indexOf('customFields');
56
- const { customFields } = instance;
57
- if (customFieldsIdx > -1 && customFields) {
58
- const fieldsNames = Object.keys(customFields);
59
- const missingFields = requiredFieldsNames.filter((name) => !fieldsNames.includes(name));
60
- if (missingFields?.length > 0) {
61
- throw new errors_1.MissingRequiredCustomFieldError(missingFields);
62
- }
63
- await ValueRepo.updateValues(modelType, instance.id, identifiers, customFields, {
64
- transaction: options.transaction,
65
- modelOptions,
66
- }, true);
62
+ if ((customFieldsIdx === -1 || !instance.customFields) && requiredFieldsNames?.length > 0) {
63
+ throw new errors_1.MissingRequiredCustomFieldError(requiredFieldsNames);
64
+ }
65
+ const fieldsWithDefaultValue = fieldDefinitions.filter((def) => ![null, undefined].includes(def.defaultValue));
66
+ if (fieldsWithDefaultValue.length) {
67
67
  // eslint-disable-next-line no-param-reassign
68
- fields.splice(customFieldsIdx, 1);
68
+ instance.customFields || (instance.customFields = {});
69
+ fieldsWithDefaultValue.filter((def) => !(def.name in instance.customFields)).forEach(({ name, defaultValue }) => {
70
+ // eslint-disable-next-line no-param-reassign
71
+ instance.customFields[name] = defaultValue;
72
+ });
69
73
  }
70
- else if (requiredFieldsNames?.length > 0) {
71
- throw new errors_1.MissingRequiredCustomFieldError(requiredFieldsNames);
74
+ const { customFields } = instance;
75
+ if (customFieldsIdx === -1 || !customFields) {
76
+ return;
77
+ }
78
+ const fieldsNames = Object.keys(customFields);
79
+ const missingFields = requiredFieldsNames.filter((name) => !fieldsNames.includes(name));
80
+ if (missingFields?.length > 0) {
81
+ throw new errors_1.MissingRequiredCustomFieldError(missingFields);
72
82
  }
83
+ await ValueRepo.updateValues(modelType, instance.id, identifiers, customFields, {
84
+ transaction: options.transaction,
85
+ modelOptions,
86
+ });
87
+ // eslint-disable-next-line no-param-reassign
88
+ fields.splice(customFieldsIdx, 1);
73
89
  };
74
90
  exports.beforeCreate = beforeCreate;
@@ -24,5 +24,5 @@ export declare const findValuesByModelIds: (modelIds: string[], options?: any) =
24
24
  */
25
25
  export declare const updateValues: (modelType: string, modelId: string, identifiers: string[], valuesToUpdate: ValuesToUpdate, options?: FindOptions & {
26
26
  modelOptions?: ModelOptions;
27
- }, defineAllDefaults?: boolean) => Promise<CustomFieldValue[]>;
27
+ }) => Promise<CustomFieldValue[]>;
28
28
  export declare const deleteValue: (id: string, options?: any) => Promise<any>;
@@ -84,7 +84,7 @@ const formatFunctions = {
84
84
  * Create new value record if not exists, but fails if value's definition not exist.
85
85
  * Return the updated values
86
86
  */
87
- const updateValues = async (modelType, modelId, identifiers, valuesToUpdate, options = {}, defineAllDefaults = false) => {
87
+ const updateValues = async (modelType, modelId, identifiers, valuesToUpdate, options = {}) => {
88
88
  const names = Object.keys(valuesToUpdate);
89
89
  logger_1.default.debug(`custom-fields: updating values for ${modelType} ${modelId}`, {
90
90
  names,
@@ -95,12 +95,10 @@ const updateValues = async (modelType, modelId, identifiers, valuesToUpdate, opt
95
95
  const { modelOptions, transaction } = options;
96
96
  const where = {
97
97
  modelType,
98
- ...(!defineAllDefaults && { name: names }),
98
+ name: names,
99
99
  ...(!options.modelOptions?.useEntityIdFromInclude && { entityId: identifiers }),
100
100
  };
101
- const allFieldDefinitions = await DefinitionRepo.findAll(where, { withDisabled: true, transaction, include: modelOptions.include?.(identifiers) }) || [];
102
- const namesSet = new Set(defineAllDefaults ? names : undefined);
103
- const fieldDefinitions = defineAllDefaults ? allFieldDefinitions.filter((cfd) => namesSet.has(cfd.name)) : allFieldDefinitions;
101
+ const fieldDefinitions = await DefinitionRepo.findAll(where, { withDisabled: true, transaction, include: modelOptions.include?.(identifiers) }) ?? [];
104
102
  const disabledDefinitions = fieldDefinitions.filter((def) => def.disabled);
105
103
  if (fieldDefinitions.length !== names.length) {
106
104
  logger_1.default.warn(`custom-fields: missing definitions for ${modelType} ${modelId}`, { names, fieldDefinitions });
@@ -112,28 +110,17 @@ const updateValues = async (modelType, modelId, identifiers, valuesToUpdate, opt
112
110
  if (valuesWithDisabledDefinitions?.length > 0) {
113
111
  logger_1.default.warn(`custom-fields: trying to update disabled values: ${valuesWithDisabledDefinitions.join(', ')}`);
114
112
  }
115
- const visitedFields = new Set();
116
113
  const values = names.map((name) => {
117
114
  const fieldDefinition = fieldDefinitions.find((def) => def.name === name);
118
- visitedFields.add(fieldDefinition);
119
115
  const formatFunction = formatFunctions[fieldDefinition.fieldType];
116
+ const value = formatFunction ? formatFunction(valuesToUpdate[name]) : valuesToUpdate[name];
120
117
  return {
121
118
  modelId,
122
- value: (formatFunction ? formatFunction(valuesToUpdate[name]) : valuesToUpdate[name]) ?? fieldDefinition.defaultValue,
123
119
  updatedAt: new Date(),
124
120
  customFieldDefinitionId: fieldDefinition.id,
121
+ value: value !== undefined ? value : fieldDefinition.defaultValue,
125
122
  };
126
123
  });
127
- if (defineAllDefaults) {
128
- allFieldDefinitions.filter((def) => !visitedFields.has(def) && ![null, undefined].includes(def.defaultValue)).forEach(({ id, defaultValue }) => {
129
- values.push({
130
- modelId,
131
- value: defaultValue,
132
- updatedAt: new Date(),
133
- customFieldDefinitionId: id,
134
- });
135
- });
136
- }
137
124
  return Promise.all(values.map(async (value) => {
138
125
  const [cfv] = await models_1.CustomFieldValue.upsert(value, {
139
126
  transaction: options.transaction,
@@ -1,4 +1,7 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.customFieldsSortScope = exports.scopeName = exports.customFieldsFilterScope = void 0;
4
7
  /* eslint-disable import/prefer-default-export */
@@ -6,6 +9,7 @@ const sequelize_1 = require("sequelize");
6
9
  const sequelize_typescript_1 = require("sequelize-typescript");
7
10
  const common_types_1 = require("@autofleet/common-types");
8
11
  const helpers_1 = require("../utils/helpers");
12
+ const logger_1 = __importDefault(require("../utils/logger"));
9
13
  const { CUSTOM_FIELDS_FILTER_SCOPE } = common_types_1.customFields;
10
14
  const isDate = (input) => input instanceof Date || Object.prototype.toString.call(input) === '[object Date]';
11
15
  const castIfNeeded = (conditionValue) => {
@@ -17,7 +21,18 @@ const castIfNeeded = (conditionValue) => {
17
21
  }
18
22
  return '';
19
23
  };
24
+ const addQuotationIfNeeded = (conditionValue, valRep) => {
25
+ if (typeof conditionValue === 'string') {
26
+ return `'"' || :${valRep} || '"'`;
27
+ }
28
+ return `:${valRep}`;
29
+ };
20
30
  const AND_DELIMETER = ' AND ';
31
+ const OR_DELIMETER = ' OR ';
32
+ const CD_TABLE_ALIAS = 'cd';
33
+ const CD_NAME_COLUMN = `${CD_TABLE_ALIAS}.name`;
34
+ const CV_TABLE_ALIAS = 'cv';
35
+ const CV_VALUE_COLUMN = `(${CV_TABLE_ALIAS}.value::varchar)`;
21
36
  /**
22
37
  * A Sequelize scope for filtering models by custom fields.
23
38
  * This scope builds a WHERE clause to be applied on the main query.
@@ -35,6 +50,7 @@ const customFieldsFilterScope = (name) => ({ replacementsMap: replacements, scop
35
50
  const replacemetKey = Object.keys(replacements).find((randomString) => replacements[randomString] === key);
36
51
  if (!replacemetKey)
37
52
  return false;
53
+ const columnCondition = `(${CD_NAME_COLUMN} = :${replacemetKey})`;
38
54
  if (Array.isArray(condition)) {
39
55
  if (condition.length === 0) {
40
56
  // if empty array, the condition is ignored
@@ -43,23 +59,23 @@ const customFieldsFilterScope = (name) => ({ replacementsMap: replacements, scop
43
59
  if (typeof condition[0] === 'string') {
44
60
  const values = condition.map((v) => {
45
61
  const valRandom = Object.keys(replacements).find((randomString) => replacements[randomString] === v);
46
- return ` :${valRandom} `;
62
+ return addQuotationIfNeeded(v, valRandom);
47
63
  }).join(',');
48
- return `(custom_fields->> :${replacemetKey} ) IN ( ${values} )`;
64
+ return `(${columnCondition} AND ${CV_VALUE_COLUMN} IN ( ${values} ))`;
49
65
  }
50
66
  return condition
51
67
  .map((c) => {
52
68
  const valRep = Object.keys(replacements).find((replacementKey) => replacements[replacementKey] === c.value);
53
- return `(custom_fields->> :${replacemetKey} )${castIfNeeded(c.value)} ${c.operator} :${valRep}`;
69
+ return `( ${columnCondition} AND ${CV_VALUE_COLUMN}${castIfNeeded(c.value)} ${c.operator} ${addQuotationIfNeeded(c.value, valRep)} )`;
54
70
  }).join(AND_DELIMETER);
55
71
  }
56
72
  if (typeof condition === 'string' || typeof condition === 'number') {
57
73
  const conditionRep = Object.keys(replacements).find((replacementKey) => replacements[replacementKey] === condition);
58
- return `(custom_fields->> :${replacemetKey} ) ${castIfNeeded(condition)} = :${conditionRep}`;
74
+ return `(${columnCondition} AND ${CV_VALUE_COLUMN}${castIfNeeded(condition)} = ${addQuotationIfNeeded(condition, conditionRep)})`;
59
75
  }
60
76
  if (condition?.operator) {
61
77
  const valueRep = Object.keys(replacements).find((replacementKey) => replacements[replacementKey] === condition.value);
62
- return `(custom_fields->> :${replacemetKey} ) ${castIfNeeded(condition.value)} ${condition.operator} :${valueRep}`;
78
+ return `( ${columnCondition} AND ${CV_VALUE_COLUMN}${castIfNeeded(condition.value)} ${condition.operator} ${addQuotationIfNeeded(condition.value, valueRep)})`;
63
79
  }
64
80
  return false;
65
81
  })
@@ -67,14 +83,17 @@ const customFieldsFilterScope = (name) => ({ replacementsMap: replacements, scop
67
83
  if (conditionsStrings.length === 0) {
68
84
  return {};
69
85
  }
70
- const customFieldConditions = conditionsStrings.join(AND_DELIMETER);
71
- const subQuery = `${'SELECT model_id FROM ('
72
- + 'SELECT cv.model_id, jsonb_object_agg(cd.name, cv.value) AS custom_fields '
73
- + 'FROM custom_field_values AS cv '
74
- + 'INNER JOIN custom_field_definitions AS cd ON cv.custom_field_definition_id = cd.id '
75
- + `AND cd.model_type = '${name}'`
76
- + 'GROUP BY cv.model_id'
77
- + ') AS CustomFieldAggregation WHERE '} ${customFieldConditions}`;
86
+ const customFieldConditions = conditionsStrings.join(OR_DELIMETER);
87
+ const subQuery = `
88
+ SELECT cv.model_id
89
+ FROM custom_field_values AS cv
90
+ INNER JOIN custom_field_definitions AS cd ON cv.custom_field_definition_id = cd.id
91
+ AND cd.model_type = '${name}'
92
+ WHERE ${customFieldConditions}
93
+ GROUP BY cv.model_id
94
+ HAVING COUNT(DISTINCT cv.custom_field_definition_id) = ${conditionsStrings.length}
95
+ `.replace(/\n/g, '');
96
+ logger_1.default.info('custom fields filter scope', { subQuery, replacements });
78
97
  return {
79
98
  where: {
80
99
  id: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autofleet/sadot",
3
- "version": "0.7.7-beta-0ecad376.0",
3
+ "version": "0.7.7-beta.1",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -8,6 +8,7 @@
8
8
  "build": "rm -rf dist && tsc",
9
9
  "linter": "eslint .",
10
10
  "test": "jest --forceExit --runInBand",
11
+ "test-debug": "node --inspect-brk node_modules/.bin/jest --runInBand --testTimeout=10000000",
11
12
  "coverage": "jest --coverage --forceExit --runInBand && rm -rf ./coverage",
12
13
  "build-to-local-repo": "node --run build && cp -r dist/* ../$REPO/node_modules/$npm_package_name/dist",
13
14
  "dev": "nodemon",
@@ -1,3 +1,4 @@
1
+ import type { WhereOptions } from 'sequelize';
1
2
  import logger from '../utils/logger';
2
3
  import * as ValueRepo from '../repository/value';
3
4
  import * as DefinitionRepo from '../repository/definition';
@@ -23,37 +24,54 @@ export const beforeCreate = (scopeAttributes: string[], modelOptions: ModelOptio
23
24
  ): Promise<void> => {
24
25
  logger.debug('sadot - before create hook');
25
26
  const { fields } = options;
27
+ const { include, useEntityIdFromInclude } = modelOptions;
26
28
  const modelType = instance.constructor.name;
27
29
 
28
30
  const identifiers = applyScopeToInstance(instance, scopeAttributes);
29
31
 
30
- // get all model's required definitions
31
- const requiredFieldsNames = await DefinitionRepo.getRequiredFields(modelType, instance.id, identifiers, modelOptions);
32
+ const where: WhereOptions = {
33
+ modelType,
34
+ disabled: false,
35
+ ...(!useEntityIdFromInclude && { entityId: identifiers }),
36
+ };
37
+ const fieldDefinitions = await DefinitionRepo.findAll(where, { withDisabled: false, transaction: options.transaction, include: include?.(identifiers) });
38
+ const requiredFieldsNames = Array.from(new Set(fieldDefinitions.filter(({ required }) => required).map(({ name }) => name)));
32
39
 
33
40
  const customFieldsIdx = fields.indexOf('customFields');
34
- const { customFields } = instance;
35
- if (customFieldsIdx > -1 && customFields) {
36
- const fieldsNames = Object.keys(customFields);
37
- const missingFields = requiredFieldsNames.filter((name) => !fieldsNames.includes(name));
38
- if (missingFields?.length > 0) {
39
- throw new MissingRequiredCustomFieldError(missingFields);
40
- }
41
-
42
- await ValueRepo.updateValues(
43
- modelType,
44
- instance.id,
45
- identifiers,
46
- customFields,
47
- {
48
- transaction: options.transaction,
49
- modelOptions,
50
- },
51
- true,
52
- );
53
41
 
54
- // eslint-disable-next-line no-param-reassign
55
- fields.splice(customFieldsIdx, 1);
56
- } else if (requiredFieldsNames?.length > 0) {
42
+ if ((customFieldsIdx === -1 || !instance.customFields) && requiredFieldsNames?.length > 0) {
57
43
  throw new MissingRequiredCustomFieldError(requiredFieldsNames);
58
44
  }
45
+ const fieldsWithDefaultValue = fieldDefinitions.filter((def) => ![null, undefined].includes(def.defaultValue));
46
+ if (fieldsWithDefaultValue.length) {
47
+ // eslint-disable-next-line no-param-reassign
48
+ instance.customFields ||= {};
49
+ fieldsWithDefaultValue.filter((def) => !(def.name in instance.customFields)).forEach(({ name, defaultValue }) => {
50
+ // eslint-disable-next-line no-param-reassign
51
+ instance.customFields[name] = defaultValue;
52
+ });
53
+ }
54
+ const { customFields } = instance;
55
+ if (customFieldsIdx === -1 || !customFields) {
56
+ return;
57
+ }
58
+ const fieldsNames = Object.keys(customFields);
59
+ const missingFields = requiredFieldsNames.filter((name) => !fieldsNames.includes(name));
60
+ if (missingFields?.length > 0) {
61
+ throw new MissingRequiredCustomFieldError(missingFields);
62
+ }
63
+
64
+ await ValueRepo.updateValues(
65
+ modelType,
66
+ instance.id,
67
+ identifiers,
68
+ customFields,
69
+ {
70
+ transaction: options.transaction,
71
+ modelOptions,
72
+ },
73
+ );
74
+
75
+ // eslint-disable-next-line no-param-reassign
76
+ fields.splice(customFieldsIdx, 1);
59
77
  };
@@ -67,7 +67,6 @@ export const updateValues = async (
67
67
  identifiers: string[],
68
68
  valuesToUpdate: ValuesToUpdate,
69
69
  options: FindOptions & { modelOptions?: ModelOptions } = {},
70
- defineAllDefaults = false,
71
70
  ): Promise<CustomFieldValue[]> => {
72
71
  const names = Object.keys(valuesToUpdate);
73
72
  logger.debug(`custom-fields: updating values for ${modelType} ${modelId}`, {
@@ -80,13 +79,11 @@ export const updateValues = async (
80
79
 
81
80
  const where: WhereOptions = {
82
81
  modelType,
83
- ...(!defineAllDefaults && { name: names }),
82
+ name: names,
84
83
  ...(!options.modelOptions?.useEntityIdFromInclude && { entityId: identifiers }),
85
84
  };
86
85
 
87
- const allFieldDefinitions = await DefinitionRepo.findAll(where, { withDisabled: true, transaction, include: modelOptions.include?.(identifiers) }) || [];
88
- const namesSet = new Set(defineAllDefaults ? names : undefined);
89
- const fieldDefinitions = defineAllDefaults ? allFieldDefinitions.filter((cfd) => namesSet.has(cfd.name)) : allFieldDefinitions;
86
+ const fieldDefinitions = await DefinitionRepo.findAll(where, { withDisabled: true, transaction, include: modelOptions.include?.(identifiers) }) ?? [];
90
87
 
91
88
  const disabledDefinitions = fieldDefinitions.filter((def) => def.disabled);
92
89
  if (fieldDefinitions.length !== names.length) {
@@ -101,31 +98,18 @@ export const updateValues = async (
101
98
  logger.warn(`custom-fields: trying to update disabled values: ${valuesWithDisabledDefinitions.join(', ')}`);
102
99
  }
103
100
 
104
- const visitedFields = new Set<CustomFieldDefinition>();
105
-
106
101
  const values: CreateCustomFieldValue[] = names.map((name) => {
107
102
  const fieldDefinition = fieldDefinitions.find((def) => def.name === name);
108
- visitedFields.add(fieldDefinition);
109
103
  const formatFunction = formatFunctions[fieldDefinition.fieldType];
104
+ const value = formatFunction ? formatFunction(valuesToUpdate[name]) : valuesToUpdate[name];
110
105
  return {
111
106
  modelId,
112
- value: (formatFunction ? formatFunction(valuesToUpdate[name]) : valuesToUpdate[name]) ?? fieldDefinition.defaultValue,
113
107
  updatedAt: new Date(),
114
108
  customFieldDefinitionId: fieldDefinition.id,
109
+ value: value !== undefined ? value : fieldDefinition.defaultValue,
115
110
  };
116
111
  });
117
112
 
118
- if (defineAllDefaults) {
119
- allFieldDefinitions.filter((def) => !visitedFields.has(def) && ![null, undefined].includes(def.defaultValue)).forEach(({ id, defaultValue }) => {
120
- values.push({
121
- modelId,
122
- value: defaultValue,
123
- updatedAt: new Date(),
124
- customFieldDefinitionId: id,
125
- });
126
- });
127
- }
128
-
129
113
  return Promise.all(values.map(async (value) => {
130
114
  const [cfv] = await CustomFieldValue.upsert(value, {
131
115
  transaction: options.transaction,
@@ -3,6 +3,7 @@ import { Op, type WhereOptions } from 'sequelize';
3
3
  import { Sequelize } from 'sequelize-typescript';
4
4
  import { customFields } from '@autofleet/common-types';
5
5
  import { generateRandomString } from '../utils/helpers';
6
+ import logger from '../utils/logger';
6
7
 
7
8
  const { CUSTOM_FIELDS_FILTER_SCOPE } = customFields;
8
9
 
@@ -43,7 +44,21 @@ const castIfNeeded = (conditionValue: string): string => {
43
44
  }
44
45
  return '';
45
46
  };
47
+
48
+ const addQuotationIfNeeded = (conditionValue: string, valRep: string): string => {
49
+ if (typeof conditionValue === 'string') {
50
+ return `'"' || :${valRep} || '"'`;
51
+ }
52
+ return `:${valRep}`;
53
+ };
54
+
46
55
  const AND_DELIMETER = ' AND ';
56
+ const OR_DELIMETER = ' OR ';
57
+
58
+ const CD_TABLE_ALIAS = 'cd';
59
+ const CD_NAME_COLUMN = `${CD_TABLE_ALIAS}.name`;
60
+ const CV_TABLE_ALIAS = 'cv';
61
+ const CV_VALUE_COLUMN = `(${CV_TABLE_ALIAS}.value::varchar)`;
47
62
 
48
63
  /**
49
64
  * A Sequelize scope for filtering models by custom fields.
@@ -71,6 +86,7 @@ export const customFieldsFilterScope = (
71
86
  (randomString) => replacements[randomString] === key,
72
87
  );
73
88
  if (!replacemetKey) return false;
89
+ const columnCondition = `(${CD_NAME_COLUMN} = :${replacemetKey})`;
74
90
 
75
91
  if (Array.isArray(condition)) {
76
92
  if (condition.length === 0) {
@@ -82,29 +98,29 @@ export const customFieldsFilterScope = (
82
98
  const valRandom = Object.keys(replacements).find(
83
99
  (randomString) => replacements[randomString] === v,
84
100
  );
85
- return ` :${valRandom} `;
101
+ return addQuotationIfNeeded(v, valRandom);
86
102
  }).join(',');
87
- return `(custom_fields->> :${replacemetKey} ) IN ( ${values} )`;
103
+ return `(${columnCondition} AND ${CV_VALUE_COLUMN} IN ( ${values} ))`;
88
104
  }
89
105
  return condition
90
106
  .map((c) => {
91
107
  const valRep = Object.keys(replacements).find(
92
108
  (replacementKey) => replacements[replacementKey] === c.value,
93
109
  );
94
- return `(custom_fields->> :${replacemetKey} )${castIfNeeded(c.value)} ${c.operator} :${valRep}`;
110
+ return `( ${columnCondition} AND ${CV_VALUE_COLUMN}${castIfNeeded(c.value)} ${c.operator} ${addQuotationIfNeeded(c.value, valRep)} )`;
95
111
  }).join(AND_DELIMETER);
96
112
  }
97
113
  if (typeof condition === 'string' || typeof condition === 'number') {
98
114
  const conditionRep = Object.keys(replacements).find(
99
115
  (replacementKey) => replacements[replacementKey] === condition,
100
116
  );
101
- return `(custom_fields->> :${replacemetKey} ) ${castIfNeeded(condition)} = :${conditionRep}`;
117
+ return `(${columnCondition} AND ${CV_VALUE_COLUMN}${castIfNeeded(condition)} = ${addQuotationIfNeeded(condition, conditionRep)})`;
102
118
  }
103
119
  if (condition?.operator) {
104
120
  const valueRep = Object.keys(replacements).find(
105
121
  (replacementKey) => replacements[replacementKey] === condition.value,
106
122
  );
107
- return `(custom_fields->> :${replacemetKey} ) ${castIfNeeded(condition.value)} ${condition.operator} :${valueRep}`;
123
+ return `( ${columnCondition} AND ${CV_VALUE_COLUMN}${castIfNeeded(condition.value)} ${condition.operator} ${addQuotationIfNeeded(condition.value, valueRep)})`;
108
124
  }
109
125
  return false;
110
126
  },
@@ -113,14 +129,17 @@ export const customFieldsFilterScope = (
113
129
  if (conditionsStrings.length === 0) {
114
130
  return {};
115
131
  }
116
- const customFieldConditions = conditionsStrings.join(AND_DELIMETER);
117
- const subQuery = `${'SELECT model_id FROM ('
118
- + 'SELECT cv.model_id, jsonb_object_agg(cd.name, cv.value) AS custom_fields '
119
- + 'FROM custom_field_values AS cv '
120
- + 'INNER JOIN custom_field_definitions AS cd ON cv.custom_field_definition_id = cd.id '
121
- + `AND cd.model_type = '${name}'`
122
- + 'GROUP BY cv.model_id'
123
- + ') AS CustomFieldAggregation WHERE '} ${customFieldConditions}`;
132
+ const customFieldConditions = conditionsStrings.join(OR_DELIMETER);
133
+ const subQuery = `
134
+ SELECT cv.model_id
135
+ FROM custom_field_values AS cv
136
+ INNER JOIN custom_field_definitions AS cd ON cv.custom_field_definition_id = cd.id
137
+ AND cd.model_type = '${name}'
138
+ WHERE ${customFieldConditions}
139
+ GROUP BY cv.model_id
140
+ HAVING COUNT(DISTINCT cv.custom_field_definition_id) = ${conditionsStrings.length}
141
+ `.replace(/\n/g, '');
142
+ logger.info('custom fields filter scope', { subQuery, replacements });
124
143
  return {
125
144
  where: {
126
145
  id: {