@autofleet/sadot 0.12.0 → 0.13.0-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.
Files changed (124) hide show
  1. package/package.json +3 -1
  2. package/src/api/v1/index.ts +2 -0
  3. package/src/api/v1/validator/index.ts +127 -0
  4. package/src/api/v1/validator/validations.ts +39 -0
  5. package/src/events/index.ts +1 -1
  6. package/src/hooks/hooks.ts +304 -0
  7. package/src/hooks/index.ts +10 -5
  8. package/src/index.ts +6 -0
  9. package/src/models/CustomValidator.ts +76 -0
  10. package/src/models/index.ts +7 -2
  11. package/src/repository/validator.ts +91 -0
  12. package/src/tests/helpers/commonHooks.ts +9 -2
  13. package/src/tests/helpers/index.ts +7 -2
  14. package/src/types/index.ts +1 -0
  15. package/src/utils/validations/schema/README.md +93 -0
  16. package/src/utils/validations/schema/validator-schema.ts +103 -0
  17. package/dist/api/index.d.ts +0 -3
  18. package/dist/api/index.js +0 -12
  19. package/dist/api/v1/definition/index.d.ts +0 -3
  20. package/dist/api/v1/definition/index.js +0 -116
  21. package/dist/api/v1/definition/validations.d.ts +0 -2
  22. package/dist/api/v1/definition/validations.js +0 -77
  23. package/dist/api/v1/errors.d.ts +0 -4
  24. package/dist/api/v1/errors.js +0 -12
  25. package/dist/api/v1/index.d.ts +0 -3
  26. package/dist/api/v1/index.js +0 -11
  27. package/dist/errors/index.d.ts +0 -24
  28. package/dist/errors/index.js +0 -66
  29. package/dist/events/index.d.ts +0 -4
  30. package/dist/events/index.js +0 -50
  31. package/dist/hooks/create.d.ts +0 -10
  32. package/dist/hooks/create.js +0 -95
  33. package/dist/hooks/enrich.d.ts +0 -30
  34. package/dist/hooks/enrich.js +0 -159
  35. package/dist/hooks/find.d.ts +0 -1
  36. package/dist/hooks/find.js +0 -29
  37. package/dist/hooks/index.d.ts +0 -6
  38. package/dist/hooks/index.js +0 -18
  39. package/dist/hooks/update.d.ts +0 -10
  40. package/dist/hooks/update.js +0 -49
  41. package/dist/hooks/utils/updateInstanceValues.d.ts +0 -15
  42. package/dist/hooks/utils/updateInstanceValues.js +0 -50
  43. package/dist/hooks/workaround.d.ts +0 -10
  44. package/dist/hooks/workaround.js +0 -37
  45. package/dist/index.d.ts +0 -13
  46. package/dist/index.js +0 -67
  47. package/dist/models/CustomFieldDefinition.d.ts +0 -25
  48. package/dist/models/CustomFieldDefinition.js +0 -192
  49. package/dist/models/CustomFieldEntries.d.ts +0 -15
  50. package/dist/models/CustomFieldEntries.js +0 -123
  51. package/dist/models/CustomFieldValue.d.ts +0 -16
  52. package/dist/models/CustomFieldValue.js +0 -151
  53. package/dist/models/index.d.ts +0 -17
  54. package/dist/models/index.js +0 -113
  55. package/dist/models/tests/AssociatedTestModel.d.ts +0 -12
  56. package/dist/models/tests/AssociatedTestModel.js +0 -71
  57. package/dist/models/tests/TestModel.d.ts +0 -12
  58. package/dist/models/tests/TestModel.js +0 -69
  59. package/dist/models/tests/contextAwareModels/ContextAwareTestModel.d.ts +0 -10
  60. package/dist/models/tests/contextAwareModels/ContextAwareTestModel.js +0 -53
  61. package/dist/models/tests/contextAwareModels/ContextTestModel.d.ts +0 -13
  62. package/dist/models/tests/contextAwareModels/ContextTestModel.js +0 -47
  63. package/dist/repository/definition.d.ts +0 -36
  64. package/dist/repository/definition.js +0 -121
  65. package/dist/repository/entries.d.ts +0 -13
  66. package/dist/repository/entries.js +0 -92
  67. package/dist/repository/utils/formatValues.d.ts +0 -3
  68. package/dist/repository/utils/formatValues.js +0 -16
  69. package/dist/repository/value.d.ts +0 -28
  70. package/dist/repository/value.js +0 -124
  71. package/dist/scopes/filter.d.ts +0 -30
  72. package/dist/scopes/filter.js +0 -75
  73. package/dist/scopes/helpers/filter.helpers.d.ts +0 -42
  74. package/dist/scopes/helpers/filter.helpers.js +0 -204
  75. package/dist/scopes/index.d.ts +0 -2
  76. package/dist/scopes/index.js +0 -6
  77. package/dist/tests/api/test-api.d.ts +0 -2
  78. package/dist/tests/api/test-api.js +0 -38
  79. package/dist/tests/functional/searching/index.d.ts +0 -8
  80. package/dist/tests/functional/searching/index.js +0 -44
  81. package/dist/tests/helpers/commonHooks.d.ts +0 -5
  82. package/dist/tests/helpers/commonHooks.js +0 -55
  83. package/dist/tests/helpers/database-config.d.ts +0 -16
  84. package/dist/tests/helpers/database-config.js +0 -17
  85. package/dist/tests/helpers/index.d.ts +0 -7
  86. package/dist/tests/helpers/index.js +0 -29
  87. package/dist/tests/mocks/definition.mock.d.ts +0 -48
  88. package/dist/tests/mocks/definition.mock.js +0 -78
  89. package/dist/tests/mocks/events.mock.d.ts +0 -4
  90. package/dist/tests/mocks/events.mock.js +0 -21
  91. package/dist/tests/mocks/testModel.d.ts +0 -12
  92. package/dist/tests/mocks/testModel.js +0 -35
  93. package/dist/types/definition/index.d.ts +0 -25
  94. package/dist/types/definition/index.js +0 -2
  95. package/dist/types/entries/index.d.ts +0 -25
  96. package/dist/types/entries/index.js +0 -2
  97. package/dist/types/index.d.ts +0 -34
  98. package/dist/types/index.js +0 -2
  99. package/dist/types/value/index.d.ts +0 -15
  100. package/dist/types/value/index.js +0 -2
  101. package/dist/utils/constants/index.d.ts +0 -19
  102. package/dist/utils/constants/index.js +0 -22
  103. package/dist/utils/db/index.d.ts +0 -4
  104. package/dist/utils/db/index.js +0 -24
  105. package/dist/utils/helpers/index.d.ts +0 -26
  106. package/dist/utils/helpers/index.js +0 -40
  107. package/dist/utils/init.d.ts +0 -7
  108. package/dist/utils/init.js +0 -109
  109. package/dist/utils/logger/index.d.ts +0 -3
  110. package/dist/utils/logger/index.js +0 -42
  111. package/dist/utils/scopeAttributes.d.ts +0 -2
  112. package/dist/utils/scopeAttributes.js +0 -11
  113. package/dist/utils/validations/index.d.ts +0 -8
  114. package/dist/utils/validations/index.js +0 -41
  115. package/dist/utils/validations/schema/custom-fields.d.ts +0 -3
  116. package/dist/utils/validations/schema/custom-fields.js +0 -9
  117. package/dist/utils/validations/type.d.ts +0 -15
  118. package/dist/utils/validations/type.js +0 -2
  119. package/dist/utils/validations/validators/index.d.ts +0 -14
  120. package/dist/utils/validations/validators/index.js +0 -40
  121. package/dist/utils/validations/validators/select.validator.d.ts +0 -5
  122. package/dist/utils/validations/validators/select.validator.js +0 -12
  123. package/dist/utils/validations/validators/status.validator.d.ts +0 -12
  124. package/dist/utils/validations/validators/status.validator.js +0 -15
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autofleet/sadot",
3
- "version": "0.12.0",
3
+ "version": "0.13.0-beta.0",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -29,6 +29,8 @@
29
29
  "dependencies": {
30
30
  "@autofleet/common-types": "^4.4.0",
31
31
  "@autofleet/events": "^4.0.0",
32
+ "ajv": "^8.12.0",
33
+ "http-status-codes": "^2.3.0",
32
34
  "joi": "^17.7.0",
33
35
  "pg": "^8.10.0",
34
36
  "reflect-metadata": "^0.1.13",
@@ -1,9 +1,11 @@
1
1
  import { Router } from '@autofleet/node-common';
2
2
  import logger from '../../utils/logger';
3
3
  import definitionRouter from './definition';
4
+ import validatorRouter from './validator';
4
5
 
5
6
  const router = Router({ logger });
6
7
 
7
8
  router.use('/custom-field-definitions/:modelName', definitionRouter);
9
+ router.use('/custom-validators/:modelName', validatorRouter);
8
10
 
9
11
  export default router;
@@ -0,0 +1,127 @@
1
+ import { ResourceNotFoundError } from '@autofleet/errors';
2
+ import { Router } from '@autofleet/node-common';
3
+ import { StatusCodes } from 'http-status-codes';
4
+ import handleError from '../errors';
5
+ import * as ValidatorRepo from '../../../repository/validator';
6
+ import validations from './validations';
7
+ import logger from '../../../utils/logger';
8
+ import { validateValidatorSchema } from '../../../utils/validations/schema/validator-schema';
9
+ import type { CustomValidator } from '../../../models';
10
+
11
+ const router = Router({ logger });
12
+ const ENTITY = 'CustomValidator';
13
+
14
+ /**
15
+ * Create
16
+ */
17
+ router.post<{ modelName: string }>('/', async (req, res) => {
18
+ const { modelName } = req.params;
19
+ try {
20
+ // Validate the request body
21
+ const validatedPayload = await validations.create.validateAsync(req.body);
22
+
23
+ // Validate that the schema is a valid AJV schema
24
+ validateValidatorSchema(validatedPayload.schema);
25
+
26
+ const validator = await ValidatorRepo.create({
27
+ ...validatedPayload,
28
+ modelType: modelName,
29
+ });
30
+
31
+ return res.status(StatusCodes.CREATED).json(validator);
32
+ } catch (err) {
33
+ return handleError(err, res, { logger, message: `Error in create ${ENTITY} request` });
34
+ }
35
+ });
36
+
37
+ /**
38
+ * Get all
39
+ */
40
+ router.get<
41
+ { modelName: string },
42
+ CustomValidator[],
43
+ never,
44
+ { entityId?: string; entityType?: string }
45
+ >('/', async (req, res) => {
46
+ try {
47
+ const { modelName } = req.params;
48
+ const { entityId, entityType } = req.query;
49
+
50
+ const where = {
51
+ modelType: modelName,
52
+ ...(entityId && { entityId }),
53
+ ...(entityType && { entityType }),
54
+ };
55
+
56
+ const validators = await ValidatorRepo.findAll(where);
57
+
58
+ return res.status(StatusCodes.OK).json(validators);
59
+ } catch (err) {
60
+ return handleError(err, res, { logger, message: `Error in get all ${ENTITY} request` });
61
+ }
62
+ });
63
+
64
+ /**
65
+ * Get by id
66
+ */
67
+ router.get<{ modelName: string; validatorId: string }, CustomValidator>('/:validatorId', async (req, res) => {
68
+ try {
69
+ const { validatorId } = req.params;
70
+ const validators = await ValidatorRepo.findAll({ id: validatorId });
71
+
72
+ if (!validators.length) {
73
+ throw new ResourceNotFoundError('Validator not found');
74
+ }
75
+
76
+ return res.status(StatusCodes.OK).json(validators[0]);
77
+ } catch (err) {
78
+ return handleError(err, res, { logger, message: `Error in get ${ENTITY} request` });
79
+ }
80
+ });
81
+
82
+ /**
83
+ * Update
84
+ */
85
+ router.patch<{ modelName: string; validatorId: string }, CustomValidator>('/:validatorId', async (req, res) => {
86
+ try {
87
+ const { validatorId } = req.params;
88
+
89
+ // Validate the request body
90
+ const validatedPayload = await validations.update.validateAsync(req.body);
91
+
92
+ // If schema is included in the update, validate that it's a valid AJV schema
93
+ if (validatedPayload.schema) {
94
+ validateValidatorSchema(validatedPayload.schema);
95
+ }
96
+
97
+ const [count, validators] = await ValidatorRepo.update(validatorId, validatedPayload);
98
+
99
+ if (!count) {
100
+ throw new ResourceNotFoundError('Validator not found');
101
+ }
102
+
103
+ return res.status(StatusCodes.OK).json(validators[0]);
104
+ } catch (err) {
105
+ return handleError(err, res, { logger, message: `Error in update ${ENTITY} request` });
106
+ }
107
+ });
108
+
109
+ /**
110
+ * Delete (disable)
111
+ */
112
+ router.delete<{ modelName: string; validatorId: string }>('/:validatorId', async (req, res) => {
113
+ try {
114
+ const { validatorId } = req.params;
115
+ const [count] = await ValidatorRepo.disable(validatorId);
116
+
117
+ if (!count) {
118
+ throw new ResourceNotFoundError('Validator not found');
119
+ }
120
+
121
+ return res.status(StatusCodes.NO_CONTENT).send();
122
+ } catch (err) {
123
+ return handleError(err, res, { logger, message: `Error in delete ${ENTITY} request` });
124
+ }
125
+ });
126
+
127
+ export default router;
@@ -0,0 +1,39 @@
1
+ import Joi from 'joi';
2
+
3
+ // Schema for validating JSON Schema objects
4
+ const jsonSchemaValidation = Joi.object().unknown(true);
5
+
6
+ const validationSchemas = {
7
+ create: Joi.object({
8
+ entityId: Joi.string().uuid().required(),
9
+ entityType: Joi.string().required(),
10
+ schema: Joi.object({
11
+ type: Joi.string().valid('object').required(),
12
+ properties: Joi.object({
13
+ before: jsonSchemaValidation,
14
+ after: jsonSchemaValidation,
15
+ }).required(),
16
+ if: Joi.object().optional(),
17
+ then: Joi.object().optional(),
18
+ else: Joi.object().optional(),
19
+ }).required(),
20
+ }),
21
+
22
+ update: Joi.object({
23
+ entityId: Joi.string().uuid(),
24
+ entityType: Joi.string(),
25
+ schema: Joi.object({
26
+ type: Joi.string().valid('object'),
27
+ properties: Joi.object({
28
+ before: jsonSchemaValidation,
29
+ after: jsonSchemaValidation,
30
+ }),
31
+ if: Joi.object().optional(),
32
+ then: Joi.object().optional(),
33
+ else: Joi.object().optional(),
34
+ }),
35
+ disabled: Joi.boolean(),
36
+ }).min(1),
37
+ };
38
+
39
+ export default validationSchemas;
@@ -47,7 +47,7 @@ export const sendDimEvent = (instance): void => {
47
47
  mapping.tableName,
48
48
  mapping.eventVersion,
49
49
  objectToSend,
50
- ).catch(() => null);
50
+ ).catch(() => {});
51
51
  };
