@kollors/deep-json-server 0.7.0 → 0.8.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 (107) hide show
  1. package/README.md +12 -9
  2. package/README.ru.md +12 -9
  3. package/dist/bin/deep-json-server.d.ts +2 -0
  4. package/dist/bin/deep-json-server.js +11 -0
  5. package/dist/bin/deep-json-server.js.map +1 -0
  6. package/dist/index.d.ts +5 -0
  7. package/dist/index.js +2 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/src/cli.d.ts +4 -0
  10. package/dist/src/cli.js +80 -0
  11. package/dist/src/cli.js.map +1 -0
  12. package/dist/src/config.d.ts +54 -0
  13. package/dist/src/config.js +145 -0
  14. package/dist/src/config.js.map +1 -0
  15. package/{src → dist/src}/constants.js +1 -0
  16. package/dist/src/constants.js.map +1 -0
  17. package/dist/src/database.d.ts +20 -0
  18. package/dist/src/database.js +136 -0
  19. package/dist/src/database.js.map +1 -0
  20. package/dist/src/files/contract.d.ts +67 -0
  21. package/dist/src/files/contract.js +106 -0
  22. package/dist/src/files/contract.js.map +1 -0
  23. package/dist/src/files/disk-store.d.ts +5 -0
  24. package/dist/src/files/disk-store.js +313 -0
  25. package/dist/src/files/disk-store.js.map +1 -0
  26. package/dist/src/files/index.d.ts +5 -0
  27. package/dist/src/files/index.js +6 -0
  28. package/dist/src/files/index.js.map +1 -0
  29. package/dist/src/files/memory-store.d.ts +3 -0
  30. package/dist/src/files/memory-store.js +80 -0
  31. package/dist/src/files/memory-store.js.map +1 -0
  32. package/dist/src/files/routes.d.ts +6 -0
  33. package/dist/src/files/routes.js +77 -0
  34. package/dist/src/files/routes.js.map +1 -0
  35. package/dist/src/openapi/config.d.ts +11 -0
  36. package/dist/src/openapi/config.js +204 -0
  37. package/dist/src/openapi/config.js.map +1 -0
  38. package/dist/src/openapi/document.d.ts +9 -0
  39. package/dist/src/openapi/document.js +338 -0
  40. package/dist/src/openapi/document.js.map +1 -0
  41. package/dist/src/openapi/index.d.ts +10 -0
  42. package/dist/src/openapi/index.js +27 -0
  43. package/dist/src/openapi/index.js.map +1 -0
  44. package/dist/src/openapi/inference.d.ts +14 -0
  45. package/dist/src/openapi/inference.js +145 -0
  46. package/dist/src/openapi/inference.js.map +1 -0
  47. package/dist/src/query/filter.d.ts +6 -0
  48. package/dist/src/query/filter.js +248 -0
  49. package/dist/src/query/filter.js.map +1 -0
  50. package/{src → dist/src}/query/index.js +1 -0
  51. package/dist/src/query/index.js.map +1 -0
  52. package/dist/src/query/pagination.d.ts +11 -0
  53. package/dist/src/query/pagination.js +28 -0
  54. package/dist/src/query/pagination.js.map +1 -0
  55. package/dist/src/query/sort.d.ts +1 -0
  56. package/dist/src/query/sort.js +54 -0
  57. package/dist/src/query/sort.js.map +1 -0
  58. package/dist/src/relation-metadata.d.ts +10 -0
  59. package/dist/src/relation-metadata.js +32 -0
  60. package/dist/src/relation-metadata.js.map +1 -0
  61. package/dist/src/relations.d.ts +12 -0
  62. package/dist/src/relations.js +155 -0
  63. package/dist/src/relations.js.map +1 -0
  64. package/dist/src/server.d.ts +13 -0
  65. package/dist/src/server.js +188 -0
  66. package/dist/src/server.js.map +1 -0
  67. package/dist/src/types.d.ts +52 -0
  68. package/dist/src/types.js +2 -0
  69. package/dist/src/types.js.map +1 -0
  70. package/dist/src/utils.d.ts +20 -0
  71. package/dist/src/utils.js +63 -0
  72. package/dist/src/utils.js.map +1 -0
  73. package/package.json +16 -15
  74. package/bin/deep-json-server.js +0 -11
  75. package/index.js +0 -12
  76. package/src/cli.js +0 -91
  77. package/src/config.js +0 -203
  78. package/src/database.js +0 -148
  79. package/src/files.js +0 -633
  80. package/src/openapi/config.js +0 -124
  81. package/src/openapi/document.js +0 -394
  82. package/src/openapi/index.js +0 -36
  83. package/src/openapi/inference.js +0 -187
  84. package/src/query/filter.js +0 -283
  85. package/src/query/pagination.js +0 -44
  86. package/src/query/sort.js +0 -63
  87. package/src/relation-metadata.js +0 -41
  88. package/src/relations.js +0 -173
  89. package/src/server.js +0 -250
  90. package/src/utils.js +0 -84
  91. package/types/index.d.ts +0 -21
  92. package/types/src/config.d.ts +0 -73
  93. package/types/src/database.d.ts +0 -33
  94. package/types/src/files.d.ts +0 -64
  95. package/types/src/openapi/config.d.ts +0 -3
  96. package/types/src/openapi/document.d.ts +0 -11
  97. package/types/src/openapi/index.d.ts +0 -9
  98. package/types/src/openapi/inference.d.ts +0 -9
  99. package/types/src/query/filter.d.ts +0 -3
  100. package/types/src/query/pagination.d.ts +0 -8
  101. package/types/src/query/sort.d.ts +0 -1
  102. package/types/src/relation-metadata.d.ts +0 -9
  103. package/types/src/relations.d.ts +0 -3
  104. package/types/src/server.d.ts +0 -14
  105. package/types/src/utils.d.ts +0 -18
  106. /package/{types → dist}/src/constants.d.ts +0 -0
  107. /package/{types → dist}/src/query/index.d.ts +0 -0
