@autofleet/sadot 0.8.2-beta-65630a08.0 → 0.8.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.
@@ -40,11 +40,11 @@ const DefaultValueSchema = joi_1.default.when('fieldType', {
40
40
  { is: constants_1.CustomFieldDefinitionType.BOOLEAN, then: joi_1.default.boolean().allow(null) },
41
41
  { is: constants_1.CustomFieldDefinitionType.DATE, then: joi_1.default.date().allow(null) },
42
42
  { is: constants_1.CustomFieldDefinitionType.DATETIME, then: joi_1.default.date().allow(null) },
43
- { is: constants_1.CustomFieldDefinitionType.FILE, then: FileValidationSchema },
44
- { is: constants_1.CustomFieldDefinitionType.IMAGE, then: joi_1.default.string().uri().allow(null) },
43
+ { is: constants_1.CustomFieldDefinitionType.FILE, then: joi_1.default.array().items(FileValidationSchema).allow(null) },
44
+ { is: constants_1.CustomFieldDefinitionType.IMAGE, then: joi_1.default.array().items(joi_1.default.string().uri()).allow(null) },
45
45
  { is: constants_1.CustomFieldDefinitionType.NUMBER, then: joi_1.default.number().allow(null) },
46
46
  { is: constants_1.CustomFieldDefinitionType.SELECT, then: joi_1.default.string().allow(null) },
47
- { is: constants_1.CustomFieldDefinitionType.STATUS, then: statusValidationObject.allow(null) },
47
+ { is: constants_1.CustomFieldDefinitionType.STATUS, then: joi_1.default.string().allow(null) },
48
48
  { is: constants_1.CustomFieldDefinitionType.TEXT, then: joi_1.default.string().allow(null) },
49
49
  ],
50
50
  });
@@ -1,4 +1,5 @@
1
1
  import { BadRequest } from '@autofleet/errors';
2
+ import type { ValidationError } from 'joi';
2
3
  export declare class MissingRequiredCustomFieldError extends BadRequest {
3
4
  constructor(missingFields: string[]);
4
5
  }