52
52
 
53
53
  export default events;
@@ -0,0 +1,304 @@
1
+ import type { WhereOptions } from 'sequelize';
2
+ import Ajv from 'ajv';
3
+ import { BadRequest } from '@autofleet/errors';
4
+ import logger from '../utils/logger';
5
+ import * as ValidatorRepo from '../repository/validator';
6
+ import * as DefinitionRepo from '../repository/definition';
7
+ import { MissingRequiredCustomFieldError } from '../errors';
8
+ import type { CustomFieldOptions, ModelOptions } from '../types';
9
+ import applyScopeToInstance from '../utils/scopeAttributes';
10
+ import updateInstanceValues from './utils/updateInstanceValues';
11
+
12
+ // Initialize Ajv with relaxed settings to avoid warnings
13
+ const ajv = new Ajv({
14
+ allErrors: true,
15
+ strict: false, // Disable strict mode to avoid warnings
16
+ strictTypes: false, // Disable strict type checking
17
+ });
18
+
19
+ /**
20
+ * Validates the model using custom validators
21
+ */
22
+ const validateModel = async (
23
+ instance,
24
+ options,
25
+ scopeAttributes: string[],
26
+ isCreate = false,
27
+ ): Promise<void> => {
28
+ const modelType = instance.constructor.name;
29
+
30
+ logger.debug('sadot - validating model', { isCreate, modelType });
31
+ const identifiers = applyScopeToInstance(instance, scopeAttributes);
32
+
33
+ logger.debug('sadot - identifiers', { identifiers });
34
+
35
+ // Skip if no identifiers
36
+ if (!identifiers || Object.keys(identifiers).length === 0) {
37
+ logger.debug('sadot - skipping validation: no identifiers');
38
+ return;
39
+ }
40
+
41
+ // Find the entityId from identifiers (fleetId, businessModelId, etc.)
42
+ const entityId = Object.values(identifiers)[0]; // Get the first value as entityId
43
+
44
+ logger.debug('sadot - entityId', { entityId });
45
+
46
+ if (!entityId) {
47
+ logger.debug('sadot - skipping validation: no entityId');
48
+ return;
49
+ }
50
+
51
+ const validators = await ValidatorRepo.findAllByModelType(
52
+ modelType,
53
+ entityId,
54
+ { transaction: options.transaction },
55
+ );
56
+
57
+ logger.debug('sadot - validators found', { count: validators.length });
58
+
59
+ if (!validators.length) {
60
+ logger.debug('sadot - skipping validation: no validators found');
61
+ return;
62
+ }
63
+
64
+ // For updates, get the previous values
65
+ const originalValues = isCreate
66
+ ? null
67
+ : {
68
+ ...instance.previous(),
69
+ customFields: instance.previous('customFields') || {},
70
+ };
71
+
72
+ // For debugging in case of update
73
+ if (!isCreate) {
74
+ logger.debug('sadot - validate with values', {
75
+ before: originalValues,
76
+ after: {
77
+ ...instance.dataValues,
78
+ ...instance.customFields,
79
+ },
80
+ schema: JSON.stringify(validators[0].schema),
81
+ });
82
+ }
83
+
84
+ // eslint-disable-next-line no-restricted-syntax
85
+ for (const validator of validators) {
86
+ const { schema } = validator;
87
+ const typedSchema = schema as Record<string, any>;
88
+
89
+ logger.debug('sadot - validating with schema', {
90
+ schema: JSON.stringify(schema),
91
+ hasAfterProps: !!typedSchema.properties?.after,
92
+ hasBeforeProps: !!typedSchema.properties?.before,
93
+ });
94
+
95
+ if (isCreate) {
96
+ // For create operations, we only need the 'after' state
97
+ if (typedSchema.properties?.after) {
98
+ const validateSchema = ajv.compile({
99
+ ...schema,
100
+ // Focus only on the 'after' validation part for create
101
+ properties: {
102
+ after: typedSchema.properties.after,
103
+ },
104
+ });
105
+
106
+ const isValid = validateSchema({
107
+ after: {
108
+ ...instance.dataValues,
109
+ ...instance.customFields,
110
+ },
111
+ });
112
+
113
+ if (!isValid) {
114
+ const errorDetails = validateSchema.errors?.map((err) =>
115
+ `${(err as any).instancePath || ''} ${(err as any).message || 'Invalid value'}`).join(', ');
116
+
117
+ throw new BadRequest([new Error(`Validation failed for ${modelType}: ${errorDetails}`)], [`Validation failed for ${modelType}`]);
118
+ }
119
+ } else if (!typedSchema.properties?.before && !typedSchema.properties?.after) {
120
+ // If no before/after properties are defined, validate the entire schema against the model
121
+ const validateSchema = ajv.compile(schema);
122
+
123
+ const isValid = validateSchema({
124
+ ...instance.dataValues,
125
+ ...instance.customFields,
126
+ });
127
+
128
+ if (!isValid) {
129
+ const errorDetails = validateSchema.errors?.map((err) =>
130
+ `${(err as any).instancePath || ''} ${(err as any).message || 'Invalid value'}`).join(', ');
131
+
132
+ throw new BadRequest([new Error(`Validation failed for ${modelType}: ${errorDetails}`)], [`Validation failed for ${modelType}`]);
133
+ }
134
+ }
135
+ } else {
136
+ // For update operations, we need both before and after
137
+ const validateSchema = ajv.compile(typedSchema);
138
+
139
+ const isValid = validateSchema({
140
+ before: originalValues,
141
+ after: {
142
+ ...instance.dataValues,
143
+ ...instance.customFields,
144
+ },
145
+ });
146
+
147
+ if (!isValid) {
148
+ const errorDetails = validateSchema.errors?.map((err) =>
149
+ `${(err as any).instancePath || ''} ${(err as any).message || 'Invalid value'}`).join(', ');
150
+
151
+ throw new BadRequest([new Error(`Validation failed for ${modelType}: ${errorDetails}`)], [`Validation failed for ${modelType}`]);
152
+ }
153
+ }
154
+ }
155
+ };
156
+
157
+ /**
158
+ * Hook to handle validation and custom fields during creation
159
+ */
160
+ export const beforeCreate = (
161
+ scopeAttributes: string[],
162
+ modelOptions: ModelOptions = {},
163
+ sadotOptions: Pick<CustomFieldOptions, 'useCustomFieldsEntries'> = { useCustomFieldsEntries: false },
164
+ ) => async (
165
+ instance,
166
+ options,
167
+ ): Promise<void> => {
168
+ logger.debug('sadot - before create hook');
169
+ const { fields } = options;
170
+ const { include, useEntityIdFromInclude } = modelOptions;
171
+ const modelType = instance.constructor.name;
172
+
173
+ const identifiers = applyScopeToInstance(instance, scopeAttributes);
174
+
175
+ // 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
+
182
+ const fieldDefinitions = await DefinitionRepo.findAll(where, {
183
+ withDisabled: false,
184
+ transaction: options.transaction,
185
+ include: include?.(identifiers),
186
+ });
187
+
188
+ const requiredFieldsNames = Array.from(
189
+ new Set(fieldDefinitions.filter(({ required }) => required).map(({ name }) => name)),
190
+ );
191
+
192
+ // Apply default values
193
+ const fieldsWithDefaultValue = fieldDefinitions.filter((def) => ![null, undefined].includes(def.defaultValue));
194
+ if (fieldsWithDefaultValue.length) {
195
+ // eslint-disable-next-line no-param-reassign
196
+ instance.customFields ||= {};
197
+ fieldsWithDefaultValue
198
+ .filter((def) => (instance.customFields?.[def.name] === undefined))
199
+ .forEach(({ name, defaultValue }) => {
200
+ // eslint-disable-next-line no-param-reassign
201
+ instance.customFields[name] = defaultValue;
202
+ });
203
+ }
204
+
205
+ // Check for required fields
206
+ const { customFields } = instance;
207
+ const fieldsNames = Object.keys(customFields ?? {});
208
+ const missingFields = requiredFieldsNames.filter((name) => !fieldsNames.includes(name));
209
+ if (missingFields?.length) {
210
+ throw new MissingRequiredCustomFieldError(missingFields);
211
+ }
212
+
213
+ // Step 2: Validate the model data (including custom fields)
214
+ await validateModel(instance, options, scopeAttributes, true);
215
+
216
+ // Step 3: Save custom field values if they exist
217
+ const customFieldsIdx = fields.indexOf('customFields');
218
+ if (customFieldsIdx === -1 || !customFields || !Object.keys(customFields).length) {
219
+ // No custom fields to update
220
+ return;
221
+ }
222
+
223
+ // Save custom field values
224
+ await updateInstanceValues({
225
+ modelId: instance.id,
226
+ modelType,
227
+ identifiers,
228
+ customFields,
229
+ options: {
230
+ useCustomFieldsEntries: sadotOptions.useCustomFieldsEntries,
231
+ transaction: options.transaction,
232
+ modelOptions,
233
+ },
234
+ });
235
+
236
+ // Remove customFields from fields array after handling
237
+ // eslint-disable-next-line no-param-reassign
238
+ fields.splice(customFieldsIdx, 1);
239
+ };
240
+
241
+ /**
242
+ * Hook to handle validation and custom fields during update
243
+ */
244
+ export const beforeUpdate = (
245
+ scopeAttributes: string[],
246
+ modelOptions: ModelOptions = {},
247
+ sadotOptions: Pick<CustomFieldOptions, 'useCustomFieldsEntries'> = { useCustomFieldsEntries: false },
248
+ ) => async (
249
+ instance,
250
+ options,
251
+ ): Promise<void> => {
252
+ logger.debug('sadot - before update hook');
253
+ const { fields } = options;
254
+ const modelType = instance.constructor.name;
255
+ const identifiers = applyScopeToInstance(instance, scopeAttributes);
256
+
257
+ // Step 1: Validate the model data (including custom fields)
258
+ await validateModel(instance, options, scopeAttributes, false);
259
+
260
+ // Step 2: Update custom field values if they exist
261
+ const customFieldsIdx = fields.indexOf('customFields');
262
+ if (customFieldsIdx > -1) {
263
+ const { customFields } = instance;
264
+
265
+ if (!Object.keys(customFields).length) {
266
+ return;
267
+ }
268
+
269
+ // Save custom field values
270
+ await updateInstanceValues({
271
+ modelId: instance.id,
272
+ modelType,
273
+ identifiers,
274
+ customFields,
275
+ options: {
276
+ useCustomFieldsEntries: sadotOptions.useCustomFieldsEntries,
277
+ transaction: options.transaction,
278
+ modelOptions,
279
+ },
280
+ });
281
+
282
+ // Remove customFields from fields array after handling
283
+ // eslint-disable-next-line no-param-reassign
284
+ fields.splice(customFieldsIdx, 1);
285
+ }
286
+ };
287
+
288
+ /**
289
+ * Hook to enable individual hooks for bulk create operations
290
+ */
291
+ export const beforeBulkCreate = (options): void => {
292
+ // This will activate the beforeCreate hook on each instance
293
+ // eslint-disable-next-line no-param-reassign
294
+ options.individualHooks = true;
295
+ };
296
+
297
+ /**
298
+ * Hook to enable individual hooks for bulk update operations
299
+ */
300
+ export const beforeBulkUpdate = (options): void => {
301
+ // This will activate the beforeUpdate hook on each instance
302
+ // eslint-disable-next-line no-param-reassign
303
+ options.individualHooks = true;
304
+ };
@@ -1,15 +1,20 @@
1
1
  import enrichResults from './enrich';
