@autofleet/sadot 0.8.6 → 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/errors/index.d.ts +4 -0
- package/dist/errors/index.js +9 -1
- package/dist/index.js +2 -2
- package/dist/models/CustomFieldEntries.d.ts +15 -0
- package/dist/models/CustomFieldEntries.js +119 -0
- package/dist/models/index.d.ts +2 -3
- package/dist/models/index.js +16 -14
- package/dist/repository/definition.d.ts +7 -0
- package/dist/repository/definition.js +32 -1
- package/dist/scopes/filter.js +3 -0
- package/dist/tests/mocks/definition.mock.d.ts +2 -2
- package/dist/tests/mocks/definition.mock.js +0 -1
- package/dist/types/entries/index.d.ts +15 -0
- package/dist/types/entries/index.js +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/utils/validations/index.d.ts +5 -0
- package/dist/utils/validations/index.js +22 -1
- package/package.json +1 -1
- package/src/errors/index.ts +9 -0
- package/src/index.ts +2 -2
- package/src/models/CustomFieldEntries.ts +77 -0
- package/src/models/index.ts +28 -17
- package/src/repository/definition.ts +43 -0
- package/src/scopes/filter.ts +3 -0
- package/src/tests/mocks/definition.mock.ts +10 -11
- package/src/types/entries/index.ts +17 -0
- package/src/types/index.ts +1 -0
- package/src/utils/validations/index.ts +24 -0
package/.env
ADDED
package/dist/errors/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BadRequest } from '@autofleet/errors';
|
|
2
2
|
import type { ValidationError } from 'joi';
|
|
3
|
+
import type { EntriesValidationError } from '../types/entries';
|
|
3
4
|
export declare class MissingRequiredCustomFieldError extends BadRequest {
|
|
4
5
|
constructor(missingFields: string[]);
|
|
5
6
|
}
|
|
@@ -15,6 +16,9 @@ export declare class InvalidFieldTypeError extends BadRequest {
|
|
|
15
16
|
export declare class InvalidValueError extends BadRequest {
|
|
16
17
|
constructor(value: any, fieldDefinitionName: string, joiValidationError: ValidationError);
|
|
17
18
|
}
|
|
19
|
+
export declare class InvalidEntriesError extends BadRequest {
|
|
20
|
+
constructor(modelId: string, validationErrors: EntriesValidationError[]);
|
|
21
|
+
}
|
|
18
22
|
export declare class MissingDefinitionError extends BadRequest {
|
|
19
23
|
constructor(fieldNames: string[]);
|
|
20
24
|
}
|
package/dist/errors/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MissingDefinitionError = exports.InvalidValueError = exports.InvalidFieldTypeError = exports.UnsupportedCustomValidationError = exports.UnsupportedCustomFieldTypeError = exports.MissingRequiredCustomFieldError = void 0;
|
|
3
|
+
exports.MissingDefinitionError = exports.InvalidEntriesError = exports.InvalidValueError = exports.InvalidFieldTypeError = exports.UnsupportedCustomValidationError = exports.UnsupportedCustomFieldTypeError = exports.MissingRequiredCustomFieldError = void 0;
|
|
4
4
|
/* eslint-disable max-classes-per-file */
|
|
5
5
|
const errors_1 = require("@autofleet/errors");
|
|
6
6
|
class MissingRequiredCustomFieldError extends errors_1.BadRequest {
|
|
@@ -48,6 +48,14 @@ class InvalidValueError extends errors_1.BadRequest {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
exports.InvalidValueError = InvalidValueError;
|
|
51
|
+
class InvalidEntriesError extends errors_1.BadRequest {
|
|
52
|
+
constructor(modelId, validationErrors) {
|
|
53
|
+
const errors = validationErrors.map((validationError) => new InvalidValueError(validationError.value, validationError.fieldDefinitionName, validationError.joiValidationError));
|
|
54
|
+
super(errors, null, null);
|
|
55
|
+
this.message = `Invalid entries on ${modelId}`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.InvalidEntriesError = InvalidEntriesError;
|
|
51
59
|
class MissingDefinitionError extends errors_1.BadRequest {
|
|
52
60
|
constructor(fieldNames) {
|
|
53
61
|
const err = new Error(`Missing custom field definition for field ${fieldNames.join(',')}`);
|
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ __exportStar(require("./utils/helpers"), exports);
|
|
|
31
31
|
* @see {@link 'custom-fields/config'} for configurations
|
|
32
32
|
*/
|
|
33
33
|
const useCustomFields = async (app, getModel, options) => {
|
|
34
|
-
const { models } = options;
|
|
34
|
+
const { models, useCustomFieldsEntries } = options;
|
|
35
35
|
if (app) {
|
|
36
36
|
app.use('/api', api_1.default);
|
|
37
37
|
}
|
|
@@ -41,7 +41,7 @@ const useCustomFields = async (app, getModel, options) => {
|
|
|
41
41
|
}
|
|
42
42
|
// The order is important
|
|
43
43
|
(0, init_1.addHooks)(models, getModel);
|
|
44
|
-
await (0, models_1.initTables)(sequelize, options.getUser);
|
|
44
|
+
await (0, models_1.initTables)(sequelize, options.getUser, { useCustomFieldsEntries });
|
|
45
45
|
(0, init_1.addScopes)(models, getModel);
|
|
46
46
|
(0, init_1.applyCustomAssociation)(models);
|
|
47
47
|
logger_1.default.debug('sadot - custom fields finished initializing with models', models);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Model } from 'sequelize-typescript';
|
|
2
|
+
declare class CustomFieldEntries extends Model {
|
|
3
|
+
/** The ID of the entity of which this row hold the custom field entries of, e.g. Vehicle / StopPoint / etc. */
|
|
4
|
+
modelId: string;
|
|
5
|
+
/** A dictionary of customFields and values with the following structure: `{ customFieldName: 'CustomFieldValue' }` */
|
|
6
|
+
customFields: any;
|
|
7
|
+
/** The type of model which this custom field entry represents. e.g. Vehicle / StopPoint / etc. */
|
|
8
|
+
modelType: string;
|
|
9
|
+
createdAt?: Date;
|
|
10
|
+
updatedAt?: Date;
|
|
11
|
+
static validateValues(instances: CustomFieldEntries[]): Promise<void>;
|
|
12
|
+
static validateValue(instance: CustomFieldEntries): Promise<void>;
|
|
13
|
+
static afterSaveHandler(instance: CustomFieldEntries, options: any): void;
|
|
14
|
+
}
|
|
15
|
+
export default CustomFieldEntries;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
19
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
20
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
21
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
22
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
23
|
+
};
|
|
24
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
25
|
+
if (mod && mod.__esModule) return mod;
|
|
26
|
+
var result = {};
|
|
27
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
28
|
+
__setModuleDefault(result, mod);
|
|
29
|
+
return result;
|
|
30
|
+
};
|
|
31
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
32
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
33
|
+
};
|
|
34
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
const sequelize_typescript_1 = require("sequelize-typescript");
|
|
36
|
+
const events_1 = require("../events");
|
|
37
|
+
const CustomFieldDefinitionRepo = __importStar(require("../repository/definition"));
|
|
38
|
+
const validations_1 = require("../utils/validations");
|
|
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
|
+
static afterSaveHandler(instance, options) {
|
|
49
|
+
if (options.transaction) {
|
|
50
|
+
options.transaction.afterCommit(() => (0, events_1.sendDimEvent)(instance[0]));
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
(0, events_1.sendDimEvent)(instance[0]);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
__decorate([
|
|
58
|
+
sequelize_typescript_1.PrimaryKey,
|
|
59
|
+
(0, sequelize_typescript_1.Column)({
|
|
60
|
+
type: sequelize_typescript_1.DataType.UUID,
|
|
61
|
+
allowNull: false,
|
|
62
|
+
})
|
|
63
|
+
/** The ID of the entity of which this row hold the custom field entries of, e.g. Vehicle / StopPoint / etc. */
|
|
64
|
+
,
|
|
65
|
+
__metadata("design:type", String)
|
|
66
|
+
], CustomFieldEntries.prototype, "modelId", void 0);
|
|
67
|
+
__decorate([
|
|
68
|
+
(0, sequelize_typescript_1.Column)({
|
|
69
|
+
type: sequelize_typescript_1.DataType.JSONB,
|
|
70
|
+
allowNull: true,
|
|
71
|
+
})
|
|
72
|
+
/** A dictionary of customFields and values with the following structure: `{ customFieldName: 'CustomFieldValue' }` */
|
|
73
|
+
,
|
|
74
|
+
__metadata("design:type", Object)
|
|
75
|
+
], CustomFieldEntries.prototype, "customFields", void 0);
|
|
76
|
+
__decorate([
|
|
77
|
+
(0, sequelize_typescript_1.Column)({
|
|
78
|
+
type: sequelize_typescript_1.DataType.STRING,
|
|
79
|
+
allowNull: false,
|
|
80
|
+
})
|
|
81
|
+
/** The type of model which this custom field entry represents. e.g. Vehicle / StopPoint / etc. */
|
|
82
|
+
,
|
|
83
|
+
__metadata("design:type", String)
|
|
84
|
+
], CustomFieldEntries.prototype, "modelType", void 0);
|
|
85
|
+
__decorate([
|
|
86
|
+
sequelize_typescript_1.Column,
|
|
87
|
+
__metadata("design:type", Date)
|
|
88
|
+
], CustomFieldEntries.prototype, "createdAt", void 0);
|
|
89
|
+
__decorate([
|
|
90
|
+
sequelize_typescript_1.Column,
|
|
91
|
+
__metadata("design:type", Date)
|
|
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);
|
|
108
|
+
__decorate([
|
|
109
|
+
sequelize_typescript_1.AfterUpsert,
|
|
110
|
+
__metadata("design:type", Function),
|
|
111
|
+
__metadata("design:paramtypes", [CustomFieldEntries, Object]),
|
|
112
|
+
__metadata("design:returntype", void 0)
|
|
113
|
+
], CustomFieldEntries, "afterSaveHandler", null);
|
|
114
|
+
CustomFieldEntries = __decorate([
|
|
115
|
+
(0, sequelize_typescript_1.Table)({
|
|
116
|
+
timestamps: true,
|
|
117
|
+
})
|
|
118
|
+
], CustomFieldEntries);
|
|
119
|
+
exports.default = CustomFieldEntries;
|
package/dist/models/index.d.ts
CHANGED
|
@@ -6,9 +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
|
-
declare const initTables: (sequelize: Sequelize, getUser: CustomFieldOptions['getUser'],
|
|
10
|
-
|
|
11
|
-
schemaVersion: string;
|
|
9
|
+
declare const initTables: (sequelize: Sequelize, getUser: CustomFieldOptions['getUser'], options: {
|
|
10
|
+
useCustomFieldsEntries: boolean;
|
|
12
11
|
}) => Promise<void>;
|
|
13
12
|
declare const initTestModels: (sequelize: Sequelize) => Promise<void>;
|
|
14
13
|
export { CustomFieldValue, CustomFieldDefinition, TestModel, AssociatedTestModel, ContextAwareTestModel, ContextTestModel, initTables, initTestModels, };
|
package/dist/models/index.js
CHANGED
|
@@ -19,18 +19,25 @@ const ContextTestModel_1 = __importDefault(require("./tests/contextAwareModels/C
|
|
|
19
19
|
exports.ContextTestModel = ContextTestModel_1.default;
|
|
20
20
|
const AssociatedTestModel_1 = __importDefault(require("./tests/AssociatedTestModel"));
|
|
21
21
|
exports.AssociatedTestModel = AssociatedTestModel_1.default;
|
|
22
|
+
const CustomFieldEntries_1 = __importDefault(require("./CustomFieldEntries"));
|
|
22
23
|
const productionModels = [CustomFieldDefinition_1.default, CustomFieldValue_1.default];
|
|
23
24
|
const testModels = [TestModel_1.default, AssociatedTestModel_1.default, ContextAwareTestModel_1.default, ContextTestModel_1.default];
|
|
24
25
|
const SADOT_MIGRATION_PREFIX = 'sadot-migration';
|
|
25
26
|
const SCHEMA_VERSION = 'fb0fa867-1241-4816-b08d-5ed9060c7ae5';
|
|
26
|
-
const
|
|
27
|
-
|
|
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;
|
|
28
31
|
logger_1.default.info('custom-fields: initialize custom-fields tables');
|
|
29
32
|
// Detect models and import them to the orm
|
|
30
33
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
31
34
|
if (!sequelize.addModels) {
|
|
32
35
|
throw new Error('sequelize instance must have addModels function');
|
|
33
36
|
}
|
|
37
|
+
const schemaVersion = useCustomFieldsEntries ? CUSTOM_FIELDS_SCHEMA_VERSION_WITH_ENTRIES : CUSTOM_FIELDS_SCHEMA_VERSION;
|
|
38
|
+
if (useCustomFieldsEntries) {
|
|
39
|
+
productionModels.push(CustomFieldEntries_1.default);
|
|
40
|
+
}
|
|
34
41
|
sequelize.addModels(productionModels);
|
|
35
42
|
CustomFieldDefinition_1.default.addScope('userScope', () => {
|
|
36
43
|
const user = getUser();
|
|
@@ -61,22 +68,17 @@ const initTables = async (sequelize, getUser, { schemaPrefix, schemaVersion } =
|
|
|
61
68
|
timestamps: false,
|
|
62
69
|
schema: 'public',
|
|
63
70
|
});
|
|
64
|
-
const migrations = await SequelizeMeta.findAll({
|
|
65
|
-
const currentSadotSchemaVersion = migrations.
|
|
66
|
-
|
|
67
|
-
|
|
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) {
|
|
68
75
|
await CustomFieldDefinition_1.default.sync({ alter: true });
|
|
69
76
|
await CustomFieldValue_1.default.sync({ alter: true });
|
|
70
|
-
if (
|
|
71
|
-
await
|
|
77
|
+
if (useCustomFieldsEntries) {
|
|
78
|
+
await CustomFieldEntries_1.default.sync({ alter: true });
|
|
72
79
|
}
|
|
80
|
+
await SequelizeMeta.create({ name: schemaVersion });
|
|
73
81
|
logger_1.default.info('custom-fields: models synced');
|
|
74
|
-
if (migrations.length && expectedSchemaVersionIndex !== -1 && expectedSchemaVersionIndex < migrations.length - 1) {
|
|
75
|
-
// We have existing migrations, and we are calling `sync`.
|
|
76
|
-
// This means we are in a `down` migration, and hence we should delete newer migrations to ensure we can reapply them.
|
|
77
|
-
const migrationsToDelete = migrations.slice(expectedSchemaVersionIndex + 1);
|
|
78
|
-
await SequelizeMeta.destroy({ where: { name: { [sequelize_1.Op.in]: migrationsToDelete.map((m) => m.name) } } });
|
|
79
|
-
}
|
|
80
82
|
}
|
|
81
83
|
};
|
|
82
84
|
exports.initTables = initTables;
|
|
@@ -2,12 +2,16 @@ import { type Includeable, type Transaction, type FindOptions, type WhereOptions
|
|
|
2
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;
|
|
8
9
|
transaction?: Transaction;
|
|
9
10
|
include?: Includeable | Includeable[];
|
|
10
11
|
}
|
|
12
|
+
type SadotGetDefinitionsByEntityIdsOptions = FindOptions & {
|
|
13
|
+
modelOptions?: ModelOptions;
|
|
14
|
+
} & Pick<SadotFindOptions, 'withDisabled'>;
|
|
11
15
|
export declare const findAll: (where: WhereOptions, options?: SadotFindOptions) => Promise<CustomFieldDefinition[]>;
|
|
12
16
|
export declare const findByIds: (ids: string[], options?: SadotFindOptions) => Promise<CustomFieldDefinition[]>;
|
|
13
17
|
export declare const findById: (id: string, options?: Pick<SadotFindOptions, 'withDisabled'>) => Promise<CustomFieldDefinition | null>;
|
|
@@ -23,4 +27,7 @@ export declare const destroy: (id: string) => Promise<number>;
|
|
|
23
27
|
* Return the names of the required fields for a given model
|
|
24
28
|
*/
|
|
25
29
|
export declare const getRequiredFields: (modelType: string, modelId: string | string[], entityId: string | string[], modelOptions?: ModelOptions) => Promise<string[]>;
|
|
30
|
+
export declare const getCustomFieldDefinitionsDictionary: (instances: CustomFieldEntriesDTO[], options?: SadotGetDefinitionsByEntityIdsOptions) => Promise<{
|
|
31
|
+
[definitionName: string]: CustomFieldDefinition;
|
|
32
|
+
}>;
|
|
26
33
|
export {};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getRequiredFields = exports.destroy = exports.disable = exports.update = exports.findDefinitionsByModels = exports.findByWhere = exports.findByEntityIds = exports.findById = exports.findByIds = exports.findAll = exports.create = void 0;
|
|
3
|
+
exports.getCustomFieldDefinitionsDictionary = exports.getRequiredFields = exports.destroy = exports.disable = exports.update = exports.findDefinitionsByModels = exports.findByWhere = exports.findByEntityIds = exports.findById = exports.findByIds = exports.findAll = exports.create = void 0;
|
|
4
4
|
const sequelize_1 = require("sequelize");
|
|
5
5
|
const models_1 = require("../models");
|
|
6
|
+
const errors_1 = require("../errors");
|
|
6
7
|
const create = (data) => models_1.CustomFieldDefinition.create(data);
|
|
7
8
|
exports.create = create;
|
|
8
9
|
const findAll = (where, options = { withDisabled: false }) => {
|
|
@@ -86,3 +87,33 @@ const getRequiredFields = async (modelType, modelId, entityId, modelOptions = {}
|
|
|
86
87
|
return [...new Set(requiredFieldsNames)];
|
|
87
88
|
};
|
|
88
89
|
exports.getRequiredFields = getRequiredFields;
|
|
90
|
+
const getCustomFieldDefinitionsDictionary = async (instances, options = { withDisabled: false, modelOptions: {} }) => {
|
|
91
|
+
const { modelType } = instances[0];
|
|
92
|
+
const customFields = new Set();
|
|
93
|
+
const modelIds = [];
|
|
94
|
+
instances.forEach((instance) => {
|
|
95
|
+
modelIds.push(instance.modelId);
|
|
96
|
+
Object.keys(instance.customFields).forEach((fieldName) => {
|
|
97
|
+
customFields.add(fieldName);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
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);
|
|
111
|
+
const matchedDefinitions = definitions.filter((def) => customFields.has(def.name));
|
|
112
|
+
const matchedDefinitionsByName = Object.fromEntries(matchedDefinitions.map((definition) => [definition.name, definition]));
|
|
113
|
+
if (!definitions?.length || matchedDefinitions.length !== customFields.size) {
|
|
114
|
+
const unmatchedCustomFields = Array.from(customFields).filter((customField) => !matchedDefinitionsByName[customField]);
|
|
115
|
+
throw new errors_1.MissingDefinitionError(unmatchedCustomFields);
|
|
116
|
+
}
|
|
117
|
+
return matchedDefinitionsByName;
|
|
118
|
+
};
|
|
119
|
+
exports.getCustomFieldDefinitionsDictionary = getCustomFieldDefinitionsDictionary;
|
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 ';
|
|
@@ -16,9 +16,9 @@ export declare const coolFieldDefinition2: {
|
|
|
16
16
|
deletedAt?: Date;
|
|
17
17
|
defaultValue?: any;
|
|
18
18
|
blockEditingFromUI?: boolean;
|
|
19
|
-
displayName?: string;
|
|
20
19
|
validation?: any;
|
|
21
20
|
fieldType: string;
|
|
21
|
+
displayName?: string;
|
|
22
22
|
entityId: string;
|
|
23
23
|
entityType: string;
|
|
24
24
|
modelType: string;
|
|
@@ -33,9 +33,9 @@ export declare const coolFieldDefinition3: {
|
|
|
33
33
|
deletedAt?: Date;
|
|
34
34
|
defaultValue?: any;
|
|
35
35
|
blockEditingFromUI?: boolean;
|
|
36
|
-
displayName?: string;
|
|
37
36
|
validation?: any;
|
|
38
37
|
fieldType: string;
|
|
38
|
+
displayName?: string;
|
|
39
39
|
entityId: string;
|
|
40
40
|
entityType: string;
|
|
41
41
|
modelType: string;
|
|
@@ -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;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ValidationError } from 'joi';
|
|
2
|
+
export interface CustomFieldEntriesDTO {
|
|
3
|
+
modelId: string;
|
|
4
|
+
modelType: string;
|
|
5
|
+
customFields: {
|
|
6
|
+
[customFieldName: string]: any;
|
|
7
|
+
};
|
|
8
|
+
createdAt?: Date;
|
|
9
|
+
updatedAt?: Date;
|
|
10
|
+
}
|
|
11
|
+
export interface EntriesValidationError {
|
|
12
|
+
value: any;
|
|
13
|
+
fieldDefinitionName: string;
|
|
14
|
+
joiValidationError: ValidationError;
|
|
15
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import type { CustomFieldDefinition } from '../../models';
|
|
2
|
+
import type { CustomFieldEntriesDTO } from '../../types/entries';
|
|
1
3
|
import type { CustomFieldDefinitionType } from '../constants';
|
|
2
4
|
export declare const validateFieldType: (type: CustomFieldDefinitionType) => boolean;
|
|
3
5
|
export declare const validateValue: (value: unknown, valueType: CustomFieldDefinitionType, validation?: unknown) => import("joi").ValidationResult;
|
|
6
|
+
export declare const validateInstanceCustomFieldEntries: (instance: CustomFieldEntriesDTO, definitionsByName: {
|
|
7
|
+
[defName: string]: CustomFieldDefinition;
|
|
8
|
+
}) => void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.validateValue = exports.validateFieldType = void 0;
|
|
3
|
+
exports.validateInstanceCustomFieldEntries = exports.validateValue = exports.validateFieldType = void 0;
|
|
4
|
+
const errors_1 = require("../../errors");
|
|
4
5
|
const validators_1 = require("./validators");
|
|
5
6
|
const validateFieldType = (type) => Object.keys(validators_1.validators).includes(type);
|
|
6
7
|
exports.validateFieldType = validateFieldType;
|
|
@@ -15,3 +16,23 @@ const validateValue = (value, valueType, validation) => {
|
|
|
15
16
|
*/
|
|
16
17
|
};
|
|
17
18
|
exports.validateValue = validateValue;
|
|
19
|
+
const validateInstanceCustomFieldEntries = (instance, definitionsByName) => {
|
|
20
|
+
const validationErrors = Object.entries(instance.customFields)
|
|
21
|
+
.map(([customFieldName, value]) => {
|
|
22
|
+
const { validation, fieldType } = definitionsByName[customFieldName];
|
|
23
|
+
const result = (0, exports.validateValue)(value, fieldType, validation);
|
|
24
|
+
if (result?.error) {
|
|
25
|
+
return {
|
|
26
|
+
joiValidationError: result.error,
|
|
27
|
+
fieldDefinitionName: customFieldName,
|
|
28
|
+
value,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
})
|
|
33
|
+
.filter((result) => !!result);
|
|
34
|
+
if (validationErrors?.length) {
|
|
35
|
+
throw new errors_1.InvalidEntriesError(instance.modelId, validationErrors);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
exports.validateInstanceCustomFieldEntries = validateInstanceCustomFieldEntries;
|
package/package.json
CHANGED
package/src/errors/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable max-classes-per-file */
|
|
2
2
|
import { BadRequest } from '@autofleet/errors';
|
|
3
3
|
import type { ValidationError } from 'joi';
|
|
4
|
+
import type { EntriesValidationError } from '../types/entries';
|
|
4
5
|
|
|
5
6
|
export class MissingRequiredCustomFieldError extends BadRequest {
|
|
6
7
|
constructor(missingFields: string[]) {
|
|
@@ -50,6 +51,14 @@ export class InvalidValueError extends BadRequest {
|
|
|
50
51
|
}
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
export class InvalidEntriesError extends BadRequest {
|
|
55
|
+
constructor(modelId: string, validationErrors: EntriesValidationError[]) {
|
|
56
|
+
const errors = validationErrors.map((validationError) => new InvalidValueError(validationError.value, validationError.fieldDefinitionName, validationError.joiValidationError));
|
|
57
|
+
super(errors, null, null);
|
|
58
|
+
this.message = `Invalid entries on ${modelId}`;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
53
62
|
export class MissingDefinitionError extends BadRequest {
|
|
54
63
|
constructor(fieldNames: string[]) {
|
|
55
64
|
const err = new Error(`Missing custom field definition for field ${fieldNames.join(',')}`);
|
package/src/index.ts
CHANGED
|
@@ -26,7 +26,7 @@ const useCustomFields = async (
|
|
|
26
26
|
getModel: ModelFetcher,
|
|
27
27
|
options: CustomFieldOptions,
|
|
28
28
|
): Promise<Sequelize> => {
|
|
29
|
-
const { models } = options;
|
|
29
|
+
const { models, useCustomFieldsEntries } = options;
|
|
30
30
|
if (app) {
|
|
31
31
|
app.use('/api', api);
|
|
32
32
|
}
|
|
@@ -36,7 +36,7 @@ const useCustomFields = async (
|
|
|
36
36
|
}
|
|
37
37
|
// The order is important
|
|
38
38
|
addHooks(models, getModel);
|
|
39
|
-
await initTables(sequelize, options.getUser);
|
|
39
|
+
await initTables(sequelize, options.getUser, { useCustomFieldsEntries });
|
|
40
40
|
addScopes(models, getModel);
|
|
41
41
|
applyCustomAssociation(models);
|
|
42
42
|
logger.debug('sadot - custom fields finished initializing with models', models);
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Table,
|
|
3
|
+
Column,
|
|
4
|
+
Model,
|
|
5
|
+
PrimaryKey,
|
|
6
|
+
DataType,
|
|
7
|
+
BeforeCreate,
|
|
8
|
+
BeforeUpsert,
|
|
9
|
+
AfterUpsert,
|
|
10
|
+
BeforeUpdate,
|
|
11
|
+
BeforeBulkCreate,
|
|
12
|
+
BeforeBulkUpdate,
|
|
13
|
+
} from 'sequelize-typescript';
|
|
14
|
+
import { sendDimEvent } from '../events';
|
|
15
|
+
import * as CustomFieldDefinitionRepo from '../repository/definition';
|
|
16
|
+
import { validateInstanceCustomFieldEntries } from '../utils/validations';
|
|
17
|
+
|
|
18
|
+
@Table({
|
|
19
|
+
timestamps: true,
|
|
20
|
+
})
|
|
21
|
+
class CustomFieldEntries extends Model {
|
|
22
|
+
@PrimaryKey
|
|
23
|
+
@Column({
|
|
24
|
+
type: DataType.UUID,
|
|
25
|
+
allowNull: false,
|
|
26
|
+
})
|
|
27
|
+
/** The ID of the entity of which this row hold the custom field entries of, e.g. Vehicle / StopPoint / etc. */
|
|
28
|
+
modelId!: string;
|
|
29
|
+
|
|
30
|
+
@Column({
|
|
31
|
+
type: DataType.JSONB,
|
|
32
|
+
allowNull: true,
|
|
33
|
+
})
|
|
34
|
+
/** A dictionary of customFields and values with the following structure: `{ customFieldName: 'CustomFieldValue' }` */
|
|
35
|
+
customFields!: any;
|
|
36
|
+
|
|
37
|
+
@Column({
|
|
38
|
+
type: DataType.STRING,
|
|
39
|
+
allowNull: false,
|
|
40
|
+
})
|
|
41
|
+
/** The type of model which this custom field entry represents. e.g. Vehicle / StopPoint / etc. */
|
|
42
|
+
modelType!: string;
|
|
43
|
+
|
|
44
|
+
@Column
|
|
45
|
+
createdAt?: Date;
|
|
46
|
+
|
|
47
|
+
@Column
|
|
48
|
+
updatedAt?: Date;
|
|
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
|
+
|
|
67
|
+
@AfterUpsert
|
|
68
|
+
static afterSaveHandler(instance: CustomFieldEntries, options): void {
|
|
69
|
+
if (options.transaction) {
|
|
70
|
+
options.transaction.afterCommit(() => sendDimEvent(instance[0]));
|
|
71
|
+
} else {
|
|
72
|
+
sendDimEvent(instance[0]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default CustomFieldEntries;
|
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';
|
|
@@ -9,25 +9,38 @@ import ContextAwareTestModel from './tests/contextAwareModels/ContextAwareTestMo
|
|
|
9
9
|
import ContextTestModel from './tests/contextAwareModels/ContextTestModel';
|
|
10
10
|
import AssociatedTestModel from './tests/AssociatedTestModel';
|
|
11
11
|
import type { CustomFieldOptions } from '../types';
|
|
12
|
+
import CustomFieldEntries from './CustomFieldEntries';
|
|
12
13
|
|
|
13
|
-
|
|
14
|
+
type ProductionModel = typeof CustomFieldDefinition | typeof CustomFieldValue | typeof CustomFieldEntries
|
|
15
|
+
|
|
16
|
+
const productionModels: ProductionModel[] = [CustomFieldDefinition, CustomFieldValue];
|
|
14
17
|
const testModels = [TestModel, AssociatedTestModel, ContextAwareTestModel, ContextTestModel];
|
|
15
18
|
|
|
16
19
|
const SADOT_MIGRATION_PREFIX = 'sadot-migration';
|
|
17
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`;
|
|
18
23
|
|
|
19
24
|
const initTables = async (
|
|
20
25
|
sequelize: Sequelize,
|
|
21
26
|
getUser: CustomFieldOptions['getUser'],
|
|
22
|
-
|
|
27
|
+
options: {
|
|
28
|
+
useCustomFieldsEntries: boolean
|
|
29
|
+
},
|
|
23
30
|
): Promise<void> => {
|
|
24
|
-
const
|
|
31
|
+
const { useCustomFieldsEntries } = options;
|
|
25
32
|
logger.info('custom-fields: initialize custom-fields tables');
|
|
26
33
|
// Detect models and import them to the orm
|
|
27
34
|
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
28
35
|
if (!sequelize.addModels) {
|
|
29
36
|
throw new Error('sequelize instance must have addModels function');
|
|
30
37
|
}
|
|
38
|
+
|
|
39
|
+
const schemaVersion = useCustomFieldsEntries ? CUSTOM_FIELDS_SCHEMA_VERSION_WITH_ENTRIES : CUSTOM_FIELDS_SCHEMA_VERSION;
|
|
40
|
+
if (useCustomFieldsEntries) {
|
|
41
|
+
productionModels.push(CustomFieldEntries);
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
sequelize.addModels(productionModels);
|
|
32
45
|
|
|
33
46
|
CustomFieldDefinition.addScope('userScope', () => {
|
|
@@ -65,24 +78,22 @@ const initTables = async (
|
|
|
65
78
|
schema: 'public',
|
|
66
79
|
},
|
|
67
80
|
);
|
|
81
|
+
const migrations = await SequelizeMeta.findAll({ raw: true });
|
|
82
|
+
const currentSadotSchemaVersion = migrations.reverse().find((m: any) => m.name.includes(SADOT_MIGRATION_PREFIX));
|
|
68
83
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (!currentSadotSchemaVersion || (currentSadotSchemaVersion as any).name !== CUSTOM_FIELDS_SCHEMA_VERSION) {
|
|
84
|
+
if (
|
|
85
|
+
!currentSadotSchemaVersion
|
|
86
|
+
|| (currentSadotSchemaVersion as any).name !== schemaVersion
|
|
87
|
+
) {
|
|
74
88
|
await CustomFieldDefinition.sync({ alter: true });
|
|
75
89
|
await CustomFieldValue.sync({ alter: true });
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
|
|
91
|
+
if (useCustomFieldsEntries) {
|
|
92
|
+
await CustomFieldEntries.sync({ alter: true });
|
|
78
93
|
}
|
|
94
|
+
|
|
95
|
+
await SequelizeMeta.create({ name: schemaVersion });
|
|
79
96
|
logger.info('custom-fields: models synced');
|
|
80
|
-
if (migrations.length && expectedSchemaVersionIndex !== -1 && expectedSchemaVersionIndex < migrations.length - 1) {
|
|
81
|
-
// We have existing migrations, and we are calling `sync`.
|
|
82
|
-
// This means we are in a `down` migration, and hence we should delete newer migrations to ensure we can reapply them.
|
|
83
|
-
const migrationsToDelete = migrations.slice(expectedSchemaVersionIndex + 1);
|
|
84
|
-
await SequelizeMeta.destroy({ where: { name: { [Op.in]: migrationsToDelete.map((m) => (m as any).name) } } });
|
|
85
|
-
}
|
|
86
97
|
}
|
|
87
98
|
};
|
|
88
99
|
|
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
import { CustomFieldDefinition } from '../models';
|
|
6
6
|
import type { CreateCustomFieldDefinition, UpdateCustomFieldDefinition } from '../types/definition';
|
|
7
7
|
import type { ModelOptions } from '../types';
|
|
8
|
+
import { MissingDefinitionError } from '../errors';
|
|
9
|
+
import type { CustomFieldEntriesDTO } from '../types/entries';
|
|
8
10
|
|
|
9
11
|
export const create = (data: CreateCustomFieldDefinition): Promise<CustomFieldDefinition> =>
|
|
10
12
|
CustomFieldDefinition.create(data);
|
|
@@ -15,6 +17,8 @@ interface SadotFindOptions {
|
|
|
15
17
|
include?: Includeable | Includeable[];
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
type SadotGetDefinitionsByEntityIdsOptions = FindOptions & { modelOptions?: ModelOptions } & Pick<SadotFindOptions, 'withDisabled'>;
|
|
21
|
+
|
|
18
22
|
export const findAll = (
|
|
19
23
|
where: WhereOptions,
|
|
20
24
|
options: SadotFindOptions = { withDisabled: false },
|
|
@@ -129,3 +133,42 @@ export const getRequiredFields = async (
|
|
|
129
133
|
const requiredFieldsNames = requiredFields.map((definition) => definition.name);
|
|
130
134
|
return [...new Set(requiredFieldsNames)];
|
|
131
135
|
};
|
|
136
|
+
|
|
137
|
+
export const getCustomFieldDefinitionsDictionary = async (
|
|
138
|
+
instances: CustomFieldEntriesDTO[],
|
|
139
|
+
options: SadotGetDefinitionsByEntityIdsOptions = { withDisabled: false, modelOptions: {} },
|
|
140
|
+
): Promise<{ [definitionName: string]: CustomFieldDefinition }> => {
|
|
141
|
+
const { modelType } = instances[0];
|
|
142
|
+
const customFields = new Set<string>();
|
|
143
|
+
const modelIds = [];
|
|
144
|
+
instances.forEach((instance) => {
|
|
145
|
+
modelIds.push(instance.modelId);
|
|
146
|
+
|
|
147
|
+
Object.keys(instance.customFields).forEach((fieldName) => {
|
|
148
|
+
customFields.add(fieldName);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
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);
|
|
164
|
+
|
|
165
|
+
const matchedDefinitions = definitions.filter((def) => customFields.has(def.name));
|
|
166
|
+
const matchedDefinitionsByName = Object.fromEntries(matchedDefinitions.map((definition) => [definition.name, definition]));
|
|
167
|
+
|
|
168
|
+
if (!definitions?.length || matchedDefinitions.length !== customFields.size) {
|
|
169
|
+
const unmatchedCustomFields = Array.from(customFields).filter((customField) => !matchedDefinitionsByName[customField]);
|
|
170
|
+
throw new MissingDefinitionError(unmatchedCustomFields);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return matchedDefinitionsByName;
|
|
174
|
+
};
|
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
|
})));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ValidationError } from 'joi';
|
|
2
|
+
|
|
3
|
+
export interface CustomFieldEntriesDTO {
|
|
4
|
+
modelId: string;
|
|
5
|
+
modelType: string;
|
|
6
|
+
customFields: {
|
|
7
|
+
[customFieldName: string]: any;
|
|
8
|
+
};
|
|
9
|
+
createdAt?: Date;
|
|
10
|
+
updatedAt?: Date;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface EntriesValidationError {
|
|
14
|
+
value: any,
|
|
15
|
+
fieldDefinitionName: string,
|
|
16
|
+
joiValidationError: ValidationError
|
|
17
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { InvalidEntriesError } from '../../errors';
|
|
2
|
+
import type { CustomFieldDefinition } from '../../models';
|
|
3
|
+
import type { CustomFieldEntriesDTO } from '../../types/entries';
|
|
1
4
|
import type { CustomFieldDefinitionType } from '../constants';
|
|
2
5
|
import { validators } from './validators';
|
|
3
6
|
|
|
@@ -17,3 +20,24 @@ export const validateValue = (
|
|
|
17
20
|
* }
|
|
18
21
|
*/
|
|
19
22
|
};
|
|
23
|
+
|
|
24
|
+
export const validateInstanceCustomFieldEntries = (instance: CustomFieldEntriesDTO, definitionsByName: { [defName: string]: CustomFieldDefinition; }) => {
|
|
25
|
+
const validationErrors = Object.entries(instance.customFields)
|
|
26
|
+
.map(([customFieldName, value]) => {
|
|
27
|
+
const { validation, fieldType } = definitionsByName[customFieldName];
|
|
28
|
+
const result = validateValue(value, fieldType, validation);
|
|
29
|
+
if (result?.error) {
|
|
30
|
+
return {
|
|
31
|
+
joiValidationError: result.error,
|
|
32
|
+
fieldDefinitionName: customFieldName,
|
|
33
|
+
value,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
})
|
|
38
|
+
.filter((result) => !!result);
|
|
39
|
+
|
|
40
|
+
if (validationErrors?.length) {
|
|
41
|
+
throw new InvalidEntriesError(instance.modelId, validationErrors);
|
|
42
|
+
}
|
|
43
|
+
};
|