@autofleet/sadot 0.13.5-beta-3 → 0.13.5-beta.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.
@@ -31,7 +31,6 @@ const ajv_1 = __importDefault(require("ajv"));
31
31
  const joi_1 = __importDefault(require("joi"));
32
32
  const ajv_formats_1 = __importDefault(require("ajv-formats"));
33
33
  const errors_1 = require("@autofleet/errors");
34
- const ajv_errors_1 = __importDefault(require("ajv-errors"));
35
34
  const logger_1 = __importDefault(require("../utils/logger"));
36
35
  const ValidatorRepo = __importStar(require("../repository/validator"));
37
36
  const DefinitionRepo = __importStar(require("../repository/definition"));
@@ -49,7 +48,6 @@ const ajv = new ajv_1.default({
49
48
  $data: true, // Enable $data references
50
49
  });
51
50
  (0, ajv_formats_1.default)(ajv);
52
- (0, ajv_errors_1.default)(ajv);
53
51
  /**
54
52
  * Helper function to manually copy object properties
55
53
  * This is more efficient for large objects and avoids excessive object creation
@@ -90,22 +88,10 @@ const getCompleteCustomFields = async (instance, options) => {
90
88
  }
91
89
  return instance.customFields || {};
92
90
  };
93
- const formatAjvErrors = (errors) => errors.reduce((acc, err) => {
94
- const basePath = (err.instancePath || '')
95
- .split('/')
96
- .filter(Boolean)
97
- .join('.')
98
- .replace(/^after\./, '');
99
- const missingProp = err.keyword === 'required' ? `.${err.params?.missingProperty}` : '';
100
- const key = (basePath + missingProp).replace(/^\./, '') || 'root';
101
- const message = err.message || 'Invalid value';
102
- acc[key] = message;
103
- return acc;
104
- }, {});
105
91
  /**
106
92
  * Validates the model using custom validators
107
93
  */
108
- const validateModel = async (instance, options, scopeAttributes, modelOptions = {}, isCreate = false) => {
94
+ const validateModel = async (instance, options, scopeAttributes, isCreate = false) => {
109
95
  var _a;
110
96
  const modelType = instance.constructor.name;
111
97
  logger_1.default.debug('sadot - validating model', { isCreate, modelType });
@@ -135,9 +121,6 @@ const validateModel = async (instance, options, scopeAttributes, modelOptions =
135
121
  validatorsPromise = ValidatorRepo.findAllByModelType(modelType, entityId, {
136
122
  transaction: options.transaction,
137
123
  attributes: CUSTOM_VALIDATOR_ATTRIBUTES_TO_PULL,
138
- ...(modelOptions.include && {
139
- include: modelOptions.include?.(entityId),
140
- }),
141
124
  raw: true,
142
125
  });
143
126
  if (options.transaction) {
@@ -190,8 +173,7 @@ const validateModel = async (instance, options, scopeAttributes, modelOptions =
190
173
  })));
191
174
  if (!isValid) {
192
175
  const errorDetails = validateSchema.errors?.map((err) => `${err.instancePath || ''} ${err.message || 'Invalid value'}`).join(', ');
193
- const formattedErrors = formatAjvErrors(validateSchema.errors);
194
- throw new errors_1.BadRequest([new Error(`Validation failed for ${modelType}: ${errorDetails}`)], undefined, formattedErrors);
176
+ throw new errors_1.BadRequest([new Error(`Validation failed for ${modelType}: ${errorDetails}`)]);
195
177
  }
196
178
  }
197
179
  }
@@ -220,8 +202,7 @@ const validateModel = async (instance, options, scopeAttributes, modelOptions =
220
202
  const errorDetails = validateSchema
221
203
  .errors
222
204
  ?.map((err) => `${err.instancePath || ''} ${err.message || 'Invalid value'}`).join(', ');
223
- const formattedErrors = formatAjvErrors(validateSchema.errors);
224
- throw new errors_1.BadRequest([new Error(`Validation failed for ${modelType}: ${errorDetails}`)], undefined, formattedErrors);
205
+ throw new errors_1.BadRequest([new Error(`Validation failed for ${modelType}: ${errorDetails}`)]);
225
206
  }
226
207
  }
227
208
  }