2
2
  import { beforeFind } from './find';
3
- import { beforeBulkUpdate, beforeUpdate } from './update';
4
- import { beforeBulkCreate, beforeCreate } from './create';
5
3
  import workaround from './workaround';
4
+ import {
5
+ beforeCreate,
6
+ beforeUpdate,
7
+ beforeBulkCreate,
8
+ beforeBulkUpdate,
9
+ } from './hooks';
6
10
 
11
+ // Export the hooks
7
12
  export {
8
13
  enrichResults,
9
14
  beforeFind,
10
- beforeBulkUpdate,
15
+ workaround,
16
+ beforeCreate,
11
17
  beforeUpdate,
12
18
  beforeBulkCreate,
13
- beforeCreate,
14
- workaround,
19
+ beforeBulkUpdate,
15
20
  };
package/src/index.ts CHANGED
@@ -17,6 +17,11 @@ 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
+
20
25
  /**
21
26
  * Adding custom fields enrichment to the models inside the MODELS_FILE_NAME json file
22
27
  * @see {@link 'custom-fields/config'} for configurations
@@ -40,6 +45,7 @@ const useCustomFields = async (
40
45
  await initTables(sequelize, options.getUser, { useCustomFieldsEntries });
41
46
  addScopes(models, getModel, { useCustomFieldsEntries });
42
47
  applyCustomAssociation(models);
48
+
43
49
  logger.debug('sadot - custom fields finished initializing with models', models);
44
50
  return sequelize;
45
51
  };
@@ -0,0 +1,76 @@
1
+ import {
2
+ Table,
3
+ Column,
4
+ Model,
5
+ PrimaryKey,
6
+ DataType,
7
+ Default,
8
+ AfterUpsert,
9
+ } from 'sequelize-typescript';
10
+ import { randomUUID } from 'node:crypto';
11
+ import { sendDimEvent } from '../events';
12
+
13
+ @Table({
14
+ timestamps: true,
15
+ })
16
+ class CustomValidator extends Model {
17
+ @PrimaryKey
18
+ @Default(() => randomUUID())
19
+ @Column({
20
+ type: DataType.UUID,
21
+ allowNull: false,
22
+ })
23
+ id!: string;
24
+
25
+ @Column({
26
+ type: DataType.UUID,
27
+ allowNull: false,
28
+ })
29
+ /** The ID of the entity for which this validator is defined, e.g. fleetId / etc. */
30
+ entityId!: string;
31
+
32
+ @Column({
33
+ type: DataType.STRING,
34
+ allowNull: false,
35
+ })
36
+ /** The type of entity (fleet, businessModel, etc). */
37
+ entityType!: string;
38
+
39
+ @Column({
40
+ type: DataType.STRING,
41
+ allowNull: false,
42
+ })
43
+ /** The type of model this validator applies to. e.g. Vehicle / StopPoint / etc. */
44
+ modelType!: string;
45
+
46
+ @Column({
47
+ type: DataType.JSONB,
48
+ allowNull: false,
49
+ })
50
+ /** JSON Schema to validate models against */
51
+ schema!: Record<string, unknown>;
52
+
53
+ @Column({
54
+ type: DataType.BOOLEAN,
55
+ allowNull: false,
56
+ defaultValue: false,
57
+ })
58
+ disabled!: boolean;
59
+
60
+ @Column
61
+ createdAt?: Date;
62
+
63
+ @Column
64
+ updatedAt?: Date;
65
+
66
+ @AfterUpsert
67
+ static afterSaveHandler(instance: CustomValidator, options): void {
68
+ if (options.transaction) {
69
+ options.transaction.afterCommit(() => sendDimEvent(instance));
70
+ } else {
71
+ sendDimEvent(instance);
72
+ }
73
+ }
74
+ }
75
+
76
+ export default CustomValidator;
@@ -10,15 +10,16 @@ import ContextTestModel from './tests/contextAwareModels/ContextTestModel';
10
10
  import AssociatedTestModel from './tests/AssociatedTestModel';
