@autofleet/sheilta 1.0.0-aaron-error-fix → 1.0.0-beta-test-2

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/lib/.DS_Store ADDED
Binary file
@@ -1,18 +1,48 @@
1
+ import type { LiteralAttribute } from '../middleware';
1
2
  declare type OrderItem = string | [string, string];
2
3
  declare type SequelizeOrder = string | OrderItem[];
4
+ export declare type FormatPayloadOptions = {
5
+ includeRawPayload?: boolean;
6
+ literalAttributes?: LiteralAttribute[];
7
+ DBFormatter?: any;
8
+ skipSearchTermFormat?: boolean;
9
+ };
10
+ declare type ConditionWithOperator = {
11
+ operator: string;
12
+ value: string;
13
+ };
14
+ export declare type ConditionValue = ConditionWithOperator | ConditionWithOperator[] | string | string[];
15
+ /**
16
+ * Generates replacements for the given conditions.
17
+ *
18
+ * @param conditions - The conditions to generate replacements for.
19
+ * @returns The replacements object.
20
+ */
21
+ export declare const generateFilterReplacements: (conditions: Record<string, ConditionValue>) => Record<string, string>;
22
+ /**
23
+ * Generates replacements for the given order array.
24
+ *
25
+ * @param order - The order array to generate replacements for.
26
+ * @returns The replacements object.
27
+ */
28
+ export declare const generateOrderReplacements: (order: string[]) => Record<string, string>;
3
29
  declare const formatPayload: ({ order, page, perPage, include, query, attributes, searchTerm, }: {
4
- order: any;
30
+ order?: any[];
5
31
  page?: number;
6
32
  perPage?: number;
7
33
  include?: any[];
8
34
  query?: {};
9
35
  attributes?: any;
10
36
  searchTerm?: any;
11
- }, model?: any) => {
37
+ }, model?: any, options?: any) => {
38
+ attributes: any[] | {
39
+ include: any[];
40
+ };
12
41
  query: {};
13
42
  order: SequelizeOrder[];
14
43
  page: any;
15
44
  perPage: any;
16
45
  include: any;
46
+ scopes: any[];
17
47
  };
18
48
  export default formatPayload;
@@ -3,11 +3,114 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateOrderReplacements = exports.generateFilterReplacements = void 0;
6
7
  const lodash_1 = __importDefault(require("lodash"));
8
+ const common_types_1 = require("@autofleet/common-types");
7
9
  const utils_1 = require("../utils");
10
+ const operators_1 = require("../operators");
11
+ const DEFAULT_ORDER = 'id';
8
12
  const DESCENDING_KEY = 'DESC';
