@autofleet/sadot 0.9.0 → 0.9.1-beta-8053a0cc.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/.env +4 -0
- package/dist/events/index.js +0 -4
- package/dist/models/CustomFieldEntries.d.ts +4 -4
- package/dist/models/CustomFieldEntries.js +25 -29
- package/dist/models/index.d.ts +4 -8
- package/dist/models/index.js +11 -22
- package/dist/repository/definition.d.ts +3 -6
- package/dist/repository/definition.js +14 -11
- package/dist/scopes/filter.js +3 -0
- package/dist/tests/mocks/definition.mock.js +0 -1
- package/dist/tests/mocks/events.mock.d.ts +0 -1
- package/dist/tests/mocks/events.mock.js +1 -3
- package/dist/types/entries/index.d.ts +3 -13
- package/package.json +1 -1
- package/src/events/index.ts +0 -4
- package/src/models/CustomFieldEntries.ts +25 -29
- package/src/models/index.ts +15 -31
- package/src/repository/definition.ts +18 -13
- package/src/scopes/filter.ts +3 -0
- package/src/tests/mocks/definition.mock.ts +10 -11
- package/src/tests/mocks/events.mock.ts +0 -3
- package/src/types/entries/index.ts +3 -13
package/.env
ADDED
package/dist/events/index.js
CHANGED
|
@@ -27,10 +27,6 @@ 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
|
-
},
|
|
34
30
|
};
|
|
35
31
|
const sendDimEvent = (instance) => {
|
|
36
32
|
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 entity of which this row hold the custom field entries of, e.g. Vehicle / StopPoint / 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;
|
|
7
5
|
/** A dictionary of customFields and values with the following structure: `{ customFieldName: 'CustomFieldValue' }` */
|
|
8
|
-
customFields:
|
|
6
|
+
customFields: any;
|
|
9
7
|
/** The type of model which this custom field entry represents. e.g. Vehicle / StopPoint / etc. */
|
|
10
8
|
modelType: string;
|
|
11
9
|
createdAt?: Date;
|
|
12
10
|
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,6 +37,14 @@ 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
|
+
}
|
|
40
48
|
static afterSaveHandler(instance, options) {
|
|
41
49
|
if (options.transaction) {
|
|
42
50
|
options.transaction.afterCommit(() => (0, events_1.sendDimEvent)(instance[0]));
|
|
@@ -52,24 +60,14 @@ __decorate([
|
|
|
52
60
|
type: sequelize_typescript_1.DataType.UUID,
|
|
53
61
|
allowNull: false,
|
|
54
62
|
})
|
|
55
|
-
/** The ID of the
|
|
63
|
+
/** The ID of the entity of which this row hold the custom field entries of, e.g. Vehicle / StopPoint / etc. */
|
|
56
64
|
,
|
|
57
65
|
__metadata("design:type", String)
|
|
58
66
|
], 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);
|
|
68
67
|
__decorate([
|
|
69
68
|
(0, sequelize_typescript_1.Column)({
|
|
70
69
|
type: sequelize_typescript_1.DataType.JSONB,
|
|
71
|
-
allowNull:
|
|
72
|
-
defaultValue: {},
|
|
70
|
+
allowNull: true,
|
|
73
71
|
})
|
|
74
72
|
/** A dictionary of customFields and values with the following structure: `{ customFieldName: 'CustomFieldValue' }` */
|
|
75
73
|
,
|
|
@@ -92,6 +90,21 @@ __decorate([
|
|
|
92
90
|
sequelize_typescript_1.Column,
|
|
93
91
|
__metadata("design:type", Date)
|
|
94
92
|
], 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);
|
|
95
108
|
__decorate([
|
|
96
109
|
sequelize_typescript_1.AfterUpsert,
|
|
97
110
|
__metadata("design:type", Function),
|
|
@@ -101,23 +114,6 @@ __decorate([
|
|
|
101
114
|
CustomFieldEntries = __decorate([
|
|
102
115
|
(0, sequelize_typescript_1.Table)({
|
|
103
116
|
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
|
-
},
|
|
121
117
|
})
|
|
122
118
|
], CustomFieldEntries);
|
|
123
119
|
exports.default = CustomFieldEntries;
|
package/dist/models/index.d.ts
CHANGED
|
@@ -6,12 +6,8 @@ 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
|
-
|
|
12
|
-
schemaVersion?: string;
|
|
13
|
-
useCustomFieldsEntries?: boolean;
|
|
14
|
-
}
|
|
15
|
-
declare const initTables: (sequelize: Sequelize, getUser: CustomFieldOptions['getUser'], { schemaPrefix, schemaVersion, useCustomFieldsEntries, }?: InitTablesOptions) => Promise<void>;
|
|
9
|
+
declare const initTables: (sequelize: Sequelize, getUser: CustomFieldOptions['getUser'], options: {
|
|
10
|
+
useCustomFieldsEntries: boolean;
|
|
11
|
+
}) => Promise<void>;
|
|
16
12
|
declare const initTestModels: (sequelize: Sequelize) => Promise<void>;
|
|
17
|
-
export { CustomFieldValue, CustomFieldDefinition,
|
|
13
|
+
export { CustomFieldValue, CustomFieldDefinition, 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.
|
|
6
|
+
exports.initTestModels = exports.initTables = exports.ContextTestModel = exports.ContextAwareTestModel = exports.AssociatedTestModel = exports.TestModel = 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,23 +20,21 @@ 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;
|
|
24
23
|
const productionModels = [CustomFieldDefinition_1.default, CustomFieldValue_1.default];
|
|
25
24
|
const testModels = [TestModel_1.default, AssociatedTestModel_1.default, ContextAwareTestModel_1.default, ContextTestModel_1.default];
|
|
26
25
|
const SADOT_MIGRATION_PREFIX = 'sadot-migration';
|
|
27
26
|
const SCHEMA_VERSION = 'fb0fa867-1241-4816-b08d-5ed9060c7ae5';
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
useCustomFieldsEntries
|
|
32
|
-
}) => {
|
|
33
|
-
const CUSTOM_FIELDS_SCHEMA_VERSION = `${schemaPrefix}_${schemaVersion}${useCustomFieldsEntries ? '_withEntries' : ''}`;
|
|
27
|
+
const CUSTOM_FIELDS_SCHEMA_VERSION = `${SADOT_MIGRATION_PREFIX}_${SCHEMA_VERSION}`;
|
|
28
|
+
const CUSTOM_FIELDS_SCHEMA_VERSION_WITH_ENTRIES = `${CUSTOM_FIELDS_SCHEMA_VERSION}_withEntries`;
|
|
29
|
+
const initTables = async (sequelize, getUser, options) => {
|
|
30
|
+
const { useCustomFieldsEntries } = options;
|
|
34
31
|
logger_1.default.info('custom-fields: initialize custom-fields tables');
|
|
35
32
|
// Detect models and import them to the orm
|
|
36
33
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
37
34
|
if (!sequelize.addModels) {
|
|
38
35
|
throw new Error('sequelize instance must have addModels function');
|
|
39
36
|
}
|
|
37
|
+
const schemaVersion = useCustomFieldsEntries ? CUSTOM_FIELDS_SCHEMA_VERSION_WITH_ENTRIES : CUSTOM_FIELDS_SCHEMA_VERSION;
|
|
40
38
|
if (useCustomFieldsEntries) {
|
|
41
39
|
productionModels.push(CustomFieldEntries_1.default);
|
|
42
40
|
}
|
|
@@ -70,26 +68,17 @@ const initTables = async (sequelize, getUser, { schemaPrefix, schemaVersion, use
|
|
|
70
68
|
timestamps: false,
|
|
71
69
|
schema: 'public',
|
|
72
70
|
});
|
|
73
|
-
const migrations = await SequelizeMeta.findAll({
|
|
74
|
-
const currentSadotSchemaVersion = migrations.
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
const migrations = await SequelizeMeta.findAll({ raw: true });
|
|
72
|
+
const currentSadotSchemaVersion = migrations.reverse().find((m) => m.name.includes(SADOT_MIGRATION_PREFIX));
|
|
73
|
+
if (!currentSadotSchemaVersion
|
|
74
|
+
|| currentSadotSchemaVersion.name !== schemaVersion) {
|
|
77
75
|
await CustomFieldDefinition_1.default.sync({ alter: true });
|
|
78
76
|
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
|
|
80
77
|
if (useCustomFieldsEntries) {
|
|
81
78
|
await CustomFieldEntries_1.default.sync({ alter: true });
|
|
82
79
|
}
|
|
83
|
-
|
|
84
|
-
await SequelizeMeta.create({ name: CUSTOM_FIELDS_SCHEMA_VERSION });
|
|
85
|
-
}
|
|
80
|
+
await SequelizeMeta.create({ name: schemaVersion });
|
|
86
81
|
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
|
-
}
|
|
93
82
|
}
|
|
94
83
|
};
|
|
95
84
|
exports.initTables = initTables;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type Includeable, type Transaction, type FindOptions, type WhereOptions } from 'sequelize';
|
|
2
|
-
import { CustomFieldDefinition
|
|
2
|
+
import { CustomFieldDefinition } 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';
|
|
5
6
|
export declare const create: (data: CreateCustomFieldDefinition) => Promise<CustomFieldDefinition>;
|
|
6
7
|
interface SadotFindOptions {
|
|
7
8
|
withDisabled?: boolean;
|
|
@@ -26,11 +27,7 @@ export declare const destroy: (id: string) => Promise<number>;
|
|
|
26
27
|
* Return the names of the required fields for a given model
|
|
27
28
|
*/
|
|
28
29
|
export declare const getRequiredFields: (modelType: string, modelId: string | string[], entityId: string | string[], modelOptions?: ModelOptions) => Promise<string[]>;
|
|
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<{
|
|
30
|
+
export declare const getCustomFieldDefinitionsDictionary: (instances: CustomFieldEntriesDTO[], options?: SadotGetDefinitionsByEntityIdsOptions) => Promise<{
|
|
34
31
|
[definitionName: string]: CustomFieldDefinition;
|
|
35
32
|
}>;
|
|
36
33
|
export {};
|
|
@@ -87,24 +87,27 @@ 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
|
-
*/
|
|
94
90
|
const getCustomFieldDefinitionsDictionary = async (instances, options = { withDisabled: false, modelOptions: {} }) => {
|
|
95
|
-
const { modelType } = instances[0]
|
|
91
|
+
const { modelType } = instances[0];
|
|
96
92
|
const customFields = new Set();
|
|
97
93
|
const modelIds = [];
|
|
98
|
-
const entityIds = new Set();
|
|
99
94
|
instances.forEach((instance) => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
entityIds.add(entityId);
|
|
103
|
-
Object.keys(instanceCustomFields ?? {}).forEach((fieldName) => {
|
|
95
|
+
modelIds.push(instance.modelId);
|
|
96
|
+
Object.keys(instance.customFields).forEach((fieldName) => {
|
|
104
97
|
customFields.add(fieldName);
|
|
105
98
|
});
|
|
106
99
|
});
|
|
107
|
-
const
|
|
100
|
+
const { include, useEntityIdFromInclude } = options.modelOptions;
|
|
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);
|
|
108
111
|
const matchedDefinitions = definitions.filter((def) => customFields.has(def.name));
|
|
109
112
|
const matchedDefinitionsByName = Object.fromEntries(matchedDefinitions.map((definition) => [definition.name, definition]));
|
|
110
113
|
if (!definitions?.length || matchedDefinitions.length !== customFields.size) {
|
package/dist/scopes/filter.js
CHANGED
|
@@ -18,6 +18,9 @@ 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
|
+
}
|
|
21
24
|
return columnName;
|
|
22
25
|
};
|
|
23
26
|
const AND_DELIMITER = ' AND ';
|
|
@@ -64,7 +64,6 @@ 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 }),
|
|
68
67
|
...(defaults?.defaultValue && { defaultValue: defaults.defaultValue }),
|
|
69
68
|
});
|
|
70
69
|
exports.createDefinition = createDefinition;
|
|
@@ -1,4 +1,3 @@
|
|
|
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.
|
|
4
|
+
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,5 +17,3 @@ 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,20 +1,10 @@
|
|
|
1
1
|
import type { ValidationError } from 'joi';
|
|
2
2
|
export interface CustomFieldEntriesDTO {
|
|
3
3
|
modelId: string;
|
|
4
|
-
entityId: string;
|
|
5
4
|
modelType: string;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*
|
|
10
|
-
* Example:
|
|
11
|
-
* {
|
|
12
|
-
* "vehicleColor": "Red",
|
|
13
|
-
* "vehicleType": "premium",
|
|
14
|
-
* "isActive": true
|
|
15
|
-
* }
|
|
16
|
-
*/
|
|
17
|
-
customFields: Record<string, any>;
|
|
5
|
+
customFields: {
|
|
6
|
+
[customFieldName: string]: any;
|
|
7
|
+
};
|
|
18
8
|
createdAt?: Date;
|
|
19
9
|
updatedAt?: Date;
|
|
20
10
|
}
|
package/package.json
CHANGED
package/src/events/index.ts
CHANGED
|
@@ -4,7 +4,12 @@ import {
|
|
|
4
4
|
Model,
|
|
5
5
|
PrimaryKey,
|
|
6
6
|
DataType,
|
|
7
|
+
BeforeCreate,
|
|
8
|
+
BeforeUpsert,
|
|
7
9
|
AfterUpsert,
|
|
10
|
+
BeforeUpdate,
|
|
11
|
+
BeforeBulkCreate,
|
|
12
|
+
BeforeBulkUpdate,
|
|
8
13
|
} from 'sequelize-typescript';
|
|
9
14
|
import { sendDimEvent } from '../events';
|
|
10
15
|
import * as CustomFieldDefinitionRepo from '../repository/definition';
|
|
@@ -12,24 +17,6 @@ import { validateInstanceCustomFieldEntries } from '../utils/validations';
|
|
|
12
17
|
|
|
13
18
|
@Table({
|
|
14
19
|
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
|
-
},
|
|
33
20
|
})
|
|
34
21
|
class CustomFieldEntries extends Model {
|
|
35
22
|
@PrimaryKey
|
|
@@ -37,23 +24,15 @@ class CustomFieldEntries extends Model {
|
|
|
37
24
|
type: DataType.UUID,
|
|
38
25
|
allowNull: false,
|
|
39
26
|
})
|
|
40
|
-
/** The ID of the
|
|
27
|
+
/** The ID of the entity of which this row hold the custom field entries of, e.g. Vehicle / StopPoint / etc. */
|
|
41
28
|
modelId!: string;
|
|
42
29
|
|
|
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
|
-
|
|
50
30
|
@Column({
|
|
51
31
|
type: DataType.JSONB,
|
|
52
|
-
allowNull:
|
|
53
|
-
defaultValue: {},
|
|
32
|
+
allowNull: true,
|
|
54
33
|
})
|
|
55
34
|
/** A dictionary of customFields and values with the following structure: `{ customFieldName: 'CustomFieldValue' }` */
|
|
56
|
-
customFields!:
|
|
35
|
+
customFields!: any;
|
|
57
36
|
|
|
58
37
|
@Column({
|
|
59
38
|
type: DataType.STRING,
|
|
@@ -68,6 +47,23 @@ class CustomFieldEntries extends Model {
|
|
|
68
47
|
@Column
|
|
69
48
|
updatedAt?: Date;
|
|
70
49
|
|
|
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
|
+
|
|
71
67
|
@AfterUpsert
|
|
72
68
|
static afterSaveHandler(instance: CustomFieldEntries, options): void {
|
|
73
69
|
if (options.transaction) {
|
package/src/models/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable no-param-reassign */
|
|
2
|
-
import { DataTypes
|
|
2
|
+
import { DataTypes } from 'sequelize';
|
|
3
3
|
import type { Sequelize } from 'sequelize-typescript';
|
|
4
4
|
import logger from '../utils/logger';
|
|
5
5
|
import CustomFieldDefinition from './CustomFieldDefinition';
|
|
@@ -12,32 +12,23 @@ 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
|
-
}
|
|
20
15
|
|
|
21
16
|
const productionModels: ProductionModel[] = [CustomFieldDefinition, CustomFieldValue];
|
|
22
17
|
const testModels = [TestModel, AssociatedTestModel, ContextAwareTestModel, ContextTestModel];
|
|
23
18
|
|
|
24
19
|
const SADOT_MIGRATION_PREFIX = 'sadot-migration';
|
|
25
20
|
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`;
|
|
26
23
|
|
|
27
24
|
const initTables = async (
|
|
28
25
|
sequelize: Sequelize,
|
|
29
26
|
getUser: CustomFieldOptions['getUser'],
|
|
30
|
-
{
|
|
31
|
-
|
|
32
|
-
schemaVersion,
|
|
33
|
-
useCustomFieldsEntries,
|
|
34
|
-
}: InitTablesOptions = {
|
|
35
|
-
schemaPrefix: SADOT_MIGRATION_PREFIX,
|
|
36
|
-
schemaVersion: SCHEMA_VERSION,
|
|
37
|
-
useCustomFieldsEntries: false,
|
|
27
|
+
options: {
|
|
28
|
+
useCustomFieldsEntries: boolean
|
|
38
29
|
},
|
|
39
30
|
): Promise<void> => {
|
|
40
|
-
const
|
|
31
|
+
const { useCustomFieldsEntries } = options;
|
|
41
32
|
logger.info('custom-fields: initialize custom-fields tables');
|
|
42
33
|
// Detect models and import them to the orm
|
|
43
34
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
@@ -45,6 +36,7 @@ const initTables = async (
|
|
|
45
36
|
throw new Error('sequelize instance must have addModels function');
|
|
46
37
|
}
|
|
47
38
|
|
|
39
|
+
const schemaVersion = useCustomFieldsEntries ? CUSTOM_FIELDS_SCHEMA_VERSION_WITH_ENTRIES : CUSTOM_FIELDS_SCHEMA_VERSION;
|
|
48
40
|
if (useCustomFieldsEntries) {
|
|
49
41
|
productionModels.push(CustomFieldEntries);
|
|
50
42
|
}
|
|
@@ -86,29 +78,22 @@ const initTables = async (
|
|
|
86
78
|
schema: 'public',
|
|
87
79
|
},
|
|
88
80
|
);
|
|
81
|
+
const migrations = await SequelizeMeta.findAll({ raw: true });
|
|
82
|
+
const currentSadotSchemaVersion = migrations.reverse().find((m: any) => m.name.includes(SADOT_MIGRATION_PREFIX));
|
|
89
83
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if (!currentSadotSchemaVersion || (currentSadotSchemaVersion as any).name !== CUSTOM_FIELDS_SCHEMA_VERSION) {
|
|
84
|
+
if (
|
|
85
|
+
!currentSadotSchemaVersion
|
|
86
|
+
|| (currentSadotSchemaVersion as any).name !== schemaVersion
|
|
87
|
+
) {
|
|
95
88
|
await CustomFieldDefinition.sync({ alter: true });
|
|
96
89
|
await CustomFieldValue.sync({ alter: true });
|
|
97
|
-
|
|
90
|
+
|
|
98
91
|
if (useCustomFieldsEntries) {
|
|
99
92
|
await CustomFieldEntries.sync({ alter: true });
|
|
100
93
|
}
|
|
101
94
|
|
|
102
|
-
|
|
103
|
-
await SequelizeMeta.create({ name: CUSTOM_FIELDS_SCHEMA_VERSION });
|
|
104
|
-
}
|
|
95
|
+
await SequelizeMeta.create({ name: schemaVersion });
|
|
105
96
|
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
|
-
}
|
|
112
97
|
}
|
|
113
98
|
};
|
|
114
99
|
|
|
@@ -135,7 +120,6 @@ const initTestModels = async (sequelize: Sequelize): Promise<void> => {
|
|
|
135
120
|
export {
|
|
136
121
|
CustomFieldValue,
|
|
137
122
|
CustomFieldDefinition,
|
|
138
|
-
CustomFieldEntries,
|
|
139
123
|
TestModel,
|
|
140
124
|
AssociatedTestModel,
|
|
141
125
|
ContextAwareTestModel,
|
|
@@ -2,10 +2,11 @@ import {
|
|
|
2
2
|
Op,
|
|
3
3
|
type Includeable, type Transaction, type FindOptions, type WhereOptions,
|
|
4
4
|
} from 'sequelize';
|
|
5
|
-
import { CustomFieldDefinition
|
|
5
|
+
import { CustomFieldDefinition } 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';
|
|
9
10
|
|
|
10
11
|
export const create = (data: CreateCustomFieldDefinition): Promise<CustomFieldDefinition> =>
|
|
11
12
|
CustomFieldDefinition.create(data);
|
|
@@ -133,29 +134,33 @@ export const getRequiredFields = async (
|
|
|
133
134
|
return [...new Set(requiredFieldsNames)];
|
|
134
135
|
};
|
|
135
136
|
|
|
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
|
-
*/
|
|
140
137
|
export const getCustomFieldDefinitionsDictionary = async (
|
|
141
|
-
instances:
|
|
138
|
+
instances: CustomFieldEntriesDTO[],
|
|
142
139
|
options: SadotGetDefinitionsByEntityIdsOptions = { withDisabled: false, modelOptions: {} },
|
|
143
140
|
): Promise<{ [definitionName: string]: CustomFieldDefinition }> => {
|
|
144
|
-
const { modelType } = instances[0]
|
|
141
|
+
const { modelType } = instances[0];
|
|
145
142
|
const customFields = new Set<string>();
|
|
146
143
|
const modelIds = [];
|
|
147
|
-
const entityIds = new Set<string>();
|
|
148
144
|
instances.forEach((instance) => {
|
|
149
|
-
|
|
150
|
-
modelIds.push(modelId);
|
|
151
|
-
entityIds.add(entityId);
|
|
145
|
+
modelIds.push(instance.modelId);
|
|
152
146
|
|
|
153
|
-
Object.keys(
|
|
147
|
+
Object.keys(instance.customFields).forEach((fieldName) => {
|
|
154
148
|
customFields.add(fieldName);
|
|
155
149
|
});
|
|
156
150
|
});
|
|
157
151
|
|
|
158
|
-
const
|
|
152
|
+
const { include, useEntityIdFromInclude } = options.modelOptions;
|
|
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);
|
|
159
164
|
|
|
160
165
|
const matchedDefinitions = definitions.filter((def) => customFields.has(def.name));
|
|
161
166
|
const matchedDefinitionsByName = Object.fromEntries(matchedDefinitions.map((definition) => [definition.name, definition]));
|
package/src/scopes/filter.ts
CHANGED
|
@@ -44,6 +44,9 @@ 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
|
+
}
|
|
47
50
|
return columnName;
|
|
48
51
|
};
|
|
49
52
|
const AND_DELIMITER = ' AND ';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
1
|
+
import { randomUUID as uuidv4 } 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: uuidv4(),
|
|
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: uuidv4(),
|
|
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: uuidv4(),
|
|
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: uuidv4(),
|
|
52
52
|
entityType: 'fleetId',
|
|
53
53
|
});
|
|
54
54
|
|
|
@@ -56,18 +56,17 @@ export const fileField = (modelType: string): CreateCustomFieldDefinition => ({
|
|
|
56
56
|
name: 'file',
|
|
57
57
|
modelType,
|
|
58
58
|
fieldType: 'file',
|
|
59
|
-
entityId:
|
|
59
|
+
entityId: uuidv4(),
|
|
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_${uuidv4()}`,
|
|
66
66
|
modelType: defaults?.modelType || 'TestModel',
|
|
67
67
|
fieldType: defaults?.fieldType || 'boolean',
|
|
68
|
-
entityId: defaults?.entityId ||
|
|
68
|
+
entityId: defaults?.entityId || uuidv4(),
|
|
69
69
|
entityType: defaults?.entityType || 'fleetId',
|
|
70
|
-
...(defaults?.validation && { validation: defaults.validation }),
|
|
71
70
|
...(defaults?.defaultValue && { defaultValue: defaults.defaultValue }),
|
|
72
71
|
});
|
|
73
72
|
|
|
@@ -76,9 +75,9 @@ export const createDefinitions = (
|
|
|
76
75
|
length = 1,
|
|
77
76
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
78
77
|
): CreateCustomFieldDefinition[] => (Array(length).fill({}).map((_) => ({
|
|
79
|
-
name: defaults?.name || `def_${
|
|
78
|
+
name: defaults?.name || `def_${uuidv4()}`,
|
|
80
79
|
modelType: defaults?.modelType || 'TestModel',
|
|
81
80
|
fieldType: defaults?.fieldType || 'boolean',
|
|
82
|
-
entityId: defaults?.entityId ||
|
|
81
|
+
entityId: defaults?.entityId || uuidv4(),
|
|
83
82
|
entityType: defaults?.entityType || 'fleetId',
|
|
84
83
|
})));
|
|
@@ -16,6 +16,3 @@ 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,20 +2,10 @@ import type { ValidationError } from 'joi';
|
|
|
2
2
|
|
|
3
3
|
export interface CustomFieldEntriesDTO {
|
|
4
4
|
modelId: string;
|
|
5
|
-
entityId: string;
|
|
6
5
|
modelType: string;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
*
|
|
11
|
-
* Example:
|
|
12
|
-
* {
|
|
13
|
-
* "vehicleColor": "Red",
|
|
14
|
-
* "vehicleType": "premium",
|
|
15
|
-
* "isActive": true
|
|
16
|
-
* }
|
|
17
|
-
*/
|
|
18
|
-
customFields: Record<string, any>;
|
|
6
|
+
customFields: {
|
|
7
|
+
[customFieldName: string]: any;
|
|
8
|
+
};
|
|
19
9
|
createdAt?: Date;
|
|
20
10
|
updatedAt?: Date;
|
|
21
11
|
}
|