@autofleet/sadot 0.13.0-beta.9 → 0.13.2-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -80,8 +80,9 @@ router.get('/', async (req, res) => {
80
80
  */
81
81
  router.get('/:validatorId', async (req, res) => {
82
82
  try {
83
- const { validatorId } = req.params;
84
- const validators = await ValidatorRepo.findAll({ id: validatorId });
83
+ const { validatorId, modelName } = req.params;
84
+ // Include disabled validators when fetching by ID
85
+ const validators = await ValidatorRepo.findAll({ id: validatorId, modelType: modelName }, { withDisabled: true });
85
86
  if (!validators.length) {
86
87
  throw new errors_1.ResourceNotFoundError('Validator not found');
87
88
  }
@@ -103,6 +104,11 @@ router.patch('/:validatorId', async (req, res) => {
103
104
  if (validatedPayload.schema) {
104
105
  (0, validator_schema_1.validateValidatorSchema)(validatedPayload.schema);
105
106
  }
107
+ // First verify the validator exists, including disabled ones
108
+ const existingValidators = await ValidatorRepo.findAll({ id: validatorId }, { withDisabled: true });
109
+ if (!existingValidators.length) {
110
+ throw new errors_1.ResourceNotFoundError('Validator not found');
111
+ }
106
112
  const [count, validators] = await ValidatorRepo.update(validatorId, validatedPayload);
107
113
  if (!count) {
108
114
  throw new errors_1.ResourceNotFoundError('Validator not found');
@@ -119,9 +125,14 @@ router.patch('/:validatorId', async (req, res) => {
119
125
  router.delete('/:validatorId', async (req, res) => {
120
126
  try {
121
127
  const { validatorId } = req.params;
128
+ // First verify the validator exists, including disabled ones
129
+ const existingValidators = await ValidatorRepo.findAll({ id: validatorId }, { withDisabled: true });
130
+ if (!existingValidators.length) {
131
+ throw new errors_1.ResourceNotFoundError('Validator not found');
132
+ }
122
133
  const [count] = await ValidatorRepo.disable(validatorId);
123
134
  if (!count) {
124
- throw new errors_1.ResourceNotFoundError('Validator not found');
135
+ throw new errors_1.ResourceNotFoundError('Validator failed to be disabled');
125
136
  }
126
137
  return res.status(http_status_codes_1.StatusCodes.NO_CONTENT).send();
127
138
  }
@@ -23,6 +23,7 @@ const validationSchemas = {
23
23
  }),
24
24
  update: joi_1.default.object({
25
25
  entityId: joi_1.default.string().uuid(),
26
+ entityType: joi_1.default.string(),
26
27
  schema: joi_1.default.object({
27
28
  type: joi_1.default.string().valid('object'),
28
29
  properties: joi_1.default.object({
@@ -36,6 +36,7 @@ const DefinitionRepo = __importStar(require("../repository/definition"));
36
36
  const errors_2 = require("../errors");
37
37
  const scopeAttributes_1 = __importDefault(require("../utils/scopeAttributes"));
38
38
  const updateInstanceValues_1 = __importDefault(require("./utils/updateInstanceValues"));
39
+ const constants_1 = require("../utils/constants");
39
40
  // Initialize Ajv with relaxed settings to avoid warnings
40
41
  const ajv = new ajv_1.default({
41
42
  allErrors: true,
@@ -44,6 +45,46 @@ const ajv = new ajv_1.default({
44
45
  $data: true, // Enable $data references
45
46
  });
46
47
  (0, ajv_formats_1.default)(ajv);
48
+ /**
49
+ * Helper function to manually copy object properties
50
+ * This is more efficient for large objects and avoids excessive object creation
51
+ */
52
+ // eslint-disable-next-line prefer-object-spread
53
+ const manualObjectCopy = (sourceObj, additionalProps) => ({ __proto__: null, ...sourceObj, ...additionalProps });
54
+ /**
55
+ * Fetches complete custom fields for an instance by merging DB values with update values
56
+ * This is needed for partial updates to ensure all related fields are available for validation
57
+ */
58
+ const getCompleteCustomFields = async (instance, options) => {
59
+ // If we don't have an instance id or no custom fields being updated, return original fields
60
+ if (!instance.id || !instance.customFields || Object.keys(instance.customFields).length === 0) {
61
+ return instance.customFields || {};
62
+ }
63
+ try {
64
+ const ModelClass = instance.constructor;
65
+ // Only select the customFields column to minimize data transfer
66
+ const currentCustomFields = await ModelClass.findOne({
67
+ where: { id: instance.id },
68
+ attributes: ['customFields'],
69
+ transaction: options.transaction,
70
+ raw: true, // Get plain object instead of model instance for better performance
71
+ });
72
+ if (currentCustomFields?.customFields) {
73
+ // Merge existing fields with update fields using our helper function
74
+ const completeFields = manualObjectCopy(currentCustomFields.customFields, instance.customFields);
75
+ logger_1.default.debug('sadot - fetched complete custom fields for validation', {
76
+ fieldsCount: Object.keys(completeFields).length,
77
+ updateFieldsCount: Object.keys(instance.customFields).length,
78
+ });
79
+ return completeFields;
80
+ }
81
+ }
82
+ catch (error) {
83
+ logger_1.default.error('sadot - error fetching complete model for validation', { error });
84
+ // Continue with partial data if we can't fetch the complete model
85
+ }
86
+ return instance.customFields || {};
87
+ };
47
88
  /**
48
89
  * Validates the model using custom validators
49
90
  */
@@ -71,21 +112,26 @@ const validateModel = async (instance, options, scopeAttributes, isCreate = fals
71
112
  return;
72
113
  }
73
114
  // For updates, get the previous values
74
- const originalValues = isCreate
75
- ? null
76
- : {
77
- ...instance.previous(),
78
- customFields: instance.previous('customFields') || {},
79
- };
80
- // For debugging in case of update
115
+ let originalValues = null;
81
116
  if (!isCreate) {
117
+ // Create originalValues with our helper function
118
+ originalValues = manualObjectCopy(instance.previous());
119
+ // Add customFields separately
120
+ originalValues.customFields = instance.previous('customFields') || {};
121
+ }
122
+ // Get complete custom fields by merging DB values with update values
123
+ // This is especially important for partial updates to ensure all related fields are available
124
+ const completeCustomFields = !isCreate
125
+ ? await getCompleteCustomFields(instance, options)
126
+ : instance.customFields || {};
127
+ // For debugging in case of update
128
+ if (!isCreate && process.env.NODE_ENV !== 'production') {
129
+ // Create after object for logging
130
+ const logAfterObj = manualObjectCopy(instance.dataValues, { customFields: completeCustomFields });
82
131
  logger_1.default.debug('sadot - validate with values', {
83
132
  before: originalValues,
84
- after: {
85
- ...instance.dataValues,
86
- ...instance.customFields,
87
- },
88
- schema: JSON.stringify(validators[0].schema),
133
+ after: logAfterObj,
134
+ schema: validators[0].schema,
89
135
  });
90
136
  }
91
137
  // eslint-disable-next-line no-restricted-syntax
@@ -93,7 +139,7 @@ const validateModel = async (instance, options, scopeAttributes, isCreate = fals
93
139
  const { schema } = validator;
94
140
  const typedSchema = schema;
95
141
  logger_1.default.debug('sadot - validating with schema', {
96
- schema: JSON.stringify(schema),
142
+ schema,
97
143
  hasAfterProps: !!typedSchema.properties?.after,
98
144
  hasBeforeProps: !!typedSchema.properties?.before,
99
145
  });
@@ -107,12 +153,12 @@ const validateModel = async (instance, options, scopeAttributes, isCreate = fals
107
153
  after: typedSchema.properties.after,
108
154
  },
109
155
  });
110
- const isValid = validateSchema({
156
+ const isValid = validateSchema(JSON.parse(JSON.stringify({
111
157
  after: {
112
158
  ...instance.dataValues,
113
- customFields: instance.customFields,
159
+ customFields: completeCustomFields,
114
160
  },
115
- });
161
+ })));
116
162
  if (!isValid) {
117
163
  const errorDetails = validateSchema.errors?.map((err) => `${err.instancePath || ''} ${err.message || 'Invalid value'}`).join(', ');
118
164
  throw new errors_1.BadRequest([new Error(`Validation failed for ${modelType}: ${errorDetails}`)]);
@@ -122,40 +168,36 @@ const validateModel = async (instance, options, scopeAttributes, isCreate = fals
122
168
  else {
123
169
  // For update operations, we need both before and after
124
170
  const validateSchema = ajv.compile(typedSchema);
125
- const isValid = validateSchema({
171
+ // Create after object with our helper function
172
+ const afterObj = manualObjectCopy(instance.dataValues);
173
+ // Add complete custom fields
174
+ afterObj.customFields = completeCustomFields;
175
+ // Create validation payload
176
+ const payload = {
126
177
  before: originalValues,
127
- after: {
128
- ...instance.dataValues,
129
- customFields: instance.customFields,
130
- },
131
- });
178
+ after: afterObj,
179
+ };
180
+ // Validate
181
+ const isValid = validateSchema(JSON.parse(JSON.stringify(payload)));
132
182
  logger_1.default.info('sadot - validation result', {
133
183
  isValid,
134
184
  test: {
135
185
  before: originalValues,
136
- after: {
137
- ...instance.dataValues,
138
- ...instance.customFields,
139
- },
186
+ after: afterObj,
140
187
  },
141
188
  });
142
189
  if (!isValid) {
143
- const errorDetails = validateSchema.errors?.map((err) => `${err.instancePath || ''} ${err.message || 'Invalid value'}`).join(', ');
190
+ const errorDetails = validateSchema.errors?.map((err) => {
191
+ logger_1.default.info('dor', err);
192
+ return `${err.instancePath || ''} ${err.message || 'Invalid value'}`;
193
+ }).join(', ');
144
194
  throw new errors_1.BadRequest([new Error(`Validation failed for ${modelType}: ${errorDetails}`)]);
145
195
  }
146
196
  }
147
197
  }
148
198
  };
149
- /**
150
- * Hook to handle validation and custom fields during creation
151
- */
152
- const beforeCreate = (scopeAttributes, modelOptions = {}, sadotOptions = { useCustomFieldsEntries: false }) => async (instance, options) => {
153
- logger_1.default.debug('sadot - before create hook');
154
- const { fields } = options;
199
+ const getFieldDefinitions = async ({ modelType, modelOptions, identifiers, options }) => {
155
200
  const { include, useEntityIdFromInclude } = modelOptions;
156
- const modelType = instance.constructor.name;
157
- const identifiers = (0, scopeAttributes_1.default)(instance, scopeAttributes);
158
- // Step 1: Handle custom fields default values and required fields
159
201
  const where = {
160
202
  modelType,
161
203
  disabled: false,
@@ -166,7 +208,32 @@ const beforeCreate = (scopeAttributes, modelOptions = {}, sadotOptions = { useCu
166
208
  transaction: options.transaction,
167
209
  include: include?.(identifiers),
168
210
  });
169
- const requiredFieldsNames = Array.from(new Set(fieldDefinitions.filter(({ required }) => required).map(({ name }) => name)));
211
+ return fieldDefinitions;
212
+ };
213
+ const formatDates = (fieldDefinitions = [], instance) => {
214
+ fieldDefinitions.forEach((fieldDefinition) => {
215
+ const { fieldType, name } = fieldDefinition;
216
+ if ([constants_1.CustomFieldDefinitionType.DATE, constants_1.CustomFieldDefinitionType.DATETIME].includes(fieldType)) {
217
+ const value = instance.customFields?.[name];
218
+ if (value) {
219
+ // eslint-disable-next-line no-param-reassign
220
+ instance.customFields[name] = new Date(value).toISOString();
221
+ }
222
+ }
223
+ });
224
+ };
225
+ /**
226
+ * Hook to handle validation and custom fields during creation
227
+ */
228
+ const beforeCreate = (scopeAttributes, modelOptions = {}, sadotOptions = { useCustomFieldsEntries: false }) => async (instance, options) => {
229
+ logger_1.default.debug('sadot - before create hook');
230
+ const { fields } = options;
231
+ const modelType = instance.constructor.name;
232
+ const identifiers = (0, scopeAttributes_1.default)(instance, scopeAttributes);
233
+ // Step 1: Handle custom fields default values and required fields
234
+ const fieldDefinitions = await getFieldDefinitions({
235
+ modelType, modelOptions, identifiers, options
236
+ });
170
237
  // Apply default values
171
238
  const fieldsWithDefaultValue = fieldDefinitions.filter((def) => ![null, undefined].includes(def.defaultValue));
172
239
  if (fieldsWithDefaultValue.length) {
@@ -180,6 +247,7 @@ const beforeCreate = (scopeAttributes, modelOptions = {}, sadotOptions = { useCu
180
247
  });
181
248
  }
182
249
  // Check for required fields
250
+ const requiredFieldsNames = Array.from(new Set(fieldDefinitions.filter(({ required }) => required).map(({ name }) => name)));
183
251
  const { customFields } = instance;
184
252
  const fieldsNames = Object.keys(customFields ?? {});
185
253
  const missingFields = requiredFieldsNames.filter((name) => !fieldsNames.includes(name));
@@ -188,6 +256,8 @@ const beforeCreate = (scopeAttributes, modelOptions = {}, sadotOptions = { useCu
188
256
  }
189
257
  // Step 2: Validate the model data (including custom fields)
190
258
  await validateModel(instance, options, scopeAttributes, true);
259
+ // format date and datetime fields
260
+ formatDates(fieldDefinitions, instance);
191
261
  // Step 3: Save custom field values if they exist
192
262
  const customFieldsIdx = fields.indexOf('customFields');
193
263
  if (customFieldsIdx === -1 || !customFields || !Object.keys(customFields).length) {
@@ -219,8 +289,13 @@ const beforeUpdate = (scopeAttributes, modelOptions = {}, sadotOptions = { useCu
219
289
  const { fields } = options;
220
290
  const modelType = instance.constructor.name;
221
291
  const identifiers = (0, scopeAttributes_1.default)(instance, scopeAttributes);
292
+ const fieldDefinitions = await getFieldDefinitions({
293
+ modelType, modelOptions, identifiers, options
294
+ });
222
295
  // Step 1: Validate the model data (including custom fields)
223
296
  await validateModel(instance, options, scopeAttributes, false);
297
+ // format date and datetime fields
298
+ formatDates(fieldDefinitions, instance);
224
299
  // Step 2: Update custom field values if they exist
225
300
  const customFieldsIdx = fields.indexOf('customFields');
226
301
  if (customFieldsIdx > -1) {
package/dist/index.d.ts CHANGED
@@ -4,9 +4,6 @@ import type { CustomFieldOptions, ModelFetcher, Models } from './types';
4
4
  export * from './utils/validations/schema/custom-fields';
5
5
  export * from './utils/constants';
6
6
  export * from './utils/helpers';
7
- export * as DefinitionRepo from './repository/definition';
8
- export * as ValueRepo from './repository/value';
9
- export * as ValidatorRepo from './repository/validator';
10
7
  /**
11
8
  * Adding custom fields enrichment to the models inside the MODELS_FILE_NAME json file
12
9
  * @see {@link 'custom-fields/config'} for configurations
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
29
29
  return (mod && mod.__esModule) ? mod : { "default": mod };
30
30
  };
31
31
  Object.defineProperty(exports, "__esModule", { value: true });
32
- exports.disableCustomFields = exports.ValidatorRepo = exports.ValueRepo = exports.DefinitionRepo = void 0;
32
+ exports.disableCustomFields = void 0;
33
33
  const models_1 = require("./models");
34
34
  const api_1 = __importDefault(require("./api"));
35
35
  const db_1 = __importDefault(require("./utils/db"));
@@ -38,10 +38,6 @@ const init_1 = require("./utils/init");
38
38
  __exportStar(require("./utils/validations/schema/custom-fields"), exports);
39
39
  __exportStar(require("./utils/constants"), exports);
40
40
  __exportStar(require("./utils/helpers"), exports);
41
- // Export repositories
42
- exports.DefinitionRepo = __importStar(require("./repository/definition"));
43
- exports.ValueRepo = __importStar(require("./repository/value"));
44
- exports.ValidatorRepo = __importStar(require("./repository/validator"));
45
41
  /**
46
42
  * Adding custom fields enrichment to the models inside the MODELS_FILE_NAME json file
47
43
  * @see {@link 'custom-fields/config'} for configurations
@@ -90,6 +90,7 @@ __decorate([
90
90
  __metadata("design:returntype", void 0)
91
91
  ], CustomValidator, "afterSaveHandler", null);
92
92
  CustomValidator = __decorate([
93
+ (0, sequelize_typescript_1.DefaultScope)(() => ({ where: { disabled: false } })),
93
94
  (0, sequelize_typescript_1.Table)({
94
95
  timestamps: true,
95
96
  })
@@ -58,6 +58,21 @@ const initTables = async (sequelize, getUser, { schemaPrefix, schemaVersion, use
58
58
  },
59
59
  };
60
60
  });
61
+ CustomValidator_1.default.addScope('userScope', () => {
62
+ const user = getUser();
63
+ if (!user?.permissions) {
64
+ return {};
65
+ }
66
+ return {
67
+ where: {
68
+ entityId: [
69
+ ...Object.keys(user.permissions.fleets),
70
+ ...Object.keys(user.permissions.businessModels),
71
+ ...Object.keys(user.permissions.demandSources),
72
+ ],
73
+ },
74
+ };
75
+ });
61
76
  logger_1.default.info('custom-fields: models added');
62
77
  const SequelizeMeta = sequelize.define('SequelizeMeta', {
63
78
  name: {
@@ -7,7 +7,7 @@ export interface ValidatorAttributes {
7
7
  entityId: string;
8
8
  entityType: string;
9
9
  modelType: string;
10
- schema: Record<string, unknown>;
10
+ schema: CustomValidator['schema'];
11
11
  disabled?: boolean;
12
12
  [key: string]: unknown;
13
13
  }
@@ -16,13 +16,23 @@ exports.create = create;
16
16
  const findAll = async (where = {}, options = { withDisabled: false }) => {
17
17
  logger_1.default.debug('custom-validator - find all validators');
18
18
  const { transaction, withDisabled } = options;
19
- const fullWhere = withDisabled
20
- ? where
21
- : { ...where, disabled: false };
22
- const validators = await models_1.CustomValidator.findAll({
23
- where: fullWhere,
24
- transaction,
25
- });
19
+ let validators;
20
+ if (withDisabled) {
21
+ // If withDisabled is true, use unscoped to ignore the default scope that filters disabled items
22
+ // Apply the userScope separately to maintain permission filtering
23
+ validators = await models_1.CustomValidator.unscoped().scope('userScope').findAll({
24
+ where,
25
+ transaction,
26
+ });
27
+ }
28
+ else {
29
+ // Use defaultScope and userScope to filter both disabled and by permissions
30
+ // The defaultScope keeps only non-disabled validators
31
+ validators = await models_1.CustomValidator.scope(['defaultScope', 'userScope']).findAll({
32
+ where,
33
+ transaction,
34
+ });
35
+ }
26
36
  return validators;
27
37
  };
28
38
  exports.findAll = findAll;
@@ -4,7 +4,7 @@ exports.default = {
4
4
  test: {
5
5
  username: process.env.DB_USERNAME || '',
6
6
  password: process.env.DB_PASSWORD || null,
7
- database: process.env.DB_NAME || 'sadot_package_test',
7
+ database: process.env.DB_NAME || 'postgres',
8
8
  host: process.env.DB_HOST || '127.0.0.1',
9
9
  port: process.env.DB_PORT || 5432,
10
10
  dialect: process.env.DB_TYPE || 'postgres',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autofleet/sadot",
3
- "version": "0.13.0-beta.9",
3
+ "version": "0.13.2-beta.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -8,6 +8,7 @@
8
8
  "build": "rm -rf dist && tsc",
9
9
  "linter": "eslint .",
10
10
  "test": "jest --runInBand",
11
+ "test-debug": "node --inspect-brk node_modules/.bin/jest --testTimeout=10000000",
11
12
  "coverage": "jest --coverage --runInBand",
12
13
  "build-to-local-repo": "node --run build && cp -r dist/* ../$REPO/node_modules/$npm_package_name/dist",
13
14
  "dev": "nodemon",
@@ -66,8 +66,9 @@ router.get<
66
66
  */
67
67
  router.get<{ modelName: string; validatorId: string }, CustomValidator>('/:validatorId', async (req, res) => {
68
68
  try {
69
- const { validatorId } = req.params;
70
- const validators = await ValidatorRepo.findAll({ id: validatorId });
69
+ const { validatorId, modelName } = req.params;
70
+ // Include disabled validators when fetching by ID
71
+ const validators = await ValidatorRepo.findAll({ id: validatorId, modelType: modelName }, { withDisabled: true });
71
72
 
72
73
  if (!validators.length) {
73
74
  throw new ResourceNotFoundError('Validator not found');
@@ -94,6 +95,12 @@ router.patch<{ modelName: string; validatorId: string }, CustomValidator>('/:val
94
95
  validateValidatorSchema(validatedPayload.schema);
95
96
  }
96
97
 
98
+ // First verify the validator exists, including disabled ones
99
+ const existingValidators = await ValidatorRepo.findAll({ id: validatorId }, { withDisabled: true });
100
+ if (!existingValidators.length) {
101
+ throw new ResourceNotFoundError('Validator not found');
102
+ }
103
+
97
104
  const [count, validators] = await ValidatorRepo.update(validatorId, validatedPayload);
98
105
 
99
106
  if (!count) {
@@ -112,10 +119,17 @@ router.patch<{ modelName: string; validatorId: string }, CustomValidator>('/:val
112
119
  router.delete<{ modelName: string; validatorId: string }>('/:validatorId', async (req, res) => {
113
120
  try {
114
121
  const { validatorId } = req.params;
122
+
123
+ // First verify the validator exists, including disabled ones
124
+ const existingValidators = await ValidatorRepo.findAll({ id: validatorId }, { withDisabled: true });
125
+ if (!existingValidators.length) {
126
+ throw new ResourceNotFoundError('Validator not found');
127
+ }
128
+
115
129
  const [count] = await ValidatorRepo.disable(validatorId);
116
130
 
117
131
  if (!count) {
118
- throw new ResourceNotFoundError('Validator not found');
132
+ throw new ResourceNotFoundError('Validator failed to be disabled');
119
133
  }
120
134
 
121
135
  return res.status(StatusCodes.NO_CONTENT).send();
@@ -21,6 +21,7 @@ const validationSchemas = {
21
21
 
22
22
  update: Joi.object({
23
23
  entityId: Joi.string().uuid(),
24
+ entityType: Joi.string(),
24
25
  schema: Joi.object({
25
26
  type: Joi.string().valid('object'),
26
27
  properties: Joi.object({
@@ -1,14 +1,17 @@
1
1
  import type { WhereOptions } from 'sequelize';
2
2
  import Ajv from 'ajv';
3
+ import Joi from 'joi';
3
4
  import addFormats from 'ajv-formats';
4
5
  import { BadRequest } from '@autofleet/errors';
5
6
  import logger from '../utils/logger';
6
7
  import * as ValidatorRepo from '../repository/validator';
7
8
  import * as DefinitionRepo from '../repository/definition';
8
- import { MissingRequiredCustomFieldError } from '../errors';
9
+ import { InvalidValueError, MissingRequiredCustomFieldError } from '../errors';
9
10
  import type { CustomFieldOptions, ModelOptions } from '../types';
10
11
  import applyScopeToInstance from '../utils/scopeAttributes';
11
12
  import updateInstanceValues from './utils/updateInstanceValues';
13
+ import { CustomFieldDefinitionType } from '../utils/constants';
14
+ import type { CustomFieldDefinition } from '../models';
12
15
 
13
16
  // Initialize Ajv with relaxed settings to avoid warnings
14
17
  const ajv = new Ajv({
@@ -20,6 +23,56 @@ const ajv = new Ajv({
20
23
 
21
24
  addFormats(ajv);
22
25
 
26
+ /**
27
+ * Helper function to manually copy object properties
28
+ * This is more efficient for large objects and avoids excessive object creation
29
+ */
30
+ // eslint-disable-next-line prefer-object-spread
31
+ const manualObjectCopy = (sourceObj: Record<string, any>, additionalProps?: Record<string, any>): Record<string, any> =>
32
+ ({ __proto__: null, ...sourceObj, ...additionalProps });
33
+
34
+ /**
35
+ * Fetches complete custom fields for an instance by merging DB values with update values
36
+ * This is needed for partial updates to ensure all related fields are available for validation
37
+ */
38
+ const getCompleteCustomFields = async (instance, options): Promise<Record<string, any>> => {
39
+ // If we don't have an instance id or no custom fields being updated, return original fields
40
+ if (!instance.id || !instance.customFields || Object.keys(instance.customFields).length === 0) {
41
+ return instance.customFields || {};
42
+ }
43
+
44
+ try {
45
+ const ModelClass = instance.constructor;
46
+ // Only select the customFields column to minimize data transfer
47
+ const currentCustomFields = await ModelClass.findOne({
48
+ where: { id: instance.id },
49
+ attributes: ['customFields'],
50
+ transaction: options.transaction,
51
+ raw: true, // Get plain object instead of model instance for better performance
52
+ });
53
+
54
+ if (currentCustomFields?.customFields) {
55
+ // Merge existing fields with update fields using our helper function
56
+ const completeFields = manualObjectCopy(
57
+ currentCustomFields.customFields,
58
+ instance.customFields,
59
+ );
60
+
61
+ logger.debug('sadot - fetched complete custom fields for validation', {
62
+ fieldsCount: Object.keys(completeFields).length,
63
+ updateFieldsCount: Object.keys(instance.customFields).length,
64
+ });
65
+
66
+ return completeFields;
67
+ }
68
+ } catch (error) {
69
+ logger.error('sadot - error fetching complete model for validation', { error });
70
+ // Continue with partial data if we can't fetch the complete model
71
+ }
72
+
73
+ return instance.customFields || {};
74
+ };
75
+
23
76
  /**
24
77
  * Validates the model using custom validators
25
78
  */
@@ -66,22 +119,30 @@ const validateModel = async (
66
119
  }
67
120
 
68
121
  // For updates, get the previous values
69
- const originalValues = isCreate
70
- ? null
71
- : {
72
- ...instance.previous(),
73
- customFields: instance.previous('customFields') || {},
74
- };
122
+ let originalValues = null;
123
+ if (!isCreate) {
124
+ // Create originalValues with our helper function
125
+ originalValues = manualObjectCopy(instance.previous());
126
+
127
+ // Add customFields separately
128
+ originalValues.customFields = instance.previous('customFields') || {};
129
+ }
130
+
131
+ // Get complete custom fields by merging DB values with update values
132
+ // This is especially important for partial updates to ensure all related fields are available
133
+ const completeCustomFields = !isCreate
134
+ ? await getCompleteCustomFields(instance, options)
135
+ : instance.customFields || {};
75
136
 
76
137
  // For debugging in case of update
77
- if (!isCreate) {
138
+ if (!isCreate && process.env.NODE_ENV !== 'production') {
139
+ // Create after object for logging
140
+ const logAfterObj = manualObjectCopy(instance.dataValues, { customFields: completeCustomFields });
141
+
78
142
  logger.debug('sadot - validate with values', {
79
143
  before: originalValues,
80
- after: {
81
- ...instance.dataValues,
82
- ...instance.customFields,
83
- },
84
- schema: JSON.stringify(validators[0].schema),
144
+ after: logAfterObj,
145
+ schema: validators[0].schema,
85
146
  });
86
147
  }
87
148
 
@@ -91,7 +152,7 @@ const validateModel = async (
91
152
  const typedSchema = schema as Record<string, any>;
92
153
 
93
154
  logger.debug('sadot - validating with schema', {
94
- schema: JSON.stringify(schema),
155
+ schema,
95
156
  hasAfterProps: !!typedSchema.properties?.after,
96
157
  hasBeforeProps: !!typedSchema.properties?.before,
97
158
  });
@@ -107,12 +168,12 @@ const validateModel = async (
107
168
  },
108
169
  });
109
170
 
110
- const isValid = validateSchema({
171
+ const isValid = validateSchema(JSON.parse(JSON.stringify({
111
172
  after: {
112
173
  ...instance.dataValues,
113
- customFields: instance.customFields,
174
+ customFields: completeCustomFields,
114
175
  },
115
- });
176
+ })));
116
177
 
117
178
  if (!isValid) {
118
179
  const errorDetails = validateSchema.errors?.map((err) =>
@@ -125,28 +186,34 @@ const validateModel = async (
125
186
  // For update operations, we need both before and after
126
187
  const validateSchema = ajv.compile(typedSchema);
127
188
 
128
- const isValid = validateSchema({
189
+ // Create after object with our helper function
190
+ const afterObj = manualObjectCopy(instance.dataValues);
191
+
192
+ // Add complete custom fields
193
+ afterObj.customFields = completeCustomFields;
194
+
195
+ // Create validation payload
196
+ const payload = {
129
197
  before: originalValues,
130
- after: {
131
- ...instance.dataValues,
132
- customFields: instance.customFields,
133
- },
134
- });
198
+ after: afterObj,
199
+ };
200
+
201
+ // Validate
202
+ const isValid = validateSchema(JSON.parse(JSON.stringify(payload)));
135
203
 
136
204
  logger.info('sadot - validation result', {
137
205
  isValid,
138
206
  test: {
139
207
  before: originalValues,
140
- after: {
141
- ...instance.dataValues,
142
- ...instance.customFields,
143
- },
208
+ after: afterObj,
144
209
  },
145
210
  });
146
211
 
147
212
  if (!isValid) {
148
- const errorDetails = validateSchema.errors?.map((err) =>
149
- `${(err as any).instancePath || ''} ${(err as any).message || 'Invalid value'}`).join(', ');
213
+ const errorDetails = validateSchema.errors?.map((err) => {
214
+ logger.info('dor', err);
215
+ return `${(err as any).instancePath || ''} ${(err as any).message || 'Invalid value'}`;
216
+ }).join(', ');
150
217
 
151
218
  throw new BadRequest([new Error(`Validation failed for ${modelType}: ${errorDetails}`)]);
152
219
  }
@@ -154,6 +221,49 @@ const validateModel = async (
154
221
  }
155
222
  };
156
223
 
224
+ const getFieldDefinitions = async ({
225
+ modelType,
226
+ modelOptions,
227
+ identifiers,
228
+ options,
229
+ }: {
230
+ modelType: any,
231
+ modelOptions: ModelOptions,
232
+ identifiers: any[],
233
+ options: any
234
+ }) => {
235
+ const { include, useEntityIdFromInclude } = modelOptions;
236
+ const where: WhereOptions = {
237
+ modelType,
238
+ disabled: false,
239
+ ...(!useEntityIdFromInclude && { entityId: identifiers }),
240
+ };
241
+
242
+ const fieldDefinitions = await DefinitionRepo.findAll(where, {
243
+ withDisabled: false,
244
+ transaction: options.transaction,
245
+ include: include?.(identifiers),
246
+ });
247
+ return fieldDefinitions;
248
+ };
249
+
250
+ const formatDates = (fieldDefinitions: CustomFieldDefinition[], instance: any) => {
251
+ (fieldDefinitions || []).forEach((fieldDefinition) => {
252
+ const { fieldType, name } = fieldDefinition;
253
+ if ([CustomFieldDefinitionType.DATE, CustomFieldDefinitionType.DATETIME].includes(fieldType)) {
254
+ const value = instance.customFields?.[name];
255
+ if (value) {
256
+ const validationError = Joi.date().validate(value).error;
257
+ if (validationError) {
258
+ throw new InvalidValueError(value, name, validationError);
259
+ }
260
+ // eslint-disable-next-line no-param-reassign
261
+ instance.customFields[name] = new Date(value).toISOString();
262
+ }
263
+ }
264
+ });
265
+ };
266
+
157
267
  /**
158
268
  * Hook to handle validation and custom fields during creation
159
269
  */
@@ -167,28 +277,16 @@ export const beforeCreate = (
167
277
  ): Promise<void> => {
168
278
  logger.debug('sadot - before create hook');
169
279
  const { fields } = options;
170
- const { include, useEntityIdFromInclude } = modelOptions;
171
280
  const modelType = instance.constructor.name;
172
281
 
173
282
  const identifiers = applyScopeToInstance(instance, scopeAttributes);
174
283
 
175
284
  // Step 1: Handle custom fields default values and required fields
176
- const where: WhereOptions = {
177
- modelType,
178
- disabled: false,
179
- ...(!useEntityIdFromInclude && { entityId: identifiers }),
180
- };
181
285
 
182
- const fieldDefinitions = await DefinitionRepo.findAll(where, {
183
- withDisabled: false,
184
- transaction: options.transaction,
185
- include: include?.(identifiers),
286
+ const fieldDefinitions = await getFieldDefinitions({
287
+ modelType, modelOptions, identifiers, options,
186
288
  });
187
289
 
188
- const requiredFieldsNames = Array.from(
189
- new Set(fieldDefinitions.filter(({ required }) => required).map(({ name }) => name)),
190
- );
191
-
192
290
  // Apply default values
193
291
  const fieldsWithDefaultValue = fieldDefinitions.filter((def) => ![null, undefined].includes(def.defaultValue));
194
292
  if (fieldsWithDefaultValue.length) {
@@ -203,6 +301,9 @@ export const beforeCreate = (
203
301
  }
204
302
 
205
303
  // Check for required fields
304
+ const requiredFieldsNames = Array.from(
305
+ new Set(fieldDefinitions.filter(({ required }) => required).map(({ name }) => name)),
306
+ );
206
307
  const { customFields } = instance;
207
308
  const fieldsNames = Object.keys(customFields ?? {});
208
309
  const missingFields = requiredFieldsNames.filter((name) => !fieldsNames.includes(name));
@@ -213,6 +314,9 @@ export const beforeCreate = (
213
314
  // Step 2: Validate the model data (including custom fields)
214
315
  await validateModel(instance, options, scopeAttributes, true);
215
316
 
317
+ // format date and datetime fields
318
+ formatDates(fieldDefinitions, instance);
319
+
216
320
  // Step 3: Save custom field values if they exist
217
321
  const customFieldsIdx = fields.indexOf('customFields');
218
322
  if (customFieldsIdx === -1 || !customFields || !Object.keys(customFields).length) {
@@ -254,9 +358,16 @@ export const beforeUpdate = (
254
358
  const modelType = instance.constructor.name;
255
359
  const identifiers = applyScopeToInstance(instance, scopeAttributes);
256
360
 
361
+ const fieldDefinitions = await getFieldDefinitions({
362
+ modelType, modelOptions, identifiers, options,
363
+ });
364
+
257
365
  // Step 1: Validate the model data (including custom fields)
258
366
  await validateModel(instance, options, scopeAttributes, false);
259
367
 
368
+ // format date and datetime fields
369
+ formatDates(fieldDefinitions, instance);
370
+
260
371
  // Step 2: Update custom field values if they exist
261
372
  const customFieldsIdx = fields.indexOf('customFields');
262
373
  if (customFieldsIdx > -1) {
package/src/index.ts CHANGED
@@ -17,11 +17,6 @@ export * from './utils/constants';
17
17
 
18
18
  export * from './utils/helpers';
19
19
 
20
- // Export repositories
21
- export * as DefinitionRepo from './repository/definition';
22
- export * as ValueRepo from './repository/value';
23
- export * as ValidatorRepo from './repository/validator';
24
-
25
20
  /**
26
21
  * Adding custom fields enrichment to the models inside the MODELS_FILE_NAME json file
27
22
  * @see {@link 'custom-fields/config'} for configurations
@@ -6,10 +6,12 @@ import {
6
6
  DataType,
7
7
  Default,
8
8
  AfterUpsert,
9
+ DefaultScope,
9
10
  } from 'sequelize-typescript';
10
11
  import { randomUUID } from 'node:crypto';
11
12
  import { sendDimEvent } from '../events';
12
13
 
14
+ @DefaultScope(() => ({ where: { disabled: false } }))
13
15
  @Table({
14
16
  timestamps: true,
15
17
  })
@@ -68,6 +68,22 @@ const initTables = async (
68
68
  };
69
69
  });
70
70
 
71
+ CustomValidator.addScope('userScope', () => {
72
+ const user = getUser();
73
+ if (!user?.permissions) {
74
+ return {};
75
+ }
76
+ return {
77
+ where: {
78
+ entityId: [
79
+ ...Object.keys(user.permissions.fleets),
80
+ ...Object.keys(user.permissions.businessModels),
81
+ ...Object.keys(user.permissions.demandSources),
82
+ ],
83
+ },
84
+ };
85
+ });
86
+
71
87
  logger.info('custom-fields: models added');
72
88
 
73
89
  const SequelizeMeta = sequelize.define(
@@ -11,7 +11,7 @@ export interface ValidatorAttributes {
11
11
  entityId: string;
12
12
  entityType: string;
13
13
  modelType: string;
14
- schema: Record<string, unknown>;
14
+ schema: CustomValidator['schema'];
15
15
  disabled?: boolean;
16
16
  [key: string]: unknown; // Add index signature for Sequelize compatibility
17
17
  }
@@ -36,14 +36,22 @@ export const findAll = async (
36
36
 
37
37
  const { transaction, withDisabled } = options;
38
38
 
39
- const fullWhere = withDisabled
40
- ? where
41
- : { ...where, disabled: false };
42
-
43
- const validators = await CustomValidator.findAll({
44
- where: fullWhere,
45
- transaction,
46
- });
39
+ let validators;
40
+ if (withDisabled) {
41
+ // If withDisabled is true, use unscoped to ignore the default scope that filters disabled items
42
+ // Apply the userScope separately to maintain permission filtering
43
+ validators = await CustomValidator.unscoped().scope('userScope').findAll({
44
+ where,
45
+ transaction,
46
+ });
47
+ } else {
48
+ // Use defaultScope and userScope to filter both disabled and by permissions
49
+ // The defaultScope keeps only non-disabled validators
50
+ validators = await CustomValidator.scope(['defaultScope', 'userScope']).findAll({
51
+ where,
52
+ transaction,
53
+ });
54
+ }
47
55
 
48
56
  return validators;
49
57
  };
@@ -2,7 +2,7 @@ export default {
2
2
  test: {
3
3
  username: process.env.DB_USERNAME || '',
4
4
  password: process.env.DB_PASSWORD || null,
5
- database: process.env.DB_NAME || 'sadot_package_test',
5
+ database: process.env.DB_NAME || 'postgres',
6
6
  host: process.env.DB_HOST || '127.0.0.1',
7
7
  port: process.env.DB_PORT || 5432,
8
8
  dialect: process.env.DB_TYPE || 'postgres',
package/validator-test.js DELETED
@@ -1,79 +0,0 @@
1
- const Ajv = require("ajv");
2
- const addFormats = require("ajv-formats");
3
- const ajv = new Ajv({
4
- allErrors: true,
5
- strict: false, // Disable strict mode to avoid warnings
6
- strictTypes: false, // Disable strict type checking
7
- $data: true, // Enable $data references
8
- }); // options can be passed, e.g. {allErrors: true}
9
- addFormats(ajv);
10
-
11
-
12
- const schema = {
13
- "type": "object",
14
- "properties": {
15
- "after": {
16
- "type": "object",
17
- "properties": {
18
- "customFields": {
19
- "type": "object",
20
- "properties": {
21
- "actualStartTime": {
22
- "type": "string",
23
- "format": "date-time"
24
- },
25
- "actualEndTime": {
26
- "type": "string",
27
- "format": "date-time",
28
- "formatMinimum": {
29
- "$data": "/after/customFields/actualStartTime"
30
- }
31
- }
32
- }
33
- }
34
- }
35
- }
36
- }
37
- };
38
-
39
- const validate = ajv.compile(schema);
40
-
41
- const data = {
42
- "id": "c9c87cec-28c7-4a1f-9376-4d8a4ba0f49c",
43
- "typeId": "4fcd7096-4c90-46a2-a20e-91bb5bff3ced",
44
- "priorityId": null,
45
- "statusId": "51d28a37-c042-429f-a9cd-fbc81bba2b39",
46
- "subjectId": null,
47
- "subjectType": null,
48
- "title": "ff",
49
- "description": null,
50
- "dueTime": null,
51
- "startTime": null,
52
- "endTime": null,
53
- "driverId": null,
54
- "vendorId": null,
55
- "userId": null,
56
- "businessModelId": "72ecf740-42a1-46f4-81ca-8132b08c4f04",
57
- "createdAt": "2025-03-12T12:58:05.529Z",
58
- "updatedAt": "2025-03-12T12:58:05.529Z",
59
- "deletedAt": null,
60
- "actions": [],
61
- "customFields": {
62
- "actualStartTime": "2025-03-14T12:48:01.373Z",
63
- "actualEndTime": "2025-03-01T12:51:46.685Z"
64
- },
65
- "isBlocker": false
66
- };
67
-
68
- const valid = validate({after: data});
69
-
70
- if (!valid) {
71
- console.log("invalid, the errors is: ");
72
- validate.errors.forEach((error) =>
73
- console.log(error.instancePath, error.message)
74
- );
75
- } else {
76
- console.log("valid");
77
- }
78
-
79
- console.log("--------------------");