json-rest-api 1.0.13 → 1.0.15
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/connectors_ideas/fastify.js +66 -68
- package/connectors_ideas/koa-plugin.js +50 -51
- package/eslint.config.js +13 -0
- package/examples/url-override-example.js +63 -63
- package/index.js +13 -13
- package/lib/enhanced-logger.js +51 -51
- package/lib/error-formatter.js +61 -61
- package/lib/rest-api-errors.js +28 -28
- package/package.json +9 -5
- package/plugins/core/bulk-operations-plugin.js +142 -145
- package/plugins/core/connectors/express-plugin.js +210 -217
- package/plugins/core/connectors/lib/busboy-detector.js +84 -84
- package/plugins/core/connectors/lib/formidable-detector.js +50 -50
- package/plugins/core/connectors/lib/request-helpers.js +74 -74
- package/plugins/core/file-handling-plugin.js +125 -125
- package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
- package/plugins/core/lib/anyapi/schema-utils.js +101 -101
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
- package/plugins/core/lib/dbTablesOperations.js +134 -135
- package/plugins/core/lib/fractional-positioning.js +74 -76
- package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
- package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
- package/plugins/core/lib/querying/knex-process-includes.js +50 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
- package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
- package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
- package/plugins/core/lib/querying/knex-window-queries.js +89 -89
- package/plugins/core/lib/querying/url-helpers.js +35 -35
- package/plugins/core/lib/querying-writing/compile-schemas.js +97 -98
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
- package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
- package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
- package/plugins/core/lib/querying-writing/field-utils.js +44 -42
- package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
- package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
- package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
- package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
- package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
- package/plugins/core/lib/storage/storage-adapter.js +68 -68
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
- package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
- package/plugins/core/lib/writing/relationship-processor.js +58 -58
- package/plugins/core/rest-api-access.js +8 -8
- package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
- package/plugins/core/rest-api-cors-plugin.js +170 -171
- package/plugins/core/rest-api-knex-plugin.js +482 -493
- package/plugins/core/rest-api-multihome-plugin.js +247 -251
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
- package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
- package/plugins/core/rest-api-plugin-methods/common.js +295 -297
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
- package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
- package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
- package/plugins/core/rest-api-plugin-methods/get.js +86 -86
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
- package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
- package/plugins/core/rest-api-plugin-methods/post.js +61 -62
- package/plugins/core/rest-api-plugin-methods/put.js +86 -87
- package/plugins/core/rest-api-plugin-methods/query.js +88 -88
- package/plugins/core/rest-api-plugin-methods/release.js +5 -6
- package/plugins/core/rest-api-plugin.js +76 -78
- package/plugins/core/rest-api-positioning-plugin.js +251 -252
- package/plugins/core/socketio-plugin.js +240 -236
- package/plugins/storage/local-storage.js +103 -103
- package/plugins/storage/s3-storage.js +41 -42
- package/quickTest.js +37 -37
- package/scripts/debug-anyapi-review-filter.mjs +18 -18
- package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
- package/scripts/inspect-anyapi-descriptor.mjs +10 -10
- package/scripts/run-docs.js +112 -113
- package/test-debug.js +28 -28
- package/tests/TEST_TEMPLATE.test.js +51 -51
- package/tests/anyapi-basic.test.js +42 -42
- package/tests/anyapi-cursor-pagination.test.js +48 -48
- package/tests/bulk-operations.test.js +208 -208
- package/tests/computed-fields.test.js +204 -204
- package/tests/cors.test.js +412 -413
- package/tests/curl.test.js +275 -276
- package/tests/custom-idproperty-relationships.test.js +266 -266
- package/tests/field-getters.test.js +132 -132
- package/tests/field-setters.test.js +110 -110
- package/tests/fixtures/api-configs.js +699 -699
- package/tests/helpers/storage-mode.js +32 -32
- package/tests/helpers/test-utils.js +76 -76
- package/tests/include-depth-validation.test.js +76 -76
- package/tests/includes.test.js +145 -145
- package/tests/multihome.test.js +175 -175
- package/tests/nested-includes.test.js +255 -255
- package/tests/pagination-cursor-multifield.test.js +164 -164
- package/tests/pagination-enhanced.test.js +225 -225
- package/tests/polymorphic-relationships.test.js +96 -96
- package/tests/polymorphic-simple.test.js +44 -44
- package/tests/positioning-advanced.test.js +226 -226
- package/tests/positioning-extended.test.js +399 -399
- package/tests/positioning-multifilter.test.js +204 -204
- package/tests/positioning.test.js +266 -266
- package/tests/queries.test.js +184 -184
- package/tests/query-limits.test.js +154 -155
- package/tests/relationship-endpoints.test.js +127 -127
- package/tests/relationships.test.js +178 -179
- package/tests/rest-api-access.test.js +86 -86
- package/tests/rest-api.test.js +103 -103
- package/tests/searchschema-merge.test.js +93 -93
- package/tests/socketio.test.js +326 -326
- package/tests/virtual-fields.test.js +164 -164
|
@@ -1,46 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Normalizes a date value to appropriate format based on field type
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* @param {*} value - The date value from the database
|
|
5
5
|
* @param {string} type - The field type ('date', 'dateTime', or 'time')
|
|
6
6
|
* @returns {Date|string|null} Date object for date/dateTime, string for time, or null
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
8
|
* @example
|
|
9
9
|
* // Input: Already a Date object
|
|
10
10
|
* normalizeDateValue(new Date('2024-01-15T10:30:00Z'), 'dateTime');
|
|
11
11
|
* // Output: Date object (unchanged)
|
|
12
|
-
*
|
|
12
|
+
*
|
|
13
13
|
* @example
|
|
14
14
|
* // Input: MySQL datetime string without timezone
|
|
15
15
|
* normalizeDateValue('2024-01-15 10:30:00', 'dateTime');
|
|
16
16
|
* // Output: Date object parsed as UTC (2024-01-15T10:30:00Z)
|
|
17
|
-
*
|
|
17
|
+
*
|
|
18
18
|
* @example
|
|
19
19
|
* // Input: Date-only string
|
|
20
20
|
* normalizeDateValue('2024-01-15', 'date');
|
|
21
21
|
* // Output: Date object at UTC midnight (2024-01-15T00:00:00Z)
|
|
22
|
-
*
|
|
22
|
+
*
|
|
23
23
|
* @example
|
|
24
24
|
* // Input: Time field (always returns string)
|
|
25
25
|
* normalizeDateValue('14:30:45', 'time');
|
|
26
26
|
* // Output: "14:30:45" (string)
|
|
27
|
-
*
|
|
27
|
+
*
|
|
28
28
|
* @example
|
|
29
29
|
* // Input: Date object for time field
|
|
30
30
|
* normalizeDateValue(new Date('2024-01-15T14:30:45Z'), 'time');
|
|
31
31
|
* // Output: "14:30:45" (extracts time portion)
|
|
32
|
-
*
|
|
32
|
+
*
|
|
33
33
|
* @description
|
|
34
34
|
* Used by:
|
|
35
35
|
* - normalizeAttributes to process individual field values
|
|
36
36
|
* - Applied to all date/time fields fetched from database
|
|
37
|
-
*
|
|
37
|
+
*
|
|
38
38
|
* Purpose:
|
|
39
39
|
* - Handles database-specific date formats (especially MySQL)
|
|
40
40
|
* - Prevents timezone shifts for date-only fields
|
|
41
41
|
* - Ensures consistent Date objects across databases
|
|
42
42
|
* - Keeps time fields as HH:MM:SS strings
|
|
43
|
-
*
|
|
43
|
+
*
|
|
44
44
|
* Data flow:
|
|
45
45
|
* 1. Checks for null/undefined (returns null)
|
|
46
46
|
* 2. For time fields: ensures HH:MM:SS string format
|
|
@@ -48,85 +48,85 @@
|
|
|
48
48
|
* 4. Handles MySQL datetime format (no T separator)
|
|
49
49
|
* 5. Forces UTC interpretation to prevent timezone issues
|
|
50
50
|
*/
|
|
51
|
-
function normalizeDateValue(value, type) {
|
|
51
|
+
function normalizeDateValue (value, type) {
|
|
52
52
|
// Handle null/undefined
|
|
53
53
|
if (value === null || value === undefined) {
|
|
54
|
-
return null
|
|
54
|
+
return null
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
// Handle time type specially - always return as string
|
|
58
58
|
if (type === 'time') {
|
|
59
59
|
// If it's already a properly formatted time string, return as-is
|
|
60
60
|
if (typeof value === 'string' && /^\d{2}:\d{2}:\d{2}/.test(value)) {
|
|
61
|
-
return value
|
|
61
|
+
return value
|
|
62
62
|
}
|
|
63
63
|
// If we got a Date object for a time field, extract the time portion
|
|
64
64
|
if (value instanceof Date) {
|
|
65
|
-
return value.toISOString().slice(11, 19)
|
|
65
|
+
return value.toISOString().slice(11, 19) // Extract HH:MM:SS
|
|
66
66
|
}
|
|
67
67
|
// Try to parse and extract time
|
|
68
68
|
if (typeof value === 'string' || typeof value === 'number') {
|
|
69
|
-
const d = new Date(value)
|
|
69
|
+
const d = new Date(value)
|
|
70
70
|
if (!isNaN(d.getTime())) {
|
|
71
|
-
return d.toISOString().slice(11, 19)
|
|
71
|
+
return d.toISOString().slice(11, 19)
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
|
-
return null
|
|
74
|
+
return null
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
// For date and dateTime: Already a Date object? Return as-is
|
|
78
78
|
if (value instanceof Date) {
|
|
79
|
-
return value
|
|
79
|
+
return value
|
|
80
80
|
}
|
|
81
|
-
|
|
81
|
+
|
|
82
82
|
// Handle string values
|
|
83
83
|
if (typeof value === 'string') {
|
|
84
84
|
// Detect MySQL datetime format: 'YYYY-MM-DD HH:MM:SS'
|
|
85
85
|
// These have no T separator and no timezone indicator
|
|
86
|
-
const isMySQLDateTime = type === 'dateTime' &&
|
|
86
|
+
const isMySQLDateTime = type === 'dateTime' &&
|
|
87
87
|
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/.test(value) &&
|
|
88
|
-
!value.includes('T') &&
|
|
88
|
+
!value.includes('T') &&
|
|
89
89
|
!value.includes('Z') &&
|
|
90
90
|
!value.includes('+') &&
|
|
91
|
-
!value.includes('-', 10)
|
|
92
|
-
|
|
91
|
+
!value.includes('-', 10) // Don't match date separators
|
|
92
|
+
|
|
93
93
|
if (isMySQLDateTime) {
|
|
94
94
|
// Convert to ISO format and force UTC interpretation
|
|
95
95
|
// '2024-01-15 10:30:00' becomes '2024-01-15T10:30:00Z'
|
|
96
|
-
return new Date(value.replace(' ', 'T') + 'Z')
|
|
96
|
+
return new Date(value.replace(' ', 'T') + 'Z')
|
|
97
97
|
}
|
|
98
|
-
|
|
98
|
+
|
|
99
99
|
// For date-only fields, ensure parsing at UTC midnight
|
|
100
100
|
// This prevents timezone shifts when parsing dates
|
|
101
101
|
if (type === 'date' && /^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
102
|
-
return new Date(value + 'T00:00:00Z')
|
|
102
|
+
return new Date(value + 'T00:00:00Z')
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
-
|
|
105
|
+
|
|
106
106
|
// Handle numeric values (Unix timestamps)
|
|
107
107
|
if (typeof value === 'number') {
|
|
108
|
-
return new Date(value)
|
|
108
|
+
return new Date(value)
|
|
109
109
|
}
|
|
110
|
-
|
|
110
|
+
|
|
111
111
|
// Try to parse any other format
|
|
112
|
-
const dateObj = new Date(value)
|
|
113
|
-
|
|
112
|
+
const dateObj = new Date(value)
|
|
113
|
+
|
|
114
114
|
// Check if date is valid
|
|
115
115
|
if (isNaN(dateObj.getTime())) {
|
|
116
|
-
console.warn(`Invalid date value for field type '${type}': ${value}`)
|
|
117
|
-
return null
|
|
116
|
+
console.warn(`Invalid date value for field type '${type}': ${value}`)
|
|
117
|
+
return null // Return null for invalid dates
|
|
118
118
|
}
|
|
119
|
-
|
|
120
|
-
return dateObj
|
|
119
|
+
|
|
120
|
+
return dateObj
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/**
|
|
124
124
|
* Normalizes database values in an attributes object
|
|
125
|
-
*
|
|
125
|
+
*
|
|
126
126
|
* @param {Object} attributes - The attributes object to normalize
|
|
127
127
|
* @param {Object} schemaStructure - The schema structure defining field types
|
|
128
128
|
* @returns {Object} The normalized attributes object
|
|
129
|
-
*
|
|
129
|
+
*
|
|
130
130
|
* @example
|
|
131
131
|
* // Input: Raw database values
|
|
132
132
|
* const attributes = {
|
|
@@ -136,16 +136,16 @@ function normalizeDateValue(value, type) {
|
|
|
136
136
|
* birth_date: '1990-05-20', // Date string
|
|
137
137
|
* shift_time: '09:00:00' // Time string
|
|
138
138
|
* };
|
|
139
|
-
*
|
|
139
|
+
*
|
|
140
140
|
* const schema = {
|
|
141
141
|
* is_active: { type: 'boolean' },
|
|
142
142
|
* created_at: { type: 'dateTime' },
|
|
143
143
|
* birth_date: { type: 'date' },
|
|
144
144
|
* shift_time: { type: 'time' }
|
|
145
145
|
* };
|
|
146
|
-
*
|
|
146
|
+
*
|
|
147
147
|
* const normalized = normalizeAttributes(attributes, schema);
|
|
148
|
-
*
|
|
148
|
+
*
|
|
149
149
|
* // Output: Properly typed values
|
|
150
150
|
* // {
|
|
151
151
|
* // id: 1,
|
|
@@ -154,7 +154,7 @@ function normalizeDateValue(value, type) {
|
|
|
154
154
|
* // birth_date: Date('1990-05-20T00:00:00Z'), // Date at midnight UTC
|
|
155
155
|
* // shift_time: '09:00:00' // String unchanged
|
|
156
156
|
* // }
|
|
157
|
-
*
|
|
157
|
+
*
|
|
158
158
|
* @example
|
|
159
159
|
* // Input: Boolean edge cases
|
|
160
160
|
* const attributes = {
|
|
@@ -163,14 +163,14 @@ function normalizeDateValue(value, type) {
|
|
|
163
163
|
* flag3: true, // Already boolean
|
|
164
164
|
* flag4: null // Null value
|
|
165
165
|
* };
|
|
166
|
-
*
|
|
166
|
+
*
|
|
167
167
|
* const normalized = normalizeAttributes(attributes, {
|
|
168
168
|
* flag1: { type: 'boolean' },
|
|
169
169
|
* flag2: { type: 'boolean' },
|
|
170
170
|
* flag3: { type: 'boolean' },
|
|
171
171
|
* flag4: { type: 'boolean' }
|
|
172
172
|
* });
|
|
173
|
-
*
|
|
173
|
+
*
|
|
174
174
|
* // Output:
|
|
175
175
|
* // {
|
|
176
176
|
* // flag1: false, // 0 → false
|
|
@@ -178,18 +178,18 @@ function normalizeDateValue(value, type) {
|
|
|
178
178
|
* // flag3: true, // unchanged
|
|
179
179
|
* // flag4: null // null preserved
|
|
180
180
|
* // }
|
|
181
|
-
*
|
|
181
|
+
*
|
|
182
182
|
* @description
|
|
183
183
|
* Used by:
|
|
184
184
|
* - normalizeRecordAttributes for each record's attributes
|
|
185
185
|
* - Applied after fetching data from database
|
|
186
|
-
*
|
|
186
|
+
*
|
|
187
187
|
* Purpose:
|
|
188
188
|
* - Handles databases without native boolean support (SQLite, older MySQL)
|
|
189
189
|
* - Normalizes date formats from different databases
|
|
190
190
|
* - Ensures consistent data types in API responses
|
|
191
191
|
* - Preserves null values appropriately
|
|
192
|
-
*
|
|
192
|
+
*
|
|
193
193
|
* Data flow:
|
|
194
194
|
* 1. Creates copy of attributes to avoid mutation
|
|
195
195
|
* 2. Iterates through each field with schema definition
|
|
@@ -197,46 +197,46 @@ function normalizeDateValue(value, type) {
|
|
|
197
197
|
* 4. Normalizes dates using normalizeDateValue
|
|
198
198
|
* 5. Returns new object with normalized values
|
|
199
199
|
*/
|
|
200
|
-
export function normalizeAttributes(attributes, schemaStructure) {
|
|
200
|
+
export function normalizeAttributes (attributes, schemaStructure) {
|
|
201
201
|
if (!attributes || !schemaStructure) {
|
|
202
|
-
return attributes
|
|
202
|
+
return attributes
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
const normalized = { ...attributes }
|
|
205
|
+
const normalized = { ...attributes }
|
|
206
206
|
|
|
207
207
|
// Iterate through each attribute
|
|
208
208
|
for (const [fieldName, value] of Object.entries(attributes)) {
|
|
209
|
-
const fieldDef = schemaStructure[fieldName]
|
|
210
|
-
|
|
209
|
+
const fieldDef = schemaStructure[fieldName]
|
|
210
|
+
|
|
211
211
|
// Skip if no field definition found
|
|
212
|
-
if (!fieldDef) continue
|
|
212
|
+
if (!fieldDef) continue
|
|
213
213
|
|
|
214
214
|
// Normalize boolean values
|
|
215
215
|
if (fieldDef.type === 'boolean') {
|
|
216
216
|
if (value === 1 || value === '1') {
|
|
217
|
-
normalized[fieldName] = true
|
|
217
|
+
normalized[fieldName] = true
|
|
218
218
|
} else if (value === 0 || value === '0') {
|
|
219
|
-
normalized[fieldName] = false
|
|
219
|
+
normalized[fieldName] = false
|
|
220
220
|
}
|
|
221
221
|
// null/undefined remain as-is
|
|
222
222
|
}
|
|
223
|
-
|
|
223
|
+
|
|
224
224
|
// Normalize date/dateTime/time values
|
|
225
225
|
if (fieldDef.type === 'date' || fieldDef.type === 'dateTime' || fieldDef.type === 'time') {
|
|
226
|
-
normalized[fieldName] = normalizeDateValue(value, fieldDef.type)
|
|
226
|
+
normalized[fieldName] = normalizeDateValue(value, fieldDef.type)
|
|
227
227
|
}
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
return normalized
|
|
230
|
+
return normalized
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
/**
|
|
234
234
|
* Normalizes all records in a JSON:API response
|
|
235
|
-
*
|
|
235
|
+
*
|
|
236
236
|
* @param {Object} record - The JSON:API response object
|
|
237
237
|
* @param {Object} scopes - All available scopes for schema lookup
|
|
238
238
|
* @returns {Object} The response with normalized values
|
|
239
|
-
*
|
|
239
|
+
*
|
|
240
240
|
* @example
|
|
241
241
|
* // Input: JSON:API response with various data types
|
|
242
242
|
* const response = {
|
|
@@ -259,9 +259,9 @@ export function normalizeAttributes(attributes, schemaStructure) {
|
|
|
259
259
|
* }
|
|
260
260
|
* }]
|
|
261
261
|
* };
|
|
262
|
-
*
|
|
262
|
+
*
|
|
263
263
|
* const normalized = normalizeRecordAttributes(response, scopes);
|
|
264
|
-
*
|
|
264
|
+
*
|
|
265
265
|
* // Output: All booleans and dates normalized
|
|
266
266
|
* // {
|
|
267
267
|
* // data: {
|
|
@@ -283,7 +283,7 @@ export function normalizeAttributes(attributes, schemaStructure) {
|
|
|
283
283
|
* // }
|
|
284
284
|
* // }]
|
|
285
285
|
* // }
|
|
286
|
-
*
|
|
286
|
+
*
|
|
287
287
|
* @example
|
|
288
288
|
* // Input: Collection response (array of records)
|
|
289
289
|
* const response = {
|
|
@@ -300,20 +300,20 @@ export function normalizeAttributes(attributes, schemaStructure) {
|
|
|
300
300
|
* }
|
|
301
301
|
* ]
|
|
302
302
|
* };
|
|
303
|
-
*
|
|
303
|
+
*
|
|
304
304
|
* // Each record in the array is normalized
|
|
305
|
-
*
|
|
305
|
+
*
|
|
306
306
|
* @description
|
|
307
307
|
* Used by:
|
|
308
308
|
* - rest-api-knex-plugin after fetching data, before sending response
|
|
309
309
|
* - Applied to both GET (single) and QUERY (collection) responses
|
|
310
|
-
*
|
|
310
|
+
*
|
|
311
311
|
* Purpose:
|
|
312
312
|
* - Ensures consistent data types in API responses
|
|
313
313
|
* - Handles both primary data and included resources
|
|
314
314
|
* - Works with single records and collections
|
|
315
315
|
* - Uses schema definitions to determine field types
|
|
316
|
-
*
|
|
316
|
+
*
|
|
317
317
|
* Data flow:
|
|
318
318
|
* 1. Checks if data is array (collection) or object (single)
|
|
319
319
|
* 2. For each record, looks up its schema by type
|
|
@@ -321,9 +321,9 @@ export function normalizeAttributes(attributes, schemaStructure) {
|
|
|
321
321
|
* 4. Processes included array the same way
|
|
322
322
|
* 5. Returns complete response with normalized values
|
|
323
323
|
*/
|
|
324
|
-
export function normalizeRecordAttributes(record, scopes) {
|
|
324
|
+
export function normalizeRecordAttributes (record, scopes) {
|
|
325
325
|
if (!record || !scopes) {
|
|
326
|
-
return record
|
|
326
|
+
return record
|
|
327
327
|
}
|
|
328
328
|
|
|
329
329
|
// Normalize main data records
|
|
@@ -331,23 +331,23 @@ export function normalizeRecordAttributes(record, scopes) {
|
|
|
331
331
|
if (Array.isArray(record.data)) {
|
|
332
332
|
// Handle array of records (query result)
|
|
333
333
|
for (const entry of record.data) {
|
|
334
|
-
const scope = scopes[entry.type]
|
|
334
|
+
const scope = scopes[entry.type]
|
|
335
335
|
if (scope?.vars?.schemaInfo?.schemaStructure && entry.attributes) {
|
|
336
336
|
entry.attributes = normalizeAttributes(
|
|
337
337
|
entry.attributes,
|
|
338
338
|
scope.vars.schemaInfo.schemaStructure
|
|
339
|
-
)
|
|
339
|
+
)
|
|
340
340
|
}
|
|
341
341
|
}
|
|
342
342
|
} else {
|
|
343
343
|
// Handle single record (get result)
|
|
344
|
-
const entry = record.data
|
|
345
|
-
const scope = scopes[entry.type]
|
|
344
|
+
const entry = record.data
|
|
345
|
+
const scope = scopes[entry.type]
|
|
346
346
|
if (scope?.vars?.schemaInfo?.schemaStructure && entry.attributes) {
|
|
347
347
|
entry.attributes = normalizeAttributes(
|
|
348
348
|
entry.attributes,
|
|
349
349
|
scope.vars.schemaInfo.schemaStructure
|
|
350
|
-
)
|
|
350
|
+
)
|
|
351
351
|
}
|
|
352
352
|
}
|
|
353
353
|
}
|
|
@@ -355,15 +355,15 @@ export function normalizeRecordAttributes(record, scopes) {
|
|
|
355
355
|
// Normalize included records
|
|
356
356
|
if (record.included && Array.isArray(record.included)) {
|
|
357
357
|
for (const entry of record.included) {
|
|
358
|
-
const scope = scopes[entry.type]
|
|
358
|
+
const scope = scopes[entry.type]
|
|
359
359
|
if (scope?.vars?.schemaInfo?.schemaStructure && entry.attributes) {
|
|
360
360
|
entry.attributes = normalizeAttributes(
|
|
361
361
|
entry.attributes,
|
|
362
362
|
scope.vars.schemaInfo.schemaStructure
|
|
363
|
-
)
|
|
363
|
+
)
|
|
364
364
|
}
|
|
365
365
|
}
|
|
366
366
|
}
|
|
367
367
|
|
|
368
|
-
return record
|
|
369
|
-
}
|
|
368
|
+
return record
|
|
369
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Default data helper implementations that instruct users to install a storage plugin
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* @description
|
|
5
5
|
* These default implementations ensure the REST API plugin fails gracefully
|
|
6
6
|
* when no storage plugin is installed. Each method throws a descriptive error
|
|
7
7
|
* guiding users to install a storage implementation.
|
|
8
|
-
*
|
|
8
|
+
*
|
|
9
9
|
* @example
|
|
10
10
|
* // When no storage plugin is installed:
|
|
11
11
|
* try {
|
|
@@ -14,23 +14,23 @@
|
|
|
14
14
|
* console.log(error.message);
|
|
15
15
|
* // "No storage implementation for get. Install a storage plugin."
|
|
16
16
|
* }
|
|
17
|
-
*
|
|
17
|
+
*
|
|
18
18
|
* @example
|
|
19
19
|
* // After installing rest-api-knex-plugin:
|
|
20
20
|
* api.use(RestApiKnexPlugin, { knex });
|
|
21
21
|
* // Now data helpers are replaced with actual implementations
|
|
22
22
|
* const article = await api.resources.articles.get({ id: 1 });
|
|
23
23
|
* // Works! Returns article from database
|
|
24
|
-
*
|
|
24
|
+
*
|
|
25
25
|
* Used by:
|
|
26
26
|
* - rest-api-plugin sets these as initial data helpers
|
|
27
27
|
* - Storage plugins replace these with actual implementations
|
|
28
|
-
*
|
|
28
|
+
*
|
|
29
29
|
* Purpose:
|
|
30
30
|
* - Provides clear error messages when storage is missing
|
|
31
31
|
* - Ensures REST API plugin can be installed independently
|
|
32
32
|
* - Allows storage plugins to be swapped without changing API
|
|
33
|
-
*
|
|
33
|
+
*
|
|
34
34
|
* Data flow:
|
|
35
35
|
* 1. REST API plugin initializes with these defaults
|
|
36
36
|
* 2. User tries to use API without storage plugin
|
|
@@ -38,35 +38,35 @@
|
|
|
38
38
|
* 4. Storage plugin replaces these with real implementations
|
|
39
39
|
*/
|
|
40
40
|
export const defaultDataHelpers = {
|
|
41
|
-
dataExists: async function(scope, deps) {
|
|
42
|
-
throw new Error(
|
|
41
|
+
dataExists: async function (scope, deps) {
|
|
42
|
+
throw new Error('No storage implementation for exists. Install a storage plugin.')
|
|
43
43
|
},
|
|
44
44
|
|
|
45
|
-
dataGet: async function(scope, deps) {
|
|
46
|
-
throw new Error(
|
|
45
|
+
dataGet: async function (scope, deps) {
|
|
46
|
+
throw new Error('No storage implementation for get. Install a storage plugin.')
|
|
47
47
|
},
|
|
48
48
|
|
|
49
|
-
dataGetMinimal: async function(scope, deps) {
|
|
50
|
-
throw new Error(
|
|
49
|
+
dataGetMinimal: async function (scope, deps) {
|
|
50
|
+
throw new Error('No storage implementation for getMinimal. Install a storage plugin.')
|
|
51
51
|
},
|
|
52
|
-
|
|
53
|
-
dataQuery: async function(scope, deps) {
|
|
54
|
-
throw new Error(
|
|
52
|
+
|
|
53
|
+
dataQuery: async function (scope, deps) {
|
|
54
|
+
throw new Error('No storage implementation for query. Install a storage plugin.')
|
|
55
55
|
},
|
|
56
|
-
|
|
57
|
-
dataPost: async function(scope, deps) {
|
|
58
|
-
throw new Error(
|
|
56
|
+
|
|
57
|
+
dataPost: async function (scope, deps) {
|
|
58
|
+
throw new Error('No storage implementation for post. Install a storage plugin.')
|
|
59
59
|
},
|
|
60
60
|
|
|
61
|
-
dataPatch: async function(scope, deps) {
|
|
62
|
-
throw new Error(
|
|
61
|
+
dataPatch: async function (scope, deps) {
|
|
62
|
+
throw new Error('No storage implementation for patch. Install a storage plugin.')
|
|
63
63
|
},
|
|
64
|
-
|
|
65
|
-
dataPut: async function(scope, deps) {
|
|
66
|
-
throw new Error(
|
|
64
|
+
|
|
65
|
+
dataPut: async function (scope, deps) {
|
|
66
|
+
throw new Error('No storage implementation for put. Install a storage plugin.')
|
|
67
67
|
},
|
|
68
|
-
|
|
69
|
-
dataDelete: async function(scope, deps) {
|
|
70
|
-
throw new Error(
|
|
68
|
+
|
|
69
|
+
dataDelete: async function (scope, deps) {
|
|
70
|
+
throw new Error('No storage implementation for delete. Install a storage plugin.')
|
|
71
71
|
}
|
|
72
|
-
}
|
|
72
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Extracts all foreign key fields from a schema definition
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* @param {Object} schema - The schema definition (Schema object or plain object)
|
|
5
5
|
* @returns {Set<string>} Set of foreign key field names
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* @example
|
|
8
8
|
* // Input: Schema with belongsTo relationships
|
|
9
9
|
* const schema = {
|
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
* category_id: { type: 'number', belongsTo: 'categories', as: 'category' },
|
|
14
14
|
* status: { type: 'string' }
|
|
15
15
|
* };
|
|
16
|
-
*
|
|
16
|
+
*
|
|
17
17
|
* const foreignKeys = getForeignKeyFields(schema);
|
|
18
|
-
*
|
|
18
|
+
*
|
|
19
19
|
* // Output: Set containing only foreign key fields
|
|
20
20
|
* // Set(['author_id', 'category_id'])
|
|
21
|
-
*
|
|
21
|
+
*
|
|
22
22
|
* @example
|
|
23
23
|
* // Input: Works with compiled Schema objects too
|
|
24
24
|
* const schemaObject = {
|
|
@@ -27,24 +27,24 @@
|
|
|
27
27
|
* user_id: { type: 'id', belongsTo: 'users', as: 'user' }
|
|
28
28
|
* }
|
|
29
29
|
* };
|
|
30
|
-
*
|
|
30
|
+
*
|
|
31
31
|
* const foreignKeys = getForeignKeyFields(schemaObject);
|
|
32
|
-
*
|
|
32
|
+
*
|
|
33
33
|
* // Output: Handles both formats
|
|
34
34
|
* // Set(['post_id', 'user_id'])
|
|
35
|
-
*
|
|
35
|
+
*
|
|
36
36
|
* @description
|
|
37
37
|
* Used by:
|
|
38
38
|
* - toJsonApi to filter foreign keys from attributes
|
|
39
39
|
* - buildFieldSelection to ensure foreign keys are always selected
|
|
40
40
|
* - knex-json-api-helpers for field filtering
|
|
41
|
-
*
|
|
41
|
+
*
|
|
42
42
|
* Purpose:
|
|
43
43
|
* - Identifies fields that should be relationships, not attributes
|
|
44
44
|
* - Foreign keys are stored in DB but not exposed as JSON:API attributes
|
|
45
45
|
* - Ensures these fields are included in SELECT for relationship building
|
|
46
46
|
* - Helps transform database structure to JSON:API format
|
|
47
|
-
*
|
|
47
|
+
*
|
|
48
48
|
* Data flow:
|
|
49
49
|
* 1. Accepts schema in either format (plain or with structure property)
|
|
50
50
|
* 2. Iterates through all field definitions
|
|
@@ -52,32 +52,32 @@
|
|
|
52
52
|
* 4. Returns Set for efficient lookups
|
|
53
53
|
*/
|
|
54
54
|
export const getForeignKeyFields = (schema) => {
|
|
55
|
-
const foreignKeys = new Set()
|
|
56
|
-
if (!schema) return foreignKeys
|
|
57
|
-
|
|
55
|
+
const foreignKeys = new Set()
|
|
56
|
+
if (!schema) return foreignKeys
|
|
57
|
+
|
|
58
58
|
// Handle both Schema objects and plain objects
|
|
59
|
-
const schemaStructure = schema.structure || schema
|
|
60
|
-
|
|
59
|
+
const schemaStructure = schema.structure || schema
|
|
60
|
+
|
|
61
61
|
Object.entries(schemaStructure).forEach(([field, def]) => {
|
|
62
62
|
if (def.belongsTo) {
|
|
63
|
-
foreignKeys.add(field)
|
|
63
|
+
foreignKeys.add(field)
|
|
64
64
|
}
|
|
65
|
-
})
|
|
66
|
-
return foreignKeys
|
|
67
|
-
}
|
|
65
|
+
})
|
|
66
|
+
return foreignKeys
|
|
67
|
+
}
|
|
68
68
|
|
|
69
69
|
/**
|
|
70
70
|
* Filters hidden fields from attributes based on schema rules.
|
|
71
|
-
*
|
|
71
|
+
*
|
|
72
72
|
* This pure function removes fields marked as hidden or normallyHidden
|
|
73
73
|
* based on the schema definition and requested fields. It ensures that
|
|
74
74
|
* sensitive data is never exposed in API responses.
|
|
75
|
-
*
|
|
75
|
+
*
|
|
76
76
|
* @param {Object} attributes - The attributes object to filter
|
|
77
77
|
* @param {Object} schema - The schema object with structure property
|
|
78
78
|
* @param {Array<string>|string} requestedFields - Fields explicitly requested (for normallyHidden)
|
|
79
79
|
* @returns {Object} Filtered attributes object
|
|
80
|
-
*
|
|
80
|
+
*
|
|
81
81
|
* @example <caption>Filtering hidden fields</caption>
|
|
82
82
|
* const attributes = {
|
|
83
83
|
* name: 'John',
|
|
@@ -96,7 +96,7 @@ export const getForeignKeyFields = (schema) => {
|
|
|
96
96
|
* const filtered = filterHiddenFields(attributes, schema, null);
|
|
97
97
|
* // Returns: { name: 'John', email: 'john@example.com' }
|
|
98
98
|
* // password_hash (hidden) and internal_id (normallyHidden) are removed
|
|
99
|
-
*
|
|
99
|
+
*
|
|
100
100
|
* @example <caption>Including normallyHidden fields when requested</caption>
|
|
101
101
|
* const filtered = filterHiddenFields(attributes, schema, 'name,internal_id');
|
|
102
102
|
* // Returns: { name: 'John', internal_id: '123' }
|
|
@@ -104,34 +104,36 @@ export const getForeignKeyFields = (schema) => {
|
|
|
104
104
|
* // password_hash is still filtered (always hidden)
|
|
105
105
|
*/
|
|
106
106
|
export const filterHiddenFields = (attributes, schema, requestedFields) => {
|
|
107
|
-
const filtered = {}
|
|
108
|
-
|
|
107
|
+
const filtered = {}
|
|
108
|
+
|
|
109
109
|
// Parse requested fields if it's a string (from query params)
|
|
110
110
|
// Example: "name,price,cost" -> ['name', 'price', 'cost']
|
|
111
|
-
const requested = requestedFields
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
111
|
+
const requested = requestedFields
|
|
112
|
+
? (
|
|
113
|
+
typeof requestedFields === 'string'
|
|
114
|
+
? requestedFields.split(',').map(f => f.trim()).filter(f => f)
|
|
115
|
+
: requestedFields
|
|
116
|
+
)
|
|
117
|
+
: null
|
|
118
|
+
|
|
117
119
|
Object.entries(attributes).forEach(([field, value]) => {
|
|
118
|
-
const fieldDef = schema.structure?.[field]
|
|
119
|
-
|
|
120
|
+
const fieldDef = schema.structure?.[field]
|
|
121
|
+
|
|
120
122
|
// Never include hidden fields - these are completely invisible
|
|
121
123
|
// Example: password_hash with hidden:true is always filtered out
|
|
122
|
-
if (fieldDef?.hidden === true) return
|
|
123
|
-
|
|
124
|
+
if (fieldDef?.hidden === true) return
|
|
125
|
+
|
|
124
126
|
// Include normallyHidden fields only if explicitly requested
|
|
125
127
|
// Example: 'cost' with normallyHidden:true is only included if user requests it
|
|
126
128
|
// via sparse fieldsets like ?fields[products]=name,cost
|
|
127
129
|
if (fieldDef?.normallyHidden === true) {
|
|
128
130
|
if (!requested || !requested.includes(field)) {
|
|
129
|
-
return
|
|
131
|
+
return // Filter out normallyHidden field
|
|
130
132
|
}
|
|
131
133
|
}
|
|
132
|
-
|
|
133
|
-
filtered[field] = value
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
return filtered
|
|
137
|
-
}
|
|
134
|
+
|
|
135
|
+
filtered[field] = value
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
return filtered
|
|
139
|
+
}
|