@autofleet/sadot 1.0.0-beta.1 → 1.0.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 (135) hide show
  1. package/README.md +5 -0
  2. package/dist/api/index.d.ts +2 -1
  3. package/dist/api/index.js +3 -2
  4. package/dist/api/v1/definition/index.d.ts +2 -1
  5. package/dist/api/v1/definition/index.js +12 -14
  6. package/dist/api/v1/definition/validations.js +14 -12
  7. package/dist/api/v1/errors.d.ts +3 -1
  8. package/dist/api/v1/errors.js +1 -4
  9. package/dist/api/v1/index.d.ts +2 -1
  10. package/dist/api/v1/index.js +5 -2
  11. package/dist/api/v1/validator/index.d.ts +3 -0
  12. package/dist/api/v1/validator/index.js +143 -0
  13. package/dist/api/v1/validator/validations.d.ts +6 -0
  14. package/dist/api/v1/validator/validations.js +40 -0
  15. package/dist/errors/index.d.ts +9 -1
  16. package/dist/errors/index.js +25 -4
  17. package/dist/events/index.d.ts +2 -1
  18. package/dist/events/index.js +17 -11
  19. package/dist/hooks/create.d.ts +2 -2
  20. package/dist/hooks/create.js +40 -19
  21. package/dist/hooks/enrich.d.ts +20 -2
  22. package/dist/hooks/enrich.js +88 -16
  23. package/dist/hooks/hooks.d.ts +17 -0
  24. package/dist/hooks/hooks.js +379 -0
  25. package/dist/hooks/index.d.ts +2 -3
  26. package/dist/hooks/index.js +6 -7
  27. package/dist/hooks/update.d.ts +2 -2
  28. package/dist/hooks/update.js +16 -26
  29. package/dist/hooks/utils/updateInstanceValues.d.ts +15 -0
  30. package/dist/hooks/utils/updateInstanceValues.js +50 -0
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +19 -6
  33. package/dist/models/CustomFieldDefinition.d.ts +1 -0
  34. package/dist/models/CustomFieldDefinition.js +10 -2
  35. package/dist/models/CustomFieldEntries.d.ts +15 -0
  36. package/dist/models/CustomFieldEntries.js +123 -0
  37. package/dist/models/CustomFieldValue.d.ts +3 -2
  38. package/dist/models/CustomFieldValue.js +22 -14
  39. package/dist/models/CustomValidator.d.ts +17 -0
  40. package/dist/models/CustomValidator.js +98 -0
  41. package/dist/models/index.d.ts +10 -2
  42. package/dist/models/index.js +60 -22
  43. package/dist/repository/definition.d.ts +23 -7
  44. package/dist/repository/definition.js +36 -7
  45. package/dist/repository/entries.d.ts +13 -0
  46. package/dist/repository/entries.js +92 -0
  47. package/dist/repository/utils/formatValues.d.ts +3 -0
  48. package/dist/repository/utils/formatValues.js +16 -0
  49. package/dist/repository/validator.d.ts +21 -0
  50. package/dist/repository/validator.js +62 -0
  51. package/dist/repository/value.d.ts +1 -1
  52. package/dist/repository/value.js +7 -32
  53. package/dist/scopes/filter.d.ts +5 -22
  54. package/dist/scopes/filter.js +18 -65
  55. package/dist/scopes/helpers/filter.helpers.d.ts +42 -0
  56. package/dist/scopes/helpers/filter.helpers.js +204 -0
  57. package/dist/tests/api/test-api.js +6 -24
  58. package/dist/tests/helpers/commonHooks.d.ts +6 -0
  59. package/dist/tests/helpers/commonHooks.js +62 -0
  60. package/dist/tests/helpers/database-config.js +1 -1
  61. package/dist/tests/helpers/index.d.ts +6 -1
  62. package/dist/tests/helpers/index.js +15 -2
  63. package/dist/tests/mocks/definition.mock.d.ts +5 -2
  64. package/dist/tests/mocks/definition.mock.js +10 -1
  65. package/dist/tests/mocks/events.mock.d.ts +1 -0
  66. package/dist/tests/mocks/events.mock.js +4 -2
  67. package/dist/types/definition/index.d.ts +1 -0
  68. package/dist/types/entries/index.d.ts +25 -0
  69. package/dist/types/entries/index.js +2 -0
  70. package/dist/types/index.d.ts +19 -3
  71. package/dist/utils/constants/index.d.ts +1 -1
  72. package/dist/utils/helpers/index.d.ts +4 -3
  73. package/dist/utils/helpers/index.js +22 -30
  74. package/dist/utils/init.d.ts +5 -3
  75. package/dist/utils/init.js +13 -11
  76. package/dist/utils/logger/index.d.ts +1 -0
  77. package/dist/utils/logger/index.js +34 -0
  78. package/dist/utils/validations/index.d.ts +7 -1
  79. package/dist/utils/validations/index.js +28 -7
  80. package/dist/utils/validations/schema/validator-schema.d.ts +9 -0
  81. package/dist/utils/validations/schema/validator-schema.js +95 -0
  82. package/dist/utils/validations/type.d.ts +2 -1
  83. package/dist/utils/validations/validators/index.js +9 -9
  84. package/dist/utils/validations/validators/select.validator.js +5 -2
  85. package/dist/utils/validations/validators/status.validator.js +8 -2
  86. package/package.json +28 -12
  87. package/src/api/index.ts +3 -2
  88. package/src/api/v1/definition/index.ts +20 -23
  89. package/src/api/v1/definition/validations.ts +16 -16
  90. package/src/api/v1/errors.ts +4 -7
  91. package/src/api/v1/index.ts +5 -3
  92. package/src/api/v1/validator/index.ts +141 -0
  93. package/src/api/v1/validator/validations.ts +39 -0
  94. package/src/errors/index.ts +31 -3
  95. package/src/events/index.ts +25 -13
  96. package/src/hooks/create.ts +50 -28
  97. package/src/hooks/enrich.ts +137 -28
  98. package/src/hooks/hooks.ts +467 -0
  99. package/src/hooks/index.ts +10 -5
  100. package/src/hooks/update.ts +20 -7
  101. package/src/hooks/utils/updateInstanceValues.ts +63 -0
  102. package/src/index.ts +10 -8
  103. package/src/models/CustomFieldDefinition.ts +9 -2
  104. package/src/models/CustomFieldEntries.ts +81 -0
  105. package/src/models/CustomFieldValue.ts +25 -17
  106. package/src/models/CustomValidator.ts +78 -0
  107. package/src/models/index.ts +83 -25
  108. package/src/repository/definition.ts +62 -14
  109. package/src/repository/entries.ts +88 -0
  110. package/src/repository/utils/formatValues.ts +14 -0
  111. package/src/repository/validator.ts +104 -0
  112. package/src/repository/value.ts +5 -35
  113. package/src/scopes/filter.ts +33 -106
  114. package/src/scopes/helpers/filter.helpers.ts +227 -0
  115. package/src/tests/api/test-api.ts +4 -2
  116. package/src/tests/helpers/commonHooks.ts +43 -0
  117. package/src/tests/helpers/database-config.ts +1 -1
  118. package/src/tests/helpers/index.ts +18 -2
  119. package/src/tests/mocks/definition.mock.ts +18 -9
  120. package/src/tests/mocks/events.mock.ts +4 -1
  121. package/src/types/definition/index.ts +1 -0
  122. package/src/types/entries/index.ts +27 -0
  123. package/src/types/index.ts +20 -3
  124. package/src/utils/helpers/index.ts +28 -35
  125. package/src/utils/init.ts +17 -12
  126. package/src/utils/logger/index.ts +9 -0
  127. package/src/utils/validations/index.ts +30 -6
  128. package/src/utils/validations/schema/README.md +93 -0
  129. package/src/utils/validations/schema/validator-schema.ts +106 -0
  130. package/src/utils/validations/type.ts +2 -1
  131. package/src/utils/validations/validators/index.ts +9 -9
  132. package/src/utils/validations/validators/select.validator.ts +3 -2
  133. package/src/utils/validations/validators/status.validator.ts +6 -2
  134. package/tsconfig.build.json +7 -0
  135. package/tsconfig.json +1 -1
