@autofleet/sadot 0.13.2-beta.1 → 0.13.2-beta.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hooks/enrich.d.ts +4 -9
- package/dist/hooks/enrich.js +45 -11
- package/dist/hooks/hooks.js +29 -17
- package/dist/models/index.js +5 -6
- package/dist/repository/validator.d.ts +1 -0
- package/dist/types/index.d.ts +11 -1
- package/package.json +1 -1
- package/src/hooks/enrich.ts +60 -22
- package/src/hooks/hooks.ts +114 -110
- package/src/models/index.ts +7 -8
- package/src/repository/validator.ts +1 -0
- package/src/types/index.ts +12 -1
package/dist/hooks/enrich.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type { Transaction } from 'sequelize';
|
|
2
1
|
import type CustomFieldValue from '../models/CustomFieldValue';
|
|
3
|
-
import type { CustomFieldOptions, ModelOptions } from '../types';
|
|
2
|
+
import type { CustomFieldOptions, ModelOptions, TransactionOptions } from '../types';
|
|
4
3
|
type SupportedHookTypes = 'afterFind' | 'afterCreate' | 'afterUpdate';
|
|
5
4
|
type CustomFieldEntries = Record<string, any>;
|
|
6
5
|
interface GetValuesGroupByInstanceResponse {
|
|
@@ -11,20 +10,16 @@ interface GetCustomFieldEntriesByInstanceIdResponse {
|
|
|
11
10
|
}
|
|
12
11
|
export declare const getCustomFieldEntriesByInstanceId: ({ instancesIds, options, sadotOptions, }: {
|
|
13
12
|
instancesIds: string[];
|
|
14
|
-
options?:
|
|
15
|
-
transaction: Transaction;
|
|
16
|
-
};
|
|
13
|
+
options?: TransactionOptions;
|
|
17
14
|
sadotOptions: Pick<CustomFieldOptions, 'useCustomFieldsEntries'>;
|
|
18
15
|
}) => Promise<GetCustomFieldEntriesByInstanceIdResponse>;
|
|
19
16
|
export declare const getValuesGroupByInstance: ({ instancesIds, options, sadotOptions, }: {
|
|
20
17
|
instancesIds: string[];
|
|
21
|
-
options?:
|
|
22
|
-
transaction: Transaction;
|
|
23
|
-
};
|
|
18
|
+
options?: TransactionOptions;
|
|
24
19
|
sadotOptions: Pick<CustomFieldOptions, 'useCustomFieldsEntries'>;
|
|
25
20
|
}) => Promise<GetValuesGroupByInstanceResponse>;
|
|
26
21
|
/**
|
|
27
22
|
* A hook to attach the custom fields when fetching a model instances.
|
|
28
23
|
*/
|
|
29
|
-
declare const enrichResults: (modelType: string, scopeAttributes: string[], hookType?: SupportedHookTypes, modelOptions?: ModelOptions, sadotOptions?: Pick<CustomFieldOptions, 'useCustomFieldsEntries'>) => (instancesOrInstance: any | any[], options:
|
|
24
|
+
declare const enrichResults: (modelType: string, scopeAttributes: string[], hookType?: SupportedHookTypes, modelOptions?: ModelOptions, sadotOptions?: Pick<CustomFieldOptions, 'useCustomFieldsEntries'>) => (instancesOrInstance: any | any[], options: TransactionOptions) => Promise<void>;
|
|
30
25
|
export default enrichResults;
|
package/dist/hooks/enrich.js
CHANGED
|
@@ -27,10 +27,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
27
27
|
};
|
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
29
|
exports.getValuesGroupByInstance = exports.getCustomFieldEntriesByInstanceId = void 0;
|
|
30
|
+
/* eslint-disable no-param-reassign */
|
|
30
31
|
const ValueRepo = __importStar(require("../repository/value"));
|
|
31
32
|
const DefinitionRepo = __importStar(require("../repository/definition"));
|
|
32
33
|
const EntriesRepo = __importStar(require("../repository/entries"));
|
|
33
34
|
const scopeAttributes_1 = __importDefault(require("../utils/scopeAttributes"));
|
|
35
|
+
const CUSTOM_FIELD_DEFINITION_ATTRIBUTES_TO_PULL = ['id', 'name', 'entityId'];
|
|
34
36
|
const getCustomFieldEntriesByInstanceId = async ({ instancesIds, options, sadotOptions, }) => {
|
|
35
37
|
if (!sadotOptions.useCustomFieldsEntries) {
|
|
36
38
|
return {};
|
|
@@ -93,7 +95,37 @@ const enrichResults = (modelType, scopeAttributes, hookType, modelOptions = {},
|
|
|
93
95
|
...map,
|
|
94
96
|
[identifier]: [],
|
|
95
97
|
}), {});
|
|
96
|
-
|
|
98
|
+
// Cache for definitions by model type and transaction to avoid redundant DB queries
|
|
99
|
+
let customFieldDefinitionsPromise;
|
|
100
|
+
let cacheKey;
|
|
101
|
+
// Check if caching is disabled via environment variable
|
|
102
|
+
const cachingDisabled = process.env.SADOT_DISABLE_DEFINITION_CACHE === 'true';
|
|
103
|
+
if (options.transaction && !cachingDisabled) {
|
|
104
|
+
// Initialize definition cache Map if not already present directly on the transaction object
|
|
105
|
+
options.transaction.definitionCache = options.transaction.definitionCache || new Map();
|
|
106
|
+
// Create a unique cache key combining model type and sorted identifiers
|
|
107
|
+
cacheKey = `${modelType}:${uniqueIdentifiers.sort().join(',')}`;
|
|
108
|
+
// Check if these definitions are already cached for this transaction
|
|
109
|
+
if (options.transaction.definitionCache.has(cacheKey)) {
|
|
110
|
+
// Use cached result to avoid duplicate database queries
|
|
111
|
+
customFieldDefinitionsPromise = options.transaction.definitionCache.get(cacheKey);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (!customFieldDefinitionsPromise) {
|
|
115
|
+
// Fetch from database (either first time in this transaction or no transaction)
|
|
116
|
+
customFieldDefinitionsPromise = DefinitionRepo.findByEntityIds(modelType, uniqueIdentifiers, { transaction: options.transaction, modelOptions, attributes: CUSTOM_FIELD_DEFINITION_ATTRIBUTES_TO_PULL });
|
|
117
|
+
// Store in cache if within a transaction and caching isn't disabled
|
|
118
|
+
if (options.transaction && options.transaction.definitionCache) {
|
|
119
|
+
options.transaction.definitionCache.set(cacheKey, customFieldDefinitionsPromise);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const customFieldDefinitions = await customFieldDefinitionsPromise;
|
|
123
|
+
if (customFieldDefinitions.length === 0) {
|
|
124
|
+
// if no custom fields, we can return
|
|
125
|
+
instances.forEach((instance) => {
|
|
126
|
+
instance.customFields = {};
|
|
127
|
+
});
|
|
128
|
+
}
|
|
97
129
|
if (modelOptions?.include && modelOptions.useEntityIdFromInclude) {
|
|
98
130
|
// if we pass useEntityIdFromInclude,
|
|
99
131
|
// map the entity from the options to the identifierCustomFieldDefinitionsMapping
|
|
@@ -114,16 +146,18 @@ const enrichResults = (modelType, scopeAttributes, hookType, modelOptions = {},
|
|
|
114
146
|
// Get the values per instates ids:
|
|
115
147
|
const instancesIds = instances.map((i) => i[primaryKey]);
|
|
116
148
|
// Group fields by modelId
|
|
117
|
-
const valuesGroupByInstance = await
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
149
|
+
const [valuesGroupByInstance, customFieldEntriesByInstanceId] = await Promise.all([
|
|
150
|
+
(0, exports.getValuesGroupByInstance)({
|
|
151
|
+
instancesIds,
|
|
152
|
+
options,
|
|
153
|
+
sadotOptions,
|
|
154
|
+
}),
|
|
155
|
+
(0, exports.getCustomFieldEntriesByInstanceId)({
|
|
156
|
+
instancesIds,
|
|
157
|
+
options,
|
|
158
|
+
sadotOptions,
|
|
159
|
+
}),
|
|
160
|
+
]);
|
|
127
161
|
// Attach custom fields to the instances
|
|
128
162
|
instances.forEach((instance) => {
|
|
129
163
|
const { id } = instance;
|
package/dist/hooks/hooks.js
CHANGED
|
@@ -28,6 +28,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
29
|
exports.beforeBulkUpdate = exports.beforeBulkCreate = exports.beforeUpdate = exports.beforeCreate = void 0;
|
|
30
30
|
const ajv_1 = __importDefault(require("ajv"));
|
|
31
|
+
const joi_1 = __importDefault(require("joi"));
|
|
31
32
|
const ajv_formats_1 = __importDefault(require("ajv-formats"));
|
|
32
33
|
const errors_1 = require("@autofleet/errors");
|
|
33
34
|
const logger_1 = __importDefault(require("../utils/logger"));
|
|
@@ -105,7 +106,24 @@ const validateModel = async (instance, options, scopeAttributes, isCreate = fals
|
|
|
105
106
|
logger_1.default.debug('sadot - skipping validation: no entityId');
|
|
106
107
|
return;
|
|
107
108
|
}
|
|
108
|
-
|
|
109
|
+
let validatorsPromise;
|
|
110
|
+
let cacheKey;
|
|
111
|
+
if (options.transaction) {
|
|
112
|
+
// eslint-disable-next-line no-param-reassign
|
|
113
|
+
options.transaction.validationsCache = options.transaction.validationsCache || new Map();
|
|
114
|
+
cacheKey = `${modelType}-${entityId}`;
|
|
115
|
+
validatorsPromise = options.transaction.validationsCache.get(cacheKey);
|
|
116
|
+
}
|
|
117
|
+
if (!validatorsPromise) {
|
|
118
|
+
validatorsPromise = ValidatorRepo.findAllByModelType(modelType, entityId, {
|
|
119
|
+
transaction: options.transaction,
|
|
120
|
+
attributes: ['schema'],
|
|
121
|
+
});
|
|
122
|
+
if (options.transaction) {
|
|
123
|
+
options.transaction.validationsCache.set(cacheKey, validatorsPromise);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const validators = await validatorsPromise;
|
|
109
127
|
logger_1.default.debug('sadot - validators found', { count: validators.length });
|
|
110
128
|
if (!validators.length) {
|
|
111
129
|
logger_1.default.debug('sadot - skipping validation: no validators found');
|
|
@@ -124,16 +142,6 @@ const validateModel = async (instance, options, scopeAttributes, isCreate = fals
|
|
|
124
142
|
const completeCustomFields = !isCreate
|
|
125
143
|
? await getCompleteCustomFields(instance, options)
|
|
126
144
|
: instance.customFields || {};
|
|
127
|
-
// For debugging in case of update
|
|
128
|
-
if (!isCreate && process.env.NODE_ENV !== 'production') {
|
|
129
|
-
// Create after object for logging
|
|
130
|
-
const logAfterObj = manualObjectCopy(instance.dataValues, { customFields: completeCustomFields });
|
|
131
|
-
logger_1.default.debug('sadot - validate with values', {
|
|
132
|
-
before: originalValues,
|
|
133
|
-
after: logAfterObj,
|
|
134
|
-
schema: validators[0].schema,
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
145
|
// eslint-disable-next-line no-restricted-syntax
|
|
138
146
|
for (const validator of validators) {
|
|
139
147
|
const { schema } = validator;
|
|
@@ -196,7 +204,7 @@ const validateModel = async (instance, options, scopeAttributes, isCreate = fals
|
|
|
196
204
|
}
|
|
197
205
|
}
|
|
198
206
|
};
|
|
199
|
-
const getFieldDefinitions = async ({ modelType, modelOptions, identifiers, options }) => {
|
|
207
|
+
const getFieldDefinitions = async ({ modelType, modelOptions, identifiers, options, }) => {
|
|
200
208
|
const { include, useEntityIdFromInclude } = modelOptions;
|
|
201
209
|
const where = {
|
|
202
210
|
modelType,
|
|
@@ -210,14 +218,18 @@ const getFieldDefinitions = async ({ modelType, modelOptions, identifiers, optio
|
|
|
210
218
|
});
|
|
211
219
|
return fieldDefinitions;
|
|
212
220
|
};
|
|
213
|
-
const formatDates = (fieldDefinitions
|
|
214
|
-
fieldDefinitions.forEach((fieldDefinition) => {
|
|
221
|
+
const formatDates = (fieldDefinitions, instance) => {
|
|
222
|
+
(fieldDefinitions || []).forEach((fieldDefinition) => {
|
|
215
223
|
const { fieldType, name } = fieldDefinition;
|
|
216
224
|
if ([constants_1.CustomFieldDefinitionType.DATE, constants_1.CustomFieldDefinitionType.DATETIME].includes(fieldType)) {
|
|
217
225
|
const value = instance.customFields?.[name];
|
|
218
226
|
if (value) {
|
|
227
|
+
const { value: joiValue, error: validationError } = joi_1.default.date().validate(value);
|
|
228
|
+
if (validationError) {
|
|
229
|
+
throw new errors_2.InvalidValueError(value, name, validationError);
|
|
230
|
+
}
|
|
219
231
|
// eslint-disable-next-line no-param-reassign
|
|
220
|
-
instance.customFields[name] =
|
|
232
|
+
instance.customFields[name] = joiValue.toISOString();
|
|
221
233
|
}
|
|
222
234
|
}
|
|
223
235
|
});
|
|
@@ -232,7 +244,7 @@ const beforeCreate = (scopeAttributes, modelOptions = {}, sadotOptions = { useCu
|
|
|
232
244
|
const identifiers = (0, scopeAttributes_1.default)(instance, scopeAttributes);
|
|
233
245
|
// Step 1: Handle custom fields default values and required fields
|
|
234
246
|
const fieldDefinitions = await getFieldDefinitions({
|
|
235
|
-
modelType, modelOptions, identifiers, options
|
|
247
|
+
modelType, modelOptions, identifiers, options,
|
|
236
248
|
});
|
|
237
249
|
// Apply default values
|
|
238
250
|
const fieldsWithDefaultValue = fieldDefinitions.filter((def) => ![null, undefined].includes(def.defaultValue));
|
|
@@ -290,7 +302,7 @@ const beforeUpdate = (scopeAttributes, modelOptions = {}, sadotOptions = { useCu
|
|
|
290
302
|
const modelType = instance.constructor.name;
|
|
291
303
|
const identifiers = (0, scopeAttributes_1.default)(instance, scopeAttributes);
|
|
292
304
|
const fieldDefinitions = await getFieldDefinitions({
|
|
293
|
-
modelType, modelOptions, identifiers, options
|
|
305
|
+
modelType, modelOptions, identifiers, options,
|
|
294
306
|
});
|
|
295
307
|
// Step 1: Validate the model data (including custom fields)
|
|
296
308
|
await validateModel(instance, options, scopeAttributes, false);
|
package/dist/models/index.js
CHANGED
|
@@ -26,12 +26,8 @@ exports.CustomValidator = CustomValidator_1.default;
|
|
|
26
26
|
const productionModels = [CustomFieldDefinition_1.default, CustomFieldValue_1.default, CustomValidator_1.default];
|
|
27
27
|
const testModels = [TestModel_1.default, AssociatedTestModel_1.default, ContextAwareTestModel_1.default, ContextTestModel_1.default];
|
|
28
28
|
const SADOT_MIGRATION_PREFIX = 'sadot-migration';
|
|
29
|
-
const SCHEMA_VERSION = '
|
|
30
|
-
const initTables = async (sequelize, getUser, { schemaPrefix, schemaVersion, useCustomFieldsEntries, } = {
|
|
31
|
-
schemaPrefix: SADOT_MIGRATION_PREFIX,
|
|
32
|
-
schemaVersion: SCHEMA_VERSION,
|
|
33
|
-
useCustomFieldsEntries: false,
|
|
34
|
-
}) => {
|
|
29
|
+
const SCHEMA_VERSION = '49c9dd1d-b1cc-445b-a911-fd349d783110';
|
|
30
|
+
const initTables = async (sequelize, getUser, { schemaPrefix = SADOT_MIGRATION_PREFIX, schemaVersion = SCHEMA_VERSION, useCustomFieldsEntries = false, } = {}) => {
|
|
35
31
|
const CUSTOM_FIELDS_SCHEMA_VERSION = `${schemaPrefix}_${schemaVersion}${useCustomFieldsEntries ? '_withEntries' : ''}`;
|
|
36
32
|
logger_1.default.info('custom-fields: initialize custom-fields tables');
|
|
37
33
|
// Detect models and import them to the orm
|
|
@@ -87,10 +83,13 @@ const initTables = async (sequelize, getUser, { schemaPrefix, schemaVersion, use
|
|
|
87
83
|
timestamps: false,
|
|
88
84
|
schema: 'public',
|
|
89
85
|
});
|
|
86
|
+
logger_1.default.info('custom-fields: starting migrations');
|
|
90
87
|
const migrations = await SequelizeMeta.findAll({ where: { name: { [sequelize_1.Op.like]: `${schemaPrefix}%` } }, raw: true });
|
|
91
88
|
const currentSadotSchemaVersion = migrations.at(-1);
|
|
92
89
|
const expectedSchemaVersionIndex = migrations.findIndex((m) => m.name === CUSTOM_FIELDS_SCHEMA_VERSION);
|
|
90
|
+
logger_1.default.info('custom-fields: migrations', { migrations, currentSadotSchemaVersion, expectedSchemaVersionIndex });
|
|
93
91
|
if (!currentSadotSchemaVersion || currentSadotSchemaVersion.name !== CUSTOM_FIELDS_SCHEMA_VERSION) {
|
|
92
|
+
logger_1.default.info('custom-fields: syncing models');
|
|
94
93
|
await CustomFieldDefinition_1.default.sync({ alter: true });
|
|
95
94
|
await CustomFieldValue_1.default.sync({ alter: true });
|
|
96
95
|
// T.Y TODO: Remove the if statement once we're ready to add the new entries table for all MS
|
|
@@ -2,6 +2,7 @@ import type { Transactionable } from 'sequelize';
|
|
|
2
2
|
import { CustomValidator } from '../models';
|
|
3
3
|
export interface FindValidatorOptions extends Transactionable {
|
|
4
4
|
withDisabled?: boolean;
|
|
5
|
+
attributes?: string[];
|
|
5
6
|
}
|
|
6
7
|
export interface ValidatorAttributes {
|
|
7
8
|
entityId: string;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import type { IncludeOptions } from 'sequelize';
|
|
1
|
+
import type { IncludeOptions, Transaction } from 'sequelize';
|
|
2
2
|
import type { ModelCtor, Sequelize } from 'sequelize-typescript';
|
|
3
3
|
import type { getUser as GetUserType } from '@autofleet/zehut';
|
|
4
|
+
import type CustomFieldDefinition from '../models/CustomFieldDefinition';
|
|
4
5
|
export type ModelFetcher = (name: string) => any;
|
|
6
|
+
export interface TransactionOptions extends Record<string, any> {
|
|
7
|
+
transaction?: Transaction & {
|
|
8
|
+
definitionCache?: Map<string, CustomFieldDefinition[]>;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
5
11
|
export type ModelOptions = {
|
|
6
12
|
/**
|
|
7
13
|
* Include options for the model
|
|
@@ -16,6 +22,10 @@ export type ModelOptions = {
|
|
|
16
22
|
* Whether to use the entity id from the instance per scope attribute
|
|
17
23
|
*/
|
|
18
24
|
useEntityIdFromInclude?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* Which attributes to include in the model
|
|
27
|
+
*/
|
|
28
|
+
attributes?: string[];
|
|
19
29
|
};
|
|
20
30
|
export type Models = {
|
|
21
31
|
name: string;
|
package/package.json
CHANGED
package/src/hooks/enrich.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/* eslint-disable no-param-reassign */
|
|
2
|
-
import type { Transaction } from 'sequelize';
|
|
3
2
|
import * as ValueRepo from '../repository/value';
|
|
4
3
|
import * as DefinitionRepo from '../repository/definition';
|
|
5
4
|
import * as EntriesRepo from '../repository/entries';
|
|
6
5
|
import type CustomFieldValue from '../models/CustomFieldValue';
|
|
7
6
|
import type CustomFieldDefinition from '../models/CustomFieldDefinition';
|
|
8
7
|
import type { SerializedCustomFields } from '../types/definition';
|
|
9
|
-
import type { CustomFieldOptions, ModelOptions } from '../types';
|
|
8
|
+
import type { CustomFieldOptions, ModelOptions, TransactionOptions } from '../types';
|
|
10
9
|
import applyScopeToInstance from '../utils/scopeAttributes';
|
|
11
10
|
|
|
11
|
+
const CUSTOM_FIELD_DEFINITION_ATTRIBUTES_TO_PULL = ['id', 'name', 'entityId'];
|
|
12
|
+
|
|
12
13
|
type SupportedHookTypes = 'afterFind' | 'afterCreate' | 'afterUpdate';
|
|
13
14
|
|
|
14
15
|
type CustomFieldEntries = Record<string, any>;
|
|
@@ -27,7 +28,7 @@ export const getCustomFieldEntriesByInstanceId = async ({
|
|
|
27
28
|
sadotOptions,
|
|
28
29
|
}: {
|
|
29
30
|
instancesIds: string[],
|
|
30
|
-
options?:
|
|
31
|
+
options?: TransactionOptions,
|
|
31
32
|
sadotOptions: Pick<CustomFieldOptions, 'useCustomFieldsEntries'>,
|
|
32
33
|
}): Promise<GetCustomFieldEntriesByInstanceIdResponse> => {
|
|
33
34
|
if (!sadotOptions.useCustomFieldsEntries) {
|
|
@@ -60,7 +61,7 @@ export const getValuesGroupByInstance = async ({
|
|
|
60
61
|
sadotOptions,
|
|
61
62
|
}: {
|
|
62
63
|
instancesIds: string[],
|
|
63
|
-
options?:
|
|
64
|
+
options?: TransactionOptions,
|
|
64
65
|
sadotOptions: Pick<CustomFieldOptions, 'useCustomFieldsEntries'>,
|
|
65
66
|
}): Promise<GetValuesGroupByInstanceResponse> => {
|
|
66
67
|
if (sadotOptions.useCustomFieldsEntries) {
|
|
@@ -108,7 +109,7 @@ const enrichResults = (
|
|
|
108
109
|
sadotOptions: Pick<CustomFieldOptions, 'useCustomFieldsEntries'> = { useCustomFieldsEntries: false },
|
|
109
110
|
) => async (
|
|
110
111
|
instancesOrInstance: any | any[],
|
|
111
|
-
options,
|
|
112
|
+
options: TransactionOptions,
|
|
112
113
|
): Promise<void> => {
|
|
113
114
|
if (
|
|
114
115
|
options.originalAttributes?.length > 0
|
|
@@ -131,14 +132,50 @@ const enrichResults = (
|
|
|
131
132
|
const identifierCustomFieldDefinitionsMapping = uniqueIdentifiers.reduce((map, identifier) => ({
|
|
132
133
|
...map,
|
|
133
134
|
[identifier]: [],
|
|
134
|
-
|
|
135
135
|
}), {});
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
137
|
+
// Cache for definitions by model type and transaction to avoid redundant DB queries
|
|
138
|
+
let customFieldDefinitionsPromise;
|
|
139
|
+
let cacheKey;
|
|
140
|
+
|
|
141
|
+
// Check if caching is disabled via environment variable
|
|
142
|
+
const cachingDisabled = process.env.SADOT_DISABLE_DEFINITION_CACHE === 'true';
|
|
143
|
+
|
|
144
|
+
if (options.transaction && !cachingDisabled) {
|
|
145
|
+
// Initialize definition cache Map if not already present directly on the transaction object
|
|
146
|
+
options.transaction.definitionCache = options.transaction.definitionCache || new Map();
|
|
147
|
+
|
|
148
|
+
// Create a unique cache key combining model type and sorted identifiers
|
|
149
|
+
cacheKey = `${modelType}:${uniqueIdentifiers.sort().join(',')}`;
|
|
150
|
+
|
|
151
|
+
// Check if these definitions are already cached for this transaction
|
|
152
|
+
if (options.transaction.definitionCache.has(cacheKey)) {
|
|
153
|
+
// Use cached result to avoid duplicate database queries
|
|
154
|
+
customFieldDefinitionsPromise = options.transaction.definitionCache.get(cacheKey);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!customFieldDefinitionsPromise) {
|
|
159
|
+
// Fetch from database (either first time in this transaction or no transaction)
|
|
160
|
+
customFieldDefinitionsPromise = DefinitionRepo.findByEntityIds(
|
|
161
|
+
modelType,
|
|
162
|
+
uniqueIdentifiers,
|
|
163
|
+
{ transaction: options.transaction, modelOptions, attributes: CUSTOM_FIELD_DEFINITION_ATTRIBUTES_TO_PULL },
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
// Store in cache if within a transaction and caching isn't disabled
|
|
167
|
+
if (options.transaction && options.transaction.definitionCache) {
|
|
168
|
+
options.transaction.definitionCache.set(cacheKey, customFieldDefinitionsPromise);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
const customFieldDefinitions = await customFieldDefinitionsPromise;
|
|
172
|
+
|
|
173
|
+
if (customFieldDefinitions.length === 0) {
|
|
174
|
+
// if no custom fields, we can return
|
|
175
|
+
instances.forEach((instance) => {
|
|
176
|
+
instance.customFields = {};
|
|
177
|
+
});
|
|
178
|
+
}
|
|
142
179
|
|
|
143
180
|
if (modelOptions?.include && modelOptions.useEntityIdFromInclude) {
|
|
144
181
|
// if we pass useEntityIdFromInclude,
|
|
@@ -164,17 +201,18 @@ const enrichResults = (
|
|
|
164
201
|
const instancesIds = instances.map((i) => i[primaryKey]);
|
|
165
202
|
|
|
166
203
|
// Group fields by modelId
|
|
167
|
-
const valuesGroupByInstance = await
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
204
|
+
const [valuesGroupByInstance, customFieldEntriesByInstanceId] = await Promise.all([
|
|
205
|
+
getValuesGroupByInstance({
|
|
206
|
+
instancesIds,
|
|
207
|
+
options,
|
|
208
|
+
sadotOptions,
|
|
209
|
+
}),
|
|
210
|
+
getCustomFieldEntriesByInstanceId({
|
|
211
|
+
instancesIds,
|
|
212
|
+
options,
|
|
213
|
+
sadotOptions,
|
|
214
|
+
}),
|
|
215
|
+
]);
|
|
178
216
|
|
|
179
217
|
// Attach custom fields to the instances
|
|
180
218
|
instances.forEach((instance) => {
|
package/src/hooks/hooks.ts
CHANGED
|
@@ -105,11 +105,29 @@ const validateModel = async (
|
|
|
105
105
|
return;
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
let validatorsPromise;
|
|
109
|
+
let cacheKey;
|
|
110
|
+
if (options.transaction) {
|
|
111
|
+
// eslint-disable-next-line no-param-reassign
|
|
112
|
+
options.transaction.validationsCache = options.transaction.validationsCache || new Map();
|
|
113
|
+
cacheKey = `${modelType}-${entityId}`;
|
|
114
|
+
validatorsPromise = options.transaction.validationsCache.get(cacheKey);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!validatorsPromise) {
|
|
118
|
+
validatorsPromise = ValidatorRepo.findAllByModelType(
|
|
119
|
+
modelType,
|
|
120
|
+
entityId,
|
|
121
|
+
{
|
|
122
|
+
transaction: options.transaction,
|
|
123
|
+
attributes: ['schema'],
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
if (options.transaction) {
|
|
127
|
+
options.transaction.validationsCache.set(cacheKey, validatorsPromise);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const validators = await validatorsPromise;
|
|
113
131
|
|
|
114
132
|
logger.debug('sadot - validators found', { count: validators.length });
|
|
115
133
|
|
|
@@ -134,17 +152,6 @@ const validateModel = async (
|
|
|
134
152
|
? await getCompleteCustomFields(instance, options)
|
|
135
153
|
: instance.customFields || {};
|
|
136
154
|
|
|
137
|
-
// For debugging in case of update
|
|
138
|
-
if (!isCreate && process.env.NODE_ENV !== 'production') {
|
|
139
|
-
// Create after object for logging
|
|
140
|
-
const logAfterObj = manualObjectCopy(instance.dataValues, { customFields: completeCustomFields });
|
|
141
|
-
|
|
142
|
-
logger.debug('sadot - validate with values', {
|
|
143
|
-
before: originalValues,
|
|
144
|
-
after: logAfterObj,
|
|
145
|
-
schema: validators[0].schema,
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
155
|
|
|
149
156
|
// eslint-disable-next-line no-restricted-syntax
|
|
150
157
|
for (const validator of validators) {
|
|
@@ -258,10 +265,7 @@ const formatDates = (fieldDefinitions: CustomFieldDefinition[], instance: any) =
|
|
|
258
265
|
throw new InvalidValueError(value, name, validationError);
|
|
259
266
|
}
|
|
260
267
|
// eslint-disable-next-line no-param-reassign
|
|
261
|
-
const typeofjoi = typeof joiValue;
|
|
262
|
-
logger.info('sadot - formatting date', { name, value, joiValue, type: typeof joiValue });
|
|
263
268
|
instance.customFields[name] = joiValue.toISOString();
|
|
264
|
-
new Date().toString
|
|
265
269
|
}
|
|
266
270
|
}
|
|
267
271
|
});
|
|
@@ -278,72 +282,72 @@ export const beforeCreate = (
|
|
|
278
282
|
instance,
|
|
279
283
|
options,
|
|
280
284
|
): Promise<void> => {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
285
|
+
logger.debug('sadot - before create hook');
|
|
286
|
+
const { fields } = options;
|
|
287
|
+
const modelType = instance.constructor.name;
|
|
284
288
|
|
|
285
|
-
|
|
289
|
+
const identifiers = applyScopeToInstance(instance, scopeAttributes);
|
|
286
290
|
|
|
287
|
-
|
|
291
|
+
// Step 1: Handle custom fields default values and required fields
|
|
288
292
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
293
|
+
const fieldDefinitions = await getFieldDefinitions({
|
|
294
|
+
modelType, modelOptions, identifiers, options,
|
|
295
|
+
});
|
|
292
296
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
297
|
+
// Apply default values
|
|
298
|
+
const fieldsWithDefaultValue = fieldDefinitions.filter((def) => ![null, undefined].includes(def.defaultValue));
|
|
299
|
+
if (fieldsWithDefaultValue.length) {
|
|
300
|
+
// eslint-disable-next-line no-param-reassign
|
|
301
|
+
instance.customFields ||= {};
|
|
302
|
+
fieldsWithDefaultValue
|
|
303
|
+
.filter((def) => (instance.customFields?.[def.name] === undefined))
|
|
304
|
+
.forEach(({ name, defaultValue }) => {
|
|
305
|
+
// eslint-disable-next-line no-param-reassign
|
|
306
|
+
instance.customFields[name] = defaultValue;
|
|
307
|
+
});
|
|
308
|
+
}
|
|
305
309
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
310
|
+
// Check for required fields
|
|
311
|
+
const requiredFieldsNames = Array.from(
|
|
312
|
+
new Set(fieldDefinitions.filter(({ required }) => required).map(({ name }) => name)),
|
|
313
|
+
);
|
|
314
|
+
const { customFields } = instance;
|
|
315
|
+
const fieldsNames = Object.keys(customFields ?? {});
|
|
316
|
+
const missingFields = requiredFieldsNames.filter((name) => !fieldsNames.includes(name));
|
|
317
|
+
if (missingFields?.length) {
|
|
318
|
+
throw new MissingRequiredCustomFieldError(missingFields);
|
|
319
|
+
}
|
|
316
320
|
|
|
317
|
-
|
|
318
|
-
|
|
321
|
+
// Step 2: Validate the model data (including custom fields)
|
|
322
|
+
await validateModel(instance, options, scopeAttributes, true);
|
|
319
323
|
|
|
320
|
-
|
|
321
|
-
|
|
324
|
+
// format date and datetime fields
|
|
325
|
+
formatDates(fieldDefinitions, instance);
|
|
322
326
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
327
|
+
// Step 3: Save custom field values if they exist
|
|
328
|
+
const customFieldsIdx = fields.indexOf('customFields');
|
|
329
|
+
if (customFieldsIdx === -1 || !customFields || !Object.keys(customFields).length) {
|
|
330
|
+
// No custom fields to update
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
329
333
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
334
|
+
// Save custom field values
|
|
335
|
+
await updateInstanceValues({
|
|
336
|
+
modelId: instance.id,
|
|
337
|
+
modelType,
|
|
338
|
+
identifiers,
|
|
339
|
+
customFields,
|
|
340
|
+
options: {
|
|
341
|
+
useCustomFieldsEntries: sadotOptions.useCustomFieldsEntries,
|
|
342
|
+
transaction: options.transaction,
|
|
343
|
+
modelOptions,
|
|
344
|
+
},
|
|
345
|
+
});
|
|
342
346
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
+
// Remove customFields from fields array after handling
|
|
348
|
+
// eslint-disable-next-line no-param-reassign
|
|
349
|
+
fields.splice(customFieldsIdx, 1);
|
|
350
|
+
};
|
|
347
351
|
|
|
348
352
|
/**
|
|
349
353
|
* Hook to handle validation and custom fields during update
|
|
@@ -356,48 +360,48 @@ export const beforeUpdate = (
|
|
|
356
360
|
instance,
|
|
357
361
|
options,
|
|
358
362
|
): Promise<void> => {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
+
logger.debug('sadot - before update hook');
|
|
364
|
+
const { fields } = options;
|
|
365
|
+
const modelType = instance.constructor.name;
|
|
366
|
+
const identifiers = applyScopeToInstance(instance, scopeAttributes);
|
|
363
367
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
368
|
+
const fieldDefinitions = await getFieldDefinitions({
|
|
369
|
+
modelType, modelOptions, identifiers, options,
|
|
370
|
+
});
|
|
367
371
|
|
|
368
|
-
|
|
369
|
-
|
|
372
|
+
// Step 1: Validate the model data (including custom fields)
|
|
373
|
+
await validateModel(instance, options, scopeAttributes, false);
|
|
370
374
|
|
|
371
|
-
|
|
372
|
-
|
|
375
|
+
// format date and datetime fields
|
|
376
|
+
formatDates(fieldDefinitions, instance);
|
|
373
377
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
+
// Step 2: Update custom field values if they exist
|
|
379
|
+
const customFieldsIdx = fields.indexOf('customFields');
|
|
380
|
+
if (customFieldsIdx > -1) {
|
|
381
|
+
const { customFields } = instance;
|
|
378
382
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
383
|
+
if (!Object.keys(customFields).length) {
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
382
386
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
387
|
+
// Save custom field values
|
|
388
|
+
await updateInstanceValues({
|
|
389
|
+
modelId: instance.id,
|
|
390
|
+
modelType,
|
|
391
|
+
identifiers,
|
|
392
|
+
customFields,
|
|
393
|
+
options: {
|
|
394
|
+
useCustomFieldsEntries: sadotOptions.useCustomFieldsEntries,
|
|
395
|
+
transaction: options.transaction,
|
|
396
|
+
modelOptions,
|
|
397
|
+
},
|
|
398
|
+
});
|
|
395
399
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
400
|
+
// Remove customFields from fields array after handling
|
|
401
|
+
// eslint-disable-next-line no-param-reassign
|
|
402
|
+
fields.splice(customFieldsIdx, 1);
|
|
403
|
+
}
|
|
404
|
+
};
|
|
401
405
|
|
|
402
406
|
/**
|
|
403
407
|
* Hook to enable individual hooks for bulk create operations
|
package/src/models/index.ts
CHANGED
|
@@ -29,14 +29,10 @@ const initTables = async (
|
|
|
29
29
|
sequelize: Sequelize,
|
|
30
30
|
getUser: CustomFieldOptions['getUser'],
|
|
31
31
|
{
|
|
32
|
-
schemaPrefix,
|
|
33
|
-
schemaVersion,
|
|
34
|
-
useCustomFieldsEntries,
|
|
35
|
-
}: InitTablesOptions = {
|
|
36
|
-
schemaPrefix: SADOT_MIGRATION_PREFIX,
|
|
37
|
-
schemaVersion: SCHEMA_VERSION,
|
|
38
|
-
useCustomFieldsEntries: false,
|
|
39
|
-
},
|
|
32
|
+
schemaPrefix = SADOT_MIGRATION_PREFIX,
|
|
33
|
+
schemaVersion = SCHEMA_VERSION,
|
|
34
|
+
useCustomFieldsEntries = false,
|
|
35
|
+
}: InitTablesOptions = {},
|
|
40
36
|
): Promise<void> => {
|
|
41
37
|
const CUSTOM_FIELDS_SCHEMA_VERSION = `${schemaPrefix}_${schemaVersion}${useCustomFieldsEntries ? '_withEntries' : ''}`;
|
|
42
38
|
logger.info('custom-fields: initialize custom-fields tables');
|
|
@@ -104,11 +100,14 @@ const initTables = async (
|
|
|
104
100
|
},
|
|
105
101
|
);
|
|
106
102
|
|
|
103
|
+
logger.info('custom-fields: starting migrations');
|
|
107
104
|
const migrations = await SequelizeMeta.findAll({ where: { name: { [Op.like]: `${schemaPrefix}%` } }, raw: true });
|
|
108
105
|
const currentSadotSchemaVersion = migrations.at(-1);
|
|
109
106
|
const expectedSchemaVersionIndex = migrations.findIndex((m) => (m as any).name === CUSTOM_FIELDS_SCHEMA_VERSION);
|
|
110
107
|
|
|
108
|
+
logger.info('custom-fields: migrations', { migrations, currentSadotSchemaVersion, expectedSchemaVersionIndex });
|
|
111
109
|
if (!currentSadotSchemaVersion || (currentSadotSchemaVersion as any).name !== CUSTOM_FIELDS_SCHEMA_VERSION) {
|
|
110
|
+
logger.info('custom-fields: syncing models');
|
|
112
111
|
await CustomFieldDefinition.sync({ alter: true });
|
|
113
112
|
await CustomFieldValue.sync({ alter: true });
|
|
114
113
|
// T.Y TODO: Remove the if statement once we're ready to add the new entries table for all MS
|
package/src/types/index.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import type { IncludeOptions } from 'sequelize';
|
|
1
|
+
import type { IncludeOptions, Transaction } from 'sequelize';
|
|
2
2
|
import type { ModelCtor, Sequelize } from 'sequelize-typescript';
|
|
3
3
|
import type { getUser as GetUserType } from '@autofleet/zehut';
|
|
4
|
+
import type CustomFieldDefinition from '../models/CustomFieldDefinition';
|
|
4
5
|
|
|
5
6
|
export type ModelFetcher = (name: string) => any;
|
|
6
7
|
|
|
8
|
+
export interface TransactionOptions extends Record<string, any> {
|
|
9
|
+
transaction?: Transaction & {
|
|
10
|
+
definitionCache?: Map<string, CustomFieldDefinition[]>;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
export type ModelOptions = {
|
|
8
15
|
/**
|
|
9
16
|
* Include options for the model
|
|
@@ -19,6 +26,10 @@ export type ModelOptions = {
|
|
|
19
26
|
* Whether to use the entity id from the instance per scope attribute
|
|
20
27
|
*/
|
|
21
28
|
useEntityIdFromInclude?: boolean
|
|
29
|
+
/**
|
|
30
|
+
* Which attributes to include in the model
|
|
31
|
+
*/
|
|
32
|
+
attributes?: string[];
|
|
22
33
|
}
|
|
23
34
|
export type Models = {
|
|
24
35
|
name: string;
|