@@ -8,8 +9,11 @@ export declare class UnsupportedCustomFieldTypeError extends BadRequest {
8
9
  export declare class UnsupportedCustomValidationError extends BadRequest {
9
10
  constructor(fieldType: string);
10
11
  }
12
+ export declare class InvalidFieldTypeError extends BadRequest {
13
+ constructor(fieldType: string);
14
+ }
11
15
  export declare class InvalidValueError extends BadRequest {
12
- constructor(value: any, fieldType: string);
16
+ constructor(value: any, fieldDefinitionName: string, joiValidationError: ValidationError);
13
17
  }
14
18
  export declare class MissingDefinitionError extends BadRequest {
15
19
  constructor(fieldNames: string[]);
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MissingDefinitionError = exports.InvalidValueError = exports.UnsupportedCustomValidationError = exports.UnsupportedCustomFieldTypeError = exports.MissingRequiredCustomFieldError = void 0;
3
+ exports.MissingDefinitionError = exports.InvalidValueError = exports.InvalidFieldTypeError = exports.UnsupportedCustomValidationError = exports.UnsupportedCustomFieldTypeError = exports.MissingRequiredCustomFieldError = void 0;
4
4
  /* eslint-disable max-classes-per-file */
5
5
  const errors_1 = require("@autofleet/errors");
6
6
  class MissingRequiredCustomFieldError extends errors_1.BadRequest {
@@ -27,11 +27,24 @@ class UnsupportedCustomValidationError extends errors_1.BadRequest {
27
27
  }
28
28
  }
29
29
  exports.UnsupportedCustomValidationError = UnsupportedCustomValidationError;
30
+ class InvalidFieldTypeError extends errors_1.BadRequest {
31
+ constructor(fieldType) {
32
+ const err = new Error(`Invalid field type ${fieldType}`);
33
+ super([err], null, null);
34
+ this.message = 'INVALID_FIELD_TYPE';
35
+ }
36
+ }
37
+ exports.InvalidFieldTypeError = InvalidFieldTypeError;
30
38
  class InvalidValueError extends errors_1.BadRequest {
31
- constructor(value, fieldType) {
32
- const err = new Error(`Invalid "${fieldType}" value ${JSON.stringify(value)}`);
39
+ constructor(value, fieldDefinitionName, joiValidationError) {
40
+ const formattedErrorMessage = joiValidationError.message
41
+ .replace(/"/g, '')
42
+ .replace('value', `'${fieldDefinitionName}'`);
43
+ const formattedValue = typeof value === 'object' ? JSON.stringify(value) : value;
44
+ const invalidValueMessage = `Invalid Value on field '${fieldDefinitionName}'. ${formattedErrorMessage}. received: '${formattedValue}'`;
45
+ const err = new Error(invalidValueMessage);
33
46
  super([err], null, null);
34
- this.message = 'INVALID_VALUE';
47
+ this.message = invalidValueMessage;
35
48
  }
36
49
  }
37
50
  exports.InvalidValueError = InvalidValueError;
@@ -28,8 +28,8 @@ let CustomFieldDefinition = class CustomFieldDefinition extends sequelize_typesc
28
28
  }
29
29
  if (![null, undefined].includes(instance.defaultValue)) {
30
30
  const isValid = (0, validations_1.validateValue)(instance.defaultValue, instance.fieldType, instance.validation);
31
- if (!isValid) {
32
- throw new errors_1.InvalidValueError(instance.defaultValue, instance.fieldType);
31
+ if (isValid.error) {
32
+ throw new errors_1.InvalidValueError(instance.defaultValue, instance.name, isValid.error);
33
33
  }
34
34
  }
35
35
  }
@@ -8,8 +8,9 @@ declare class CustomFieldValue extends Model {
8
8
  updatedAt?: Date;
9
9
  deletedAt?: Date;
10
10
  customFieldDefinition: CustomFieldDefinition;
11
- static validateValues(instances: CustomFieldValue[]): Promise<void>;
12
- static validateValue(instance: CustomFieldValue): Promise<void>;
11
+ private static validateValueAgainstDefinition;
12
+ static validateCustomFieldValues(instances: CustomFieldValue[]): Promise<void>;
13
+ static validateCustomFieldValue(instance: CustomFieldValue): Promise<void>;
13
14
  static afterSaveHandler(instance: CustomFieldValue, options: any): void;
14
15
  }
15
16
  export default CustomFieldValue;
@@ -39,7 +39,21 @@ const validations_1 = require("../utils/validations");
39
39
  const CustomFieldDefinitionRepo = __importStar(require("../repository/definition"));
40
40
  const errors_1 = require("../errors");
41
41
  let CustomFieldValue = class CustomFieldValue extends sequelize_typescript_1.Model {
42
- static async validateValues(instances) {
42
+ static validateValueAgainstDefinition(instance, definition) {
43
+ const { validation, fieldType, name } = definition;
44
+ const isValidFieldType = (0, validations_1.validateFieldType)(fieldType);
45
+ if (!isValidFieldType) {
46
+ throw new errors_1.InvalidFieldTypeError(fieldType);
47
+ }
48
+ // Always allow null values
49
+ if (instance.value === null)
50
+ return;
51
+ const validateValueResponse = (0, validations_1.validateValue)(instance.value, fieldType, validation);
52
+ if (validateValueResponse.error) {
53
+ throw new errors_1.InvalidValueError(instance.value, name, validateValueResponse.error);
54
+ }
55
+ }
56
+ static async validateCustomFieldValues(instances) {
43
57
  const ids = instances.map((instance) => instance.customFieldDefinitionId);
44
58
  const uniqueIds = [...new Set(ids)];
45
59
  const definitions = await CustomFieldDefinitionRepo.findByIds(uniqueIds, { withDisabled: true });
@@ -47,23 +61,17 @@ let CustomFieldValue = class CustomFieldValue extends sequelize_typescript_1.Mod
47
61
  throw new Error('Definitions not found');
48
62
  }
49
63
  instances.forEach((instance) => {
50
- const { validation, fieldType, } = definitions
51
- .find((definition) => definition.id === instance.customFieldDefinitionId);
52
- const isValid = (0, validations_1.validateValue)(instance.value, fieldType, validation);
53
- if (!isValid) {
54
- throw new errors_1.InvalidValueError(instance.value, fieldType);
64
+ const definition = definitions.find((d) => d.id === instance.customFieldDefinitionId);
65
+ if (definition) {
66
+ this.validateValueAgainstDefinition(instance, definition);
55
67
  }
56
68
  });
57
69
  }
58
- static async validateValue(instance) {
70
+ static async validateCustomFieldValue(instance) {
59
71
  const { customFieldDefinitionId } = instance;
60
72
  // eslint-disable-next-line max-len
61
73
  const cfd = await CustomFieldDefinitionRepo.findById(customFieldDefinitionId, { withDisabled: true });
62
- const { validation, fieldType } = cfd;
63
- const isValid = (0, validations_1.validateValue)(instance.value, fieldType, validation);
64
- if (!isValid) {
65
- throw new errors_1.InvalidValueError(instance.value, fieldType);
66
- }
74
+ this.validateValueAgainstDefinition(instance, cfd);
67
75
  }
68
76
  static afterSaveHandler(instance, options) {
69
77
  if (options.transaction) {
@@ -120,7 +128,7 @@ __decorate([
120
128
  __metadata("design:type", Function),
121
129
  __metadata("design:paramtypes", [Array]),
122
130
  __metadata("design:returntype", Promise)
123
- ], CustomFieldValue, "validateValues", null);
131
+ ], CustomFieldValue, "validateCustomFieldValues", null);
124
132
  __decorate([
125
133
  sequelize_typescript_1.BeforeUpdate,
126
134
  sequelize_typescript_1.BeforeCreate,
@@ -128,7 +136,7 @@ __decorate([
128
136
  __metadata("design:type", Function),
129
137
  __metadata("design:paramtypes", [CustomFieldValue]),
130
138
  __metadata("design:returntype", Promise)
131
- ], CustomFieldValue, "validateValue", null);
139
+ ], CustomFieldValue, "validateCustomFieldValue", null);
132
140
  __decorate([
133
141
  sequelize_typescript_1.AfterUpsert,
134
142
  __metadata("design:type", Function),
@@ -10,24 +10,25 @@ const { CUSTOM_FIELDS_FILTER_SCOPE } = common_types_1.customFields;
10
10
  const isConditionStringArray = (input) => Array.isArray(input) && typeof input[0] === 'string';
11
11
  const isBooleanString = (input) => ['true', 'false'].includes(input.toString());
12
12
  const isDate = (input) => input instanceof Date || Object.prototype.toString.call(input) === '[object Date]';
13
- const castIfNeeded = (conditionValue) => {
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) => {
14
18
  if (isDate(conditionValue)) {
15
- return '::timestamp';
19
+ return castValueToJsonb(columnName, 'timestamp');
16
20
  }
17
21
  if (!Number.isNaN(Number(conditionValue))) {
18
- return '::numeric';
22
+ return castValueToJsonbNumeric(columnName);
19
23
  }
20
- return '';
24
+ return columnName;
21
25
  };
22
26
  const AND_DELIMITER = ' AND ';
23
27
  const OR_DELIMITER = ' OR ';
24
28
  const CD_TABLE_ALIAS = 'cd';
25
29
  const CD_NAME_COLUMN = `${CD_TABLE_ALIAS}.name`;
26
30
  const CV_TABLE_ALIAS = 'cv';
27
- const CV_VALUE_COLUMN = `(${CV_TABLE_ALIAS}.value)`;
28
- const castValueToJsonb = (value, type) => `to_jsonb(${value}::${type})`;
29
- const castValueToJsonbText = (value) => castValueToJsonb(value, 'text');
30
- const castValueToJsonbBoolean = (value) => castValueToJsonb(value, 'boolean');
31
+ const CV_VALUE_COLUMN = `${CV_TABLE_ALIAS}.value`;
31
32
  /**
32
33
  * A Sequelize scope for filtering models by custom fields.
33
34
  * This scope builds a WHERE clause to be applied on the main query.
@@ -57,6 +58,9 @@ const customFieldsFilterScope = (name) => ({ replacementsMap: replacements, scop
57
58
  if (isBooleanString(v)) {
58
59
  return [castValueToJsonbText(`:${valRandom}`), castValueToJsonbBoolean(`:${valRandom}`)];
59
60
  }
61
+ if (!Number.isNaN(Number(v))) {
62
+ return castValueToJsonbNumeric(`:${valRandom}`);
63
+ }
60
64
  return castValueToJsonbText(`:${valRandom}`);
61
65
  }).join(',');
62
66
  return `(${columnCondition}${AND_DELIMITER}${CV_VALUE_COLUMN} IN (${values}))`;
@@ -64,19 +68,19 @@ const customFieldsFilterScope = (name) => ({ replacementsMap: replacements, scop
64
68
  return condition.map((c) => {
65
69
  const valRep = reverseReplacementsMap.get(c.value);
66
70
  const valueAsJsonb = castValueToJsonbText(`:${valRep}`);
67
- return `(${columnCondition}${AND_DELIMITER}${CV_VALUE_COLUMN}${castIfNeeded(c.value)} ${c.operator} ${valueAsJsonb})`;
71
+ return `(${columnCondition}${AND_DELIMITER}${castIfNeeded(CV_VALUE_COLUMN, c.value)} ${c.operator} ${valueAsJsonb})`;
68
72
  }).join(AND_DELIMITER);
69
73
  }
70
74
  if (typeof condition === 'string' || typeof condition === 'number') {
71
75
  const conditionRep = reverseReplacementsMap.get(condition);
72
- const valueAsJsonb = castValueToJsonbText(`:${conditionRep}`);
76
+ const valueAsJsonb = !Number.isNaN(Number(condition)) ? castValueToJsonbNumeric(`:${conditionRep}`) : castValueToJsonbText(`:${conditionRep}`);
73
77
  const valueAsJsonbBoolean = isBooleanString(condition) ? `${OR_DELIMITER}${CV_VALUE_COLUMN} = ${castValueToJsonbBoolean(`:${conditionRep}`)}` : '';
74
- return `(${columnCondition}${AND_DELIMITER}(${CV_VALUE_COLUMN}${castIfNeeded(condition)} = ${valueAsJsonb}${valueAsJsonbBoolean}))`;
78
+ return `(${columnCondition}${AND_DELIMITER}(${castIfNeeded(CV_VALUE_COLUMN, condition)} = ${valueAsJsonb}${valueAsJsonbBoolean}))`;
75
79
  }
76
80
  if (condition?.operator) {
77
81
  const valueRep = reverseReplacementsMap.get(condition.value);
78
82
  const valueAsJsonb = castValueToJsonbText(`:${valueRep}`);
79
- return `( ${columnCondition}${AND_DELIMITER}${CV_VALUE_COLUMN}${castIfNeeded(condition.value)} ${condition.operator} ${valueAsJsonb})`;
83
+ return `( ${columnCondition}${AND_DELIMITER}${castIfNeeded(CV_VALUE_COLUMN, condition.value)} ${condition.operator} ${valueAsJsonb})`;
80
84
  }
81
85
  return false;
82
86
  }).filter(Boolean);
@@ -43,5 +43,6 @@ export declare const coolFieldDefinition3: {
43
43
  export declare const booleanField: (modelType: string) => CreateCustomFieldDefinition;
44
44
  export declare const selectField: (modelType: string, options: any) => CreateCustomFieldDefinition;
45
45
  export declare const statusField: (modelType: string, options: any) => CreateCustomFieldDefinition;
46
+ export declare const fileField: (modelType: string) => CreateCustomFieldDefinition;
46
47
  export declare const createDefinition: (defaults: Partial<CustomFieldDefinitionDTO>) => CreateCustomFieldDefinition;
47
48
  export declare const createDefinitions: (defaults: Partial<CustomFieldDefinitionDTO>, length?: number) => CreateCustomFieldDefinition[];
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createDefinitions = exports.createDefinition = exports.statusField = exports.selectField = exports.booleanField = exports.coolFieldDefinition3 = exports.coolFieldDefinition2 = exports.coolFieldDefinition = exports.contextAwareFieldDefinition = void 0;
3
+ exports.createDefinitions = exports.createDefinition = exports.fileField = exports.statusField = exports.selectField = exports.booleanField = exports.coolFieldDefinition3 = exports.coolFieldDefinition2 = exports.coolFieldDefinition = exports.contextAwareFieldDefinition = void 0;
4
4
  const node_crypto_1 = require("node:crypto");
5
5
  exports.contextAwareFieldDefinition = {
6
6
  name: 'cool field',
@@ -49,6 +49,14 @@ const statusField = (modelType, options) => ({
49
49
  entityType: 'fleetId',
50
50
  });
51
51
  exports.statusField = statusField;
52
+ const fileField = (modelType) => ({
53
+ name: 'file',
54
+ modelType,
55
+ fieldType: 'file',
56
+ entityId: (0, node_crypto_1.randomUUID)(),
57
+ entityType: 'fleetId',
58
+ });
59
+ exports.fileField = fileField;
52
60
  // eslint-disable-next-line max-len
53
61
  const createDefinition = (defaults) => ({
54
62
  name: defaults?.name || `def_${(0, node_crypto_1.randomUUID)()}`,
@@ -1,2 +1,3 @@
1
1
  import type { CustomFieldDefinitionType } from '../constants';
2
- export declare const validateValue: (value: unknown, valueType: CustomFieldDefinitionType, validation?: unknown) => boolean;
2
+ export declare const validateFieldType: (type: CustomFieldDefinitionType) => boolean;
3
+ export declare const validateValue: (value: unknown, valueType: CustomFieldDefinitionType, validation?: unknown) => import("joi").ValidationResult;
@@ -1,15 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.validateValue = void 0;
3
+ exports.validateValue = exports.validateFieldType = void 0;
4
4
  const validators_1 = require("./validators");
5
+ const validateFieldType = (type) => Object.keys(validators_1.validators).includes(type);
6
+ exports.validateFieldType = validateFieldType;
5
7
  const validateValue = (value, valueType, validation) => {
6
8
  const validator = validators_1.validators[valueType];
7
- if (!validator) {
8
- // Unsupported field type
9
- return false;
10
- }
11
- // Always allow null values
12
- return value === null || validator(value, validation);
9
+ return validator(value, validation);
13
10
  /** TODO: Add validation for required fields
14
11
  * @example
15
12
  * if (validations.required && !value) {
@@ -1,10 +1,11 @@
1
+ import type { ValidationResult } from 'joi';
1
2
  import type { CustomFieldDefinitionType } from '../constants';
2
3
  /**
3
4
  * Validator is a function that validates a custom-field `value`,
4
5
  * against a custom-field definition `validation object`.
5
6
  * @returns `true` if the value is valid, `false` otherwise.
6
7
  */
7
- export type Validator<Value, DefinitionValidationObject> = (value: Value, validation?: DefinitionValidationObject) => boolean;
8
+ export type Validator<Value, DefinitionValidationObject> = (value: Value, validation?: DefinitionValidationObject) => ValidationResult;
8
9
  /**
9
10
  * Validators is a map of custom-field types to their respective validators.
10
11
  * The key is the custom-field type, and the value is the validator function.
@@ -22,19 +22,19 @@ exports.CustomValidationTypes = {
22
22
  exports.validators = {
23
23
  [constants_1.CustomFieldDefinitionType.SELECT]: select_validator_1.validateSelect,
24
24
  [constants_1.CustomFieldDefinitionType.STATUS]: status_validator_1.validateStatus,
25
- [constants_1.CustomFieldDefinitionType.TEXT]: (value) => (typeof value === 'string'),
26
- [constants_1.CustomFieldDefinitionType.NUMBER]: (value) => (typeof value === 'number'),
27
- [constants_1.CustomFieldDefinitionType.BOOLEAN]: (value) => (typeof value === 'boolean'),
28
- [constants_1.CustomFieldDefinitionType.DATE]: (value) => (!joi_1.default.date().validate(value).error),
29
- [constants_1.CustomFieldDefinitionType.DATETIME]: (value) => (!joi_1.default.date().validate(value).error),
30
- [constants_1.CustomFieldDefinitionType.IMAGE]: (value) => (!joi_1.default.array().min(1).unique()
25
+ [constants_1.CustomFieldDefinitionType.TEXT]: (value) => joi_1.default.string().min(0).validate(value),
26
+ [constants_1.CustomFieldDefinitionType.NUMBER]: (value) => joi_1.default.number().strict(true).validate(value),
27
+ [constants_1.CustomFieldDefinitionType.BOOLEAN]: (value) => joi_1.default.boolean().strict().validate(value),
28
+ [constants_1.CustomFieldDefinitionType.DATE]: (value) => joi_1.default.date().validate(value),
29
+ [constants_1.CustomFieldDefinitionType.DATETIME]: (value) => joi_1.default.date().validate(value),
30
+ [constants_1.CustomFieldDefinitionType.IMAGE]: (value) => joi_1.default.array().min(1).unique()
31
31
  .items(joi_1.default.string().uri())
32
- .validate(value).error),
33
- [constants_1.CustomFieldDefinitionType.FILE]: (value) => (!joi_1.default.array().min(1).unique().items(joi_1.default.object({
32
+ .validate(value),
33
+ [constants_1.CustomFieldDefinitionType.FILE]: (value) => joi_1.default.array().min(1).unique().items(joi_1.default.object({
34
34
  name: joi_1.default.string().required(),
35
35
  type: joi_1.default.string(),
36
36
  size: joi_1.default.string(),
37
37
  addedBy: joi_1.default.string().uuid(),
38
38
  }))
39
- .validate(value).error),
39
+ .validate(value),
40
40
  };
@@ -1,9 +1,12 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.validateSelect = void 0;
7
+ const joi_1 = __importDefault(require("joi"));
4
8
  /**
5
9
  * Validate that the value is one of the select values
6
10
  */
7
- const validateSelect = (value, selectValues) => (Array.isArray(selectValues)
8
- && selectValues.includes(value));
11
+ const validateSelect = (value, selectValues) => (joi_1.default.string().allow(null).valid(...selectValues).validate(value));
9
12
  exports.validateSelect = validateSelect;
@@ -1,9 +1,15 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.validateStatus = void 0;
7
+ const joi_1 = __importDefault(require("joi"));
4
8
  /**
5
9
  * Validate that the value is one of the status values
6
10
  */
7
- const validateStatus = (value, statusValues) => (Array.isArray(statusValues)
8
- && statusValues.some((status) => status.value === value));
11
+ const validateStatus = (value, statusValues) => (joi_1.default.string()
12
+ .allow(null)
13
+ .valid(...statusValues.map((statusValue) => statusValue.value))
14
+ .validate(value));
9
15
  exports.validateStatus = validateStatus;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autofleet/sadot",
3
- "version": "0.8.2-beta-65630a08.0",
3
+ "version": "0.8.2",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -50,8 +50,7 @@
50
50
  "npm-watch": "^0.11.0",
51
51
  "ts-jest": "^29.1.2",
52
52
  "ts-node": "^8.6.2",
53
- "typescript": "^5.3.3",
54
- "typescript-eslint": "^0.0.1-alpha.0"
53
+ "typescript": "^5.3.3"
55
54
  },
56
55
  "peerDependencies": {
57
56
  "@autofleet/sheilta": ">=1.4.0"
@@ -36,11 +36,11 @@ const DefaultValueSchema = Joi.when('fieldType', {
36
36
  { is: CustomFieldDefinitionType.BOOLEAN, then: Joi.boolean().allow(null) },
37
37
  { is: CustomFieldDefinitionType.DATE, then: Joi.date().allow(null) },
38
38
  { is: CustomFieldDefinitionType.DATETIME, then: Joi.date().allow(null) },
39
- { is: CustomFieldDefinitionType.FILE, then: FileValidationSchema },
40
- { is: CustomFieldDefinitionType.IMAGE, then: Joi.string().uri().allow(null) },
39
+ { is: CustomFieldDefinitionType.FILE, then: Joi.array().items(FileValidationSchema).allow(null) },
40
+ { is: CustomFieldDefinitionType.IMAGE, then: Joi.array().items(Joi.string().uri()).allow(null) },
41
41
  { is: CustomFieldDefinitionType.NUMBER, then: Joi.number().allow(null) },
42
42
  { is: CustomFieldDefinitionType.SELECT, then: Joi.string().allow(null) },
43
- { is: CustomFieldDefinitionType.STATUS, then: statusValidationObject.allow(null) },
43
+ { is: CustomFieldDefinitionType.STATUS, then: Joi.string().allow(null) },
44
44
  { is: CustomFieldDefinitionType.TEXT, then: Joi.string().allow(null) },
45
45
  ],
46
46
  });
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable max-classes-per-file */
2
2
  import { BadRequest } from '@autofleet/errors';
3
+ import type { ValidationError } from 'joi';
3
4
 
4
5
  export class MissingRequiredCustomFieldError extends BadRequest {
5
6
  constructor(missingFields: string[]) {
@@ -25,11 +26,27 @@ export class UnsupportedCustomValidationError extends BadRequest {
25
26
  }
26
27
  }
27
28
 
29
+ export class InvalidFieldTypeError extends BadRequest {
30
+ constructor(fieldType: string) {
31
+ const err = new Error(`Invalid field type ${fieldType}`);
32
+ super([err], null, null);
33
+ this.message = 'INVALID_FIELD_TYPE';
34
+ }
35
+ }
36
+
28
37
  export class InvalidValueError extends BadRequest {
29
- constructor(value: any, fieldType: string) {
30
- const err = new Error(`Invalid "${fieldType}" value ${JSON.stringify(value)}`);
38
+ constructor(value: any, fieldDefinitionName: string, joiValidationError: ValidationError) {
39
+ const formattedErrorMessage = joiValidationError.message
40
+ .replace(/"/g, '')
41
+ .replace('value', `'${fieldDefinitionName}'`);
42
+
43
+ const formattedValue = typeof value === 'object' ? JSON.stringify(value) : value;
44
+
45
+ const invalidValueMessage = `Invalid Value on field '${fieldDefinitionName}'. ${formattedErrorMessage}. received: '${formattedValue}'`;
46
+
47
+ const err = new Error(invalidValueMessage);
31
48
  super([err], null, null);
32
- this.message = 'INVALID_VALUE';
49
+ this.message = invalidValueMessage;
33
50
  }
34
51
  }
35
52
 
@@ -143,8 +143,8 @@ class CustomFieldDefinition extends Model {
143
143
  }
144
144
  if (![null, undefined].includes(instance.defaultValue)) {
145
145
  const isValid = validateValue(instance.defaultValue, instance.fieldType, instance.validation);
146
- if (!isValid) {
147
- throw new InvalidValueError(instance.defaultValue, instance.fieldType);
146
+ if (isValid.error) {
147
+ throw new InvalidValueError(instance.defaultValue, instance.name, isValid.error);
148
148
  }
149
149
  }
150
150
  }
@@ -15,9 +15,9 @@ import {
15
15
  } from 'sequelize-typescript';
16
16
  import { sendDimEvent } from '../events';
17
17
  import { CustomFieldDefinition } from '.';
18
- import { validateValue } from '../utils/validations';
18
+ import { validateFieldType, validateValue } from '../utils/validations';
19
19
  import * as CustomFieldDefinitionRepo from '../repository/definition';
20
- import { InvalidValueError } from '../errors';
20
+ import { InvalidFieldTypeError, InvalidValueError } from '../errors';
21
21
 
22
22
  @Table({
23
23
  timestamps: true,
@@ -56,9 +56,26 @@ class CustomFieldValue extends Model {
56
56
  @BelongsTo(() => CustomFieldDefinition, { scope: { disabled: false } })
57
57
  customFieldDefinition: CustomFieldDefinition;
58
58
 
59
+ private static validateValueAgainstDefinition(
60
+ instance: CustomFieldValue,
61
+ definition: CustomFieldDefinition,
62
+ ): void {
63
+ const { validation, fieldType, name } = definition;
64
+ const isValidFieldType = validateFieldType(fieldType);
65
+ if (!isValidFieldType) {
66
+ throw new InvalidFieldTypeError(fieldType);
67
+ }
68
+ // Always allow null values
69
+ if (instance.value === null) return;
70
+ const validateValueResponse = validateValue(instance.value, fieldType, validation);
71
+ if (validateValueResponse.error) {
72
+ throw new InvalidValueError(instance.value, name, validateValueResponse.error);
73
+ }
74
+ }
75
+
59
76
  @BeforeBulkCreate
60
77
  @BeforeBulkUpdate
61
- static async validateValues(instances: CustomFieldValue[]): Promise<void> {
78
+ static async validateCustomFieldValues(instances: CustomFieldValue[]): Promise<void> {
62
79
  const ids = instances.map((instance) => instance.customFieldDefinitionId);
63
80
  const uniqueIds = [...new Set(ids)];
64
81
  const definitions = await CustomFieldDefinitionRepo.findByIds(
@@ -71,14 +88,9 @@ class CustomFieldValue extends Model {
71
88
  }
72
89
 
73
90
  instances.forEach((instance) => {
74
- const {
75
- validation,
76
- fieldType,
77
- } = definitions
78
- .find((definition) => definition.id === instance.customFieldDefinitionId);
79
- const isValid = validateValue(instance.value, fieldType, validation);
80
- if (!isValid) {
81
- throw new InvalidValueError(instance.value, fieldType);
91
+ const definition = definitions.find((d) => d.id === instance.customFieldDefinitionId);
92
+ if (definition) {
93
+ this.validateValueAgainstDefinition(instance, definition);
82
94
  }
83
95
  });
84
96
  }
@@ -86,15 +98,11 @@ class CustomFieldValue extends Model {
86
98
  @BeforeUpdate
87
99
  @BeforeCreate
88
100
  @BeforeUpsert
89
- static async validateValue(instance: CustomFieldValue): Promise<void> {
101
+ static async validateCustomFieldValue(instance: CustomFieldValue): Promise<void> {
90
102
  const { customFieldDefinitionId } = instance;
91
103
  // eslint-disable-next-line max-len
92
104
  const cfd = await CustomFieldDefinitionRepo.findById(customFieldDefinitionId, { withDisabled: true });
93
- const { validation, fieldType } = cfd;
94
- const isValid = validateValue(instance.value, fieldType, validation);
95
- if (!isValid) {
96
- throw new InvalidValueError(instance.value, fieldType);
97
- }
105
+ this.validateValueAgainstDefinition(instance, cfd);
98
106
  }
99
107
 
100
108
  @AfterUpsert
@@ -36,24 +36,25 @@ const isConditionStringArray = (input: any): input is string[] => Array.isArray(
36
36
  const isBooleanString = (input: string): boolean => ['true', 'false'].includes(input.toString());
37
37
  const isDate = (input: any): input is Date => input instanceof Date || Object.prototype.toString.call(input) === '[object Date]';
38
38
 
39
- const castIfNeeded = (conditionValue: string): string => {
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 => {
40
44
  if (isDate(conditionValue)) {
41
- return '::timestamp';
45
+ return castValueToJsonb(columnName, 'timestamp');
42
46
  }
43
47
  if (!Number.isNaN(Number(conditionValue))) {
44
- return '::numeric';
48
+ return castValueToJsonbNumeric(columnName);
45
49
  }
46
- return '';
50
+ return columnName;
47
51
  };
48
52
  const AND_DELIMITER = ' AND ';
49
53
  const OR_DELIMITER = ' OR ';
50
54
  const CD_TABLE_ALIAS = 'cd';
51
55
  const CD_NAME_COLUMN = `${CD_TABLE_ALIAS}.name`;
52
56
  const CV_TABLE_ALIAS = 'cv';
53
- const CV_VALUE_COLUMN = `(${CV_TABLE_ALIAS}.value)`;
54
- const castValueToJsonb = (value: string, type: string) => `to_jsonb(${value}::${type})`;
55
- const castValueToJsonbText = (value: string) => castValueToJsonb(value, 'text');
56
- const castValueToJsonbBoolean = (value: string) => castValueToJsonb(value, 'boolean');
57
+ const CV_VALUE_COLUMN = `${CV_TABLE_ALIAS}.value`;
57
58
 
58
59
  /**
59
60
  * A Sequelize scope for filtering models by custom fields.
@@ -86,6 +87,9 @@ export const customFieldsFilterScope = (
86
87
  if (isBooleanString(v)) {
87
88
  return [castValueToJsonbText(`:${valRandom}`), castValueToJsonbBoolean(`:${valRandom}`)];
88
89
  }
90
+ if (!Number.isNaN(Number(v))) {
91
+ return castValueToJsonbNumeric(`:${valRandom}`);
92
+ }
89
93
  return castValueToJsonbText(`:${valRandom}`);
90
94
  }).join(',');
91
95
  return `(${columnCondition}${AND_DELIMITER}${CV_VALUE_COLUMN} IN (${values}))`;
@@ -93,19 +97,19 @@ export const customFieldsFilterScope = (
93
97
  return condition.map((c) => {
94
98
  const valRep = reverseReplacementsMap.get(c.value);
95
99
  const valueAsJsonb = castValueToJsonbText(`:${valRep}`);
96
- return `(${columnCondition}${AND_DELIMITER}${CV_VALUE_COLUMN}${castIfNeeded(c.value)} ${c.operator} ${valueAsJsonb})`;
100
+ return `(${columnCondition}${AND_DELIMITER}${castIfNeeded(CV_VALUE_COLUMN, c.value)} ${c.operator} ${valueAsJsonb})`;
97
101
  }).join(AND_DELIMITER);
98
102
  }
99
103
  if (typeof condition === 'string' || typeof condition === 'number') {
100
104
  const conditionRep = reverseReplacementsMap.get(condition);
101
- const valueAsJsonb = castValueToJsonbText(`:${conditionRep}`);
105
+ const valueAsJsonb = !Number.isNaN(Number(condition)) ? castValueToJsonbNumeric(`:${conditionRep}`) : castValueToJsonbText(`:${conditionRep}`);
102
106
  const valueAsJsonbBoolean = isBooleanString(condition) ? `${OR_DELIMITER}${CV_VALUE_COLUMN} = ${castValueToJsonbBoolean(`:${conditionRep}`)}` : '';
103
- return `(${columnCondition}${AND_DELIMITER}(${CV_VALUE_COLUMN}${castIfNeeded(condition)} = ${valueAsJsonb}${valueAsJsonbBoolean}))`;
107
+ return `(${columnCondition}${AND_DELIMITER}(${castIfNeeded(CV_VALUE_COLUMN, condition)} = ${valueAsJsonb}${valueAsJsonbBoolean}))`;
104
108
  }
105
109
  if (condition?.operator) {
106
110
  const valueRep = reverseReplacementsMap.get(condition.value);
107
111
  const valueAsJsonb = castValueToJsonbText(`:${valueRep}`);
108
- return `( ${columnCondition}${AND_DELIMITER}${CV_VALUE_COLUMN}${castIfNeeded(condition.value)} ${condition.operator} ${valueAsJsonb})`;
112
+ return `( ${columnCondition}${AND_DELIMITER}${castIfNeeded(CV_VALUE_COLUMN, condition.value)} ${condition.operator} ${valueAsJsonb})`;
109
113
  }
110
114
  return false;
111
115
  }).filter(Boolean);
@@ -52,6 +52,14 @@ export const statusField = (modelType: string, options): CreateCustomFieldDefini
52
52
  entityType: 'fleetId',
53
53
  });
54
54
 
55
+ export const fileField = (modelType: string): CreateCustomFieldDefinition => ({
56
+ name: 'file',
57
+ modelType,
58
+ fieldType: 'file',
59
+ entityId: uuidv4(),
60
+ entityType: 'fleetId',
61
+ });
62
+
55
63
  // eslint-disable-next-line max-len
56
64
  export const createDefinition = (defaults: Partial<CustomFieldDefinitionDTO>): CreateCustomFieldDefinition => ({
57
65
  name: defaults?.name || `def_${uuidv4()}`,
@@ -1,18 +1,15 @@
1
1
  import type { CustomFieldDefinitionType } from '../constants';
2
2
  import { validators } from './validators';
3
3
 
4
+ export const validateFieldType = (type: CustomFieldDefinitionType): boolean => Object.keys(validators).includes(type);
5
+
4
6
  export const validateValue = (
5
7
  value: unknown,
6
8
  valueType: CustomFieldDefinitionType,
7
9
  validation?: unknown,
8
10
  ) => {
9
11
  const validator = validators[valueType];
10
- if (!validator) {
11
- // Unsupported field type
12
- return false;
13
- }
14
- // Always allow null values
15
- return value === null || validator(value, validation);
12
+ return validator(value, validation);
16
13
  /** TODO: Add validation for required fields
17
14
  * @example
18
15
  * if (validations.required && !value) {
@@ -1,3 +1,4 @@
1
+ import type { ValidationResult } from 'joi';
1
2
  import type { CustomFieldDefinitionType } from '../constants';
2
3
 
3
4
  /**
@@ -8,7 +9,7 @@ import type { CustomFieldDefinitionType } from '../constants';
8
9
  export type Validator<Value, DefinitionValidationObject> = (
9
10
  value: Value,
10
11
  validation?: DefinitionValidationObject
11
- ) => boolean;
12
+ ) => ValidationResult;
12
13
 
13
14
  /**
14
15
  * Validators is a map of custom-field types to their respective validators.
@@ -20,19 +20,19 @@ export const CustomValidationTypes = {
20
20
  export const validators: Validators = {
21
21
  [CustomFieldDefinitionType.SELECT]: validateSelect,
22
22
  [CustomFieldDefinitionType.STATUS]: validateStatus,
23
- [CustomFieldDefinitionType.TEXT]: (value) => (typeof value === 'string'),
24
- [CustomFieldDefinitionType.NUMBER]: (value) => (typeof value === 'number'),
25
- [CustomFieldDefinitionType.BOOLEAN]: (value) => (typeof value === 'boolean'),
26
- [CustomFieldDefinitionType.DATE]: (value) => (!Joi.date().validate(value).error),
27
- [CustomFieldDefinitionType.DATETIME]: (value) => (!Joi.date().validate(value).error),
28
- [CustomFieldDefinitionType.IMAGE]: (value) => (!Joi.array().min(1).unique()
23
+ [CustomFieldDefinitionType.TEXT]: (value) => Joi.string().min(0).validate(value),
24
+ [CustomFieldDefinitionType.NUMBER]: (value) => Joi.number().strict(true).validate(value),
25
+ [CustomFieldDefinitionType.BOOLEAN]: (value) => Joi.boolean().strict().validate(value),
26
+ [CustomFieldDefinitionType.DATE]: (value) => Joi.date().validate(value),
27
+ [CustomFieldDefinitionType.DATETIME]: (value) => Joi.date().validate(value),
28
+ [CustomFieldDefinitionType.IMAGE]: (value) => Joi.array().min(1).unique()
29
29
  .items(Joi.string().uri())
30
- .validate(value).error),
31
- [CustomFieldDefinitionType.FILE]: (value) => (!Joi.array().min(1).unique().items(Joi.object({
30
+ .validate(value),
31
+ [CustomFieldDefinitionType.FILE]: (value) => Joi.array().min(1).unique().items(Joi.object({
32
32
  name: Joi.string().required(),
33
33
  type: Joi.string(),
34
34
  size: Joi.string(),
35
35
  addedBy: Joi.string().uuid(),
36
36
  }))
37
- .validate(value).error),
37
+ .validate(value),
38
38
  };
@@ -1,3 +1,4 @@
1
+ import Joi from 'joi';
1
2
  import type { Validator } from '../type';
2
3
 
3
4
  /**
@@ -6,6 +7,6 @@ import type { Validator } from '../type';
6
7
  export const validateSelect: Validator<string, string[]> = (
7
8
  value,
8
9
  selectValues,
9
- ) => (Array.isArray(selectValues)
10
- && selectValues.includes(value)
10
+ ) => (
11
+ Joi.string().allow(null).valid(...selectValues).validate(value)
11
12
  );
@@ -1,3 +1,4 @@
1
+ import Joi from 'joi';
1
2
  import type { Validator } from '../type';
2
3
 
3
4
  type StatusColor = string | null; // TODO: Takes from @autofleet/colors ?
@@ -13,6 +14,9 @@ type StatusOption = {
13
14
  export const validateStatus: Validator<StatusValue, StatusOption[]> = (
14
15
  value,
15
16
  statusValues,
16
- ) => (Array.isArray(statusValues)
17
- && statusValues.some((status) => status.value === value)
17
+ ) => (
18
+ Joi.string()
19
+ .allow(null)
20
+ .valid(...statusValues.map((statusValue) => statusValue.value))
21
+ .validate(value)
18
22
  );
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "include": [
4
+ "src/**/*",
5
+ ],
6
+ "exclude": ["node_modules", "src/tests/**", "**/*.test.ts"]
7
+ }
package/tsconfig.json CHANGED
@@ -12,5 +12,5 @@
12
12
  "include": [
13
13
  "src/**/*",
14
14
  ],
15
- "exclude": ["node_modules", "**/*.test.ts"]
15
+ "exclude": ["node_modules"]
16
16
  }