@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.
Files changed (135) hide show
  1. package/README.md +5 -0
  2. package/dist/api/index.d.ts +2 -1
  3. package/dist/api/index.js +3 -2
  4. package/dist/api/v1/definition/index.d.ts +2 -1
  5. package/dist/api/v1/definition/index.js +12 -14
  6. package/dist/api/v1/definition/validations.js +14 -12
  7. package/dist/api/v1/errors.d.ts +3 -1
  8. package/dist/api/v1/errors.js +1 -4
  9. package/dist/api/v1/index.d.ts +2 -1
  10. package/dist/api/v1/index.js +5 -2
  11. package/dist/api/v1/validator/index.d.ts +3 -0
  12. package/dist/api/v1/validator/index.js +143 -0
  13. package/dist/api/v1/validator/validations.d.ts +6 -0
  14. package/dist/api/v1/validator/validations.js +40 -0
  15. package/dist/errors/index.d.ts +9 -1
  16. package/dist/errors/index.js +25 -4
  17. package/dist/events/index.d.ts +2 -1
  18. package/dist/events/index.js +17 -11
  19. package/dist/hooks/create.d.ts +2 -2
  20. package/dist/hooks/create.js +40 -19
  21. package/dist/hooks/enrich.d.ts +20 -2
  22. package/dist/hooks/enrich.js +88 -16
  23. package/dist/hooks/hooks.d.ts +17 -0
  24. package/dist/hooks/hooks.js +379 -0
  25. package/dist/hooks/index.d.ts +2 -3
  26. package/dist/hooks/index.js +6 -7
  27. package/dist/hooks/update.d.ts +2 -2
  28. package/dist/hooks/update.js +16 -26
  29. package/dist/hooks/utils/updateInstanceValues.d.ts +15 -0
  30. package/dist/hooks/utils/updateInstanceValues.js +50 -0
  31. package/dist/index.d.ts +2 -2
  32. package/dist/index.js +19 -6
  33. package/dist/models/CustomFieldDefinition.d.ts +1 -0
  34. package/dist/models/CustomFieldDefinition.js +10 -2
  35. package/dist/models/CustomFieldEntries.d.ts +15 -0
  36. package/dist/models/CustomFieldEntries.js +123 -0
  37. package/dist/models/CustomFieldValue.d.ts +3 -2
  38. package/dist/models/CustomFieldValue.js +22 -14
  39. package/dist/models/CustomValidator.d.ts +17 -0
  40. package/dist/models/CustomValidator.js +98 -0
  41. package/dist/models/index.d.ts +10 -2
  42. package/dist/models/index.js +60 -22
  43. package/dist/repository/definition.d.ts +23 -7
  44. package/dist/repository/definition.js +36 -7
  45. package/dist/repository/entries.d.ts +13 -0
  46. package/dist/repository/entries.js +92 -0
  47. package/dist/repository/utils/formatValues.d.ts +3 -0
  48. package/dist/repository/utils/formatValues.js +16 -0
  49. package/dist/repository/validator.d.ts +21 -0
  50. package/dist/repository/validator.js +62 -0
  51. package/dist/repository/value.d.ts +1 -1
  52. package/dist/repository/value.js +7 -32
  53. package/dist/scopes/filter.d.ts +5 -22
  54. package/dist/scopes/filter.js +18 -65
  55. package/dist/scopes/helpers/filter.helpers.d.ts +42 -0
  56. package/dist/scopes/helpers/filter.helpers.js +204 -0
  57. package/dist/tests/api/test-api.js +6 -24
  58. package/dist/tests/helpers/commonHooks.d.ts +6 -0
  59. package/dist/tests/helpers/commonHooks.js +62 -0
  60. package/dist/tests/helpers/database-config.js +1 -1
  61. package/dist/tests/helpers/index.d.ts +6 -1
  62. package/dist/tests/helpers/index.js +15 -2
  63. package/dist/tests/mocks/definition.mock.d.ts +5 -2
  64. package/dist/tests/mocks/definition.mock.js +10 -1
  65. package/dist/tests/mocks/events.mock.d.ts +1 -0
  66. package/dist/tests/mocks/events.mock.js +4 -2
  67. package/dist/types/definition/index.d.ts +1 -0
  68. package/dist/types/entries/index.d.ts +25 -0
  69. package/dist/types/entries/index.js +2 -0
  70. package/dist/types/index.d.ts +19 -3
  71. package/dist/utils/constants/index.d.ts +1 -1
  72. package/dist/utils/helpers/index.d.ts +4 -3
  73. package/dist/utils/helpers/index.js +22 -30
  74. package/dist/utils/init.d.ts +5 -3
  75. package/dist/utils/init.js +13 -11
  76. package/dist/utils/logger/index.d.ts +1 -0
  77. package/dist/utils/logger/index.js +34 -0
  78. package/dist/utils/validations/index.d.ts +7 -1
  79. package/dist/utils/validations/index.js +28 -7
  80. package/dist/utils/validations/schema/validator-schema.d.ts +9 -0
  81. package/dist/utils/validations/schema/validator-schema.js +95 -0
  82. package/dist/utils/validations/type.d.ts +2 -1
  83. package/dist/utils/validations/validators/index.js +9 -9
  84. package/dist/utils/validations/validators/select.validator.js +5 -2
  85. package/dist/utils/validations/validators/status.validator.js +8 -2
  86. package/package.json +28 -12
  87. package/src/api/index.ts +3 -2
  88. package/src/api/v1/definition/index.ts +20 -23
  89. package/src/api/v1/definition/validations.ts +16 -16
  90. package/src/api/v1/errors.ts +4 -7
  91. package/src/api/v1/index.ts +5 -3
  92. package/src/api/v1/validator/index.ts +141 -0
  93. package/src/api/v1/validator/validations.ts +39 -0
  94. package/src/errors/index.ts +31 -3
  95. package/src/events/index.ts +25 -13
  96. package/src/hooks/create.ts +50 -28
  97. package/src/hooks/enrich.ts +137 -28
  98. package/src/hooks/hooks.ts +467 -0
  99. package/src/hooks/index.ts +10 -5
  100. package/src/hooks/update.ts +20 -7
  101. package/src/hooks/utils/updateInstanceValues.ts +63 -0
  102. package/src/index.ts +10 -8
  103. package/src/models/CustomFieldDefinition.ts +9 -2
  104. package/src/models/CustomFieldEntries.ts +81 -0
  105. package/src/models/CustomFieldValue.ts +25 -17
  106. package/src/models/CustomValidator.ts +78 -0
  107. package/src/models/index.ts +83 -25
  108. package/src/repository/definition.ts +62 -14
  109. package/src/repository/entries.ts +88 -0
  110. package/src/repository/utils/formatValues.ts +14 -0
  111. package/src/repository/validator.ts +104 -0
  112. package/src/repository/value.ts +5 -35
  113. package/src/scopes/filter.ts +33 -106
  114. package/src/scopes/helpers/filter.helpers.ts +227 -0
  115. package/src/tests/api/test-api.ts +4 -2
  116. package/src/tests/helpers/commonHooks.ts +43 -0
  117. package/src/tests/helpers/database-config.ts +1 -1
  118. package/src/tests/helpers/index.ts +18 -2
  119. package/src/tests/mocks/definition.mock.ts +18 -9
  120. package/src/tests/mocks/events.mock.ts +4 -1
  121. package/src/types/definition/index.ts +1 -0
  122. package/src/types/entries/index.ts +27 -0
  123. package/src/types/index.ts +20 -3
  124. package/src/utils/helpers/index.ts +28 -35
  125. package/src/utils/init.ts +17 -12
  126. package/src/utils/logger/index.ts +9 -0
  127. package/src/utils/validations/index.ts +30 -6
  128. package/src/utils/validations/schema/README.md +93 -0
  129. package/src/utils/validations/schema/validator-schema.ts +106 -0
  130. package/src/utils/validations/type.ts +2 -1
  131. package/src/utils/validations/validators/index.ts +9 -9
  132. package/src/utils/validations/validators/select.validator.ts +3 -2
  133. package/src/utils/validations/validators/status.validator.ts +6 -2
  134. package/tsconfig.build.json +7 -0
  135. package/tsconfig.json +1 -1
