json-rest-api 1.0.21 → 1.0.23
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/CLAUDE.md +1 -2
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
- package/docs/API.md +51 -14
- package/docs/COMPARISON.md +2 -2
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +28 -1
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
- package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
- package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +250 -0
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
- package/docs/GUIDE/index.md +18 -9
- package/docs/QUICKSTART.md +5 -1
- package/docs/index.md +1 -1
- package/index.js +3 -2
- package/package.json +4 -4
- package/plugins/core/connectors/express-plugin.js +97 -184
- package/plugins/core/connectors/fastify-plugin.js +206 -0
- package/plugins/core/connectors/lib/connector-core.js +195 -0
- package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
- package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
- package/plugins/core/lib/dbIntrospection.js +1078 -0
- package/plugins/core/lib/dbTablesOperations.js +1104 -160
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -2
- package/plugins/core/lib/querying/knex-process-includes.js +3 -4
- package/plugins/core/lib/querying/knex-query-helpers.js +17 -71
- package/plugins/core/lib/querying/knex-relationship-includes.js +466 -206
- package/plugins/core/lib/querying/query-builder-utils.js +19 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
- package/plugins/core/lib/querying/sort-helpers.js +33 -0
- package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
- package/plugins/core/lib/querying/url-helpers.js +8 -2
- package/plugins/core/lib/querying-writing/compile-schemas.js +12 -1
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -13
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
- package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
- package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +113 -0
- package/plugins/core/lib/storage/storage-adapter.js +79 -2
- package/plugins/core/lib/storage/storage-mapping.js +204 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +355 -228
- package/plugins/core/rest-api-autofilter-plugin.js +414 -0
- package/plugins/core/rest-api-knex-plugin.js +249 -211
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
- package/plugins/core/rest-api-plugin-methods/common.js +121 -83
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
- package/plugins/core/rest-api-plugin-methods/get.js +23 -14
- package/plugins/core/rest-api-plugin-methods/patch.js +28 -12
- package/plugins/core/rest-api-plugin-methods/post.js +13 -8
- package/plugins/core/rest-api-plugin-methods/put.js +45 -21
- package/plugins/core/rest-api-plugin-methods/query.js +15 -53
- package/plugins/core/rest-api-plugin.js +2 -0
- package/plugins/core/rest-api-positioning-plugin.js +1 -1
- package/plugins/core/rest-api-query-projections-plugin.js +130 -0
- package/plugins/core/socketio-plugin.js +1 -3
- package/tests/autofilter.test.js +463 -0
- package/tests/custom-id-simplified-post.test.js +75 -0
- package/tests/custom-idproperty-relationships.test.js +33 -0
- package/tests/db-introspection.test.js +518 -0
- package/tests/db-table-operations.test.js +781 -0
- package/tests/fastify-plugin.test.js +141 -0
- package/tests/field-getters.test.js +1 -2
- package/tests/field-setters.test.js +0 -4
- package/tests/fixtures/api-configs.js +188 -219
- package/tests/helpers/fake-fastify.js +99 -0
- package/tests/http-connectors-parity.test.js +235 -0
- package/tests/projected-fields.test.js +319 -0
- package/tests/relationships.test.js +3 -3
- package/tests/searchschema-knex-behavior.test.js +187 -0
- package/tests/searchschema-merge.test.js +21 -69
- package/tests/storage-mapping.test.js +210 -0
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
- package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
- package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
- package/plugins/core/rest-api-access.js +0 -459
- package/plugins/core/rest-api-multihome-plugin.js +0 -314
- package/tests/multihome.test.js +0 -553
- package/tests/multihome.test.js.bak +0 -550
- package/tests/ownership-optout.test.js +0 -76
- package/tests/rest-api-access.test.js +0 -206
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "json-rest-api",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.23",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"description": "REST API plugin for hooked-api with JSON:API compliance",
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"hooked-api": "^1.0.24",
|
|
28
|
-
"json-rest-schema": "^1.0.
|
|
28
|
+
"json-rest-schema": "^1.0.13",
|
|
29
29
|
"semver": "^7.7.2"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"express": "^4.0.0 || ^5.0.0",
|
|
36
36
|
"formidable": "^3.0.0",
|
|
37
37
|
"fractional-indexing": "^3.2.0",
|
|
38
|
-
"jose": "^5.10.0",
|
|
38
|
+
"jose": "^5.10.0 || ^6.0.0",
|
|
39
39
|
"knex": "^3.1.0",
|
|
40
40
|
"raw-body": "^2.0.0",
|
|
41
41
|
"redis": "^5.6.0",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"better-sqlite3": "^11.10.0",
|
|
82
82
|
"express": "^5.1.0",
|
|
83
83
|
"fractional-indexing": "^3.2.0",
|
|
84
|
-
"jose": "^
|
|
84
|
+
"jose": "^6.2.2",
|
|
85
85
|
"knex": "^3.1.0",
|
|
86
86
|
"redis": "^5.6.0",
|
|
87
87
|
"socket.io": "^4.8.1",
|
|
@@ -18,6 +18,20 @@ import { requirePackage } from 'hooked-api'
|
|
|
18
18
|
import { parseJsonApiQuery } from '../lib/querying-writing/connectors-query-parser.js'
|
|
19
19
|
import { createContext } from './lib/request-helpers.js'
|
|
20
20
|
import { createEnhancedLogger } from '../../../lib/enhanced-logger.js'
|
|
21
|
+
import {
|
|
22
|
+
isWriteMethod,
|
|
23
|
+
isAllowedWriteContentType,
|
|
24
|
+
getUnsupportedMediaTypeErrorBody
|
|
25
|
+
} from './lib/transport-http-helpers.js'
|
|
26
|
+
import {
|
|
27
|
+
extractQueryString,
|
|
28
|
+
buildTransportRequestData,
|
|
29
|
+
createConnectorContext,
|
|
30
|
+
runTransportRequestLifecycle,
|
|
31
|
+
buildTransportRejectionBody,
|
|
32
|
+
executeConnectorRoute,
|
|
33
|
+
handleConnectorError
|
|
34
|
+
} from './lib/connector-core.js'
|
|
21
35
|
|
|
22
36
|
export const ExpressPlugin = {
|
|
23
37
|
name: 'express',
|
|
@@ -115,58 +129,39 @@ export const ExpressPlugin = {
|
|
|
115
129
|
|
|
116
130
|
// Add transport hook middleware
|
|
117
131
|
router.use(async (req, res, next) => {
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
response: {
|
|
141
|
-
headers: {},
|
|
142
|
-
status: null
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Add transport data to context
|
|
147
|
-
context.transport = transportData
|
|
148
|
-
|
|
149
|
-
// Run transport hooks - don't check return value for consistency with other hooks
|
|
150
|
-
// Hooks communicate via context flags (rejection, handled) not return values
|
|
151
|
-
await runHooks('transport:request', context)
|
|
132
|
+
const requestData = buildTransportRequestData({
|
|
133
|
+
method: req.method,
|
|
134
|
+
url: req.url,
|
|
135
|
+
path: req.path,
|
|
136
|
+
headers: req.headers,
|
|
137
|
+
body: req.body,
|
|
138
|
+
params: req.params,
|
|
139
|
+
query: req.query
|
|
140
|
+
})
|
|
141
|
+
const { context, transportData } = createConnectorContext({
|
|
142
|
+
request: req,
|
|
143
|
+
reply: res,
|
|
144
|
+
source: 'express',
|
|
145
|
+
mountPath: basePath,
|
|
146
|
+
requestData,
|
|
147
|
+
createContext,
|
|
148
|
+
urlPrefixOverride: req.urlPrefixOverride
|
|
149
|
+
})
|
|
150
|
+
const { rejected, handled } = await runTransportRequestLifecycle({
|
|
151
|
+
context,
|
|
152
|
+
runHooks
|
|
153
|
+
})
|
|
152
154
|
|
|
153
|
-
|
|
154
|
-
if (context.rejection) {
|
|
155
|
-
// Apply response headers from hooks
|
|
155
|
+
if (rejected) {
|
|
156
156
|
if (transportData.response.headers) {
|
|
157
157
|
res.set(transportData.response.headers)
|
|
158
158
|
}
|
|
159
|
-
return res.status(context.rejection.status || 500).json(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
title: context.rejection.title || 'Request Rejected',
|
|
163
|
-
detail: context.rejection.message
|
|
164
|
-
}]
|
|
165
|
-
})
|
|
159
|
+
return res.status(context.rejection.status || 500).json(
|
|
160
|
+
buildTransportRejectionBody(context)
|
|
161
|
+
)
|
|
166
162
|
}
|
|
167
163
|
|
|
168
|
-
|
|
169
|
-
if (context.handled) {
|
|
164
|
+
if (handled) {
|
|
170
165
|
return
|
|
171
166
|
}
|
|
172
167
|
|
|
@@ -179,18 +174,13 @@ export const ExpressPlugin = {
|
|
|
179
174
|
// Content type validation middleware
|
|
180
175
|
if (strictContentType) {
|
|
181
176
|
router.use((req, res, next) => {
|
|
182
|
-
if (
|
|
177
|
+
if (isWriteMethod(req.method)) {
|
|
183
178
|
const contentType = req.get('Content-Type')
|
|
184
179
|
|
|
185
|
-
if (contentType
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
status: '415',
|
|
190
|
-
title: 'Unsupported Media Type',
|
|
191
|
-
detail: 'Content-Type must be application/vnd.api+json or application/json'
|
|
192
|
-
}]
|
|
193
|
-
})
|
|
180
|
+
if (!isAllowedWriteContentType(contentType, { allowMultipart: true })) {
|
|
181
|
+
return res.status(415).json(
|
|
182
|
+
getUnsupportedMediaTypeErrorBody({ allowMultipart: true })
|
|
183
|
+
)
|
|
194
184
|
}
|
|
195
185
|
}
|
|
196
186
|
next()
|
|
@@ -207,77 +197,16 @@ export const ExpressPlugin = {
|
|
|
207
197
|
url: req.url
|
|
208
198
|
})
|
|
209
199
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}]
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Map error codes to HTTP status
|
|
220
|
-
if (error.code === 'REST_API_VALIDATION') {
|
|
221
|
-
status = 422
|
|
222
|
-
errorResponse.errors = [{
|
|
223
|
-
status: '422',
|
|
224
|
-
title: 'Validation Error',
|
|
225
|
-
detail: error.message,
|
|
226
|
-
source: error.details
|
|
227
|
-
}]
|
|
228
|
-
if (error.details?.violations) {
|
|
229
|
-
errorResponse.errors = error.details.violations.map(v => ({
|
|
230
|
-
status: '422',
|
|
231
|
-
title: 'Validation Error',
|
|
232
|
-
detail: v.message,
|
|
233
|
-
source: { pointer: v.field }
|
|
234
|
-
}))
|
|
235
|
-
}
|
|
236
|
-
} else if (error.code === 'REST_API_RESOURCE') {
|
|
237
|
-
switch (error.subtype) {
|
|
238
|
-
case 'not_found':
|
|
239
|
-
status = 404
|
|
240
|
-
errorResponse.errors[0].status = '404'
|
|
241
|
-
errorResponse.errors[0].title = 'Not Found'
|
|
242
|
-
break
|
|
243
|
-
case 'conflict':
|
|
244
|
-
status = 409
|
|
245
|
-
errorResponse.errors[0].status = '409'
|
|
246
|
-
errorResponse.errors[0].title = 'Conflict'
|
|
247
|
-
break
|
|
248
|
-
case 'forbidden':
|
|
249
|
-
status = 403
|
|
250
|
-
errorResponse.errors[0].status = '403'
|
|
251
|
-
errorResponse.errors[0].title = 'Forbidden'
|
|
252
|
-
break
|
|
253
|
-
default:
|
|
254
|
-
status = 400
|
|
255
|
-
errorResponse.errors[0].status = '400'
|
|
256
|
-
errorResponse.errors[0].title = 'Bad Request'
|
|
257
|
-
}
|
|
258
|
-
} else if (error.code === 'REST_API_PAYLOAD') {
|
|
259
|
-
status = 400
|
|
260
|
-
errorResponse.errors = [{
|
|
261
|
-
status: '400',
|
|
262
|
-
title: 'Bad Request',
|
|
263
|
-
detail: error.message,
|
|
264
|
-
source: { pointer: error.path }
|
|
265
|
-
}]
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Run transport:response hook for errors
|
|
269
|
-
if (req.transportData && req.context) {
|
|
270
|
-
req.transportData.response.status = status
|
|
271
|
-
req.transportData.response.body = errorResponse
|
|
272
|
-
req.context.transport = req.transportData
|
|
273
|
-
await runHooks('transport:response', req.context)
|
|
200
|
+
const { status, body: errorResponse, headers } = await handleConnectorError({
|
|
201
|
+
error,
|
|
202
|
+
context: req.context,
|
|
203
|
+
transportData: req.transportData,
|
|
204
|
+
runHooks
|
|
205
|
+
})
|
|
274
206
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
res.set(req.transportData.response.headers)
|
|
278
|
-
}
|
|
207
|
+
if (headers) {
|
|
208
|
+
res.set(headers)
|
|
279
209
|
}
|
|
280
|
-
|
|
281
210
|
res.status(status).json(errorResponse)
|
|
282
211
|
}
|
|
283
212
|
|
|
@@ -285,7 +214,7 @@ export const ExpressPlugin = {
|
|
|
285
214
|
* Listen to addRoute hook to create Express routes
|
|
286
215
|
*/
|
|
287
216
|
addHook('addRoute', 'expressRouteCreator', {}, async ({ context }) => {
|
|
288
|
-
const { method, path, handler } = context
|
|
217
|
+
const { method, path, handler, routeMeta } = context
|
|
289
218
|
|
|
290
219
|
// Apply any global before middleware
|
|
291
220
|
const beforeMiddleware = expressOptions.middleware?.beforeAll || []
|
|
@@ -294,78 +223,62 @@ export const ExpressPlugin = {
|
|
|
294
223
|
// Extract the handler logic into a shared function to keep it DRY (Don't Repeat Yourself).
|
|
295
224
|
const expressHandler = async (req, res) => {
|
|
296
225
|
try {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
226
|
+
let context = req.context
|
|
227
|
+
let transportData = req.transportData
|
|
228
|
+
|
|
229
|
+
if (!context || !transportData) {
|
|
230
|
+
const requestData = buildTransportRequestData({
|
|
231
|
+
method: req.method,
|
|
232
|
+
url: req.url,
|
|
233
|
+
path: req.path,
|
|
234
|
+
headers: req.headers,
|
|
235
|
+
body: req.body,
|
|
236
|
+
params: req.params,
|
|
237
|
+
query: req.query
|
|
238
|
+
})
|
|
239
|
+
const setup = createConnectorContext({
|
|
240
|
+
request: req,
|
|
241
|
+
reply: res,
|
|
242
|
+
source: 'express',
|
|
243
|
+
mountPath: basePath,
|
|
244
|
+
requestData,
|
|
245
|
+
createContext,
|
|
246
|
+
urlPrefixOverride: req.urlPrefixOverride
|
|
247
|
+
})
|
|
248
|
+
context = setup.context
|
|
249
|
+
transportData = setup.transportData
|
|
250
|
+
req.context = context
|
|
251
|
+
req.transportData = transportData
|
|
252
|
+
}
|
|
300
253
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
254
|
+
const outcome = await executeConnectorRoute({
|
|
255
|
+
method: req.method,
|
|
256
|
+
handler,
|
|
257
|
+
queryString: extractQueryString(req.url),
|
|
304
258
|
headers: req.headers,
|
|
305
259
|
params: req.params,
|
|
306
260
|
body: req.body,
|
|
307
|
-
context
|
|
261
|
+
context,
|
|
262
|
+
transportData,
|
|
263
|
+
routeMeta,
|
|
264
|
+
helpers,
|
|
265
|
+
mountPath: basePath,
|
|
266
|
+
runHooks
|
|
308
267
|
})
|
|
309
|
-
|
|
310
|
-
// Prepare response status
|
|
311
|
-
let responseStatus = 200
|
|
312
|
-
if (result && typeof result.statusCode === 'number') {
|
|
313
|
-
responseStatus = result.statusCode
|
|
314
|
-
} else if (req.method === 'POST' && result) {
|
|
315
|
-
// POST with content returns 201 Created
|
|
316
|
-
responseStatus = 201
|
|
317
|
-
} else if (req.method === 'POST' && !result) {
|
|
318
|
-
// POST without content returns 204 No Content
|
|
319
|
-
responseStatus = 204
|
|
320
|
-
} else if (req.method === 'DELETE') {
|
|
321
|
-
// DELETE always returns 204 No Content
|
|
322
|
-
responseStatus = 204
|
|
323
|
-
} else if ((req.method === 'PUT' || req.method === 'PATCH') && !result) {
|
|
324
|
-
// PUT/PATCH return 204 when no content is returned
|
|
325
|
-
responseStatus = 204
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Apply headers from the handler result
|
|
329
|
-
if (result && result.headers) {
|
|
330
|
-
res.set(result.headers)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Update transport data for response (if transport data exists)
|
|
334
|
-
if (req.transportData) {
|
|
335
|
-
req.transportData.response.status = responseStatus
|
|
336
|
-
req.transportData.response.body = result
|
|
337
|
-
context.transport = req.transportData
|
|
338
|
-
await runHooks('transport:response', context)
|
|
339
|
-
if (req.transportData.response.headers) {
|
|
340
|
-
res.set(req.transportData.response.headers)
|
|
341
|
-
}
|
|
342
|
-
}
|
|
268
|
+
res.set(outcome.headers)
|
|
343
269
|
|
|
344
270
|
// Set content type
|
|
345
271
|
res.set('Content-Type', 'application/vnd.api+json')
|
|
346
272
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
// Extract scopeName from the path (e.g., /api/countries -> countries)
|
|
350
|
-
const pathParts = path.split('/')
|
|
351
|
-
const scopeName = pathParts[pathParts.length - 1]
|
|
352
|
-
|
|
353
|
-
// Set Location header for any successful POST (201 or 204)
|
|
354
|
-
if (helpers.getLocation) {
|
|
355
|
-
const location = helpers.getLocation({ scopeName, id: context.id })
|
|
356
|
-
const baseUrl = context.urlPrefix || basePath
|
|
357
|
-
res.set('Location', `${baseUrl}${location}`)
|
|
358
|
-
}
|
|
273
|
+
if (outcome.location) {
|
|
274
|
+
res.set('Location', outcome.location)
|
|
359
275
|
}
|
|
360
276
|
|
|
361
277
|
// Handle response based on status
|
|
362
|
-
if (
|
|
278
|
+
if (outcome.status === 204) {
|
|
363
279
|
res.sendStatus(204)
|
|
364
280
|
} else {
|
|
365
|
-
|
|
366
|
-
// This happens when handler returns { statusCode, body, headers }
|
|
367
|
-
const responseBody = result && result.body !== undefined ? result.body : result
|
|
368
|
-
res.status(responseStatus).json(responseBody)
|
|
281
|
+
res.status(outcome.status).json(outcome.body)
|
|
369
282
|
}
|
|
370
283
|
} catch (error) {
|
|
371
284
|
handleError(error, req, res)
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { createContext } from './lib/request-helpers.js'
|
|
2
|
+
import { createEnhancedLogger } from '../../../lib/enhanced-logger.js'
|
|
3
|
+
import { buildTransportRouteSchema } from './lib/transport-route-schemas.js'
|
|
4
|
+
import {
|
|
5
|
+
isWriteMethod,
|
|
6
|
+
isAllowedWriteContentType,
|
|
7
|
+
getUnsupportedMediaTypeErrorBody
|
|
8
|
+
} from './lib/transport-http-helpers.js'
|
|
9
|
+
import {
|
|
10
|
+
extractQueryString,
|
|
11
|
+
buildTransportRequestData,
|
|
12
|
+
createConnectorContext,
|
|
13
|
+
runTransportRequestLifecycle,
|
|
14
|
+
buildTransportRejectionBody,
|
|
15
|
+
executeConnectorRoute,
|
|
16
|
+
handleConnectorError
|
|
17
|
+
} from './lib/connector-core.js'
|
|
18
|
+
|
|
19
|
+
function applyHeaders (reply, headers = {}) {
|
|
20
|
+
for (const [headerName, headerValue] of Object.entries(headers)) {
|
|
21
|
+
reply.header(headerName, headerValue)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function registerVendorJsonParser (app) {
|
|
26
|
+
if (typeof app?.addContentTypeParser !== 'function') return
|
|
27
|
+
if (typeof app?.hasContentTypeParser === 'function' && app.hasContentTypeParser('application/vnd.api+json')) {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
app.addContentTypeParser(
|
|
32
|
+
'application/vnd.api+json',
|
|
33
|
+
{ parseAs: 'string' },
|
|
34
|
+
(request, body, done) => {
|
|
35
|
+
if (body === '' || body === undefined || body === null) {
|
|
36
|
+
done(null, {})
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
done(null, JSON.parse(body))
|
|
42
|
+
} catch (error) {
|
|
43
|
+
error.statusCode = 400
|
|
44
|
+
done(error)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const FastifyPlugin = {
|
|
51
|
+
name: 'fastify',
|
|
52
|
+
dependencies: ['rest-api'],
|
|
53
|
+
|
|
54
|
+
async install ({ vars, helpers, pluginOptions, log, api, runHooks, addHook }) {
|
|
55
|
+
const fastifyOptions = pluginOptions || {}
|
|
56
|
+
const app = fastifyOptions.app
|
|
57
|
+
|
|
58
|
+
if (!app || typeof app.route !== 'function') {
|
|
59
|
+
throw new Error('FastifyPlugin requires a Fastify instance in pluginOptions.app.')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const enhancedLog = createEnhancedLogger(log, {
|
|
63
|
+
logFullErrors: true,
|
|
64
|
+
includeStack: true
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
if (!api.http) {
|
|
68
|
+
api.http = {}
|
|
69
|
+
}
|
|
70
|
+
api.http.fastify = { app }
|
|
71
|
+
|
|
72
|
+
const mountPath = fastifyOptions.mountPath || ''
|
|
73
|
+
const strictContentType = fastifyOptions.strictContentType !== false
|
|
74
|
+
|
|
75
|
+
vars.transport = {
|
|
76
|
+
type: 'fastify',
|
|
77
|
+
matchAll: '*',
|
|
78
|
+
mountPath
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
registerVendorJsonParser(app)
|
|
82
|
+
|
|
83
|
+
const buildFastifyHandler = ({ method, path, handler, routeMeta }) => {
|
|
84
|
+
const routeSchema = buildTransportRouteSchema({ routeMeta, api })
|
|
85
|
+
|
|
86
|
+
const fastifyErrorHandler = async (error, request, reply) => {
|
|
87
|
+
enhancedLog.logError('Fastify request error', error, {
|
|
88
|
+
method: request.method,
|
|
89
|
+
path: request.url
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
const context = request.jsonRestContext || null
|
|
93
|
+
const transportData = context?.transport || null
|
|
94
|
+
const { status, body, headers } = await handleConnectorError({
|
|
95
|
+
error,
|
|
96
|
+
context,
|
|
97
|
+
transportData,
|
|
98
|
+
runHooks
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
applyHeaders(reply, headers)
|
|
102
|
+
reply.code(status)
|
|
103
|
+
reply.type('application/vnd.api+json')
|
|
104
|
+
return reply.send(body)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const preValidation = async (request, reply) => {
|
|
108
|
+
if (!strictContentType || !isWriteMethod(method)) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const contentType = request.headers?.['content-type'] || ''
|
|
113
|
+
if (isAllowedWriteContentType(contentType)) {
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
reply.code(415)
|
|
118
|
+
reply.type('application/vnd.api+json')
|
|
119
|
+
return reply.send(getUnsupportedMediaTypeErrorBody())
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
method,
|
|
124
|
+
url: path,
|
|
125
|
+
...(routeSchema ? { schema: routeSchema } : {}),
|
|
126
|
+
errorHandler: fastifyErrorHandler,
|
|
127
|
+
preValidation,
|
|
128
|
+
handler: async (request, reply) => {
|
|
129
|
+
const requestData = buildTransportRequestData({
|
|
130
|
+
method: request.method,
|
|
131
|
+
url: request.url,
|
|
132
|
+
path: request.url,
|
|
133
|
+
headers: request.headers,
|
|
134
|
+
body: request.body,
|
|
135
|
+
params: request.params,
|
|
136
|
+
query: request.query
|
|
137
|
+
})
|
|
138
|
+
const { context, transportData } = createConnectorContext({
|
|
139
|
+
request,
|
|
140
|
+
reply,
|
|
141
|
+
source: 'fastify',
|
|
142
|
+
mountPath,
|
|
143
|
+
requestData,
|
|
144
|
+
createContext
|
|
145
|
+
})
|
|
146
|
+
request.jsonRestContext = context
|
|
147
|
+
|
|
148
|
+
const { rejected, handled } = await runTransportRequestLifecycle({
|
|
149
|
+
context,
|
|
150
|
+
runHooks
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
if (rejected) {
|
|
154
|
+
const rejectionBody = buildTransportRejectionBody(context)
|
|
155
|
+
applyHeaders(reply, transportData.response.headers)
|
|
156
|
+
reply.code(context.rejection.status || 500)
|
|
157
|
+
reply.type('application/vnd.api+json')
|
|
158
|
+
return reply.send(rejectionBody)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (handled) {
|
|
162
|
+
return
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const outcome = await executeConnectorRoute({
|
|
166
|
+
method,
|
|
167
|
+
handler,
|
|
168
|
+
queryString: extractQueryString(request?.raw?.url || request?.url || ''),
|
|
169
|
+
headers: request.headers,
|
|
170
|
+
params: request.params,
|
|
171
|
+
body: request.body,
|
|
172
|
+
context,
|
|
173
|
+
transportData,
|
|
174
|
+
routeMeta,
|
|
175
|
+
helpers,
|
|
176
|
+
mountPath,
|
|
177
|
+
runHooks
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
applyHeaders(reply, outcome.headers)
|
|
181
|
+
reply.type('application/vnd.api+json')
|
|
182
|
+
|
|
183
|
+
if (outcome.location) {
|
|
184
|
+
reply.header('Location', outcome.location)
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (outcome.status === 204) {
|
|
188
|
+
reply.code(204)
|
|
189
|
+
return reply.send()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
reply.code(outcome.status)
|
|
193
|
+
return reply.send(outcome.body)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
addHook('addRoute', 'fastifyRouteCreator', {}, async ({ context }) => {
|
|
199
|
+
const route = buildFastifyHandler(context)
|
|
200
|
+
app.route(route)
|
|
201
|
+
log.trace(`Fastify route created: ${context.method} ${context.path}`)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
log.info('Fastify plugin initialized successfully')
|
|
205
|
+
}
|
|
206
|
+
}
|