@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.
- package/README.md +5 -0
- package/dist/api/index.d.ts +2 -1
- package/dist/api/index.js +3 -2
- package/dist/api/v1/definition/index.d.ts +2 -1
- package/dist/api/v1/definition/index.js +12 -14
- package/dist/api/v1/definition/validations.js +14 -12
- package/dist/api/v1/errors.d.ts +3 -1
- package/dist/api/v1/errors.js +1 -4
- package/dist/api/v1/index.d.ts +2 -1
- package/dist/api/v1/index.js +5 -2
- package/dist/api/v1/validator/index.d.ts +3 -0
- package/dist/api/v1/validator/index.js +143 -0
- package/dist/api/v1/validator/validations.d.ts +6 -0
- package/dist/api/v1/validator/validations.js +40 -0
- package/dist/errors/index.d.ts +9 -1
- package/dist/errors/index.js +25 -4
- package/dist/events/index.d.ts +2 -1
- package/dist/events/index.js +17 -11
- package/dist/hooks/create.d.ts +2 -2
- package/dist/hooks/create.js +40 -19
- package/dist/hooks/enrich.d.ts +20 -2
- package/dist/hooks/enrich.js +88 -16
- package/dist/hooks/hooks.d.ts +17 -0
- package/dist/hooks/hooks.js +379 -0
- package/dist/hooks/index.d.ts +2 -3
- package/dist/hooks/index.js +6 -7
- package/dist/hooks/update.d.ts +2 -2
- package/dist/hooks/update.js +16 -26
- package/dist/hooks/utils/updateInstanceValues.d.ts +15 -0
- package/dist/hooks/utils/updateInstanceValues.js +50 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +19 -6
- package/dist/models/CustomFieldDefinition.d.ts +1 -0
- package/dist/models/CustomFieldDefinition.js +10 -2
- package/dist/models/CustomFieldEntries.d.ts +15 -0
- package/dist/models/CustomFieldEntries.js +123 -0
- package/dist/models/CustomFieldValue.d.ts +3 -2
- package/dist/models/CustomFieldValue.js +22 -14
- package/dist/models/CustomValidator.d.ts +17 -0
- package/dist/models/CustomValidator.js +98 -0
- package/dist/models/index.d.ts +10 -2
- package/dist/models/index.js +60 -22
- package/dist/repository/definition.d.ts +23 -7
- package/dist/repository/definition.js +36 -7
- package/dist/repository/entries.d.ts +13 -0
- package/dist/repository/entries.js +92 -0
- package/dist/repository/utils/formatValues.d.ts +3 -0
- package/dist/repository/utils/formatValues.js +16 -0
- package/dist/repository/validator.d.ts +21 -0
- package/dist/repository/validator.js +62 -0
- package/dist/repository/value.d.ts +1 -1
- package/dist/repository/value.js +7 -32
- package/dist/scopes/filter.d.ts +5 -22
- package/dist/scopes/filter.js +18 -65
- package/dist/scopes/helpers/filter.helpers.d.ts +42 -0
- package/dist/scopes/helpers/filter.helpers.js +204 -0
- package/dist/tests/api/test-api.js +6 -24
- package/dist/tests/helpers/commonHooks.d.ts +6 -0
- package/dist/tests/helpers/commonHooks.js +62 -0
- package/dist/tests/helpers/database-config.js +1 -1
- package/dist/tests/helpers/index.d.ts +6 -1
- package/dist/tests/helpers/index.js +15 -2
- package/dist/tests/mocks/definition.mock.d.ts +5 -2
- package/dist/tests/mocks/definition.mock.js +10 -1
- package/dist/tests/mocks/events.mock.d.ts +1 -0
- package/dist/tests/mocks/events.mock.js +4 -2
- package/dist/types/definition/index.d.ts +1 -0
- package/dist/types/entries/index.d.ts +25 -0
- package/dist/types/entries/index.js +2 -0
- package/dist/types/index.d.ts +19 -3
- package/dist/utils/constants/index.d.ts +1 -1
- package/dist/utils/helpers/index.d.ts +4 -3
- package/dist/utils/helpers/index.js +22 -30
- package/dist/utils/init.d.ts +5 -3
- package/dist/utils/init.js +13 -11
- package/dist/utils/logger/index.d.ts +1 -0
- package/dist/utils/logger/index.js +34 -0
- package/dist/utils/validations/index.d.ts +7 -1
- package/dist/utils/validations/index.js +28 -7
- package/dist/utils/validations/schema/validator-schema.d.ts +9 -0
- package/dist/utils/validations/schema/validator-schema.js +95 -0
- package/dist/utils/validations/type.d.ts +2 -1
- package/dist/utils/validations/validators/index.js +9 -9
- package/dist/utils/validations/validators/select.validator.js +5 -2
- package/dist/utils/validations/validators/status.validator.js +8 -2
- package/package.json +28 -12
- package/src/api/index.ts +3 -2
- package/src/api/v1/definition/index.ts +20 -23
- package/src/api/v1/definition/validations.ts +16 -16
- package/src/api/v1/errors.ts +4 -7
- package/src/api/v1/index.ts +5 -3
- package/src/api/v1/validator/index.ts +141 -0
- package/src/api/v1/validator/validations.ts +39 -0
- package/src/errors/index.ts +31 -3
- package/src/events/index.ts +25 -13
- package/src/hooks/create.ts +50 -28
- package/src/hooks/enrich.ts +137 -28
- package/src/hooks/hooks.ts +467 -0
- package/src/hooks/index.ts +10 -5
- package/src/hooks/update.ts +20 -7
- package/src/hooks/utils/updateInstanceValues.ts +63 -0
- package/src/index.ts +10 -8
- package/src/models/CustomFieldDefinition.ts +9 -2
- package/src/models/CustomFieldEntries.ts +81 -0
- package/src/models/CustomFieldValue.ts +25 -17
- package/src/models/CustomValidator.ts +78 -0
- package/src/models/index.ts +83 -25
- package/src/repository/definition.ts +62 -14
- package/src/repository/entries.ts +88 -0
- package/src/repository/utils/formatValues.ts +14 -0
- package/src/repository/validator.ts +104 -0
- package/src/repository/value.ts +5 -35
- package/src/scopes/filter.ts +33 -106
- package/src/scopes/helpers/filter.helpers.ts +227 -0
- package/src/tests/api/test-api.ts +4 -2
- package/src/tests/helpers/commonHooks.ts +43 -0
- package/src/tests/helpers/database-config.ts +1 -1
- package/src/tests/helpers/index.ts +18 -2
- package/src/tests/mocks/definition.mock.ts +18 -9
- package/src/tests/mocks/events.mock.ts +4 -1
- package/src/types/definition/index.ts +1 -0
- package/src/types/entries/index.ts +27 -0
- package/src/types/index.ts +20 -3
- package/src/utils/helpers/index.ts +28 -35
- package/src/utils/init.ts +17 -12
- package/src/utils/logger/index.ts +9 -0
- package/src/utils/validations/index.ts +30 -6
- package/src/utils/validations/schema/README.md +93 -0
- package/src/utils/validations/schema/validator-schema.ts +106 -0
- package/src/utils/validations/type.ts +2 -1
- package/src/utils/validations/validators/index.ts +9 -9
- package/src/utils/validations/validators/select.validator.ts +3 -2
- package/src/utils/validations/validators/status.validator.ts +6 -2
- package/tsconfig.build.json +7 -0
- package/tsconfig.json +1 -1
package/src/hooks/enrich.ts
CHANGED
|
@@ -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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
};
|
package/src/hooks/index.ts
CHANGED
|
@@ -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
|
-
|
|
15
|
+
workaround,
|
|
16
|
+
beforeCreate,
|
|
11
17
|
beforeUpdate,
|
|
12
18
|
beforeBulkCreate,
|
|
13
|
-
|
|
14
|
-
workaround,
|
|
19
|
+
beforeBulkUpdate,
|
|
15
20
|
};
|