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