@@ -289,7 +270,7 @@ const beforeCreate = (scopeAttributes, modelOptions = {}, sadotOptions = { useCu
289
270
  throw new errors_2.MissingRequiredCustomFieldError(missingFields);
290
271
  }
291
272
  // Step 2: Validate the model data (including custom fields)
292
- await validateModel(instance, options, scopeAttributes, modelOptions, true);
273
+ await validateModel(instance, options, scopeAttributes, true);
293
274
  // format date and datetime fields
294
275
  formatDates(fieldDefinitions, instance);
295
276
  // Step 3: Save custom field values if they exist
@@ -327,7 +308,7 @@ const beforeUpdate = (scopeAttributes, modelOptions = {}, sadotOptions = { useCu
327
308
  modelType, modelOptions, identifiers, options,
328
309
  });
329
310
  // Step 1: Validate the model data (including custom fields)
330
- await validateModel(instance, options, scopeAttributes, modelOptions, false);
311
+ await validateModel(instance, options, scopeAttributes, false);
331
312
  // format date and datetime fields
332
313
  formatDates(fieldDefinitions, instance);
333
314
  // Step 2: Update custom field values if they exist
@@ -1,10 +1,9 @@
1
- import type { IncludeOptions, Transactionable } from 'sequelize';
1
+ import type { Transactionable } from 'sequelize';
2
2
  import { CustomValidator } from '../models';
3
3
  export interface FindValidatorOptions extends Transactionable {
4
4
  withDisabled?: boolean;
5
5
  attributes?: string[];
6
6
  raw?: boolean;
7
- include?: IncludeOptions[];
8
7
  }
9
8
  export interface ValidatorAttributes {
10
9
  entityId: string;
@@ -40,9 +40,7 @@ const findAllByModelType = async (modelType, entityId, options = { withDisabled:
40
40
  logger_1.default.debug('custom-validator - find all validators by model type');
41
41
  return (0, exports.findAll)({
42
42
  modelType,
43
- ...(!options.include && {
44
- entityId,
45
- }),
43
+ entityId,
46
44
  }, options);
47
45
  };
48
46
  exports.findAllByModelType = findAllByModelType;
@@ -1,4 +1,4 @@
1
- import { type WhereOptions, type BindOrReplacements } from 'sequelize';
1
+ import { type WhereOptions, type IncludeOptions } from 'sequelize';
2
2
  import { type ModelStatic } from 'sequelize-typescript';
3
3
  import { CustomFieldDefinitionType } from '../constants';
4
4
  /**
@@ -14,12 +14,11 @@ import { CustomFieldDefinitionType } from '../constants';
14
14
  * @param {CustomFieldDefinitionType[]} excludedCustomFieldsTypes - An array of custom field types
15
15
  * to exclude from the search
16
16
  *
17
- * @returns {CustomFieldsSearchPayload} - An object containing the WHERE clause and replacements
18
- * for Sequelize.
17
+ * @returns {CustomFieldsSearchPayload} - An object containing the INCLUDE clause and WHERE clause to add to query payload
19
18
  */
20
19
  interface CustomFieldsSearchPayload {
21
20
  where: WhereOptions;
22
- replacements: BindOrReplacements;
21
+ include: IncludeOptions[];
23
22
  }
24
23
  export declare const generateRandomString: (length?: number) => string;
25
24
  export declare const generateCustomFieldSearchQueryPayload: (searchTerm: string, model: ModelStatic, entityId: string, customFieldsTypesToExclude?: CustomFieldDefinitionType[]) => CustomFieldsSearchPayload;
@@ -6,6 +6,7 @@ const sequelize_1 = require("sequelize");
6
6
  const sequelize_typescript_1 = require("sequelize-typescript");
7
7
  const node_crypto_1 = require("node:crypto");
8
8
  const constants_1 = require("../constants");
9
+ const models_1 = require("../../models");
9
10
  const generateRandomString = (length = 5) => {
10
11
  const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
11
12
  return Array.from({ length }, () => characters.charAt((0, node_crypto_1.randomInt)(characters.length))).join('');
@@ -14,27 +15,34 @@ exports.generateRandomString = generateRandomString;
14
15
  const generateCustomFieldSearchQueryPayload = (searchTerm, model, entityId, customFieldsTypesToExclude = [
15
16
  constants_1.CustomFieldDefinitionType.DATETIME,
16
17
  constants_1.CustomFieldDefinitionType.DATE,
17
- ]) => {
18
- const excludedTypesString = customFieldsTypesToExclude.map((type) => `'${type}'`).join(',');
19
- const subQuery = 'EXISTS ('
20
- + ' SELECT 1'
21
- + ' FROM "custom_field_values" AS "cv"'
22
- + ' INNER JOIN custom_field_definitions AS cd '
23
- + ` ON cd.entity_id = '${entityId}'`
24
- + ' AND cv.custom_field_definition_id = cd.id'
25
- + ` AND cd.model_type = '${model.name}'`
26
- + ` ${excludedTypesString ? `AND cd.field_type NOT IN (${excludedTypesString})` : ''}`
27
- + ' WHERE'
28
- + ' "cv"."deleted_at" IS NULL'
29
- + ` AND "cv"."model_id" = "${model.name}"."id"`
30
- + ' AND CAST("cv"."value" AS TEXT) ILIKE :searchTerm)';
31
- return {
32
- where: {
33
- [sequelize_1.Op.or]: [
34
- sequelize_typescript_1.Sequelize.where(sequelize_typescript_1.Sequelize.literal(subQuery), true),
18
+ ]) => ({
19
+ include: [{
20
+ attributes: ['value', 'model_id'],
21
+ model: models_1.CustomFieldValue,
22
+ as: 'customFieldValue',
23
+ required: false,
24
+ include: [
25
+ {
26
+ model: models_1.CustomFieldDefinition,
27
+ as: 'customFieldDefinition',
28
+ attributes: ['entity_id', 'field_type', 'model_type'],
29
+ required: true,
30
+ on: {
31
+ id: sequelize_typescript_1.Sequelize.where(sequelize_typescript_1.Sequelize.col('id'), { [sequelize_1.Op.eq]: sequelize_typescript_1.Sequelize.col('customFieldValue.custom_field_definition_id') }),
32
+ entityId: sequelize_typescript_1.Sequelize.where(sequelize_typescript_1.Sequelize.col('entity_id'), { [sequelize_1.Op.eq]: `${entityId}` }),
33
+ modelType: sequelize_typescript_1.Sequelize.where(sequelize_typescript_1.Sequelize.col('model_type'), { [sequelize_1.Op.eq]: `${model.name}` }),
34
+ fieldType: sequelize_typescript_1.Sequelize.where(sequelize_typescript_1.Sequelize.col('field_type'), { [sequelize_1.Op.notIn]: customFieldsTypesToExclude }),
35
+ },
36
+ where: {
37
+ deleted_at: null,
38
+ },
39
+ },
35
40
  ],
36
- },
37
- replacements: { searchTerm: `%${searchTerm}%` },
38
- };
39
- };
41
+ on: {
42
+ value: sequelize_typescript_1.Sequelize.where(sequelize_typescript_1.Sequelize.cast(sequelize_typescript_1.Sequelize.col('value'), 'text'), { [sequelize_1.Op.iLike]: `%${searchTerm}%` }),
43
+ model_id: sequelize_typescript_1.Sequelize.where(sequelize_typescript_1.Sequelize.col('model_id'), { [sequelize_1.Op.eq]: sequelize_typescript_1.Sequelize.col(`${model.name}.id`) }),
44
+ },
45
+ }],
46
+ where: sequelize_typescript_1.Sequelize.where(sequelize_typescript_1.Sequelize.col('customFieldValue.model_id'), { [sequelize_1.Op.not]: null }),
47
+ });
40
48
  exports.generateCustomFieldSearchQueryPayload = generateCustomFieldSearchQueryPayload;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autofleet/sadot",
3
- "version": "0.13.5-beta-3",
3
+ "version": "0.13.5-beta.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -31,7 +31,6 @@
31
31
  "@autofleet/common-types": "^4.4.0",
32
32
  "@autofleet/events": "^4.0.0",
33
33
  "ajv": "^8.12.0",
34
- "ajv-errors": "^3.0.0",
35
34
  "ajv-formats": "^3.0.1",
36
35
  "http-status-codes": "^2.3.0",
37
36
  "joi": "^17.7.0",
@@ -3,7 +3,6 @@ import Ajv from 'ajv';
3
3
  import Joi from 'joi';
4
4
  import addFormats from 'ajv-formats';
5
5
  import { BadRequest } from '@autofleet/errors';
6
- import ajvErrors from 'ajv-errors';
7
6
  import logger from '../utils/logger';
8
7
  import * as ValidatorRepo from '../repository/validator';
9
8
  import * as DefinitionRepo from '../repository/definition';
@@ -26,7 +25,6 @@ const ajv = new Ajv({
26
25
  });
27
26
 
28
27
  addFormats(ajv);
29
- ajvErrors(ajv);
30
28
 
31
29
  /**
32
30
  * Helper function to manually copy object properties
@@ -78,29 +76,6 @@ const getCompleteCustomFields = async (instance, options): Promise<Record<string
78
76
  return instance.customFields || {};
79
77
  };
80
78
 
81
- const formatAjvErrors = (
82
- errors: {
83
- instancePath?: string;
84
- keyword: string;
85
- message?: string;
86
- params?: Record<string, any>;
87
- }[],
88
- ): Record<string, string> => errors.reduce((acc, err) => {
89
- const basePath = (err.instancePath || '')
90
- .split('/')
91
- .filter(Boolean)
92
- .join('.')
93
- .replace(/^after\./, '');
94
-
95
- const missingProp = err.keyword === 'required' ? `.${err.params?.missingProperty}` : '';
96
- const key = (basePath + missingProp).replace(/^\./, '') || 'root';
97
-
98
- const message = err.message || 'Invalid value';
99
- acc[key] = message;
100
-
101
- return acc;
102
- }, {} as Record<string, string>);
103
-
104
79
  /**
105
80
  * Validates the model using custom validators
106
81
  */
@@ -108,7 +83,6 @@ const validateModel = async (
108
83
  instance,
109
84
  options,
110
85
  scopeAttributes: string[],
111
- modelOptions: ModelOptions = {},
112
86
  isCreate = false,
113
87
  ): Promise<void> => {
114
88
  const modelType = instance.constructor.name;
@@ -150,9 +124,6 @@ const validateModel = async (
150
124
  {
151
125
  transaction: options.transaction,
152
126
  attributes: CUSTOM_VALIDATOR_ATTRIBUTES_TO_PULL,
153
- ...(modelOptions.include && {
154
- include: modelOptions.include?.(entityId),
155
- }),
156
127
  raw: true,
157
128
  },
158
129
  );
@@ -218,12 +189,7 @@ const validateModel = async (
218
189
  const errorDetails = validateSchema.errors?.map((err) =>
219
190
  `${(err as any).instancePath || ''} ${(err as any).message || 'Invalid value'}`).join(', ');
220
191
 
221
- const formattedErrors = formatAjvErrors(validateSchema.errors);
222
- throw new BadRequest(
223
- [new Error(`Validation failed for ${modelType}: ${errorDetails}`)],
224
- undefined,
225
- formattedErrors,
226
- );
192
+ throw new BadRequest([new Error(`Validation failed for ${modelType}: ${errorDetails}`)]);
227
193
  }
228
194
  }
229
195
  } else {
@@ -258,12 +224,7 @@ const validateModel = async (
258
224
  .errors
259
225
  ?.map((err) => `${(err as any).instancePath || ''} ${(err as any).message || 'Invalid value'}`).join(', ');
260
226
 
261
- const formattedErrors = formatAjvErrors(validateSchema.errors);
262
- throw new BadRequest(
263
- [new Error(`Validation failed for ${modelType}: ${errorDetails}`)],
264
- undefined,
265
- formattedErrors,
266
- );
227
+ throw new BadRequest([new Error(`Validation failed for ${modelType}: ${errorDetails}`)]);
267
228
  }
268
229
  }
269
230
  }
@@ -360,7 +321,7 @@ export const beforeCreate = (
360
321
  }
361
322
 
362
323
  // Step 2: Validate the model data (including custom fields)
363
- await validateModel(instance, options, scopeAttributes, modelOptions, true);
324
+ await validateModel(instance, options, scopeAttributes, true);
364
325
 
365
326
  // format date and datetime fields
366
327
  formatDates(fieldDefinitions, instance);
@@ -411,7 +372,7 @@ export const beforeUpdate = (
411
372
  });