@@ -1,14 +1,100 @@
1
1
  /* eslint-disable no-param-reassign */
2
2
  import * as ValueRepo from '../repository/value';
3
3
  import * as DefinitionRepo from '../repository/definition';
4
+ import * as EntriesRepo from '../repository/entries';
4
5
  import type CustomFieldValue from '../models/CustomFieldValue';
5
6
  import type CustomFieldDefinition from '../models/CustomFieldDefinition';
6
7
  import type { SerializedCustomFields } from '../types/definition';
7
- import type { ModelOptions } from '../types';
8
+ import type { CustomFieldOptions, ModelOptions, TransactionOptions } from '../types';
8
9
  import applyScopeToInstance from '../utils/scopeAttributes';
9
10
 
11
+ // Include all required fields for proper functioning
12
+ const CUSTOM_FIELD_DEFINITION_ATTRIBUTES_TO_PULL = [
13
+ 'id',
14
+ 'name',
15
+ 'entityId',
16
+ 'fieldType',
17
+ 'displayName',
18
+ 'validation',
19
+ 'entityType',
20
+ 'modelType',
21
+ 'required',
22
+ 'disabled',
23
+ 'defaultValue',
24
+ ];
25
+
10
26
  type SupportedHookTypes = 'afterFind' | 'afterCreate' | 'afterUpdate';
