@autofleet/sadot 0.9.1 → 0.10.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.
@@ -52,7 +52,7 @@ class InvalidEntriesError extends errors_1.BadRequest {
52
52
  constructor(modelId, validationErrors) {
53
53
  const errors = validationErrors.map((validationError) => new InvalidValueError(validationError.value, validationError.fieldDefinitionName, validationError.joiValidationError));
54
54
  super(errors, null, null);
55
- this.message = `Invalid entries on ${modelId}`;
55
+ this.message = `Invalid entries on ${modelId}\n${validationErrors.map((validationError) => (`${validationError.fieldDefinitionName} - ${validationError.joiValidationError.message}`)).join('\n')}`;
56
56
  }
57
57
  }
58
58
  exports.InvalidEntriesError = InvalidEntriesError;
@@ -1,4 +1,4 @@
1
- import type { ModelOptions } from '../types';
1
+ import type { CustomFieldOptions, ModelOptions } from '../types';
2
2
  /**
3
3
  * A hook to create the custom fields when updating a model (more then one instance).
4
4
  */
@@ -7,4 +7,4 @@ export declare const beforeBulkCreate: (options: any) => void;
7
7
  * A hook to create the custom fields when updating a model instance.
8
8
  * TODO - cleanup if update fail
9
9
  */
10
- export declare const beforeCreate: (scopeAttributes: string[], modelOptions?: ModelOptions) => (instance: any, options: any) => Promise<void>;
10
+ export declare const beforeCreate: (scopeAttributes: string[], modelOptions?: ModelOptions, sadotOptions?: Pick<CustomFieldOptions, 'useCustomFieldsEntries'>) => (instance: any, options: any) => Promise<void>;
@@ -28,10 +28,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
28
28
  Object.defineProperty(exports, "__esModule", { value: true });
29
29
  exports.beforeCreate = exports.beforeBulkCreate = void 0;
30
30
  const logger_1 = __importDefault(require("../utils/logger"));
31
- const ValueRepo = __importStar(require("../repository/value"));
32
31
  const DefinitionRepo = __importStar(require("../repository/definition"));
33
32
  const errors_1 = require("../errors");
34
33
  const scopeAttributes_1 = __importDefault(require("../utils/scopeAttributes"));
34
+ const updateInstanceValues_1 = __importDefault(require("./utils/updateInstanceValues"));
35
35
  /**
36
36
  * A hook to create the custom fields when updating a model (more then one instance).
37
37
  */
@@ -45,7 +45,7 @@ exports.beforeBulkCreate = beforeBulkCreate;
45
45
  * A hook to create the custom fields when updating a model instance.
46
46
  * TODO - cleanup if update fail
47
47
  */
