@autofleet/sadot 1.0.0-beta.0 → 1.0.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/README.md +5 -0
- package/dist/api/index.d.ts +2 -1
- package/dist/api/index.js +3 -2
- package/dist/api/v1/definition/index.d.ts +2 -1
- package/dist/api/v1/definition/index.js +12 -14
- package/dist/api/v1/definition/validations.js +14 -12
- package/dist/api/v1/errors.d.ts +3 -1
- package/dist/api/v1/errors.js +1 -4
- package/dist/api/v1/index.d.ts +2 -1
- package/dist/api/v1/index.js +5 -2
- package/dist/api/v1/validator/index.d.ts +3 -0
- package/dist/api/v1/validator/index.js +143 -0
- package/dist/api/v1/validator/validations.d.ts +6 -0
- package/dist/api/v1/validator/validations.js +40 -0
- package/dist/errors/index.d.ts +9 -1
- package/dist/errors/index.js +25 -4
- package/dist/events/index.d.ts +2 -1
- package/dist/events/index.js +17 -11
- package/dist/hooks/create.d.ts +2 -2
- package/dist/hooks/create.js +40 -19
- package/dist/hooks/enrich.d.ts +20 -2
- package/dist/hooks/enrich.js +88 -16
- package/dist/hooks/hooks.d.ts +17 -0
- package/dist/hooks/hooks.js +379 -0
- package/dist/hooks/index.d.ts +2 -3
- package/dist/hooks/index.js +6 -7
- package/dist/hooks/update.d.ts +2 -2
- package/dist/hooks/update.js +16 -26
- package/dist/hooks/utils/updateInstanceValues.d.ts +15 -0
- package/dist/hooks/utils/updateInstanceValues.js +50 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +19 -6
- package/dist/models/CustomFieldDefinition.d.ts +1 -0
- package/dist/models/CustomFieldDefinition.js +10 -2
- package/dist/models/CustomFieldEntries.d.ts +15 -0
- package/dist/models/CustomFieldEntries.js +123 -0
- package/dist/models/CustomFieldValue.d.ts +3 -2
- package/dist/models/CustomFieldValue.js +22 -14
- package/dist/models/CustomValidator.d.ts +17 -0
- package/dist/models/CustomValidator.js +98 -0
- package/dist/models/index.d.ts +10 -2
- package/dist/models/index.js +60 -22
- package/dist/repository/definition.d.ts +23 -7
- package/dist/repository/definition.js +36 -7
- package/dist/repository/entries.d.ts +13 -0
- package/dist/repository/entries.js +92 -0
- package/dist/repository/utils/formatValues.d.ts +3 -0
- package/dist/repository/utils/formatValues.js +16 -0
- package/dist/repository/validator.d.ts +21 -0
- package/dist/repository/validator.js +62 -0
- package/dist/repository/value.d.ts +1 -1
- package/dist/repository/value.js +7 -32
- package/dist/scopes/filter.d.ts +5 -22
- package/dist/scopes/filter.js +18 -65
- package/dist/scopes/helpers/filter.helpers.d.ts +42 -0
- package/dist/scopes/helpers/filter.helpers.js +204 -0
- package/dist/tests/api/test-api.js +6 -24
- package/dist/tests/helpers/commonHooks.d.ts +6 -0
- package/dist/tests/helpers/commonHooks.js +62 -0
- package/dist/tests/helpers/database-config.js +1 -1
- package/dist/tests/helpers/index.d.ts +6 -1
- package/dist/tests/helpers/index.js +15 -2
- package/dist/tests/mocks/definition.mock.d.ts +5 -2
- package/dist/tests/mocks/definition.mock.js +10 -1
- package/dist/tests/mocks/events.mock.d.ts +1 -0
- package/dist/tests/mocks/events.mock.js +4 -2
- package/dist/types/definition/index.d.ts +1 -0
- package/dist/types/entries/index.d.ts +25 -0
- package/dist/types/entries/index.js +2 -0
- package/dist/types/index.d.ts +19 -3
- package/dist/utils/constants/index.d.ts +1 -1
- package/dist/utils/helpers/index.d.ts +4 -3
- package/dist/utils/helpers/index.js +22 -30
- package/dist/utils/init.d.ts +5 -3
- package/dist/utils/init.js +13 -11
- package/dist/utils/logger/index.d.ts +1 -0
- package/dist/utils/logger/index.js +34 -0
- package/dist/utils/validations/index.d.ts +7 -1
- package/dist/utils/validations/index.js +28 -7
- package/dist/utils/validations/schema/validator-schema.d.ts +9 -0
- package/dist/utils/validations/schema/validator-schema.js +95 -0
- package/dist/utils/validations/type.d.ts +2 -1
- package/dist/utils/validations/validators/index.js +9 -9
- package/dist/utils/validations/validators/select.validator.js +5 -2
- package/dist/utils/validations/validators/status.validator.js +8 -2
- package/package.json +28 -12
- package/src/api/index.ts +3 -2
- package/src/api/v1/definition/index.ts +20 -23
- package/src/api/v1/definition/validations.ts +16 -16
- package/src/api/v1/errors.ts +4 -7
- package/src/api/v1/index.ts +5 -3
- package/src/api/v1/validator/index.ts +141 -0
- package/src/api/v1/validator/validations.ts +39 -0
- package/src/errors/index.ts +31 -3
- package/src/events/index.ts +25 -13
- package/src/hooks/create.ts +50 -28
- package/src/hooks/enrich.ts +137 -28
- package/src/hooks/hooks.ts +467 -0
- package/src/hooks/index.ts +10 -5
- package/src/hooks/update.ts +20 -7
- package/src/hooks/utils/updateInstanceValues.ts +63 -0
- package/src/index.ts +10 -8
- package/src/models/CustomFieldDefinition.ts +9 -2
- package/src/models/CustomFieldEntries.ts +81 -0
- package/src/models/CustomFieldValue.ts +25 -17
- package/src/models/CustomValidator.ts +78 -0
- package/src/models/index.ts +83 -25
- package/src/repository/definition.ts +62 -14
- package/src/repository/entries.ts +88 -0
- package/src/repository/utils/formatValues.ts +14 -0
- package/src/repository/validator.ts +104 -0
- package/src/repository/value.ts +5 -35
- package/src/scopes/filter.ts +33 -106
- package/src/scopes/helpers/filter.helpers.ts +227 -0
- package/src/tests/api/test-api.ts +4 -2
- package/src/tests/helpers/commonHooks.ts +43 -0
- package/src/tests/helpers/database-config.ts +1 -1
- package/src/tests/helpers/index.ts +18 -2
- package/src/tests/mocks/definition.mock.ts +18 -9
- package/src/tests/mocks/events.mock.ts +4 -1
- package/src/types/definition/index.ts +1 -0
- package/src/types/entries/index.ts +27 -0
- package/src/types/index.ts +20 -3
- package/src/utils/helpers/index.ts +28 -35
- package/src/utils/init.ts +17 -12
- package/src/utils/logger/index.ts +9 -0
- package/src/utils/validations/index.ts +30 -6
- package/src/utils/validations/schema/README.md +93 -0
- package/src/utils/validations/schema/validator-schema.ts +106 -0
- package/src/utils/validations/type.ts +2 -1
- package/src/utils/validations/validators/index.ts +9 -9
- package/src/utils/validations/validators/select.validator.ts +3 -2
- package/src/utils/validations/validators/status.validator.ts +6 -2
- package/tsconfig.build.json +7 -0
- package/tsconfig.json +1 -1
|
@@ -1,14 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
Op,
|
|
3
|
+
type Includeable, type Transaction, type FindOptions, type WhereOptions,
|
|
4
|
+
} from 'sequelize';
|
|
5
|
+
import { CustomFieldDefinition, type CustomFieldEntries } from '../models';
|
|
3
6
|
import type { CreateCustomFieldDefinition, UpdateCustomFieldDefinition } from '../types/definition';
|
|
4
7
|
import type { ModelOptions } from '../types';
|
|
8
|
+
import { MissingDefinitionError } from '../errors';
|
|
5
9
|
|
|
6
10
|
export const create = (data: CreateCustomFieldDefinition): Promise<CustomFieldDefinition> =>
|
|
7
11
|
CustomFieldDefinition.create(data);
|
|
8
12
|
|
|
13
|
+
interface SadotFindOptions {
|
|
14
|
+
withDisabled?: boolean;
|
|
15
|
+
transaction?: Transaction;
|
|
16
|
+
include?: Includeable | Includeable[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type SadotGetDefinitionsByEntityIdsOptions = FindOptions & { modelOptions?: ModelOptions } & Pick<SadotFindOptions, 'withDisabled'>;
|
|
20
|
+
|
|
9
21
|
export const findAll = (
|
|
10
22
|
where: WhereOptions,
|
|
11
|
-
options:
|
|
23
|
+
options: SadotFindOptions = { withDisabled: false },
|
|
12
24
|
): Promise<CustomFieldDefinition[]> => {
|
|
13
25
|
const queryModel = options.withDisabled
|
|
14
26
|
? CustomFieldDefinition.unscoped()
|
|
@@ -24,12 +36,12 @@ export const findAll = (
|
|
|
24
36
|
|
|
25
37
|
export const findByIds = (
|
|
26
38
|
ids: string[],
|
|
27
|
-
options:
|
|
39
|
+
options: SadotFindOptions = { withDisabled: false },
|
|
28
40
|
): Promise<CustomFieldDefinition[]> => findAll({ id: { [Op.in]: ids } }, options);
|
|
29
41
|
|
|
30
42
|
export const findById = (
|
|
31
43
|
id: string,
|
|
32
|
-
options:
|
|
44
|
+
options: Pick<SadotFindOptions, 'withDisabled'> = { withDisabled: false },
|
|
33
45
|
): Promise<CustomFieldDefinition | null> => {
|
|
34
46
|
const { withDisabled } = options;
|
|
35
47
|
if (withDisabled) {
|
|
@@ -46,11 +58,9 @@ export const findByEntityIds = async (
|
|
|
46
58
|
const { include, useEntityIdFromInclude } = options.modelOptions;
|
|
47
59
|
const where: WhereOptions = {
|
|
48
60
|
modelType,
|
|
61
|
+
...(!useEntityIdFromInclude && { entityId: { [Op.in]: entityIds } }),
|
|
49
62
|
};
|
|
50
63
|
|
|
51
|
-
if (!useEntityIdFromInclude) {
|
|
52
|
-
where.entityId = entityIds;
|
|
53
|
-
}
|
|
54
64
|
return CustomFieldDefinition.findAll({
|
|
55
65
|
where,
|
|
56
66
|
transaction: options.transaction,
|
|
@@ -87,13 +97,13 @@ export const update = async (
|
|
|
87
97
|
return updatedDefinition;
|
|
88
98
|
};
|
|
89
99
|
|
|
90
|
-
export const disable = (id: string): Promise<
|
|
100
|
+
export const disable = (id: string): Promise<[affectedCount: number]> =>
|
|
91
101
|
CustomFieldDefinition.update(
|
|
92
102
|
{ disabled: true },
|
|
93
103
|
{ where: { id } },
|
|
94
104
|
);
|
|
95
105
|
|
|
96
|
-
export const destroy = (id: string): Promise<
|
|
106
|
+
export const destroy = (id: string): Promise<number> =>
|
|
97
107
|
CustomFieldDefinition.destroy({ where: { id } });
|
|
98
108
|
|
|
99
109
|
/**
|
|
@@ -111,12 +121,9 @@ export const getRequiredFields = async (
|
|
|
111
121
|
const where: WhereOptions = {
|
|
112
122
|
modelType,
|
|
113
123
|
required: true,
|
|
124
|
+
...(!useEntityIdFromInclude && { entityId: { [Op.in]: entityIds } }),
|
|
114
125
|
};
|
|
115
126
|
|
|
116
|
-
if (!useEntityIdFromInclude) {
|
|
117
|
-
where.entityId = entityIds;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
127
|
const requiredFields = await CustomFieldDefinition.findAll({
|
|
121
128
|
where,
|
|
122
129
|
include: include?.(entityIds),
|
|
@@ -125,3 +132,44 @@ export const getRequiredFields = async (
|
|
|
125
132
|
const requiredFieldsNames = requiredFields.map((definition) => definition.name);
|
|
126
133
|
return [...new Set(requiredFieldsNames)];
|
|
127
134
|
};
|
|
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
|
+
*/
|
|
140
|
+
export const getCustomFieldDefinitionsDictionary = async (
|
|
141
|
+
instances: CustomFieldEntries[],
|
|
142
|
+
options: SadotGetDefinitionsByEntityIdsOptions = { withDisabled: false, modelOptions: {} },
|
|
143
|
+
): Promise<{ [definitionName: string]: CustomFieldDefinition }> => {
|
|
144
|
+
const { modelType } = instances[0]?.dataValues ?? {};
|
|
145
|
+
const customFields = new Set<string>();
|
|
146
|
+
const modelIds = [];
|
|
147
|
+
const entityIds = new Set<string>();
|
|
148
|
+
instances.forEach((instance) => {
|
|
149
|
+
const { dataValues: { modelId, entityId, customFields: instanceCustomFields } } = instance;
|
|
150
|
+
modelIds.push(modelId);
|
|
151
|
+
entityIds.add(entityId);
|
|
152
|
+
|
|
153
|
+
Object.keys(instanceCustomFields ?? {}).forEach((fieldName) => {
|
|
154
|
+
customFields.add(fieldName);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const where: WhereOptions = {
|
|
159
|
+
modelType,
|
|
160
|
+
entityId: { [Op.in]: Array.from(entityIds) },
|
|
161
|
+
name: { [Op.in]: Array.from(customFields) },
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const definitions = await findAll(where, { ...options });
|
|
165
|
+
|
|
166
|
+
const matchedDefinitions = definitions.filter((def) => customFields.has(def.name));
|
|
167
|
+
const matchedDefinitionsByName = Object.fromEntries(matchedDefinitions.map((definition) => [definition.name, definition]));
|
|
168
|
+
|
|
169
|
+
if (!definitions?.length || matchedDefinitions.length !== customFields.size) {
|
|
170
|
+
const unmatchedCustomFields = Array.from(customFields).filter((customField) => !matchedDefinitionsByName[customField]);
|
|
171
|
+
throw new MissingDefinitionError(unmatchedCustomFields);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return matchedDefinitionsByName;
|
|
175
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/* eslint-disable no-param-reassign */
|
|
2
|
+
import type {
|
|
3
|
+
FindOptions,
|
|
4
|
+
Includeable,
|
|
5
|
+
Transaction,
|
|
6
|
+
WhereOptions,
|
|
7
|
+
} from 'sequelize';
|
|
8
|
+
import { CustomFieldEntries } from '../models';
|
|
9
|
+
import type { ModelOptions } from '../types';
|
|
10
|
+
import logger from '../utils/logger';
|
|
11
|
+
import { MissingDefinitionError } from '../errors';
|
|
12
|
+
import * as DefinitionRepo from './definition';
|
|
13
|
+
import { formatFunctions } from './utils/formatValues';
|
|
14
|
+
|
|
15
|
+
type CustomFieldEntriesModelOptions = ModelOptions & { include?: Includeable, transaction?: Transaction };
|
|
16
|
+
|
|
17
|
+
export const findEntriesByModelId = async (modelId: string, options: CustomFieldEntriesModelOptions = {}) => {
|
|
18
|
+
const { transaction } = options;
|
|
19
|
+
return CustomFieldEntries.findOne({
|
|
20
|
+
where: { modelId },
|
|
21
|
+
transaction,
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const findEntriesByModelIds = async (modelIds: string[], options: CustomFieldEntriesModelOptions = {}) => {
|
|
26
|
+
const { transaction } = options;
|
|
27
|
+
return CustomFieldEntries.findAll({
|
|
28
|
+
where: { modelId: modelIds },
|
|
29
|
+
transaction,
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const updateEntries = async (
|
|
34
|
+
modelId: string,
|
|
35
|
+
modelType: string,
|
|
36
|
+
customFields: Record<string, any>,
|
|
37
|
+
identifiers: string[],
|
|
38
|
+
options: FindOptions & { modelOptions?: ModelOptions } = {},
|
|
39
|
+
) => {
|
|
40
|
+
const customFieldsNames = Object.keys(customFields);
|
|
41
|
+
logger.debug(`custom-fields: updating entries for ${modelType} ${modelId}`, {
|
|
42
|
+
customFieldsNames,
|
|
43
|
+
optionsKeys: options ? Object.keys(options) : null,
|
|
44
|
+
customFields,
|
|
45
|
+
identifiers,
|
|
46
|
+
});
|
|
47
|
+
const { modelOptions, transaction } = options;
|
|
48
|
+
|
|
49
|
+
const where: WhereOptions = {
|
|
50
|
+
modelType,
|
|
51
|
+
name: customFieldsNames,
|
|
52
|
+
...(!options.modelOptions?.useEntityIdFromInclude && { entityId: identifiers }),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const fieldDefinitions = await DefinitionRepo.findAll(where, { withDisabled: true, transaction, include: modelOptions.include?.(identifiers) }) ?? [];
|
|
56
|
+
|
|
57
|
+
const disabledDefinitions = fieldDefinitions.filter((def) => def.disabled);
|
|
58
|
+
if (fieldDefinitions.length !== customFieldsNames.length) {
|
|
59
|
+
logger.warn(`custom-fields: missing definitions for ${modelType} ${modelId}`, { names: customFieldsNames, fieldDefinitions });
|
|
60
|
+
const missingDefinitions = customFieldsNames.filter((name) => !fieldDefinitions.some((def) => def.name === name));
|
|
61
|
+
throw new MissingDefinitionError(missingDefinitions);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const disabledNames = disabledDefinitions?.map((def) => def.name) || [];
|
|
65
|
+
const valuesWithDisabledDefinitions = customFieldsNames.filter((name) => disabledNames.includes(name));
|
|
66
|
+
if (valuesWithDisabledDefinitions?.length > 0) {
|
|
67
|
+
logger.warn(`custom-fields: trying to update disabled values: ${valuesWithDisabledDefinitions.join(', ')}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const definitionsByName = Object.fromEntries(fieldDefinitions.map((definition) => [definition.name, definition]));
|
|
71
|
+
// If we need to format the value before we save it
|
|
72
|
+
Object.entries(customFields)
|
|
73
|
+
.filter(([definitionName]) => formatFunctions[definitionsByName[definitionName].fieldType])
|
|
74
|
+
.forEach(([definitionName, value]) => {
|
|
75
|
+
const { fieldType } = definitionsByName[definitionName];
|
|
76
|
+
customFields[definitionName] = formatFunctions[fieldType](value);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return CustomFieldEntries.upsert(
|
|
80
|
+
{
|
|
81
|
+
modelId,
|
|
82
|
+
entityId: fieldDefinitions[0].entityId,
|
|
83
|
+
modelType,
|
|
84
|
+
customFields,
|
|
85
|
+
},
|
|
86
|
+
options,
|
|
87
|
+
);
|
|
88
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CustomFieldDefinitionType } from '../../utils/constants';
|
|
2
|
+
|
|
3
|
+
export const formatFunctions = {
|
|
4
|
+
[CustomFieldDefinitionType.DATE]: (value) => {
|
|
5
|
+
if (value) {
|
|
6
|
+
const date = new Date(value);
|
|
7
|
+
if (date.toString() === 'Invalid Date') {
|
|
8
|
+
throw new Error(`Invalid date value: ${value}`);
|
|
9
|
+
}
|
|
10
|
+
return date.toISOString();
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
},
|
|
14
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import type { IncludeOptions, Transactionable } from 'sequelize';
|
|
2
|
+
import logger from '../utils/logger';
|
|
3
|
+
import { CustomValidator } from '../models';
|
|
4
|
+
|
|
5
|
+
export interface FindValidatorOptions extends Transactionable {
|
|
6
|
+
withDisabled?: boolean;
|
|
7
|
+
attributes?: string[];
|
|
8
|
+
raw?: boolean;
|
|
9
|
+
include?: IncludeOptions[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Make sure this interface is compatible with the Sequelize model
|
|
13
|
+
export interface ValidatorAttributes {
|
|
14
|
+
entityId: string;
|
|
15
|
+
entityType: string;
|
|
16
|
+
modelType: string;
|
|
17
|
+
schema: CustomValidator['schema'];
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
[key: string]: unknown; // Add index signature for Sequelize compatibility
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const create = async (
|
|
23
|
+
validatorAttributes: ValidatorAttributes,
|
|
24
|
+
options: Transactionable = {},
|
|
25
|
+
): Promise<CustomValidator> => {
|
|
26
|
+
logger.debug('custom-validator - create validator');
|
|
27
|
+
|
|
28
|
+
// Use unknown type to bypass TypeScript errors while maintaining compatibility
|
|
29
|
+
const validator = await CustomValidator.create(validatorAttributes as Record<string, unknown>, options);
|
|
30
|
+
|
|
31
|
+
return validator;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const findAll = async (
|
|
35
|
+
where = {},
|
|
36
|
+
options: FindValidatorOptions = { withDisabled: false },
|
|
37
|
+
): Promise<CustomValidator[]> => {
|
|
38
|
+
logger.debug('custom-validator - find all validators');
|
|
39
|
+
|
|
40
|
+
const { transaction, withDisabled } = options;
|
|
41
|
+
|
|
42
|
+
let validators;
|
|
43
|
+
if (withDisabled) {
|
|
44
|
+
// If withDisabled is true, use unscoped to ignore the default scope that filters disabled items
|
|
45
|
+
// Apply the userScope separately to maintain permission filtering
|
|
46
|
+
validators = await CustomValidator.unscoped().scope('userScope').findAll({
|
|
47
|
+
where,
|
|
48
|
+
transaction,
|
|
49
|
+
});
|
|
50
|
+
} else {
|
|
51
|
+
// Use defaultScope and userScope to filter both disabled and by permissions
|
|
52
|
+
// The defaultScope keeps only non-disabled validators
|
|
53
|
+
validators = await CustomValidator.scope(['defaultScope', 'userScope']).findAll({
|
|
54
|
+
where,
|
|
55
|
+
transaction,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return validators;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const findAllByModelType = async (
|
|
63
|
+
modelType: string,
|
|
64
|
+
entityId: string,
|
|
65
|
+
options: FindValidatorOptions = { withDisabled: false },
|
|
66
|
+
): Promise<CustomValidator[]> => {
|
|
67
|
+
logger.debug('custom-validator - find all validators by model type');
|
|
68
|
+
|
|
69
|
+
return findAll(
|
|
70
|
+
{
|
|
71
|
+
modelType,
|
|
72
|
+
...(!options.include && {
|
|
73
|
+
entityId,
|
|
74
|
+
}),
|
|
75
|
+
},
|
|
76
|
+
options,
|
|
77
|
+
);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const update = async (
|
|
81
|
+
id: string,
|
|
82
|
+
updates: Partial<ValidatorAttributes>,
|
|
83
|
+
options?: Transactionable,
|
|
84
|
+
): Promise<[number, CustomValidator[]]> => {
|
|
85
|
+
logger.debug('custom-validator - update validator');
|
|
86
|
+
|
|
87
|
+
return CustomValidator.update(
|
|
88
|
+
updates,
|
|
89
|
+
{
|
|
90
|
+
where: { id },
|
|
91
|
+
returning: true,
|
|
92
|
+
...options,
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const disable = async (
|
|
98
|
+
id: string,
|
|
99
|
+
options?: Transactionable,
|
|
100
|
+
): Promise<[number, CustomValidator[]]> => {
|
|
101
|
+
logger.debug('custom-validator - disable validator');
|
|
102
|
+
|
|
103
|
+
return update(id, { disabled: true }, options);
|
|
104
|
+
};
|
package/src/repository/value.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable max-len */
|
|
2
1
|
import type { FindOptions, WhereOptions } from 'sequelize';
|
|
3
2
|
import { CustomFieldValue, CustomFieldDefinition } from '../models';
|
|
4
3
|
import * as DefinitionRepo from './definition';
|
|
@@ -6,7 +5,7 @@ import type { CreateCustomFieldValue, ValuesToUpdate } from '../types/value';
|
|
|
6
5
|
import logger from '../utils/logger';
|
|
7
6
|
import { MissingDefinitionError } from '../errors';
|
|
8
7
|
import type { ModelOptions } from '../types';
|
|
9
|
-
import {
|
|
8
|
+
import { formatFunctions } from './utils/formatValues';
|
|
10
9
|
|
|
11
10
|
export const findByModelIdAndDefinition = async (modelId: string, customFieldDefinitionId: string) =>
|
|
12
11
|
CustomFieldValue.findAll({ where: { modelId, customFieldDefinitionId }, include: [CustomFieldDefinition] });
|
|
@@ -43,19 +42,6 @@ export const findValuesByModelIds = async (modelIds: string[], options?): Promis
|
|
|
43
42
|
});
|
|
44
43
|
};
|
|
45
44
|
|
|
46
|
-
const formatFunctions = {
|
|
47
|
-
[CustomFieldDefinitionType.DATE]: (value) => {
|
|
48
|
-
if (value) {
|
|
49
|
-
const date = new Date(value);
|
|
50
|
-
if (date.toString() === 'Invalid Date') {
|
|
51
|
-
throw new Error(`Invalid date value: ${value}`);
|
|
52
|
-
}
|
|
53
|
-
return date.toISOString();
|
|
54
|
-
}
|
|
55
|
-
return null;
|
|
56
|
-
},
|
|
57
|
-
};
|
|
58
|
-
|
|
59
45
|
/**
|
|
60
46
|
* Try to update custom field values for a model instance.
|
|
61
47
|
* Create new value record if not exists, but fails if value's definition not exist.
|
|
@@ -67,7 +53,6 @@ export const updateValues = async (
|
|
|
67
53
|
identifiers: string[],
|
|
68
54
|
valuesToUpdate: ValuesToUpdate,
|
|
69
55
|
options: FindOptions & { modelOptions?: ModelOptions } = {},
|
|
70
|
-
defineAllDefaults = false,
|
|
71
56
|
): Promise<CustomFieldValue[]> => {
|
|
72
57
|
const names = Object.keys(valuesToUpdate);
|
|
73
58
|
logger.debug(`custom-fields: updating values for ${modelType} ${modelId}`, {
|
|
@@ -81,12 +66,10 @@ export const updateValues = async (
|
|
|
81
66
|
const where: WhereOptions = {
|
|
82
67
|
modelType,
|
|
83
68
|
name: names,
|
|
69
|
+
...(!options.modelOptions?.useEntityIdFromInclude && { entityId: identifiers }),
|
|
84
70
|
};
|
|
85
71
|
|
|
86
|
-
|
|
87
|
-
where.entityId = identifiers;
|
|
88
|
-
}
|
|
89
|
-
const fieldDefinitions = await DefinitionRepo.findAll(where, { withDisabled: true, transaction, include: modelOptions.include?.(identifiers) }) || [];
|
|
72
|
+
const fieldDefinitions = await DefinitionRepo.findAll(where, { withDisabled: true, transaction, include: modelOptions.include?.(identifiers) }) ?? [];
|
|
90
73
|
|
|
91
74
|
const disabledDefinitions = fieldDefinitions.filter((def) => def.disabled);
|
|
92
75
|
if (fieldDefinitions.length !== names.length) {
|
|
@@ -101,31 +84,18 @@ export const updateValues = async (
|
|
|
101
84
|
logger.warn(`custom-fields: trying to update disabled values: ${valuesWithDisabledDefinitions.join(', ')}`);
|
|
102
85
|
}
|
|
103
86
|
|
|
104
|
-
const visitedFields = new Set<CustomFieldDefinition>();
|
|
105
|
-
|
|
106
87
|
const values: CreateCustomFieldValue[] = names.map((name) => {
|
|
107
88
|
const fieldDefinition = fieldDefinitions.find((def) => def.name === name);
|
|
108
|
-
visitedFields.add(fieldDefinition);
|
|
109
89
|
const formatFunction = formatFunctions[fieldDefinition.fieldType];
|
|
90
|
+
const value = formatFunction ? formatFunction(valuesToUpdate[name]) : valuesToUpdate[name];
|
|
110
91
|
return {
|
|
111
92
|
modelId,
|
|
112
|
-
value: (formatFunction ? formatFunction(valuesToUpdate[name]) : valuesToUpdate[name]) ?? fieldDefinition.defaultValue,
|
|
113
93
|
updatedAt: new Date(),
|
|
114
94
|
customFieldDefinitionId: fieldDefinition.id,
|
|
95
|
+
value: value !== undefined ? value : fieldDefinition.defaultValue,
|
|
115
96
|
};
|
|
116
97
|
});
|
|
117
98
|
|
|
118
|
-
if (defineAllDefaults) {
|
|
119
|
-
fieldDefinitions.filter((def) => !visitedFields.has(def) && ![null, undefined].includes(def.defaultValue)).forEach(({ id, defaultValue }) => {
|
|
120
|
-
values.push({
|
|
121
|
-
modelId,
|
|
122
|
-
value: defaultValue,
|
|
123
|
-
updatedAt: new Date(),
|
|
124
|
-
customFieldDefinitionId: id,
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
|
|
129
99
|
return Promise.all(values.map(async (value) => {
|
|
130
100
|
const [cfv] = await CustomFieldValue.upsert(value, {
|
|
131
101
|
transaction: options.transaction,
|
package/src/scopes/filter.ts
CHANGED
|
@@ -1,50 +1,26 @@
|
|
|
1
1
|
/* eslint-disable import/prefer-default-export */
|
|
2
|
-
import { Op
|
|
2
|
+
import { Op } from 'sequelize';
|
|
3
3
|
import { Sequelize } from 'sequelize-typescript';
|
|
4
4
|
import { customFields } from '@autofleet/common-types';
|
|
5
5
|
import { generateRandomString } from '../utils/helpers';
|
|
6
|
+
import type { CustomFieldOptions } from '../types';
|
|
7
|
+
import {
|
|
8
|
+
formatConditionsForEntries,
|
|
9
|
+
formatConditionsForValues,
|
|
10
|
+
getFilterCustomFieldsSubQuery,
|
|
11
|
+
getSortCustomFieldsSubQuery,
|
|
12
|
+
SubQueryType,
|
|
13
|
+
type ConditionValue,
|
|
14
|
+
type CustomFieldFilterOptions,
|
|
15
|
+
} from './helpers/filter.helpers';
|
|
6
16
|
|
|
7
17
|
const { CUSTOM_FIELDS_FILTER_SCOPE } = customFields;
|
|
8
18
|
|
|
9
|
-
/**
|
|
10
|
-
* Type representing possible condition values.
|
|
11
|
-
* Currently supporting strings and arrays of strings.
|
|
12
|
-
* More types to be added (TBA).
|
|
13
|
-
*/
|
|
14
|
-
type ConditionWithOperator = {
|
|
15
|
-
operator: string;
|
|
16
|
-
value: string;
|
|
17
|
-
};
|
|
18
|
-
export type ConditionValue = ConditionWithOperator | ConditionWithOperator[] | string | string[];
|
|
19
|
-
|
|
20
|
-
export type CustomFieldSort = {
|
|
21
|
-
field: string;
|
|
22
|
-
direction: 'ASC' | 'DESC';
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export type CustomFieldFilterOptions = {
|
|
26
|
-
where?: WhereOptions;
|
|
27
|
-
replacements?: Record<string, string>;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
19
|
type customFieldsFilterScopeParams = {
|
|
31
20
|
replacementsMap: Record<string, string>;
|
|
32
21
|
scopeValue: Record<string, ConditionValue>;
|
|
33
22
|
}
|
|
34
23
|
|
|
35
|
-
const isDate = (input: any): input is Date => input instanceof Date || Object.prototype.toString.call(input) === '[object Date]';
|
|
36
|
-
|
|
37
|
-
const castIfNeeded = (conditionValue: string): string => {
|
|
38
|
-
if (isDate(conditionValue)) {
|
|
39
|
-
return '::timestamp';
|
|
40
|
-
}
|
|
41
|
-
if (!Number.isNaN(Number(conditionValue))) {
|
|
42
|
-
return '::numeric';
|
|
43
|
-
}
|
|
44
|
-
return '';
|
|
45
|
-
};
|
|
46
|
-
const AND_DELIMETER = ' AND ';
|
|
47
|
-
|
|
48
24
|
/**
|
|
49
25
|
* A Sequelize scope for filtering models by custom fields.
|
|
50
26
|
* This scope builds a WHERE clause to be applied on the main query.
|
|
@@ -54,73 +30,30 @@ const AND_DELIMETER = ' AND ';
|
|
|
54
30
|
*/
|
|
55
31
|
export const customFieldsFilterScope = (
|
|
56
32
|
name: string,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
replacementsMap: replacements,
|
|
60
|
-
scopeValue: conditions,
|
|
61
|
-
}: customFieldsFilterScopeParams,
|
|
62
|
-
): CustomFieldFilterOptions => {
|
|
33
|
+
options?: Pick<CustomFieldOptions, 'useCustomFieldsEntries'>,
|
|
34
|
+
) => ({ replacementsMap: replacements, scopeValue: conditions }: customFieldsFilterScopeParams): CustomFieldFilterOptions => {
|
|
63
35
|
if (!conditions || Object.keys(conditions).length === 0) {
|
|
64
36
|
return {};
|
|
65
37
|
}
|
|
66
|
-
// Build the WHERE clause for custom field filtering
|
|
67
|
-
const conditionsStrings = Object.entries(conditions)
|
|
68
|
-
.map(
|
|
69
|
-
([key, condition]) => {
|
|
70
|
-
const replacemetKey = Object.keys(replacements).find(
|
|
71
|
-
(randomString) => replacements[randomString] === key,
|
|
72
|
-
);
|
|
73
|
-
if (!replacemetKey) return false;
|
|
74
38
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return ` :${valRandom} `;
|
|
86
|
-
}).join(',');
|
|
87
|
-
return `(custom_fields->> :${replacemetKey} ) IN ( ${values} )`;
|
|
88
|
-
}
|
|
89
|
-
return condition
|
|
90
|
-
.map((c) => {
|
|
91
|
-
const valRep = Object.keys(replacements).find(
|
|
92
|
-
(replacementKey) => replacements[replacementKey] === c.value,
|
|
93
|
-
);
|
|
94
|
-
return `(custom_fields->> :${replacemetKey} )${castIfNeeded(c.value)} ${c.operator} :${valRep}`;
|
|
95
|
-
}).join(AND_DELIMETER);
|
|
96
|
-
}
|
|
97
|
-
if (typeof condition === 'string' || typeof condition === 'number') {
|
|
98
|
-
const conditionRep = Object.keys(replacements).find(
|
|
99
|
-
(replacementKey) => replacements[replacementKey] === condition,
|
|
100
|
-
);
|
|
101
|
-
return `(custom_fields->> :${replacemetKey} ) ${castIfNeeded(condition)} = :${conditionRep}`;
|
|
102
|
-
}
|
|
103
|
-
if (condition?.operator) {
|
|
104
|
-
const valueRep = Object.keys(replacements).find(
|
|
105
|
-
(replacementKey) => replacements[replacementKey] === condition.value,
|
|
106
|
-
);
|
|
107
|
-
return `(custom_fields->> :${replacemetKey} ) ${castIfNeeded(condition.value)} ${condition.operator} :${valueRep}`;
|
|
108
|
-
}
|
|
39
|
+
const queryType = options?.useCustomFieldsEntries ? SubQueryType.ENTRIES : SubQueryType.VALUES;
|
|
40
|
+
const reverseReplacementsMap = new Map(Object.entries(replacements).map(([key, value]) => [value, key]));
|
|
41
|
+
// Build the WHERE clause for custom field filtering
|
|
42
|
+
const conditionsStrings = Object.entries(conditions).map(([key, condition]) => {
|
|
43
|
+
switch (queryType) {
|
|
44
|
+
case SubQueryType.ENTRIES:
|
|
45
|
+
return formatConditionsForEntries(key, condition, reverseReplacementsMap);
|
|
46
|
+
case SubQueryType.VALUES:
|
|
47
|
+
return formatConditionsForValues(key, condition, reverseReplacementsMap);
|
|
48
|
+
default:
|
|
109
49
|
return false;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
.filter(Boolean);
|
|
50
|
+
}
|
|
51
|
+
}).filter(Boolean);
|
|
113
52
|
if (conditionsStrings.length === 0) {
|
|
114
53
|
return {};
|
|
115
54
|
}
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
+ 'SELECT cv.model_id, jsonb_object_agg(cd.name, cv.value) AS custom_fields '
|
|
119
|
-
+ 'FROM custom_field_values AS cv '
|
|
120
|
-
+ 'INNER JOIN custom_field_definitions AS cd ON cv.custom_field_definition_id = cd.id '
|
|
121
|
-
+ `AND cd.model_type = '${name}'`
|
|
122
|
-
+ 'GROUP BY cv.model_id'
|
|
123
|
-
+ ') AS CustomFieldAggregation WHERE '} ${customFieldConditions}`;
|
|
55
|
+
const subQuery = getFilterCustomFieldsSubQuery(queryType, name, conditionsStrings);
|
|
56
|
+
|
|
124
57
|
return {
|
|
125
58
|
where: {
|
|
126
59
|
id: {
|
|
@@ -135,27 +68,21 @@ export const scopeName = CUSTOM_FIELDS_FILTER_SCOPE;
|
|
|
135
68
|
|
|
136
69
|
export const customFieldsSortScope = (
|
|
137
70
|
name: string,
|
|
71
|
+
options?: Pick<CustomFieldOptions, 'useCustomFieldsEntries'>,
|
|
138
72
|
) => ({ replacementsMap, scopeValue: sort }) => {
|
|
139
73
|
if (!sort || sort.length === 0) {
|
|
140
74
|
return {};
|
|
141
75
|
}
|
|
76
|
+
|
|
77
|
+
const queryType = options?.useCustomFieldsEntries ? SubQueryType.ENTRIES : SubQueryType.VALUES;
|
|
142
78
|
const randomStr = generateRandomString();
|
|
143
79
|
const includes = Object.entries(sort).map(([key]) => {
|
|
144
|
-
const
|
|
80
|
+
const replacementKey = Object.keys(replacementsMap).find(
|
|
145
81
|
(randomString) => replacementsMap[randomString] === key,
|
|
146
82
|
);
|
|
147
83
|
return ([
|
|
148
|
-
Sequelize.literal(
|
|
149
|
-
|
|
150
|
-
FROM (SELECT cv.model_id, cv.value
|
|
151
|
-
FROM custom_field_values AS cv INNER JOIN custom_field_definitions AS cd
|
|
152
|
-
ON cv.custom_field_definition_id = cd.id
|
|
153
|
-
AND cd.model_type = '${name}'
|
|
154
|
-
WHERE cv.model_id = "${name}"."id"
|
|
155
|
-
AND cd.name = :${replacemetKey}
|
|
156
|
-
) AS CustomFieldAggregation
|
|
157
|
-
)
|
|
158
|
-
`), randomStr,
|
|
84
|
+
Sequelize.literal(getSortCustomFieldsSubQuery(queryType, name, replacementKey)),
|
|
85
|
+
randomStr,
|
|
159
86
|
]);
|
|
160
87
|
});
|
|
161
88
|
|