11
27
 
28
+ type CustomFieldEntries = Record<string, any>;
29
+
30
+ interface GetValuesGroupByInstanceResponse {
31
+ [modelId: string]: CustomFieldValue[];
32
+ }
33
+
34
+ interface GetCustomFieldEntriesByInstanceIdResponse {
35
+ [modelId: string]: CustomFieldEntries;
36
+ }
37
+
38
+ export const getCustomFieldEntriesByInstanceId = async ({
39
+ instancesIds,
40
+ options,
41
+ sadotOptions,
42
+ }: {
43
+ instancesIds: string[],
44
+ options?: TransactionOptions,
45
+ sadotOptions: Pick<CustomFieldOptions, 'useCustomFieldsEntries'>,
46
+ }): Promise<GetCustomFieldEntriesByInstanceIdResponse> => {
47
+ if (!sadotOptions.useCustomFieldsEntries) {
48
+ return {};
49
+ }
50
+
51
+ const customFieldEntries = await EntriesRepo.findEntriesByModelIds(
52
+ instancesIds,
53
+ options ?? {},
54
+ );
55
+
56
+ const customFieldEntriesByInstanceId = Object.fromEntries(customFieldEntries.map((instanceEntries) => {
57
+ const { modelId, customFields } = instanceEntries?.dataValues ?? {};
58
+ if (!modelId) {
59
+ return undefined;
60
+ }
61
+ return [modelId, customFields];
62
+ }).filter(Boolean));
63
+
64
+ instancesIds.forEach((instanceId) => {
65
+ customFieldEntriesByInstanceId[instanceId] ??= {};
66
+ });
67
+
68
+ return customFieldEntriesByInstanceId;
69
+ };
70
+
71
+ export const getValuesGroupByInstance = async ({
72
+ instancesIds,
73
+ options,
74
+ sadotOptions,
75
+ }: {
76
+ instancesIds: string[],
77
+ options?: TransactionOptions,
78
+ sadotOptions: Pick<CustomFieldOptions, 'useCustomFieldsEntries'>,
79
+ }): Promise<GetValuesGroupByInstanceResponse> => {
80
+ if (sadotOptions.useCustomFieldsEntries) {
81
+ return {};
82
+ }
83
+
84
+ const customFieldValues = await ValueRepo.findValuesByModelIds(
85
+ instancesIds,
86
+ options ?? {},
87
+ );
88
+
89
+ // Group fields by modelId
90
+ return customFieldValues.reduce((acc, v) => {
91
+ const { modelId } = v;
92
+ acc[modelId] ??= [];
93
+ acc[modelId].push(v);
94
+ return acc;
95
+ }, {});
96
+ };
97
+
12
98
  /**
13
99
  * Serialize custom fields value into the format of {[name] -> [fieldData]}
14
100
  */