412
373
 
413
374
  // Step 1: Validate the model data (including custom fields)
414
- await validateModel(instance, options, scopeAttributes, modelOptions, false);
375
+ await validateModel(instance, options, scopeAttributes, false);
415
376
 
416
377
  // format date and datetime fields
417
378
  formatDates(fieldDefinitions, instance);
@@ -1,4 +1,4 @@
1
- import type { IncludeOptions, Transactionable } from 'sequelize';
1
+ import type { Transactionable } from 'sequelize';
2
2
  import logger from '../utils/logger';
3
3
  import { CustomValidator } from '../models';
4
4
 
@@ -6,7 +6,6 @@ export interface FindValidatorOptions extends Transactionable {
6
6
  withDisabled?: boolean;
7
7
  attributes?: string[];
8
8
  raw?: boolean;
9
- include?: IncludeOptions[];
10
9
  }
11
10
 
12
11
  // Make sure this interface is compatible with the Sequelize model
@@ -69,9 +68,7 @@ export const findAllByModelType = async (
69
68
  return findAll(
70
69
  {
71
70
  modelType,
72
- ...(!options.include && {
73
- entityId,
74
- }),
71
+ entityId,
75
72
  },
76
73
  options,
77
74
  );
@@ -1,8 +1,11 @@
1
1
  /* eslint-disable import/prefer-default-export */
2
- import { type WhereOptions, Op, type BindOrReplacements } from 'sequelize';
2
+ import {
3
+ type WhereOptions, Op, type IncludeOptions,
4
+ } from 'sequelize';
3
5
  import { type ModelStatic, Sequelize } from 'sequelize-typescript';
4
6
  import { randomInt } from 'node:crypto';
5
7
  import { CustomFieldDefinitionType } from '../constants';
8
+ import { CustomFieldDefinition, CustomFieldValue } from '../../models';
6
9
 
7
10
  /**
8
11
  * Builds a WHERE clause and replacements for free-text search by custom fields.
@@ -17,13 +20,12 @@ import { CustomFieldDefinitionType } from '../constants';
17
20
  * @param {CustomFieldDefinitionType[]} excludedCustomFieldsTypes - An array of custom field types
18
21
  * to exclude from the search
19
22
  *
20
- * @returns {CustomFieldsSearchPayload} - An object containing the WHERE clause and replacements
21
- * for Sequelize.
23
+ * @returns {CustomFieldsSearchPayload} - An object containing the INCLUDE clause and WHERE clause to add to query payload
22
24
  */
23
25
 
24
26
  interface CustomFieldsSearchPayload {
25
27
  where: WhereOptions;
26
- replacements: BindOrReplacements;
28
+ include: IncludeOptions[];
27
29
  }
28
30
 
29
31
  export const generateRandomString = (length = 5): string => {
@@ -39,28 +41,33 @@ export const generateCustomFieldSearchQueryPayload = (
39
41
  CustomFieldDefinitionType.DATETIME,
40
42
  CustomFieldDefinitionType.DATE,
41
43
  ],
42
- ): CustomFieldsSearchPayload => {
43
- const excludedTypesString = customFieldsTypesToExclude.map((type) => `'${type}'`).join(',');
44
-
45
- const subQuery = 'EXISTS ('
46
- + ' SELECT 1'
47
- + ' FROM "custom_field_values" AS "cv"'
48
- + ' INNER JOIN custom_field_definitions AS cd '
49
- + ` ON cd.entity_id = '${entityId}'`
50
- + ' AND cv.custom_field_definition_id = cd.id'
51
- + ` AND cd.model_type = '${model.name}'`
52
- + ` ${excludedTypesString ? `AND cd.field_type NOT IN (${excludedTypesString})` : ''}`
53
- + ' WHERE'
54
- + ' "cv"."deleted_at" IS NULL'
55
- + ` AND "cv"."model_id" = "${model.name}"."id"`
56
- + ' AND CAST("cv"."value" AS TEXT) ILIKE :searchTerm)';
57
-
58
- return {
59
- where: {
60
- [Op.or]: [
61
- Sequelize.where(Sequelize.literal(subQuery), true),
62
- ],
44
+ ): CustomFieldsSearchPayload => ({
45
+ include: [{
46
+ attributes: ['value', 'model_id'],
47
+ model: CustomFieldValue,
48
+ as: 'customFieldValue',
49
+ required: false,
50
+ include: [
51
+ {
52
+ model: CustomFieldDefinition,
53
+ as: 'customFieldDefinition',
54
+ attributes: ['entity_id', 'field_type', 'model_type'],
55
+ required: true,
56
+ on: {
57
+ id: Sequelize.where(Sequelize.col('id'), { [Op.eq]: Sequelize.col('customFieldValue.custom_field_definition_id') }),
58
+ entityId: Sequelize.where(Sequelize.col('entity_id'), { [Op.eq]: `${entityId}` }),
59
+ modelType: Sequelize.where(Sequelize.col('model_type'), { [Op.eq]: `${model.name}` }),
60
+ fieldType: Sequelize.where(Sequelize.col('field_type'), { [Op.notIn]: customFieldsTypesToExclude }),
61
+ },
62
+ where: {
63
+ deleted_at: null,
64
+ },
65
+ },
66
+ ],
67
+ on: {
68
+ value: Sequelize.where(Sequelize.cast(Sequelize.col('value'), 'text'), { [Op.iLike]: `%${searchTerm}%` }),
69
+ model_id: Sequelize.where(Sequelize.col('model_id'), { [Op.eq]: Sequelize.col(`${model.name}.id`) }),
63
70
  },
64
- replacements: { searchTerm: `%${searchTerm}%` },
65
- };
66
- };
71
+ }],
72
+ where: Sequelize.where(Sequelize.col('customFieldValue.model_id'), { [Op.not]: null }),
73
+ });