@autofleet/sadot 0.10.3-beta-ab27bd53.0 → 0.10.3

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.
@@ -62,7 +62,7 @@ const beforeCreate = (scopeAttributes, modelOptions = {}, sadotOptions = { useCu
62
62
  if (fieldsWithDefaultValue.length) {
63
63
  // eslint-disable-next-line no-param-reassign
64
64
  instance.customFields || (instance.customFields = {});
65
- fieldsWithDefaultValue.filter((def) => !(def.name in instance.customFields)).forEach(({ name, defaultValue }) => {
65
+ fieldsWithDefaultValue.filter((def) => (instance.customFields?.[def.name] === undefined)).forEach(({ name, defaultValue }) => {
66
66
  // eslint-disable-next-line no-param-reassign
67
67
  instance.customFields[name] = defaultValue;
68
68
  });
@@ -1,7 +1,30 @@
1
- import type { ModelOptions } from '../types';
1
+ import type { Transaction } from 'sequelize';
2
+ import type CustomFieldValue from '../models/CustomFieldValue';
3
+ import type { CustomFieldOptions, ModelOptions } from '../types';
2
4
  type SupportedHookTypes = 'afterFind' | 'afterCreate' | 'afterUpdate';
5
+ type CustomFieldEntries = Record<string, any>;
6
+ interface GetValuesGroupByInstanceResponse {
7
+ [modelId: string]: CustomFieldValue[];
8
+ }
9
+ interface GetCustomFieldEntriesByInstanceIdResponse {
10
+ [modelId: string]: CustomFieldEntries;
11
+ }
12
+ export declare const getCustomFieldEntriesByInstanceId: ({ instancesIds, options, sadotOptions, }: {
13
+ instancesIds: string[];
14
+ options?: {
15
+ transaction: Transaction;
16
+ };
17
+ sadotOptions: Pick<CustomFieldOptions, 'useCustomFieldsEntries'>;
18
+ }) => Promise<GetCustomFieldEntriesByInstanceIdResponse>;
19
+ export declare const getValuesGroupByInstance: ({ instancesIds, options, sadotOptions, }: {
20
+ instancesIds: string[];
21
+ options?: {
22
+ transaction: Transaction;
23
+ };
24
+ sadotOptions: Pick<CustomFieldOptions, 'useCustomFieldsEntries'>;
25
+ }) => Promise<GetValuesGroupByInstanceResponse>;
3
26
  /**
4
27
  * A hook to attach the custom fields when fetching a model instances.
5
28
  */
6
- declare const enrichResults: (modelType: string, scopeAttributes: string[], hookType?: SupportedHookTypes, modelOptions?: ModelOptions) => (instancesOrInstance: any | any[], options: any) => Promise<void>;
29
+ declare const enrichResults: (modelType: string, scopeAttributes: string[], hookType?: SupportedHookTypes, modelOptions?: ModelOptions, sadotOptions?: Pick<CustomFieldOptions, 'useCustomFieldsEntries'>) => (instancesOrInstance: any | any[], options: any) => Promise<void>;
7
30
  export default enrichResults;
@@ -26,10 +26,43 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
26
26
  return (mod && mod.__esModule) ? mod : { "default": mod };
27
27
  };
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
- /* eslint-disable no-param-reassign */
29
+ exports.getValuesGroupByInstance = exports.getCustomFieldEntriesByInstanceId = void 0;
30
30
  const ValueRepo = __importStar(require("../repository/value"));
31
31
  const DefinitionRepo = __importStar(require("../repository/definition"));
32
+ const EntriesRepo = __importStar(require("../repository/entries"));
32
33
  const scopeAttributes_1 = __importDefault(require("../utils/scopeAttributes"));
34
+ const getCustomFieldEntriesByInstanceId = async ({ instancesIds, options, sadotOptions, }) => {
35
+ if (!sadotOptions.useCustomFieldsEntries) {
36
+ return {};
37
+ }
38
+ const customFieldEntries = await EntriesRepo.findEntriesByModelIds(instancesIds, options ?? {});
39
+ const customFieldEntriesByInstanceId = Object.fromEntries(customFieldEntries.map((instanceEntries) => {
40
+ const { modelId, customFields } = instanceEntries?.dataValues ?? {};
41
+ if (!modelId) {
42
+ return undefined;
43
+ }
44
+ return [modelId, customFields];
45
+ }).filter(Boolean));
46
+ instancesIds.forEach((instanceId) => {
47
+ customFieldEntriesByInstanceId[instanceId] ?? (customFieldEntriesByInstanceId[instanceId] = {});
48
+ });
49
+ return customFieldEntriesByInstanceId;
50
+ };
51
+ exports.getCustomFieldEntriesByInstanceId = getCustomFieldEntriesByInstanceId;
52
+ const getValuesGroupByInstance = async ({ instancesIds, options, sadotOptions, }) => {
53
+ if (sadotOptions.useCustomFieldsEntries) {
54
+ return {};
55
+ }
56
+ const customFieldValues = await ValueRepo.findValuesByModelIds(instancesIds, options ?? {});
57
+ // Group fields by modelId
58
+ return customFieldValues.reduce((acc, v) => {
59
+ const { modelId } = v;
60
+ acc[modelId] ?? (acc[modelId] = []);
61
+ acc[modelId].push(v);
62
+ return acc;
63
+ }, {});
64
+ };
65
+ exports.getValuesGroupByInstance = getValuesGroupByInstance;
33
66
  /**
34
67
  * Serialize custom fields value into the format of {[name] -> [fieldData]}
35
68
  */
@@ -44,7 +77,7 @@ const serializeCustomFields = (customFieldValues, customFieldDefinitionsHash) =>
44
77
  /**
45
78
  * A hook to attach the custom fields when fetching a model instances.
46
79
  */
47
- const enrichResults = (modelType, scopeAttributes, hookType, modelOptions = {}) => async (instancesOrInstance, options) => {
80
+ const enrichResults = (modelType, scopeAttributes, hookType, modelOptions = {}, sadotOptions = { useCustomFieldsEntries: false }) => async (instancesOrInstance, options) => {
48
81
  if (options.originalAttributes?.length > 0
49
82
  && !options.originalAttributes?.includes?.('customFields')) {
50
83
  return;
@@ -80,25 +113,25 @@ const enrichResults = (modelType, scopeAttributes, hookType, modelOptions = {})
80
113
  });
81
114
  // Get the values per instates ids:
82
115
  const instancesIds = instances.map((i) => i[primaryKey]);
83
- const customFieldValues = await ValueRepo.findValuesByModelIds(instancesIds, { transaction: options.transaction });
84
116
  // Group fields by modelId
85
- const valuesGroupByInstance = customFieldValues.reduce((acc, v) => {
86
- const { modelId } = v;
87
- if (!acc[modelId]) {
88
- acc[modelId] = [];
89
- }
90
- acc[modelId].push(v);
91
- return acc;
92
- }, {});
117
+ const valuesGroupByInstance = await (0, exports.getValuesGroupByInstance)({
118
+ instancesIds,
119
+ options,
120
+ sadotOptions,
121
+ });
122
+ const customFieldEntriesByInstanceId = await (0, exports.getCustomFieldEntriesByInstanceId)({
123
+ instancesIds,
124
+ options,
125
+ sadotOptions,
126
+ });
93
127
  // Attach custom fields to the instances
94
128
  instances.forEach((instance) => {
95
- const customFields = {};
96
129
  const { id } = instance;
97
130
  const instanceValues = valuesGroupByInstance[id];
98
- if (instanceValues) {
99
- const serializedCustomFields = serializeCustomFields(instanceValues, definitionsMap);
100
- Object.assign(customFields, serializedCustomFields);
101
- }
131
+ const serializedCustomFieldsValues = instanceValues ? serializeCustomFields(instanceValues, definitionsMap) : {};
132
+ const customFields = sadotOptions.useCustomFieldsEntries
133
+ ? customFieldEntriesByInstanceId[id]
134
+ : serializedCustomFieldsValues;
102
135
  scopeAttributes.forEach((attribute) => {
103
136
  const identifier = instance[attribute];
104
137
  const entityCustomFieldDefinitions = identifierCustomFieldDefinitionsMapping[identifier];
package/dist/index.js CHANGED
@@ -42,7 +42,7 @@ const useCustomFields = async (app, getModel, options) => {
42
42
  // The order is important
43
43
  (0, init_1.addHooks)(models, getModel, { useCustomFieldsEntries });
44
44
  await (0, models_1.initTables)(sequelize, options.getUser, { useCustomFieldsEntries });
45
- (0, init_1.addScopes)(models, getModel);
45
+ (0, init_1.addScopes)(models, getModel, { useCustomFieldsEntries });
46
46
  (0, init_1.applyCustomAssociation)(models);
47
47
  logger_1.default.debug('sadot - custom fields finished initializing with models', models);
48
48
  return sequelize;
@@ -31,6 +31,7 @@ const models_1 = require("../models");
31
31
  const logger_1 = __importDefault(require("../utils/logger"));
32
32
  const errors_1 = require("../errors");
33
33
  const DefinitionRepo = __importStar(require("./definition"));
34
+ const formatValues_1 = require("./utils/formatValues");
34
35
  const findEntriesByModelId = async (modelId, options = {}) => {
35
36
  const { transaction } = options;
36
37
  return models_1.CustomFieldEntries.findOne({
@@ -73,6 +74,14 @@ const updateEntries = async (modelId, modelType, customFields, identifiers, opti
73
74
  if (valuesWithDisabledDefinitions?.length > 0) {
74
75
  logger_1.default.warn(`custom-fields: trying to update disabled values: ${valuesWithDisabledDefinitions.join(', ')}`);
75
76
  }
77
+ const definitionsByName = Object.fromEntries(fieldDefinitions.map((definition) => [definition.name, definition]));
78
+ // If we need to format the value before we save it
79
+ Object.entries(customFields)
80
+ .filter(([definitionName]) => formatValues_1.formatFunctions[definitionsByName[definitionName].fieldType])
81
+ .forEach(([definitionName, value]) => {
82
+ const { fieldType } = definitionsByName[definitionName];
83
+ customFields[definitionName] = formatValues_1.formatFunctions[fieldType](value);
84
+ });
76
85
  return models_1.CustomFieldEntries.upsert({
77
86
  modelId,
78
87
  entityId: fieldDefinitions[0].entityId,
@@ -0,0 +1,3 @@
1
+ export declare const formatFunctions: {
2
+ date: (value: any) => string;
3
+ };
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatFunctions = void 0;
4
+ const constants_1 = require("../../utils/constants");
5
+ exports.formatFunctions = {
6
+ [constants_1.CustomFieldDefinitionType.DATE]: (value) => {
7
+ if (value) {
8
+ const date = new Date(value);
9
+ if (date.toString() === 'Invalid Date') {
10
+ throw new Error(`Invalid date value: ${value}`);
11
+ }
12
+ return date.toISOString();
13
+ }
14
+ return null;
15
+ },
16
+ };
@@ -31,7 +31,7 @@ const models_1 = require("../models");
31
31
  const DefinitionRepo = __importStar(require("./definition"));
32
32
  const logger_1 = __importDefault(require("../utils/logger"));
33
33
  const errors_1 = require("../errors");
34
- const constants_1 = require("../utils/constants");
34
+ const formatValues_1 = require("./utils/formatValues");
35
35
  const findByModelIdAndDefinition = async (modelId, customFieldDefinitionId) => models_1.CustomFieldValue.findAll({ where: { modelId, customFieldDefinitionId }, include: [models_1.CustomFieldDefinition] });
36
36
  exports.findByModelIdAndDefinition = findByModelIdAndDefinition;
37
37
  const create = async (data, withAssociations = false) => {
@@ -67,18 +67,6 @@ const findValuesByModelIds = async (modelIds, options) => {
67
67
  });
68
68
  };
69
69
  exports.findValuesByModelIds = findValuesByModelIds;
70
- const formatFunctions = {
71
- [constants_1.CustomFieldDefinitionType.DATE]: (value) => {
72
- if (value) {
73
- const date = new Date(value);
74
- if (date.toString() === 'Invalid Date') {
75
- throw new Error(`Invalid date value: ${value}`);
76
- }
77
- return date.toISOString();
78
- }
79
- return null;
80
- },
81
- };
82
70
  /**
83
71
  * Try to update custom field values for a model instance.
84
72
  * Create new value record if not exists, but fails if value's definition not exist.
@@ -112,7 +100,7 @@ const updateValues = async (modelType, modelId, identifiers, valuesToUpdate, opt
112
100
  }
113
101
  const values = names.map((name) => {
114
102
  const fieldDefinition = fieldDefinitions.find((def) => def.name === name);
115
- const formatFunction = formatFunctions[fieldDefinition.fieldType];
103
+ const formatFunction = formatValues_1.formatFunctions[fieldDefinition.fieldType];
116
104
  const value = formatFunction ? formatFunction(valuesToUpdate[name]) : valuesToUpdate[name];
117
105
  return {
118
106
  modelId,
@@ -1,22 +1,5 @@
1
- import { type WhereOptions } from 'sequelize';
2
- /**
3
- * Type representing possible condition values.
4
- * Currently supporting strings and arrays of strings.
5
- * More types to be added (TBA).
6
- */
7
- type ConditionWithOperator = {
8
- operator: string;
9
- value: string;
10
- };
11
- export type ConditionValue = ConditionWithOperator | ConditionWithOperator[] | string | string[];
12
- export type CustomFieldSort = {
13
- field: string;
14
- direction: 'ASC' | 'DESC';
15
- };
16
- export type CustomFieldFilterOptions = {
17
- where?: WhereOptions;
18
- replacements?: Record<string, string>;
19
- };
1
+ import type { CustomFieldOptions } from '../types';
2
+ import { type ConditionValue, type CustomFieldFilterOptions } from './helpers/filter.helpers';
20
3
  type customFieldsFilterScopeParams = {
21
4
  replacementsMap: Record<string, string>;
22
5
  scopeValue: Record<string, ConditionValue>;
@@ -28,9 +11,9 @@ type customFieldsFilterScopeParams = {
28
11
  * @param name - The model type name used to join custom_field_definitions.
29
12
  * @returns A function that takes conditions and returns the Sequelize options object.
30
13
  */
31
- export declare const customFieldsFilterScope: (name: string) => ({ replacementsMap: replacements, scopeValue: conditions }: customFieldsFilterScopeParams) => CustomFieldFilterOptions;
14
+ export declare const customFieldsFilterScope: (name: string, options?: Pick<CustomFieldOptions, 'useCustomFieldsEntries'>) => ({ replacementsMap: replacements, scopeValue: conditions }: customFieldsFilterScopeParams) => CustomFieldFilterOptions;
32
15
  export declare const scopeName = "filterByCustomFields";
33
- export declare const customFieldsSortScope: (name: string) => ({ replacementsMap, scopeValue: sort }: {
16
+ export declare const customFieldsSortScope: (name: string, options?: Pick<CustomFieldOptions, 'useCustomFieldsEntries'>) => ({ replacementsMap, scopeValue: sort }: {
34
17
  replacementsMap: any;
35
18
  scopeValue: any;
36
19
  }) => {
@@ -6,26 +6,8 @@ const sequelize_1 = require("sequelize");
6
6
  const sequelize_typescript_1 = require("sequelize-typescript");
7
7
  const common_types_1 = require("@autofleet/common-types");
8
8
  const helpers_1 = require("../utils/helpers");
9
+ const filter_helpers_1 = require("./helpers/filter.helpers");
9
10
  const { CUSTOM_FIELDS_FILTER_SCOPE } = common_types_1.customFields;
10
- const isConditionStringArray = (input) => Array.isArray(input) && typeof input[0] === 'string';
11
- const isBooleanString = (input) => ['true', 'false'].includes(input.toString());
12
- const isDate = (input) => input instanceof Date || Object.prototype.toString.call(input) === '[object Date]';
13
- const castValueToJsonb = (value, type) => `to_jsonb(${value}::${type})`;
14
- const castValueToJsonbText = (value) => castValueToJsonb(value, 'text');
15
- const castValueToJsonbBoolean = (value) => castValueToJsonb(value, 'boolean');
16
- const castValueToJsonbNumeric = (value) => castValueToJsonb(value, 'numeric');
17
- const castIfNeeded = (columnName, conditionValue) => {
18
- if (isDate(conditionValue)) {
19
- return castValueToJsonb(columnName, 'timestamp');
20
- }
21
- return columnName;
22
- };
23
- const AND_DELIMITER = ' AND ';
24
- const OR_DELIMITER = ' OR ';
25
- const CD_TABLE_ALIAS = 'cd';
26
- const CD_NAME_COLUMN = `${CD_TABLE_ALIAS}.name`;
27
- const CV_TABLE_ALIAS = 'cv';
28
- const CV_VALUE_COLUMN = `${CV_TABLE_ALIAS}.value`;
29
11
  /**
30
12
  * A Sequelize scope for filtering models by custom fields.
31
13
  * This scope builds a WHERE clause to be applied on the main query.
@@ -33,68 +15,27 @@ const CV_VALUE_COLUMN = `${CV_TABLE_ALIAS}.value`;
33
15
  * @param name - The model type name used to join custom_field_definitions.
34
16
  * @returns A function that takes conditions and returns the Sequelize options object.
35
17
  */
36
- const customFieldsFilterScope = (name) => ({ replacementsMap: replacements, scopeValue: conditions }) => {
18
+ const customFieldsFilterScope = (name, options) => ({ replacementsMap: replacements, scopeValue: conditions }) => {
37
19
  if (!conditions || Object.keys(conditions).length === 0) {
38
20
  return {};
39
21
  }
22
+ const queryType = options?.useCustomFieldsEntries ? filter_helpers_1.SubQueryType.ENTRIES : filter_helpers_1.SubQueryType.VALUES;
40
23
  const reverseReplacementsMap = new Map(Object.entries(replacements).map(([key, value]) => [value, key]));
41
24
  // Build the WHERE clause for custom field filtering
42
25
  const conditionsStrings = Object.entries(conditions).map(([key, condition]) => {
43
- const replacementKey = reverseReplacementsMap.get(key);
44
- if (!replacementKey)
45
- return false;
46
- const columnCondition = `(${CD_NAME_COLUMN} = :${replacementKey})`;
47
- if (Array.isArray(condition)) {
48
- if (condition.length === 0) {
49
- // if empty array, the condition is ignored
26
+ switch (queryType) {
27
+ case filter_helpers_1.SubQueryType.ENTRIES:
28
+ return (0, filter_helpers_1.formatConditionsForEntries)(key, condition, reverseReplacementsMap);
29
+ case filter_helpers_1.SubQueryType.VALUES:
30
+ return (0, filter_helpers_1.formatConditionsForValues)(key, condition, reverseReplacementsMap);
31
+ default:
50
32
  return false;
51
- }
52
- if (isConditionStringArray(condition)) {
53
- const values = condition.flatMap((v) => {
54
- const valRandom = reverseReplacementsMap.get(v);
55
- if (isBooleanString(v)) {
56
- return [castValueToJsonbText(`:${valRandom}`), castValueToJsonbBoolean(`:${valRandom}`)];
57
- }
58
- if (!Number.isNaN(Number(v))) {
59
- return castValueToJsonbNumeric(`:${valRandom}`);
60
- }
61
- return castValueToJsonbText(`:${valRandom}`);
62
- }).join(',');
63
- return `(${columnCondition}${AND_DELIMITER}${CV_VALUE_COLUMN} IN (${values}))`;
64
- }
65
- return condition.map((c) => {
66
- const valRep = reverseReplacementsMap.get(c.value);
67
- const valueAsJsonb = castValueToJsonbText(`:${valRep}`);
68
- return `(${columnCondition}${AND_DELIMITER}${castIfNeeded(CV_VALUE_COLUMN, c.value)} ${c.operator} ${valueAsJsonb})`;
69
- }).join(AND_DELIMITER);
70
- }
71
- if (typeof condition === 'string' || typeof condition === 'number') {
72
- const conditionRep = reverseReplacementsMap.get(condition);
73
- const valueAsJsonb = !Number.isNaN(Number(condition)) ? castValueToJsonbNumeric(`:${conditionRep}`) : castValueToJsonbText(`:${conditionRep}`);
74
- const valueAsJsonbBoolean = isBooleanString(condition) ? `${OR_DELIMITER}${CV_VALUE_COLUMN} = ${castValueToJsonbBoolean(`:${conditionRep}`)}` : '';
75
- return `(${columnCondition}${AND_DELIMITER}(${castIfNeeded(CV_VALUE_COLUMN, condition)} = ${valueAsJsonb}${valueAsJsonbBoolean}))`;
76
- }
77
- if (condition?.operator) {
78
- const valueRep = reverseReplacementsMap.get(condition.value);
79
- const valueAsJsonb = castValueToJsonbText(`:${valueRep}`);
80
- return `( ${columnCondition}${AND_DELIMITER}${castIfNeeded(CV_VALUE_COLUMN, condition.value)} ${condition.operator} ${valueAsJsonb})`;
81
33
  }
82
- return false;
83
34
  }).filter(Boolean);
84
35
  if (conditionsStrings.length === 0) {
85
36
  return {};
86
37
  }
87
- const customFieldConditions = conditionsStrings.join(OR_DELIMITER);
88
- const subQuery = `
89
- SELECT cv.model_id
90
- FROM custom_field_values AS cv
91
- INNER JOIN custom_field_definitions AS cd ON cv.custom_field_definition_id = cd.id
92
- ${AND_DELIMITER}cd.model_type = '${name}'
93
- WHERE ${customFieldConditions}
94
- ${AND_DELIMITER}cv.deleted_at IS NULL${AND_DELIMITER}cd.deleted_at IS NULL
95
- GROUP BY cv.model_id
96
- HAVING COUNT(DISTINCT cv.custom_field_definition_id) = ${conditionsStrings.length}
97
- `.replace(/\n/g, '');
38
+ const subQuery = (0, filter_helpers_1.getFilterCustomFieldsSubQuery)(queryType, name, conditionsStrings);
98
39
  return {
99
40
  where: {
100
41
  id: {
@@ -106,25 +47,17 @@ const customFieldsFilterScope = (name) => ({ replacementsMap: replacements, scop
106
47
  };
107
48
  exports.customFieldsFilterScope = customFieldsFilterScope;
108
49
  exports.scopeName = CUSTOM_FIELDS_FILTER_SCOPE;
109
- const customFieldsSortScope = (name) => ({ replacementsMap, scopeValue: sort }) => {
50
+ const customFieldsSortScope = (name, options) => ({ replacementsMap, scopeValue: sort }) => {
110
51
  if (!sort || sort.length === 0) {
111
52
  return {};
112
53
  }
54
+ const queryType = options?.useCustomFieldsEntries ? filter_helpers_1.SubQueryType.ENTRIES : filter_helpers_1.SubQueryType.VALUES;
113
55
  const randomStr = (0, helpers_1.generateRandomString)();
114
56
  const includes = Object.entries(sort).map(([key]) => {
115
- const replacemetKey = Object.keys(replacementsMap).find((randomString) => replacementsMap[randomString] === key);
57
+ const replacementKey = Object.keys(replacementsMap).find((randomString) => replacementsMap[randomString] === key);
116
58
  return ([
117
- sequelize_typescript_1.Sequelize.literal(`(
118
- SELECT value
119
- FROM (SELECT cv.model_id, cv.value
120
- FROM custom_field_values AS cv INNER JOIN custom_field_definitions AS cd
121
- ON cv.custom_field_definition_id = cd.id
122
- ${AND_DELIMITER}cd.model_type = '${name}'
123
- WHERE cv.model_id = "${name}"."id"
124
- ${AND_DELIMITER}cd.name = :${replacemetKey}
125
- ) AS CustomFieldAggregation
126
- )
127
- `), randomStr,
59
+ sequelize_typescript_1.Sequelize.literal((0, filter_helpers_1.getSortCustomFieldsSubQuery)(queryType, name, replacementKey)),
60
+ randomStr,
128
61
  ]);
129
62
  });
130
63
  const orders = Object.entries(sort).map(([, sortObject]) => {
@@ -0,0 +1,42 @@
1
+ import type { WhereOptions } from 'sequelize';
2
+ /**
3
+ * Type representing possible condition values.
4
+ * Currently supporting strings and arrays of strings.
5
+ * More types to be added (TBA).
6
+ */
7
+ export type ConditionWithOperator = {
8
+ operator: string;
9
+ value: string;
10
+ };
11
+ export type ConditionValue = ConditionWithOperator | ConditionWithOperator[] | string | string[];
12
+ export type CustomFieldSort = {
13
+ field: string;
14
+ direction: 'ASC' | 'DESC';
15
+ };
16
+ export type CustomFieldFilterOptions = {
17
+ where?: WhereOptions;
18
+ replacements?: Record<string, string>;
19
+ };
20
+ export declare enum SubQueryType {
21
+ VALUES = "values",
22
+ ENTRIES = "entries"
23
+ }
24
+ export declare const isConditionStringArray: (input: any) => input is string[];
25
+ export declare const isBooleanString: (input: string) => boolean;
26
+ export declare const isDate: (input: any) => input is Date;
27
+ export declare const castValueToJsonb: (value: string, type: string) => string;
28
+ export declare const castValueToJsonbText: (value: string) => string;
29
+ export declare const castValueToJsonbBoolean: (value: string) => string;
30
+ export declare const castValueToJsonbNumeric: (value: string) => string;
31
+ export declare const castIfNeeded: (columnName: string, conditionValue: string) => string;
32
+ export declare const AND_DELIMITER = " AND ";
33
+ export declare const OR_DELIMITER = " OR ";
34
+ export declare const CD_TABLE_ALIAS = "cd";
35
+ export declare const CD_NAME_COLUMN = "cd.name";
36
+ export declare const CV_TABLE_ALIAS = "cv";
37
+ export declare const CV_VALUE_COLUMN = "cv.value";
38
+ export declare const CE_TABLE_ALIAS = "ce";
39
+ export declare const getFilterCustomFieldsSubQuery: (queryType: SubQueryType, modelType: string, conditionsStrings: Array<string | boolean>) => string;
40
+ export declare const getSortCustomFieldsSubQuery: (queryType: SubQueryType, modelType: string, replacementKey: string) => string;
41
+ export declare const formatConditionsForValues: (key: string, condition: ConditionValue, reverseReplacementsMap: Map<string, string>) => string | false;
42
+ export declare const formatConditionsForEntries: (key: string, condition: ConditionValue, reverseReplacementsMap: Map<string, string>) => string | false;
@@ -0,0 +1,204 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatConditionsForEntries = exports.formatConditionsForValues = exports.getSortCustomFieldsSubQuery = exports.getFilterCustomFieldsSubQuery = exports.CE_TABLE_ALIAS = exports.CV_VALUE_COLUMN = exports.CV_TABLE_ALIAS = exports.CD_NAME_COLUMN = exports.CD_TABLE_ALIAS = exports.OR_DELIMITER = exports.AND_DELIMITER = exports.castIfNeeded = exports.castValueToJsonbNumeric = exports.castValueToJsonbBoolean = exports.castValueToJsonbText = exports.castValueToJsonb = exports.isDate = exports.isBooleanString = exports.isConditionStringArray = exports.SubQueryType = void 0;
4
+ var SubQueryType;
5
+ (function (SubQueryType) {
6
+ SubQueryType["VALUES"] = "values";
7
+ SubQueryType["ENTRIES"] = "entries";
8
+ })(SubQueryType || (exports.SubQueryType = SubQueryType = {}));
9
+ const isConditionStringArray = (input) => Array.isArray(input) && typeof input[0] === 'string';
10
+ exports.isConditionStringArray = isConditionStringArray;
11
+ const isBooleanString = (input) => ['true', 'false'].includes(input.toString());
12
+ exports.isBooleanString = isBooleanString;
13
+ const isDate = (input) => input instanceof Date || Object.prototype.toString.call(input) === '[object Date]';
14
+ exports.isDate = isDate;
15
+ const castValueToJsonb = (value, type) => `to_jsonb(${value}::${type})`;
16
+ exports.castValueToJsonb = castValueToJsonb;
17
+ const castValueToJsonbText = (value) => (0, exports.castValueToJsonb)(value, 'text');
18
+ exports.castValueToJsonbText = castValueToJsonbText;
19
+ const castValueToJsonbBoolean = (value) => (0, exports.castValueToJsonb)(value, 'boolean');
20
+ exports.castValueToJsonbBoolean = castValueToJsonbBoolean;
21
+ const castValueToJsonbNumeric = (value) => (0, exports.castValueToJsonb)(value, 'numeric');
22
+ exports.castValueToJsonbNumeric = castValueToJsonbNumeric;
23
+ const castIfNeeded = (columnName, conditionValue) => {
24
+ if ((0, exports.isDate)(conditionValue)) {
25
+ return (0, exports.castValueToJsonb)(columnName, 'timestamp');
26
+ }
27
+ return columnName;
28
+ };
29
+ exports.castIfNeeded = castIfNeeded;
30
+ exports.AND_DELIMITER = ' AND ';
31
+ exports.OR_DELIMITER = ' OR ';
32
+ exports.CD_TABLE_ALIAS = 'cd';
33
+ exports.CD_NAME_COLUMN = `${exports.CD_TABLE_ALIAS}.name`;
34
+ exports.CV_TABLE_ALIAS = 'cv';
35
+ exports.CV_VALUE_COLUMN = `${exports.CV_TABLE_ALIAS}.value`;
36
+ exports.CE_TABLE_ALIAS = 'ce';
37
+ const getSingleConditionWithOperator = (value, operator, replacementKey, reverseReplacementsMap) => {
38
+ let type = 'text';
39
+ if ((0, exports.isDate)(value)) {
40
+ type = 'date';
41
+ }
42
+ else if (!Number.isNaN(Number(value))) {
43
+ type = 'numeric';
44
+ }
45
+ else if ((0, exports.isBooleanString)(value)) {
46
+ type = 'boolean';
47
+ }
48
+ const replacedValue = reverseReplacementsMap.get(value);
49
+ return `(jsonb_extract_path_text(${exports.CE_TABLE_ALIAS}.custom_fields, :${replacementKey})::${type}) ${operator} :${replacedValue}`;
50
+ };
51
+ const getFormattedValue = (value) => {
52
+ let formattedValue = value;
53
+ if ((0, exports.isBooleanString)(value)) {
54
+ formattedValue = value === 'true';
55
+ }
56
+ else if (!Number.isNaN(Number(value))) {
57
+ formattedValue = Number(value);
58
+ }
59
+ return formattedValue;
60
+ };
61
+ const getJSONSubQuery = (value, key) => {
62
+ const formattedValue = getFormattedValue(value);
63
+ const jsonQuery = JSON.stringify({ [key]: formattedValue });
64
+ let jsonQueryWithStringBoolean;
65
+ if ((0, exports.isBooleanString)(value)) {
66
+ jsonQueryWithStringBoolean = `${exports.CE_TABLE_ALIAS}.custom_fields @> '${JSON.stringify({ [key]: value })}'`;
67
+ }
68
+ return `
69
+ (
70
+ ${jsonQueryWithStringBoolean ? `${jsonQueryWithStringBoolean} OR` : ''}
71
+ ${exports.CE_TABLE_ALIAS}.custom_fields @> '${jsonQuery}'
72
+ )
73
+ `;
74
+ };
75
+ const getFilterCustomFieldsSubQuery = (queryType, modelType, conditionsStrings) => {
76
+ switch (queryType) {
77
+ case SubQueryType.VALUES:
78
+ return `
79
+ SELECT ${exports.CV_TABLE_ALIAS}.model_id
80
+ FROM custom_field_values AS ${exports.CV_TABLE_ALIAS}
81
+ INNER JOIN custom_field_definitions AS ${exports.CD_TABLE_ALIAS} ON ${exports.CV_TABLE_ALIAS}.custom_field_definition_id = ${exports.CD_TABLE_ALIAS}.id
82
+ ${exports.AND_DELIMITER}${exports.CD_TABLE_ALIAS}.model_type = '${modelType}'
83
+ WHERE ${conditionsStrings.join(exports.OR_DELIMITER)}
84
+ ${exports.AND_DELIMITER}${exports.CV_TABLE_ALIAS}.deleted_at IS NULL${exports.AND_DELIMITER}${exports.CD_TABLE_ALIAS}.deleted_at IS NULL
85
+ GROUP BY ${exports.CV_TABLE_ALIAS}.model_id
86
+ HAVING COUNT(DISTINCT ${exports.CV_TABLE_ALIAS}.custom_field_definition_id) = ${conditionsStrings.length}
87
+ `.replace(/\n/g, '');
88
+ case SubQueryType.ENTRIES:
89
+ return `
90
+ SELECT ce.model_id
91
+ FROM custom_field_entries ce
92
+ JOIN custom_field_definitions cfd
93
+ ON ce.model_type = cfd.model_type
94
+ AND ce.entity_id = cfd.entity_id
95
+ WHERE
96
+ cfd.deleted_at IS NULL AND
97
+ ${conditionsStrings.join(exports.AND_DELIMITER)}
98
+ `;
99
+ default:
100
+ throw new Error('Invalid query type');
101
+ }
102
+ };
103
+ exports.getFilterCustomFieldsSubQuery = getFilterCustomFieldsSubQuery;
104
+ const getSortCustomFieldsSubQuery = (queryType, modelType, replacementKey) => {
105
+ switch (queryType) {
106
+ case SubQueryType.VALUES:
107
+ return `(
108
+ SELECT value
109
+ FROM (SELECT cv.model_id, cv.value
110
+ FROM custom_field_values AS cv INNER JOIN custom_field_definitions AS cd
111
+ ON cv.custom_field_definition_id = cd.id
112
+ ${exports.AND_DELIMITER}cd.model_type = '${modelType}'
113
+ WHERE cv.model_id = "${modelType}"."id"
114
+ ${exports.AND_DELIMITER}cd.name = :${replacementKey}
115
+ ) AS CustomFieldAggregation
116
+ )
117
+ `;
118
+ case SubQueryType.ENTRIES:
119
+ return `(
120
+ SELECT
121
+ customFields.value
122
+ FROM
123
+ custom_field_entries AS ${exports.CE_TABLE_ALIAS},
124
+ jsonb_each_text(custom_fields) AS customFields
125
+ WHERE
126
+ customFields.key = :${replacementKey}${exports.AND_DELIMITER}
127
+ ${exports.CE_TABLE_ALIAS}.model_type = '${modelType}'${exports.AND_DELIMITER}
128
+ ${exports.CE_TABLE_ALIAS}.model_id = "${modelType}"."id"
129
+ )
130
+ `;
131
+ default:
132
+ throw new Error('Invalid query type');
133
+ }
134
+ };
135
+ exports.getSortCustomFieldsSubQuery = getSortCustomFieldsSubQuery;
136
+ const formatConditionsForValues = (key, condition, reverseReplacementsMap) => {
137
+ const replacementKey = reverseReplacementsMap.get(key);
138
+ if (!replacementKey) {
139
+ return false;
140
+ }
141
+ const columnCondition = `(${exports.CD_NAME_COLUMN} = :${replacementKey})`;
142
+ if (Array.isArray(condition)) {
143
+ if (condition.length === 0) {
144
+ // if empty array, the condition is ignored
145
+ return false;
146
+ }
147
+ if ((0, exports.isConditionStringArray)(condition)) {
148
+ const values = condition.flatMap((v) => {
149
+ const valRandom = reverseReplacementsMap.get(v);
150
+ if ((0, exports.isBooleanString)(v)) {
151
+ return [(0, exports.castValueToJsonbText)(`:${valRandom}`), (0, exports.castValueToJsonbBoolean)(`:${valRandom}`)];
152
+ }
153
+ if (!Number.isNaN(Number(v))) {
154
+ return (0, exports.castValueToJsonbNumeric)(`:${valRandom}`);
155
+ }
156
+ return (0, exports.castValueToJsonbText)(`:${valRandom}`);
157
+ }).join(',');
158
+ return `(${columnCondition}${exports.AND_DELIMITER}${exports.CV_VALUE_COLUMN} IN (${values}))`;
159
+ }
160
+ return condition.map((c) => {
161
+ const valRep = reverseReplacementsMap.get(c.value);
162
+ const valueAsJsonb = (0, exports.castValueToJsonbText)(`:${valRep}`);
163
+ return `(${columnCondition}${exports.AND_DELIMITER}${(0, exports.castIfNeeded)(exports.CV_VALUE_COLUMN, c.value)} ${c.operator} ${valueAsJsonb})`;
164
+ }).join(exports.AND_DELIMITER);
165
+ }
166
+ if (typeof condition === 'string' || typeof condition === 'number') {
167
+ const conditionRep = reverseReplacementsMap.get(condition);
168
+ const valueAsJsonb = !Number.isNaN(Number(condition)) ? (0, exports.castValueToJsonbNumeric)(`:${conditionRep}`) : (0, exports.castValueToJsonbText)(`:${conditionRep}`);
169
+ const valueAsJsonbBoolean = (0, exports.isBooleanString)(condition) ? `${exports.OR_DELIMITER}${exports.CV_VALUE_COLUMN} = ${(0, exports.castValueToJsonbBoolean)(`:${conditionRep}`)}` : '';
170
+ return `(${columnCondition}${exports.AND_DELIMITER}(${(0, exports.castIfNeeded)(exports.CV_VALUE_COLUMN, condition)} = ${valueAsJsonb}${valueAsJsonbBoolean}))`;
171
+ }
172
+ if (condition?.operator) {
173
+ const valueRep = reverseReplacementsMap.get(condition.value);
174
+ const valueAsJsonb = (0, exports.castValueToJsonbText)(`:${valueRep}`);
175
+ return `( ${columnCondition}${exports.AND_DELIMITER}${(0, exports.castIfNeeded)(exports.CV_VALUE_COLUMN, condition.value)} ${condition.operator} ${valueAsJsonb})`;
176
+ }
177
+ return false;
178
+ };
179
+ exports.formatConditionsForValues = formatConditionsForValues;
180
+ const formatConditionsForEntries = (key, condition, reverseReplacementsMap) => {
181
+ const replacementKey = reverseReplacementsMap.get(key);
182
+ if (!replacementKey) {
183
+ return false;
184
+ }
185
+ if (Array.isArray(condition)) {
186
+ if (condition.length === 0) {
187
+ // if empty array, the condition is ignored
188
+ return false;
189
+ }
190
+ if ((0, exports.isConditionStringArray)(condition)) {
191
+ const values = condition.map((value) => getJSONSubQuery(value, key)).join(`${exports.OR_DELIMITER}\n`);
192
+ return `( ${values})`;
193
+ }
194
+ return condition.map((c) => getSingleConditionWithOperator(c.value, c.operator, replacementKey, reverseReplacementsMap)).join(exports.AND_DELIMITER);
195
+ }
196
+ if (typeof condition === 'string' || typeof condition === 'number') {
197
+ return getJSONSubQuery(condition, key);
198
+ }
199
+ if (condition?.operator) {
200
+ return getSingleConditionWithOperator(condition.value, condition.operator, replacementKey, reverseReplacementsMap);
201
+ }
202
+ return false;
203
+ };
204
+ exports.formatConditionsForEntries = formatConditionsForEntries;