@@ -33,9 +119,10 @@ const enrichResults = (
33
119
  scopeAttributes: string[],
34
120
  hookType?: SupportedHookTypes,
35
121
  modelOptions: ModelOptions = {},
122
+ sadotOptions: Pick<CustomFieldOptions, 'useCustomFieldsEntries'> = { useCustomFieldsEntries: false },
36
123
  ) => async (
37
124
  instancesOrInstance: any | any[],
38
- options,
125
+ options: TransactionOptions,
39
126
  ): Promise<void> => {
40
127
  if (
41
128
  options.originalAttributes?.length > 0
@@ -58,14 +145,38 @@ const enrichResults = (
58
145
  const identifierCustomFieldDefinitionsMapping = uniqueIdentifiers.reduce((map, identifier) => ({
59
146
  ...map,
60
147
  [identifier]: [],
61
-
62
148
  }), {});
63
149
 
64
- const customFieldDefinitions = await DefinitionRepo.findByEntityIds(
65
- modelType,
66
- uniqueIdentifiers,
67
- { transaction: options.transaction, modelOptions },
68
- );
150
+ // Cache for definitions by model type and transaction to avoid redundant DB queries
151
+ let customFieldDefinitionsPromise;
152
+ let cacheKey;
153
+
154
+ if (options.transaction) {
155
+ // Initialize definition cache Map if not already present directly on the transaction object
156
+ options.transaction.definitionCache ||= new Map();
157
+ cacheKey = `${modelType}:${uniqueIdentifiers.slice().sort().join(',')}`;
158
+ customFieldDefinitionsPromise = options.transaction.definitionCache.get(cacheKey);
159
+ }
160
+
161
+ if (!customFieldDefinitionsPromise) {
162
+ // Fetch from database (either first time in this transaction or no transaction)
163
+ customFieldDefinitionsPromise = DefinitionRepo.findByEntityIds(
164
+ modelType,
165
+ uniqueIdentifiers,
166
+ { transaction: options.transaction, modelOptions, attributes: CUSTOM_FIELD_DEFINITION_ATTRIBUTES_TO_PULL },
167
+ );
168
+
169
+ options.transaction?.definitionCache?.set(cacheKey, customFieldDefinitionsPromise);
170
+ }
171
+ const customFieldDefinitions = await customFieldDefinitionsPromise;
172
+
173
+ if (customFieldDefinitions.length === 0) {
174
+ // if no custom fields, we can return
175
+ instances.forEach((instance) => {
176
+ instance.customFields = {};
177
+ });
178
+ return;
179
+ }
69
180
 
70
181
  if (modelOptions?.include && modelOptions.useEntityIdFromInclude) {
71
182
  // if we pass useEntityIdFromInclude,
@@ -90,32 +201,30 @@ const enrichResults = (
90
201
  // Get the values per instates ids:
91
202
  const instancesIds = instances.map((i) => i[primaryKey]);
92
203
 
93
- const customFieldValues = await ValueRepo.findValuesByModelIds(
94
- instancesIds,
95
- { transaction: options.transaction },
96
- );
97
-
98
204
  // Group fields by modelId
99
- const valuesGroupByInstance: {
100
- [modelId: string]: CustomFieldValue[];
101
- } = customFieldValues.reduce((acc, v) => {
102
- const { modelId } = v;
103
- if (!acc[modelId]) {
104
- acc[modelId] = [];
105
- }
106
- acc[modelId].push(v);
107
- return acc;
108
- }, {});
205
+ const [valuesGroupByInstance, customFieldEntriesByInstanceId] = await Promise.all([
206
+ getValuesGroupByInstance({
207
+ instancesIds,
208
+ options,
209
+ sadotOptions,
210
+ }),
211
+ getCustomFieldEntriesByInstanceId({
212
+ instancesIds,
213
+ options,
214
+ sadotOptions,
215
+ }),
216
+ ]);
109
217
 
110
218
  // Attach custom fields to the instances
111
219
  instances.forEach((instance) => {
112
- const customFields = {};
113
220
  const { id } = instance;
221
+
114
222
  const instanceValues = valuesGroupByInstance[id];
115
- if (instanceValues) {
116
- const serializedCustomFields = serializeCustomFields(instanceValues, definitionsMap);
117
- Object.assign(customFields, serializedCustomFields);
118
- }
223
+ const serializedCustomFieldsValues = instanceValues ? serializeCustomFields(instanceValues, definitionsMap) : {};
224
+
225
+ const customFields = sadotOptions.useCustomFieldsEntries
226
+ ? customFieldEntriesByInstanceId[id]
227
+ : serializedCustomFieldsValues;
119
228
 
120
229
  scopeAttributes.forEach((attribute) => {
121
230
  const identifier = instance[attribute];
@@ -0,0 +1,467 @@
1
+ import type { WhereOptions } from 'sequelize';
2
+ import Ajv from 'ajv';
3
+ import Joi from 'joi';
4
+ import addFormats from 'ajv-formats';
5
+ import { BadRequest } from '@autofleet/errors';
6
+ import ajvErrors from 'ajv-errors';
7
+ import logger from '../utils/logger';
8
+ import * as ValidatorRepo from '../repository/validator';
9
+ import * as DefinitionRepo from '../repository/definition';
10
+ import { InvalidValueError, MissingRequiredCustomFieldError } from '../errors';
11
+ import type { CustomFieldOptions, ModelOptions } from '../types';
12
+ import applyScopeToInstance from '../utils/scopeAttributes';
13
+ import updateInstanceValues from './utils/updateInstanceValues';
14
+ import { CustomFieldDefinitionType } from '../utils/constants';
15
+ import type { CustomFieldDefinition } from '../models';
16
+
17
+ // Include all required fields for proper validation
18
+ const CUSTOM_VALIDATOR_ATTRIBUTES_TO_PULL = ['id', 'schema', 'modelType', 'entityId', 'disabled'];
19
+
20
+ // Initialize Ajv with relaxed settings to avoid warnings
21
+ const ajv = new Ajv({
22
+ allErrors: true,
23
+ strict: false, // Disable strict mode to avoid warnings
24
+ strictTypes: false, // Disable strict type checking
25
+ $data: true, // Enable $data references
26
+ });
27
+
28
+ addFormats(ajv);
29
+ ajvErrors(ajv);
30
+
31
+ /**
32
+ * Helper function to manually copy object properties
33
+ * This is more efficient for large objects and avoids excessive object creation
34
+ */
35
+ // eslint-disable-next-line prefer-object-spread
36
+ const manualObjectCopy = (sourceObj: Record<string, any>, additionalProps?: Record<string, any>): Record<string, any> =>
37
+ ({ __proto__: null, ...sourceObj, ...additionalProps });
38
+
39
+ /**
40
+ * Fetches complete custom fields for an instance by merging DB values with update values
41
+ * This is needed for partial updates to ensure all related fields are available for validation
42
+ */
43
+ const getCompleteCustomFields = async (instance, options): Promise<Record<string, any>> => {
44
+ // If we don't have an instance id or no custom fields being updated, return original fields
45
+ if (!instance.id || !instance.customFields || Object.keys(instance.customFields).length === 0) {
46
+ return instance.customFields || {};
47
+ }
48
+
49
+ try {
50
+ const ModelClass = instance.constructor;
51
+ // Only select the customFields column to minimize data transfer
52
+ const currentCustomFields = await ModelClass.findOne({
53
+ where: { id: instance.id },
54
+ attributes: ['customFields'],
55
+ transaction: options.transaction,
56
+ raw: true, // Get plain object instead of model instance for better performance
57
+ });
58
+
59
+ if (currentCustomFields?.customFields) {
60
+ // Merge existing fields with update fields using our helper function
61
+ const completeFields = manualObjectCopy(
62
+ currentCustomFields.customFields,
63
+ instance.customFields,
64
+ );
65
+
66
+ logger.debug('sadot - fetched complete custom fields for validation', {
67
+ fieldsCount: Object.keys(completeFields).length,
68
+ updateFieldsCount: Object.keys(instance.customFields).length,
69
+ });
70
+
71
+ return completeFields;
72
+ }
73
+ } catch (error) {
74
+ logger.error('sadot - error fetching complete model for validation', { error });
75
+ // Continue with partial data if we can't fetch the complete model
76
+ }
77
+
78
+ return instance.customFields || {};
79
+ };
80
+
81
+ const formatAjvErrors = (
82
+ errors: {
83
+ instancePath?: string;
84
+ keyword: string;
85
+ message?: string;
86
+ params?: Record<string, any>;
87
+ }[],
88
+ ): Record<string, string> => errors.reduce((acc, err) => {
89
+ const basePath = (err.instancePath || '')
90
+ .split('/')
91
+ .filter(Boolean)
92
+ .join('.')
93
+ .replace(/^after\./, '');
94
+
95
+ const missingProp = err.keyword === 'required' ? `.${err.params?.missingProperty}` : '';
96
+ const key = (basePath + missingProp).replace(/^\./, '') || 'root';
97
+
98
+ const message = err.message || 'Invalid value';
99
+ acc[key] = message;
100
+
101
+ return acc;
102
+ }, {} as Record<string, string>);
103
+
104
+ /**
105
+ * Validates the model using custom validators
106
+ */
107
+ const validateModel = async (
108
+ instance,
109
+ options,
110
+ scopeAttributes: string[],
111
+ modelOptions: ModelOptions = {},
112
+ isCreate = false,
113
+ ): Promise<void> => {
114
+ const modelType = instance.constructor.name;
115
+
116
+ logger.debug('sadot - validating model', { isCreate, modelType });
117
+ const identifiers = applyScopeToInstance(instance, scopeAttributes);
118
+
119
+ logger.debug('sadot - identifiers', { identifiers });
120
+
121
+ // Skip if no identifiers
122
+ if (!identifiers || Object.keys(identifiers).length === 0) {
123
+ logger.debug('sadot - skipping validation: no identifiers');
124
+ return;
125
+ }
126
+
127
+ // Find the entityId from identifiers (fleetId, businessModelId, etc.)
128
+ const entityId = Object.values(identifiers)[0]; // Get the first value as entityId
129
+
130
+ logger.debug('sadot - entityId', { entityId });
131
+
132
+ if (!entityId) {
133
+ logger.debug('sadot - skipping validation: no entityId');
134
+ return;
135
+ }
136
+
137
+ let validatorsPromise;
138
+ let cacheKey;
139
+ if (options.transaction) {
140
+ // eslint-disable-next-line no-param-reassign
141
+ options.transaction.validationsCache ||= new Map();
142
+ cacheKey = `${modelType}-${entityId}`;
143
+ validatorsPromise = options.transaction.validationsCache.get(cacheKey);
144
+ }
145
+
146
+ if (!validatorsPromise) {
147
+ validatorsPromise = ValidatorRepo.findAllByModelType(
148
+ modelType,
149
+ entityId,
150
+ {
151
+ transaction: options.transaction,
152
+ attributes: CUSTOM_VALIDATOR_ATTRIBUTES_TO_PULL,
153
+ ...(modelOptions.include && {
154
+ include: modelOptions.include?.(entityId),
155
+ }),
156
+ raw: true,
157
+ },
158
+ );
159
+ if (options.transaction) {
160
+ options?.transaction?.validationsCache.set(cacheKey, validatorsPromise);
161
+ }
162
+ }
163
+ const validators = await validatorsPromise;
164
+
165
+ logger.debug('sadot - validators found', { count: validators.length });
166
+
167
+ if (!validators.length) {
168
+ logger.debug('sadot - skipping validation: no validators found');
169
+ return;
170
+ }
171
+
172
+ // For updates, get the previous values
173
+ let originalValues = null;
174
+ if (!isCreate) {
175
+ // Create originalValues with our helper function
176
+ originalValues = manualObjectCopy(instance.previous());
177
+
178
+ // Add customFields separately
179
+ originalValues.customFields = instance.previous('customFields') || {};
180
+ }
181
+
182
+ // Get complete custom fields by merging DB values with update values
183
+ // This is especially important for partial updates to ensure all related fields are available
184
+ const completeCustomFields = !isCreate
185
+ ? await getCompleteCustomFields(instance, options)
186
+ : instance.customFields || {};
187
+
188
+ // eslint-disable-next-line no-restricted-syntax
189
+ for (const validator of validators) {
190
+ const { schema } = validator;
191
+ const typedSchema = schema as Record<string, any>;
192
+
193
+ logger.debug('sadot - validating with schema', {
194
+ schema,
195
+ hasAfterProps: !!typedSchema.properties?.after,
196
+ hasBeforeProps: !!typedSchema.properties?.before,
197
+ });
198
+
199
+ if (isCreate) {
200
+ // For create operations, we only need the 'after' state
201
+ if (typedSchema.properties?.after) {
202
+ const validateSchema = ajv.compile({
203
+ ...schema,
204
+ // Focus only on the 'after' validation part for create
205
+ properties: {
206
+ after: typedSchema.properties.after,
207
+ },
208
+ });
209
+
210
+ const isValid = validateSchema(JSON.parse(JSON.stringify({
211
+ after: {
212
+ ...instance.dataValues,
213
+ customFields: completeCustomFields,
214
+ },
215
+ })));
216
+
217
+ if (!isValid) {
218
+ const errorDetails = validateSchema.errors?.map((err) =>
219
+ `${(err as any).instancePath || ''} ${(err as any).message || 'Invalid value'}`).join(', ');
220
+
221
+ const formattedErrors = formatAjvErrors(validateSchema.errors);
222
+ throw new BadRequest(
223
+ [new Error(`Validation failed for ${modelType}: ${errorDetails}`)],
224
+ undefined,
225
+ {
226
+ customError: formattedErrors,
227
+ },
228
+ );
229
+ }
230
+ }
231
+ } else {
232
+ // For update operations, we need both before and after
233
+ const validateSchema = ajv.compile(typedSchema);
234
+
235
+ // Create after object with our helper function
236
+ const afterObj = manualObjectCopy(instance.dataValues);
237
+
238
+ // Add complete custom fields
239
+ afterObj.customFields = completeCustomFields;
240
+
241
+ // Create validation payload
242
+ const payload = {
243
+ before: originalValues,
244
+ after: afterObj,
245
+ };
246
+
247
+ // Validate
248
+ const isValid = validateSchema(JSON.parse(JSON.stringify(payload)));
249
+
250
+ logger.debug('sadot - validation result', {
251
+ isValid,
252
+ test: {
253
+ before: originalValues,
254
+ after: afterObj,
255
+ },
256
+ });
257
+
258
+ if (!isValid) {
259
+ const errorDetails = validateSchema
260
+ .errors
261
+ ?.map((err) => `${(err as any).instancePath || ''} ${(err as any).message || 'Invalid value'}`).join(', ');
262
+
263
+ const formattedErrors = formatAjvErrors(validateSchema.errors);
264
+ throw new BadRequest(
265
+ [new Error(`Validation failed for ${modelType}: ${errorDetails}`)],
266
+ undefined,
267
+ {
268
+ customError: formattedErrors,
269
+ },
270
+ );
271
+ }
272
+ }
273
+ }
274
+ };
275
+
276
+ const getFieldDefinitions = async ({
277
+ modelType,
278
+ modelOptions,
279
+ identifiers,
280
+ options,
281
+ }: {
282
+ modelType: any,
283
+ modelOptions: ModelOptions,
284
+ identifiers: any[],
285
+ options: any
286
+ }) => {
287
+ const { include, useEntityIdFromInclude } = modelOptions;
288
+ const where: WhereOptions = {
289
+ modelType,
290
+ disabled: false,
291
+ ...(!useEntityIdFromInclude && { entityId: identifiers }),
292
+ };
293
+
294
+ const fieldDefinitions = await DefinitionRepo.findAll(where, {
295
+ withDisabled: false,
296
+ transaction: options.transaction,
297
+ include: include?.(identifiers),
298
+ });
299
+ return fieldDefinitions;
300
+ };
301
+
302
+ const formatDates = (fieldDefinitions: CustomFieldDefinition[], instance: any) => {
303
+ (fieldDefinitions || []).forEach((fieldDefinition) => {
304
+ const { fieldType, name } = fieldDefinition;
305
+ if ([CustomFieldDefinitionType.DATE, CustomFieldDefinitionType.DATETIME].includes(fieldType)) {
306
+ const value = instance.customFields?.[name];
307
+ if (value) {
308
+ const { value: joiValue, error: validationError } = Joi.date().validate(value);
309
+ if (validationError) {
310
+ throw new InvalidValueError(value, name, validationError);
311
+ }
312
+ // eslint-disable-next-line no-param-reassign
313
+ instance.customFields[name] = joiValue.toISOString();
314
+ }
315
+ }
316
+ });
317
+ };
318
+
319
+ /**
320
+ * Hook to handle validation and custom fields during creation
321
+ */
322
+ export const beforeCreate = (
323
+ scopeAttributes: string[],
324
+ modelOptions: ModelOptions = {},
325
+ sadotOptions: Pick<CustomFieldOptions, 'useCustomFieldsEntries'> = { useCustomFieldsEntries: false },
326
+ ) => async (
327
+ instance,
328
+ options,
329
+ ): Promise<void> => {
330
+ logger.debug('sadot - before create hook');
331
+ const { fields } = options;
332
+ const modelType = instance.constructor.name;
333
+
334
+ const identifiers = applyScopeToInstance(instance, scopeAttributes);
335
+
336
+ // Step 1: Handle custom fields default values and required fields
337
+
338
+ const fieldDefinitions = await getFieldDefinitions({
339
+ modelType, modelOptions, identifiers, options,
340
+ });
341
+
342
+ // Apply default values
343
+ const fieldsWithDefaultValue = fieldDefinitions.filter((def) => ![null, undefined].includes(def.defaultValue));
344
+ if (fieldsWithDefaultValue.length) {
345
+ // eslint-disable-next-line no-param-reassign
346
+ instance.customFields ||= {};
347
+ fieldsWithDefaultValue
348
+ .filter((def) => (instance.customFields?.[def.name] === undefined))
349
+ .forEach(({ name, defaultValue }) => {
350
+ // eslint-disable-next-line no-param-reassign
351
+ instance.customFields[name] = defaultValue;
352
+ });
353
+ }
354
+
355
+ // Check for required fields
356
+ const requiredFieldsNames = Array.from(
357
+ new Set(fieldDefinitions.filter(({ required }) => required).map(({ name }) => name)),
358
+ );
359
+ const { customFields } = instance;
360
+ const fieldsNames = Object.keys(customFields ?? {});
361
+ const missingFields = requiredFieldsNames.filter((name) => !fieldsNames.includes(name));
362
+ if (missingFields?.length) {
363
+ throw new MissingRequiredCustomFieldError(missingFields);
364
+ }
365
+
366
+ // Step 2: Validate the model data (including custom fields)
367
+ await validateModel(instance, options, scopeAttributes, modelOptions, true);
368
+
369
+ // format date and datetime fields
370
+ formatDates(fieldDefinitions, instance);
371
+
372
+ // Step 3: Save custom field values if they exist
373
+ const customFieldsIdx = fields.indexOf('customFields');
374
+ if (customFieldsIdx === -1 || !customFields || !Object.keys(customFields).length) {
375
+ // No custom fields to update
376
+ return;
377
+ }
378
+
379
+ // Save custom field values
380
+ await updateInstanceValues({
381
+ modelId: instance.id,
382
+ modelType,
383
+ identifiers,
384
+ customFields,
385
+ options: {
386
+ useCustomFieldsEntries: sadotOptions.useCustomFieldsEntries,
387
+ transaction: options.transaction,
388
+ modelOptions,
389
+ },
390
+ });
391
+
392
+ // Remove customFields from fields array after handling
393
+ // eslint-disable-next-line no-param-reassign
394
+ fields.splice(customFieldsIdx, 1);
395
+ };
396
+
397
+ /**
398
+ * Hook to handle validation and custom fields during update
399
+ */
400
+ export const beforeUpdate = (
401
+ scopeAttributes: string[],
402
+ modelOptions: ModelOptions = {},
403
+ sadotOptions: Pick<CustomFieldOptions, 'useCustomFieldsEntries'> = { useCustomFieldsEntries: false },
404
+ ) => async (
405
+ instance,
406
+ options,
407
+ ): Promise<void> => {
408
+ logger.debug('sadot - before update hook');
409
+ const { fields } = options;
410
+ const modelType = instance.constructor.name;
411
+ const identifiers = applyScopeToInstance(instance, scopeAttributes);
412
+
413
+ const fieldDefinitions = await getFieldDefinitions({
414
+ modelType, modelOptions, identifiers, options,
415
+ });
416
+
417
+ // Step 1: Validate the model data (including custom fields)
418
+ await validateModel(instance, options, scopeAttributes, modelOptions, false);
419
+
420
+ // format date and datetime fields
421
+ formatDates(fieldDefinitions, instance);
422
+
423
+ // Step 2: Update custom field values if they exist
424
+ const customFieldsIdx = fields.indexOf('customFields');
425
+ if (customFieldsIdx > -1) {
426
+ const { customFields } = instance;
427
+
428
+ if (!Object.keys(customFields).length) {
429
+ return;
430
+ }
431
+
432
+ // Save custom field values
433
+ await updateInstanceValues({
434
+ modelId: instance.id,
435
+ modelType,
436
+ identifiers,
437
+ customFields,
438
+ options: {
439
+ useCustomFieldsEntries: sadotOptions.useCustomFieldsEntries,
440
+ transaction: options.transaction,
441
+ modelOptions,
442
+ },
443
+ });
444
+
445
+ // Remove customFields from fields array after handling
446
+ // eslint-disable-next-line no-param-reassign
447
+ fields.splice(customFieldsIdx, 1);
448
+ }
449
+ };
450
+
451
+ /**
452
+ * Hook to enable individual hooks for bulk create operations
453
+ */
454
+ export const beforeBulkCreate = (options): void => {
455
+ // This will activate the beforeCreate hook on each instance
456
+ // eslint-disable-next-line no-param-reassign
457
+ options.individualHooks = true;
458
+ };
459
+
460
+ /**
461
+ * Hook to enable individual hooks for bulk update operations
462
+ */
463
+ export const beforeBulkUpdate = (options): void => {
464
+ // This will activate the beforeUpdate hook on each instance
465
+ // eslint-disable-next-line no-param-reassign
466
+ options.individualHooks = true;
467
+ };
@@ -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
  };