@autofleet/sadot 0.8.6 → 0.9.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.
@@ -1,5 +1,6 @@
1
1
  import { BadRequest } from '@autofleet/errors';
2
2
  import type { ValidationError } from 'joi';
3
+ import type { EntriesValidationError } from '../types/entries';
3
4
  export declare class MissingRequiredCustomFieldError extends BadRequest {
4
5
  constructor(missingFields: string[]);
5
6
  }
@@ -15,6 +16,9 @@ export declare class InvalidFieldTypeError extends BadRequest {
15
16
  export declare class InvalidValueError extends BadRequest {
16
17
  constructor(value: any, fieldDefinitionName: string, joiValidationError: ValidationError);
17
18
  }
19
+ export declare class InvalidEntriesError extends BadRequest {
20
+ constructor(modelId: string, validationErrors: EntriesValidationError[]);
21
+ }
18
22
  export declare class MissingDefinitionError extends BadRequest {
19
23
  constructor(fieldNames: string[]);
20
24
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.MissingDefinitionError = exports.InvalidValueError = exports.InvalidFieldTypeError = exports.UnsupportedCustomValidationError = exports.UnsupportedCustomFieldTypeError = exports.MissingRequiredCustomFieldError = void 0;
3
+ exports.MissingDefinitionError = exports.InvalidEntriesError = 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 {
@@ -48,6 +48,14 @@ class InvalidValueError extends errors_1.BadRequest {
48
48
  }
49
49
  }
50
50
  exports.InvalidValueError = InvalidValueError;
51
+ class InvalidEntriesError extends errors_1.BadRequest {
52
+ constructor(modelId, validationErrors) {
53
+ const errors = validationErrors.map((validationError) => new InvalidValueError(validationError.value, validationError.fieldDefinitionName, validationError.joiValidationError));
54
+ super(errors, null, null);
55
+ this.message = `Invalid entries on ${modelId}`;
56
+ }
57
+ }
58
+ exports.InvalidEntriesError = InvalidEntriesError;
51
59
  class MissingDefinitionError extends errors_1.BadRequest {
52
60
  constructor(fieldNames) {
53
61
  const err = new Error(`Missing custom field definition for field ${fieldNames.join(',')}`);
@@ -27,6 +27,10 @@ const modelTableMapping = {
27
27
  tableName: 'dim_custom_field_value',
28
28
  eventVersion: '1',
29
29
  },
30
+ CustomFieldEntries: {
31
+ tableName: 'dim_custom_field_entries',
32
+ eventVersion: '1',
33
+ },
30
34
  };
31
35
  const sendDimEvent = (instance) => {
32
36
  const mapping = modelTableMapping[instance.constructor.name];
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ __exportStar(require("./utils/helpers"), exports);
31
31
  * @see {@link 'custom-fields/config'} for configurations
32
32
  */
33
33
  const useCustomFields = async (app, getModel, options) => {
34
- const { models } = options;
34
+ const { models, useCustomFieldsEntries } = options;
35
35
  if (app) {
36
36
  app.use('/api', api_1.default);
37
37
  }
@@ -41,7 +41,7 @@ const useCustomFields = async (app, getModel, options) => {
41
41
  }
42
42
  // The order is important
43
43
  (0, init_1.addHooks)(models, getModel);
44
- await (0, models_1.initTables)(sequelize, options.getUser);
44
+ await (0, models_1.initTables)(sequelize, options.getUser, { useCustomFieldsEntries });
45
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);
@@ -0,0 +1,15 @@
1
+ import { Model } from 'sequelize-typescript';
2
+ declare class CustomFieldEntries extends Model {
3
+ /** The ID of the model of which this row hold the custom field entries of, e.g. vehicleId / stopPointId / etc. */
4
+ modelId: string;
5
+ /** The ID of the entity of which this row hold the custom field entries of, e.g. fleetId / etc. */
6
+ entityId: string;
7
+ /** A dictionary of customFields and values with the following structure: `{ customFieldName: 'CustomFieldValue' }` */
8
+ customFields: Record<string, any>;
9
+ /** The type of model which this custom field entry represents. e.g. Vehicle / StopPoint / etc. */
10
+ modelType: string;
11
+ createdAt?: Date;
12
+ updatedAt?: Date;
13
+ static afterSaveHandler(instance: CustomFieldEntries, options: any): void;
14
+ }
15
+ export default CustomFieldEntries;
@@ -0,0 +1,123 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
19
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
20
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
21
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
22
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
23
+ };
24
+ var __importStar = (this && this.__importStar) || function (mod) {
25
+ if (mod && mod.__esModule) return mod;
26
+ var result = {};
27
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
28
+ __setModuleDefault(result, mod);
29
+ return result;
30
+ };
31
+ var __metadata = (this && this.__metadata) || function (k, v) {
32
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
33
+ };
34
+ Object.defineProperty(exports, "__esModule", { value: true });
35
+ const sequelize_typescript_1 = require("sequelize-typescript");
36
+ const events_1 = require("../events");
37
+ const CustomFieldDefinitionRepo = __importStar(require("../repository/definition"));
38
+ const validations_1 = require("../utils/validations");
39
+ let CustomFieldEntries = class CustomFieldEntries extends sequelize_typescript_1.Model {
40
+ static afterSaveHandler(instance, options) {
41
+ if (options.transaction) {
42
+ options.transaction.afterCommit(() => (0, events_1.sendDimEvent)(instance[0]));
43
+ }
44
+ else {
45
+ (0, events_1.sendDimEvent)(instance[0]);
46
+ }
47
+ }
48
+ };
49
+ __decorate([
50
+ sequelize_typescript_1.PrimaryKey,
51
+ (0, sequelize_typescript_1.Column)({
52
+ type: sequelize_typescript_1.DataType.UUID,
53
+ allowNull: false,
54
+ })
55
+ /** The ID of the model of which this row hold the custom field entries of, e.g. vehicleId / stopPointId / etc. */
56
+ ,
57
+ __metadata("design:type", String)
58
+ ], CustomFieldEntries.prototype, "modelId", void 0);
59
+ __decorate([
60
+ (0, sequelize_typescript_1.Column)({
61
+ type: sequelize_typescript_1.DataType.UUID,
62
+ allowNull: false,
63
+ })
64
+ /** The ID of the entity of which this row hold the custom field entries of, e.g. fleetId / etc. */
65
+ ,
66
+ __metadata("design:type", String)
67
+ ], CustomFieldEntries.prototype, "entityId", void 0);
68
+ __decorate([
69
+ (0, sequelize_typescript_1.Column)({
70
+ type: sequelize_typescript_1.DataType.JSONB,
71
+ allowNull: false,
72
+ defaultValue: {},
73
+ })
74
+ /** A dictionary of customFields and values with the following structure: `{ customFieldName: 'CustomFieldValue' }` */
75
+ ,
76
+ __metadata("design:type", Object)
77
+ ], CustomFieldEntries.prototype, "customFields", void 0);
78
+ __decorate([
79
+ (0, sequelize_typescript_1.Column)({
80
+ type: sequelize_typescript_1.DataType.STRING,
81
+ allowNull: false,
82
+ })
83
+ /** The type of model which this custom field entry represents. e.g. Vehicle / StopPoint / etc. */
84
+ ,
85
+ __metadata("design:type", String)
86
+ ], CustomFieldEntries.prototype, "modelType", void 0);
87
+ __decorate([
88
+ sequelize_typescript_1.Column,
89
+ __metadata("design:type", Date)
90
+ ], CustomFieldEntries.prototype, "createdAt", void 0);
91
+ __decorate([
92
+ sequelize_typescript_1.Column,
93
+ __metadata("design:type", Date)
94
+ ], CustomFieldEntries.prototype, "updatedAt", void 0);
95
+ __decorate([
96
+ sequelize_typescript_1.AfterUpsert,
97
+ __metadata("design:type", Function),
98
+ __metadata("design:paramtypes", [CustomFieldEntries, Object]),
99
+ __metadata("design:returntype", void 0)
100
+ ], CustomFieldEntries, "afterSaveHandler", null);
101
+ CustomFieldEntries = __decorate([
102
+ (0, sequelize_typescript_1.Table)({
103
+ timestamps: true,
104
+ indexes: [
105
+ {
106
+ name: 'idx_cfe_custom_fields',
107
+ using: 'gin',
108
+ operator: 'jsonb_path_ops',
109
+ fields: ['custom_fields'],
110
+ },
111
+ ],
112
+ validate: {
113
+ async validationByType() {
114
+ if (!Object.keys(this.customFields ?? {}).length) {
115
+ return;
116
+ }
117
+ const definitionsByName = await CustomFieldDefinitionRepo.getCustomFieldDefinitionsDictionary([this]);
118
+ (0, validations_1.validateInstanceCustomFieldEntries)(this, definitionsByName);
119
+ },
120
+ },
121
+ })
122
+ ], CustomFieldEntries);
123
+ exports.default = CustomFieldEntries;
@@ -6,9 +6,12 @@ import ContextAwareTestModel from './tests/contextAwareModels/ContextAwareTestMo
6
6
  import ContextTestModel from './tests/contextAwareModels/ContextTestModel';
7
7
  import AssociatedTestModel from './tests/AssociatedTestModel';
8
8
  import type { CustomFieldOptions } from '../types';
9
- declare const initTables: (sequelize: Sequelize, getUser: CustomFieldOptions['getUser'], { schemaPrefix, schemaVersion }?: {
10
- schemaPrefix: string;
11
- schemaVersion: string;
12
- }) => Promise<void>;
9
+ import CustomFieldEntries from './CustomFieldEntries';
10
+ interface InitTablesOptions {
11
+ schemaPrefix?: string;
12
+ schemaVersion?: string;
13
+ useCustomFieldsEntries?: boolean;
14
+ }
15
+ declare const initTables: (sequelize: Sequelize, getUser: CustomFieldOptions['getUser'], { schemaPrefix, schemaVersion, useCustomFieldsEntries, }?: InitTablesOptions) => Promise<void>;
13
16
  declare const initTestModels: (sequelize: Sequelize) => Promise<void>;
14
- export { CustomFieldValue, CustomFieldDefinition, TestModel, AssociatedTestModel, ContextAwareTestModel, ContextTestModel, initTables, initTestModels, };
17
+ export { CustomFieldValue, CustomFieldDefinition, CustomFieldEntries, TestModel, AssociatedTestModel, ContextAwareTestModel, ContextTestModel, initTables, initTestModels, };
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.initTestModels = exports.initTables = exports.ContextTestModel = exports.ContextAwareTestModel = exports.AssociatedTestModel = exports.TestModel = exports.CustomFieldDefinition = exports.CustomFieldValue = void 0;
6
+ exports.initTestModels = exports.initTables = exports.ContextTestModel = exports.ContextAwareTestModel = exports.AssociatedTestModel = exports.TestModel = exports.CustomFieldEntries = exports.CustomFieldDefinition = exports.CustomFieldValue = void 0;
7
7
  /* eslint-disable no-param-reassign */
8
8
  const sequelize_1 = require("sequelize");
9
9
  const logger_1 = __importDefault(require("../utils/logger"));
@@ -19,18 +19,27 @@ const ContextTestModel_1 = __importDefault(require("./tests/contextAwareModels/C
19
19
  exports.ContextTestModel = ContextTestModel_1.default;
20
20
  const AssociatedTestModel_1 = __importDefault(require("./tests/AssociatedTestModel"));
21
21
  exports.AssociatedTestModel = AssociatedTestModel_1.default;
22
+ const CustomFieldEntries_1 = __importDefault(require("./CustomFieldEntries"));
23
+ exports.CustomFieldEntries = CustomFieldEntries_1.default;
22
24
  const productionModels = [CustomFieldDefinition_1.default, CustomFieldValue_1.default];
23
25
  const testModels = [TestModel_1.default, AssociatedTestModel_1.default, ContextAwareTestModel_1.default, ContextTestModel_1.default];
24
26
  const SADOT_MIGRATION_PREFIX = 'sadot-migration';
25
27
  const SCHEMA_VERSION = 'fb0fa867-1241-4816-b08d-5ed9060c7ae5';
26
- const initTables = async (sequelize, getUser, { schemaPrefix, schemaVersion } = { schemaPrefix: SADOT_MIGRATION_PREFIX, schemaVersion: SCHEMA_VERSION }) => {
27
- const CUSTOM_FIELDS_SCHEMA_VERSION = `${schemaPrefix}_${schemaVersion}`;
28
+ const initTables = async (sequelize, getUser, { schemaPrefix, schemaVersion, useCustomFieldsEntries, } = {
29
+ schemaPrefix: SADOT_MIGRATION_PREFIX,
30
+ schemaVersion: SCHEMA_VERSION,
31
+ useCustomFieldsEntries: false,
32
+ }) => {
33
+ const CUSTOM_FIELDS_SCHEMA_VERSION = `${schemaPrefix}_${schemaVersion}${useCustomFieldsEntries ? '_withEntries' : ''}`;
28
34
  logger_1.default.info('custom-fields: initialize custom-fields tables');
29
35
  // Detect models and import them to the orm
30
36
  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
31
37
  if (!sequelize.addModels) {
32
38
  throw new Error('sequelize instance must have addModels function');
33
39
  }
40
+ if (useCustomFieldsEntries) {
41
+ productionModels.push(CustomFieldEntries_1.default);
42
+ }
34
43
  sequelize.addModels(productionModels);
35
44
  CustomFieldDefinition_1.default.addScope('userScope', () => {
36
45
  const user = getUser();
@@ -67,6 +76,10 @@ const initTables = async (sequelize, getUser, { schemaPrefix, schemaVersion } =
67
76
  if (!currentSadotSchemaVersion || currentSadotSchemaVersion.name !== CUSTOM_FIELDS_SCHEMA_VERSION) {
68
77
  await CustomFieldDefinition_1.default.sync({ alter: true });
69
78
  await CustomFieldValue_1.default.sync({ alter: true });
79
+ // T.Y TODO: Remove the if statement once we're ready to add the new entries table for all MS
80
+ if (useCustomFieldsEntries) {
81
+ await CustomFieldEntries_1.default.sync({ alter: true });
82
+ }
70
83
  if (expectedSchemaVersionIndex === -1) {
71
84
  await SequelizeMeta.create({ name: CUSTOM_FIELDS_SCHEMA_VERSION });
72
85
  }
@@ -1,5 +1,5 @@
1
1
  import { type Includeable, type Transaction, type FindOptions, type WhereOptions } from 'sequelize';
2
- import { CustomFieldDefinition } from '../models';
2
+ import { CustomFieldDefinition, type CustomFieldEntries } from '../models';
3
3
  import type { CreateCustomFieldDefinition, UpdateCustomFieldDefinition } from '../types/definition';
4
4
  import type { ModelOptions } from '../types';
5
5
  export declare const create: (data: CreateCustomFieldDefinition) => Promise<CustomFieldDefinition>;
@@ -8,6 +8,9 @@ interface SadotFindOptions {
8
8
  transaction?: Transaction;
9
9
  include?: Includeable | Includeable[];
10
10
  }
11
+ type SadotGetDefinitionsByEntityIdsOptions = FindOptions & {
12
+ modelOptions?: ModelOptions;
13
+ } & Pick<SadotFindOptions, 'withDisabled'>;
11
14
  export declare const findAll: (where: WhereOptions, options?: SadotFindOptions) => Promise<CustomFieldDefinition[]>;
12
15
  export declare const findByIds: (ids: string[], options?: SadotFindOptions) => Promise<CustomFieldDefinition[]>;
13
16
  export declare const findById: (id: string, options?: Pick<SadotFindOptions, 'withDisabled'>) => Promise<CustomFieldDefinition | null>;
@@ -23,4 +26,11 @@ export declare const destroy: (id: string) => Promise<number>;
23
26
  * Return the names of the required fields for a given model
24
27
  */
25
28
  export declare const getRequiredFields: (modelType: string, modelId: string | string[], entityId: string | string[], modelOptions?: ModelOptions) => Promise<string[]>;
29
+ /**
30
+ * @returns A promise resolving with a dictionary of custom field definitions by name.
31
+ * @throws A {@link MissingDefinitionError} if any of the custom fields doesn't have a definition.
32
+ */
33
+ export declare const getCustomFieldDefinitionsDictionary: (instances: CustomFieldEntries[], options?: SadotGetDefinitionsByEntityIdsOptions) => Promise<{
34
+ [definitionName: string]: CustomFieldDefinition;
35
+ }>;
26
36
  export {};
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getRequiredFields = exports.destroy = exports.disable = exports.update = exports.findDefinitionsByModels = exports.findByWhere = exports.findByEntityIds = exports.findById = exports.findByIds = exports.findAll = exports.create = void 0;
3
+ exports.getCustomFieldDefinitionsDictionary = exports.getRequiredFields = exports.destroy = exports.disable = exports.update = exports.findDefinitionsByModels = exports.findByWhere = exports.findByEntityIds = exports.findById = exports.findByIds = exports.findAll = exports.create = void 0;
4
4
  const sequelize_1 = require("sequelize");
5
5
  const models_1 = require("../models");
6
+ const errors_1 = require("../errors");
6
7
  const create = (data) => models_1.CustomFieldDefinition.create(data);
7
8
  exports.create = create;
8
9
  const findAll = (where, options = { withDisabled: false }) => {
@@ -86,3 +87,30 @@ const getRequiredFields = async (modelType, modelId, entityId, modelOptions = {}
86
87
  return [...new Set(requiredFieldsNames)];
87
88
  };
88
89
  exports.getRequiredFields = getRequiredFields;
90
+ /**
91
+ * @returns A promise resolving with a dictionary of custom field definitions by name.
92
+ * @throws A {@link MissingDefinitionError} if any of the custom fields doesn't have a definition.
93
+ */
94
+ const getCustomFieldDefinitionsDictionary = async (instances, options = { withDisabled: false, modelOptions: {} }) => {
95
+ const { modelType } = instances[0]?.dataValues ?? {};
96
+ const customFields = new Set();
97
+ const modelIds = [];
98
+ const entityIds = new Set();
99
+ instances.forEach((instance) => {
100
+ const { dataValues: { modelId, entityId, customFields: instanceCustomFields } } = instance;
101
+ modelIds.push(modelId);
102
+ entityIds.add(entityId);
103
+ Object.keys(instanceCustomFields ?? {}).forEach((fieldName) => {
104
+ customFields.add(fieldName);
105
+ });
106
+ });
107
+ const definitions = await (0, exports.findByEntityIds)(modelType, Array.from(entityIds), { ...options });
108
+ const matchedDefinitions = definitions.filter((def) => customFields.has(def.name));
109
+ const matchedDefinitionsByName = Object.fromEntries(matchedDefinitions.map((definition) => [definition.name, definition]));
110
+ if (!definitions?.length || matchedDefinitions.length !== customFields.size) {
111
+ const unmatchedCustomFields = Array.from(customFields).filter((customField) => !matchedDefinitionsByName[customField]);
112
+ throw new errors_1.MissingDefinitionError(unmatchedCustomFields);
113
+ }
114
+ return matchedDefinitionsByName;
115
+ };
116
+ exports.getCustomFieldDefinitionsDictionary = getCustomFieldDefinitionsDictionary;
@@ -16,9 +16,9 @@ export declare const coolFieldDefinition2: {
16
16
  deletedAt?: Date;
17
17
  defaultValue?: any;
18
18
  blockEditingFromUI?: boolean;
19
- displayName?: string;
20
19
  validation?: any;
21
20
  fieldType: string;
21
+ displayName?: string;
22
22
  entityId: string;
23
23
  entityType: string;
24
24
  modelType: string;
@@ -33,9 +33,9 @@ export declare const coolFieldDefinition3: {
33
33
  deletedAt?: Date;
34
34
  defaultValue?: any;
35
35
  blockEditingFromUI?: boolean;
36
- displayName?: string;
37
36
  validation?: any;
38
37
  fieldType: string;
38
+ displayName?: string;
39
39
  entityId: string;
40
40
  entityType: string;
41
41
  modelType: string;
@@ -1,3 +1,4 @@
1
1
  export declare const mockEvent: (events: any, eventName: any, numberOfEvents: any) => any[];
2
2
  export declare const mockDimCustomFieldDefinitionEvent: (events: any, numberOfEvents: any) => any[];
3
3
  export declare const mockDimCustomFieldValueEvent: (events: any, numberOfEvents: any) => any[];
4
+ export declare const mockDimCustomFieldEntriesEvent: (events: any, numberOfEvents: any) => any[];
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  /* eslint-disable no-param-reassign */
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.mockDimCustomFieldValueEvent = exports.mockDimCustomFieldDefinitionEvent = exports.mockEvent = void 0;
4
+ exports.mockDimCustomFieldEntriesEvent = exports.mockDimCustomFieldValueEvent = exports.mockDimCustomFieldDefinitionEvent = exports.mockEvent = void 0;
5
5
  const mockEvent = (events, eventName, numberOfEvents) => {
6
6
  events.sendObject = jest.fn();
7
7
  return [
@@ -17,3 +17,5 @@ const mockDimCustomFieldDefinitionEvent = (events, numberOfEvents) => (0, export
17
17
  exports.mockDimCustomFieldDefinitionEvent = mockDimCustomFieldDefinitionEvent;
18
18
  const mockDimCustomFieldValueEvent = (events, numberOfEvents) => (0, exports.mockEvent)(events, 'dim_custom_field_value', numberOfEvents);
19
19
  exports.mockDimCustomFieldValueEvent = mockDimCustomFieldValueEvent;
20
+ const mockDimCustomFieldEntriesEvent = (events, numberOfEvents) => (0, exports.mockEvent)(events, 'dim_custom_field_entries', numberOfEvents);
21
+ exports.mockDimCustomFieldEntriesEvent = mockDimCustomFieldEntriesEvent;
@@ -0,0 +1,25 @@
1
+ import type { ValidationError } from 'joi';
2
+ export interface CustomFieldEntriesDTO {
3
+ modelId: string;
4
+ entityId: string;
5
+ modelType: string;
6
+ /**
7
+ * A collection of custom fields where each key is the name of a CustomFieldDefinition and
8
+ * each value is the value of that custom field for this specific `modelId`.
9
+ *
10
+ * Example:
11
+ * {
12
+ * "vehicleColor": "Red",
13
+ * "vehicleType": "premium",
14
+ * "isActive": true
15
+ * }
16
+ */
17
+ customFields: Record<string, any>;
18
+ createdAt?: Date;
19
+ updatedAt?: Date;
20
+ }
21
+ export interface EntriesValidationError {
22
+ value: any;
23
+ fieldDefinitionName: string;
24
+ joiValidationError: ValidationError;
25
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -30,4 +30,5 @@ export type CustomFieldOptions = {
30
30
  databaseConfig: any;
31
31
  getUser: typeof GetUserType;
32
32
  sequelize?: Sequelize;
33
+ useCustomFieldsEntries?: boolean;
33
34
  };
@@ -1,3 +1,8 @@
1
+ import type { CustomFieldDefinition } from '../../models';
2
+ import type { CustomFieldEntriesDTO } from '../../types/entries';
1
3
  import type { CustomFieldDefinitionType } from '../constants';
2
4
  export declare const validateFieldType: (type: CustomFieldDefinitionType) => boolean;
3
5
  export declare const validateValue: (value: unknown, valueType: CustomFieldDefinitionType, validation?: unknown) => import("joi").ValidationResult;
6
+ export declare const validateInstanceCustomFieldEntries: (instance: CustomFieldEntriesDTO, definitionsByName: {
7
+ [defName: string]: CustomFieldDefinition;
8
+ }) => void;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.validateValue = exports.validateFieldType = void 0;
3
+ exports.validateInstanceCustomFieldEntries = exports.validateValue = exports.validateFieldType = void 0;
4
+ const errors_1 = require("../../errors");
4
5
  const validators_1 = require("./validators");
5
6
  const validateFieldType = (type) => Object.keys(validators_1.validators).includes(type);
6
7
  exports.validateFieldType = validateFieldType;
@@ -15,3 +16,23 @@ const validateValue = (value, valueType, validation) => {
15
16
  */
16
17
  };
17
18
  exports.validateValue = validateValue;
19
+ const validateInstanceCustomFieldEntries = (instance, definitionsByName) => {
20
+ const validationErrors = Object.entries(instance.customFields)
21
+ .map(([customFieldName, value]) => {
22
+ const { validation, fieldType } = definitionsByName[customFieldName];
23
+ const result = (0, exports.validateValue)(value, fieldType, validation);
24
+ if (result?.error) {
25
+ return {
26
+ joiValidationError: result.error,
27
+ fieldDefinitionName: customFieldName,
28
+ value,
29
+ };
30
+ }
31
+ return null;
32
+ })
33
+ .filter((result) => !!result);
34
+ if (validationErrors?.length) {
35
+ throw new errors_1.InvalidEntriesError(instance.modelId, validationErrors);
36
+ }
37
+ };
38
+ exports.validateInstanceCustomFieldEntries = validateInstanceCustomFieldEntries;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autofleet/sadot",
3
- "version": "0.8.6",
3
+ "version": "0.9.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable max-classes-per-file */
2
2
  import { BadRequest } from '@autofleet/errors';
3
3
  import type { ValidationError } from 'joi';
4
+ import type { EntriesValidationError } from '../types/entries';
4
5
 
5
6
  export class MissingRequiredCustomFieldError extends BadRequest {
6
7
  constructor(missingFields: string[]) {
@@ -50,6 +51,14 @@ export class InvalidValueError extends BadRequest {
50
51
  }
51
52
  }
52
53
 
54
+ export class InvalidEntriesError extends BadRequest {
55
+ constructor(modelId: string, validationErrors: EntriesValidationError[]) {
56
+ const errors = validationErrors.map((validationError) => new InvalidValueError(validationError.value, validationError.fieldDefinitionName, validationError.joiValidationError));
57
+ super(errors, null, null);
58
+ this.message = `Invalid entries on ${modelId}`;
59
+ }
60
+ }
61
+
53
62
  export class MissingDefinitionError extends BadRequest {
54
63
  constructor(fieldNames: string[]) {
55
64
  const err = new Error(`Missing custom field definition for field ${fieldNames.join(',')}`);
@@ -25,6 +25,10 @@ const modelTableMapping = {
25
25
  tableName: 'dim_custom_field_value',
26
26
  eventVersion: '1',
27
27
  },
28
+ CustomFieldEntries: {
29
+ tableName: 'dim_custom_field_entries',
30
+ eventVersion: '1',
31
+ },
28
32
  };
29
33
 
30
34
  export const sendDimEvent = (instance): void => {
package/src/index.ts CHANGED
@@ -26,7 +26,7 @@ const useCustomFields = async (
26
26
  getModel: ModelFetcher,
27
27
  options: CustomFieldOptions,
28
28
  ): Promise<Sequelize> => {
29
- const { models } = options;
29
+ const { models, useCustomFieldsEntries } = options;
30
30
  if (app) {
31
31
  app.use('/api', api);
32
32
  }
@@ -36,7 +36,7 @@ const useCustomFields = async (
36
36
  }
37
37
  // The order is important
38
38
  addHooks(models, getModel);
39
- await initTables(sequelize, options.getUser);
39
+ await initTables(sequelize, options.getUser, { useCustomFieldsEntries });
40
40
  addScopes(models, getModel);
41
41
  applyCustomAssociation(models);
42
42
  logger.debug('sadot - custom fields finished initializing with models', models);
@@ -0,0 +1,81 @@
1
+ import {
2
+ Table,
3
+ Column,
4
+ Model,
5
+ PrimaryKey,
6
+ DataType,
7
+ AfterUpsert,
8
+ } from 'sequelize-typescript';
9
+ import { sendDimEvent } from '../events';
10
+ import * as CustomFieldDefinitionRepo from '../repository/definition';
11
+ import { validateInstanceCustomFieldEntries } from '../utils/validations';
12
+
13
+ @Table({
14
+ timestamps: true,
15
+ indexes: [
16
+ {
17
+ name: 'idx_cfe_custom_fields',
18
+ using: 'gin',
19
+ operator: 'jsonb_path_ops',
20
+ fields: ['custom_fields'],
21
+ },
22
+ ],
23
+ validate: {
24
+ async validationByType(this: CustomFieldEntries) {
25
+ if (!Object.keys(this.customFields ?? {}).length) {
26
+ return;
27
+ }
28
+
29
+ const definitionsByName = await CustomFieldDefinitionRepo.getCustomFieldDefinitionsDictionary([this]);
30
+ validateInstanceCustomFieldEntries(this, definitionsByName);
31
+ },
32
+ },
33
+ })
34
+ class CustomFieldEntries extends Model {
35
+ @PrimaryKey
36
+ @Column({
37
+ type: DataType.UUID,
38
+ allowNull: false,
39
+ })
40
+ /** The ID of the model of which this row hold the custom field entries of, e.g. vehicleId / stopPointId / etc. */
41
+ modelId!: string;
42
+
43
+ @Column({
44
+ type: DataType.UUID,
45
+ allowNull: false,
46
+ })
47
+ /** The ID of the entity of which this row hold the custom field entries of, e.g. fleetId / etc. */
48
+ entityId!: string;
49
+
50
+ @Column({
51
+ type: DataType.JSONB,
52
+ allowNull: false,
53
+ defaultValue: {},
54
+ })
55
+ /** A dictionary of customFields and values with the following structure: `{ customFieldName: 'CustomFieldValue' }` */
56
+ customFields!: Record<string, any>;
57
+
58
+ @Column({
59
+ type: DataType.STRING,
60
+ allowNull: false,
61
+ })
62
+ /** The type of model which this custom field entry represents. e.g. Vehicle / StopPoint / etc. */
63
+ modelType!: string;
64
+
65
+ @Column
66
+ createdAt?: Date;
67
+
68
+ @Column
69
+ updatedAt?: Date;
70
+
71
+ @AfterUpsert
72
+ static afterSaveHandler(instance: CustomFieldEntries, options): void {
73
+ if (options.transaction) {
74
+ options.transaction.afterCommit(() => sendDimEvent(instance[0]));
75
+ } else {
76
+ sendDimEvent(instance[0]);
77
+ }
78
+ }
79
+ }
80
+
81
+ export default CustomFieldEntries;
@@ -9,8 +9,16 @@ import ContextAwareTestModel from './tests/contextAwareModels/ContextAwareTestMo
9
9
  import ContextTestModel from './tests/contextAwareModels/ContextTestModel';
10
10
  import AssociatedTestModel from './tests/AssociatedTestModel';
11
11
  import type { CustomFieldOptions } from '../types';
12
+ import CustomFieldEntries from './CustomFieldEntries';
12
13
 
13
- const productionModels = [CustomFieldDefinition, CustomFieldValue];
14
+ type ProductionModel = typeof CustomFieldDefinition | typeof CustomFieldValue | typeof CustomFieldEntries
15
+ interface InitTablesOptions {
16
+ schemaPrefix?: string
17
+ schemaVersion?: string
18
+ useCustomFieldsEntries?: boolean
19
+ }
20
+
21
+ const productionModels: ProductionModel[] = [CustomFieldDefinition, CustomFieldValue];
14
22
  const testModels = [TestModel, AssociatedTestModel, ContextAwareTestModel, ContextTestModel];
15
23
 
16
24
  const SADOT_MIGRATION_PREFIX = 'sadot-migration';
@@ -19,15 +27,28 @@ const SCHEMA_VERSION = 'fb0fa867-1241-4816-b08d-5ed9060c7ae5';
19
27
  const initTables = async (
20
28
  sequelize: Sequelize,
21
29
  getUser: CustomFieldOptions['getUser'],
22
- { schemaPrefix, schemaVersion } = { schemaPrefix: SADOT_MIGRATION_PREFIX, schemaVersion: SCHEMA_VERSION },
30
+ {
31
+ schemaPrefix,
32
+ schemaVersion,
33
+ useCustomFieldsEntries,
34
+ }: InitTablesOptions = {
35
+ schemaPrefix: SADOT_MIGRATION_PREFIX,
36
+ schemaVersion: SCHEMA_VERSION,
37
+ useCustomFieldsEntries: false,
38
+ },
23
39
  ): Promise<void> => {
24
- const CUSTOM_FIELDS_SCHEMA_VERSION = `${schemaPrefix}_${schemaVersion}`;
40
+ const CUSTOM_FIELDS_SCHEMA_VERSION = `${schemaPrefix}_${schemaVersion}${useCustomFieldsEntries ? '_withEntries' : ''}`;
25
41
  logger.info('custom-fields: initialize custom-fields tables');
26
42
  // Detect models and import them to the orm
27
43
  // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
28
44
  if (!sequelize.addModels) {
29
45
  throw new Error('sequelize instance must have addModels function');
30
46
  }
47
+
48
+ if (useCustomFieldsEntries) {
49
+ productionModels.push(CustomFieldEntries);
50
+ }
51
+
31
52
  sequelize.addModels(productionModels);
32
53
 
33
54
  CustomFieldDefinition.addScope('userScope', () => {
@@ -73,6 +94,11 @@ const initTables = async (
73
94
  if (!currentSadotSchemaVersion || (currentSadotSchemaVersion as any).name !== CUSTOM_FIELDS_SCHEMA_VERSION) {
74
95
  await CustomFieldDefinition.sync({ alter: true });
75
96
  await CustomFieldValue.sync({ alter: true });
97
+ // T.Y TODO: Remove the if statement once we're ready to add the new entries table for all MS
98
+ if (useCustomFieldsEntries) {
99
+ await CustomFieldEntries.sync({ alter: true });
100
+ }
101
+
76
102
  if (expectedSchemaVersionIndex === -1) {
77
103
  await SequelizeMeta.create({ name: CUSTOM_FIELDS_SCHEMA_VERSION });
78
104
  }
@@ -109,6 +135,7 @@ const initTestModels = async (sequelize: Sequelize): Promise<void> => {
109
135
  export {
110
136
  CustomFieldValue,
111
137
  CustomFieldDefinition,
138
+ CustomFieldEntries,
112
139
  TestModel,
113
140
  AssociatedTestModel,
114
141
  ContextAwareTestModel,
@@ -2,9 +2,10 @@ import {
2
2
  Op,
3
3
  type Includeable, type Transaction, type FindOptions, type WhereOptions,
4
4
  } from 'sequelize';
5
- import { CustomFieldDefinition } from '../models';
5
+ import { CustomFieldDefinition, type CustomFieldEntries } from '../models';
6
6
  import type { CreateCustomFieldDefinition, UpdateCustomFieldDefinition } from '../types/definition';
7
7
  import type { ModelOptions } from '../types';
8
+ import { MissingDefinitionError } from '../errors';
8
9
 
9
10
  export const create = (data: CreateCustomFieldDefinition): Promise<CustomFieldDefinition> =>
10
11
  CustomFieldDefinition.create(data);
@@ -15,6 +16,8 @@ interface SadotFindOptions {
15
16
  include?: Includeable | Includeable[];
16
17
  }
17
18
 
19
+ type SadotGetDefinitionsByEntityIdsOptions = FindOptions & { modelOptions?: ModelOptions } & Pick<SadotFindOptions, 'withDisabled'>;
20
+
18
21
  export const findAll = (
19
22
  where: WhereOptions,
20
23
  options: SadotFindOptions = { withDisabled: false },
@@ -129,3 +132,38 @@ export const getRequiredFields = async (
129
132
  const requiredFieldsNames = requiredFields.map((definition) => definition.name);
130
133
  return [...new Set(requiredFieldsNames)];
131
134
  };
135
+
136
+ /**
137
+ * @returns A promise resolving with a dictionary of custom field definitions by name.
138
+ * @throws A {@link MissingDefinitionError} if any of the custom fields doesn't have a definition.
139
+ */
140
+ export const getCustomFieldDefinitionsDictionary = async (
141
+ instances: CustomFieldEntries[],
142
+ options: SadotGetDefinitionsByEntityIdsOptions = { withDisabled: false, modelOptions: {} },
143
+ ): Promise<{ [definitionName: string]: CustomFieldDefinition }> => {
144
+ const { modelType } = instances[0]?.dataValues ?? {};
145
+ const customFields = new Set<string>();
146
+ const modelIds = [];
147
+ const entityIds = new Set<string>();
148
+ instances.forEach((instance) => {
149
+ const { dataValues: { modelId, entityId, customFields: instanceCustomFields } } = instance;
150
+ modelIds.push(modelId);
151
+ entityIds.add(entityId);
152
+
153
+ Object.keys(instanceCustomFields ?? {}).forEach((fieldName) => {
154
+ customFields.add(fieldName);
155
+ });
156
+ });
157
+
158
+ const definitions = await findByEntityIds(modelType, Array.from(entityIds), { ...options });
159
+
160
+ const matchedDefinitions = definitions.filter((def) => customFields.has(def.name));
161
+ const matchedDefinitionsByName = Object.fromEntries(matchedDefinitions.map((definition) => [definition.name, definition]));
162
+
163
+ if (!definitions?.length || matchedDefinitions.length !== customFields.size) {
164
+ const unmatchedCustomFields = Array.from(customFields).filter((customField) => !matchedDefinitionsByName[customField]);
165
+ throw new MissingDefinitionError(unmatchedCustomFields);
166
+ }
167
+
168
+ return matchedDefinitionsByName;
169
+ };
@@ -16,3 +16,6 @@ export const mockDimCustomFieldDefinitionEvent = (events, numberOfEvents) =>
16
16
 
17
17
  export const mockDimCustomFieldValueEvent = (events, numberOfEvents) =>
18
18
  mockEvent(events, 'dim_custom_field_value', numberOfEvents);
19
+
20
+ export const mockDimCustomFieldEntriesEvent = (events, numberOfEvents) =>
21
+ mockEvent(events, 'dim_custom_field_entries', numberOfEvents);
@@ -0,0 +1,27 @@
1
+ import type { ValidationError } from 'joi';
2
+
3
+ export interface CustomFieldEntriesDTO {
4
+ modelId: string;
5
+ entityId: string;
6
+ modelType: string;
7
+ /**
8
+ * A collection of custom fields where each key is the name of a CustomFieldDefinition and
9
+ * each value is the value of that custom field for this specific `modelId`.
10
+ *
11
+ * Example:
12
+ * {
13
+ * "vehicleColor": "Red",
14
+ * "vehicleType": "premium",
15
+ * "isActive": true
16
+ * }
17
+ */
18
+ customFields: Record<string, any>;
19
+ createdAt?: Date;
20
+ updatedAt?: Date;
21
+ }
22
+
23
+ export interface EntriesValidationError {
24
+ value: any,
25
+ fieldDefinitionName: string,
26
+ joiValidationError: ValidationError
27
+ }
@@ -34,4 +34,5 @@ export type CustomFieldOptions = {
34
34
  databaseConfig: any;
35
35
  getUser: typeof GetUserType;
36
36
  sequelize?: Sequelize;
37
+ useCustomFieldsEntries?: boolean;
37
38
  };
@@ -1,3 +1,6 @@
1
+ import { InvalidEntriesError } from '../../errors';
2
+ import type { CustomFieldDefinition } from '../../models';
3
+ import type { CustomFieldEntriesDTO } from '../../types/entries';
1
4
  import type { CustomFieldDefinitionType } from '../constants';
2
5
  import { validators } from './validators';
3
6
 
@@ -17,3 +20,24 @@ export const validateValue = (
17
20
  * }
18
21
  */
19
22
  };
23
+
24
+ export const validateInstanceCustomFieldEntries = (instance: CustomFieldEntriesDTO, definitionsByName: { [defName: string]: CustomFieldDefinition; }) => {
25
+ const validationErrors = Object.entries(instance.customFields)
26
+ .map(([customFieldName, value]) => {
27
+ const { validation, fieldType } = definitionsByName[customFieldName];
28
+ const result = validateValue(value, fieldType, validation);
29
+ if (result?.error) {
30
+ return {
31
+ joiValidationError: result.error,
32
+ fieldDefinitionName: customFieldName,
33
+ value,
34
+ };
35
+ }
36
+ return null;
37
+ })
38
+ .filter((result) => !!result);
39
+
40
+ if (validationErrors?.length) {
41
+ throw new InvalidEntriesError(instance.modelId, validationErrors);
42
+ }
43
+ };