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,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Express Plugin for Hooked API
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* This plugin creates HTTP endpoints for your REST API by listening to
|
|
5
5
|
* route registrations from the REST API plugin and creating Express routes.
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* Features:
|
|
8
8
|
* - Automatic route creation via addRoute hook
|
|
9
9
|
* - JSON:API compliant request/response handling
|
|
@@ -14,121 +14,118 @@
|
|
|
14
14
|
* - File upload support with busboy or formidable
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import { requirePackage } from 'hooked-api'
|
|
18
|
-
import { parseJsonApiQuery } from '../lib/querying-writing/connectors-query-parser.js'
|
|
19
|
-
import { createContext } from './lib/request-helpers.js'
|
|
20
|
-
import { createEnhancedLogger } from '../../../lib/enhanced-logger.js'
|
|
17
|
+
import { requirePackage } from 'hooked-api'
|
|
18
|
+
import { parseJsonApiQuery } from '../lib/querying-writing/connectors-query-parser.js'
|
|
19
|
+
import { createContext } from './lib/request-helpers.js'
|
|
20
|
+
import { createEnhancedLogger } from '../../../lib/enhanced-logger.js'
|
|
21
21
|
|
|
22
22
|
export const ExpressPlugin = {
|
|
23
23
|
name: 'express',
|
|
24
24
|
dependencies: ['rest-api'],
|
|
25
|
-
|
|
26
|
-
async install({ on, vars, helpers, pluginOptions, log, scopes, api, runHooks, addHook }) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
addHook('release', 'releaseHook', {},
|
|
30
|
-
async ({ api }) => {} // TODO: Anything to do here?
|
|
31
|
-
);
|
|
32
25
|
|
|
26
|
+
async install ({ on, vars, helpers, pluginOptions, log, scopes, api, runHooks, addHook }) {
|
|
27
|
+
addHook('release', 'releaseHook', {},
|
|
28
|
+
async ({ api }) => {} // TODO: Anything to do here?
|
|
29
|
+
)
|
|
33
30
|
|
|
34
31
|
// Dynamic import for Express
|
|
35
|
-
let express
|
|
32
|
+
let express
|
|
36
33
|
try {
|
|
37
|
-
express = (await import('express')).default
|
|
34
|
+
express = (await import('express')).default
|
|
38
35
|
} catch (e) {
|
|
39
|
-
requirePackage('express', 'express',
|
|
40
|
-
'Express.js is required for HTTP server functionality. This is a peer dependency.')
|
|
36
|
+
requirePackage('express', 'express',
|
|
37
|
+
'Express.js is required for HTTP server functionality. This is a peer dependency.')
|
|
41
38
|
}
|
|
42
39
|
// Enhance the logger
|
|
43
|
-
const enhancedLog = createEnhancedLogger(log, {
|
|
44
|
-
logFullErrors: true,
|
|
45
|
-
includeStack: true
|
|
46
|
-
})
|
|
47
|
-
|
|
40
|
+
const enhancedLog = createEnhancedLogger(log, {
|
|
41
|
+
logFullErrors: true,
|
|
42
|
+
includeStack: true
|
|
43
|
+
})
|
|
44
|
+
|
|
48
45
|
// Initialize express namespace
|
|
49
46
|
if (!api.http) {
|
|
50
|
-
api.http = {}
|
|
47
|
+
api.http = {}
|
|
51
48
|
}
|
|
52
|
-
api.http.express = {}
|
|
53
|
-
|
|
54
|
-
const expressOptions = pluginOptions || {}
|
|
55
|
-
|
|
49
|
+
api.http.express = {}
|
|
50
|
+
|
|
51
|
+
const expressOptions = pluginOptions || {}
|
|
52
|
+
|
|
56
53
|
// Get mountPath from options (this is now a transport concern)
|
|
57
|
-
const mountPath = expressOptions.mountPath || ''
|
|
58
|
-
const basePath = mountPath
|
|
59
|
-
const strictContentType = expressOptions.strictContentType !== false
|
|
60
|
-
const requestSizeLimit = expressOptions.requestSizeLimit || '1mb'
|
|
61
|
-
|
|
54
|
+
const mountPath = expressOptions.mountPath || ''
|
|
55
|
+
const basePath = mountPath // Keep basePath for internal use
|
|
56
|
+
const strictContentType = expressOptions.strictContentType !== false
|
|
57
|
+
const requestSizeLimit = expressOptions.requestSizeLimit || '1mb'
|
|
58
|
+
|
|
62
59
|
// Set transport information for other plugins
|
|
63
60
|
vars.transport = {
|
|
64
61
|
type: 'express',
|
|
65
62
|
matchAll: '*', // Express wildcard pattern for matching all routes
|
|
66
|
-
mountPath
|
|
67
|
-
}
|
|
68
|
-
|
|
63
|
+
mountPath // Transport-specific mount path
|
|
64
|
+
}
|
|
65
|
+
|
|
69
66
|
// Register file detector if enabled
|
|
70
67
|
if (expressOptions.enableFileUploads !== false && api.rest?.registerFileDetector) {
|
|
71
|
-
const parserLib = expressOptions.fileParser || 'busboy'
|
|
72
|
-
const parserOptions = expressOptions.fileParserOptions || {}
|
|
73
|
-
|
|
74
|
-
let detector
|
|
75
|
-
|
|
68
|
+
const parserLib = expressOptions.fileParser || 'busboy'
|
|
69
|
+
const parserOptions = expressOptions.fileParserOptions || {}
|
|
70
|
+
|
|
71
|
+
let detector
|
|
72
|
+
|
|
76
73
|
if (parserLib === 'busboy') {
|
|
77
74
|
try {
|
|
78
|
-
const { createBusboyDetector } = await import('../lib/busboy-detector.js')
|
|
79
|
-
detector = createBusboyDetector(parserOptions)
|
|
75
|
+
const { createBusboyDetector } = await import('../lib/busboy-detector.js')
|
|
76
|
+
detector = createBusboyDetector(parserOptions)
|
|
80
77
|
} catch (e) {
|
|
81
|
-
log.warn('Busboy not installed. Install with: npm install busboy')
|
|
78
|
+
log.warn('Busboy not installed. Install with: npm install busboy')
|
|
82
79
|
}
|
|
83
80
|
} else if (parserLib === 'formidable') {
|
|
84
81
|
try {
|
|
85
|
-
const { createFormidableDetector } = await import('../lib/formidable-detector.js')
|
|
86
|
-
detector = createFormidableDetector(parserOptions)
|
|
82
|
+
const { createFormidableDetector } = await import('../lib/formidable-detector.js')
|
|
83
|
+
detector = createFormidableDetector(parserOptions)
|
|
87
84
|
} catch (e) {
|
|
88
|
-
log.warn('Formidable not installed. Install with: npm install formidable')
|
|
85
|
+
log.warn('Formidable not installed. Install with: npm install formidable')
|
|
89
86
|
}
|
|
90
87
|
}
|
|
91
|
-
|
|
88
|
+
|
|
92
89
|
if (detector) {
|
|
93
90
|
api.rest.registerFileDetector({
|
|
94
91
|
name: `express-${detector.name}`,
|
|
95
92
|
detect: (params, context) => {
|
|
96
|
-
if (!context || !context.raw || !context.raw.req) return false
|
|
97
|
-
const detectParams = { ...params, _expressReq: context.raw.req }
|
|
98
|
-
return detector.detect(detectParams)
|
|
93
|
+
if (!context || !context.raw || !context.raw.req) return false
|
|
94
|
+
const detectParams = { ...params, _expressReq: context.raw.req }
|
|
95
|
+
return detector.detect(detectParams)
|
|
99
96
|
},
|
|
100
97
|
parse: (params, context) => {
|
|
101
|
-
const parseParams = { ...params, _expressReq: context.raw.req, _expressRes: context.raw.res }
|
|
102
|
-
return detector.parse(parseParams)
|
|
98
|
+
const parseParams = { ...params, _expressReq: context.raw.req, _expressRes: context.raw.res }
|
|
99
|
+
return detector.parse(parseParams)
|
|
103
100
|
}
|
|
104
|
-
})
|
|
105
|
-
log.info(`Express plugin registered file detector: ${detector.name}`)
|
|
101
|
+
})
|
|
102
|
+
log.info(`Express plugin registered file detector: ${detector.name}`)
|
|
106
103
|
}
|
|
107
104
|
}
|
|
108
|
-
|
|
105
|
+
|
|
109
106
|
// Create Express routers
|
|
110
|
-
const router = expressOptions.router || express.Router()
|
|
111
|
-
const notFoundRouter = express.Router()
|
|
112
|
-
|
|
107
|
+
const router = expressOptions.router || express.Router()
|
|
108
|
+
const notFoundRouter = express.Router()
|
|
109
|
+
|
|
113
110
|
// Add body parsing middleware
|
|
114
|
-
router.use(express.json({
|
|
111
|
+
router.use(express.json({
|
|
115
112
|
limit: requestSizeLimit,
|
|
116
113
|
type: ['application/json', 'application/vnd.api+json']
|
|
117
|
-
}))
|
|
118
|
-
|
|
114
|
+
}))
|
|
115
|
+
|
|
119
116
|
// Add transport hook middleware
|
|
120
117
|
router.use(async (req, res, next) => {
|
|
121
|
-
const context = createContext(req, res, 'express')
|
|
122
|
-
|
|
118
|
+
const context = createContext(req, res, 'express')
|
|
119
|
+
|
|
123
120
|
// Check for Express middleware override first
|
|
124
121
|
if (req.urlPrefixOverride) {
|
|
125
|
-
context.urlPrefixOverride = req.urlPrefixOverride
|
|
122
|
+
context.urlPrefixOverride = req.urlPrefixOverride
|
|
126
123
|
}
|
|
127
|
-
|
|
124
|
+
|
|
128
125
|
// Calculate and cache URL prefix (getUrlPrefix handles all logic)
|
|
129
|
-
const { getUrlPrefix } = await import('../lib/querying/url-helpers.js')
|
|
130
|
-
context.urlPrefix = getUrlPrefix(context, { vars: { transport: { mountPath: basePath } } }, req)
|
|
131
|
-
|
|
126
|
+
const { getUrlPrefix } = await import('../lib/querying/url-helpers.js')
|
|
127
|
+
context.urlPrefix = getUrlPrefix(context, { vars: { transport: { mountPath: basePath } } }, req)
|
|
128
|
+
|
|
132
129
|
// Transport-specific data for hooks
|
|
133
130
|
const transportData = {
|
|
134
131
|
request: {
|
|
@@ -144,20 +141,20 @@ export const ExpressPlugin = {
|
|
|
144
141
|
headers: {},
|
|
145
142
|
status: null
|
|
146
143
|
}
|
|
147
|
-
}
|
|
148
|
-
|
|
144
|
+
}
|
|
145
|
+
|
|
149
146
|
// Add transport data to context
|
|
150
|
-
context.transport = transportData
|
|
147
|
+
context.transport = transportData
|
|
151
148
|
|
|
152
149
|
// Run transport hooks - don't check return value for consistency with other hooks
|
|
153
150
|
// Hooks communicate via context flags (rejection, handled) not return values
|
|
154
|
-
await runHooks('transport:request', context)
|
|
151
|
+
await runHooks('transport:request', context)
|
|
155
152
|
|
|
156
153
|
// Check if request was rejected (e.g., authentication failure)
|
|
157
154
|
if (context.rejection) {
|
|
158
155
|
// Apply response headers from hooks
|
|
159
156
|
if (transportData.response.headers) {
|
|
160
|
-
res.set(transportData.response.headers)
|
|
157
|
+
res.set(transportData.response.headers)
|
|
161
158
|
}
|
|
162
159
|
return res.status(context.rejection.status || 500).json({
|
|
163
160
|
errors: [{
|
|
@@ -165,27 +162,27 @@ export const ExpressPlugin = {
|
|
|
165
162
|
title: context.rejection.title || 'Request Rejected',
|
|
166
163
|
detail: context.rejection.message
|
|
167
164
|
}]
|
|
168
|
-
})
|
|
165
|
+
})
|
|
169
166
|
}
|
|
170
167
|
|
|
171
168
|
// Check if request was already handled by a hook
|
|
172
169
|
if (context.handled) {
|
|
173
|
-
return
|
|
170
|
+
return
|
|
174
171
|
}
|
|
175
|
-
|
|
172
|
+
|
|
176
173
|
// Store transport data and context for later use
|
|
177
|
-
req.transportData = transportData
|
|
178
|
-
req.context = context
|
|
179
|
-
next()
|
|
180
|
-
})
|
|
181
|
-
|
|
174
|
+
req.transportData = transportData
|
|
175
|
+
req.context = context
|
|
176
|
+
next()
|
|
177
|
+
})
|
|
178
|
+
|
|
182
179
|
// Content type validation middleware
|
|
183
180
|
if (strictContentType) {
|
|
184
181
|
router.use((req, res, next) => {
|
|
185
182
|
if (['POST', 'PUT', 'PATCH'].includes(req.method)) {
|
|
186
|
-
const contentType = req.get('Content-Type')
|
|
187
|
-
|
|
188
|
-
if (contentType && !contentType.includes('application/vnd.api+json') &&
|
|
183
|
+
const contentType = req.get('Content-Type')
|
|
184
|
+
|
|
185
|
+
if (contentType && !contentType.includes('application/vnd.api+json') &&
|
|
189
186
|
!contentType.includes('application/json') && !contentType.includes('multipart/form-data')) {
|
|
190
187
|
return res.status(415).json({
|
|
191
188
|
errors: [{
|
|
@@ -193,13 +190,13 @@ export const ExpressPlugin = {
|
|
|
193
190
|
title: 'Unsupported Media Type',
|
|
194
191
|
detail: 'Content-Type must be application/vnd.api+json or application/json'
|
|
195
192
|
}]
|
|
196
|
-
})
|
|
193
|
+
})
|
|
197
194
|
}
|
|
198
195
|
}
|
|
199
|
-
next()
|
|
200
|
-
})
|
|
196
|
+
next()
|
|
197
|
+
})
|
|
201
198
|
}
|
|
202
|
-
|
|
199
|
+
|
|
203
200
|
/**
|
|
204
201
|
* Error handler - maps REST API errors to HTTP responses
|
|
205
202
|
*/
|
|
@@ -208,99 +205,98 @@ export const ExpressPlugin = {
|
|
|
208
205
|
method: req.method,
|
|
209
206
|
path: req.path,
|
|
210
207
|
url: req.url
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
let status = 500
|
|
214
|
-
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
let status = 500
|
|
211
|
+
const errorResponse = {
|
|
215
212
|
errors: [{
|
|
216
213
|
status: '500',
|
|
217
214
|
title: 'Internal Server Error',
|
|
218
215
|
detail: error.message
|
|
219
216
|
}]
|
|
220
|
-
}
|
|
221
|
-
|
|
217
|
+
}
|
|
218
|
+
|
|
222
219
|
// Map error codes to HTTP status
|
|
223
220
|
if (error.code === 'REST_API_VALIDATION') {
|
|
224
|
-
status = 422
|
|
221
|
+
status = 422
|
|
225
222
|
errorResponse.errors = [{
|
|
226
223
|
status: '422',
|
|
227
224
|
title: 'Validation Error',
|
|
228
225
|
detail: error.message,
|
|
229
226
|
source: error.details
|
|
230
|
-
}]
|
|
227
|
+
}]
|
|
231
228
|
if (error.details?.violations) {
|
|
232
229
|
errorResponse.errors = error.details.violations.map(v => ({
|
|
233
230
|
status: '422',
|
|
234
231
|
title: 'Validation Error',
|
|
235
232
|
detail: v.message,
|
|
236
233
|
source: { pointer: v.field }
|
|
237
|
-
}))
|
|
234
|
+
}))
|
|
238
235
|
}
|
|
239
236
|
} else if (error.code === 'REST_API_RESOURCE') {
|
|
240
237
|
switch (error.subtype) {
|
|
241
238
|
case 'not_found':
|
|
242
|
-
status = 404
|
|
243
|
-
errorResponse.errors[0].status = '404'
|
|
244
|
-
errorResponse.errors[0].title = 'Not Found'
|
|
245
|
-
break
|
|
239
|
+
status = 404
|
|
240
|
+
errorResponse.errors[0].status = '404'
|
|
241
|
+
errorResponse.errors[0].title = 'Not Found'
|
|
242
|
+
break
|
|
246
243
|
case 'conflict':
|
|
247
|
-
status = 409
|
|
248
|
-
errorResponse.errors[0].status = '409'
|
|
249
|
-
errorResponse.errors[0].title = 'Conflict'
|
|
250
|
-
break
|
|
244
|
+
status = 409
|
|
245
|
+
errorResponse.errors[0].status = '409'
|
|
246
|
+
errorResponse.errors[0].title = 'Conflict'
|
|
247
|
+
break
|
|
251
248
|
case 'forbidden':
|
|
252
|
-
status = 403
|
|
253
|
-
errorResponse.errors[0].status = '403'
|
|
254
|
-
errorResponse.errors[0].title = 'Forbidden'
|
|
255
|
-
break
|
|
249
|
+
status = 403
|
|
250
|
+
errorResponse.errors[0].status = '403'
|
|
251
|
+
errorResponse.errors[0].title = 'Forbidden'
|
|
252
|
+
break
|
|
256
253
|
default:
|
|
257
|
-
status = 400
|
|
258
|
-
errorResponse.errors[0].status = '400'
|
|
259
|
-
errorResponse.errors[0].title = 'Bad Request'
|
|
254
|
+
status = 400
|
|
255
|
+
errorResponse.errors[0].status = '400'
|
|
256
|
+
errorResponse.errors[0].title = 'Bad Request'
|
|
260
257
|
}
|
|
261
258
|
} else if (error.code === 'REST_API_PAYLOAD') {
|
|
262
|
-
status = 400
|
|
259
|
+
status = 400
|
|
263
260
|
errorResponse.errors = [{
|
|
264
261
|
status: '400',
|
|
265
262
|
title: 'Bad Request',
|
|
266
263
|
detail: error.message,
|
|
267
264
|
source: { pointer: error.path }
|
|
268
|
-
}]
|
|
265
|
+
}]
|
|
269
266
|
}
|
|
270
|
-
|
|
267
|
+
|
|
271
268
|
// Run transport:response hook for errors
|
|
272
269
|
if (req.transportData && req.context) {
|
|
273
|
-
req.transportData.response.status = status
|
|
274
|
-
req.transportData.response.body = errorResponse
|
|
275
|
-
req.context.transport = req.transportData
|
|
276
|
-
await runHooks('transport:response', req.context)
|
|
277
|
-
|
|
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)
|
|
274
|
+
|
|
278
275
|
// Apply response headers from hooks
|
|
279
276
|
if (req.transportData.response.headers) {
|
|
280
|
-
res.set(req.transportData.response.headers)
|
|
277
|
+
res.set(req.transportData.response.headers)
|
|
281
278
|
}
|
|
282
279
|
}
|
|
283
|
-
|
|
284
|
-
res.status(status).json(errorResponse)
|
|
285
|
-
}
|
|
286
|
-
|
|
280
|
+
|
|
281
|
+
res.status(status).json(errorResponse)
|
|
282
|
+
}
|
|
283
|
+
|
|
287
284
|
/**
|
|
288
285
|
* Listen to addRoute hook to create Express routes
|
|
289
286
|
*/
|
|
290
287
|
addHook('addRoute', 'expressRouteCreator', {}, async ({ context }) => {
|
|
291
|
-
const { method, path, handler } = context
|
|
292
|
-
|
|
288
|
+
const { method, path, handler } = context
|
|
289
|
+
|
|
293
290
|
// Apply any global before middleware
|
|
294
|
-
const beforeMiddleware = expressOptions.middleware?.beforeAll || []
|
|
295
|
-
|
|
291
|
+
const beforeMiddleware = expressOptions.middleware?.beforeAll || []
|
|
292
|
+
|
|
296
293
|
try {
|
|
297
|
-
|
|
298
294
|
// Extract the handler logic into a shared function to keep it DRY (Don't Repeat Yourself).
|
|
299
295
|
const expressHandler = async (req, res) => {
|
|
300
296
|
try {
|
|
301
297
|
// Extract request data
|
|
302
|
-
const queryString = req.url.split('?')[1] || ''
|
|
303
|
-
const context = req.context || createContext(req, res, 'express')
|
|
298
|
+
const queryString = req.url.split('?')[1] || ''
|
|
299
|
+
const context = req.context || createContext(req, res, 'express')
|
|
304
300
|
|
|
305
301
|
// Call the generic handler
|
|
306
302
|
const result = await handler({
|
|
@@ -309,85 +305,85 @@ export const ExpressPlugin = {
|
|
|
309
305
|
params: req.params,
|
|
310
306
|
body: req.body,
|
|
311
307
|
context
|
|
312
|
-
})
|
|
308
|
+
})
|
|
313
309
|
|
|
314
310
|
// Prepare response status
|
|
315
|
-
let responseStatus = 200
|
|
311
|
+
let responseStatus = 200
|
|
316
312
|
if (result && typeof result.statusCode === 'number') {
|
|
317
|
-
responseStatus = result.statusCode
|
|
313
|
+
responseStatus = result.statusCode
|
|
318
314
|
} else if (req.method === 'POST' && result) {
|
|
319
315
|
// POST with content returns 201 Created
|
|
320
|
-
responseStatus = 201
|
|
316
|
+
responseStatus = 201
|
|
321
317
|
} else if (req.method === 'POST' && !result) {
|
|
322
318
|
// POST without content returns 204 No Content
|
|
323
|
-
responseStatus = 204
|
|
319
|
+
responseStatus = 204
|
|
324
320
|
} else if (req.method === 'DELETE') {
|
|
325
321
|
// DELETE always returns 204 No Content
|
|
326
|
-
responseStatus = 204
|
|
322
|
+
responseStatus = 204
|
|
327
323
|
} else if ((req.method === 'PUT' || req.method === 'PATCH') && !result) {
|
|
328
324
|
// PUT/PATCH return 204 when no content is returned
|
|
329
|
-
responseStatus = 204
|
|
325
|
+
responseStatus = 204
|
|
330
326
|
}
|
|
331
327
|
|
|
332
328
|
// Apply headers from the handler result
|
|
333
329
|
if (result && result.headers) {
|
|
334
|
-
res.set(result.headers)
|
|
330
|
+
res.set(result.headers)
|
|
335
331
|
}
|
|
336
332
|
|
|
337
333
|
// Update transport data for response (if transport data exists)
|
|
338
334
|
if (req.transportData) {
|
|
339
|
-
req.transportData.response.status = responseStatus
|
|
340
|
-
req.transportData.response.body = result
|
|
341
|
-
context.transport = req.transportData
|
|
342
|
-
await runHooks('transport:response', context)
|
|
335
|
+
req.transportData.response.status = responseStatus
|
|
336
|
+
req.transportData.response.body = result
|
|
337
|
+
context.transport = req.transportData
|
|
338
|
+
await runHooks('transport:response', context)
|
|
343
339
|
if (req.transportData.response.headers) {
|
|
344
|
-
res.set(req.transportData.response.headers)
|
|
340
|
+
res.set(req.transportData.response.headers)
|
|
345
341
|
}
|
|
346
342
|
}
|
|
347
343
|
|
|
348
344
|
// Set content type
|
|
349
|
-
res.set('Content-Type', 'application/vnd.api+json')
|
|
345
|
+
res.set('Content-Type', 'application/vnd.api+json')
|
|
350
346
|
|
|
351
347
|
// Set Location header for successful POST requests
|
|
352
348
|
if (req.method === 'POST' && context.id) {
|
|
353
349
|
// Extract scopeName from the path (e.g., /api/countries -> countries)
|
|
354
|
-
const pathParts = path.split('/')
|
|
355
|
-
const scopeName = pathParts[pathParts.length - 1]
|
|
356
|
-
|
|
350
|
+
const pathParts = path.split('/')
|
|
351
|
+
const scopeName = pathParts[pathParts.length - 1]
|
|
352
|
+
|
|
357
353
|
// Set Location header for any successful POST (201 or 204)
|
|
358
354
|
if (helpers.getLocation) {
|
|
359
|
-
const location = helpers.getLocation({ scopeName, id: context.id })
|
|
360
|
-
const baseUrl = context.urlPrefix || basePath
|
|
361
|
-
res.set('Location', `${baseUrl}${location}`)
|
|
355
|
+
const location = helpers.getLocation({ scopeName, id: context.id })
|
|
356
|
+
const baseUrl = context.urlPrefix || basePath
|
|
357
|
+
res.set('Location', `${baseUrl}${location}`)
|
|
362
358
|
}
|
|
363
359
|
}
|
|
364
360
|
|
|
365
361
|
// Handle response based on status
|
|
366
362
|
if (responseStatus === 204) {
|
|
367
|
-
res.sendStatus(204)
|
|
363
|
+
res.sendStatus(204)
|
|
368
364
|
} else {
|
|
369
365
|
// If result has a body property, that's what we should send
|
|
370
366
|
// This happens when handler returns { statusCode, body, headers }
|
|
371
|
-
const responseBody = result && result.body !== undefined ? result.body : result
|
|
372
|
-
res.status(responseStatus).json(responseBody)
|
|
367
|
+
const responseBody = result && result.body !== undefined ? result.body : result
|
|
368
|
+
res.status(responseStatus).json(responseBody)
|
|
373
369
|
}
|
|
374
370
|
} catch (error) {
|
|
375
|
-
handleError(error, req, res)
|
|
371
|
+
handleError(error, req, res)
|
|
376
372
|
}
|
|
377
|
-
}
|
|
373
|
+
}
|
|
378
374
|
|
|
379
375
|
// CRITICAL: Express routing method selection - wildcard vs specific routes
|
|
380
|
-
//
|
|
376
|
+
//
|
|
381
377
|
// Express provides two different ways to register routes:
|
|
382
378
|
// 1. router.METHOD(path, handler) - e.g., router.get('/users', handler)
|
|
383
379
|
// - Only responds to the SPECIFIC HTTP method
|
|
384
380
|
// - Perfect for normal REST endpoints
|
|
385
|
-
//
|
|
386
|
-
// 2. router.use(path, handler)
|
|
381
|
+
//
|
|
382
|
+
// 2. router.use(path, handler)
|
|
387
383
|
// - Responds to ALL HTTP methods
|
|
388
384
|
// - Needed for wildcard paths that must handle any method
|
|
389
385
|
//
|
|
390
|
-
// The CORS plugin needs wildcard routes because it must handle OPTIONS
|
|
386
|
+
// The CORS plugin needs wildcard routes because it must handle OPTIONS
|
|
391
387
|
// requests for ANY path under the API prefix, even paths that don't exist
|
|
392
388
|
// as defined routes. For example:
|
|
393
389
|
// - Defined route: GET /api/users
|
|
@@ -400,22 +396,21 @@ export const ExpressPlugin = {
|
|
|
400
396
|
// - We need to catch ALL paths (using '*' or '/api/*')
|
|
401
397
|
// - We need to handle a SPECIFIC method (e.g., OPTIONS)
|
|
402
398
|
// - router.options('*') would NOT work for paths like '/api/some/nested/path'
|
|
403
|
-
|
|
404
399
|
|
|
405
400
|
// Since router.use() responds to ALL methods, we need a wrapper
|
|
406
401
|
// that only handles our specific method (e.g., OPTIONS)
|
|
407
402
|
const methodSpecificMiddleware = (req, res, next) => {
|
|
408
403
|
if (req.method.toLowerCase() === method.toLowerCase()) {
|
|
409
|
-
expressHandler(req, res)
|
|
404
|
+
expressHandler(req, res)
|
|
410
405
|
} else {
|
|
411
406
|
// Not our method, pass to next middleware
|
|
412
|
-
next()
|
|
407
|
+
next()
|
|
413
408
|
}
|
|
414
|
-
}
|
|
409
|
+
}
|
|
415
410
|
// IMPORTANT: We do NOT need to pass a path to router.use() here!
|
|
416
411
|
// When no path is provided, router.use() matches ALL requests
|
|
417
412
|
// This is exactly what we want for wildcard routes
|
|
418
|
-
router.use(...beforeMiddleware, methodSpecificMiddleware)
|
|
413
|
+
router.use(...beforeMiddleware, methodSpecificMiddleware)
|
|
419
414
|
} else {
|
|
420
415
|
// This is a normal route with a specific path (e.g., '/api/users')
|
|
421
416
|
// We use router.METHOD() because:
|
|
@@ -426,48 +421,46 @@ export const ExpressPlugin = {
|
|
|
426
421
|
// Note: Routes from RestApiPlugin already include the full path with mountPath
|
|
427
422
|
// So we use them as-is without adding basePath to avoid double-prefixing
|
|
428
423
|
|
|
429
|
-
router[method.toLowerCase()](path, ...beforeMiddleware, expressHandler)
|
|
424
|
+
router[method.toLowerCase()](path, ...beforeMiddleware, expressHandler)
|
|
430
425
|
|
|
431
426
|
// Debug logging for route registration
|
|
432
427
|
|
|
433
428
|
// Check if the route was actually added
|
|
434
429
|
if (router.stack) {
|
|
435
|
-
const lastRoute = router.stack[router.stack.length - 1]
|
|
430
|
+
const lastRoute = router.stack[router.stack.length - 1]
|
|
436
431
|
}
|
|
437
432
|
}
|
|
438
|
-
|
|
439
|
-
|
|
440
433
|
} catch (routeError) {
|
|
441
434
|
log.error('[EXPRESS DEBUG] Error creating route:', {
|
|
442
435
|
error: routeError.message,
|
|
443
436
|
stack: routeError.stack,
|
|
444
|
-
path
|
|
437
|
+
path,
|
|
445
438
|
method: method.toLowerCase()
|
|
446
|
-
})
|
|
447
|
-
throw routeError
|
|
439
|
+
})
|
|
440
|
+
throw routeError
|
|
448
441
|
}
|
|
449
|
-
|
|
450
|
-
const logPath = path === vars.transport.matchAll
|
|
442
|
+
|
|
443
|
+
const logPath = path === vars.transport.matchAll
|
|
451
444
|
? '(all paths)' // router.use() with no path matches everything
|
|
452
|
-
: path
|
|
453
|
-
log.trace(`Express route created: ${method} ${logPath}`)
|
|
454
|
-
})
|
|
455
|
-
|
|
445
|
+
: path
|
|
446
|
+
log.trace(`Express route created: ${method} ${logPath}`)
|
|
447
|
+
})
|
|
448
|
+
|
|
456
449
|
// Apply global middleware if configured
|
|
457
|
-
let finalRouter = router
|
|
450
|
+
let finalRouter = router
|
|
458
451
|
if (expressOptions.middleware?.beforeAll) {
|
|
459
|
-
const globalRouter = express.Router()
|
|
460
|
-
globalRouter.use(...expressOptions.middleware.beforeAll)
|
|
461
|
-
globalRouter.use(router)
|
|
462
|
-
finalRouter = globalRouter
|
|
452
|
+
const globalRouter = express.Router()
|
|
453
|
+
globalRouter.use(...expressOptions.middleware.beforeAll)
|
|
454
|
+
globalRouter.use(router)
|
|
455
|
+
finalRouter = globalRouter
|
|
463
456
|
}
|
|
464
|
-
|
|
457
|
+
|
|
465
458
|
// Set up 404 handler in separate router (unless disabled)
|
|
466
459
|
if (expressOptions.handle404 !== false) {
|
|
467
460
|
notFoundRouter.use(async (req, res, next) => {
|
|
468
461
|
if (basePath && req.path.startsWith(basePath)) {
|
|
469
462
|
// Create minimal context for 404
|
|
470
|
-
const context = createContext(req, res, 'express')
|
|
463
|
+
const context = createContext(req, res, 'express')
|
|
471
464
|
const transportData = {
|
|
472
465
|
request: {
|
|
473
466
|
method: req.method,
|
|
@@ -486,54 +479,54 @@ export const ExpressPlugin = {
|
|
|
486
479
|
}]
|
|
487
480
|
}
|
|
488
481
|
}
|
|
489
|
-
}
|
|
490
|
-
|
|
482
|
+
}
|
|
483
|
+
|
|
491
484
|
// Add transport data to context
|
|
492
|
-
context.transport = transportData
|
|
493
|
-
|
|
485
|
+
context.transport = transportData
|
|
486
|
+
|
|
494
487
|
// Run transport:response hook for 404
|
|
495
|
-
await runHooks('transport:response', context)
|
|
496
|
-
|
|
488
|
+
await runHooks('transport:response', context)
|
|
489
|
+
|
|
497
490
|
// Apply response headers from hooks
|
|
498
491
|
if (transportData.response.headers) {
|
|
499
|
-
res.set(transportData.response.headers)
|
|
492
|
+
res.set(transportData.response.headers)
|
|
500
493
|
}
|
|
501
|
-
|
|
502
|
-
res.status(404).json(transportData.response.body)
|
|
494
|
+
|
|
495
|
+
res.status(404).json(transportData.response.body)
|
|
503
496
|
} else {
|
|
504
|
-
next()
|
|
497
|
+
next()
|
|
505
498
|
}
|
|
506
|
-
})
|
|
499
|
+
})
|
|
507
500
|
}
|
|
508
|
-
|
|
501
|
+
|
|
509
502
|
// Store routers in api.http.express namespace
|
|
510
|
-
api.http.express.router = finalRouter
|
|
511
|
-
api.http.express.notFoundRouter = notFoundRouter
|
|
512
|
-
|
|
503
|
+
api.http.express.router = finalRouter
|
|
504
|
+
api.http.express.notFoundRouter = notFoundRouter
|
|
505
|
+
|
|
513
506
|
// Allow other plugins to add middleware before 404 handler
|
|
514
|
-
const beforeNotFoundMiddleware = []
|
|
507
|
+
const beforeNotFoundMiddleware = []
|
|
515
508
|
api.http.express.beforeNotFound = (middleware) => {
|
|
516
|
-
beforeNotFoundMiddleware.push(middleware)
|
|
517
|
-
}
|
|
518
|
-
|
|
509
|
+
beforeNotFoundMiddleware.push(middleware)
|
|
510
|
+
}
|
|
511
|
+
|
|
519
512
|
// Add convenient mounting method
|
|
520
513
|
api.http.express.mount = (app, path = '') => {
|
|
521
514
|
// Mount main router with all routes
|
|
522
|
-
app.use(path, finalRouter)
|
|
523
|
-
|
|
515
|
+
app.use(path, finalRouter)
|
|
516
|
+
|
|
524
517
|
// Mount any middleware that should come before 404
|
|
525
518
|
beforeNotFoundMiddleware.forEach(middleware => {
|
|
526
|
-
app.use(path, middleware)
|
|
527
|
-
})
|
|
528
|
-
|
|
519
|
+
app.use(path, middleware)
|
|
520
|
+
})
|
|
521
|
+
|
|
529
522
|
// Mount 404 handler router after all other routes (if enabled)
|
|
530
523
|
if (expressOptions.handle404 !== false) {
|
|
531
|
-
app.use(path, notFoundRouter)
|
|
524
|
+
app.use(path, notFoundRouter)
|
|
532
525
|
}
|
|
533
|
-
|
|
534
|
-
log.info(`Express routes mounted at ${path || '/'}`)
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
log.info('Express plugin initialized successfully')
|
|
526
|
+
|
|
527
|
+
log.info(`Express routes mounted at ${path || '/'}`)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
log.info('Express plugin initialized successfully')
|
|
538
531
|
}
|
|
539
|
-
}
|
|
532
|
+
}
|