@autofleet/sadot 0.6.11 → 0.7.0-beta.2

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.
@@ -73,11 +73,8 @@ exports.findValuesByModelIds = findValuesByModelIds;
73
73
  */
74
74
  const updateValues = async (modelType, modelId, identifiers, valuesToUpdate, options = {}) => {
75
75
  const names = Object.keys(valuesToUpdate);
76
- logger_1.default.debug(`custom-fields: updating values for ${modelType} ${modelId}`, {
77
- names,
78
- optionsKeys: options ? Object.keys(options) : null,
79
- valuesToUpdate,
80
- identifiers,
76
+ logger_1.default.info(`custom-fields: updating values for ${modelType} ${modelId}`, {
77
+ names, options, valuesToUpdate, identifiers,
81
78
  });
82
79
  const { modelOptions, transaction } = options;
83
80
  const where = {
@@ -90,7 +87,7 @@ const updateValues = async (modelType, modelId, identifiers, valuesToUpdate, opt
90
87
  const fieldDefinitions = await DefinitionRepo.findAll(where, { withDisabled: true, transaction, include: modelOptions.include?.(identifiers) }) || [];
91
88
  const disabledDefinitions = fieldDefinitions.filter((def) => def.disabled);
92
89
  if (fieldDefinitions.length !== names.length) {
93
- logger_1.default.warn(`custom-fields: missing definitions for ${modelType} ${modelId}`, { names, fieldDefinitions });
90
+ logger_1.default.info(`custom-fields: missing definitions for ${modelType} ${modelId}`, { names, fieldDefinitions });
94
91
  const missingDefinitions = names.filter((name) => !fieldDefinitions.some((def) => def.name === name));
95
92
  throw new errors_1.MissingDefinitionError(missingDefinitions);
96
93
  }
@@ -24,9 +24,15 @@ export type CustomFieldFilterOptions = {
24
24
  * @param name - The model type name used to join custom_field_definitions.
25
25
  * @returns A function that takes conditions and returns the Sequelize options object.
26
26
  */
27
- export declare const customFieldsFilterScope: (name: string) => (conditions: Record<string, ConditionValue>) => CustomFieldFilterOptions;
27
+ export declare const customFieldsFilterScope: (name: string) => ({ replacementsMap: replacements, scopeValue: conditions, }: {
28
+ replacementsMap: Record<string, string>;
29
+ scopeValue: Record<string, ConditionValue>;
30
+ }) => CustomFieldFilterOptions;
28
31
  export declare const scopeName = "filterByCustomFields";
29
- export declare const customFieldsSortScope: (name: string) => (sort: CustomFieldSort[]) => {
32
+ export declare const customFieldsSortScope: (name: string) => ({ replacementsMap, scopeValue: sort }: {
33
+ replacementsMap: Record<string, string>;
34
+ scopeValue: string[];
35
+ }) => {
30
36
  attributes?: undefined;
31
37
  order?: undefined;
32
38
  replacements?: undefined;
@@ -21,12 +21,36 @@ const castIfNeeded = (conditionValue) => {
21
21
  return '';
22
22
  };
23
23
  const AND_DELIMETER = ' AND ';
24
- const OR_DELIMETER = ' 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
- const castValueToJsonb = (value) => `to_jsonb(${value}::text)`;
24
+ /**
25
+ * Helper function to build condition strings for the WHERE clause.
26
+ */
27
+ // eslint-disable-next-line max-len
28
+ const buildConditionString = (key, condition, replacements) => {
29
+ const replacementKey = Object.keys(replacements).find((randomString) => replacements[randomString] === key);
30
+ if (!replacementKey)
31
+ return false;
32
+ if (Array.isArray(condition)) {
33
+ if (condition.length === 0)
34
+ return false;
35
+ if (typeof condition[0] === 'string') {
36
+ const values = condition.map((v) => `:${replacements[v]}`).join(',');
37
+ return `(custom_fields->> :${replacementKey}) IN (${values})`;
38
+ }
39
+ return condition.map((c) => {
40
+ const valRep = replacements[c.value];
41
+ return `(custom_fields->> :${replacementKey})${castIfNeeded(c.value)} ${c.operator} :${valRep}`;
42
+ }).join(AND_DELIMETER);
43
+ }
44
+ if (typeof condition === 'string') {
45
+ const conditionRep = replacements[condition];
46
+ return `(custom_fields->> :${replacementKey}) ${castIfNeeded(condition)} = :${conditionRep}`;
47
+ }
48
+ if (condition?.operator) {
49
+ const valueRep = replacements[condition.value];
50
+ return `(custom_fields->> :${replacementKey}) ${castIfNeeded(condition.value)} ${condition.operator} :${valueRep}`;
51
+ }
52
+ return false;
53
+ };
30
54
  /**
31
55
  * A Sequelize scope for filtering models by custom fields.
32
56
  * This scope builds a WHERE clause to be applied on the main query.
@@ -34,68 +58,25 @@ const castValueToJsonb = (value) => `to_jsonb(${value}::text)`;
34
58
  * @param name - The model type name used to join custom_field_definitions.
35
59
  * @returns A function that takes conditions and returns the Sequelize options object.
36
60
  */
37
- const customFieldsFilterScope = (name) => (conditions) => {
61
+ const customFieldsFilterScope = (name) => ({ replacementsMap: replacements, scopeValue: conditions, }) => {
38
62
  if (!conditions || Object.keys(conditions).length === 0) {
39
63
  return {};
40
64
  }
41
- const ConditionNameRandomStr = (0, helpers_1.generateRandomString)();
42
- const replacements = {};
43
- replacements[ConditionNameRandomStr] = `${name}`;
44
65
  // Build the WHERE clause for custom field filtering
45
66
  const conditionsStrings = Object.entries(conditions)
46
- .map(([key, condition]) => {
47
- const replacemetKey = (0, helpers_1.generateRandomString)();
48
- replacements[replacemetKey] = `${key}`;
49
- const columnCondition = `(${CD_NAME_COLUMN} = :${replacemetKey})`;
50
- if (Array.isArray(condition)) {
51
- if (condition.length === 0) {
52
- // if empty array, the condition is ignored
53
- return false;
54
- }
55
- if (typeof condition[0] === 'string') {
56
- const values = condition.map((v) => {
57
- const valRandom = (0, helpers_1.generateRandomString)();
58
- replacements[`${valRandom}`] = `${v}`;
59
- return castValueToJsonb(`:${valRandom}`);
60
- }).join(',');
61
- return `(${columnCondition} AND ${CV_VALUE_COLUMN} IN ( ${values} ))`;
62
- }
63
- return condition
64
- .map((c) => {
65
- const valRep = (0, helpers_1.generateRandomString)();
66
- replacements[valRep] = `${c.value}`;
67
- const valueAsJsonb = castValueToJsonb(`:${valRep}`);
68
- return `(${columnCondition} AND ${CV_VALUE_COLUMN}${castIfNeeded(c.value)} ${c.operator} ${valueAsJsonb})`;
69
- }).join(AND_DELIMETER);
70
- }
71
- if (typeof condition === 'string' || typeof condition === 'number') {
72
- const conditionRep = (0, helpers_1.generateRandomString)();
73
- replacements[conditionRep] = `${condition}`;
74
- const valueAsJsonb = castValueToJsonb(`:${conditionRep}`);
75
- return `(${columnCondition} AND ${CV_VALUE_COLUMN}${castIfNeeded(condition)} = ${valueAsJsonb})`;
76
- }
77
- if (condition?.operator) {
78
- const valueRep = (0, helpers_1.generateRandomString)();
79
- replacements[valueRep] = `${condition.value}`;
80
- const valueAsJsonb = castValueToJsonb(`:${valueRep}`);
81
- return `( ${columnCondition} AND ${CV_VALUE_COLUMN}${castIfNeeded(condition.value)} ${condition.operator} ${valueAsJsonb})`;
82
- }
83
- return false;
84
- })
67
+ .map(([key, condition]) => buildConditionString(key, condition, replacements))
85
68
  .filter(Boolean);
86
69
  if (conditionsStrings.length === 0) {
87
- return {};
70
+ return { replacements };
88
71
  }
89
- const customFieldConditions = conditionsStrings.join(OR_DELIMETER);
90
- const subQuery = `
91
- SELECT cv.model_id
92
- FROM custom_field_values AS cv
93
- INNER JOIN custom_field_definitions AS cd ON cv.custom_field_definition_id = cd.id
94
- AND cd.model_type = :${ConditionNameRandomStr}
95
- WHERE ${customFieldConditions}
96
- GROUP BY cv.model_id
97
- HAVING COUNT(DISTINCT cv.custom_field_definition_id) = ${conditionsStrings.length}
98
- `.replace(/\n/g, '');
72
+ const customFieldConditions = conditionsStrings.join(AND_DELIMETER);
73
+ const subQuery = `${'SELECT model_id FROM ('
74
+ + 'SELECT cv.model_id, jsonb_object_agg(cd.name, cv.value) AS custom_fields '
75
+ + 'FROM custom_field_values AS cv '
76
+ + 'INNER JOIN custom_field_definitions AS cd ON cv.custom_field_definition_id = cd.id '
77
+ + `AND cd.model_type = '${name}'`
78
+ + 'GROUP BY cv.model_id'
79
+ + ') AS CustomFieldAggregation WHERE '} ${customFieldConditions}`;
99
80
  return {
100
81
  where: {
101
82
  id: {
@@ -107,28 +88,27 @@ const customFieldsFilterScope = (name) => (conditions) => {
107
88
  };
108
89
  exports.customFieldsFilterScope = customFieldsFilterScope;
109
90
  exports.scopeName = CUSTOM_FIELDS_FILTER_SCOPE;
110
- const customFieldsSortScope = (name) => (sort) => {
91
+ const customFieldsSortScope = (name) => ({ replacementsMap, scopeValue: sort }) => {
111
92
  if (!sort || sort.length === 0) {
112
93
  return {};
113
94
  }
114
95
  const randomStr = (0, helpers_1.generateRandomString)();
115
- const replacements = {};
116
96
  const includes = Object.entries(sort).map(([key]) => {
117
- const keyRandomReplacement = (0, helpers_1.generateRandomString)();
118
- replacements[keyRandomReplacement] = `${key}`;
119
- return ([
97
+ const replacementKey = Object.keys(replacementsMap)
98
+ .find((randomString) => replacementsMap[randomString] === key);
99
+ return [
120
100
  sequelize_typescript_1.Sequelize.literal(`(
121
- SELECT value
122
- FROM (SELECT cv.model_id, cv.value
123
- FROM custom_field_values AS cv INNER JOIN custom_field_definitions AS cd
124
- ON cv.custom_field_definition_id = cd.id
125
- AND cd.model_type = '${name}'
126
- WHERE cv.model_id = "${name}"."id"
127
- AND cd.name = :${keyRandomReplacement}
128
- ) AS CustomFieldAggregation
129
- )
130
- `), randomStr,
131
- ]);
101
+ SELECT value
102
+ FROM (
103
+ SELECT cv.model_id, cv.value
104
+ FROM custom_field_values AS cv
105
+ INNER JOIN custom_field_definitions AS cd ON cv.custom_field_definition_id = cd.id
106
+ AND cd.model_type = '${name}'
107
+ WHERE cv.model_id = "${name}"."id" AND cd.name = :${replacementKey}
108
+ ) AS CustomFieldAggregation
109
+ )`),
110
+ randomStr,
111
+ ];
132
112
  });
133
113
  const orders = Object.entries(sort).map(([, value]) => sequelize_typescript_1.Sequelize.literal(`"${randomStr}" ${value}`));
134
114
  return {
@@ -136,7 +116,7 @@ const customFieldsSortScope = (name) => (sort) => {
136
116
  include: includes,
137
117
  },
138
118
  order: orders,
139
- replacements,
119
+ replacements: replacementsMap,
140
120
  };
141
121
  };
142
122
  exports.customFieldsSortScope = customFieldsSortScope;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autofleet/sadot",
3
- "version": "0.6.11",
3
+ "version": "0.7.0-beta.2",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -59,8 +59,8 @@
59
59
  "typescript": "^5.3.3",
60
60
  "typescript-eslint": "^0.0.1-alpha.0"
61
61
  },
62
- "engines": {
63
- "node": ">=16.0.0"
62
+ "peerDependencies": {
63
+ "@autofleet/sheilta": ">=1.4.0-beta.2"
64
64
  },
65
65
  "author": "Autofleet",
66
66
  "license": "ISC"
@@ -55,11 +55,8 @@ export const updateValues = async (
55
55
  options: FindOptions & { modelOptions?: ModelOptions } = {},
56
56
  ): Promise<CustomFieldValue[]> => {
57
57
  const names = Object.keys(valuesToUpdate);
58
- logger.debug(`custom-fields: updating values for ${modelType} ${modelId}`, {
59
- names,
60
- optionsKeys: options ? Object.keys(options) : null,
61
- valuesToUpdate,
62
- identifiers,
58
+ logger.info(`custom-fields: updating values for ${modelType} ${modelId}`, {
59
+ names, options, valuesToUpdate, identifiers,
63
60
  });
64
61
  const { modelOptions, transaction } = options;
65
62
 
@@ -75,7 +72,7 @@ export const updateValues = async (
75
72
 
76
73
  const disabledDefinitions = fieldDefinitions.filter((def) => def.disabled);
77
74
  if (fieldDefinitions.length !== names.length) {
78
- logger.warn(`custom-fields: missing definitions for ${modelType} ${modelId}`, { names, fieldDefinitions });
75
+ logger.info(`custom-fields: missing definitions for ${modelType} ${modelId}`, { names, fieldDefinitions });
79
76
  const missingDefinitions = names.filter((name) => !fieldDefinitions.some((def) => def.name === name));
80
77
  throw new MissingDefinitionError(missingDefinitions);
81
78
  }
@@ -31,19 +31,48 @@ export type CustomFieldFilterOptions = {
31
31
  const castIfNeeded = (conditionValue: string): string => {
32
32
  if (moment.isDate(conditionValue)) {
33
33
  return '::timestamp';
34
- } if (!Number.isNaN(Number(conditionValue))) {
34
+ }
35
+ if (!Number.isNaN(Number(conditionValue))) {
35
36
  return '::numeric';
36
37
  }
37
38
  return '';
38
39
  };
39
40
  const AND_DELIMETER = ' AND ';
40
- const OR_DELIMETER = ' OR ';
41
41
 
42
- const CD_TABLE_ALIAS = 'cd';
43
- const CD_NAME_COLUMN = `${CD_TABLE_ALIAS}.name`;
44
- const CV_TABLE_ALIAS = 'cv';
45
- const CV_VALUE_COLUMN = `(${CV_TABLE_ALIAS}.value)`;
46
- const castValueToJsonb = (value) => `to_jsonb(${value}::text)`;
42
+ /**
43
+ * Helper function to build condition strings for the WHERE clause.
44
+ */
45
+ // eslint-disable-next-line max-len
46
+ const buildConditionString = (key: string, condition: ConditionValue | ConditionValue[], replacements: Record<string, string>): string | false => {
47
+ const replacementKey = Object.keys(replacements).find((randomString) => replacements[randomString] === key);
48
+ if (!replacementKey) return false;
49
+
50
+ if (Array.isArray(condition)) {
51
+ if (condition.length === 0) return false;
52
+
53
+ if (typeof condition[0] === 'string') {
54
+ const values = condition.map((v) => `:${replacements[v]}`).join(',');
55
+ return `(custom_fields->> :${replacementKey}) IN (${values})`;
56
+ }
57
+
58
+ return condition.map((c) => {
59
+ const valRep = replacements[c.value];
60
+ return `(custom_fields->> :${replacementKey})${castIfNeeded(c.value)} ${c.operator} :${valRep}`;
61
+ }).join(AND_DELIMETER);
62
+ }
63
+
64
+ if (typeof condition === 'string') {
65
+ const conditionRep = replacements[condition];
66
+ return `(custom_fields->> :${replacementKey}) ${castIfNeeded(condition)} = :${conditionRep}`;
67
+ }
68
+
69
+ if (condition?.operator) {
70
+ const valueRep = replacements[condition.value];
71
+ return `(custom_fields->> :${replacementKey}) ${castIfNeeded(condition.value)} ${condition.operator} :${valueRep}`;
72
+ }
73
+
74
+ return false;
75
+ };
47
76
 
48
77
  /**
49
78
  * A Sequelize scope for filtering models by custom fields.
@@ -54,72 +83,33 @@ const castValueToJsonb = (value) => `to_jsonb(${value}::text)`;
54
83
  */
55
84
  export const customFieldsFilterScope = (
56
85
  name: string,
57
- ) => (conditions: Record<string, ConditionValue>): CustomFieldFilterOptions => {
86
+ ) => (
87
+ {
88
+ replacementsMap: replacements,
89
+ scopeValue: conditions,
90
+ }: { replacementsMap: Record<string, string>, scopeValue: Record<string, ConditionValue> },
91
+ ): CustomFieldFilterOptions => {
58
92
  if (!conditions || Object.keys(conditions).length === 0) {
59
93
  return {};
60
94
  }
61
- const ConditionNameRandomStr = generateRandomString();
62
- const replacements: Record<string, string> = {};
63
- replacements[ConditionNameRandomStr] = `${name}`;
64
95
 
65
96
  // Build the WHERE clause for custom field filtering
66
97
  const conditionsStrings = Object.entries(conditions)
67
- .map(
68
- ([key, condition]) => {
69
- const replacemetKey = generateRandomString();
70
- replacements[replacemetKey] = `${key}`;
71
- const columnCondition = `(${CD_NAME_COLUMN} = :${replacemetKey})`;
72
- if (Array.isArray(condition)) {
73
- if (condition.length === 0) {
74
- // if empty array, the condition is ignored
75
- return false;
76
- }
77
- if (typeof condition[0] === 'string') {
78
- const values = condition.map((v) => {
79
- const valRandom = generateRandomString();
80
- replacements[`${valRandom}`] = `${v}`;
81
- return castValueToJsonb(`:${valRandom}`);
82
- }).join(',');
83
- return `(${columnCondition} AND ${CV_VALUE_COLUMN} IN ( ${values} ))`;
84
- }
85
- return condition
86
- .map((c) => {
87
- const valRep = generateRandomString();
88
- replacements[valRep] = `${c.value}`;
89
- const valueAsJsonb = castValueToJsonb(`:${valRep}`);
90
- return `(${columnCondition} AND ${CV_VALUE_COLUMN}${castIfNeeded(c.value)} ${c.operator} ${valueAsJsonb})`;
91
- }).join(AND_DELIMETER);
92
- }
93
- if (typeof condition === 'string' || typeof condition === 'number') {
94
- const conditionRep = generateRandomString();
95
- replacements[conditionRep] = `${condition}`;
96
- const valueAsJsonb = castValueToJsonb(`:${conditionRep}`);
97
- return `(${columnCondition} AND ${CV_VALUE_COLUMN}${castIfNeeded(condition)} = ${valueAsJsonb})`;
98
- }
99
- if (condition?.operator) {
100
- const valueRep = generateRandomString();
101
- replacements[valueRep] = `${condition.value}`;
102
- const valueAsJsonb = castValueToJsonb(`:${valueRep}`);
103
- return `( ${columnCondition} AND ${CV_VALUE_COLUMN}${castIfNeeded(condition.value)} ${condition.operator} ${valueAsJsonb})`;
104
- }
105
-
106
- return false;
107
- },
108
- )
109
- .filter(Boolean);
98
+ .map(([key, condition]) => buildConditionString(key, condition, replacements))
99
+ .filter(Boolean) as string[];
100
+
110
101
  if (conditionsStrings.length === 0) {
111
- return {};
102
+ return { replacements };
112
103
  }
113
- const customFieldConditions = conditionsStrings.join(OR_DELIMETER);
114
- const subQuery = `
115
- SELECT cv.model_id
116
- FROM custom_field_values AS cv
117
- INNER JOIN custom_field_definitions AS cd ON cv.custom_field_definition_id = cd.id
118
- AND cd.model_type = :${ConditionNameRandomStr}
119
- WHERE ${customFieldConditions}
120
- GROUP BY cv.model_id
121
- HAVING COUNT(DISTINCT cv.custom_field_definition_id) = ${conditionsStrings.length}
122
- `.replace(/\n/g, '');
104
+
105
+ const customFieldConditions = conditionsStrings.join(AND_DELIMETER);
106
+ const subQuery = `${'SELECT model_id FROM ('
107
+ + 'SELECT cv.model_id, jsonb_object_agg(cd.name, cv.value) AS custom_fields '
108
+ + 'FROM custom_field_values AS cv '
109
+ + 'INNER JOIN custom_field_definitions AS cd ON cv.custom_field_definition_id = cd.id '
110
+ + `AND cd.model_type = '${name}'`
111
+ + 'GROUP BY cv.model_id'
112
+ + ') AS CustomFieldAggregation WHERE '} ${customFieldConditions}`;
123
113
  return {
124
114
  where: {
125
115
  id: {
@@ -134,28 +124,28 @@ export const scopeName = CUSTOM_FIELDS_FILTER_SCOPE;
134
124
 
135
125
  export const customFieldsSortScope = (
136
126
  name: string,
137
- ) => (sort: CustomFieldSort[]) => {
127
+ ) => ({ replacementsMap, scopeValue: sort }: { replacementsMap: Record<string, string>, scopeValue: string[] }) => {
138
128
  if (!sort || sort.length === 0) {
139
129
  return {};
140
130
  }
131
+
141
132
  const randomStr = generateRandomString();
142
- const replacements: Record<string, string> = {};
143
133
  const includes = Object.entries(sort).map(([key]) => {
144
- const keyRandomReplacement = generateRandomString();
145
- replacements[keyRandomReplacement] = `${key}`;
146
- return ([
134
+ const replacementKey = Object.keys(replacementsMap)
135
+ .find((randomString) => replacementsMap[randomString] === key);
136
+ return [
147
137
  Sequelize.literal(`(
148
- SELECT value
149
- FROM (SELECT cv.model_id, cv.value
150
- FROM custom_field_values AS cv INNER JOIN custom_field_definitions AS cd
151
- ON cv.custom_field_definition_id = cd.id
152
- AND cd.model_type = '${name}'
153
- WHERE cv.model_id = "${name}"."id"
154
- AND cd.name = :${keyRandomReplacement}
155
- ) AS CustomFieldAggregation
156
- )
157
- `), randomStr,
158
- ]);
138
+ SELECT value
139
+ FROM (
140
+ SELECT cv.model_id, cv.value
141
+ FROM custom_field_values AS cv
142
+ INNER JOIN custom_field_definitions AS cd ON cv.custom_field_definition_id = cd.id
143
+ AND cd.model_type = '${name}'
144
+ WHERE cv.model_id = "${name}"."id" AND cd.name = :${replacementKey}
145
+ ) AS CustomFieldAggregation
146
+ )`),
147
+ randomStr,
148
+ ];
159
149
  });
160
150
 
161
151
  const orders = Object.entries(sort).map(([, value]) => Sequelize.literal(`"${randomStr}" ${value}`));
@@ -164,6 +154,6 @@ export const customFieldsSortScope = (
164
154
  include: includes,
165
155
  },
166
156
  order: orders,
167
- replacements,
157
+ replacements: replacementsMap,
168
158
  };
169
159
  };