@autofleet/sadot 0.9.1-beta-8053a0cc.0 → 0.9.1
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/api/v1/definition/index.js +4 -4
- package/dist/api/v1/definition/validations.js +9 -9
- package/dist/api/v1/errors.d.ts +3 -1
- package/dist/api/v1/errors.js +1 -4
- package/dist/events/index.js +4 -0
- package/dist/models/CustomFieldEntries.d.ts +4 -4
- package/dist/models/CustomFieldEntries.js +29 -25
- package/dist/models/index.d.ts +8 -4
- package/dist/models/index.js +22 -11
- package/dist/repository/definition.d.ts +6 -3
- package/dist/repository/definition.js +11 -14
- package/dist/scopes/filter.js +0 -3
- package/dist/tests/mocks/definition.mock.js +1 -0
- package/dist/tests/mocks/events.mock.d.ts +1 -0
- package/dist/tests/mocks/events.mock.js +3 -1
- package/dist/types/entries/index.d.ts +13 -3
- package/package.json +2 -2
- package/src/api/v1/definition/index.ts +13 -14
- package/src/api/v1/definition/validations.ts +11 -13
- package/src/api/v1/errors.ts +4 -7
- package/src/events/index.ts +4 -0
- package/src/models/CustomFieldEntries.ts +29 -25
- package/src/models/index.ts +31 -15
- package/src/repository/definition.ts +13 -18
- package/src/scopes/filter.ts +0 -3
- package/src/tests/mocks/definition.mock.ts +11 -10
- package/src/tests/mocks/events.mock.ts +3 -0
- package/src/types/entries/index.ts +13 -3
- package/.env +0 -4
|
@@ -51,7 +51,7 @@ router.post('/', async (req, res) => {
|
|
|
51
51
|
}
|
|
52
52
|
catch (err) {
|
|
53
53
|
logger_1.default.error('Failed to create custom field definition', err);
|
|
54
|
-
return (0, errors_2.default)(err, res, { message: `Error in create ${ENTITY} request` });
|
|
54
|
+
return (0, errors_2.default)(err, res, { logger: logger_1.default, message: `Error in create ${ENTITY} request` });
|
|
55
55
|
}
|
|
56
56
|
});
|
|
57
57
|
/**
|
|
@@ -68,7 +68,7 @@ router.get('/:customFieldDefinitionId', async (req, res) => {
|
|
|
68
68
|
}
|
|
69
69
|
catch (err) {
|
|
70
70
|
logger_1.default.error('Failed to fetch custom field definition', err);
|
|
71
|
-
return (0, errors_2.default)(err, res, { message: `Error in get ${ENTITY} request` });
|
|
71
|
+
return (0, errors_2.default)(err, res, { logger: logger_1.default, message: `Error in get ${ENTITY} request` });
|
|
72
72
|
}
|
|
73
73
|
});
|
|
74
74
|
/**
|
|
@@ -87,7 +87,7 @@ router.get('/', async (req, res) => {
|
|
|
87
87
|
}
|
|
88
88
|
catch (err) {
|
|
89
89
|
logger_1.default.error('Failed to fetch custom field definitions', err);
|
|
90
|
-
return (0, errors_2.default)(err, res, { message: `Error in get all ${ENTITY} request` });
|
|
90
|
+
return (0, errors_2.default)(err, res, { logger: logger_1.default, message: `Error in get all ${ENTITY} request` });
|
|
91
91
|
}
|
|
92
92
|
});
|
|
93
93
|
/**
|
|
@@ -110,7 +110,7 @@ router.patch('/:customFieldDefinitionId', async (req, res) => {
|
|
|
110
110
|
}
|
|
111
111
|
catch (err) {
|
|
112
112
|
logger_1.default.error('Failed to patch custom field definition', err);
|
|
113
|
-
return (0, errors_2.default)(err, res, { message: `Error in update ${ENTITY} request` });
|
|
113
|
+
return (0, errors_2.default)(err, res, { logger: logger_1.default, message: `Error in update ${ENTITY} request` });
|
|
114
114
|
}
|
|
115
115
|
});
|
|
116
116
|
exports.default = router;
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.validateCustomFieldDefinitionUpdate = exports.validateCustomFieldDefinitionCreation = void 0;
|
|
7
|
+
/* eslint-disable newline-per-chained-call */
|
|
7
8
|
const joi_1 = __importDefault(require("joi"));
|
|
8
9
|
const constants_1 = require("../../../utils/constants");
|
|
9
10
|
const FileValidationSchema = joi_1.default.object({
|
|
@@ -16,7 +17,6 @@ const statusValidationObject = joi_1.default.object({
|
|
|
16
17
|
value: joi_1.default.string().required(),
|
|
17
18
|
color: joi_1.default.string().required(),
|
|
18
19
|
});
|
|
19
|
-
const statusValidationObjectSchema = joi_1.default.array().items(statusValidationObject).min(1).unique('value');
|
|
20
20
|
/**
|
|
21
21
|
* Schema for the validation of custom field definition
|
|
22
22
|
* The only custom validation is for
|
|
@@ -28,12 +28,12 @@ const statusValidationObjectSchema = joi_1.default.array().items(statusValidatio
|
|
|
28
28
|
*/
|
|
29
29
|
const ValidationSchema = joi_1.default.when('fieldType', {
|
|
30
30
|
is: constants_1.CustomFieldDefinitionType.SELECT,
|
|
31
|
-
then: joi_1.default.array().items(joi_1.default.string()).min(1).unique(),
|
|
32
|
-
otherwise: joi_1.default.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
then: joi_1.default.array().required().items(joi_1.default.string()).min(1).unique(),
|
|
32
|
+
otherwise: joi_1.default.when('fieldType', {
|
|
33
|
+
is: constants_1.CustomFieldDefinitionType.STATUS,
|
|
34
|
+
then: joi_1.default.array().required().items(statusValidationObject).min(1).unique('value'),
|
|
35
|
+
otherwise: joi_1.default.forbidden(),
|
|
36
|
+
}),
|
|
37
37
|
});
|
|
38
38
|
const DefaultValueSchema = joi_1.default.when('fieldType', {
|
|
39
39
|
switch: [
|
|
@@ -60,7 +60,7 @@ const CustomFieldDefinitionCreationSchema = joi_1.default.object({
|
|
|
60
60
|
required: joi_1.default.boolean(),
|
|
61
61
|
disabled: joi_1.default.boolean(),
|
|
62
62
|
blockEditingFromUI: joi_1.default.boolean(),
|
|
63
|
-
});
|
|
63
|
+
}).oxor('required', 'blockEditingFromUI', { isPresent: (value) => value === true });
|
|
64
64
|
const CustomFieldDefinitionUpdateSchema = joi_1.default.object({
|
|
65
65
|
displayName: joi_1.default.string(),
|
|
66
66
|
validation: ValidationSchema,
|
|
@@ -70,7 +70,7 @@ const CustomFieldDefinitionUpdateSchema = joi_1.default.object({
|
|
|
70
70
|
required: joi_1.default.boolean(),
|
|
71
71
|
disabled: joi_1.default.boolean(),
|
|
72
72
|
blockEditingFromUI: joi_1.default.boolean(),
|
|
73
|
-
});
|
|
73
|
+
}).oxor('required', 'blockEditingFromUI', { isPresent: (value) => value === true });
|
|
74
74
|
const validateCustomFieldDefinitionCreation = (payload) => CustomFieldDefinitionCreationSchema.validateAsync(payload, { abortEarly: false });
|
|
75
75
|
exports.validateCustomFieldDefinitionCreation = validateCustomFieldDefinitionCreation;
|
|
76
76
|
const validateCustomFieldDefinitionUpdate = (payload) => CustomFieldDefinitionUpdateSchema.validateAsync(payload, { abortEarly: false });
|
package/dist/api/v1/errors.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
import type { Response } from 'express';
|
|
2
|
+
import { type LogPayload } from '@autofleet/errors';
|
|
3
|
+
declare const _default: (err: any, res: Response, additionalData?: LogPayload) => Promise<any>;
|
|
2
4
|
export default _default;
|
package/dist/api/v1/errors.js
CHANGED
|
@@ -5,10 +5,7 @@ const joi_1 = require("joi");
|
|
|
5
5
|
const sequelize_1 = require("sequelize");
|
|
6
6
|
exports.default = (err, res, additionalData = undefined) => {
|
|
7
7
|
let error = err;
|
|
8
|
-
if (err instanceof
|
|
9
|
-
error = new errors_1.BadRequest([err], null);
|
|
10
|
-
}
|
|
11
|
-
if (err instanceof sequelize_1.ValidationError) {
|
|
8
|
+
if ([joi_1.ValidationError, sequelize_1.ValidationError].some((ErrClass) => err instanceof ErrClass)) {
|
|
12
9
|
error = new errors_1.BadRequest([err], null);
|
|
13
10
|
}
|
|
14
11
|
return (0, errors_1.handleError)(error, res, additionalData);
|
package/dist/events/index.js
CHANGED
|
@@ -27,6 +27,10 @@ const modelTableMapping = {
|
|
|
27
27
|
tableName: 'dim_custom_field_value',
|
|
28
28
|
eventVersion: '1',
|
|
29
29
|
},
|
|
30
|
+
CustomFieldEntries: {
|
|
31
|
+
tableName: 'dim_custom_field_entries',
|
|
32
|
+
eventVersion: '1',
|
|
33
|
+
},
|
|
30
34
|
};
|
|
31
35
|
const sendDimEvent = (instance) => {
|
|
32
36
|
const mapping = modelTableMapping[instance.constructor.name];
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { Model } from 'sequelize-typescript';
|
|
2
2
|
declare class CustomFieldEntries extends Model {
|
|
3
|
-
/** The ID of the
|
|
3
|
+
/** The ID of the model of which this row hold the custom field entries of, e.g. vehicleId / stopPointId / etc. */
|
|
4
4
|
modelId: string;
|
|
5
|
+
/** The ID of the entity of which this row hold the custom field entries of, e.g. fleetId / etc. */
|
|
6
|
+
entityId: string;
|
|
5
7
|
/** A dictionary of customFields and values with the following structure: `{ customFieldName: 'CustomFieldValue' }` */
|
|
6
|
-
customFields: any
|
|
8
|
+
customFields: Record<string, any>;
|
|
7
9
|
/** The type of model which this custom field entry represents. e.g. Vehicle / StopPoint / etc. */
|
|
8
10
|
modelType: string;
|
|
9
11
|
createdAt?: Date;
|
|
10
12
|
updatedAt?: Date;
|
|
11
|
-
static validateValues(instances: CustomFieldEntries[]): Promise<void>;
|
|
12
|
-
static validateValue(instance: CustomFieldEntries): Promise<void>;
|
|
13
13
|
static afterSaveHandler(instance: CustomFieldEntries, options: any): void;
|
|
14
14
|
}
|
|
15
15
|
export default CustomFieldEntries;
|
|
@@ -37,14 +37,6 @@ const events_1 = require("../events");
|
|
|
37
37
|
const CustomFieldDefinitionRepo = __importStar(require("../repository/definition"));
|
|
38
38
|
const validations_1 = require("../utils/validations");
|
|
39
39
|
let CustomFieldEntries = class CustomFieldEntries extends sequelize_typescript_1.Model {
|
|
40
|
-
static async validateValues(instances) {
|
|
41
|
-
const definitionsByName = await CustomFieldDefinitionRepo.getCustomFieldDefinitionsDictionary(instances);
|
|
42
|
-
instances.forEach((instance) => (0, validations_1.validateInstanceCustomFieldEntries)(instance, definitionsByName));
|
|
43
|
-
}
|
|
44
|
-
static async validateValue(instance) {
|
|
45
|
-
const definitionsByName = await CustomFieldDefinitionRepo.getCustomFieldDefinitionsDictionary([instance]);
|
|
46
|
-
(0, validations_1.validateInstanceCustomFieldEntries)(instance, definitionsByName);
|
|
47
|
-
}
|
|
48
40
|
static afterSaveHandler(instance, options) {
|
|
49
41
|
if (options.transaction) {
|
|
50
42
|
options.transaction.afterCommit(() => (0, events_1.sendDimEvent)(instance[0]));
|
|
@@ -60,14 +52,24 @@ __decorate([
|
|
|
60
52
|
type: sequelize_typescript_1.DataType.UUID,
|
|
61
53
|
allowNull: false,
|
|
62
54
|
})
|
|
63
|
-
/** The ID of the
|
|
55
|
+
/** The ID of the model of which this row hold the custom field entries of, e.g. vehicleId / stopPointId / etc. */
|
|
64
56
|
,
|
|
65
57
|
__metadata("design:type", String)
|
|
66
58
|
], CustomFieldEntries.prototype, "modelId", void 0);
|
|
59
|
+
__decorate([
|
|
60
|
+
(0, sequelize_typescript_1.Column)({
|
|
61
|
+
type: sequelize_typescript_1.DataType.UUID,
|
|
62
|
+
allowNull: false,
|
|
63
|
+
})
|
|
64
|
+
/** The ID of the entity of which this row hold the custom field entries of, e.g. fleetId / etc. */
|
|
65
|
+
,
|
|
66
|
+
__metadata("design:type", String)
|
|
67
|
+
], CustomFieldEntries.prototype, "entityId", void 0);
|
|
67
68
|
__decorate([
|
|
68
69
|
(0, sequelize_typescript_1.Column)({
|
|
69
70
|
type: sequelize_typescript_1.DataType.JSONB,
|
|
70
|
-
allowNull:
|
|
71
|
+
allowNull: false,
|
|
72
|
+
defaultValue: {},
|
|
71
73
|
})
|
|
72
74
|
/** A dictionary of customFields and values with the following structure: `{ customFieldName: 'CustomFieldValue' }` */
|
|
73
75
|
,
|
|
@@ -90,21 +92,6 @@ __decorate([
|
|
|
90
92
|
sequelize_typescript_1.Column,
|
|
91
93
|
__metadata("design:type", Date)
|
|
92
94
|
], CustomFieldEntries.prototype, "updatedAt", void 0);
|
|
93
|
-
__decorate([
|
|
94
|
-
sequelize_typescript_1.BeforeBulkCreate,
|
|
95
|
-
sequelize_typescript_1.BeforeBulkUpdate,
|
|
96
|
-
__metadata("design:type", Function),
|
|
97
|
-
__metadata("design:paramtypes", [Array]),
|
|
98
|
-
__metadata("design:returntype", Promise)
|
|
99
|
-
], CustomFieldEntries, "validateValues", null);
|
|
100
|
-
__decorate([
|
|
101
|
-
sequelize_typescript_1.BeforeUpdate,
|
|
102
|
-
sequelize_typescript_1.BeforeCreate,
|
|
103
|
-
sequelize_typescript_1.BeforeUpsert,
|
|
104
|
-
__metadata("design:type", Function),
|
|
105
|
-
__metadata("design:paramtypes", [CustomFieldEntries]),
|
|
106
|
-
__metadata("design:returntype", Promise)
|
|
107
|
-
], CustomFieldEntries, "validateValue", null);
|
|
108
95
|
__decorate([
|
|
109
96
|
sequelize_typescript_1.AfterUpsert,
|
|
110
97
|
__metadata("design:type", Function),
|
|
@@ -114,6 +101,23 @@ __decorate([
|
|
|
114
101
|
CustomFieldEntries = __decorate([
|
|
115
102
|
(0, sequelize_typescript_1.Table)({
|
|
116
103
|
timestamps: true,
|
|
104
|
+
indexes: [
|
|
105
|
+
{
|
|
106
|
+
name: 'idx_cfe_custom_fields',
|
|
107
|
+
using: 'gin',
|
|
108
|
+
operator: 'jsonb_path_ops',
|
|
109
|
+
fields: ['custom_fields'],
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
validate: {
|
|
113
|
+
async validationByType() {
|
|
114
|
+
if (!Object.keys(this.customFields ?? {}).length) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const definitionsByName = await CustomFieldDefinitionRepo.getCustomFieldDefinitionsDictionary([this]);
|
|
118
|
+
(0, validations_1.validateInstanceCustomFieldEntries)(this, definitionsByName);
|
|
119
|
+
},
|
|
120
|
+
},
|
|
117
121
|
})
|
|
118
122
|
], CustomFieldEntries);
|
|
119
123
|
exports.default = CustomFieldEntries;
|
package/dist/models/index.d.ts
CHANGED
|
@@ -6,8 +6,12 @@ import ContextAwareTestModel from './tests/contextAwareModels/ContextAwareTestMo
|
|
|
6
6
|
import ContextTestModel from './tests/contextAwareModels/ContextTestModel';
|
|
7
7
|
import AssociatedTestModel from './tests/AssociatedTestModel';
|
|
8
8
|
import type { CustomFieldOptions } from '../types';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
import CustomFieldEntries from './CustomFieldEntries';
|
|
10
|
+
interface InitTablesOptions {
|
|
11
|
+
schemaPrefix?: string;
|
|
12
|
+
schemaVersion?: string;
|
|
13
|
+
useCustomFieldsEntries?: boolean;
|
|
14
|
+
}
|
|
15
|
+
declare const initTables: (sequelize: Sequelize, getUser: CustomFieldOptions['getUser'], { schemaPrefix, schemaVersion, useCustomFieldsEntries, }?: InitTablesOptions) => Promise<void>;
|
|
12
16
|
declare const initTestModels: (sequelize: Sequelize) => Promise<void>;
|
|
13
|
-
export { CustomFieldValue, CustomFieldDefinition, TestModel, AssociatedTestModel, ContextAwareTestModel, ContextTestModel, initTables, initTestModels, };
|
|
17
|
+
export { CustomFieldValue, CustomFieldDefinition, CustomFieldEntries, TestModel, AssociatedTestModel, ContextAwareTestModel, ContextTestModel, initTables, initTestModels, };
|
package/dist/models/index.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.initTestModels = exports.initTables = exports.ContextTestModel = exports.ContextAwareTestModel = exports.AssociatedTestModel = exports.TestModel = exports.CustomFieldDefinition = exports.CustomFieldValue = void 0;
|
|
6
|
+
exports.initTestModels = exports.initTables = exports.ContextTestModel = exports.ContextAwareTestModel = exports.AssociatedTestModel = exports.TestModel = exports.CustomFieldEntries = exports.CustomFieldDefinition = exports.CustomFieldValue = void 0;
|
|
7
7
|
/* eslint-disable no-param-reassign */
|
|
8
8
|
const sequelize_1 = require("sequelize");
|
|
9
9
|
const logger_1 = __importDefault(require("../utils/logger"));
|
|
@@ -20,21 +20,23 @@ exports.ContextTestModel = ContextTestModel_1.default;
|
|
|
20
20
|
const AssociatedTestModel_1 = __importDefault(require("./tests/AssociatedTestModel"));
|
|
21
21
|
exports.AssociatedTestModel = AssociatedTestModel_1.default;
|
|
22
22
|
const CustomFieldEntries_1 = __importDefault(require("./CustomFieldEntries"));
|
|
23
|
+
exports.CustomFieldEntries = CustomFieldEntries_1.default;
|
|
23
24
|
const productionModels = [CustomFieldDefinition_1.default, CustomFieldValue_1.default];
|
|
24
25
|
const testModels = [TestModel_1.default, AssociatedTestModel_1.default, ContextAwareTestModel_1.default, ContextTestModel_1.default];
|
|
25
26
|
const SADOT_MIGRATION_PREFIX = 'sadot-migration';
|
|
26
27
|
const SCHEMA_VERSION = 'fb0fa867-1241-4816-b08d-5ed9060c7ae5';
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
const initTables = async (sequelize, getUser, { schemaPrefix, schemaVersion, useCustomFieldsEntries, } = {
|
|
29
|
+
schemaPrefix: SADOT_MIGRATION_PREFIX,
|
|
30
|
+
schemaVersion: SCHEMA_VERSION,
|
|
31
|
+
useCustomFieldsEntries: false,
|
|
32
|
+
}) => {
|
|
33
|
+
const CUSTOM_FIELDS_SCHEMA_VERSION = `${schemaPrefix}_${schemaVersion}${useCustomFieldsEntries ? '_withEntries' : ''}`;
|
|
31
34
|
logger_1.default.info('custom-fields: initialize custom-fields tables');
|
|
32
35
|
// Detect models and import them to the orm
|
|
33
36
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
34
37
|
if (!sequelize.addModels) {
|
|
35
38
|
throw new Error('sequelize instance must have addModels function');
|
|
36
39
|
}
|
|
37
|
-
const schemaVersion = useCustomFieldsEntries ? CUSTOM_FIELDS_SCHEMA_VERSION_WITH_ENTRIES : CUSTOM_FIELDS_SCHEMA_VERSION;
|
|
38
40
|
if (useCustomFieldsEntries) {
|
|
39
41
|
productionModels.push(CustomFieldEntries_1.default);
|
|
40
42
|
}
|
|
@@ -68,17 +70,26 @@ const initTables = async (sequelize, getUser, options) => {
|
|
|
68
70
|
timestamps: false,
|
|
69
71
|
schema: 'public',
|
|
70
72
|
});
|
|
71
|
-
const migrations = await SequelizeMeta.findAll({ raw: true });
|
|
72
|
-
const currentSadotSchemaVersion = migrations.
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
const migrations = await SequelizeMeta.findAll({ where: { name: { [sequelize_1.Op.like]: `${schemaPrefix}%` } }, raw: true });
|
|
74
|
+
const currentSadotSchemaVersion = migrations.at(-1);
|
|
75
|
+
const expectedSchemaVersionIndex = migrations.findIndex((m) => m.name === CUSTOM_FIELDS_SCHEMA_VERSION);
|
|
76
|
+
if (!currentSadotSchemaVersion || currentSadotSchemaVersion.name !== CUSTOM_FIELDS_SCHEMA_VERSION) {
|
|
75
77
|
await CustomFieldDefinition_1.default.sync({ alter: true });
|
|
76
78
|
await CustomFieldValue_1.default.sync({ alter: true });
|
|
79
|
+
// T.Y TODO: Remove the if statement once we're ready to add the new entries table for all MS
|
|
77
80
|
if (useCustomFieldsEntries) {
|
|
78
81
|
await CustomFieldEntries_1.default.sync({ alter: true });
|
|
79
82
|
}
|
|
80
|
-
|
|
83
|
+
if (expectedSchemaVersionIndex === -1) {
|
|
84
|
+
await SequelizeMeta.create({ name: CUSTOM_FIELDS_SCHEMA_VERSION });
|
|
85
|
+
}
|
|
81
86
|
logger_1.default.info('custom-fields: models synced');
|
|
87
|
+
if (migrations.length && expectedSchemaVersionIndex !== -1 && expectedSchemaVersionIndex < migrations.length - 1) {
|
|
88
|
+
// We have existing migrations, and we are calling `sync`.
|
|
89
|
+
// This means we are in a `down` migration, and hence we should delete newer migrations to ensure we can reapply them.
|
|
90
|
+
const migrationsToDelete = migrations.slice(expectedSchemaVersionIndex + 1);
|
|
91
|
+
await SequelizeMeta.destroy({ where: { name: { [sequelize_1.Op.in]: migrationsToDelete.map((m) => m.name) } } });
|
|
92
|
+
}
|
|
82
93
|
}
|
|
83
94
|
};
|
|
84
95
|
exports.initTables = initTables;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { type Includeable, type Transaction, type FindOptions, type WhereOptions } from 'sequelize';
|
|
2
|
-
import { CustomFieldDefinition } from '../models';
|
|
2
|
+
import { CustomFieldDefinition, type CustomFieldEntries } from '../models';
|
|
3
3
|
import type { CreateCustomFieldDefinition, UpdateCustomFieldDefinition } from '../types/definition';
|
|
4
4
|
import type { ModelOptions } from '../types';
|
|
5
|
-
import type { CustomFieldEntriesDTO } from '../types/entries';
|
|
6
5
|
export declare const create: (data: CreateCustomFieldDefinition) => Promise<CustomFieldDefinition>;
|
|
7
6
|
interface SadotFindOptions {
|
|
8
7
|
withDisabled?: boolean;
|
|
@@ -27,7 +26,11 @@ export declare const destroy: (id: string) => Promise<number>;
|
|
|
27
26
|
* Return the names of the required fields for a given model
|
|
28
27
|
*/
|
|
29
28
|
export declare const getRequiredFields: (modelType: string, modelId: string | string[], entityId: string | string[], modelOptions?: ModelOptions) => Promise<string[]>;
|
|
30
|
-
|
|
29
|
+
/**
|
|
30
|
+
* @returns A promise resolving with a dictionary of custom field definitions by name.
|
|
31
|
+
* @throws A {@link MissingDefinitionError} if any of the custom fields doesn't have a definition.
|
|
32
|
+
*/
|
|
33
|
+
export declare const getCustomFieldDefinitionsDictionary: (instances: CustomFieldEntries[], options?: SadotGetDefinitionsByEntityIdsOptions) => Promise<{
|
|
31
34
|
[definitionName: string]: CustomFieldDefinition;
|
|
32
35
|
}>;
|
|
33
36
|
export {};
|
|
@@ -87,27 +87,24 @@ const getRequiredFields = async (modelType, modelId, entityId, modelOptions = {}
|
|
|
87
87
|
return [...new Set(requiredFieldsNames)];
|
|
88
88
|
};
|
|
89
89
|
exports.getRequiredFields = getRequiredFields;
|
|
90
|
+
/**
|
|
91
|
+
* @returns A promise resolving with a dictionary of custom field definitions by name.
|
|
92
|
+
* @throws A {@link MissingDefinitionError} if any of the custom fields doesn't have a definition.
|
|
93
|
+
*/
|
|
90
94
|
const getCustomFieldDefinitionsDictionary = async (instances, options = { withDisabled: false, modelOptions: {} }) => {
|
|
91
|
-
const { modelType } = instances[0];
|
|
95
|
+
const { modelType } = instances[0]?.dataValues ?? {};
|
|
92
96
|
const customFields = new Set();
|
|
93
97
|
const modelIds = [];
|
|
98
|
+
const entityIds = new Set();
|
|
94
99
|
instances.forEach((instance) => {
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
const { dataValues: { modelId, entityId, customFields: instanceCustomFields } } = instance;
|
|
101
|
+
modelIds.push(modelId);
|
|
102
|
+
entityIds.add(entityId);
|
|
103
|
+
Object.keys(instanceCustomFields ?? {}).forEach((fieldName) => {
|
|
97
104
|
customFields.add(fieldName);
|
|
98
105
|
});
|
|
99
106
|
});
|
|
100
|
-
const
|
|
101
|
-
const where = {
|
|
102
|
-
modelType,
|
|
103
|
-
...(!useEntityIdFromInclude && { entityId: modelIds }),
|
|
104
|
-
};
|
|
105
|
-
const definitions = await (0, exports.findAll)({
|
|
106
|
-
where,
|
|
107
|
-
transaction: options.transaction,
|
|
108
|
-
include: include?.(modelIds),
|
|
109
|
-
raw: true,
|
|
110
|
-
}, options);
|
|
107
|
+
const definitions = await (0, exports.findByEntityIds)(modelType, Array.from(entityIds), { ...options });
|
|
111
108
|
const matchedDefinitions = definitions.filter((def) => customFields.has(def.name));
|
|
112
109
|
const matchedDefinitionsByName = Object.fromEntries(matchedDefinitions.map((definition) => [definition.name, definition]));
|
|
113
110
|
if (!definitions?.length || matchedDefinitions.length !== customFields.size) {
|
package/dist/scopes/filter.js
CHANGED
|
@@ -18,9 +18,6 @@ const castIfNeeded = (columnName, conditionValue) => {
|
|
|
18
18
|
if (isDate(conditionValue)) {
|
|
19
19
|
return castValueToJsonb(columnName, 'timestamp');
|
|
20
20
|
}
|
|
21
|
-
if (!Number.isNaN(Number(conditionValue))) {
|
|
22
|
-
return castValueToJsonbNumeric(columnName);
|
|
23
|
-
}
|
|
24
21
|
return columnName;
|
|
25
22
|
};
|
|
26
23
|
const AND_DELIMITER = ' AND ';
|
|
@@ -64,6 +64,7 @@ const createDefinition = (defaults) => ({
|
|
|
64
64
|
fieldType: defaults?.fieldType || 'boolean',
|
|
65
65
|
entityId: defaults?.entityId || (0, node_crypto_1.randomUUID)(),
|
|
66
66
|
entityType: defaults?.entityType || 'fleetId',
|
|
67
|
+
...(defaults?.validation && { validation: defaults.validation }),
|
|
67
68
|
...(defaults?.defaultValue && { defaultValue: defaults.defaultValue }),
|
|
68
69
|
});
|
|
69
70
|
exports.createDefinition = createDefinition;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export declare const mockEvent: (events: any, eventName: any, numberOfEvents: any) => any[];
|
|
2
2
|
export declare const mockDimCustomFieldDefinitionEvent: (events: any, numberOfEvents: any) => any[];
|
|
3
3
|
export declare const mockDimCustomFieldValueEvent: (events: any, numberOfEvents: any) => any[];
|
|
4
|
+
export declare const mockDimCustomFieldEntriesEvent: (events: any, numberOfEvents: any) => any[];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/* eslint-disable no-param-reassign */
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.mockDimCustomFieldValueEvent = exports.mockDimCustomFieldDefinitionEvent = exports.mockEvent = void 0;
|
|
4
|
+
exports.mockDimCustomFieldEntriesEvent = exports.mockDimCustomFieldValueEvent = exports.mockDimCustomFieldDefinitionEvent = exports.mockEvent = void 0;
|
|
5
5
|
const mockEvent = (events, eventName, numberOfEvents) => {
|
|
6
6
|
events.sendObject = jest.fn();
|
|
7
7
|
return [
|
|
@@ -17,3 +17,5 @@ const mockDimCustomFieldDefinitionEvent = (events, numberOfEvents) => (0, export
|
|
|
17
17
|
exports.mockDimCustomFieldDefinitionEvent = mockDimCustomFieldDefinitionEvent;
|
|
18
18
|
const mockDimCustomFieldValueEvent = (events, numberOfEvents) => (0, exports.mockEvent)(events, 'dim_custom_field_value', numberOfEvents);
|
|
19
19
|
exports.mockDimCustomFieldValueEvent = mockDimCustomFieldValueEvent;
|
|
20
|
+
const mockDimCustomFieldEntriesEvent = (events, numberOfEvents) => (0, exports.mockEvent)(events, 'dim_custom_field_entries', numberOfEvents);
|
|
21
|
+
exports.mockDimCustomFieldEntriesEvent = mockDimCustomFieldEntriesEvent;
|
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import type { ValidationError } from 'joi';
|
|
2
2
|
export interface CustomFieldEntriesDTO {
|
|
3
3
|
modelId: string;
|
|
4
|
+
entityId: string;
|
|
4
5
|
modelType: string;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* A collection of custom fields where each key is the name of a CustomFieldDefinition and
|
|
8
|
+
* each value is the value of that custom field for this specific `modelId`.
|
|
9
|
+
*
|
|
10
|
+
* Example:
|
|
11
|
+
* {
|
|
12
|
+
* "vehicleColor": "Red",
|
|
13
|
+
* "vehicleType": "premium",
|
|
14
|
+
* "isActive": true
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
customFields: Record<string, any>;
|
|
8
18
|
createdAt?: Date;
|
|
9
19
|
updatedAt?: Date;
|
|
10
20
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@autofleet/sadot",
|
|
3
|
-
"version": "0.9.1
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"build": "rm -rf dist && tsc",
|
|
9
9
|
"linter": "eslint .",
|
|
10
10
|
"test": "jest --runInBand",
|
|
11
|
-
"coverage": "jest --coverage --runInBand
|
|
11
|
+
"coverage": "jest --coverage --runInBand",
|
|
12
12
|
"build-to-local-repo": "node --run build && cp -r dist/* ../$REPO/node_modules/$npm_package_name/dist",
|
|
13
13
|
"dev": "nodemon",
|
|
14
14
|
"watch": "npm-watch build-to-local-repo",
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { ResourceNotFoundError } from '@autofleet/errors';
|
|
2
2
|
import { Router } from 'express';
|
|
3
|
-
import type { Request, Response } from 'express';
|
|
4
3
|
import handleError from '../errors';
|
|
5
4
|
import * as DefinitionRepo from '../../../repository/definition';
|
|
6
5
|
import type { CreateCustomFieldDefinition, UpdateCustomFieldDefinition } from '../../../types/definition';
|
|
7
6
|
import { validateCustomFieldDefinitionCreation, validateCustomFieldDefinitionUpdate } from './validations';
|
|
8
7
|
import logger from '../../../utils/logger';
|
|
8
|
+
import type { CustomFieldDefinition } from '../../../models';
|
|
9
9
|
|
|
10
10
|
const router = Router({ mergeParams: true });
|
|
11
11
|
const ENTITY = 'CustomFieldDefinition';
|
|
@@ -15,12 +15,11 @@ const toPascalCase = (str: string): string => str.replace(/(^\w|-\w)/g, (subStr)
|
|
|
15
15
|
/**
|
|
16
16
|
* Create
|
|
17
17
|
*/
|
|
18
|
-
router.post('/', async (req
|
|
19
|
-
const { modelName } = req.params
|
|
18
|
+
router.post<{ modelName: string; }>('/', async (req, res) => {
|
|
19
|
+
const { modelName } = req.params;
|
|
20
20
|
const modelType = toPascalCase(modelName);
|
|
21
21
|
try {
|
|
22
|
-
const validatedPayload: CreateCustomFieldDefinition = await
|
|
23
|
-
validateCustomFieldDefinitionCreation(req.body);
|
|
22
|
+
const validatedPayload: CreateCustomFieldDefinition = await validateCustomFieldDefinitionCreation(req.body);
|
|
24
23
|
|
|
25
24
|
const customFieldDefinition = await DefinitionRepo.create({
|
|
26
25
|
...validatedPayload,
|
|
@@ -29,14 +28,14 @@ router.post('/', async (req: Request, res: Response) => {
|
|
|
29
28
|
return res.status(201).json(customFieldDefinition);
|
|
30
29
|
} catch (err) {
|
|
31
30
|
logger.error('Failed to create custom field definition', err);
|
|
32
|
-
return handleError(err, res, { message: `Error in create ${ENTITY} request` });
|
|
31
|
+
return handleError(err, res, { logger, message: `Error in create ${ENTITY} request` });
|
|
33
32
|
}
|
|
34
33
|
});
|
|
35
34
|
|
|
36
35
|
/**
|
|
37
36
|
* Get by id
|
|
38
37
|
*/
|
|
39
|
-
router.get('/:customFieldDefinitionId', async (req, res) => {
|
|
38
|
+
router.get<{ modelName: string; customFieldDefinitionId: string; }, CustomFieldDefinition>('/:customFieldDefinitionId', async (req, res) => {
|
|
40
39
|
const { customFieldDefinitionId } = req.params;
|
|
41
40
|
try {
|
|
42
41
|
const customFieldDefinition = await DefinitionRepo.findById(customFieldDefinitionId);
|
|
@@ -48,15 +47,15 @@ router.get('/:customFieldDefinitionId', async (req, res) => {
|
|
|
48
47
|
return res.json(customFieldDefinition);
|
|
49
48
|
} catch (err) {
|
|
50
49
|
logger.error('Failed to fetch custom field definition', err);
|
|
51
|
-
return handleError(err, res, { message: `Error in get ${ENTITY} request` });
|
|
50
|
+
return handleError(err, res, { logger, message: `Error in get ${ENTITY} request` });
|
|
52
51
|
}
|
|
53
52
|
});
|
|
54
53
|
|
|
55
54
|
/**
|
|
56
55
|
* Get all
|
|
57
56
|
*/
|
|
58
|
-
router.get('/', async (req, res) => {
|
|
59
|
-
const { params: { modelName }, query: { entityIds } } = req
|
|
57
|
+
router.get<{ modelName: string; }, CustomFieldDefinition[], never, { entityIds?: string[]; }>('/', async (req, res) => {
|
|
58
|
+
const { params: { modelName }, query: { entityIds } } = req;
|
|
60
59
|
|
|
61
60
|
const modelType = toPascalCase(modelName);
|
|
62
61
|
try {
|
|
@@ -68,15 +67,15 @@ router.get('/', async (req, res) => {
|
|
|
68
67
|
return res.json(customFieldDefinitions);
|
|
69
68
|
} catch (err) {
|
|
70
69
|
logger.error('Failed to fetch custom field definitions', err);
|
|
71
|
-
return handleError(err, res, { message: `Error in get all ${ENTITY} request` });
|
|
70
|
+
return handleError(err, res, { logger, message: `Error in get all ${ENTITY} request` });
|
|
72
71
|
}
|
|
73
72
|
});
|
|
74
73
|
|
|
75
74
|
/**
|
|
76
75
|
* Update
|
|
77
76
|
*/
|
|
78
|
-
router.patch('/:customFieldDefinitionId', async (req, res) => {
|
|
79
|
-
const { customFieldDefinitionId, modelName } = req.params
|
|
77
|
+
router.patch<{ modelName: string; customFieldDefinitionId: string; }, CustomFieldDefinition>('/:customFieldDefinitionId', async (req, res) => {
|
|
78
|
+
const { customFieldDefinitionId, modelName } = req.params;
|
|
80
79
|
const modelType = toPascalCase(modelName);
|
|
81
80
|
try {
|
|
82
81
|
const validatedPayload: UpdateCustomFieldDefinition = await validateCustomFieldDefinitionUpdate(req.body);
|
|
@@ -98,7 +97,7 @@ router.patch('/:customFieldDefinitionId', async (req, res) => {
|
|
|
98
97
|
return res.status(200).json(updatedCustomFieldDefinition);
|
|
99
98
|
} catch (err) {
|
|
100
99
|
logger.error('Failed to patch custom field definition', err);
|
|
101
|
-
return handleError(err, res, { message: `Error in update ${ENTITY} request` });
|
|
100
|
+
return handleError(err, res, { logger, message: `Error in update ${ENTITY} request` });
|
|
102
101
|
}
|
|
103
102
|
});
|
|
104
103
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable newline-per-chained-call */
|
|
1
2
|
import Joi from 'joi';
|
|
2
3
|
import { CustomFieldDefinitionType } from '../../../utils/constants';
|
|
3
4
|
|
|
@@ -11,7 +12,6 @@ const statusValidationObject = Joi.object({
|
|
|
11
12
|
value: Joi.string().required(),
|
|
12
13
|
color: Joi.string().required(),
|
|
13
14
|
});
|
|
14
|
-
const statusValidationObjectSchema = Joi.array().items(statusValidationObject).min(1).unique('value');
|
|
15
15
|
/**
|
|
16
16
|
* Schema for the validation of custom field definition
|
|
17
17
|
* The only custom validation is for
|
|
@@ -23,12 +23,12 @@ const statusValidationObjectSchema = Joi.array().items(statusValidationObject).m
|
|
|
23
23
|
*/
|
|
24
24
|
const ValidationSchema = Joi.when('fieldType', {
|
|
25
25
|
is: CustomFieldDefinitionType.SELECT,
|
|
26
|
-
then: Joi.array().items(Joi.string()).min(1).unique(),
|
|
27
|
-
otherwise: Joi.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
then: Joi.array().required().items(Joi.string()).min(1).unique(),
|
|
27
|
+
otherwise: Joi.when('fieldType', {
|
|
28
|
+
is: CustomFieldDefinitionType.STATUS,
|
|
29
|
+
then: Joi.array().required().items(statusValidationObject).min(1).unique('value'),
|
|
30
|
+
otherwise: Joi.forbidden(),
|
|
31
|
+
}),
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
const DefaultValueSchema = Joi.when('fieldType', {
|
|
@@ -57,7 +57,7 @@ const CustomFieldDefinitionCreationSchema = Joi.object({
|
|
|
57
57
|
required: Joi.boolean(),
|
|
58
58
|
disabled: Joi.boolean(),
|
|
59
59
|
blockEditingFromUI: Joi.boolean(),
|
|
60
|
-
});
|
|
60
|
+
}).oxor('required', 'blockEditingFromUI', { isPresent: (value) => value === true });
|
|
61
61
|
|
|
62
62
|
const CustomFieldDefinitionUpdateSchema = Joi.object({
|
|
63
63
|
displayName: Joi.string(),
|
|
@@ -68,10 +68,8 @@ const CustomFieldDefinitionUpdateSchema = Joi.object({
|
|
|
68
68
|
required: Joi.boolean(),
|
|
69
69
|
disabled: Joi.boolean(),
|
|
70
70
|
blockEditingFromUI: Joi.boolean(),
|
|
71
|
-
});
|
|
71
|
+
}).oxor('required', 'blockEditingFromUI', { isPresent: (value) => value === true });
|
|
72
72
|
|
|
73
|
-
export const validateCustomFieldDefinitionCreation = (payload) =>
|
|
74
|
-
CustomFieldDefinitionCreationSchema.validateAsync(payload, { abortEarly: false });
|
|
73
|
+
export const validateCustomFieldDefinitionCreation = (payload) => CustomFieldDefinitionCreationSchema.validateAsync(payload, { abortEarly: false });
|
|
75
74
|
|
|
76
|
-
export const validateCustomFieldDefinitionUpdate = (payload) =>
|
|
77
|
-
CustomFieldDefinitionUpdateSchema.validateAsync(payload, { abortEarly: false });
|
|
75
|
+
export const validateCustomFieldDefinitionUpdate = (payload) => CustomFieldDefinitionUpdateSchema.validateAsync(payload, { abortEarly: false });
|
package/src/api/v1/errors.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Response } from 'express';
|
|
2
|
+
import { handleError, BadRequest, type LogPayload } from '@autofleet/errors';
|
|
2
3
|
import { ValidationError as InputValidationError } from 'joi';
|
|
3
4
|
import { ValidationError as DatabaseValidationError } from 'sequelize';
|
|
4
5
|
|
|
5
|
-
export default (err, res, additionalData = undefined) => {
|
|
6
|
+
export default (err, res: Response, additionalData: LogPayload = undefined) => {
|
|
6
7
|
let error = err;
|
|
7
|
-
if (err instanceof
|
|
8
|
-
error = new BadRequest([err], null);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
if (err instanceof DatabaseValidationError) {
|
|
8
|
+
if ([InputValidationError, DatabaseValidationError].some((ErrClass) => err instanceof ErrClass)) {
|
|
12
9
|
error = new BadRequest([err], null);
|
|
13
10
|
}
|
|
14
11
|
|
package/src/events/index.ts
CHANGED
|
@@ -4,12 +4,7 @@ import {
|
|
|
4
4
|
Model,
|
|
5
5
|
PrimaryKey,
|
|
6
6
|
DataType,
|
|
7
|
-
BeforeCreate,
|
|
8
|
-
BeforeUpsert,
|
|
9
7
|
AfterUpsert,
|
|
10
|
-
BeforeUpdate,
|
|
11
|
-
BeforeBulkCreate,
|
|
12
|
-
BeforeBulkUpdate,
|
|
13
8
|
} from 'sequelize-typescript';
|
|
14
9
|
import { sendDimEvent } from '../events';
|
|
15
10
|
import * as CustomFieldDefinitionRepo from '../repository/definition';
|
|
@@ -17,6 +12,24 @@ import { validateInstanceCustomFieldEntries } from '../utils/validations';
|
|
|
17
12
|
|
|
18
13
|
@Table({
|
|
19
14
|
timestamps: true,
|
|
15
|
+
indexes: [
|
|
16
|
+
{
|
|
17
|
+
name: 'idx_cfe_custom_fields',
|
|
18
|
+
using: 'gin',
|
|
19
|
+
operator: 'jsonb_path_ops',
|
|
20
|
+
fields: ['custom_fields'],
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
validate: {
|
|
24
|
+
async validationByType(this: CustomFieldEntries) {
|
|
25
|
+
if (!Object.keys(this.customFields ?? {}).length) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const definitionsByName = await CustomFieldDefinitionRepo.getCustomFieldDefinitionsDictionary([this]);
|
|
30
|
+
validateInstanceCustomFieldEntries(this, definitionsByName);
|
|
31
|
+
},
|
|
32
|
+
},
|
|
20
33
|
})
|
|
21
34
|
class CustomFieldEntries extends Model {
|
|
22
35
|
@PrimaryKey
|
|
@@ -24,15 +37,23 @@ class CustomFieldEntries extends Model {
|
|
|
24
37
|
type: DataType.UUID,
|
|
25
38
|
allowNull: false,
|
|
26
39
|
})
|
|
27
|
-
/** The ID of the
|
|
40
|
+
/** The ID of the model of which this row hold the custom field entries of, e.g. vehicleId / stopPointId / etc. */
|
|
28
41
|
modelId!: string;
|
|
29
42
|
|
|
43
|
+
@Column({
|
|
44
|
+
type: DataType.UUID,
|
|
45
|
+
allowNull: false,
|
|
46
|
+
})
|
|
47
|
+
/** The ID of the entity of which this row hold the custom field entries of, e.g. fleetId / etc. */
|
|
48
|
+
entityId!: string;
|
|
49
|
+
|
|
30
50
|
@Column({
|
|
31
51
|
type: DataType.JSONB,
|
|
32
|
-
allowNull:
|
|
52
|
+
allowNull: false,
|
|
53
|
+
defaultValue: {},
|
|
33
54
|
})
|
|
34
55
|
/** A dictionary of customFields and values with the following structure: `{ customFieldName: 'CustomFieldValue' }` */
|
|
35
|
-
customFields!: any
|
|
56
|
+
customFields!: Record<string, any>;
|
|
36
57
|
|
|
37
58
|
@Column({
|
|
38
59
|
type: DataType.STRING,
|
|
@@ -47,23 +68,6 @@ class CustomFieldEntries extends Model {
|
|
|
47
68
|
@Column
|
|
48
69
|
updatedAt?: Date;
|
|
49
70
|
|
|
50
|
-
@BeforeBulkCreate
|
|
51
|
-
@BeforeBulkUpdate
|
|
52
|
-
static async validateValues(instances: CustomFieldEntries[]): Promise<void> {
|
|
53
|
-
const definitionsByName = await CustomFieldDefinitionRepo.getCustomFieldDefinitionsDictionary(instances);
|
|
54
|
-
|
|
55
|
-
instances.forEach((instance) => validateInstanceCustomFieldEntries(instance, definitionsByName));
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
@BeforeUpdate
|
|
59
|
-
@BeforeCreate
|
|
60
|
-
@BeforeUpsert
|
|
61
|
-
static async validateValue(instance: CustomFieldEntries): Promise<void> {
|
|
62
|
-
const definitionsByName = await CustomFieldDefinitionRepo.getCustomFieldDefinitionsDictionary([instance]);
|
|
63
|
-
|
|
64
|
-
validateInstanceCustomFieldEntries(instance, definitionsByName);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
71
|
@AfterUpsert
|
|
68
72
|
static afterSaveHandler(instance: CustomFieldEntries, options): void {
|
|
69
73
|
if (options.transaction) {
|
package/src/models/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable no-param-reassign */
|
|
2
|
-
import { DataTypes } from 'sequelize';
|
|
2
|
+
import { DataTypes, Op } from 'sequelize';
|
|
3
3
|
import type { Sequelize } from 'sequelize-typescript';
|
|
4
4
|
import logger from '../utils/logger';
|
|
5
5
|
import CustomFieldDefinition from './CustomFieldDefinition';
|
|
@@ -12,23 +12,32 @@ import type { CustomFieldOptions } from '../types';
|
|
|
12
12
|
import CustomFieldEntries from './CustomFieldEntries';
|
|
13
13
|
|
|
14
14
|
type ProductionModel = typeof CustomFieldDefinition | typeof CustomFieldValue | typeof CustomFieldEntries
|
|
15
|
+
interface InitTablesOptions {
|
|
16
|
+
schemaPrefix?: string
|
|
17
|
+
schemaVersion?: string
|
|
18
|
+
useCustomFieldsEntries?: boolean
|
|
19
|
+
}
|
|
15
20
|
|
|
16
21
|
const productionModels: ProductionModel[] = [CustomFieldDefinition, CustomFieldValue];
|
|
17
22
|
const testModels = [TestModel, AssociatedTestModel, ContextAwareTestModel, ContextTestModel];
|
|
18
23
|
|
|
19
24
|
const SADOT_MIGRATION_PREFIX = 'sadot-migration';
|
|
20
25
|
const SCHEMA_VERSION = 'fb0fa867-1241-4816-b08d-5ed9060c7ae5';
|
|
21
|
-
const CUSTOM_FIELDS_SCHEMA_VERSION = `${SADOT_MIGRATION_PREFIX}_${SCHEMA_VERSION}`;
|
|
22
|
-
const CUSTOM_FIELDS_SCHEMA_VERSION_WITH_ENTRIES = `${CUSTOM_FIELDS_SCHEMA_VERSION}_withEntries`;
|
|
23
26
|
|
|
24
27
|
const initTables = async (
|
|
25
28
|
sequelize: Sequelize,
|
|
26
29
|
getUser: CustomFieldOptions['getUser'],
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
{
|
|
31
|
+
schemaPrefix,
|
|
32
|
+
schemaVersion,
|
|
33
|
+
useCustomFieldsEntries,
|
|
34
|
+
}: InitTablesOptions = {
|
|
35
|
+
schemaPrefix: SADOT_MIGRATION_PREFIX,
|
|
36
|
+
schemaVersion: SCHEMA_VERSION,
|
|
37
|
+
useCustomFieldsEntries: false,
|
|
29
38
|
},
|
|
30
39
|
): Promise<void> => {
|
|
31
|
-
const
|
|
40
|
+
const CUSTOM_FIELDS_SCHEMA_VERSION = `${schemaPrefix}_${schemaVersion}${useCustomFieldsEntries ? '_withEntries' : ''}`;
|
|
32
41
|
logger.info('custom-fields: initialize custom-fields tables');
|
|
33
42
|
// Detect models and import them to the orm
|
|
34
43
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
@@ -36,7 +45,6 @@ const initTables = async (
|
|
|
36
45
|
throw new Error('sequelize instance must have addModels function');
|
|
37
46
|
}
|
|
38
47
|
|
|
39
|
-
const schemaVersion = useCustomFieldsEntries ? CUSTOM_FIELDS_SCHEMA_VERSION_WITH_ENTRIES : CUSTOM_FIELDS_SCHEMA_VERSION;
|
|
40
48
|
if (useCustomFieldsEntries) {
|
|
41
49
|
productionModels.push(CustomFieldEntries);
|
|
42
50
|
}
|
|
@@ -78,22 +86,29 @@ const initTables = async (
|
|
|
78
86
|
schema: 'public',
|
|
79
87
|
},
|
|
80
88
|
);
|
|
81
|
-
const migrations = await SequelizeMeta.findAll({ raw: true });
|
|
82
|
-
const currentSadotSchemaVersion = migrations.reverse().find((m: any) => m.name.includes(SADOT_MIGRATION_PREFIX));
|
|
83
89
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
const migrations = await SequelizeMeta.findAll({ where: { name: { [Op.like]: `${schemaPrefix}%` } }, raw: true });
|
|
91
|
+
const currentSadotSchemaVersion = migrations.at(-1);
|
|
92
|
+
const expectedSchemaVersionIndex = migrations.findIndex((m) => (m as any).name === CUSTOM_FIELDS_SCHEMA_VERSION);
|
|
93
|
+
|
|
94
|
+
if (!currentSadotSchemaVersion || (currentSadotSchemaVersion as any).name !== CUSTOM_FIELDS_SCHEMA_VERSION) {
|
|
88
95
|
await CustomFieldDefinition.sync({ alter: true });
|
|
89
96
|
await CustomFieldValue.sync({ alter: true });
|
|
90
|
-
|
|
97
|
+
// T.Y TODO: Remove the if statement once we're ready to add the new entries table for all MS
|
|
91
98
|
if (useCustomFieldsEntries) {
|
|
92
99
|
await CustomFieldEntries.sync({ alter: true });
|
|
93
100
|
}
|
|
94
101
|
|
|
95
|
-
|
|
102
|
+
if (expectedSchemaVersionIndex === -1) {
|
|
103
|
+
await SequelizeMeta.create({ name: CUSTOM_FIELDS_SCHEMA_VERSION });
|
|
104
|
+
}
|
|
96
105
|
logger.info('custom-fields: models synced');
|
|
106
|
+
if (migrations.length && expectedSchemaVersionIndex !== -1 && expectedSchemaVersionIndex < migrations.length - 1) {
|
|
107
|
+
// We have existing migrations, and we are calling `sync`.
|
|
108
|
+
// This means we are in a `down` migration, and hence we should delete newer migrations to ensure we can reapply them.
|
|
109
|
+
const migrationsToDelete = migrations.slice(expectedSchemaVersionIndex + 1);
|
|
110
|
+
await SequelizeMeta.destroy({ where: { name: { [Op.in]: migrationsToDelete.map((m) => (m as any).name) } } });
|
|
111
|
+
}
|
|
97
112
|
}
|
|
98
113
|
};
|
|
99
114
|
|
|
@@ -120,6 +135,7 @@ const initTestModels = async (sequelize: Sequelize): Promise<void> => {
|
|
|
120
135
|
export {
|
|
121
136
|
CustomFieldValue,
|
|
122
137
|
CustomFieldDefinition,
|
|
138
|
+
CustomFieldEntries,
|
|
123
139
|
TestModel,
|
|
124
140
|
AssociatedTestModel,
|
|
125
141
|
ContextAwareTestModel,
|
|
@@ -2,11 +2,10 @@ import {
|
|
|
2
2
|
Op,
|
|
3
3
|
type Includeable, type Transaction, type FindOptions, type WhereOptions,
|
|
4
4
|
} from 'sequelize';
|
|
5
|
-
import { CustomFieldDefinition } from '../models';
|
|
5
|
+
import { CustomFieldDefinition, type CustomFieldEntries } from '../models';
|
|
6
6
|
import type { CreateCustomFieldDefinition, UpdateCustomFieldDefinition } from '../types/definition';
|
|
7
7
|
import type { ModelOptions } from '../types';
|
|
8
8
|
import { MissingDefinitionError } from '../errors';
|
|
9
|
-
import type { CustomFieldEntriesDTO } from '../types/entries';
|
|
10
9
|
|
|
11
10
|
export const create = (data: CreateCustomFieldDefinition): Promise<CustomFieldDefinition> =>
|
|
12
11
|
CustomFieldDefinition.create(data);
|
|
@@ -134,33 +133,29 @@ export const getRequiredFields = async (
|
|
|
134
133
|
return [...new Set(requiredFieldsNames)];
|
|
135
134
|
};
|
|
136
135
|
|
|
136
|
+
/**
|
|
137
|
+
* @returns A promise resolving with a dictionary of custom field definitions by name.
|
|
138
|
+
* @throws A {@link MissingDefinitionError} if any of the custom fields doesn't have a definition.
|
|
139
|
+
*/
|
|
137
140
|
export const getCustomFieldDefinitionsDictionary = async (
|
|
138
|
-
instances:
|
|
141
|
+
instances: CustomFieldEntries[],
|
|
139
142
|
options: SadotGetDefinitionsByEntityIdsOptions = { withDisabled: false, modelOptions: {} },
|
|
140
143
|
): Promise<{ [definitionName: string]: CustomFieldDefinition }> => {
|
|
141
|
-
const { modelType } = instances[0];
|
|
144
|
+
const { modelType } = instances[0]?.dataValues ?? {};
|
|
142
145
|
const customFields = new Set<string>();
|
|
143
146
|
const modelIds = [];
|
|
147
|
+
const entityIds = new Set<string>();
|
|
144
148
|
instances.forEach((instance) => {
|
|
145
|
-
|
|
149
|
+
const { dataValues: { modelId, entityId, customFields: instanceCustomFields } } = instance;
|
|
150
|
+
modelIds.push(modelId);
|
|
151
|
+
entityIds.add(entityId);
|
|
146
152
|
|
|
147
|
-
Object.keys(
|
|
153
|
+
Object.keys(instanceCustomFields ?? {}).forEach((fieldName) => {
|
|
148
154
|
customFields.add(fieldName);
|
|
149
155
|
});
|
|
150
156
|
});
|
|
151
157
|
|
|
152
|
-
const
|
|
153
|
-
const where: WhereOptions = {
|
|
154
|
-
modelType,
|
|
155
|
-
...(!useEntityIdFromInclude && { entityId: modelIds }),
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
const definitions = await findAll({
|
|
159
|
-
where,
|
|
160
|
-
transaction: options.transaction,
|
|
161
|
-
include: include?.(modelIds),
|
|
162
|
-
raw: true,
|
|
163
|
-
}, options);
|
|
158
|
+
const definitions = await findByEntityIds(modelType, Array.from(entityIds), { ...options });
|
|
164
159
|
|
|
165
160
|
const matchedDefinitions = definitions.filter((def) => customFields.has(def.name));
|
|
166
161
|
const matchedDefinitionsByName = Object.fromEntries(matchedDefinitions.map((definition) => [definition.name, definition]));
|
package/src/scopes/filter.ts
CHANGED
|
@@ -44,9 +44,6 @@ const castIfNeeded = (columnName: string, conditionValue: string): string => {
|
|
|
44
44
|
if (isDate(conditionValue)) {
|
|
45
45
|
return castValueToJsonb(columnName, 'timestamp');
|
|
46
46
|
}
|
|
47
|
-
if (!Number.isNaN(Number(conditionValue))) {
|
|
48
|
-
return castValueToJsonbNumeric(columnName);
|
|
49
|
-
}
|
|
50
47
|
return columnName;
|
|
51
48
|
};
|
|
52
49
|
const AND_DELIMITER = ' AND ';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomUUID
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import type { CreateCustomFieldDefinition, CustomFieldDefinitionDTO } from '../../types/definition';
|
|
3
3
|
|
|
4
4
|
export const contextAwareFieldDefinition = {
|
|
@@ -12,7 +12,7 @@ export const coolFieldDefinition: CreateCustomFieldDefinition = {
|
|
|
12
12
|
name: 'cool field',
|
|
13
13
|
modelType: 'TestModel',
|
|
14
14
|
fieldType: 'number',
|
|
15
|
-
entityId:
|
|
15
|
+
entityId: randomUUID(),
|
|
16
16
|
entityType: 'fleetId',
|
|
17
17
|
};
|
|
18
18
|
|
|
@@ -30,7 +30,7 @@ export const booleanField = (modelType: string): CreateCustomFieldDefinition =>
|
|
|
30
30
|
name: 'shapeless',
|
|
31
31
|
modelType,
|
|
32
32
|
fieldType: 'boolean',
|
|
33
|
-
entityId:
|
|
33
|
+
entityId: randomUUID(),
|
|
34
34
|
entityType: 'fleetId',
|
|
35
35
|
});
|
|
36
36
|
|
|
@@ -39,7 +39,7 @@ export const selectField = (modelType: string, options): CreateCustomFieldDefini
|
|
|
39
39
|
modelType,
|
|
40
40
|
fieldType: 'select',
|
|
41
41
|
validation: options,
|
|
42
|
-
entityId:
|
|
42
|
+
entityId: randomUUID(),
|
|
43
43
|
entityType: 'fleetId',
|
|
44
44
|
});
|
|
45
45
|
|
|
@@ -48,7 +48,7 @@ export const statusField = (modelType: string, options): CreateCustomFieldDefini
|
|
|
48
48
|
modelType,
|
|
49
49
|
fieldType: 'status',
|
|
50
50
|
validation: options,
|
|
51
|
-
entityId:
|
|
51
|
+
entityId: randomUUID(),
|
|
52
52
|
entityType: 'fleetId',
|
|
53
53
|
});
|
|
54
54
|
|
|
@@ -56,17 +56,18 @@ export const fileField = (modelType: string): CreateCustomFieldDefinition => ({
|
|
|
56
56
|
name: 'file',
|
|
57
57
|
modelType,
|
|
58
58
|
fieldType: 'file',
|
|
59
|
-
entityId:
|
|
59
|
+
entityId: randomUUID(),
|
|
60
60
|
entityType: 'fleetId',
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
// eslint-disable-next-line max-len
|
|
64
64
|
export const createDefinition = (defaults: Partial<CustomFieldDefinitionDTO>): CreateCustomFieldDefinition => ({
|
|
65
|
-
name: defaults?.name || `def_${
|
|
65
|
+
name: defaults?.name || `def_${randomUUID()}`,
|
|
66
66
|
modelType: defaults?.modelType || 'TestModel',
|
|
67
67
|
fieldType: defaults?.fieldType || 'boolean',
|
|
68
|
-
entityId: defaults?.entityId ||
|
|
68
|
+
entityId: defaults?.entityId || randomUUID(),
|
|
69
69
|
entityType: defaults?.entityType || 'fleetId',
|
|
70
|
+
...(defaults?.validation && { validation: defaults.validation }),
|
|
70
71
|
...(defaults?.defaultValue && { defaultValue: defaults.defaultValue }),
|
|
71
72
|
});
|
|
72
73
|
|
|
@@ -75,9 +76,9 @@ export const createDefinitions = (
|
|
|
75
76
|
length = 1,
|
|
76
77
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
77
78
|
): CreateCustomFieldDefinition[] => (Array(length).fill({}).map((_) => ({
|
|
78
|
-
name: defaults?.name || `def_${
|
|
79
|
+
name: defaults?.name || `def_${randomUUID()}`,
|
|
79
80
|
modelType: defaults?.modelType || 'TestModel',
|
|
80
81
|
fieldType: defaults?.fieldType || 'boolean',
|
|
81
|
-
entityId: defaults?.entityId ||
|
|
82
|
+
entityId: defaults?.entityId || randomUUID(),
|
|
82
83
|
entityType: defaults?.entityType || 'fleetId',
|
|
83
84
|
})));
|
|
@@ -16,3 +16,6 @@ export const mockDimCustomFieldDefinitionEvent = (events, numberOfEvents) =>
|
|
|
16
16
|
|
|
17
17
|
export const mockDimCustomFieldValueEvent = (events, numberOfEvents) =>
|
|
18
18
|
mockEvent(events, 'dim_custom_field_value', numberOfEvents);
|
|
19
|
+
|
|
20
|
+
export const mockDimCustomFieldEntriesEvent = (events, numberOfEvents) =>
|
|
21
|
+
mockEvent(events, 'dim_custom_field_entries', numberOfEvents);
|
|
@@ -2,10 +2,20 @@ import type { ValidationError } from 'joi';
|
|
|
2
2
|
|
|
3
3
|
export interface CustomFieldEntriesDTO {
|
|
4
4
|
modelId: string;
|
|
5
|
+
entityId: string;
|
|
5
6
|
modelType: string;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* A collection of custom fields where each key is the name of a CustomFieldDefinition and
|
|
9
|
+
* each value is the value of that custom field for this specific `modelId`.
|
|
10
|
+
*
|
|
11
|
+
* Example:
|
|
12
|
+
* {
|
|
13
|
+
* "vehicleColor": "Red",
|
|
14
|
+
* "vehicleType": "premium",
|
|
15
|
+
* "isActive": true
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
customFields: Record<string, any>;
|
|
9
19
|
createdAt?: Date;
|
|
10
20
|
updatedAt?: Date;
|
|
11
21
|
}
|
package/.env
DELETED