@@ -1,187 +0,0 @@
1
- import { isObject } from '../utils.js';
2
-
3
- export const mergeSchemas = (schemas) => {
4
- const uniqueSchemas = [...new Map(schemas.map((schema) => [JSON.stringify(schema), schema])).values()];
5
- const nullable = uniqueSchemas.some((schema) => schema.type === 'null');
6
- const nonNullSchemas = uniqueSchemas.filter((schema) => schema.type !== 'null');
7
-
8
- if (nonNullSchemas.length === 0) {
9
- return { nullable: true };
10
- }
11
-
12
- if (nonNullSchemas.length === 1) {
13
- return nullable ? { ...nonNullSchemas[0], nullable: true } : nonNullSchemas[0];
14
- }
15
-
16
- return { oneOf: nonNullSchemas, ...(nullable && { nullable: true }) };
17
- };
18
-
19
- export const mergeSchemaOverrides = (schema, overrides) => {
20
- if (!isObject(overrides)) {
21
- return schema;
22
- }
23
-
24
- const result = { ...schema, ...overrides };
25
-
26
- if (Object.hasOwn(overrides, 'type') && !Object.hasOwn(overrides, 'oneOf')) {
27
- delete result.oneOf;
28
- }
29
-
30
- if (isObject(schema.properties) || isObject(overrides.properties)) {
31
- const properties = isObject(schema.properties) ? { ...schema.properties } : {};
32
-
33
- Object.entries(isObject(overrides.properties) ? overrides.properties : {}).forEach(([key, value]) => {
34
- properties[key] = mergeSchemaOverrides(properties[key] ?? {}, value);
35
- });
36
-
37
- result.properties = properties;
38
- }
39
-
40
- if (isObject(schema.items) && isObject(overrides.items)) {
41
- result.items = mergeSchemaOverrides(schema.items, overrides.items);
42
- }
43
-
44
- return result;
45
- };
46
-
47
- export const applyRequiredFields = (schema, path, requiredFields) => {
48
- if (Array.isArray(schema.oneOf)) {
49
- return { ...schema, oneOf: schema.oneOf.map((nestedSchema) => applyRequiredFields(nestedSchema, path, requiredFields)) };
50
- }
51
-
52
- if (schema.type === 'array') {
53
- return { ...schema, items: applyRequiredFields(schema.items ?? {}, path, requiredFields) };
54
- }
55
-
56
- if (schema.type !== 'object' || !isObject(schema.properties)) {
57
- return schema;
58
- }
59
-
60
- const properties = Object.fromEntries(
61
- Object.entries(schema.properties).map(([key, value]) => {
62
- const fieldPath = path === '' ? key : `${path}.${key}`;
63
-
64
- return [key, applyRequiredFields(value, fieldPath, requiredFields)];
65
- }),
66
- );
67
- const required = Object.keys(properties).filter((key) => {
68
- const fieldPath = path === '' ? key : `${path}.${key}`;
69
-
70
- return (path === '' && key === 'id') || requiredFields.has(fieldPath);
71
- });
72
- const result = { ...schema, properties };
73
-
74
- delete result.required;
75
-
76
- return required.length === 0 ? result : { ...result, required };
77
- };
78
-
79
- export const inferSchema = (values) => {
80
- const schemas = [];
81
- const arrays = values.filter(Array.isArray);
82
- const objects = values.filter(isObject);
83
-
84
- if (arrays.length > 0) {
85
- const items = arrays.flat();
86
-
87
- schemas.push({ items: items.length === 0 ? {} : inferSchema(items), type: 'array' });
88
- }
89
-
90
- if (objects.length > 0) {
91
- schemas.push(inferObjectSchema(objects));
92
- }
93
-
94
- values
95
- .filter((value) => !Array.isArray(value) && !isObject(value))
96
- .forEach((value) => {
97
- if (value === null) {
98
- schemas.push({ type: 'null' });
99
- } else if (typeof value === 'number') {
100
- schemas.push({ type: Number.isInteger(value) ? 'integer' : 'number' });
101
- } else {
102
- schemas.push({ type: typeof value });
103
- }
104
- });
105
-
106
- return mergeSchemas(schemas);
107
- };
108
-
109
- export function inferObjectSchema(values) {
110
- const keys = [...new Set(values.flatMap((value) => Object.keys(value)))].sort((left, right) => (left === 'id' ? -1 : right === 'id' ? 1 : left.localeCompare(right)));
111
- const properties = Object.fromEntries(
112
- keys.map((key) => {
113
- const fieldValues = values.filter((value) => Object.hasOwn(value, key)).map((value) => value[key]);
114
-
115
- return [key, inferSchema(fieldValues)];
116
- }),
117
- );
118
-
119
- return { properties, type: 'object' };
120
- }
121
-
122
- export const ensureGeneratedIdSchema = (schema) => {
123
- const idSchema = schema.properties?.id ?? {};
124
- const idSchemas = Array.isArray(idSchema.oneOf) ? idSchema.oneOf : [idSchema];
125
- const properties = {
126
- ...schema.properties,
127
- id: idSchemas.some(({ type }) => type === 'string') ? idSchema : { oneOf: [...idSchemas, { type: 'string' }] },
128
- };
129
-
130
- return { ...schema, properties };
131
- };
132
-
133
- export const getSchemasAtPath = (schema, keys) => {
134
- if (Array.isArray(schema.oneOf)) {
135
- return schema.oneOf.flatMap((nestedSchema) => getSchemasAtPath(nestedSchema, keys));
136
- }
137
-
138
- if (schema.type === 'array') {
139
- return getSchemasAtPath(schema.items ?? {}, keys);
140
- }
141
-
142
- if (keys.length === 0) {
143
- return [schema];
144
- }
145
-
146
- if (schema.type !== 'object' || !isObject(schema.properties) || !Object.hasOwn(schema.properties, keys[0])) {
147
- return [];
148
- }
149
-
150
- return getSchemasAtPath(schema.properties[keys[0]], keys.slice(1));
151
- };
152
-
153
- export const updateSchemasAtPath = (schema, keys, update) => {
154
- if (Array.isArray(schema.oneOf)) {
155
- return { ...schema, oneOf: schema.oneOf.map((nestedSchema) => updateSchemasAtPath(nestedSchema, keys, update)) };
156
- }
157
-
158
- if (schema.type === 'array') {
159
- return { ...schema, items: updateSchemasAtPath(schema.items ?? {}, keys, update) };
160
- }
161
-
162
- if (keys.length === 0) {
163
- return update(schema);
164
- }
165
-
166
- if (schema.type !== 'object' || !isObject(schema.properties) || !Object.hasOwn(schema.properties, keys[0])) {
167
- return schema;
168
- }
169
-
170
- return {
171
- ...schema,
172
- properties: {
173
- ...schema.properties,
174
- [keys[0]]: updateSchemasAtPath(schema.properties[keys[0]], keys.slice(1), update),
175
- },
176
- };
177
- };
178
-
179
- export const omitId = (schema, keepRequired) => {
180
- const properties = Object.fromEntries(Object.entries(schema.properties ?? {}).filter(([key]) => key !== 'id'));
181
- const required = keepRequired ? schema.required?.filter((key) => key !== 'id') : undefined;
182
- const result = { ...schema, properties };
183
-
184
- delete result.required;
185
-
186
- return required?.length > 0 ? { ...result, required } : result;
187
- };
@@ -1,283 +0,0 @@
1
- import { createHttpError, isEqual, isObject, isSafeKey, toArray } from '../utils.js';
2
-
3
- const FIELD_OPERATORS = new Set(['contains', 'endsWith', 'eq', 'every', 'gt', 'gte', 'in', 'lt', 'lte', 'ne', 'none', 'not', 'some', 'startsWith']);
4
- const RESERVED_QUERY_KEYS = new Set(['_embed', '_page', '_perPage', '_sort', '_where']);
5
- const NUMBER_PATTERN = /^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
6
-
7
- const isFilterEqual = (left, right) => {
8
- if (isEqual(left, right)) {
9
- return true;
10
- }
11
-
12
- if (typeof left === 'number' && typeof right === 'string' && NUMBER_PATTERN.test(right)) {
13
- // Query parameters are strings, so numeric strings must match stored numbers.
14
- return left === Number(right);
15
- }
16
-
17
- return typeof right === 'number' && typeof left === 'string' && NUMBER_PATTERN.test(left) && right === Number(left);
18
- };
19
-
20
- const validateLogicalConditions = (operator, value) => {
21
- if (!Array.isArray(value) || (operator === 'or' && value.length === 0) || value.some((condition) => !isObject(condition))) {
22
- throw createHttpError(400, `Оператор «${operator}» должен содержать ${operator === 'or' ? 'непустой ' : ''}массив JSON-объектов`);
23
- }
24
-
25
- return value;
26
- };
27
-
28
- const matchesOperator = (field, operator, expectedValue) => {
29
- switch (operator) {
30
- case 'contains':
31
- return typeof field === 'string' ? field.toLowerCase().includes(String(expectedValue).toLowerCase()) : Array.isArray(field) && field.some((value) => isFilterEqual(value, expectedValue));
32
- case 'endsWith':
33
- return typeof field === 'string' && field.toLowerCase().endsWith(String(expectedValue).toLowerCase());
34
- case 'eq':
35
- return isFilterEqual(field, expectedValue);
36
- case 'every':
37
- return Array.isArray(field) && field.every((value) => matchesValue(value, expectedValue));
38
- case 'gt':
39
- return field != null && field > expectedValue;
40
- case 'gte':
41
- return field != null && field >= expectedValue;
42
- case 'in': {
43
- const expectedValues = toArray(expectedValue);
44
-
45
- return Array.isArray(field)
46
- ? field.some((value) => expectedValues.some((expectedItem) => isFilterEqual(value, expectedItem)))
47
- : expectedValues.some((expectedItem) => isFilterEqual(field, expectedItem));
48
- }
49
- case 'lt':
50
- return field != null && field < expectedValue;
51
- case 'lte':
52
- return field != null && field <= expectedValue;
53
- case 'ne':
54
- return !isFilterEqual(field, expectedValue);
55
- case 'none':
56
- return Array.isArray(field) && !field.some((value) => matchesValue(value, expectedValue));
57
- case 'not':
58
- return !matchesValue(field, expectedValue);
59
- case 'some':
60
- return Array.isArray(field) && field.some((value) => matchesValue(value, expectedValue));
61
- case 'startsWith':
62
- return typeof field === 'string' && field.toLowerCase().startsWith(String(expectedValue).toLowerCase());
63
- default:
64
- return false;
65
- }
66
- };
67
-
68
- function matchesValue(field, condition) {
69
- if (!isObject(condition)) {
70
- return isFilterEqual(field, condition);
71
- }
72
-
73
- const conditionEntries = Object.entries(condition);
74
- const operatorEntries = conditionEntries.filter(([operator]) => FIELD_OPERATORS.has(operator));
75
- const nestedEntries = conditionEntries.filter(([key]) => !FIELD_OPERATORS.has(key));
76
-
77
- if (!operatorEntries.every(([operator, expectedValue]) => matchesOperator(field, operator, expectedValue))) {
78
- return false;
79
- }
80
-
81
- return nestedEntries.length === 0 || (isObject(field) && matchesWhere(field, Object.fromEntries(nestedEntries)));
82
- }
83
-
84
- export function matchesWhere(value, where) {
85
- if (!isObject(value) || !isObject(where)) {
86
- return false;
87
- }
88
-
89
- return Object.entries(where).every(([key, condition]) => {
90
- if (key === 'and') {
91
- return Array.isArray(condition) && condition.every((nestedWhere) => matchesWhere(value, nestedWhere));
92
- }
93
-
94
- if (key === 'or') {
95
- return Array.isArray(condition) && condition.length > 0 && condition.some((nestedWhere) => matchesWhere(value, nestedWhere));
96
- }
97
-
98
- if (key === 'not') {
99
- return isObject(condition) && !matchesWhere(value, condition);
100
- }
101
-
102
- return isSafeKey(key) && matchesValue(value[key], condition);
103
- });
104
- }
105
-
106
- const parsePrimitive = (value) => {
107
- if (typeof value !== 'string') {
108
- return value;
109
- }
110
-
111
- if (value === 'true') {
112
- return true;
113
- }
114
-
115
- if (value === 'false') {
116
- return false;
117
- }
118
-
119
- if (value === 'null') {
120
- return null;
121
- }
122
-
123
- return NUMBER_PATTERN.test(value) && Number.isFinite(Number(value)) ? Number(value) : value;
124
- };
125
-
126
- const parseFilterKey = (key) => {
127
- const colonIndex = key.lastIndexOf(':');
128
-
129
- if (colonIndex !== -1) {
130
- const path = key.slice(0, colonIndex);
131
- const operator = key.slice(colonIndex + 1);
132
-
133
- if (!FIELD_OPERATORS.has(operator)) {
134
- throw createHttpError(400, `Неизвестный оператор «${operator}» в фильтре «${key}»`);
135
- }
136
-
137
- return { operator, path };
138
- }
139
-
140
- const legacyOperator = key.match(/^(.*)_([a-zA-Z]+)$/);
141
-
142
- return legacyOperator?.[1] != null && legacyOperator[2] != null && FIELD_OPERATORS.has(legacyOperator[2]) ? { operator: legacyOperator[2], path: legacyOperator[1] } : { operator: 'eq', path: key };
143
- };
144
-
145
- const setWhereOperator = (where, path, operator, value) => {
146
- const keys = path.split('.');
147
-
148
- if (keys.some((key) => key === '' || !isSafeKey(key))) {
149
- throw createHttpError(400, `Недопустимый путь фильтра «${path}»`);
150
- }
151
-
152
- const fieldKey = keys.pop();
153
- let currentValue = where;
154
-
155
- keys.forEach((key) => {
156
- currentValue[key] = isObject(currentValue[key]) ? currentValue[key] : {};
157
- currentValue = currentValue[key];
158
- });
159
-
160
- currentValue[fieldKey] = isObject(currentValue[fieldKey]) ? currentValue[fieldKey] : {};
161
- currentValue[fieldKey][operator] = operator === 'in' && typeof value === 'string' ? value.split(',').map((item) => parsePrimitive(item.trim())) : parsePrimitive(value);
162
- };
163
-
164
- export const parseWhere = (query) => {
165
- const rawWhere = toArray(query._where).at(-1);
166
-
167
- if (rawWhere != null) {
168
- try {
169
- const where = JSON.parse(rawWhere);
170
-
171
- if (!isObject(where)) {
172
- throw new Error();
173
- }
174
-
175
- return where;
176
- } catch {
177
- throw createHttpError(400, 'Параметр _where должен содержать JSON-объект');
178
- }
179
- }
180
-
181
- const where = {};
182
-
183
- Object.entries(query).forEach(([key, rawValue]) => {
184
- if (!RESERVED_QUERY_KEYS.has(key)) {
185
- const filterKey = parseFilterKey(key);
186
-
187
- toArray(rawValue).forEach((value) => {
188
- setWhereOperator(where, filterKey.path, filterKey.operator, value);
189
- });
190
- }
191
- });
192
-
193
- return where;
194
- };
195
-
196
- const validateCondition = (condition, samples, path) => {
197
- if (!isObject(condition)) {
198
- return;
199
- }
200
-
201
- Object.entries(condition).forEach(([key, value]) => {
202
- if (key === 'and' || key === 'or') {
203
- validateLogicalConditions(key, value).forEach((nestedWhere) => {
204
- validateWhere(nestedWhere, samples, path);
205
- });
206
- return;
207
- }
208
-
209
- if (FIELD_OPERATORS.has(key)) {
210
- if (['every', 'none', 'some'].includes(key)) {
211
- if (samples.length > 0 && !samples.some(Array.isArray)) {
212
- throw createHttpError(400, `Оператор «${key}» в фильтре «${path}» можно применить только к массиву`);
213
- }
214
-
215
- validateCondition(
216
- value,
217
- samples.flatMap((sample) => (Array.isArray(sample) ? sample : [])),
218
- `${path}.${key}`,
219
- );
220
- } else if (key === 'not') {
221
- validateCondition(value, samples, `${path}.not`);
222
- } else if (['endsWith', 'startsWith'].includes(key) && samples.length > 0 && !samples.some((sample) => typeof sample === 'string')) {
223
- throw createHttpError(400, `Оператор «${key}» в фильтре «${path}» можно применить только к строке`);
224
- } else if (key === 'contains' && samples.length > 0 && !samples.some((sample) => typeof sample === 'string' || Array.isArray(sample))) {
225
- throw createHttpError(400, `Оператор «contains» в фильтре «${path}» можно применить только к строке или массиву`);
226
- }
227
-
228
- return;
229
- }
230
-
231
- if (!isSafeKey(key)) {
232
- throw createHttpError(400, `Недопустимый путь фильтра «${path}.${key}»`);
233
- }
234
-
235
- const objectSamples = samples.filter(isObject);
236
-
237
- if (samples.length > 0 && objectSamples.length === 0) {
238
- throw createHttpError(400, `Неизвестный оператор или вложенное поле «${path}.${key}»`);
239
- }
240
-
241
- const nestedSamples = objectSamples.filter((sample) => Object.hasOwn(sample, key)).map((sample) => sample[key]);
242
-
243
- if (objectSamples.length > 0 && nestedSamples.length === 0) {
244
- throw createHttpError(400, `Неизвестное поле фильтра «${path}.${key}»`);
245
- }
246
-
247
- validateCondition(value, nestedSamples, `${path}.${key}`);
248
- });
249
- };
250
-
251
- export const validateWhere = (where, items, path = '') => {
252
- Object.entries(where).forEach(([key, condition]) => {
253
- if (key === 'and' || key === 'or') {
254
- validateLogicalConditions(key, condition).forEach((nestedWhere) => {
255
- validateWhere(nestedWhere, items, path);
256
- });
257
- return;
258
- }
259
-
260
- if (key === 'not') {
261
- if (!isObject(condition)) {
262
- throw createHttpError(400, 'Оператор «not» должен содержать JSON-объект');
263
- }
264
-
265
- validateWhere(condition, items, path);
266
- return;
267
- }
268
-
269
- if (!isSafeKey(key)) {
270
- throw createHttpError(400, `Недопустимый путь фильтра «${path === '' ? key : `${path}.${key}`}»`);
271
- }
272
-
273
- const fieldPath = path === '' ? key : `${path}.${key}`;
274
- const objectItems = items.filter(isObject);
275
- const samples = objectItems.filter((item) => Object.hasOwn(item, key)).map((item) => item[key]);
276
-
277
- if (objectItems.length > 0 && samples.length === 0) {
278
- throw createHttpError(400, `Неизвестное поле фильтра «${fieldPath}»`);
279
- }
280
-
281
- validateCondition(condition, samples, fieldPath);
282
- });
283
- };
@@ -1,44 +0,0 @@
1
- import { DEFAULT_MAX_PAGE_SIZE, DEFAULT_PAGE_SIZE } from '../constants.js';
2
- import { createHttpError } from '../utils.js';
3
-
4
- const parsePositiveInteger = (value, name, defaultValue) => {
5
- if (value == null) {
6
- return defaultValue;
7
- }
8
-
9
- if (Array.isArray(value) || !/^[1-9]\d*$/.test(String(value))) {
10
- throw createHttpError(400, `Параметр ${name} должен быть положительным целым числом`);
11
- }
12
-
13
- const number = Number(value);
14
-
15
- if (!Number.isSafeInteger(number)) {
16
- throw createHttpError(400, `Параметр ${name} должен быть положительным целым числом`);
17
- }
18
-
19
- return number;
20
- };
21
-
22
- export const parsePagination = (query, maxPageSize = DEFAULT_MAX_PAGE_SIZE) => {
23
- const page = parsePositiveInteger(query._page, '_page', 1);
24
- const pageSize = parsePositiveInteger(query._perPage, '_perPage', DEFAULT_PAGE_SIZE);
25
-
26
- if (!Number.isInteger(maxPageSize) || maxPageSize < 1) {
27
- throw new Error('Максимальный размер страницы должен быть положительным целым числом');
28
- }
29
-
30
- if (pageSize > maxPageSize) {
31
- throw createHttpError(400, `Параметр _perPage не должен превышать ${maxPageSize}`);
32
- }
33
-
34
- return { page, pageSize };
35
- };
36
-
37
- export const paginateItems = (items, page, pageSize) => {
38
- const offset = (page - 1) * pageSize;
39
-
40
- return {
41
- data: items.slice(offset, offset + pageSize),
42
- total: items.length,
43
- };
44
- };
package/src/query/sort.js DELETED
@@ -1,63 +0,0 @@
1
- import { createHttpError, isObject, isSafeKey } from '../utils.js';
2
-
3
- const compareValues = (left, right) => {
4
- if (Object.is(left, right)) {
5
- return 0;
6
- }
7
-
8
- if (left == null) {
9
- return 1;
10
- }
11
-
12
- if (right == null) {
13
- return -1;
14
- }
15
-
16
- return typeof left === 'number' && typeof right === 'number' ? left - right : String(left).localeCompare(String(right), undefined, { numeric: true, sensitivity: 'base' });
17
- };
18
-
19
- const getValueByPath = (value, path) => path.split('.').reduce((currentValue, key) => (isObject(currentValue) && isSafeKey(key) ? currentValue[key] : undefined), value);
20
-
21
- const parseSortRules = (sort, items) => {
22
- if (sort == null || sort === '') {
23
- return [];
24
- }
25
-
26
- if (typeof sort !== 'string') {
27
- throw createHttpError(400, 'Параметр _sort должен содержать строку');
28
- }
29
-
30
- return sort.split(',').map((sortRule) => {
31
- const isDescending = sortRule.startsWith('-');
32
- const path = isDescending ? sortRule.slice(1) : sortRule;
33
- const keys = path.split('.');
34
-
35
- if (keys.some((key) => key === '' || !isSafeKey(key))) {
36
- throw createHttpError(400, `Недопустимое поле сортировки «${path}»`);
37
- }
38
-
39
- if (items.length > 0 && !items.some((item) => getValueByPath(item, path) !== undefined)) {
40
- throw createHttpError(400, `Неизвестное поле сортировки «${path}»`);
41
- }
42
-
43
- return { isDescending, path };
44
- });
45
- };
46
-
47
- export const sortItems = (items, sort, validationItems = items) => {
48
- const sortRules = parseSortRules(sort, validationItems);
49
-
50
- return sortRules.length === 0
51
- ? [...items]
52
- : [...items].sort((left, right) => {
53
- for (const { isDescending, path } of sortRules) {
54
- const comparison = compareValues(getValueByPath(left, path), getValueByPath(right, path));
55
-
56
- if (comparison !== 0) {
57
- return isDescending ? -comparison : comparison;
58
- }
59
- }
60
-
61
- return 0;
62
- });
63
- };
@@ -1,41 +0,0 @@
1
- import { singularize } from './utils.js';
2
-
3
- const SELF_RELATIONS = new Set(['child', 'children', 'parent', 'parents']);
4
-
5
- export const getRelationKeys = (...names) => [...new Set(names.flatMap((name) => [`${name}Id`, `${name}Ids`]))];
6
-
7
- export const resolveRelationResource = (resourceNames, relation, sourceResource) => {
8
- const resource = resourceNames.find((resourceName) => resourceName === relation) ?? resourceNames.find((resourceName) => singularize(resourceName) === relation);
9
-
10
- if (resource != null) {
11
- return resource;
12
- }
13
-
14
- return SELF_RELATIONS.has(relation) && resourceNames.includes(sourceResource) ? sourceResource : undefined;
15
- };
16
-
17
- export const getRelationMetadata = (key, resourceNames, sourceResource) => {
18
- // Relation fields follow the <resource>Id and <resource>Ids conventions.
19
- const match = key.match(/^(.+)(Id|Ids)$/);
20
-
21
- if (match == null) {
22
- return undefined;
23
- }
24
-
25
- const [, relation, suffix] = match;
26
- const isMany = suffix === 'Ids';
27
- const relationName = isMany ? (resourceNames.find((resource) => singularize(resource) === relation) ?? `${relation}s`) : relation;
28
- const targetResource = resolveRelationResource(resourceNames, relationName, sourceResource);
29
-
30
- if (targetResource == null) {
31
- return undefined;
32
- }
33
-
34
- return {
35
- isMany,
36
- relationName,
37
- reverseRelationName: ['parent', 'parents'].includes(relationName) ? 'children' : sourceResource,
38
- sourceResource,
39
- targetResource,
40
- };
41
- };