@autofleet/sadot 0.13.3-beta-1bef9935.8 → 0.13.3
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 +36 -11
- package/dist/hooks/hooks.js +24 -16
- package/dist/index.js +4 -19
- package/dist/models/index.js +15 -39
- package/dist/repository/validator.d.ts +1 -0
- package/dist/types/index.d.ts +13 -1
- package/package.json +1 -1
- package/src/hooks/enrich.ts +47 -22
- package/src/hooks/hooks.ts +29 -22
- package/src/index.ts +4 -26
- package/src/models/index.ts +17 -39
- package/src/repository/validator.ts +1 -0
- package/src/types/index.ts +14 -1
- package/.env +0 -4
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 {};
|
|
@@ -78,6 +80,7 @@ const serializeCustomFields = (customFieldValues, customFieldDefinitionsHash) =>
|
|
|
78
80
|
* A hook to attach the custom fields when fetching a model instances.
|
|
79
81
|
*/
|
|
80
82
|
const enrichResults = (modelType, scopeAttributes, hookType, modelOptions = {}, sadotOptions = { useCustomFieldsEntries: false }) => async (instancesOrInstance, options) => {
|
|
83
|
+
var _a;
|
|
81
84
|
if (options.originalAttributes?.length > 0
|
|
82
85
|
&& !options.originalAttributes?.includes?.('customFields')) {
|
|
83
86
|
return;
|
|
@@ -93,7 +96,27 @@ const enrichResults = (modelType, scopeAttributes, hookType, modelOptions = {},
|
|
|
93
96
|
...map,
|
|
94
97
|
[identifier]: [],
|
|
95
98
|
}), {});
|
|
96
|
-
|
|
99
|
+
// Cache for definitions by model type and transaction to avoid redundant DB queries
|
|
100
|
+
let customFieldDefinitionsPromise;
|
|
101
|
+
let cacheKey;
|
|
102
|
+
if (options.transaction) {
|
|
103
|
+
// Initialize definition cache Map if not already present directly on the transaction object
|
|
104
|
+
(_a = options.transaction).definitionCache || (_a.definitionCache = new Map());
|
|
105
|
+
cacheKey = `${modelType}:${uniqueIdentifiers.slice().sort().join(',')}`;
|
|
106
|
+
customFieldDefinitionsPromise = options.transaction.definitionCache.get(cacheKey);
|
|
107
|
+
}
|
|
108
|
+
if (!customFieldDefinitionsPromise) {
|
|
109
|
+
// Fetch from database (either first time in this transaction or no transaction)
|
|
110
|
+
customFieldDefinitionsPromise = DefinitionRepo.findByEntityIds(modelType, uniqueIdentifiers, { transaction: options.transaction, modelOptions, attributes: CUSTOM_FIELD_DEFINITION_ATTRIBUTES_TO_PULL });
|
|
111
|
+
options.transaction?.definitionCache?.set(cacheKey, customFieldDefinitionsPromise);
|
|
112
|
+
}
|
|
113
|
+
const customFieldDefinitions = await customFieldDefinitionsPromise;
|
|
114
|
+
if (customFieldDefinitions.length === 0) {
|
|
115
|
+
// if no custom fields, we can return
|
|
116
|
+
instances.forEach((instance) => {
|
|
117
|
+
instance.customFields = {};
|
|
118
|
+
});
|
|
119
|
+
}
|
|
97
120
|
if (modelOptions?.include && modelOptions.useEntityIdFromInclude) {
|
|
98
121
|
// if we pass useEntityIdFromInclude,
|
|
99
122
|
// map the entity from the options to the identifierCustomFieldDefinitionsMapping
|
|
@@ -114,16 +137,18 @@ const enrichResults = (modelType, scopeAttributes, hookType, modelOptions = {},
|
|
|
114
137
|
// Get the values per instates ids:
|
|
115
138
|
const instancesIds = instances.map((i) => i[primaryKey]);
|
|
116
139
|
// Group fields by modelId
|
|
117
|
-
const valuesGroupByInstance = await
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
140
|
+
const [valuesGroupByInstance, customFieldEntriesByInstanceId] = await Promise.all([
|
|
141
|
+
(0, exports.getValuesGroupByInstance)({
|
|
142
|
+
instancesIds,
|
|
143
|
+
options,
|
|
144
|
+
sadotOptions,
|
|
145
|
+
}),
|
|
146
|
+
(0, exports.getCustomFieldEntriesByInstanceId)({
|
|
147
|
+
instancesIds,
|
|
148
|
+
options,
|
|
149
|
+
sadotOptions,
|
|
150
|
+
}),
|
|
151
|
+
]);
|
|
127
152
|
// Attach custom fields to the instances
|
|
128
153
|
instances.forEach((instance) => {
|
|
129
154
|
const { id } = instance;
|
package/dist/hooks/hooks.js
CHANGED
|
@@ -38,6 +38,7 @@ const errors_2 = require("../errors");
|
|
|
38
38
|
const scopeAttributes_1 = __importDefault(require("../utils/scopeAttributes"));
|
|
39
39
|
const updateInstanceValues_1 = __importDefault(require("./utils/updateInstanceValues"));
|
|
40
40
|
const constants_1 = require("../utils/constants");
|
|
41
|
+
const CUSTOM_VALIDATOR_ATTRIBUTES_TO_PULL = ['schema'];
|
|
41
42
|
// Initialize Ajv with relaxed settings to avoid warnings
|
|
42
43
|
const ajv = new ajv_1.default({
|
|
43
44
|
allErrors: true,
|
|
@@ -90,6 +91,7 @@ const getCompleteCustomFields = async (instance, options) => {
|
|
|
90
91
|
* Validates the model using custom validators
|
|
91
92
|
*/
|
|
92
93
|
const validateModel = async (instance, options, scopeAttributes, isCreate = false) => {
|
|
94
|
+
var _a;
|
|
93
95
|
const modelType = instance.constructor.name;
|
|
94
96
|
logger_1.default.debug('sadot - validating model', { isCreate, modelType });
|
|
95
97
|
const identifiers = (0, scopeAttributes_1.default)(instance, scopeAttributes);
|
|
@@ -106,7 +108,24 @@ const validateModel = async (instance, options, scopeAttributes, isCreate = fals
|
|
|
106
108
|
logger_1.default.debug('sadot - skipping validation: no entityId');
|
|
107
109
|
return;
|
|
108
110
|
}
|
|
109
|
-
|
|
111
|
+
let validatorsPromise;
|
|
112
|
+
let cacheKey;
|
|
113
|
+
if (options.transaction) {
|
|
114
|
+
// eslint-disable-next-line no-param-reassign
|
|
115
|
+
(_a = options.transaction).validationsCache || (_a.validationsCache = new Map());
|
|
116
|
+
cacheKey = `${modelType}-${entityId}`;
|
|
117
|
+
validatorsPromise = options.transaction.validationsCache.get(cacheKey);
|
|
118
|
+
}
|
|
119
|
+
if (!validatorsPromise) {
|
|
120
|
+
validatorsPromise = ValidatorRepo.findAllByModelType(modelType, entityId, {
|
|
121
|
+
transaction: options.transaction,
|
|
122
|
+
attributes: CUSTOM_VALIDATOR_ATTRIBUTES_TO_PULL,
|
|
123
|
+
});
|
|
124
|
+
if (options.transaction) {
|
|
125
|
+
options?.transaction?.validationsCache.set(cacheKey, validatorsPromise);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const validators = await validatorsPromise;
|
|
110
129
|
logger_1.default.debug('sadot - validators found', { count: validators.length });
|
|
111
130
|
if (!validators.length) {
|
|
112
131
|
logger_1.default.debug('sadot - skipping validation: no validators found');
|
|
@@ -125,16 +144,6 @@ const validateModel = async (instance, options, scopeAttributes, isCreate = fals
|
|
|
125
144
|
const completeCustomFields = !isCreate
|
|
126
145
|
? await getCompleteCustomFields(instance, options)
|
|
127
146
|
: instance.customFields || {};
|
|
128
|
-
// For debugging in case of update
|
|
129
|
-
if (!isCreate && process.env.NODE_ENV !== 'production') {
|
|
130
|
-
// Create after object for logging
|
|
131
|
-
const logAfterObj = manualObjectCopy(instance.dataValues, { customFields: completeCustomFields });
|
|
132
|
-
logger_1.default.debug('sadot - validate with values', {
|
|
133
|
-
before: originalValues,
|
|
134
|
-
after: logAfterObj,
|
|
135
|
-
schema: validators[0].schema,
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
147
|
// eslint-disable-next-line no-restricted-syntax
|
|
139
148
|
for (const validator of validators) {
|
|
140
149
|
const { schema } = validator;
|
|
@@ -180,7 +189,7 @@ const validateModel = async (instance, options, scopeAttributes, isCreate = fals
|
|
|
180
189
|
};
|
|
181
190
|
// Validate
|
|
182
191
|
const isValid = validateSchema(JSON.parse(JSON.stringify(payload)));
|
|
183
|
-
logger_1.default.
|
|
192
|
+
logger_1.default.debug('sadot - validation result', {
|
|
184
193
|
isValid,
|
|
185
194
|
test: {
|
|
186
195
|
before: originalValues,
|
|
@@ -188,10 +197,9 @@ const validateModel = async (instance, options, scopeAttributes, isCreate = fals
|
|
|
188
197
|
},
|
|
189
198
|
});
|
|
190
199
|
if (!isValid) {
|
|
191
|
-
const errorDetails = validateSchema
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}).join(', ');
|
|
200
|
+
const errorDetails = validateSchema
|
|
201
|
+
.errors
|
|
202
|
+
?.map((err) => `${err.instancePath || ''} ${err.message || 'Invalid value'}`).join(', ');
|
|
195
203
|
throw new errors_1.BadRequest([new Error(`Validation failed for ${modelType}: ${errorDetails}`)]);
|
|
196
204
|
}
|
|
197
205
|
}
|
package/dist/index.js
CHANGED
|
@@ -43,36 +43,21 @@ __exportStar(require("./utils/helpers"), exports);
|
|
|
43
43
|
* @see {@link 'custom-fields/config'} for configurations
|
|
44
44
|
*/
|
|
45
45
|
const useCustomFields = async (app, getModel, options) => {
|
|
46
|
-
|
|
47
|
-
(0, logger_1.tryAddingTraceIdMiddleware)()
|
|
48
|
-
.then(() => console.log('useCustomFields: tryAddingTraceIdMiddleware done'))
|
|
49
|
-
.catch(() => console.log('useCustomFields: tryAddingTraceIdMiddleware failed'));
|
|
46
|
+
(0, logger_1.tryAddingTraceIdMiddleware)().catch(() => null);
|
|
50
47
|
const { models, useCustomFieldsEntries } = options;
|
|
51
|
-
console.log('useCustomFields: options extracted', { models, useCustomFieldsEntries });
|
|
52
48
|
if (app) {
|
|
53
|
-
console.log('useCustomFields: app exists, setting up /api route');
|
|
54
49
|
app.use('/api', api_1.default);
|
|
55
50
|
}
|
|
56
|
-
else {
|
|
57
|
-
console.log('useCustomFields: no app provided');
|
|
58
|
-
}
|
|
59
51
|
const sequelize = options.sequelize ?? (0, db_1.default)(options.databaseConfig);
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
console.log('useCustomFields: using provided Sequelize instance');
|
|
52
|
+
if (process.env.NODE_ENV === 'test') {
|
|
53
|
+
await (0, models_1.initTestModels)(sequelize);
|
|
65
54
|
}
|
|
66
|
-
|
|
55
|
+
// The order is important
|
|
67
56
|
(0, init_1.addHooks)(models, getModel, { useCustomFieldsEntries });
|
|
68
|
-
console.log('useCustomFields: initializing tables');
|
|
69
57
|
await (0, models_1.initTables)(sequelize, options.getUser, { useCustomFieldsEntries });
|
|
70
|
-
console.log('useCustomFields: adding scopes');
|
|
71
58
|
(0, init_1.addScopes)(models, getModel, { useCustomFieldsEntries });
|
|
72
|
-
console.log('useCustomFields: applying custom associations');
|
|
73
59
|
(0, init_1.applyCustomAssociation)(models);
|
|
74
60
|
logger_1.default.debug('sadot - custom fields finished initializing with models', models);
|
|
75
|
-
console.log('useCustomFields: finished and returning sequelize');
|
|
76
61
|
return sequelize;
|
|
77
62
|
};
|
|
78
63
|
exports.default = useCustomFields;
|
package/dist/models/index.js
CHANGED
|
@@ -112,44 +112,20 @@ const initTables = async (sequelize, getUser, { schemaPrefix = SADOT_MIGRATION_P
|
|
|
112
112
|
};
|
|
113
113
|
exports.initTables = initTables;
|
|
114
114
|
const initTestModels = async (sequelize) => {
|
|
115
|
-
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
//
|
|
131
|
-
// await sequelize.createSchema('custom-fields', { logging: true });
|
|
132
|
-
// console.log('initTestModels: schema created');
|
|
133
|
-
// } catch (error) {
|
|
134
|
-
// console.log('initTestModels: error dropping or creating schema', error);
|
|
135
|
-
// throw new Error('Failed to drop or create schema');
|
|
136
|
-
// }
|
|
137
|
-
//
|
|
138
|
-
// logger.info('custom-fields: test models added');
|
|
139
|
-
//
|
|
140
|
-
// console.log('initTestModels: syncing TestModel');
|
|
141
|
-
// await TestModel.sync({ alter: true });
|
|
142
|
-
//
|
|
143
|
-
// console.log('initTestModels: syncing AssociatedTestModel');
|
|
144
|
-
// await AssociatedTestModel.sync({ alter: true });
|
|
145
|
-
//
|
|
146
|
-
// console.log('initTestModels: syncing ContextTestModel');
|
|
147
|
-
// await ContextTestModel.sync({ alter: true });
|
|
148
|
-
//
|
|
149
|
-
// console.log('initTestModels: syncing ContextAwareTestModel');
|
|
150
|
-
// await ContextAwareTestModel.sync({ alter: true });
|
|
151
|
-
//
|
|
152
|
-
// logger.info('custom-fields: test models synced');
|
|
153
|
-
// console.log('initTestModels: finished');
|
|
115
|
+
logger_1.default.info('custom-fields: initialize custom-fields test models');
|
|
116
|
+
// Detect models and import them to the orm
|
|
117
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
118
|
+
if (!sequelize.addModels) {
|
|
119
|
+
throw new Error('sequelize instance must have addModels function');
|
|
120
|
+
}
|
|
121
|
+
sequelize.addModels(testModels);
|
|
122
|
+
await sequelize.dropSchema('custom-fields', { logging: false });
|
|
123
|
+
await sequelize.createSchema('custom-fields', { logging: false });
|
|
124
|
+
logger_1.default.info('custom-fields: test models added');
|
|
125
|
+
await TestModel_1.default.sync({ alter: true });
|
|
126
|
+
await AssociatedTestModel_1.default.sync({ alter: true });
|
|
127
|
+
await ContextTestModel_1.default.sync({ alter: true });
|
|
128
|
+
await ContextAwareTestModel_1.default.sync({ alter: true });
|
|
129
|
+
logger_1.default.info('custom-fields: test models synced');
|
|
154
130
|
};
|
|
155
131
|
exports.initTestModels = initTestModels;
|
|
@@ -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,15 @@
|
|
|
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';
|
|
5
|
+
import type CustomValidator from '../models/CustomValidator';
|
|
4
6
|
export type ModelFetcher = (name: string) => any;
|
|
7
|
+
export interface TransactionOptions extends Record<string, any> {
|
|
8
|
+
transaction?: Transaction & {
|
|
9
|
+
definitionCache?: Map<string, CustomFieldDefinition[]>;
|
|
10
|
+
validationsCache?: Map<string, CustomValidator[]>;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
5
13
|
export type ModelOptions = {
|
|
6
14
|
/**
|
|
7
15
|
* Include options for the model
|
|
@@ -16,6 +24,10 @@ export type ModelOptions = {
|
|
|
16
24
|
* Whether to use the entity id from the instance per scope attribute
|
|
17
25
|
*/
|
|
18
26
|
useEntityIdFromInclude?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Which attributes to include in the model
|
|
29
|
+
*/
|
|
30
|
+
attributes?: string[];
|
|
19
31
|
};
|
|
20
32
|
export type Models = {
|
|
21
33
|
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,37 @@ 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
|
+
if (options.transaction) {
|
|
142
|
+
// Initialize definition cache Map if not already present directly on the transaction object
|
|
143
|
+
options.transaction.definitionCache ||= new Map();
|
|
144
|
+
cacheKey = `${modelType}:${uniqueIdentifiers.slice().sort().join(',')}`;
|
|
145
|
+
customFieldDefinitionsPromise = options.transaction.definitionCache.get(cacheKey);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!customFieldDefinitionsPromise) {
|
|
149
|
+
// Fetch from database (either first time in this transaction or no transaction)
|
|
150
|
+
customFieldDefinitionsPromise = DefinitionRepo.findByEntityIds(
|
|
151
|
+
modelType,
|
|
152
|
+
uniqueIdentifiers,
|
|
153
|
+
{ transaction: options.transaction, modelOptions, attributes: CUSTOM_FIELD_DEFINITION_ATTRIBUTES_TO_PULL },
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
options.transaction?.definitionCache?.set(cacheKey, customFieldDefinitionsPromise);
|
|
157
|
+
}
|
|
158
|
+
const customFieldDefinitions = await customFieldDefinitionsPromise;
|
|
159
|
+
|
|
160
|
+
if (customFieldDefinitions.length === 0) {
|
|
161
|
+
// if no custom fields, we can return
|
|
162
|
+
instances.forEach((instance) => {
|
|
163
|
+
instance.customFields = {};
|
|
164
|
+
});
|
|
165
|
+
}
|
|
142
166
|
|
|
143
167
|
if (modelOptions?.include && modelOptions.useEntityIdFromInclude) {
|
|
144
168
|
// if we pass useEntityIdFromInclude,
|
|
@@ -164,17 +188,18 @@ const enrichResults = (
|
|
|
164
188
|
const instancesIds = instances.map((i) => i[primaryKey]);
|
|
165
189
|
|
|
166
190
|
// Group fields by modelId
|
|
167
|
-
const valuesGroupByInstance = await
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
191
|
+
const [valuesGroupByInstance, customFieldEntriesByInstanceId] = await Promise.all([
|
|
192
|
+
getValuesGroupByInstance({
|
|
193
|
+
instancesIds,
|
|
194
|
+
options,
|
|
195
|
+
sadotOptions,
|
|
196
|
+
}),
|
|
197
|
+
getCustomFieldEntriesByInstanceId({
|
|
198
|
+
instancesIds,
|
|
199
|
+
options,
|
|
200
|
+
sadotOptions,
|
|
201
|
+
}),
|
|
202
|
+
]);
|
|
178
203
|
|
|
179
204
|
// Attach custom fields to the instances
|
|
180
205
|
instances.forEach((instance) => {
|
package/src/hooks/hooks.ts
CHANGED
|
@@ -13,6 +13,8 @@ import updateInstanceValues from './utils/updateInstanceValues';
|
|
|
13
13
|
import { CustomFieldDefinitionType } from '../utils/constants';
|
|
14
14
|
import type { CustomFieldDefinition } from '../models';
|
|
15
15
|
|
|
16
|
+
const CUSTOM_VALIDATOR_ATTRIBUTES_TO_PULL = ['schema'];
|
|
17
|
+
|
|
16
18
|
// Initialize Ajv with relaxed settings to avoid warnings
|
|
17
19
|
const ajv = new Ajv({
|
|
18
20
|
allErrors: true,
|
|
@@ -105,11 +107,29 @@ const validateModel = async (
|
|
|
105
107
|
return;
|
|
106
108
|
}
|
|
107
109
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
110
|
+
let validatorsPromise;
|
|
111
|
+
let cacheKey;
|
|
112
|
+
if (options.transaction) {
|
|
113
|
+
// eslint-disable-next-line no-param-reassign
|
|
114
|
+
options.transaction.validationsCache ||= new Map();
|
|
115
|
+
cacheKey = `${modelType}-${entityId}`;
|
|
116
|
+
validatorsPromise = options.transaction.validationsCache.get(cacheKey);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!validatorsPromise) {
|
|
120
|
+
validatorsPromise = ValidatorRepo.findAllByModelType(
|
|
121
|
+
modelType,
|
|
122
|
+
entityId,
|
|
123
|
+
{
|
|
124
|
+
transaction: options.transaction,
|
|
125
|
+
attributes: CUSTOM_VALIDATOR_ATTRIBUTES_TO_PULL,
|
|
126
|
+
},
|
|
127
|
+
);
|
|
128
|
+
if (options.transaction) {
|
|
129
|
+
options?.transaction?.validationsCache.set(cacheKey, validatorsPromise);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const validators = await validatorsPromise;
|
|
113
133
|
|
|
114
134
|
logger.debug('sadot - validators found', { count: validators.length });
|
|
115
135
|
|
|
@@ -134,18 +154,6 @@ const validateModel = async (
|
|
|
134
154
|
? await getCompleteCustomFields(instance, options)
|
|
135
155
|
: instance.customFields || {};
|
|
136
156
|
|
|
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
|
-
|
|
149
157
|
// eslint-disable-next-line no-restricted-syntax
|
|
150
158
|
for (const validator of validators) {
|
|
151
159
|
const { schema } = validator;
|
|
@@ -201,7 +209,7 @@ const validateModel = async (
|
|
|
201
209
|
// Validate
|
|
202
210
|
const isValid = validateSchema(JSON.parse(JSON.stringify(payload)));
|
|
203
211
|
|
|
204
|
-
logger.
|
|
212
|
+
logger.debug('sadot - validation result', {
|
|
205
213
|
isValid,
|
|
206
214
|
test: {
|
|
207
215
|
before: originalValues,
|
|
@@ -210,10 +218,9 @@ const validateModel = async (
|
|
|
210
218
|
});
|
|
211
219
|
|
|
212
220
|
if (!isValid) {
|
|
213
|
-
const errorDetails = validateSchema
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}).join(', ');
|
|
221
|
+
const errorDetails = validateSchema
|
|
222
|
+
.errors
|
|
223
|
+
?.map((err) => `${(err as any).instancePath || ''} ${(err as any).message || 'Invalid value'}`).join(', ');
|
|
217
224
|
|
|
218
225
|
throw new BadRequest([new Error(`Validation failed for ${modelType}: ${errorDetails}`)]);
|
|
219
226
|
}
|
package/src/index.ts
CHANGED
|
@@ -26,44 +26,22 @@ const useCustomFields = async (
|
|
|
26
26
|
getModel: ModelFetcher,
|
|
27
27
|
options: CustomFieldOptions,
|
|
28
28
|
): Promise<Sequelize> => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
tryAddingTraceIdMiddleware()
|
|
32
|
-
.then(() => console.log('useCustomFields: tryAddingTraceIdMiddleware done'))
|
|
33
|
-
.catch(() => console.log('useCustomFields: tryAddingTraceIdMiddleware failed'));
|
|
34
|
-
|
|
29
|
+
tryAddingTraceIdMiddleware().catch(() => null);
|
|
35
30
|
const { models, useCustomFieldsEntries } = options;
|
|
36
|
-
console.log('useCustomFields: options extracted', { models, useCustomFieldsEntries });
|
|
37
|
-
|
|
38
31
|
if (app) {
|
|
39
|
-
console.log('useCustomFields: app exists, setting up /api route');
|
|
40
32
|
app.use('/api', api);
|
|
41
|
-
} else {
|
|
42
|
-
console.log('useCustomFields: no app provided');
|
|
43
33
|
}
|
|
44
|
-
|
|
45
34
|
const sequelize = options.sequelize ?? initDB(options.databaseConfig);
|
|
46
|
-
if (
|
|
47
|
-
|
|
48
|
-
} else {
|
|
49
|
-
console.log('useCustomFields: using provided Sequelize instance');
|
|
35
|
+
if (process.env.NODE_ENV === 'test') {
|
|
36
|
+
await initTestModels(sequelize);
|
|
50
37
|
}
|
|
51
|
-
|
|
52
|
-
console.log('useCustomFields: adding hooks');
|
|
38
|
+
// The order is important
|
|
53
39
|
addHooks(models, getModel, { useCustomFieldsEntries });
|
|
54
|
-
|
|
55
|
-
console.log('useCustomFields: initializing tables');
|
|
56
40
|
await initTables(sequelize, options.getUser, { useCustomFieldsEntries });
|
|
57
|
-
|
|
58
|
-
console.log('useCustomFields: adding scopes');
|
|
59
41
|
addScopes(models, getModel, { useCustomFieldsEntries });
|
|
60
|
-
|
|
61
|
-
console.log('useCustomFields: applying custom associations');
|
|
62
42
|
applyCustomAssociation(models);
|
|
63
43
|
|
|
64
44
|
logger.debug('sadot - custom fields finished initializing with models', models);
|
|
65
|
-
console.log('useCustomFields: finished and returning sequelize');
|
|
66
|
-
|
|
67
45
|
return sequelize;
|
|
68
46
|
};
|
|
69
47
|
|
package/src/models/index.ts
CHANGED
|
@@ -132,45 +132,23 @@ const initTables = async (
|
|
|
132
132
|
};
|
|
133
133
|
|
|
134
134
|
const initTestModels = async (sequelize: Sequelize): Promise<void> => {
|
|
135
|
-
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
// console.log('initTestModels: schema created');
|
|
153
|
-
// } catch (error) {
|
|
154
|
-
// console.log('initTestModels: error dropping or creating schema', error);
|
|
155
|
-
// throw new Error('Failed to drop or create schema');
|
|
156
|
-
// }
|
|
157
|
-
//
|
|
158
|
-
// logger.info('custom-fields: test models added');
|
|
159
|
-
//
|
|
160
|
-
// console.log('initTestModels: syncing TestModel');
|
|
161
|
-
// await TestModel.sync({ alter: true });
|
|
162
|
-
//
|
|
163
|
-
// console.log('initTestModels: syncing AssociatedTestModel');
|
|
164
|
-
// await AssociatedTestModel.sync({ alter: true });
|
|
165
|
-
//
|
|
166
|
-
// console.log('initTestModels: syncing ContextTestModel');
|
|
167
|
-
// await ContextTestModel.sync({ alter: true });
|
|
168
|
-
//
|
|
169
|
-
// console.log('initTestModels: syncing ContextAwareTestModel');
|
|
170
|
-
// await ContextAwareTestModel.sync({ alter: true });
|
|
171
|
-
//
|
|
172
|
-
// logger.info('custom-fields: test models synced');
|
|
173
|
-
// console.log('initTestModels: finished');
|
|
135
|
+
logger.info('custom-fields: initialize custom-fields test models');
|
|
136
|
+
// Detect models and import them to the orm
|
|
137
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
138
|
+
if (!sequelize.addModels) {
|
|
139
|
+
throw new Error('sequelize instance must have addModels function');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
sequelize.addModels(testModels);
|
|
143
|
+
await sequelize.dropSchema('custom-fields', { logging: false });
|
|
144
|
+
await sequelize.createSchema('custom-fields', { logging: false });
|
|
145
|
+
|
|
146
|
+
logger.info('custom-fields: test models added');
|
|
147
|
+
await TestModel.sync({ alter: true });
|
|
148
|
+
await AssociatedTestModel.sync({ alter: true });
|
|
149
|
+
await ContextTestModel.sync({ alter: true });
|
|
150
|
+
await ContextAwareTestModel.sync({ alter: true });
|
|
151
|
+
logger.info('custom-fields: test models synced');
|
|
174
152
|
};
|
|
175
153
|
|
|
176
154
|
export {
|
package/src/types/index.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
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';
|
|
5
|
+
import type CustomValidator from '../models/CustomValidator';
|
|
4
6
|
|
|
5
7
|
export type ModelFetcher = (name: string) => any;
|
|
6
8
|
|
|
9
|
+
export interface TransactionOptions extends Record<string, any> {
|
|
10
|
+
transaction?: Transaction & {
|
|
11
|
+
definitionCache?: Map<string, CustomFieldDefinition[]>;
|
|
12
|
+
validationsCache?: Map<string, CustomValidator[]>;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
export type ModelOptions = {
|
|
8
17
|
/**
|
|
9
18
|
* Include options for the model
|
|
@@ -19,6 +28,10 @@ export type ModelOptions = {
|
|
|
19
28
|
* Whether to use the entity id from the instance per scope attribute
|
|
20
29
|
*/
|
|
21
30
|
useEntityIdFromInclude?: boolean
|
|
31
|
+
/**
|
|
32
|
+
* Which attributes to include in the model
|
|
33
|
+
*/
|
|
34
|
+
attributes?: string[];
|
|
22
35
|
}
|
|
23
36
|
export type Models = {
|
|
24
37
|
name: string;
|
package/.env
DELETED