48
- const beforeCreate = (scopeAttributes, modelOptions = {}) => async (instance, options) => {
48
+ const beforeCreate = (scopeAttributes, modelOptions = {}, sadotOptions = { useCustomFieldsEntries: false }) => async (instance, options) => {
49
49
  logger_1.default.debug('sadot - before create hook');
50
50
  const { fields } = options;
51
51
  const { include, useEntityIdFromInclude } = modelOptions;
@@ -80,9 +80,16 @@ const beforeCreate = (scopeAttributes, modelOptions = {}) => async (instance, op
80
80
  if (missingFields?.length > 0) {
81
81
  throw new errors_1.MissingRequiredCustomFieldError(missingFields);
82
82
  }
83
- await ValueRepo.updateValues(modelType, instance.id, identifiers, customFields, {
84
- transaction: options.transaction,
85
- modelOptions,
83
+ await (0, updateInstanceValues_1.default)({
84
+ modelId: instance.id,
85
+ modelType,
86
+ identifiers,
87
+ customFields,
88
+ options: {
89
+ useCustomFieldsEntries: sadotOptions.useCustomFieldsEntries,
90
+ transaction: options.transaction,
91
+ modelOptions,
92
+ },
86
93
  });
87
94
  // eslint-disable-next-line no-param-reassign
88
95
  fields.splice(customFieldsIdx, 1);
@@ -1,4 +1,4 @@
1
- import type { ModelOptions } from '../types';
1
+ import type { CustomFieldOptions, ModelOptions } from '../types';
2
2
  /**
3
3
  * A hook to update the custom fields when updating a model (more then one instance).
4
4
  */
@@ -7,4 +7,4 @@ export declare const beforeBulkUpdate: (options: any) => void;
7
7
  * A hook to update the custom fields when updating a model instance.
8
8
  * TODO - cleanup if update fail
9
9
  */
10
- export declare const beforeUpdate: (scopeAttributes: string[], modelOptions?: ModelOptions) => (instance: any, options: any) => Promise<void>;
10
+ export declare const beforeUpdate: (scopeAttributes: string[], modelOptions?: ModelOptions, sadotOptions?: Pick<CustomFieldOptions, 'useCustomFieldsEntries'>) => (instance: any, options: any) => Promise<void>;
@@ -1,35 +1,12 @@
1
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 __importStar = (this && this.__importStar) || function (mod) {
19
- if (mod && mod.__esModule) return mod;
20
- var result = {};
21
- if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
- __setModuleDefault(result, mod);
23
- return result;
24
- };
25
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
26
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
27
4
  };
28
5
  Object.defineProperty(exports, "__esModule", { value: true });
29
6
  exports.beforeUpdate = exports.beforeBulkUpdate = void 0;
30
7
  const logger_1 = __importDefault(require("../utils/logger"));
31
- const ValueRepo = __importStar(require("../repository/value"));
32
8
  const scopeAttributes_1 = __importDefault(require("../utils/scopeAttributes"));
9
+ const updateInstanceValues_1 = __importDefault(require("./utils/updateInstanceValues"));
33
10
  /**
34
11
  * A hook to update the custom fields when updating a model (more then one instance).
35
12
  */
@@ -43,7 +20,7 @@ exports.beforeBulkUpdate = beforeBulkUpdate;
43
20
  * A hook to update the custom fields when updating a model instance.
44
21
  * TODO - cleanup if update fail
45
22
  */
46
- const beforeUpdate = (scopeAttributes, modelOptions = {}) => async (instance, options) => {
23
+ const beforeUpdate = (scopeAttributes, modelOptions = {}, sadotOptions = { useCustomFieldsEntries: false }) => async (instance, options) => {
47
24
  logger_1.default.debug('sadot - before update hook');
48
25
  const { fields } = options;
49
26
  const modelType = instance.constructor.name;
@@ -51,7 +28,17 @@ const beforeUpdate = (scopeAttributes, modelOptions = {}) => async (instance, op
51
28
  const customFieldsIdx = fields.indexOf('customFields');
52
29
  if (customFieldsIdx > -1) {
53
30
  const { customFields } = instance;
54
- await ValueRepo.updateValues(modelType, instance.id, identifiers, customFields, { transaction: options.transaction, modelOptions });
31
+ await (0, updateInstanceValues_1.default)({
32
+ modelId: instance.id,
33
+ modelType,
34
+ identifiers,
35
+ customFields,
36
+ options: {
37
+ useCustomFieldsEntries: sadotOptions.useCustomFieldsEntries,
38
+ transaction: options.transaction,
39
+ modelOptions,
40
+ },
41
+ });
55
42
  // eslint-disable-next-line no-param-reassign
56
43
  fields.splice(customFieldsIdx, 1);
57
44
  }
@@ -0,0 +1,15 @@
1
+ import type { Transaction } from 'sequelize';
2
+ import type { ModelOptions } from '../../types';
3
+ interface UpdateInstanceValuesParams {
4
+ modelId: string;
5
+ modelType: string;
6
+ identifiers: string[];
7
+ customFields: Record<string, any>;
8
+ options?: {
9
+ transaction?: Transaction;
10
+ modelOptions?: ModelOptions;
11
+ useCustomFieldsEntries?: boolean;
12
+ };
13
+ }
14
+ declare const updateInstanceValues: ({ modelId, modelType, identifiers, customFields, options, }: UpdateInstanceValuesParams) => Promise<void>;
15
+ export default updateInstanceValues;
@@ -0,0 +1,50 @@
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 __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ const ValueRepo = __importStar(require("../../repository/value"));
27
+ const EntriesRepo = __importStar(require("../../repository/entries"));
28
+ const updateInstanceValues = async ({ modelId, modelType, identifiers, customFields, options = {
29
+ modelOptions: {},
30
+ useCustomFieldsEntries: false,
31
+ }, }) => {
32
+ await ValueRepo.updateValues(modelType, modelId, identifiers, customFields, {
33
+ ...options,
34
+ modelOptions: options.modelOptions ?? {},
35
+ });
36
+ /*
37
+ T.Y TODO - Once we're ready to switch from custom_field_values to custom_field_entries, we should remove the ValueRepo.updateValues call.
38
+ Currently, We're updating both tables to keep the data in sync, but not all microservices are using the new table yet.
39
+ */
40
+ if (!options?.useCustomFieldsEntries) {
41
+ return;
42
+ }
43
+ const { dataValues: { customFields: oldCustomFields } } = await EntriesRepo.findEntriesByModelId(modelId) ?? { dataValues: {} };
44
+ const newCustomFields = { ...oldCustomFields, ...customFields };
45
+ await EntriesRepo.updateEntries(modelId, modelType, newCustomFields, identifiers, {
46
+ ...options,
47
+ modelOptions: options.modelOptions ?? {},
48
+ });
49
+ };
50
+ exports.default = updateInstanceValues;
package/dist/index.js CHANGED
@@ -40,7 +40,7 @@ const useCustomFields = async (app, getModel, options) => {
40
40
  await (0, models_1.initTestModels)(sequelize);
41
41
  }
42
42
  // The order is important
43
- (0, init_1.addHooks)(models, getModel);
43
+ (0, init_1.addHooks)(models, getModel, { useCustomFieldsEntries });
44
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);
@@ -104,7 +104,12 @@ const getCustomFieldDefinitionsDictionary = async (instances, options = { withDi
104
104
  customFields.add(fieldName);
105
105
  });
106
106
  });
107
- const definitions = await (0, exports.findByEntityIds)(modelType, Array.from(entityIds), { ...options });
107
+ const where = {
108
+ modelType,
109
+ entityId: { [sequelize_1.Op.in]: Array.from(entityIds) },
110
+ name: { [sequelize_1.Op.in]: Array.from(customFields) },
111
+ };
112
+ const definitions = await (0, exports.findAll)(where, { ...options });
108
113
  const matchedDefinitions = definitions.filter((def) => customFields.has(def.name));
109
114
  const matchedDefinitionsByName = Object.fromEntries(matchedDefinitions.map((definition) => [definition.name, definition]));
110
115
  if (!definitions?.length || matchedDefinitions.length !== customFields.size) {
@@ -0,0 +1,13 @@
1
+ import type { FindOptions, Includeable, Transaction } from 'sequelize';
2
+ import { CustomFieldEntries } from '../models';
3
+ import type { ModelOptions } from '../types';
4
+ type CustomFieldEntriesModelOptions = ModelOptions & {
5
+ include?: Includeable;
6
+ transaction?: Transaction;
7
+ };
8
+ export declare const findEntriesByModelId: (modelId: string) => Promise<CustomFieldEntries>;
9
+ export declare const findEntriesByModelIds: (modelIds: string[], options?: CustomFieldEntriesModelOptions) => Promise<CustomFieldEntries[]>;
10
+ export declare const updateEntries: (modelId: string, modelType: string, customFields: Record<string, any>, identifiers: string[], options?: FindOptions & {
11
+ modelOptions?: ModelOptions;
12
+ }) => Promise<[CustomFieldEntries, boolean]>;
13
+ export {};
@@ -0,0 +1,77 @@
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 __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.updateEntries = exports.findEntriesByModelIds = exports.findEntriesByModelId = void 0;
30
+ const models_1 = require("../models");
31
+ const logger_1 = __importDefault(require("../utils/logger"));
32
+ const errors_1 = require("../errors");
33
+ const DefinitionRepo = __importStar(require("./definition"));
34
+ const findEntriesByModelId = async (modelId) => models_1.CustomFieldEntries.findOne({ where: { modelId } });
35
+ exports.findEntriesByModelId = findEntriesByModelId;
36
+ const findEntriesByModelIds = async (modelIds, options = {}) => {
37
+ const { transaction } = options;
38
+ return models_1.CustomFieldEntries.findAll({
39
+ where: { modelId: modelIds },
40
+ transaction,
41
+ });
42
+ };
43
+ exports.findEntriesByModelIds = findEntriesByModelIds;
44
+ const updateEntries = async (modelId, modelType, customFields, identifiers, options = {}) => {
45
+ const customFieldsNames = Object.keys(customFields);
46
+ logger_1.default.debug(`custom-fields: updating entries for ${modelType} ${modelId}`, {
47
+ customFieldsNames,
48
+ optionsKeys: options ? Object.keys(options) : null,
49
+ customFields,
50
+ identifiers,
51
+ });
52
+ const { modelOptions, transaction } = options;
53
+ const where = {
54
+ modelType,
55
+ name: customFieldsNames,
56
+ ...(!options.modelOptions?.useEntityIdFromInclude && { entityId: identifiers }),
57
+ };
58
+ const fieldDefinitions = await DefinitionRepo.findAll(where, { withDisabled: true, transaction, include: modelOptions.include?.(identifiers) }) ?? [];
59
+ const disabledDefinitions = fieldDefinitions.filter((def) => def.disabled);
60
+ if (fieldDefinitions.length !== customFieldsNames.length) {
61
+ logger_1.default.warn(`custom-fields: missing definitions for ${modelType} ${modelId}`, { names: customFieldsNames, fieldDefinitions });
62
+ const missingDefinitions = customFieldsNames.filter((name) => !fieldDefinitions.some((def) => def.name === name));
63
+ throw new errors_1.MissingDefinitionError(missingDefinitions);
64
+ }
65
+ const disabledNames = disabledDefinitions?.map((def) => def.name) || [];
66
+ const valuesWithDisabledDefinitions = customFieldsNames.filter((name) => disabledNames.includes(name));
67
+ if (valuesWithDisabledDefinitions?.length > 0) {
68
+ logger_1.default.warn(`custom-fields: trying to update disabled values: ${valuesWithDisabledDefinitions.join(', ')}`);
69
+ }
70
+ return models_1.CustomFieldEntries.upsert({
71
+ modelId,
72
+ entityId: fieldDefinitions[0].entityId,
73
+ modelType,
74
+ customFields,
75
+ }, options);
76
+ };
77
+ exports.updateEntries = updateEntries;
@@ -1,3 +1,5 @@
1
1
  import type { Application } from 'express';
2
2
  import type { Models } from '../../types';
3
- export declare function commonTestHooks(app?: Application | null, models?: Models[]): void;
3
+ export declare function commonTestHooks(app?: Application | null, models?: Models[], options?: {
4
+ useCustomFieldsEntries: boolean;
5
+ }): void;
@@ -31,7 +31,7 @@ const __1 = __importDefault(require("../.."));
31
31
  const _1 = require(".");
32
32
  const database_config_1 = __importDefault(require("./database-config"));
33
33
  const db_1 = __importStar(require("../../utils/db"));
34
- function commonTestHooks(app = null, models = [{ name: 'TestModel', scopeAttributes: ['fleetId'] }]) {
34
+ function commonTestHooks(app = null, models = [{ name: 'TestModel', scopeAttributes: ['fleetId'] }], options = { useCustomFieldsEntries: false }) {
35
35
  let sequelize;
36
36
  beforeAll(async () => {
37
37
  sequelize = (0, db_1.default)(database_config_1.default);
@@ -41,11 +41,12 @@ function commonTestHooks(app = null, models = [{ name: 'TestModel', scopeAttribu
41
41
  databaseConfig: database_config_1.default,
42
42
  getUser: () => undefined,
43
43
  sequelize,
44
+ useCustomFieldsEntries: options.useCustomFieldsEntries,
44
45
  });
45
46
  });
46
47
  afterEach(async () => {
47
48
  jest.clearAllMocks();
48
- await (0, _1.cleanup)();
49
+ await (0, _1.cleanup)({ useCustomFieldsEntries: options.useCustomFieldsEntries });
49
50
  });
50
51
  afterAll(async () => {
51
52
  await sequelize.close();
@@ -1,4 +1,5 @@
1
- export declare const cleanup: () => Promise<void>;
1
+ import type { CustomFieldOptions } from '../../types';
2
+ export declare const cleanup: (options?: Pick<CustomFieldOptions, 'useCustomFieldsEntries'>) => Promise<void>;
2
3
  export declare const getModel: (name: string) => any;
3
4
  export declare const getReplacementMapWithScopeValue: (conditions: Record<string, any>) => {
4
5
  replacementsMap: Record<string, string>;
@@ -4,12 +4,15 @@ exports.getReplacementMapWithScopeValue = exports.getModel = exports.cleanup = v
4
4
  const formatter_1 = require("@autofleet/sheilta/lib/formatter");
5
5
  const models_1 = require("../../models");
6
6
  // eslint-disable-next-line import/prefer-default-export
7
- const cleanup = async () => {
7
+ const cleanup = async (options) => {
8
8
  if (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'development') {
9
9
  await models_1.CustomFieldDefinition.unscoped().destroy({ where: {} });
10
10
  await models_1.TestModel.destroy({ where: {} });
11
11
  await models_1.ContextAwareTestModel.destroy({ where: {} });
12
12
  await models_1.ContextTestModel.destroy({ where: {} });
13
+ if (options?.useCustomFieldsEntries) {
14
+ await models_1.CustomFieldEntries.unscoped().destroy({ where: {} });
15
+ }
13
16
  }
14
17
  };
15
18
  exports.cleanup = cleanup;
@@ -1,5 +1,7 @@
1
1
  import type { ModelFetcher, Models } from '../types';
2
- export declare const addHooks: (models: Models[], getModel: ModelFetcher) => void;
2
+ export declare const addHooks: (models: Models[], getModel: ModelFetcher, sadotOptions?: {
3
+ useCustomFieldsEntries: boolean;
4
+ }) => void;
3
5
  export declare const removeHooks: (models: Models[], getModel: ModelFetcher) => void;
4
6
  export declare const addScopes: (models: Models[], getModel: ModelFetcher) => void;
5
7
  export declare const applyCustomAssociation: (models: Models[]) => void;
@@ -13,7 +13,7 @@ const scopes_1 = require("../scopes");
13
13
  const logger_1 = __importDefault(require("./logger"));
14
14
  const filter_1 = require("../scopes/filter");
15
15
  const { CUSTOM_FIELDS_FILTER_SCOPE } = common_types_1.customFields;
16
- const addHooks = (models, getModel) => {
16
+ const addHooks = (models, getModel, sadotOptions = { useCustomFieldsEntries: false }) => {
17
17
  models.forEach(async ({ name, scopeAttributes, modelOptions, }) => {
18
18
  try {
19
19
  const model = getModel(name);
@@ -33,8 +33,8 @@ const addHooks = (models, getModel) => {
33
33
  model.addHook('beforeFind', 'sadot-beforeFind', (0, hooks_1.beforeFind)(scopeAttributes));
34
34
  model.addHook('beforeBulkCreate', 'sadot-beforeBulkCreate', hooks_1.beforeBulkCreate);
35
35
  model.addHook('beforeBulkUpdate', 'sadot-beforeBulkUpdate', hooks_1.beforeBulkUpdate);
36
- model.addHook('beforeCreate', 'sadot-beforeCreate', (0, hooks_1.beforeCreate)(scopeAttributes, modelOptions));
37
- model.addHook('beforeUpdate', 'sadot-beforeUpdate', (0, hooks_1.beforeUpdate)(scopeAttributes, modelOptions));
36
+ model.addHook('beforeCreate', 'sadot-beforeCreate', (0, hooks_1.beforeCreate)(scopeAttributes, modelOptions, sadotOptions));
37
+ model.addHook('beforeUpdate', 'sadot-beforeUpdate', (0, hooks_1.beforeUpdate)(scopeAttributes, modelOptions, sadotOptions));
38
38
  model.addHook('afterFind', 'sadot-afterFind', (0, hooks_1.enrichResults)(name, scopeAttributes, 'afterFind', modelOptions));
39
39
  model.addHook('afterUpdate', 'sadot-afterUpdate', (0, hooks_1.enrichResults)(name, scopeAttributes, null, modelOptions));
40
40
  model.addHook('afterCreate', 'sadot-afterCreate', (0, hooks_1.enrichResults)(name, scopeAttributes, null, modelOptions));
@@ -19,6 +19,9 @@ exports.validateValue = validateValue;
19
19
  const validateInstanceCustomFieldEntries = (instance, definitionsByName) => {
20
20
  const validationErrors = Object.entries(instance.customFields)
21
21
  .map(([customFieldName, value]) => {
22
+ // Allow NULL values, just like we do in custom_field_values.
23
+ if (value === null)
24
+ return null;
22
25
  const { validation, fieldType } = definitionsByName[customFieldName];
23
26
  const result = (0, exports.validateValue)(value, fieldType, validation);
24
27
  if (result?.error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autofleet/sadot",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -55,7 +55,9 @@ export class InvalidEntriesError extends BadRequest {
55
55
  constructor(modelId: string, validationErrors: EntriesValidationError[]) {
56
56
  const errors = validationErrors.map((validationError) => new InvalidValueError(validationError.value, validationError.fieldDefinitionName, validationError.joiValidationError));
57
57
  super(errors, null, null);
58
- this.message = `Invalid entries on ${modelId}`;
58
+ this.message = `Invalid entries on ${modelId}\n${validationErrors.map((validationError) => (
59
+ `${validationError.fieldDefinitionName} - ${validationError.joiValidationError.message}`
60
+ )).join('\n')}`;
59
61
  }
60
62
  }
61
63
 
@@ -1,10 +1,10 @@
1
1
  import type { WhereOptions } from 'sequelize';
2
2
  import logger from '../utils/logger';
3
- import * as ValueRepo from '../repository/value';
4
3
  import * as DefinitionRepo from '../repository/definition';
5
4
  import { MissingRequiredCustomFieldError } from '../errors';
6
- import type { ModelOptions } from '../types';
5
+ import type { CustomFieldOptions, ModelOptions } from '../types';
7
6
  import applyScopeToInstance from '../utils/scopeAttributes';
7
+ import updateInstanceValues from './utils/updateInstanceValues';
8
8
 
9
9
  /**
10
10
  * A hook to create the custom fields when updating a model (more then one instance).
@@ -18,7 +18,11 @@ export const beforeBulkCreate = (options): void => {
18
18
  * A hook to create the custom fields when updating a model instance.
19
19
  * TODO - cleanup if update fail
20
20
  */
21
- export const beforeCreate = (scopeAttributes: string[], modelOptions: ModelOptions = {}) => async (
21
+ export const beforeCreate = (
22
+ scopeAttributes: string[],
23
+ modelOptions: ModelOptions = {},
24
+ sadotOptions: Pick<CustomFieldOptions, 'useCustomFieldsEntries'> = { useCustomFieldsEntries: false },
25
+ ) => async (
22
26
  instance,
23
27
  options,
24
28
  ): Promise<void> => {
@@ -61,16 +65,17 @@ export const beforeCreate = (scopeAttributes: string[], modelOptions: ModelOptio
61
65
  throw new MissingRequiredCustomFieldError(missingFields);
62
66
  }
63
67
 
64
- await ValueRepo.updateValues(
68
+ await updateInstanceValues({
69
+ modelId: instance.id,
65
70
  modelType,
66
- instance.id,
67
71
  identifiers,
68
72
  customFields,
69
- {
73
+ options: {
74
+ useCustomFieldsEntries: sadotOptions.useCustomFieldsEntries,
70
75
  transaction: options.transaction,
71
76
  modelOptions,
72
77
  },
73
- );
78
+ });
74
79
 
75
80
  // eslint-disable-next-line no-param-reassign
76
81
  fields.splice(customFieldsIdx, 1);
@@ -1,7 +1,7 @@
1
1
  import logger from '../utils/logger';
2
- import * as ValueRepo from '../repository/value';
3
- import type { ModelOptions } from '../types';
2
+ import type { CustomFieldOptions, ModelOptions } from '../types';
4
3
  import applyScopeToInstance from '../utils/scopeAttributes';
4
+ import updateInstanceValues from './utils/updateInstanceValues';
5
5
 
6
6
  /**
7
7
  * A hook to update the custom fields when updating a model (more then one instance).
@@ -16,7 +16,11 @@ export const beforeBulkUpdate = (options): void => {
16
16
  * A hook to update the custom fields when updating a model instance.
17
17
  * TODO - cleanup if update fail
18
18
  */
19
- export const beforeUpdate = (scopeAttributes: string[], modelOptions: ModelOptions = {}) => async (
19
+ export const beforeUpdate = (
20
+ scopeAttributes: string[],
21
+ modelOptions: ModelOptions = {},
22
+ sadotOptions: Pick<CustomFieldOptions, 'useCustomFieldsEntries'> = { useCustomFieldsEntries: false },
23
+ ) => async (
20
24
  instance,
21
25
  options,
22
26
  ): Promise<void> => {
@@ -28,14 +32,19 @@ export const beforeUpdate = (scopeAttributes: string[], modelOptions: ModelOptio
28
32
  const customFieldsIdx = fields.indexOf('customFields');
29
33
  if (customFieldsIdx > -1) {
30
34
  const { customFields } = instance;
31
- await ValueRepo.updateValues(
35
+
36
+ await updateInstanceValues({
37
+ modelId: instance.id,
32
38
  modelType,
33
- instance.id,
34
39
  identifiers,
35
40
  customFields,
36
- { transaction: options.transaction, modelOptions },
41
+ options: {
42
+ useCustomFieldsEntries: sadotOptions.useCustomFieldsEntries,
43
+ transaction: options.transaction,
44
+ modelOptions,
45
+ },
46
+ });
37
47
 
38
- );
39
48
  // eslint-disable-next-line no-param-reassign
40
49
  fields.splice(customFieldsIdx, 1);
41
50
  }
@@ -0,0 +1,63 @@
1
+ import type { Transaction } from 'sequelize';
2
+ import * as ValueRepo from '../../repository/value';
3
+ import * as EntriesRepo from '../../repository/entries';
4
+ import type { ModelOptions } from '../../types';
5
+
6
+ interface UpdateInstanceValuesParams {
7
+ modelId: string;
8
+ modelType: string;
9
+ identifiers: string[];
10
+ customFields: Record<string, any>;
11
+ options?: {
12
+ transaction?: Transaction;
13
+ modelOptions?: ModelOptions;
14
+ useCustomFieldsEntries?: boolean;
15
+ };
16
+ }
17
+
18
+ const updateInstanceValues = async ({
19
+ modelId,
20
+ modelType,
21
+ identifiers,
22
+ customFields,
23
+ options = {
24
+ modelOptions: {},
25
+ useCustomFieldsEntries: false,
26
+ },
27
+ }: UpdateInstanceValuesParams): Promise<void> => {
28
+ await ValueRepo.updateValues(
29
+ modelType,
30
+ modelId,
31
+ identifiers,
32
+ customFields,
33
+ {
34
+ ...options,
35
+ modelOptions: options.modelOptions ?? {},
36
+ },
37
+ );
38
+
39
+ /*
40
+ T.Y TODO - Once we're ready to switch from custom_field_values to custom_field_entries, we should remove the ValueRepo.updateValues call.
41
+ Currently, We're updating both tables to keep the data in sync, but not all microservices are using the new table yet.
42
+ */
43
+
44
+ if (!options?.useCustomFieldsEntries) {
45
+ return;
46
+ }
47
+
48
+ const { dataValues: { customFields: oldCustomFields } } = await EntriesRepo.findEntriesByModelId(modelId) ?? { dataValues: {} };
49
+ const newCustomFields = { ...oldCustomFields, ...customFields };
50
+
51
+ await EntriesRepo.updateEntries(
52
+ modelId,
53
+ modelType,
54
+ newCustomFields,
55
+ identifiers,
56
+ {
57
+ ...options,
58
+ modelOptions: options.modelOptions ?? {},
59
+ },
60
+ );
61
+ };
62
+
63
+ export default updateInstanceValues;
package/src/index.ts CHANGED
@@ -35,7 +35,7 @@ const useCustomFields = async (
35
35
  await initTestModels(sequelize);
36
36
  }
37
37
  // The order is important
38
- addHooks(models, getModel);
38
+ addHooks(models, getModel, { useCustomFieldsEntries });
39
39
  await initTables(sequelize, options.getUser, { useCustomFieldsEntries });
40
40
  addScopes(models, getModel);
41
41
  applyCustomAssociation(models);
@@ -155,7 +155,13 @@ export const getCustomFieldDefinitionsDictionary = async (
155
155
  });
156
156
  });
157
157
 
158
- const definitions = await findByEntityIds(modelType, Array.from(entityIds), { ...options });
158
+ const where: WhereOptions = {
159
+ modelType,
160
+ entityId: { [Op.in]: Array.from(entityIds) },
161
+ name: { [Op.in]: Array.from(customFields) },
162
+ };
163
+
164
+ const definitions = await findAll(where, { ...options });
159
165
 
160
166
  const matchedDefinitions = definitions.filter((def) => customFields.has(def.name));
161
167
  const matchedDefinitionsByName = Object.fromEntries(matchedDefinitions.map((definition) => [definition.name, definition]));
@@ -0,0 +1,71 @@
1
+ import type {
2
+ FindOptions,
3
+ Includeable,
4
+ Transaction,
5
+ WhereOptions,
6
+ } from 'sequelize';
7
+ import { CustomFieldEntries } from '../models';
8
+ import type { ModelOptions } from '../types';
9
+ import logger from '../utils/logger';
10
+ import { MissingDefinitionError } from '../errors';
11
+ import * as DefinitionRepo from './definition';
12
+
13
+ type CustomFieldEntriesModelOptions = ModelOptions & { include?: Includeable, transaction?: Transaction };
14
+
15
+ export const findEntriesByModelId = async (modelId: string) => CustomFieldEntries.findOne({ where: { modelId } });
16
+
17
+ export const findEntriesByModelIds = async (modelIds: string[], options: CustomFieldEntriesModelOptions = {}) => {
18
+ const { transaction } = options;
19
+ return CustomFieldEntries.findAll({
20
+ where: { modelId: modelIds },
21
+ transaction,
22
+ });
23
+ };
24
+
25
+ export const updateEntries = async (
26
+ modelId: string,
27
+ modelType: string,
28
+ customFields: Record<string, any>,
29
+ identifiers: string[],
30
+ options: FindOptions & { modelOptions?: ModelOptions } = {},
31
+ ) => {
32
+ const customFieldsNames = Object.keys(customFields);
33
+ logger.debug(`custom-fields: updating entries for ${modelType} ${modelId}`, {
34
+ customFieldsNames,
35
+ optionsKeys: options ? Object.keys(options) : null,
36
+ customFields,
37
+ identifiers,
38
+ });
39
+ const { modelOptions, transaction } = options;
40
+
41
+ const where: WhereOptions = {
42
+ modelType,
43
+ name: customFieldsNames,
44
+ ...(!options.modelOptions?.useEntityIdFromInclude && { entityId: identifiers }),
45
+ };
46
+
47
+ const fieldDefinitions = await DefinitionRepo.findAll(where, { withDisabled: true, transaction, include: modelOptions.include?.(identifiers) }) ?? [];
48
+
49
+ const disabledDefinitions = fieldDefinitions.filter((def) => def.disabled);
50
+ if (fieldDefinitions.length !== customFieldsNames.length) {
51
+ logger.warn(`custom-fields: missing definitions for ${modelType} ${modelId}`, { names: customFieldsNames, fieldDefinitions });
52
+ const missingDefinitions = customFieldsNames.filter((name) => !fieldDefinitions.some((def) => def.name === name));
53
+ throw new MissingDefinitionError(missingDefinitions);
54
+ }
55
+
56
+ const disabledNames = disabledDefinitions?.map((def) => def.name) || [];
57
+ const valuesWithDisabledDefinitions = customFieldsNames.filter((name) => disabledNames.includes(name));
58
+ if (valuesWithDisabledDefinitions?.length > 0) {
59
+ logger.warn(`custom-fields: trying to update disabled values: ${valuesWithDisabledDefinitions.join(', ')}`);
60
+ }
61
+
62
+ return CustomFieldEntries.upsert(
63
+ {
64
+ modelId,
65
+ entityId: fieldDefinitions[0].entityId,
66
+ modelType,
67
+ customFields,
68
+ },
69
+ options,
70
+ );
71
+ };
@@ -6,7 +6,11 @@ import databaseConfig from './database-config';
6
6
  import initDB, { createSequelizeMeta } from '../../utils/db';
7
7
  import type { Models } from '../../types';
8
8
 
9
- export function commonTestHooks(app: Application | null = null, models: Models[] = [{ name: 'TestModel', scopeAttributes: ['fleetId'] }]) {
9
+ export function commonTestHooks(
10
+ app: Application | null = null,
11
+ models: Models[] = [{ name: 'TestModel', scopeAttributes: ['fleetId'] }],
12
+ options: { useCustomFieldsEntries: boolean } = { useCustomFieldsEntries: false },
13
+ ) {
10
14
  let sequelize: Sequelize;
11
15
 
12
16
  beforeAll(async () => {
@@ -17,12 +21,13 @@ export function commonTestHooks(app: Application | null = null, models: Models[]
17
21
  databaseConfig,
18
22
  getUser: () => undefined,
19
23
  sequelize,
24
+ useCustomFieldsEntries: options.useCustomFieldsEntries,
20
25
  });
21
26
  });
22
27
 
23
28
  afterEach(async () => {
24
29
  jest.clearAllMocks();
25
- await cleanup();
30
+ await cleanup({ useCustomFieldsEntries: options.useCustomFieldsEntries });
26
31
  });
27
32
 
28
33
  afterAll(async () => {
@@ -1,15 +1,20 @@
1
1
  import { generateFilterReplacements } from '@autofleet/sheilta/lib/formatter';
2
2
  import {
3
- ContextAwareTestModel, ContextTestModel, CustomFieldDefinition, TestModel,
3
+ ContextAwareTestModel, ContextTestModel, CustomFieldDefinition, CustomFieldEntries, TestModel,
4
4
  } from '../../models';
5
+ import type { CustomFieldOptions } from '../../types';
5
6
 
6
7
  // eslint-disable-next-line import/prefer-default-export
7
- export const cleanup = async (): Promise<void> => {
8
+ export const cleanup = async (options?: Pick<CustomFieldOptions, 'useCustomFieldsEntries'>): Promise<void> => {
8
9
  if (process.env.NODE_ENV === 'test' || process.env.NODE_ENV === 'development') {
9
10
  await CustomFieldDefinition.unscoped().destroy({ where: {} });
10
11
  await TestModel.destroy({ where: {} });
11
12
  await ContextAwareTestModel.destroy({ where: {} });
12
13
  await ContextTestModel.destroy({ where: {} });
14
+
15
+ if (options?.useCustomFieldsEntries) {
16
+ await CustomFieldEntries.unscoped().destroy({ where: {} });
17
+ }
13
18
  }
14
19
  };
15
20
 
package/src/utils/init.ts CHANGED
@@ -21,7 +21,11 @@ import { customFieldsSortScope } from '../scopes/filter';
21
21
 
22
22
  const { CUSTOM_FIELDS_FILTER_SCOPE } = customFields;
23
23
 
24
- export const addHooks = (models: Models[], getModel: ModelFetcher): void => {
24
+ export const addHooks = (
25
+ models: Models[],
26
+ getModel: ModelFetcher,
27
+ sadotOptions: { useCustomFieldsEntries: boolean } = { useCustomFieldsEntries: false },
28
+ ): void => {
25
29
  models.forEach(async ({
26
30
  name, scopeAttributes, modelOptions,
27
31
  }) => {
@@ -43,8 +47,8 @@ export const addHooks = (models: Models[], getModel: ModelFetcher): void => {
43
47
  model.addHook('beforeFind', 'sadot-beforeFind', beforeFind(scopeAttributes));
44
48
  model.addHook('beforeBulkCreate', 'sadot-beforeBulkCreate', beforeBulkCreate);
45
49
  model.addHook('beforeBulkUpdate', 'sadot-beforeBulkUpdate', beforeBulkUpdate);
46
- model.addHook('beforeCreate', 'sadot-beforeCreate', beforeCreate(scopeAttributes, modelOptions));
47
- model.addHook('beforeUpdate', 'sadot-beforeUpdate', beforeUpdate(scopeAttributes, modelOptions));
50
+ model.addHook('beforeCreate', 'sadot-beforeCreate', beforeCreate(scopeAttributes, modelOptions, sadotOptions));
51
+ model.addHook('beforeUpdate', 'sadot-beforeUpdate', beforeUpdate(scopeAttributes, modelOptions, sadotOptions));
48
52
  model.addHook('afterFind', 'sadot-afterFind', enrichResults(name, scopeAttributes, 'afterFind', modelOptions));
49
53
  model.addHook('afterUpdate', 'sadot-afterUpdate', enrichResults(name, scopeAttributes, null, modelOptions));
50
54
  model.addHook('afterCreate', 'sadot-afterCreate', enrichResults(name, scopeAttributes, null, modelOptions));
@@ -24,6 +24,9 @@ export const validateValue = (
24
24
  export const validateInstanceCustomFieldEntries = (instance: CustomFieldEntriesDTO, definitionsByName: { [defName: string]: CustomFieldDefinition; }) => {
25
25
  const validationErrors = Object.entries(instance.customFields)
26
26
  .map(([customFieldName, value]) => {
27
+ // Allow NULL values, just like we do in custom_field_values.
28
+ if (value === null) return null;
29
+
27
30
  const { validation, fieldType } = definitionsByName[customFieldName];
28
31
  const result = validateValue(value, fieldType, validation);
29
32
  if (result?.error) {