@@ -1,14 +1,26 @@
1
- import { Op, type FindOptions, type WhereOptions } from 'sequelize';
2
- import { CustomFieldDefinition } from '../models';
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: any = { withDisabled: false },
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: any = { withDisabled: false },
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: any = { withDisabled: false },
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<any> =>
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<any> =>
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
+ };
@@ -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 { CustomFieldDefinitionType } from '../utils/constants';
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
- if (!options.modelOptions?.useEntityIdFromInclude) {
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,
@@ -1,50 +1,26 @@
1
1
  /* eslint-disable import/prefer-default-export */
2
- import { Op, type WhereOptions } from 'sequelize';
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
- if (Array.isArray(condition)) {
76
- if (condition.length === 0) {
77
- // if empty array, the condition is ignored
78
- return false;
79
- }
80
- if (typeof condition[0] === 'string') {
81
- const values = condition.map((v) => {
82
- const valRandom = Object.keys(replacements).find(
83
- (randomString) => replacements[randomString] === v,
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 customFieldConditions = conditionsStrings.join(AND_DELIMETER);
117
- const subQuery = `${'SELECT model_id FROM ('
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 replacemetKey = Object.keys(replacementsMap).find(
80
+ const replacementKey = Object.keys(replacementsMap).find(
145
81
  (randomString) => replacementsMap[randomString] === key,
146
82
  );
147
83
  return ([
148
- Sequelize.literal(`(
149
- SELECT value
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