9
- const formatOrder = ({ order = [], associationModels = [], }) => (order.length === 0 ? undefined :
10
- order.map((o) => {
13
+ const ASCENDING_KEY = 'ASC';
14
+ const CUSTOM_FIELDS_QUERY_PREFIX = 'customFields.';
15
+ const { CUSTOM_FIELDS_FILTER_SCOPE, CUSTOM_FIELDS_SORT_SCOPE } = common_types_1.customFields;
16
+ const parseCustomFieldScopeQueryValue = (value) => {
17
+ if (['string', 'number'].includes(typeof value) || Array.isArray(value)) {
18
+ return value;
19
+ }
20
+ return Object.entries(value).map(([operator, conditionValue]) => ({
21
+ operator: operators_1.OPERATORS_TO_SQL[operator],
22
+ value: conditionValue,
23
+ }));
24
+ };
25
+ const getAttributeFromOrder = (order, options = {}) => {
26
+ const { literalAttributes = [], DBFormatter = undefined } = options;
27
+ const [formattedOrder, attributes] = order.reduce((acc, o) => {
28
+ const [item, orderStyle = 'ASC'] = Array.isArray(o) ? o : [o];
29
+ const found = literalAttributes === null || literalAttributes === void 0 ? void 0 : literalAttributes.find(obj => obj.attribute === item);
30
+ if (found) {
31
+ acc[1].push(found.literal);
32
+ acc[0].push([DBFormatter ? DBFormatter(`"${found.attribute}" ${orderStyle}`) : `${found.attribute} ${orderStyle}`]);
33
+ }
34
+ else {
35
+ acc[0].push(o);
36
+ }
37
+ return acc;
38
+ }, [[], []]);
39
+ return [formattedOrder, attributes];
40
+ };
41
+ /**
42
+ * Generates replacements for the given conditions.
43
+ *
44
+ * @param conditions - The conditions to generate replacements for.
45
+ * @returns The replacements object.
46
+ */
47
+ exports.generateFilterReplacements = (conditions) => {
48
+ const replacements = {};
49
+ Object.entries(conditions).forEach(([key, condition]) => {
50
+ const replacementKey = utils_1.generateRandomString();
51
+ // eslint-disable-next-line prefer-destructuring
52
+ replacements[replacementKey] = key.split(CUSTOM_FIELDS_QUERY_PREFIX)[1];
53
+ if (Array.isArray(condition)) {
54
+ condition.forEach((value) => {
55
+ const valueKey = utils_1.generateRandomString();
56
+ replacements[valueKey] = typeof value === 'string' ? value : value.value;
57
+ });
58
+ }
59
+ else if (typeof condition === 'string' || typeof condition === 'number') {
60
+ const conditionKey = utils_1.generateRandomString();
61
+ replacements[conditionKey] = condition;
62
+ }
63
+ else if (condition === null || condition === void 0 ? void 0 : condition.operator) {
64
+ const operatorKey = utils_1.generateRandomString();
65
+ replacements[operatorKey] = condition.value;
66
+ }
67
+ });
68
+ return replacements;
69
+ };
70
+ /**
71
+ * Generates replacements for the given order array.
72
+ *
73
+ * @param order - The order array to generate replacements for.
74
+ * @returns The replacements object.
75
+ */
76
+ exports.generateOrderReplacements = (order) => {
77
+ const replacementMap = {};
78
+ order.forEach((o) => {
79
+ if (o.startsWith(CUSTOM_FIELDS_QUERY_PREFIX)) {
80
+ const rand = utils_1.generateRandomString();
81
+ // eslint-disable-next-line prefer-destructuring
82
+ replacementMap[rand] = o.split(CUSTOM_FIELDS_QUERY_PREFIX)[1];
83
+ }
84
+ else if (o.substring(1).startsWith(CUSTOM_FIELDS_QUERY_PREFIX)) {
85
+ const rand = utils_1.generateRandomString();
86
+ // eslint-disable-next-line prefer-destructuring
87
+ replacementMap[rand] = o.substring(1).split(CUSTOM_FIELDS_QUERY_PREFIX)[1];
88
+ }
89
+ });
90
+ return replacementMap;
91
+ };
92
+ /**
93
+ * Creates a combined replacement map from order and query.
94
+ *
95
+ * @param order - The order array.
96
+ * @param query - The query object.
97
+ * @returns The combined replacements object.
98
+ */
99
+ const createReplacementMap = (order, query) => (Object.assign(Object.assign({}, exports.generateOrderReplacements(order)), exports.generateFilterReplacements(query)));
100
+ const formatOrder = ({ order, associationModels = [], replacementsMap = {}, }) => {
101
+ const formattedOrders = [];
102
+ const orderScopesMap = new Map();
103
+ order.forEach((o) => {
104
+ if ([o, o.substring(1)].some(t => t.startsWith(CUSTOM_FIELDS_QUERY_PREFIX))) {
105
+ if (!orderScopesMap.has(CUSTOM_FIELDS_SORT_SCOPE)) {
106
+ orderScopesMap.set(CUSTOM_FIELDS_SORT_SCOPE, {});
107
+ }
108
+ const scopeKey = o.split(CUSTOM_FIELDS_QUERY_PREFIX)[1];
109
+ orderScopesMap.get(CUSTOM_FIELDS_SORT_SCOPE)[scopeKey] = (utils_1.isOrderDesc(o) ?
110
+ DESCENDING_KEY :
111
+ ASCENDING_KEY);
112
+ return;
113
+ }
11
114
  const formattedOrder = [utils_1.extractAttributeNameFromOrder(o, associationModels)];
12
115
  const isOrderDescOrder = utils_1.isOrderDesc(o);
13
116
  const isOrderAssociation = utils_1.isAttributeByAssociation(isOrderDescOrder
@@ -19,17 +122,71 @@ const formatOrder = ({ order = [], associationModels = [], }) => (order.length =
19
122
  if (isOrderDescOrder) {
20
123
  formattedOrder.push(DESCENDING_KEY);
21
124
  }
22
- return formattedOrder;
23
- }));
125
+ formattedOrders.push(formattedOrder);
126
+ });
127
+ return {
128
+ formattedOrders,
129
+ replacementsMap,
130
+ orderScopes: Array.from(orderScopesMap.entries()).map(([scopeName, scopeValue]) => {
131
+ if (!scopeValue) {
132
+ return scopeName;
133
+ }
134
+ return {
135
+ method: [scopeName, {
136
+ replacementsMap,
137
+ scopeValue,
138
+ }],
139
+ };
140
+ }),
141
+ };
142
+ };
24
143
  const formatPage = page => page || utils_1.PAGE_DEFAULT;
25
144
  const formatPerPage = perPage => perPage || utils_1.PER_PAGE_DEFAULT;
26
145
  const formatInclude = (include, associationsMap = {}) => {
27
- let formattedInclude = include.map(i => (Object.assign(Object.assign({}, i), { association: associationsMap[i.association || i.model], required: i.required !== 'false' })));
146
+ let formattedInclude = include.map((i) => {
147
+ var _a;
148
+ const includedAssociation = associationsMap[typeof i === 'string' ? i : (i.association || i.model)];
149
+ return Object.assign(Object.assign(Object.assign({}, i), { association: includedAssociation, required: i.required !== false }), (i.include && {
150
+ include: formatInclude(i.include, (_a = includedAssociation === null || includedAssociation === void 0 ? void 0 : includedAssociation.target) === null || _a === void 0 ? void 0 : _a.associations),
151
+ }));
152
+ });
28
153
  formattedInclude = formattedInclude.map(i => lodash_1.default.omit(i, ['model']));
29
154
  return formattedInclude;
30
155
  };
31
- const formatQuery = (query, associationModels) => Object.keys(query).reduce((acc, queryItemKey) => (Object.assign(Object.assign({}, acc), { [utils_1.isAttributeByAssociation(queryItemKey, associationModels)
32
- ? utils_1.wrapAttributeWithOperator(queryItemKey) : queryItemKey]: query[queryItemKey] })), {});
156
+ const formatQuery = (query, associationModels, replacementsMap) => {
157
+ const formattedQuery = {};
158
+ const formattedScopeMap = new Map();
159
+ Object.entries(query).forEach(([queryItemKey, queryItemValue]) => {
160
+ if (queryItemKey.startsWith(CUSTOM_FIELDS_QUERY_PREFIX)) {
161
+ if (!formattedScopeMap.has(CUSTOM_FIELDS_FILTER_SCOPE)) {
162
+ formattedScopeMap.set(CUSTOM_FIELDS_FILTER_SCOPE, {});
163
+ }
164
+ const scopeKey = queryItemKey.split(CUSTOM_FIELDS_QUERY_PREFIX)[1];
165
+ formattedScopeMap.get(CUSTOM_FIELDS_FILTER_SCOPE)[scopeKey] =
166
+ parseCustomFieldScopeQueryValue(queryItemValue);
167
+ return;
168
+ }
169
+ const key = utils_1.isAttributeByAssociation(queryItemKey, associationModels)
170
+ ? utils_1.wrapAttributeWithOperator(queryItemKey)
171
+ : queryItemKey;
172
+ formattedQuery[key] = queryItemValue;
173
+ });
174
+ const formattedScopes = Array.from(formattedScopeMap.entries()).map(([scopeName, scopeValue]) => {
175
+ if (!scopeValue) {
176
+ return scopeName;
177
+ }
178
+ return {
179
+ method: [scopeName, {
180
+ replacementsMap,
181
+ scopeValue,
182
+ }],
183
+ };
184
+ });
185
+ return {
186
+ formattedQuery,
187
+ formattedScopes,
188
+ };
189
+ };
33
190
  const formatSearchTerm = (searchTerm, attributesToSend, rawAttributes) => ({
34
191
  $and: searchTerm.split(' ').map(term => ({
35
192
  $or: attributesToSend.filter(attrKey => rawAttributes[attrKey].type.key === 'STRING').map(attr => ({
@@ -39,17 +196,24 @@ const formatSearchTerm = (searchTerm, attributesToSend, rawAttributes) => ({
39
196
  })),
40
197
  })),
41
198
  });
42
- const formatPayload = ({ order, page = utils_1.PAGE_DEFAULT, perPage = utils_1.PER_PAGE_DEFAULT, include = [], query = {}, attributes = null, searchTerm = null, }, model) => {
199
+ const formatPayload = ({ order = [], page = utils_1.PAGE_DEFAULT, perPage = utils_1.PER_PAGE_DEFAULT, include = [], query = {}, attributes = null, searchTerm = null, }, model, options) => {
200
+ const replacementsMap = createReplacementMap(order, query);
43
201
  const associationModels = Object.keys((model === null || model === void 0 ? void 0 : model.associations) || {});
44
- const formattedOrder = formatOrder({
45
- order,
202
+ const { formattedOrders, orderScopes } = formatOrder({
203
+ order: [...order, DEFAULT_ORDER],
46
204
  associationModels,
205
+ replacementsMap,
47
206
  });
207
+ const [filteredFormattedOrder, filteredLiteralAttributes] = getAttributeFromOrder(formattedOrders, options);
208
+ const allAttributes = [...filteredLiteralAttributes, ...(attributes || [])];
209
+ const formattedAttribute = (attributes === null || attributes === void 0 ? void 0 : attributes.length) ? allAttributes : { include: allAttributes };
48
210
  const formattedInclude = formatInclude(include, model === null || model === void 0 ? void 0 : model.associations);
49
211
  const formattedPage = formatPage(page);
50
212
  const formattedPerPage = formatPerPage(perPage);
51
- let formattedQuery = formatQuery(query, associationModels);
52
- if (searchTerm) {
213
+ const result = formatQuery(query, associationModels, replacementsMap);
214
+ const queryScopes = result.formattedScopes;
215
+ let { formattedQuery } = result;
216
+ if (searchTerm && !(options === null || options === void 0 ? void 0 : options.skipSearchTermFormat)) {
53
217
  const attributesToSend = (attributes === null || attributes === void 0 ? void 0 : attributes.length) ? attributes : Object.keys(model.rawAttributes);
54
218
  const queryWithSearchTerm = formatSearchTerm(searchTerm, attributesToSend, model.rawAttributes);
55
219
  formattedQuery = lodash_1.default.isEmpty(formattedQuery) ? queryWithSearchTerm : {
@@ -59,12 +223,6 @@ const formatPayload = ({ order, page = utils_1.PAGE_DEFAULT, perPage = utils_1.P
59
223
  ],
60
224
  };
61
225
  }
62
- return {
63
- query: formattedQuery,
64
- order: formattedOrder,
65
- page: formattedPage,
66
- perPage: formattedPerPage,
67
- include: formattedInclude,
68
- };
226
+ return Object.assign({ query: formattedQuery, order: filteredFormattedOrder, page: formattedPage, perPage: formattedPerPage, include: formattedInclude, scopes: [...queryScopes, ...orderScopes] }, (formattedAttribute && { attributes: formattedAttribute }));
69
227
  };
70
228
  exports.default = formatPayload;
@@ -135,13 +135,13 @@ describe('formatting test', () => {
135
135
  const { order } = _1.default({
136
136
  order: ['startTime', '-endTime'],
137
137
  });
138
- expect(JSON.stringify(order)).toBe(JSON.stringify([['startTime'], ['endTime', 'DESC']]));
138
+ expect(JSON.stringify(order)).toBe(JSON.stringify([['startTime'], ['endTime', 'DESC'], ['id']]));
139
139
  });
140
140
  it('simple check asc', () => {
141
141
  const { order } = _1.default({
142
142
  order: ['startTime', 'endTime'],
143
143
  });
144
- expect(JSON.stringify(order)).toBe(JSON.stringify([['startTime'], ['endTime']]));
144
+ expect(JSON.stringify(order)).toBe(JSON.stringify([['startTime'], ['endTime'], ['id']]));
145
145
  });
146
146
  it('by included model', () => {
147
147
  const { order } = _1.default({
@@ -154,7 +154,7 @@ describe('formatting test', () => {
154
154
  status: {},
155
155
  },
156
156
  });
157
- expect(JSON.stringify(order)).toBe(JSON.stringify([['status', 'name']]));
157
+ expect(JSON.stringify(order)).toBe(JSON.stringify([['status', 'name'], ['id']]));
158
158
  });
159
159
  it('by included model descending', () => {
160
160
  const { order } = _1.default({
@@ -167,7 +167,7 @@ describe('formatting test', () => {
167
167
  status: {},
168
168
  },
169
169
  });
170
- expect(JSON.stringify(order)).toBe(JSON.stringify([['status', 'name', 'DESC']]));
170
+ expect(JSON.stringify(order)).toBe(JSON.stringify([['status', 'name', 'DESC'], ['id']]));
171
171
  });
172
172
  it('by json field', () => {
173
173
  const { order } = _1.default({
@@ -177,7 +177,7 @@ describe('formatting test', () => {
177
177
  startTime: '', endTime: '', currencySymbol: '', currencyCode: '', startStationId: '', json: '',
178
178
  },
179
179
  });
180
- expect(JSON.stringify(order)).toBe(JSON.stringify([['status.name']]));
180
+ expect(JSON.stringify(order)).toBe(JSON.stringify([['status.name'], ['id']]));
181
181
  });
182
182
  it('by json field descending', () => {
183
183
  const { order } = _1.default({
@@ -187,7 +187,81 @@ describe('formatting test', () => {
187
187
  startTime: '', endTime: '', currencySymbol: '', currencyCode: '', startStationId: '', json: '',
188
188
  },
189
189
  });
190
- expect(JSON.stringify(order)).toBe(JSON.stringify([['status.name', 'DESC']]));
190
+ expect(JSON.stringify(order)).toBe(JSON.stringify([['status.name', 'DESC'], ['id']]));
191
+ });
192
+ it('by literal field ascending', () => {
193
+ const literalAttribute = { attribute: 'genericField', literal: [{ 0: e => `${e}0` }] };
194
+ const { order, attributes } = _1.default({
195
+ order: [literalAttribute.attribute],
196
+ }, {
197
+ rawAttributes: {
198
+ startTime: '', endTime: '', currencySymbol: '', currencyCode: '', startStationId: '',
199
+ },
200
+ }, {
201
+ literalAttributes: [literalAttribute],
202
+ });
203
+ expect(JSON.stringify(order)).toBe(JSON.stringify([[`${literalAttribute.attribute} ASC`], ['id']]));
204
+ expect(JSON.stringify(attributes))
205
+ .toBe(JSON.stringify({ include: [literalAttribute.literal] }));
206
+ });
207
+ it('by literal field descending', () => {
208
+ const literalAttribute = { attribute: 'genericField', literal: ['void', 'field'] };
209
+ const { order, attributes } = _1.default({
210
+ order: [`-${literalAttribute.attribute}`, 'currencySymbol'],
211
+ attributes: ['endTime'],
212
+ }, {
213
+ rawAttributes: {
214
+ startTime: '', endTime: '', currencySymbol: '', currencyCode: '', startStationId: '',
215
+ },
216
+ }, {
217
+ literalAttributes: [literalAttribute],
218
+ });
219
+ expect(JSON.stringify(order)).toBe(JSON.stringify([[`${literalAttribute.attribute} DESC`], ['currencySymbol'], ['id']]));
220
+ expect(JSON.stringify(attributes))
221
+ .toBe(JSON.stringify([[literalAttribute.literal[0], literalAttribute.literal[1]], 'endTime']));
222
+ });
223
+ it('by literal field - not found', () => {
224
+ const literalAttribute = { attribute: 'genericField', literal: 'void' };
225
+ const { order, attributes } = _1.default({
226
+ order: [`-O${literalAttribute.attribute}`],
227
+ }, {
228
+ rawAttributes: {
229
+ startTime: '', endTime: '', currencySymbol: '', currencyCode: '', startStationId: '',
230
+ },
231
+ }, {
232
+ literalAttributes: [literalAttribute],
233
+ });
234
+ expect(JSON.stringify(order)).toBe(JSON.stringify([[`O${literalAttribute.attribute}`, 'DESC'], ['id']]));
235
+ expect(JSON.stringify(attributes)).toBe(JSON.stringify({ include: [] }));
236
+ });
237
+ });
238
+ describe('include formatting', () => {
239
+ it('include required should be true', () => {
240
+ const { include } = _1.default({
241
+ order: [],
242
+ include: [{
243
+ model: 'status',
244
+ }],
245
+ }, {
246
+ associations: {
247
+ status: {},
248
+ },
249
+ });
250
+ expect(include[0].required).toBe(true);
251
+ });
252
+ it('include required should be false', () => {
253
+ const { include } = _1.default({
254
+ order: [],
255
+ include: [{
256
+ model: 'status',
257
+ required: false,
258
+ }],
259
+ }, {
260
+ associations: {
261
+ status: {},
262
+ },
263
+ });
264
+ expect(include[0].required).toBe(false);
191
265
  });
192
266
  });
193
267
  });
package/lib/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  import { formatOperators } from './operators';
2
- import { queryFormatMiddleware, queryValidationMiddleware } from './middleware';
3
- export { formatOperators, queryFormatMiddleware, queryValidationMiddleware, };
2
+ import { queryFormatMiddleware, queryValidationMiddleware, MiddlewareValidationOption, LiteralAttribute } from './middleware';
3
+ export { formatOperators, queryFormatMiddleware, queryValidationMiddleware, MiddlewareValidationOption, LiteralAttribute, };
@@ -1,4 +1,16 @@
1
- export declare const newQueryValidationMiddleware: (inner?: string) => (model: any) => (req: any, res: any, next: any) => Promise<any>;
2
- export declare const newQueryFormatMiddleware: (inner?: string) => (model: any) => (req: any, res: any, next: any) => Promise<any>;
3
- export declare const queryValidationMiddleware: (model: any) => (req: any, res: any, next: any) => Promise<any>;
4
- export declare const queryFormatMiddleware: (model: any) => (req: any, res: any, next: any) => Promise<any>;
1
+ import { FormatPayloadOptions } from '../formatter';
2
+ declare type literal = any;
3
+ declare type LiteralQuery = (literal | string)[] | literal;
4
+ export declare type LiteralAttribute = {
5
+ attribute: string;
6
+ literal: LiteralQuery;
7
+ };
8
+ export declare type MiddlewareValidationOption = {
9
+ literalAttributes?: LiteralAttribute[];
10
+ enrichmentAttributes?: string[];
11
+ };
12
+ export declare const newQueryValidationMiddleware: (inner?: string) => (model: any, options?: MiddlewareValidationOption) => (req: any, res: any, next: any) => Promise<void>;
13
+ export declare const newQueryFormatMiddleware: (inner?: string) => (model: any, options?: FormatPayloadOptions) => (req: any, res: any, next: any) => Promise<void>;
14
+ export declare const queryValidationMiddleware: (model: any, options?: MiddlewareValidationOption) => (req: any, res: any, next: any) => Promise<void>;
15
+ export declare const queryFormatMiddleware: (model: any, options?: FormatPayloadOptions) => (req: any, res: any, next: any) => Promise<void>;
16
+ export {};
@@ -14,11 +14,26 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.queryFormatMiddleware = exports.queryValidationMiddleware = exports.newQueryFormatMiddleware = exports.newQueryValidationMiddleware = void 0;
16
16
  const errors_1 = require("@autofleet/errors");
17
+ const joi_1 = require("joi");
17
18
  const formatter_1 = __importDefault(require("../formatter"));
18
19
  const validations_1 = require("../validations");
19
- exports.newQueryValidationMiddleware = (inner = 'body') => model => (req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
20
+ const querySchema = joi_1.object({
21
+ query: joi_1.object(),
22
+ attributes: joi_1.array().items(joi_1.string()),
23
+ order: joi_1.array().items(joi_1.string()),
24
+ page: joi_1.number(),
25
+ perPage: joi_1.number(),
26
+ include: joi_1.array().items(joi_1.any()),
27
+ searchTerm: joi_1.string(),
28
+ enrichmentAttributes: joi_1.array().items(joi_1.string()),
29
+ });
30
+ exports.newQueryValidationMiddleware = (inner = 'body') => (model, options = {}) => (req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
20
31
  const { query, attributes, order, page, perPage, include, } = req[inner];
21
32
  try {
33
+ const result = querySchema.validate(req[inner]);
34
+ if (result.error) {
35
+ throw new errors_1.BadRequest([result.error], null);
36
+ }
22
37
  validations_1.validatePayload({
23
38
  query,
24
39
  attributes,
@@ -26,7 +41,7 @@ exports.newQueryValidationMiddleware = (inner = 'body') => model => (req, res, n
26
41
  page,
27
42
  perPage,
28
43
  include,
29
- }, model);
44
+ }, model, options);
30
45
  return next();
31
46
  }
32
47
  catch (error) {
@@ -41,9 +56,9 @@ exports.newQueryValidationMiddleware = (inner = 'body') => model => (req, res, n
41
56
  });
42
57
  }
43
58
  });
44
- exports.newQueryFormatMiddleware = (inner = 'body') => model => (req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
59
+ exports.newQueryFormatMiddleware = (inner = 'body') => (model, options = {}) => (req, res, next) => __awaiter(void 0, void 0, void 0, function* () {
45
60
  const { order, page, perPage, include, query, attributes, searchTerm, } = req[inner];
46
- const { query: formattedQuery, order: formattedOrder, page: formattedPage, perPage: formattedPerPage, include: formattedInclude, } = formatter_1.default({
61
+ const { query: formattedQuery, order: formattedOrder, page: formattedPage, perPage: formattedPerPage, include: formattedInclude, scopes: formattedScopes, attributes: formattedAttribute, } = formatter_1.default({
47
62
  query,
48
63
  order,
49
64
  page,
@@ -51,12 +66,25 @@ exports.newQueryFormatMiddleware = (inner = 'body') => model => (req, res, next)
51
66
  include,
52
67
  attributes,
53
68
  searchTerm,
54
- }, model);
69
+ }, model, options);
55
70
  req[inner].query = formattedQuery;
56
71
  req[inner].order = formattedOrder;
72
+ req[inner].attributes = formattedAttribute;
57
73
  req[inner].page = formattedPage;
58
74
  req[inner].perPage = formattedPerPage;
59
75
  req[inner].include = formattedInclude;
76
+ req[inner].scopes = formattedScopes;
77
+ if (options.includeRawPayload) {
78
+ req[inner].rawPayload = {
79
+ order,
80
+ page,
81
+ perPage,
82
+ include,
83
+ query,
84
+ attributes,
85
+ searchTerm,
86
+ };
87
+ }
60
88
  return next();
61
89
  });
62
90
  exports.queryValidationMiddleware = exports.newQueryValidationMiddleware();
@@ -1,3 +1,20 @@
1
1
  export declare const OPERATORS: string[];
2
2
  export declare const OPERATOR_PREFIX = "$";
3
+ export declare const OPERATORS_TO_SQL: {
4
+ $eq: string;
5
+ $ne: string;
6
+ $gte: string;
7
+ $gt: string;
8
+ $lte: string;
9
+ $lt: string;
10
+ $not: string;
11
+ $in: string;
12
+ $notIn: string;
13
+ $is: string;
14
+ $like: string;
15
+ $iLike: string;
16
+ $notLike: string;
17
+ $and: string;
18
+ $or: string;
19
+ };
3
20
  export declare const formatOperators: (Sequelize: any) => {};
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.formatOperators = exports.OPERATOR_PREFIX = exports.OPERATORS = void 0;
3
+ exports.formatOperators = exports.OPERATORS_TO_SQL = exports.OPERATOR_PREFIX = exports.OPERATORS = void 0;
4
4
  exports.OPERATORS = [
5
5
  'eq',
6
6
  'ne',
@@ -22,6 +22,23 @@ exports.OPERATORS = [
22
22
  'contains',
23
23
  ];
24
24
  exports.OPERATOR_PREFIX = '$';
25
+ exports.OPERATORS_TO_SQL = {
26
+ $eq: '=',
27
+ $ne: '!=',
28
+ $gte: '>=',
29
+ $gt: '>',
30
+ $lte: '<=',
31
+ $lt: '<',
32
+ $not: 'NOT',
33
+ $in: 'IN',
34
+ $notIn: 'NOT IN',
35
+ $is: 'IS',
36
+ $like: 'LIKE',
37
+ $iLike: 'ILIKE',
38
+ $notLike: 'NOT LIKE',
39
+ $and: 'AND',
40
+ $or: 'OR',
41
+ };
25
42
  exports.formatOperators = (Sequelize) => {
26
43
  const { Op } = Sequelize;
27
44
  return exports.OPERATORS.reduce((map, o) => {
package/lib/utils.d.ts CHANGED
@@ -11,3 +11,4 @@ export declare const extractAttributeNameFromOrder: (order: any, associationMode
11
11
  export declare const isOrderDesc: (order: any) => boolean;
12
12
  export declare const throwBadRequestError: (message: string) => never;
13
13
  export declare const extractAssociatedAttributeNameFromOrder: (order: any) => string;
14
+ export declare const generateRandomString: (length?: number) => string;
package/lib/utils.js CHANGED
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.extractAssociatedAttributeNameFromOrder = exports.throwBadRequestError = exports.isOrderDesc = exports.extractAttributeNameFromOrder = exports.isAttributeByAssociation = exports.wrapAttributeWithOperator = exports.PAGE_MIN = exports.PER_PAGE_MIN_LIMIT = exports.PER_PAGE_MAX_LIMIT = exports.PAGE_DEFAULT = exports.PER_PAGE_DEFAULT = exports.ASSOCIATION_PREFIX = exports.ORDER_PREFIX = void 0;
3
+ exports.generateRandomString = exports.extractAssociatedAttributeNameFromOrder = exports.throwBadRequestError = exports.isOrderDesc = exports.extractAttributeNameFromOrder = exports.isAttributeByAssociation = exports.wrapAttributeWithOperator = exports.PAGE_MIN = exports.PER_PAGE_MIN_LIMIT = exports.PER_PAGE_MAX_LIMIT = exports.PAGE_DEFAULT = exports.PER_PAGE_DEFAULT = exports.ASSOCIATION_PREFIX = exports.ORDER_PREFIX = void 0;
4
4
  const errors_1 = require("@autofleet/errors");
5
5
  const operators_1 = require("./operators");
6
+ const randomInt = max => Math.floor(Math.random() * Math.floor(max));
6
7
  exports.ORDER_PREFIX = '-';
7
8
  exports.ASSOCIATION_PREFIX = '.';
8
9
  exports.PER_PAGE_DEFAULT = 20;
@@ -29,3 +30,7 @@ exports.throwBadRequestError = (message) => {
29
30
  throw new errors_1.BadRequest([{ message }], null);
30
31
  };
31
32
  exports.extractAssociatedAttributeNameFromOrder = (order) => order.split(exports.ASSOCIATION_PREFIX)[1];
33
+ exports.generateRandomString = (length = 5) => {
34
+ const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
35
+ return Array.from({ length }, () => characters.charAt(randomInt(characters.length))).join('');
36
+ };
@@ -1,8 +1,10 @@
1
- export declare const validatePayload: ({ query, order, attributes, include, page, perPage, }: {
1
+ import type { MiddlewareValidationOption } from '../middleware';
2
+ export declare const validatePayload: ({ query, order, attributes, include, page, perPage, enrichments, }: {
2
3
  query?: {};
3
4
  order?: any[];
4
5
  attributes?: any[];
5
6
  include?: any[];
6
7
  page?: number;
7
8
  perPage?: number;
8
- }, model?: any) => boolean;
9
+ enrichments?: any[];
10
+ }, model?: any, options?: MiddlewareValidationOption) => boolean;
@@ -11,7 +11,8 @@ const validateOperator = (operator) => operators_1.OPERATORS.includes(operator.s
11
11
  const validateQueryAttribute = (attribute, modelAttributes = [], associationModels = []) => [...modelAttributes, ...associationModels]
12
12
  .includes(attribute.includes(utils_1.ASSOCIATION_PREFIX)
13
13
  ? attribute.split(utils_1.ASSOCIATION_PREFIX)[0] : attribute);
14
- const validateSingleOrder = (currentOrder, rawAttributes, associationModels) => {
14
+ const validateSingleOrder = (currentOrder, rawAttributes, associationModels, options = {}) => {
15
+ var _a, _b;
15
16
  const isOrderDescOrder = utils_1.isOrderDesc(currentOrder);
16
17
  if (isOrderDescOrder && currentOrder[0] !== utils_1.ORDER_PREFIX) {
17
18
  utils_1.throwBadRequestError(`${utils_1.ORDER_PREFIX} must be only at the beginning of the word`);
@@ -20,11 +21,12 @@ const validateSingleOrder = (currentOrder, rawAttributes, associationModels) =>
20
21
  currentOrder.split(utils_1.ORDER_PREFIX)[1] : currentOrder;
21
22
  const isOrderAssociation = utils_1.isAttributeByAssociation(orderStringWithoutDesc, associationModels);
22
23
  let formattedOrderString = utils_1.extractAttributeNameFromOrder(currentOrder, associationModels);
24
+ const isLiteralAttribute = (_b = (_a = options === null || options === void 0 ? void 0 : options.literalAttributes) === null || _a === void 0 ? void 0 : _a.map(la => la.attribute)) === null || _b === void 0 ? void 0 : _b.includes(orderStringWithoutDesc);
23
25
  if (!isOrderAssociation && formattedOrderString.includes(utils_1.ASSOCIATION_PREFIX)) {
24
26
  [formattedOrderString] = formattedOrderString.split(utils_1.ASSOCIATION_PREFIX);
25
27
  }
26
- if (!(rawAttributes.includes(formattedOrderString) || isOrderAssociation)) {
27
- utils_1.throwBadRequestError(`${currentOrder} is invalid`);
28
+ if (!(rawAttributes.includes(formattedOrderString) || isOrderAssociation || isLiteralAttribute)) {
29
+ utils_1.throwBadRequestError(`${currentOrder} is invalid. isLiteralAttribute: ${isLiteralAttribute}`);
28
30
  }
29
31
  };
30
32
  const validateSingleAttribute = (currentAttribute, rawAttributes) => {
@@ -32,17 +34,27 @@ const validateSingleAttribute = (currentAttribute, rawAttributes) => {
32
34
  utils_1.throwBadRequestError(`${currentAttribute} is invalid`);
33
35
  }
34
36
  };
35
- const validateOrderAttributes = (order, rawAttributes, associationModels = []) => {
36
- order.map(o => validateSingleOrder(o, rawAttributes, associationModels));
37
+ const validateOrderAttributes = (order, rawAttributes, associationModels = [], options = {}) => {
38
+ order.map(o => validateSingleOrder(o, rawAttributes, associationModels, options));
37
39
  };
38
40
  const validateAttributes = (attributes, rawAttributes) => {
39
41
  attributes.map(a => validateSingleAttribute(a, rawAttributes));
40
42
  };
43
+ const validateEnrichments = (enrichments, options) => {
44
+ if (!(enrichments === null || enrichments === void 0 ? void 0 : enrichments.length)) {
45
+ return;
46
+ }
47
+ const invalidEnrichment = enrichments
48
+ .find(enrichment => { var _a; return !((_a = options === null || options === void 0 ? void 0 : options.enrichmentAttributes) === null || _a === void 0 ? void 0 : _a.includes(enrichment)); });
49
+ if (invalidEnrichment) {
50
+ utils_1.throwBadRequestError(`enrichment attribute ${enrichments} is invalid`);
51
+ }
52
+ };
41
53
  const validateQueryPayload = (query, rawAttributes, associationModels = []) => {
42
54
  // eslint-disable-next-line array-callback-return
43
55
  Object.keys(query).map((key) => {
44
56
  const value = query[key];
45
- if (lodash_1.default.isArray(value)) {
57
+ if (Array.isArray(value)) {
46
58
  if (lodash_1.default.isObject(value[0])) {
47
59
  value.map(v => validateQueryPayload(v, rawAttributes, associationModels));
48
60
  }
@@ -76,21 +88,22 @@ const validateIncludePayload = (include, associations) => {
76
88
  utils_1.throwBadRequestError('model not found in associations');
77
89
  }
78
90
  const { rawAttributes } = target;
79
- if (include.where) {
80
- validateQueryPayload(i.where, rawAttributes);
91
+ const attributeKeys = Object.keys(rawAttributes);
92
+ if (i.where) {
93
+ validateQueryPayload(i.where, attributeKeys);
81
94
  }
82
- if (include.order) {
83
- validateOrderAttributes(i.order, rawAttributes);
95
+ if (i.order) {
96
+ validateOrderAttributes(i.order, attributeKeys);
84
97
  }
85
- if (include.attributes) {
86
- validateAttributes(i.attributes, rawAttributes);
98
+ if (i.attributes) {
99
+ validateAttributes(i.attributes, attributeKeys);
87
100
  }
88
- if (!lodash_1.default.isNil(include.required) && !lodash_1.default.isBoolean(include.required)) {
101
+ if (!lodash_1.default.isNil(i.required) && !lodash_1.default.isBoolean(i.required)) {
89
102
  utils_1.throwBadRequestError('include.required must be a boolean');
90
103
  }
91
104
  });
92
105
  };
93
- exports.validatePayload = ({ query = {}, order = [], attributes = [], include = [], page = utils_1.PAGE_DEFAULT, perPage = utils_1.PER_PAGE_DEFAULT, }, model) => {
106
+ exports.validatePayload = ({ query = {}, order = [], attributes = [], include = [], page = utils_1.PAGE_DEFAULT, perPage = utils_1.PER_PAGE_DEFAULT, enrichments = [], }, model, options = {}) => {
94
107
  const rawAttributes = Object.keys(model.rawAttributes);
95
108
  const associationModels = Object.keys((model === null || model === void 0 ? void 0 : model.associations) || {});
96
109
  if (!attributes || attributes.length === 0) {
@@ -100,11 +113,15 @@ exports.validatePayload = ({ query = {}, order = [], attributes = [], include =
100
113
  else {
101
114
  validateAttributes(attributes, rawAttributes);
102
115
  }
103
- validateOrderAttributes(order, rawAttributes, associationModels);
116
+ validateOrderAttributes(order, rawAttributes, associationModels, options);
104
117
  validateQueryPayload(query, rawAttributes, associationModels);
105
- if (include.length) {
118
+ validateEnrichments(enrichments, options);
119
+ if (include.length && typeof include === 'object') {
106
120
  validateIncludePayload(include, model === null || model === void 0 ? void 0 : model.associations);
107
121
  }
122
+ else if (include && typeof include !== 'object') {
123
+ utils_1.throwBadRequestError('include must be an array');
124
+ }
108
125
  validatePagination({
109
126
  page,
110
127
  perPage,
@@ -180,5 +180,40 @@ describe('validations test', () => {
180
180
  expect(error.statusCode).toBe(400);
181
181
  }
182
182
  });
183
+ it('try order by literal field that exist', () => {
184
+ const literalAttribute = { attribute: 'genericField', literal: 'void' };
185
+ const payload = _1.validatePayload({ order: [literalAttribute.attribute] }, {
186
+ rawAttributes: {
187
+ startTime: '', endTime: '', currencySymbol: '', currencyCode: '', startStationId: '', status: '',
188
+ },
189
+ }, {
190
+ literalAttributes: [literalAttribute],
191
+ });
192
+ expect(payload).toBe(true);
193
+ });
194
+ it('try order by literal field that exist - desc', () => {
195
+ const literalAttribute = { attribute: 'genericField', literal: 'void' };
196
+ const payload = _1.validatePayload({ order: [`-${literalAttribute.attribute}`] }, {
197
+ rawAttributes: {
198
+ startTime: '', endTime: '', currencySymbol: '', currencyCode: '', startStationId: '', status: '',
199
+ },
200
+ }, {
201
+ literalAttributes: [literalAttribute],
202
+ });
203
+ expect(payload).toBe(true);
204
+ });
205
+ it('try order by literal field that doesnt exist', () => {
206
+ const literalAttribute = { attribute: 'genericField', literal: 'void' };
207
+ try {
208
+ _1.validatePayload({ order: ['here'] }, {
209
+ rawAttributes: {
210
+ startTime: '', endTime: '', currencySymbol: '', currencyCode: '', startStationId: '', status: '',
211
+ },
212
+ }, { literalAttributes: [literalAttribute] });
213
+ }
214
+ catch (error) {
215
+ expect(error.statusCode).toBe(400);
216
+ }
217
+ });
183
218
  });
184
219
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autofleet/sheilta",
3
- "version": "1.0.0-aaron-error-fix",
3
+ "version": "1.0.0-beta-test-2",
4
4
  "description": "manage cache",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -10,7 +10,24 @@
10
10
  "coverage": "jest --coverage --forceExit --runInBand",
11
11
  "test": "jest --forceExit --runInBand",
12
12
  "test-auto": "jest --watch --runInBand",
13
- "linter": "./node_modules/.bin/eslint src/**/*.ts"
13
+ "linter": "./node_modules/.bin/eslint src/**/*.ts",
14
+ "build-to-local-repo": "npm run build && cp -r lib/* ../$REPO/node_modules/$npm_package_name/lib",
15
+ "watch": "npm-watch build-to-local-repo"
16
+ },
17
+ "watch": {
18
+ "build-to-local-repo": {
19
+ "extensions": [
20
+ "js",
21
+ "jsx",
22
+ "ts",
23
+ "tsx",
24
+ "css"
25
+ ],
26
+ "patterns": [
27
+ "src"
28
+ ],
29
+ "quiet": false
30
+ }
14
31
  },
15
32
  "jest": {
16
33
  "setupTestFrameworkScriptFile": "jest-extended",
@@ -27,21 +44,24 @@
27
44
  },
28
45
  "homepage": "https://github.com/Autofleet/sheilta",
29
46
  "dependencies": {
47
+ "@autofleet/common-types": "^1.7.42",
30
48
  "@autofleet/errors": "^1.0.10",
31
- "@types/node": "^14.14.20",
49
+ "joi": "^17.9.2",
32
50
  "lodash": "^4.17.21"
33
51
  },
34
52
  "devDependencies": {
35
- "typescript": "^3.9.5",
53
+ "@types/jest": "^24.9.0",
54
+ "@types/node": "^16.11.68",
55
+ "@typescript-eslint/eslint-plugin": "^2.34.0",
56
+ "@typescript-eslint/parser": "^2.23.0",
36
57
  "eslint": "^6.8.0",
37
58
  "eslint-config-airbnb": "^16.1.0",
38
59
  "eslint-plugin-import": "^2.20.1",
39
- "@typescript-eslint/eslint-plugin": "^2.34.0",
40
- "@typescript-eslint/parser": "^2.23.0",
41
- "typescript-eslint": "0.0.1-alpha.0",
42
- "ts-jest": "^25.0.0",
43
60
  "jest": "^25.1.0",
44
- "@types/jest": "^24.9.0"
61
+ "ts-jest": "^25.0.0",
62
+ "typescript": "^3.9.5",
63
+ "typescript-eslint": "0.0.1-alpha.0",
64
+ "npm-watch": "^0.13.0"
45
65
  },
46
66
  "files": [
47
67
  "lib/**/*"