11
11
  import type { CustomFieldOptions } from '../types';
12
12
  import CustomFieldEntries from './CustomFieldEntries';
13
+ import CustomValidator from './CustomValidator';
13
14
 
14
- type ProductionModel = typeof CustomFieldDefinition | typeof CustomFieldValue | typeof CustomFieldEntries
15
+ type ProductionModel = typeof CustomFieldDefinition | typeof CustomFieldValue | typeof CustomFieldEntries | typeof CustomValidator
15
16
  interface InitTablesOptions {
16
17
  schemaPrefix?: string
17
18
  schemaVersion?: string
18
19
  useCustomFieldsEntries?: boolean
19
20
  }
20
21
 
21
- const productionModels: ProductionModel[] = [CustomFieldDefinition, CustomFieldValue];
22
+ const productionModels: ProductionModel[] = [CustomFieldDefinition, CustomFieldValue, CustomValidator];
22
23
  const testModels = [TestModel, AssociatedTestModel, ContextAwareTestModel, ContextTestModel];
23
24
 
24
25
  const SADOT_MIGRATION_PREFIX = 'sadot-migration';
@@ -99,6 +100,9 @@ const initTables = async (
99
100
  await CustomFieldEntries.sync({ alter: true });
100
101
  }
101
102
 
103
+ // Always sync CustomValidator
104
+ await CustomValidator.sync({ alter: true });
105
+
102
106
  if (expectedSchemaVersionIndex === -1) {
103
107
  await SequelizeMeta.create({ name: CUSTOM_FIELDS_SCHEMA_VERSION });
104
108
  }
@@ -136,6 +140,7 @@ export {
136
140
  CustomFieldValue,
137
141
  CustomFieldDefinition,
138
142
  CustomFieldEntries,
143
+ CustomValidator,
139
144
  TestModel,
140
145
  AssociatedTestModel,
141
146
  ContextAwareTestModel,