@autofleet/sadot 0.0.1-beta
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/dist/jest.config.d.ts +12 -0
- package/dist/src/api/index.d.ts +2 -0
- package/dist/src/api/index.js +11 -0
- package/dist/src/api/v1/definition/index.d.ts +2 -0
- package/dist/src/api/v1/definition/index.js +118 -0
- package/dist/src/api/v1/definition/validations.d.ts +2 -0
- package/dist/src/api/v1/definition/validations.js +36 -0
- package/dist/src/api/v1/errors.d.ts +2 -0
- package/dist/src/api/v1/errors.js +15 -0
- package/dist/src/api/v1/index.d.ts +2 -0
- package/dist/src/api/v1/index.js +10 -0
- package/dist/src/errors/index.d.ts +16 -0
- package/dist/src/errors/index.js +45 -0
- package/dist/src/events/index.d.ts +4 -0
- package/dist/src/events/index.js +47 -0
- package/dist/src/hooks/create.d.ts +9 -0
- package/dist/src/hooks/create.js +70 -0
- package/dist/src/hooks/enrich.d.ts +5 -0
- package/dist/src/hooks/enrich.js +118 -0
- package/dist/src/hooks/find.d.ts +1 -0
- package/dist/src/hooks/find.js +29 -0
- package/dist/src/hooks/index.d.ts +6 -0
- package/dist/src/hooks/index.js +18 -0
- package/dist/src/hooks/update.d.ts +9 -0
- package/dist/src/hooks/update.js +58 -0
- package/dist/src/hooks/workaround.d.ts +10 -0
- package/dist/src/hooks/workaround.js +37 -0
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.js +105 -0
- package/dist/src/models/CustomFieldDefinition.d.ts +23 -0
- package/dist/src/models/CustomFieldDefinition.js +165 -0
- package/dist/src/models/CustomFieldValue.d.ts +15 -0
- package/dist/src/models/CustomFieldValue.js +148 -0
- package/dist/src/models/index.d.ts +8 -0
- package/dist/src/models/index.js +87 -0
- package/dist/src/models/tests/AssociatedTestModel.d.ts +12 -0
- package/dist/src/models/tests/AssociatedTestModel.js +71 -0
- package/dist/src/models/tests/TestModel.d.ts +11 -0
- package/dist/src/models/tests/TestModel.js +63 -0
- package/dist/src/repository/definition.d.ts +17 -0
- package/dist/src/repository/definition.js +80 -0
- package/dist/src/repository/value.d.ts +24 -0
- package/dist/src/repository/value.js +107 -0
- package/dist/src/tests/api/test-api.d.ts +2 -0
- package/dist/src/tests/api/test-api.js +56 -0
- package/dist/src/tests/helpers/database-config.d.ts +15 -0
- package/dist/src/tests/helpers/database-config.js +16 -0
- package/dist/src/tests/helpers/index.d.ts +2 -0
- package/dist/src/tests/helpers/index.js +18 -0
- package/dist/src/tests/mocks/definition.mock.d.ts +37 -0
- package/dist/src/tests/mocks/definition.mock.js +64 -0
- package/dist/src/tests/mocks/events.mock.d.ts +3 -0
- package/dist/src/tests/mocks/events.mock.js +19 -0
- package/dist/src/tests/mocks/testModel.d.ts +12 -0
- package/dist/src/tests/mocks/testModel.js +35 -0
- package/dist/src/types/definition/index.d.ts +23 -0
- package/dist/src/types/definition/index.js +2 -0
- package/dist/src/types/index.d.ts +13 -0
- package/dist/src/types/index.js +2 -0
- package/dist/src/types/value/index.d.ts +15 -0
- package/dist/src/types/value/index.js +2 -0
- package/dist/src/utils/constants/index.d.ts +1 -0
- package/dist/src/utils/constants/index.js +5 -0
- package/dist/src/utils/db/index.d.ts +4 -0
- package/dist/src/utils/db/index.js +24 -0
- package/dist/src/utils/logger/index.d.ts +2 -0
- package/dist/src/utils/logger/index.js +6 -0
- package/dist/src/utils/validations/custom-fields.d.ts +2 -0
- package/dist/src/utils/validations/custom-fields.js +10 -0
- package/dist/src/utils/validations/custom.d.ts +15 -0
- package/dist/src/utils/validations/custom.js +42 -0
- package/dist/src/utils/validations/index.d.ts +2 -0
- package/dist/src/utils/validations/index.js +20 -0
- package/dist/src/utils/validations/type.d.ts +18 -0
- package/dist/src/utils/validations/type.js +50 -0
- package/dist/src/utils/validations/validators.d.ts +12 -0
- package/dist/src/utils/validations/validators.js +33 -0
- package/package.json +44 -0
- package/src/api/index.ts +9 -0
- package/src/api/v1/definition/index.ts +110 -0
- package/src/api/v1/definition/validations.ts +35 -0
- package/src/api/v1/errors.ts +16 -0
- package/src/api/v1/index.ts +9 -0
- package/src/errors/index.ts +42 -0
- package/src/events/index.ts +50 -0
- package/src/hooks/create.ts +51 -0
- package/src/hooks/enrich.ts +125 -0
- package/src/hooks/find.ts +27 -0
- package/src/hooks/index.ts +15 -0
- package/src/hooks/update.ts +39 -0
- package/src/hooks/workaround.ts +47 -0
- package/src/index.ts +101 -0
- package/src/models/CustomFieldDefinition.ts +140 -0
- package/src/models/CustomFieldValue.ts +114 -0
- package/src/models/index.ts +101 -0
- package/src/models/tests/AssociatedTestModel.ts +57 -0
- package/src/models/tests/TestModel.ts +49 -0
- package/src/repository/definition.ts +111 -0
- package/src/repository/value.ts +99 -0
- package/src/tests/api/test-api.ts +38 -0
- package/src/tests/helpers/database-config.ts +14 -0
- package/src/tests/helpers/index.ts +15 -0
- package/src/tests/mocks/definition.mock.ts +69 -0
- package/src/tests/mocks/events.mock.ts +18 -0
- package/src/tests/mocks/testModel.ts +37 -0
- package/src/types/definition/index.ts +22 -0
- package/src/types/index.ts +15 -0
- package/src/types/value/index.ts +14 -0
- package/src/utils/constants/index.ts +2 -0
- package/src/utils/db/index.ts +21 -0
- package/src/utils/logger/index.ts +6 -0
- package/src/utils/validations/custom-fields.ts +9 -0
- package/src/utils/validations/custom.ts +39 -0
- package/src/utils/validations/index.ts +19 -0
- package/src/utils/validations/type.ts +45 -0
- package/src/utils/validations/validators.ts +34 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import Joi from '@hapi/joi';
|
|
2
|
+
import { CustomFieldDefinitionType } from '../../../utils/validations/type';
|
|
3
|
+
|
|
4
|
+
const ValidationSchema = Joi.when('fieldType', {
|
|
5
|
+
is: CustomFieldDefinitionType.SELECT,
|
|
6
|
+
then: Joi.array().items(Joi.string()).min(1).unique(),
|
|
7
|
+
otherwise: Joi.any(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const CustomFieldDefinitionCreationSchema = Joi.object({
|
|
11
|
+
name: Joi.string().required(),
|
|
12
|
+
displayName: Joi.string().required(),
|
|
13
|
+
validation: ValidationSchema,
|
|
14
|
+
fieldType: Joi.string().valid(...Object.values(CustomFieldDefinitionType)).required(),
|
|
15
|
+
entityId: Joi.string().guid().required(),
|
|
16
|
+
entityType: Joi.string().required(),
|
|
17
|
+
description: Joi.string(),
|
|
18
|
+
required: Joi.boolean(),
|
|
19
|
+
disabled: Joi.boolean(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const CustomFieldDefinitionUpdateSchema = Joi.object({
|
|
23
|
+
displayName: Joi.string(),
|
|
24
|
+
validation: ValidationSchema,
|
|
25
|
+
fieldType: Joi.string().valid(...Object.values(CustomFieldDefinitionType)),
|
|
26
|
+
description: Joi.string(),
|
|
27
|
+
required: Joi.boolean(),
|
|
28
|
+
disabled: Joi.boolean(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export const validateCustomFieldDefinitionCreation = (payload) =>
|
|
32
|
+
CustomFieldDefinitionCreationSchema.validateAsync(payload, { abortEarly: false });
|
|
33
|
+
|
|
34
|
+
export const validateCustomFieldDefinitionUpdate = (payload) =>
|
|
35
|
+
CustomFieldDefinitionUpdateSchema.validateAsync(payload, { abortEarly: false });
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { handleError, BadRequest } from '@autofleet/errors';
|
|
2
|
+
import { ValidationError as InputValidationError } from '@hapi/joi';
|
|
3
|
+
import { ValidationError as DatabaseValidationError } from 'sequelize';
|
|
4
|
+
|
|
5
|
+
export default (err, res, additionalData = {}) => {
|
|
6
|
+
let error = err;
|
|
7
|
+
if (err instanceof InputValidationError) {
|
|
8
|
+
error = new BadRequest([err], null);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (err instanceof DatabaseValidationError) {
|
|
12
|
+
error = new BadRequest([err], null);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return handleError(error, res, additionalData);
|
|
16
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* eslint-disable max-classes-per-file */
|
|
2
|
+
import { BadRequest } from '@autofleet/errors';
|
|
3
|
+
|
|
4
|
+
export class MissingRequiredCustomFieldError extends BadRequest {
|
|
5
|
+
constructor(missingFields: string[]) {
|
|
6
|
+
const err = new Error(`The following custom fields are required: ${missingFields.join(',')}`);
|
|
7
|
+
super([err], null, missingFields);
|
|
8
|
+
this.message = 'MISSING_REQUIRED_CUSTOM_FIELDS';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class UnsupportedCustomFieldTypeError extends BadRequest {
|
|
13
|
+
constructor(fieldType: string) {
|
|
14
|
+
const err = new Error(`Type "${fieldType}" is not supported`);
|
|
15
|
+
super([err], null, null);
|
|
16
|
+
this.message = 'UNSUPPORTED_CUSTOM_FIELD_TYPE';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class UnsupportedCustomValidationError extends BadRequest {
|
|
21
|
+
constructor(fieldType: string) {
|
|
22
|
+
const err = new Error(`Validation for "${fieldType}" is not supported`);
|
|
23
|
+
super([err], null, null);
|
|
24
|
+
this.message = 'UNSUPPORTED_CUSTOM_VALIDATION_TYPE';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class InvalidValueError extends BadRequest {
|
|
29
|
+
constructor(value: any, fieldType: string) {
|
|
30
|
+
const err = new Error(`Invalid "${fieldType}" value ${JSON.stringify(value)}`);
|
|
31
|
+
super([err], null, null);
|
|
32
|
+
this.message = 'INVALID_VALUE';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class MissingDefinitionError extends BadRequest {
|
|
37
|
+
constructor(fieldNames: string[]) {
|
|
38
|
+
const err = new Error(`Missing custom field definition for field ${fieldNames.join(',')}`);
|
|
39
|
+
super([err], null, null);
|
|
40
|
+
this.message = 'MISSING_DEFINITION';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import Events from '@autofleet/events';
|
|
2
|
+
import logger from '../utils/logger';
|
|
3
|
+
|
|
4
|
+
const events = new Events({ logger });
|
|
5
|
+
|
|
6
|
+
const KEYS_TO_CONVERT = ['value'];
|
|
7
|
+
|
|
8
|
+
const stringifyBools = (savedObject: any, keysToConvert: Array<string>) => {
|
|
9
|
+
if (Object.keys(savedObject).some((key) => keysToConvert.includes(key))) {
|
|
10
|
+
const objectToReturn = { ...savedObject };
|
|
11
|
+
keysToConvert.forEach((key) => {
|
|
12
|
+
if (typeof savedObject[key] === 'boolean') {
|
|
13
|
+
objectToReturn[key] = savedObject[key].toString();
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
return objectToReturn;
|
|
17
|
+
}
|
|
18
|
+
return savedObject;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const modelTableMapping = {
|
|
22
|
+
CustomFieldDefinition: {
|
|
23
|
+
tableName: 'dim_custom_field_definition',
|
|
24
|
+
eventVersion: '1',
|
|
25
|
+
},
|
|
26
|
+
CustomFieldValue: {
|
|
27
|
+
tableName: 'dim_custom_field_value',
|
|
28
|
+
eventVersion: '1',
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const sendDimEvent = (instance): void => {
|
|
33
|
+
const mapping = modelTableMapping[instance.constructor.name];
|
|
34
|
+
if (mapping) {
|
|
35
|
+
let objectToSend = instance.get();
|
|
36
|
+
try {
|
|
37
|
+
objectToSend = stringifyBools(instance.get(), KEYS_TO_CONVERT);
|
|
38
|
+
} catch (err) {
|
|
39
|
+
logger.error('Failed to convert booleans in dim event payload', err);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
events.sendObject(
|
|
43
|
+
mapping.tableName,
|
|
44
|
+
mapping.eventVersion,
|
|
45
|
+
objectToSend,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export default events;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import logger from '../utils/logger';
|
|
2
|
+
import * as ValueRepo from '../repository/value';
|
|
3
|
+
import * as DefinitionRepo from '../repository/definition';
|
|
4
|
+
import { MissingRequiredCustomFieldError } from '../errors';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A hook to create the custom fields when updating a model (more then one instance).
|
|
8
|
+
*/
|
|
9
|
+
export const beforeBulkCreate = (options): void => {
|
|
10
|
+
// This will activate the beforeCreate hook on each updating instance.
|
|
11
|
+
// eslint-disable-next-line no-param-reassign
|
|
12
|
+
options.individualHooks = true;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* A hook to create the custom fields when updating a model instance.
|
|
16
|
+
* TODO - cleanup if update fail
|
|
17
|
+
*/
|
|
18
|
+
export const beforeCreate = (scopeAttributes: string[]) => async (
|
|
19
|
+
instance,
|
|
20
|
+
options,
|
|
21
|
+
): Promise<void> => {
|
|
22
|
+
logger.debug('sadot - before create hook');
|
|
23
|
+
const { fields } = options;
|
|
24
|
+
const modelType = instance.constructor.name;
|
|
25
|
+
const identifiers = scopeAttributes.map((attribute) => instance[attribute]);
|
|
26
|
+
// get all model's required definitions
|
|
27
|
+
const requiredFieldsNames = await DefinitionRepo.getRequiredFields(modelType,
|
|
28
|
+
instance.id,
|
|
29
|
+
identifiers);
|
|
30
|
+
const customFieldsIdx = fields.indexOf('customFields');
|
|
31
|
+
const { customFields } = instance;
|
|
32
|
+
if (customFieldsIdx > -1 && customFields) {
|
|
33
|
+
const fieldsNames = Object.keys(customFields);
|
|
34
|
+
const missingFields = requiredFieldsNames.filter((name) => !fieldsNames.includes(name));
|
|
35
|
+
if (missingFields?.length > 0) {
|
|
36
|
+
throw new MissingRequiredCustomFieldError(missingFields);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await ValueRepo.updateValues(
|
|
40
|
+
modelType,
|
|
41
|
+
instance.id,
|
|
42
|
+
identifiers,
|
|
43
|
+
customFields,
|
|
44
|
+
{ transaction: options.transaction },
|
|
45
|
+
);
|
|
46
|
+
// eslint-disable-next-line no-param-reassign
|
|
47
|
+
fields.splice(customFieldsIdx, 1);
|
|
48
|
+
} else if (requiredFieldsNames?.length > 0) {
|
|
49
|
+
throw new MissingRequiredCustomFieldError(requiredFieldsNames);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/* eslint-disable no-param-reassign */
|
|
2
|
+
import * as ValueRepo from '../repository/value';
|
|
3
|
+
import * as DefinitionRepo from '../repository/definition';
|
|
4
|
+
import CustomFieldValue from '../models/CustomFieldValue';
|
|
5
|
+
import CustomFieldDefinition from '../models/CustomFieldDefinition';
|
|
6
|
+
import { SerializedCustomFields } from '../types/definition';
|
|
7
|
+
import logger from '../utils/logger';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Serialize custom fields value into the format of {[name] -> [fieldData]}
|
|
11
|
+
*/
|
|
12
|
+
const serializeCustomFields = (
|
|
13
|
+
customFieldValues: CustomFieldValue[],
|
|
14
|
+
customFieldDefinitionsHash: Record<string, CustomFieldDefinition>,
|
|
15
|
+
): SerializedCustomFields => {
|
|
16
|
+
const customFields = customFieldValues.reduce((acc, cfv) => ({
|
|
17
|
+
...acc,
|
|
18
|
+
...(
|
|
19
|
+
customFieldDefinitionsHash[cfv.customFieldDefinitionId]
|
|
20
|
+
&& { [customFieldDefinitionsHash[cfv.customFieldDefinitionId].name]: cfv.value }
|
|
21
|
+
),
|
|
22
|
+
}), {});
|
|
23
|
+
return customFields;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* A hook to attach the custom fields when fetching a model instances.
|
|
27
|
+
*/
|
|
28
|
+
const enrichResults = (modelType: string, scopeAttributes: string[]) => async (
|
|
29
|
+
instancesOrInstance: any | any[],
|
|
30
|
+
options,
|
|
31
|
+
): Promise<void> => {
|
|
32
|
+
if (
|
|
33
|
+
options.originalAttributes?.length > 0
|
|
34
|
+
&& !options.originalAttributes?.includes?.('customFields')
|
|
35
|
+
) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const primaryKey = 'id';
|
|
40
|
+
let instances = Array.isArray(instancesOrInstance)
|
|
41
|
+
? instancesOrInstance
|
|
42
|
+
: [instancesOrInstance];
|
|
43
|
+
|
|
44
|
+
instances = instances.filter(Boolean);
|
|
45
|
+
|
|
46
|
+
const identifiers = instances.map((instance) =>
|
|
47
|
+
scopeAttributes
|
|
48
|
+
.map((attr) => instance[attr])).flat();
|
|
49
|
+
|
|
50
|
+
const uniqueIdentifiers = [...new Set(identifiers)].filter(Boolean);
|
|
51
|
+
const identifierCustomFieldDefinitionsMapping = uniqueIdentifiers.reduce((map, identifier) => ({
|
|
52
|
+
...map,
|
|
53
|
+
[identifier]: [],
|
|
54
|
+
}), {});
|
|
55
|
+
const customFieldDefinitions = await DefinitionRepo.findByEntityIds(
|
|
56
|
+
modelType,
|
|
57
|
+
uniqueIdentifiers,
|
|
58
|
+
{ transaction: options.transaction },
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const definitionsMap = customFieldDefinitions.reduce((map, definition) => ({
|
|
62
|
+
...map,
|
|
63
|
+
[definition.id]: definition,
|
|
64
|
+
}), {});
|
|
65
|
+
|
|
66
|
+
customFieldDefinitions.forEach((cfd) => {
|
|
67
|
+
identifierCustomFieldDefinitionsMapping[cfd.entityId].push(cfd);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Get the values per instates ids:
|
|
71
|
+
const instancesIds = instances.map((i) => i[primaryKey]);
|
|
72
|
+
|
|
73
|
+
const customFieldValues = await ValueRepo.findValuesByModelIds(
|
|
74
|
+
instancesIds,
|
|
75
|
+
{ transaction: options.transaction },
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Group fields by modelId
|
|
79
|
+
const valuesGroupByInstance: {
|
|
80
|
+
[modelId: string]: CustomFieldValue[];
|
|
81
|
+
} = customFieldValues.reduce((acc, v) => {
|
|
82
|
+
const { modelId } = v;
|
|
83
|
+
if (!acc[modelId]) {
|
|
84
|
+
acc[modelId] = [];
|
|
85
|
+
}
|
|
86
|
+
acc[modelId].push(v);
|
|
87
|
+
return acc;
|
|
88
|
+
}, {});
|
|
89
|
+
|
|
90
|
+
// Attach custom fields to the instances
|
|
91
|
+
instances.forEach((instance) => {
|
|
92
|
+
const customFields = {};
|
|
93
|
+
const { id } = instance;
|
|
94
|
+
const instanceValues = valuesGroupByInstance[id];
|
|
95
|
+
if (instanceValues) {
|
|
96
|
+
const serializedCustomFields = serializeCustomFields(instanceValues, definitionsMap);
|
|
97
|
+
Object.assign(customFields, serializedCustomFields);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
scopeAttributes.forEach((attribute) => {
|
|
101
|
+
const identifier = instance[attribute];
|
|
102
|
+
const entityCustomFieldDefinitions = identifierCustomFieldDefinitionsMapping[identifier];
|
|
103
|
+
if (entityCustomFieldDefinitions?.length > 0) {
|
|
104
|
+
entityCustomFieldDefinitions.forEach((customFieldDefinition) => {
|
|
105
|
+
if (customFields[customFieldDefinition.name] === undefined) {
|
|
106
|
+
customFields[customFieldDefinition.name] = null;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
if (customFields && Object.keys(customFields).length > 0) {
|
|
112
|
+
logger.info('sadot - enrichResults - customFields', customFields);
|
|
113
|
+
instance.customFields = customFields;
|
|
114
|
+
} else {
|
|
115
|
+
logger.info('sadot - enrichResults - no customFields');
|
|
116
|
+
}
|
|
117
|
+
options.attributesToRemove?.forEach?.((attribute) => {
|
|
118
|
+
delete instance.dataValues?.[attribute];
|
|
119
|
+
// if raw:
|
|
120
|
+
delete instance?.[attribute];
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export default enrichResults;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* eslint-disable no-param-reassign */
|
|
2
|
+
import logger from '../utils/logger';
|
|
3
|
+
|
|
4
|
+
const doScopeAttributesMissing = (
|
|
5
|
+
scopeAttributes: string[],
|
|
6
|
+
queryAttributes: (string | string[])[],
|
|
7
|
+
): string[] => {
|
|
8
|
+
const attributes = scopeAttributes
|
|
9
|
+
.filter((attribute) => !queryAttributes.includes(attribute));
|
|
10
|
+
if (!queryAttributes.includes?.('id')) {
|
|
11
|
+
attributes.push('id');
|
|
12
|
+
}
|
|
13
|
+
return attributes;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// eslint-disable-next-line import/prefer-default-export
|
|
17
|
+
export const beforeFind = (scopeAttributes: string[]) => (options): void => {
|
|
18
|
+
const { attributes: queryAttributes } = options;
|
|
19
|
+
if (queryAttributes?.includes('customFields')) {
|
|
20
|
+
const missingScopeAttributes = doScopeAttributesMissing(scopeAttributes, queryAttributes);
|
|
21
|
+
logger.debug('sadot - before find hook');
|
|
22
|
+
if (missingScopeAttributes?.length > 0) {
|
|
23
|
+
queryAttributes.push(...missingScopeAttributes);
|
|
24
|
+
options.attributesToRemove = missingScopeAttributes;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import enrichResults from './enrich';
|
|
2
|
+
import { beforeFind } from './find';
|
|
3
|
+
import { beforeBulkUpdate, beforeUpdate } from './update';
|
|
4
|
+
import { beforeBulkCreate, beforeCreate } from './create';
|
|
5
|
+
import workaround from './workaround';
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
enrichResults,
|
|
9
|
+
beforeFind,
|
|
10
|
+
beforeBulkUpdate,
|
|
11
|
+
beforeUpdate,
|
|
12
|
+
beforeBulkCreate,
|
|
13
|
+
beforeCreate,
|
|
14
|
+
workaround,
|
|
15
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import logger from '../utils/logger';
|
|
2
|
+
import * as ValueRepo from '../repository/value';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A hook to update the custom fields when updating a model (more then one instance).
|
|
6
|
+
*/
|
|
7
|
+
export const beforeBulkUpdate = (options): void => {
|
|
8
|
+
// This will activate the beforeUpdate hook on each updating instance.
|
|
9
|
+
// eslint-disable-next-line no-param-reassign
|
|
10
|
+
options.individualHooks = true;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A hook to update the custom fields when updating a model instance.
|
|
15
|
+
* TODO - cleanup if update fail
|
|
16
|
+
*/
|
|
17
|
+
export const beforeUpdate = (scopeAttributes: string[]) => async (
|
|
18
|
+
instance,
|
|
19
|
+
options,
|
|
20
|
+
): Promise<void> => {
|
|
21
|
+
logger.debug('sadot - before update hook');
|
|
22
|
+
const { fields } = options;
|
|
23
|
+
const modelType = instance.constructor.name;
|
|
24
|
+
const identifiers = scopeAttributes.map((attribute) => instance[attribute]);
|
|
25
|
+
|
|
26
|
+
const customFieldsIdx = fields.indexOf('customFields');
|
|
27
|
+
if (customFieldsIdx > -1) {
|
|
28
|
+
const { customFields } = instance;
|
|
29
|
+
await ValueRepo.updateValues(
|
|
30
|
+
modelType,
|
|
31
|
+
instance.id,
|
|
32
|
+
identifiers,
|
|
33
|
+
customFields,
|
|
34
|
+
{ transaction: options.transaction },
|
|
35
|
+
);
|
|
36
|
+
// eslint-disable-next-line no-param-reassign
|
|
37
|
+
fields.splice(customFieldsIdx, 1);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workaround to a bug in sequelize.🐛
|
|
3
|
+
*
|
|
4
|
+
* **afterFind hook** isn't working on nested (included) models.
|
|
5
|
+
* The solution here is to add a global afterFind hook,
|
|
6
|
+
* which manually calls the afterFind hook of each model, recursively
|
|
7
|
+
* https://github.com/sequelize/sequelize/issues/4627
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const handleChildrenAfterFindHook = async (instances, options, level = 0) => {
|
|
11
|
+
if (!instances) return Promise.resolve();
|
|
12
|
+
|
|
13
|
+
if (Array.isArray(instances)) {
|
|
14
|
+
return Promise.all(instances.map((instance) => {
|
|
15
|
+
const { options: instanceOptions } = instance.constructor;
|
|
16
|
+
return handleChildrenAfterFindHook(
|
|
17
|
+
instance,
|
|
18
|
+
instanceOptions,
|
|
19
|
+
level,
|
|
20
|
+
);
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const instance = instances;
|
|
25
|
+
const { constructor } = instance;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Root model will have already run their "afterFind" hook.
|
|
29
|
+
* Only run children "afterFind" hooks.
|
|
30
|
+
*/
|
|
31
|
+
if (level >= 1) {
|
|
32
|
+
await constructor.runHooks('afterFind', instance, options);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { associations } = constructor;
|
|
36
|
+
const associatedNames = Object.keys(instance).filter((attribute) =>
|
|
37
|
+
Object.keys(associations).includes(attribute));
|
|
38
|
+
|
|
39
|
+
if (associatedNames.length) {
|
|
40
|
+
const childInstances = associatedNames.map((name) => instance[name]);
|
|
41
|
+
return handleChildrenAfterFindHook(childInstances, options, level + 1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return Promise.resolve();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default handleChildrenAfterFindHook;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Application } from 'express';
|
|
2
|
+
import { DataTypes } from 'sequelize';
|
|
3
|
+
import { Sequelize } from 'sequelize-typescript';
|
|
4
|
+
import { initTables, initTestModels } from './models';
|
|
5
|
+
import api from './api';
|
|
6
|
+
import {
|
|
7
|
+
beforeFind,
|
|
8
|
+
enrichResults,
|
|
9
|
+
beforeBulkUpdate,
|
|
10
|
+
beforeUpdate,
|
|
11
|
+
beforeCreate,
|
|
12
|
+
beforeBulkCreate,
|
|
13
|
+
} from './hooks';
|
|
14
|
+
import initDB from './utils/db';
|
|
15
|
+
import logger from './utils/logger';
|
|
16
|
+
import { CustomFieldOptions, ModelFetcher, ModelOptions } from './types';
|
|
17
|
+
|
|
18
|
+
export * from './utils/validations/custom-fields';
|
|
19
|
+
|
|
20
|
+
const addHooks = (models: ModelOptions[], getModel: ModelFetcher) => {
|
|
21
|
+
models.forEach(async ({ name, scopeAttributes }) => {
|
|
22
|
+
try {
|
|
23
|
+
const model = getModel(name);
|
|
24
|
+
if (!model) {
|
|
25
|
+
logger.warn('sadot - tried to addHooks to a model that does not exist yet', {
|
|
26
|
+
name,
|
|
27
|
+
scopeAttributes,
|
|
28
|
+
});
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
model.rawAttributes.customFields = {
|
|
32
|
+
type: DataTypes.VIRTUAL,
|
|
33
|
+
};
|
|
34
|
+
model.refreshAttributes();
|
|
35
|
+
// TODO: Uncomment after tests are passed
|
|
36
|
+
// model.addHook('afterFind', workaround);
|
|
37
|
+
model.addHook('beforeFind', 'sadot-beforeFind', beforeFind(scopeAttributes));
|
|
38
|
+
model.addHook('beforeBulkCreate', 'sadot-beforeBulkCreate', beforeBulkCreate);
|
|
39
|
+
model.addHook('beforeBulkUpdate', 'sadot-beforeBulkUpdate', beforeBulkUpdate);
|
|
40
|
+
model.addHook('beforeCreate', 'sadot-beforeCreate', beforeCreate(scopeAttributes));
|
|
41
|
+
model.addHook('beforeUpdate', 'sadot-beforeUpdate', beforeUpdate(scopeAttributes));
|
|
42
|
+
model.addHook('afterFind', 'sadot-afterFind', enrichResults(name, scopeAttributes));
|
|
43
|
+
model.addHook('afterUpdate', 'sadot-afterUpdate', enrichResults(name, scopeAttributes));
|
|
44
|
+
model.addHook('afterCreate', 'sadot-afterCreate', enrichResults(name, scopeAttributes));
|
|
45
|
+
} catch (e) {
|
|
46
|
+
logger.error(`Could not add custom fields hook to model ${name}. `, e);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Adding custom fields enrichment to the models inside the MODELS_FILE_NAME json file
|
|
53
|
+
* @see {@link 'custom-fields/config'} for configurations
|
|
54
|
+
*/
|
|
55
|
+
const useCustomFields = async (
|
|
56
|
+
app: Application | null,
|
|
57
|
+
getModel: ModelFetcher,
|
|
58
|
+
options: CustomFieldOptions,
|
|
59
|
+
): Promise<Sequelize> => {
|
|
60
|
+
const { models } = options;
|
|
61
|
+
if (app) {
|
|
62
|
+
app.use('/api', api);
|
|
63
|
+
}
|
|
64
|
+
const sequelize = initDB(options.databaseConfig);
|
|
65
|
+
if (process.env.NODE_ENV === 'test') {
|
|
66
|
+
await initTestModels(sequelize);
|
|
67
|
+
}
|
|
68
|
+
addHooks(models, getModel);
|
|
69
|
+
await initTables(sequelize, options.getUser);
|
|
70
|
+
logger.debug('sadot - custom fields finished initializing with models', models);
|
|
71
|
+
return sequelize;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export default useCustomFields;
|
|
75
|
+
|
|
76
|
+
const removeHooks = (models: ModelOptions[], getModel: ModelFetcher) => {
|
|
77
|
+
models.forEach(async ({ name }) => {
|
|
78
|
+
try {
|
|
79
|
+
const model = getModel(name);
|
|
80
|
+
if (!model) return;
|
|
81
|
+
if (model.rawAttributes.customFields) {
|
|
82
|
+
delete model.rawAttributes.customFields;
|
|
83
|
+
model.refreshAttributes();
|
|
84
|
+
}
|
|
85
|
+
// model.removeHook('afterFind', 'sadot-workaround');
|
|
86
|
+
model.removeHook('beforeFind', 'sadot-beforeFind');
|
|
87
|
+
model.removeHook('beforeBulkCreate', 'sadot-beforeBulkCreate');
|
|
88
|
+
model.removeHook('beforeBulkUpdate', 'sadot-beforeBulkUpdate');
|
|
89
|
+
model.removeHook('beforeCreate', 'sadot-beforeCreate');
|
|
90
|
+
model.removeHook('beforeUpdate', 'sadot-beforeUpdate');
|
|
91
|
+
model.removeHook('afterFind', 'sadot-afterFind');
|
|
92
|
+
model.removeHook('afterUpdate', 'sadot-afterUpdate');
|
|
93
|
+
} catch (e) {
|
|
94
|
+
logger.error(`Could not add custom fields hook to model ${name}. `, e);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const disableCustomFields = (models, getModel) => {
|
|
100
|
+
removeHooks(models, getModel);
|
|
101
|
+
};
|