@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.
- package/package.json +3 -1
- package/src/api/v1/index.ts +2 -0
- package/src/api/v1/validator/index.ts +127 -0
- package/src/api/v1/validator/validations.ts +39 -0
- package/src/events/index.ts +1 -1
- package/src/hooks/hooks.ts +304 -0
- package/src/hooks/index.ts +10 -5
- package/src/index.ts +6 -0
- package/src/models/CustomValidator.ts +76 -0
- package/src/models/index.ts +7 -2
- package/src/repository/validator.ts +91 -0
- package/src/tests/helpers/commonHooks.ts +9 -2
- package/src/tests/helpers/index.ts +7 -2
- package/src/types/index.ts +1 -0
- package/src/utils/validations/schema/README.md +93 -0
- package/src/utils/validations/schema/validator-schema.ts +103 -0
- package/dist/api/index.d.ts +0 -3
- package/dist/api/index.js +0 -12
- package/dist/api/v1/definition/index.d.ts +0 -3
- package/dist/api/v1/definition/index.js +0 -116
- package/dist/api/v1/definition/validations.d.ts +0 -2
- package/dist/api/v1/definition/validations.js +0 -77
- package/dist/api/v1/errors.d.ts +0 -4
- package/dist/api/v1/errors.js +0 -12
- package/dist/api/v1/index.d.ts +0 -3
- package/dist/api/v1/index.js +0 -11
- package/dist/errors/index.d.ts +0 -24
- package/dist/errors/index.js +0 -66
- package/dist/events/index.d.ts +0 -4
- package/dist/events/index.js +0 -50
- package/dist/hooks/create.d.ts +0 -10
- package/dist/hooks/create.js +0 -95
- package/dist/hooks/enrich.d.ts +0 -30
- package/dist/hooks/enrich.js +0 -159
- package/dist/hooks/find.d.ts +0 -1
- package/dist/hooks/find.js +0 -29
- package/dist/hooks/index.d.ts +0 -6
- package/dist/hooks/index.js +0 -18
- package/dist/hooks/update.d.ts +0 -10
- package/dist/hooks/update.js +0 -49
- package/dist/hooks/utils/updateInstanceValues.d.ts +0 -15
- package/dist/hooks/utils/updateInstanceValues.js +0 -50
- package/dist/hooks/workaround.d.ts +0 -10
- package/dist/hooks/workaround.js +0 -37
- package/dist/index.d.ts +0 -13
- package/dist/index.js +0 -67
- package/dist/models/CustomFieldDefinition.d.ts +0 -25
- package/dist/models/CustomFieldDefinition.js +0 -192
- package/dist/models/CustomFieldEntries.d.ts +0 -15
- package/dist/models/CustomFieldEntries.js +0 -123
- package/dist/models/CustomFieldValue.d.ts +0 -16
- package/dist/models/CustomFieldValue.js +0 -151
- package/dist/models/index.d.ts +0 -17
- package/dist/models/index.js +0 -113
- package/dist/models/tests/AssociatedTestModel.d.ts +0 -12
- package/dist/models/tests/AssociatedTestModel.js +0 -71
- package/dist/models/tests/TestModel.d.ts +0 -12
- package/dist/models/tests/TestModel.js +0 -69
- package/dist/models/tests/contextAwareModels/ContextAwareTestModel.d.ts +0 -10
- package/dist/models/tests/contextAwareModels/ContextAwareTestModel.js +0 -53
- package/dist/models/tests/contextAwareModels/ContextTestModel.d.ts +0 -13
- package/dist/models/tests/contextAwareModels/ContextTestModel.js +0 -47
- package/dist/repository/definition.d.ts +0 -36
- package/dist/repository/definition.js +0 -121
- package/dist/repository/entries.d.ts +0 -13
- package/dist/repository/entries.js +0 -92
- package/dist/repository/utils/formatValues.d.ts +0 -3
- package/dist/repository/utils/formatValues.js +0 -16
- package/dist/repository/value.d.ts +0 -28
- package/dist/repository/value.js +0 -124
- package/dist/scopes/filter.d.ts +0 -30
- package/dist/scopes/filter.js +0 -75
- package/dist/scopes/helpers/filter.helpers.d.ts +0 -42
- package/dist/scopes/helpers/filter.helpers.js +0 -204
- package/dist/scopes/index.d.ts +0 -2
- package/dist/scopes/index.js +0 -6
- package/dist/tests/api/test-api.d.ts +0 -2
- package/dist/tests/api/test-api.js +0 -38
- package/dist/tests/functional/searching/index.d.ts +0 -8
- package/dist/tests/functional/searching/index.js +0 -44
- package/dist/tests/helpers/commonHooks.d.ts +0 -5
- package/dist/tests/helpers/commonHooks.js +0 -55
- package/dist/tests/helpers/database-config.d.ts +0 -16
- package/dist/tests/helpers/database-config.js +0 -17
- package/dist/tests/helpers/index.d.ts +0 -7
- package/dist/tests/helpers/index.js +0 -29
- package/dist/tests/mocks/definition.mock.d.ts +0 -48
- package/dist/tests/mocks/definition.mock.js +0 -78
- package/dist/tests/mocks/events.mock.d.ts +0 -4
- package/dist/tests/mocks/events.mock.js +0 -21
- package/dist/tests/mocks/testModel.d.ts +0 -12
- package/dist/tests/mocks/testModel.js +0 -35
- package/dist/types/definition/index.d.ts +0 -25
- package/dist/types/definition/index.js +0 -2
- package/dist/types/entries/index.d.ts +0 -25
- package/dist/types/entries/index.js +0 -2
- package/dist/types/index.d.ts +0 -34
- package/dist/types/index.js +0 -2
- package/dist/types/value/index.d.ts +0 -15
- package/dist/types/value/index.js +0 -2
- package/dist/utils/constants/index.d.ts +0 -19
- package/dist/utils/constants/index.js +0 -22
- package/dist/utils/db/index.d.ts +0 -4
- package/dist/utils/db/index.js +0 -24
- package/dist/utils/helpers/index.d.ts +0 -26
- package/dist/utils/helpers/index.js +0 -40
- package/dist/utils/init.d.ts +0 -7
- package/dist/utils/init.js +0 -109
- package/dist/utils/logger/index.d.ts +0 -3
- package/dist/utils/logger/index.js +0 -42
- package/dist/utils/scopeAttributes.d.ts +0 -2
- package/dist/utils/scopeAttributes.js +0 -11
- package/dist/utils/validations/index.d.ts +0 -8
- package/dist/utils/validations/index.js +0 -41
- package/dist/utils/validations/schema/custom-fields.d.ts +0 -3
- package/dist/utils/validations/schema/custom-fields.js +0 -9
- package/dist/utils/validations/type.d.ts +0 -15
- package/dist/utils/validations/type.js +0 -2
- package/dist/utils/validations/validators/index.d.ts +0 -14
- package/dist/utils/validations/validators/index.js +0 -40
- package/dist/utils/validations/validators/select.validator.d.ts +0 -5
- package/dist/utils/validations/validators/select.validator.js +0 -12
- package/dist/utils/validations/validators/status.validator.d.ts +0 -12
- 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.
|
|
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",
|
package/src/api/v1/index.ts
CHANGED
|
@@ -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;
|
package/src/events/index.ts
CHANGED
|
@@ -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
|
+
};
|
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
|
};
|
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;
|
package/src/models/index.ts
CHANGED
|
@@ -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,
|