json-rest-api 1.0.14 ā 1.0.16
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 +11 -2
- 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 +1 -1
- 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 +99 -99
- 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 +11 -15
- 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
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* You'll need to install dependencies: npm install fastify
|
|
7
7
|
*/
|
|
8
|
-
import { HookedApiError } from './hooked-api.js'
|
|
9
|
-
import { handleFastifyApiError } from './error-handler-fastify.js'
|
|
8
|
+
import { HookedApiError } from './hooked-api.js'
|
|
9
|
+
import { handleFastifyApiError } from './error-handler-fastify.js'
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* **UPGRADED:** Automatically generates a comprehensive Fastify schema object that conforms
|
|
@@ -15,19 +15,19 @@ import { handleFastifyApiError } from './error-handler-fastify.js';
|
|
|
15
15
|
* @param {object} scopeOptions - The full options object for the scope.
|
|
16
16
|
* @returns {object} A complete Fastify schema for all CRUD routes.
|
|
17
17
|
*/
|
|
18
|
-
function _generateFastifySchema(scopeName, scopeOptions = {}) {
|
|
19
|
-
const appSchema = scopeOptions.schema || {}
|
|
20
|
-
const relationshipsSchema = scopeOptions.relationships || {}
|
|
18
|
+
function _generateFastifySchema (scopeName, scopeOptions = {}) {
|
|
19
|
+
const appSchema = scopeOptions.schema || {}
|
|
20
|
+
const relationshipsSchema = scopeOptions.relationships || {}
|
|
21
21
|
|
|
22
22
|
// --- Reusable Schema Components ---
|
|
23
|
-
const metaObject = { type: 'object', additionalProperties: true }
|
|
23
|
+
const metaObject = { type: 'object', additionalProperties: true }
|
|
24
24
|
const linksObject = {
|
|
25
25
|
type: 'object',
|
|
26
26
|
properties: {
|
|
27
27
|
self: { type: 'string', format: 'uri-reference' },
|
|
28
28
|
related: { type: 'string', format: 'uri-reference' },
|
|
29
29
|
}
|
|
30
|
-
}
|
|
30
|
+
}
|
|
31
31
|
const resourceIdentifierObject = {
|
|
32
32
|
type: 'object',
|
|
33
33
|
required: ['type', 'id'],
|
|
@@ -35,34 +35,34 @@ function _generateFastifySchema(scopeName, scopeOptions = {}) {
|
|
|
35
35
|
type: { type: 'string' },
|
|
36
36
|
id: { type: 'string' }
|
|
37
37
|
}
|
|
38
|
-
}
|
|
38
|
+
}
|
|
39
39
|
|
|
40
40
|
// --- Build Attribute Properties from simple schema ---
|
|
41
|
-
const attributeProperties = {}
|
|
42
|
-
const requiredAttributes = []
|
|
41
|
+
const attributeProperties = {}
|
|
42
|
+
const requiredAttributes = []
|
|
43
43
|
for (const [key, value] of Object.entries(appSchema)) {
|
|
44
|
-
let jsonSchemaType = 'string'
|
|
45
|
-
if (value.type === 'number') jsonSchemaType = 'number'
|
|
46
|
-
if (value.type === 'boolean') jsonSchemaType = 'boolean'
|
|
47
|
-
|
|
48
|
-
attributeProperties[key] = { type: jsonSchemaType }
|
|
49
|
-
if (value.format) attributeProperties[key].format = value.format
|
|
50
|
-
if (value.required) requiredAttributes.push(key)
|
|
44
|
+
let jsonSchemaType = 'string'
|
|
45
|
+
if (value.type === 'number') jsonSchemaType = 'number'
|
|
46
|
+
if (value.type === 'boolean') jsonSchemaType = 'boolean'
|
|
47
|
+
|
|
48
|
+
attributeProperties[key] = { type: jsonSchemaType }
|
|
49
|
+
if (value.format) attributeProperties[key].format = value.format
|
|
50
|
+
if (value.required) requiredAttributes.push(key)
|
|
51
51
|
}
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
// --- Build Relationship Properties ---
|
|
54
|
-
const relationshipProperties = {}
|
|
54
|
+
const relationshipProperties = {}
|
|
55
55
|
for (const [key, value] of Object.entries(relationshipsSchema)) {
|
|
56
|
-
const isToMany = value.type === 'hasMany'
|
|
56
|
+
const isToMany = value.type === 'hasMany'
|
|
57
57
|
relationshipProperties[key] = {
|
|
58
58
|
type: 'object',
|
|
59
59
|
properties: {
|
|
60
60
|
links: linksObject,
|
|
61
|
-
data: isToMany
|
|
62
|
-
? { type: 'array', items: resourceIdentifierObject }
|
|
61
|
+
data: isToMany
|
|
62
|
+
? { type: 'array', items: resourceIdentifierObject }
|
|
63
63
|
: resourceIdentifierObject
|
|
64
64
|
}
|
|
65
|
-
}
|
|
65
|
+
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
// --- Define the core Resource Object schema ---
|
|
@@ -82,7 +82,7 @@ function _generateFastifySchema(scopeName, scopeOptions = {}) {
|
|
|
82
82
|
},
|
|
83
83
|
links: linksObject
|
|
84
84
|
}
|
|
85
|
-
}
|
|
85
|
+
}
|
|
86
86
|
|
|
87
87
|
// --- Define the full top-level JSON:API Document ---
|
|
88
88
|
const singleDocument = {
|
|
@@ -93,7 +93,7 @@ function _generateFastifySchema(scopeName, scopeOptions = {}) {
|
|
|
93
93
|
links: linksObject,
|
|
94
94
|
meta: metaObject
|
|
95
95
|
}
|
|
96
|
-
}
|
|
96
|
+
}
|
|
97
97
|
const collectionDocument = {
|
|
98
98
|
type: 'object',
|
|
99
99
|
properties: {
|
|
@@ -102,8 +102,7 @@ function _generateFastifySchema(scopeName, scopeOptions = {}) {
|
|
|
102
102
|
links: { ...linksObject, properties: { ...linksObject.properties, next: { type: 'string' }, prev: { type: 'string' } } },
|
|
103
103
|
meta: metaObject
|
|
104
104
|
}
|
|
105
|
-
}
|
|
106
|
-
|
|
105
|
+
}
|
|
107
106
|
|
|
108
107
|
// --- Build the final schema for each route ---
|
|
109
108
|
return {
|
|
@@ -143,77 +142,76 @@ function _generateFastifySchema(scopeName, scopeOptions = {}) {
|
|
|
143
142
|
response: { 200: singleDocument }
|
|
144
143
|
},
|
|
145
144
|
put: {
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
body: { /* Similar to POST body */ },
|
|
146
|
+
response: { 200: singleDocument }
|
|
148
147
|
}
|
|
149
|
-
}
|
|
148
|
+
}
|
|
150
149
|
}
|
|
151
150
|
|
|
152
|
-
|
|
153
151
|
export const HttpFastifyPlugin = {
|
|
154
152
|
name: 'http-fastify',
|
|
155
153
|
|
|
156
|
-
install(context, api) {
|
|
157
|
-
const { pluginOptions, log } = context
|
|
158
|
-
const httpOptions = pluginOptions.http || {}
|
|
159
|
-
const { app, prefix = '/api' } = httpOptions
|
|
154
|
+
install (context, api) {
|
|
155
|
+
const { pluginOptions, log } = context
|
|
156
|
+
const httpOptions = pluginOptions.http || {}
|
|
157
|
+
const { app, prefix = '/api' } = httpOptions
|
|
160
158
|
|
|
161
159
|
if (!app) {
|
|
162
160
|
throw new HookedApiError(
|
|
163
161
|
"The 'http-fastify' plugin requires a Fastify 'app' instance to be passed in the options. Example: api.use(HttpFastifyPlugin, { http: { app: myFastifyApp } })",
|
|
164
162
|
'CONFIGURATION_ERROR'
|
|
165
|
-
)
|
|
163
|
+
)
|
|
166
164
|
}
|
|
167
165
|
|
|
168
|
-
log.info(`Fastify HTTP plugin installed. Routes will be prefixed with '${prefix}'.`)
|
|
166
|
+
log.info(`Fastify HTTP plugin installed. Routes will be prefixed with '${prefix}'.`)
|
|
169
167
|
|
|
170
168
|
const _createFastifyHandler = (methodName) => {
|
|
171
169
|
return async (request, reply) => {
|
|
172
|
-
const { scopeName, id } = request.params
|
|
170
|
+
const { scopeName, id } = request.params
|
|
173
171
|
const apiParams = {
|
|
174
172
|
id,
|
|
175
173
|
inputRecord: request.body,
|
|
176
174
|
queryParams: request.query,
|
|
177
|
-
}
|
|
178
|
-
log.debug(`Fastify request for ${scopeName}.${methodName}`, { id, query: apiParams.queryParams })
|
|
179
|
-
const result = await api.scopes[scopeName][methodName](apiParams)
|
|
175
|
+
}
|
|
176
|
+
log.debug(`Fastify request for ${scopeName}.${methodName}`, { id, query: apiParams.queryParams })
|
|
177
|
+
const result = await api.scopes[scopeName][methodName](apiParams)
|
|
180
178
|
switch (methodName) {
|
|
181
179
|
case 'post':
|
|
182
|
-
reply.code(201).send(result)
|
|
183
|
-
break
|
|
180
|
+
reply.code(201).send(result)
|
|
181
|
+
break
|
|
184
182
|
case 'delete':
|
|
185
|
-
reply.code(204).send()
|
|
186
|
-
break
|
|
183
|
+
reply.code(204).send()
|
|
184
|
+
break
|
|
187
185
|
default:
|
|
188
|
-
reply.code(200).send(result)
|
|
186
|
+
reply.code(200).send(result)
|
|
189
187
|
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
192
190
|
|
|
193
191
|
const _createScopePlugin = (scopeName, scopeOptions) => {
|
|
194
|
-
const fastifySchema = scopeOptions.fastifySchema || _generateFastifySchema(scopeName, scopeOptions)
|
|
192
|
+
const fastifySchema = scopeOptions.fastifySchema || _generateFastifySchema(scopeName, scopeOptions)
|
|
195
193
|
|
|
196
194
|
return async (fastifyInstance) => {
|
|
197
|
-
log.info(`Creating Fastify HTTP routes for scope: '${scopeName}'`)
|
|
198
|
-
fastifyInstance.get(`/${scopeName}`, { schema: fastifySchema.query }, _createFastifyHandler('query'))
|
|
199
|
-
fastifyInstance.get(`/${scopeName}/:id`, { schema: fastifySchema.get }, _createFastifyHandler('get'))
|
|
200
|
-
fastifyInstance.post(`/${scopeName}`, { schema: fastifySchema.post }, _createFastifyHandler('post'))
|
|
201
|
-
fastifyInstance.put(`/${scopeName}/:id`, { schema: fastifySchema.put }, _createFastifyHandler('put'))
|
|
202
|
-
fastifyInstance.patch(`/${scopeName}/:id`, { schema: fastifySchema.patch }, _createFastifyHandler('patch'))
|
|
203
|
-
fastifyInstance.delete(`/${scopeName}/:id`, { schema: fastifySchema.delete }, _createFastifyHandler('delete'))
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const originalAddScope = api.addScope
|
|
195
|
+
log.info(`Creating Fastify HTTP routes for scope: '${scopeName}'`)
|
|
196
|
+
fastifyInstance.get(`/${scopeName}`, { schema: fastifySchema.query }, _createFastifyHandler('query'))
|
|
197
|
+
fastifyInstance.get(`/${scopeName}/:id`, { schema: fastifySchema.get }, _createFastifyHandler('get'))
|
|
198
|
+
fastifyInstance.post(`/${scopeName}`, { schema: fastifySchema.post }, _createFastifyHandler('post'))
|
|
199
|
+
fastifyInstance.put(`/${scopeName}/:id`, { schema: fastifySchema.put }, _createFastifyHandler('put'))
|
|
200
|
+
fastifyInstance.patch(`/${scopeName}/:id`, { schema: fastifySchema.patch }, _createFastifyHandler('patch'))
|
|
201
|
+
fastifyInstance.delete(`/${scopeName}/:id`, { schema: fastifySchema.delete }, _createFastifyHandler('delete'))
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const originalAddScope = api.addScope
|
|
208
206
|
|
|
209
207
|
api.addScope = (name, options, extras) => {
|
|
210
|
-
const result = originalAddScope.call(api, name, options, extras)
|
|
211
|
-
app.register(_createScopePlugin(name, options), { prefix })
|
|
212
|
-
return result
|
|
213
|
-
}
|
|
214
|
-
|
|
208
|
+
const result = originalAddScope.call(api, name, options, extras)
|
|
209
|
+
app.register(_createScopePlugin(name, options), { prefix })
|
|
210
|
+
return result
|
|
211
|
+
}
|
|
212
|
+
|
|
215
213
|
app.setErrorHandler((error, request, reply) => {
|
|
216
|
-
|
|
217
|
-
})
|
|
214
|
+
handleFastifyApiError(error, request, reply, log)
|
|
215
|
+
})
|
|
218
216
|
}
|
|
219
|
-
}
|
|
217
|
+
}
|
|
@@ -6,28 +6,28 @@
|
|
|
6
6
|
* It assumes you have `koa-bodyparser` installed and used before this plugin.
|
|
7
7
|
* You'll need to install dependencies: npm install koa koa-router koa-bodyparser qs
|
|
8
8
|
*/
|
|
9
|
-
import KoaRouter from 'koa-router'
|
|
10
|
-
import qs from 'qs'
|
|
11
|
-
import { HookedApiError } from './hooked-api.js'
|
|
12
|
-
import { handleKoaApiError } from './error-handler-koa.js'
|
|
9
|
+
import KoaRouter from 'koa-router'
|
|
10
|
+
import qs from 'qs' // The same query string parser Express uses
|
|
11
|
+
import { HookedApiError } from './hooked-api.js'
|
|
12
|
+
import { handleKoaApiError } from './error-handler-koa.js'
|
|
13
13
|
|
|
14
14
|
export const HttpKoaPlugin = {
|
|
15
15
|
name: 'http-koa',
|
|
16
16
|
|
|
17
|
-
install(context, api) {
|
|
18
|
-
const { pluginOptions, log } = context
|
|
19
|
-
const httpOptions = pluginOptions.http || {}
|
|
20
|
-
const { app, prefix = '/api' } = httpOptions
|
|
17
|
+
install (context, api) {
|
|
18
|
+
const { pluginOptions, log } = context
|
|
19
|
+
const httpOptions = pluginOptions.http || {}
|
|
20
|
+
const { app, prefix = '/api' } = httpOptions
|
|
21
21
|
|
|
22
22
|
if (!app) {
|
|
23
23
|
throw new HookedApiError(
|
|
24
24
|
"The 'http-koa' plugin requires a Koa 'app' instance to be passed in the options. Example: api.use(HttpKoaPlugin, { http: { app: myKoaApp } })",
|
|
25
25
|
'CONFIGURATION_ERROR'
|
|
26
|
-
)
|
|
26
|
+
)
|
|
27
27
|
}
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
// Create a router instance for our API routes
|
|
30
|
-
const router = new KoaRouter({ prefix })
|
|
30
|
+
const router = new KoaRouter({ prefix })
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
33
|
* Creates a generic Koa middleware for a specific API method.
|
|
@@ -36,78 +36,77 @@ export const HttpKoaPlugin = {
|
|
|
36
36
|
*/
|
|
37
37
|
const _createKoaHandler = (methodName) => {
|
|
38
38
|
return async (ctx) => {
|
|
39
|
-
const { scopeName, id } = ctx.params
|
|
40
|
-
|
|
39
|
+
const { scopeName, id } = ctx.params
|
|
40
|
+
|
|
41
41
|
try {
|
|
42
42
|
// 1. Translate Koa ctx into API method params
|
|
43
|
-
const apiParams = {}
|
|
44
|
-
|
|
43
|
+
const apiParams = {}
|
|
44
|
+
|
|
45
45
|
if (['get', 'put', 'patch', 'delete'].includes(methodName)) {
|
|
46
|
-
apiParams.id = id
|
|
46
|
+
apiParams.id = id
|
|
47
47
|
}
|
|
48
48
|
if (['post', 'put', 'patch'].includes(methodName)) {
|
|
49
49
|
// Assumes koa-bodyparser is used
|
|
50
|
-
apiParams.inputRecord = ctx.request.body
|
|
50
|
+
apiParams.inputRecord = ctx.request.body
|
|
51
51
|
}
|
|
52
52
|
// Use qs to properly parse nested query objects
|
|
53
|
-
apiParams.queryParams = ctx.querystring ? qs.parse(ctx.querystring) : {}
|
|
53
|
+
apiParams.queryParams = ctx.querystring ? qs.parse(ctx.querystring) : {}
|
|
54
54
|
|
|
55
|
-
log.debug(`Koa request for ${scopeName}.${methodName}`, { id, query: apiParams.queryParams })
|
|
55
|
+
log.debug(`Koa request for ${scopeName}.${methodName}`, { id, query: apiParams.queryParams })
|
|
56
56
|
|
|
57
57
|
// 2. Execute the corresponding API method
|
|
58
|
-
const result = await api.scopes[scopeName][methodName](apiParams)
|
|
59
|
-
|
|
58
|
+
const result = await api.scopes[scopeName][methodName](apiParams)
|
|
59
|
+
|
|
60
60
|
// 3. Set the successful Koa response
|
|
61
61
|
switch (methodName) {
|
|
62
62
|
case 'post':
|
|
63
|
-
ctx.status = 201
|
|
64
|
-
ctx.body = result
|
|
65
|
-
break
|
|
63
|
+
ctx.status = 201
|
|
64
|
+
ctx.body = result
|
|
65
|
+
break
|
|
66
66
|
case 'delete':
|
|
67
|
-
ctx.status = 204
|
|
67
|
+
ctx.status = 204
|
|
68
68
|
// No body for 204
|
|
69
|
-
break
|
|
69
|
+
break
|
|
70
70
|
default:
|
|
71
|
-
ctx.status = 200
|
|
72
|
-
ctx.body = result
|
|
71
|
+
ctx.status = 200
|
|
72
|
+
ctx.body = result
|
|
73
73
|
}
|
|
74
|
-
|
|
75
74
|
} catch (error) {
|
|
76
75
|
// 4. Handle any errors thrown by the API
|
|
77
|
-
handleKoaApiError(error, ctx, log)
|
|
76
|
+
handleKoaApiError(error, ctx, log)
|
|
78
77
|
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
81
80
|
|
|
82
81
|
/**
|
|
83
82
|
* Creates all the standard RESTful routes for a given scope on the router.
|
|
84
83
|
* @param {string} scopeName - The name of the scope to create routes for.
|
|
85
84
|
*/
|
|
86
85
|
const _createRoutesForScope = (scopeName) => {
|
|
87
|
-
const collectionPath = `/${scopeName}
|
|
88
|
-
const resourcePath = `${collectionPath}/:id
|
|
86
|
+
const collectionPath = `/${scopeName}`
|
|
87
|
+
const resourcePath = `${collectionPath}/:id`
|
|
89
88
|
|
|
90
|
-
log.info(`Creating Koa HTTP routes for scope: '${scopeName}'`)
|
|
89
|
+
log.info(`Creating Koa HTTP routes for scope: '${scopeName}'`)
|
|
91
90
|
|
|
92
|
-
router.get(collectionPath, _createKoaHandler('query'))
|
|
93
|
-
router.get(resourcePath, _createKoaHandler('get'))
|
|
94
|
-
router.post(collectionPath, _createKoaHandler('post'))
|
|
95
|
-
router.put(resourcePath, _createKoaHandler('put'))
|
|
96
|
-
router.patch(resourcePath, _createKoaHandler('patch'))
|
|
97
|
-
router.del(resourcePath, _createKoaHandler('delete'))
|
|
98
|
-
}
|
|
91
|
+
router.get(collectionPath, _createKoaHandler('query'))
|
|
92
|
+
router.get(resourcePath, _createKoaHandler('get'))
|
|
93
|
+
router.post(collectionPath, _createKoaHandler('post'))
|
|
94
|
+
router.put(resourcePath, _createKoaHandler('put'))
|
|
95
|
+
router.patch(resourcePath, _createKoaHandler('patch'))
|
|
96
|
+
router.del(resourcePath, _createKoaHandler('delete')) // .del is koa-router's alias for delete
|
|
97
|
+
}
|
|
99
98
|
|
|
100
99
|
// --- Main Logic: Wrap `addScope` to automate route creation ---
|
|
101
|
-
const originalAddScope = api.addScope
|
|
100
|
+
const originalAddScope = api.addScope
|
|
102
101
|
|
|
103
102
|
api.addScope = (name, options, extras) => {
|
|
104
|
-
const result = originalAddScope.call(api, name, options, extras)
|
|
105
|
-
_createRoutesForScope(name)
|
|
106
|
-
return result
|
|
107
|
-
}
|
|
108
|
-
|
|
103
|
+
const result = originalAddScope.call(api, name, options, extras)
|
|
104
|
+
_createRoutesForScope(name)
|
|
105
|
+
return result
|
|
106
|
+
}
|
|
107
|
+
|
|
109
108
|
// Apply the router middleware to the Koa app
|
|
110
|
-
app.use(router.routes()).use(router.allowedMethods())
|
|
111
|
-
log.info(`Koa router installed with prefix '${prefix}'.`)
|
|
109
|
+
app.use(router.routes()).use(router.allowedMethods())
|
|
110
|
+
log.info(`Koa router installed with prefix '${prefix}'.`)
|
|
112
111
|
}
|
|
113
|
-
}
|
|
112
|
+
}
|
package/eslint.config.js
CHANGED
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import neostandard from 'neostandard'
|
|
2
2
|
|
|
3
|
-
export default
|
|
4
|
-
|
|
3
|
+
export default [
|
|
4
|
+
...neostandard({
|
|
5
|
+
ignores: ['node_modules/**', 'docs/**', 'tests/**', 'examples/**']
|
|
6
|
+
}),
|
|
7
|
+
{
|
|
8
|
+
files: ['index.js'],
|
|
9
|
+
rules: {
|
|
10
|
+
'import-x/export': 'off'
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
]
|
|
@@ -2,75 +2,75 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Example: Custom URL Override Using Hooks
|
|
5
|
-
*
|
|
5
|
+
*
|
|
6
6
|
* This example demonstrates how to override URL generation using the
|
|
7
7
|
* hooked-api hooks system. This is the recommended approach for custom
|
|
8
8
|
* URL scenarios like API gateways, CDNs, or multi-tenant applications.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { Api } from 'hooked-api'
|
|
12
|
-
import { RestApiPlugin, RestApiKnexPlugin, ExpressPlugin } from 'json-rest-api'
|
|
13
|
-
import knexLib from 'knex'
|
|
14
|
-
import express from 'express'
|
|
11
|
+
import { Api } from 'hooked-api'
|
|
12
|
+
import { RestApiPlugin, RestApiKnexPlugin, ExpressPlugin } from 'json-rest-api'
|
|
13
|
+
import knexLib from 'knex'
|
|
14
|
+
import express from 'express'
|
|
15
15
|
|
|
16
16
|
// Create in-memory database
|
|
17
17
|
const knex = knexLib({
|
|
18
18
|
client: 'better-sqlite3',
|
|
19
19
|
connection: { filename: ':memory:' },
|
|
20
20
|
useNullAsDefault: true
|
|
21
|
-
})
|
|
21
|
+
})
|
|
22
22
|
|
|
23
23
|
// Create API with URL override hook
|
|
24
|
-
const api = new Api({
|
|
24
|
+
const api = new Api({
|
|
25
25
|
name: 'url-override-demo',
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
// Define hooks at API creation time
|
|
28
28
|
hooks: {
|
|
29
29
|
// This hook runs at the start of each request
|
|
30
30
|
'transport:request': [
|
|
31
31
|
async (payload) => {
|
|
32
|
-
const { context, req } = payload
|
|
33
|
-
|
|
32
|
+
const { context, req } = payload
|
|
33
|
+
|
|
34
34
|
// Example 1: Override based on custom header
|
|
35
35
|
// Useful for CDN or API gateway scenarios
|
|
36
36
|
if (req?.headers?.['x-public-url']) {
|
|
37
|
-
context.urlPrefixOverride = req.headers['x-public-url']
|
|
38
|
-
console.log(`ā URL overridden via X-Public-URL header: ${context.urlPrefixOverride}`)
|
|
37
|
+
context.urlPrefixOverride = req.headers['x-public-url']
|
|
38
|
+
console.log(`ā URL overridden via X-Public-URL header: ${context.urlPrefixOverride}`)
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
|
|
41
41
|
// Example 2: API versioning via header
|
|
42
42
|
// Route different API versions while maintaining same backend
|
|
43
43
|
else if (req?.headers?.['x-api-version'] === 'v2') {
|
|
44
|
-
context.urlPrefixOverride = 'https://api.example.com/v2'
|
|
45
|
-
console.log(`ā URL overridden for API v2: ${context.urlPrefixOverride}`)
|
|
44
|
+
context.urlPrefixOverride = 'https://api.example.com/v2'
|
|
45
|
+
console.log(`ā URL overridden for API v2: ${context.urlPrefixOverride}`)
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
// Example 3: Multi-tenant based on host
|
|
49
49
|
// Each tenant gets their own domain in responses
|
|
50
50
|
else if (req?.hostname?.includes('tenant-a')) {
|
|
51
|
-
context.urlPrefixOverride = `https://tenant-a.api.com${req.baseUrl || ''}
|
|
52
|
-
console.log(`ā URL overridden for tenant-a: ${context.urlPrefixOverride}`)
|
|
51
|
+
context.urlPrefixOverride = `https://tenant-a.api.com${req.baseUrl || ''}`
|
|
52
|
+
console.log(`ā URL overridden for tenant-a: ${context.urlPrefixOverride}`)
|
|
53
53
|
}
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
// Example 4: Environment-based override
|
|
56
56
|
// Force production URLs in staging environment
|
|
57
57
|
else if (process.env.FORCE_PRODUCTION_URLS === 'true') {
|
|
58
|
-
context.urlPrefixOverride = 'https://api.production.com/api'
|
|
59
|
-
console.log(`ā URL overridden via environment: ${context.urlPrefixOverride}`)
|
|
58
|
+
context.urlPrefixOverride = 'https://api.production.com/api'
|
|
59
|
+
console.log(`ā URL overridden via environment: ${context.urlPrefixOverride}`)
|
|
60
60
|
}
|
|
61
|
-
|
|
62
|
-
return payload
|
|
61
|
+
|
|
62
|
+
return payload
|
|
63
63
|
}
|
|
64
64
|
]
|
|
65
65
|
}
|
|
66
|
-
})
|
|
66
|
+
})
|
|
67
67
|
|
|
68
68
|
// Install plugins
|
|
69
69
|
await api.use(RestApiPlugin, {
|
|
70
70
|
returnRecordTransport: { post: 'full' } // Return full JSON:API response
|
|
71
|
-
})
|
|
72
|
-
await api.use(RestApiKnexPlugin, { knex })
|
|
73
|
-
await api.use(ExpressPlugin, { mountPath: '/api' })
|
|
71
|
+
})
|
|
72
|
+
await api.use(RestApiKnexPlugin, { knex })
|
|
73
|
+
await api.use(ExpressPlugin, { mountPath: '/api' })
|
|
74
74
|
|
|
75
75
|
// Define a simple resource
|
|
76
76
|
await api.addResource('items', {
|
|
@@ -78,11 +78,11 @@ await api.addResource('items', {
|
|
|
78
78
|
name: { type: 'string', required: true, max: 100 },
|
|
79
79
|
description: { type: 'string', max: 500 }
|
|
80
80
|
}
|
|
81
|
-
})
|
|
82
|
-
await api.resources.items.createKnexTable()
|
|
81
|
+
})
|
|
82
|
+
await api.resources.items.createKnexTable()
|
|
83
83
|
|
|
84
84
|
// Create some test data
|
|
85
|
-
console.log('\nš Creating test data...')
|
|
85
|
+
console.log('\nš Creating test data...')
|
|
86
86
|
await api.resources.items.post({
|
|
87
87
|
inputRecord: {
|
|
88
88
|
data: {
|
|
@@ -94,44 +94,44 @@ await api.resources.items.post({
|
|
|
94
94
|
}
|
|
95
95
|
},
|
|
96
96
|
simplified: false
|
|
97
|
-
})
|
|
97
|
+
})
|
|
98
98
|
|
|
99
99
|
// Create Express app
|
|
100
|
-
const app = express()
|
|
101
|
-
app.use(api.http.express.router)
|
|
100
|
+
const app = express()
|
|
101
|
+
app.use(api.http.express.router)
|
|
102
102
|
|
|
103
103
|
// Start server
|
|
104
|
-
const PORT = process.env.PORT || 3333
|
|
104
|
+
const PORT = process.env.PORT || 3333
|
|
105
105
|
const server = app.listen(PORT, () => {
|
|
106
|
-
console.log(`\nš Server running on http://localhost:${PORT}`)
|
|
107
|
-
console.log('\nš Try these examples:\n')
|
|
108
|
-
|
|
109
|
-
console.log('1. Default URL (auto-detected):')
|
|
110
|
-
console.log(` curl http://localhost:${PORT}/api/items`)
|
|
111
|
-
console.log(' ā URLs will be: http://localhost:${PORT}/api/items/1\n')
|
|
112
|
-
|
|
113
|
-
console.log('2. Custom CDN URL:')
|
|
114
|
-
console.log(` curl -H "X-Public-URL: https://cdn.example.com/api" http://localhost:${PORT}/api/items`)
|
|
115
|
-
console.log(' ā URLs will be: https://cdn.example.com/api/items/1\n')
|
|
116
|
-
|
|
117
|
-
console.log('3. API Version Header:')
|
|
118
|
-
console.log(` curl -H "X-API-Version: v2" http://localhost:${PORT}/api/items`)
|
|
119
|
-
console.log(' ā URLs will be: https://api.example.com/v2/items/1\n')
|
|
120
|
-
|
|
121
|
-
console.log('4. Create with custom URL:')
|
|
122
|
-
console.log(
|
|
123
|
-
console.log(
|
|
124
|
-
console.log(
|
|
125
|
-
console.log(` http://localhost:${PORT}/api/items`)
|
|
126
|
-
console.log(' ā Response URLs will use: https://public.api.com/api/items/2\n')
|
|
127
|
-
|
|
128
|
-
console.log('Press Ctrl+C to stop the server\n')
|
|
129
|
-
})
|
|
106
|
+
console.log(`\nš Server running on http://localhost:${PORT}`)
|
|
107
|
+
console.log('\nš Try these examples:\n')
|
|
108
|
+
|
|
109
|
+
console.log('1. Default URL (auto-detected):')
|
|
110
|
+
console.log(` curl http://localhost:${PORT}/api/items`)
|
|
111
|
+
console.log(' ā URLs will be: http://localhost:${PORT}/api/items/1\n')
|
|
112
|
+
|
|
113
|
+
console.log('2. Custom CDN URL:')
|
|
114
|
+
console.log(` curl -H "X-Public-URL: https://cdn.example.com/api" http://localhost:${PORT}/api/items`)
|
|
115
|
+
console.log(' ā URLs will be: https://cdn.example.com/api/items/1\n')
|
|
116
|
+
|
|
117
|
+
console.log('3. API Version Header:')
|
|
118
|
+
console.log(` curl -H "X-API-Version: v2" http://localhost:${PORT}/api/items`)
|
|
119
|
+
console.log(' ā URLs will be: https://api.example.com/v2/items/1\n')
|
|
120
|
+
|
|
121
|
+
console.log('4. Create with custom URL:')
|
|
122
|
+
console.log(' curl -X POST -H "Content-Type: application/vnd.api+json" \\')
|
|
123
|
+
console.log(' -H "X-Public-URL: https://public.api.com/api" \\')
|
|
124
|
+
console.log(' -d \'{"data":{"type":"items","attributes":{"name":"New Item"}}}\' \\')
|
|
125
|
+
console.log(` http://localhost:${PORT}/api/items`)
|
|
126
|
+
console.log(' ā Response URLs will use: https://public.api.com/api/items/2\n')
|
|
127
|
+
|
|
128
|
+
console.log('Press Ctrl+C to stop the server\n')
|
|
129
|
+
})
|
|
130
130
|
|
|
131
131
|
// Graceful shutdown
|
|
132
132
|
process.on('SIGINT', async () => {
|
|
133
|
-
console.log('\n\nš Shutting down gracefully...')
|
|
134
|
-
server.close()
|
|
135
|
-
await knex.destroy()
|
|
136
|
-
process.exit(0)
|
|
137
|
-
})
|
|
133
|
+
console.log('\n\nš Shutting down gracefully...')
|
|
134
|
+
server.close()
|
|
135
|
+
await knex.destroy()
|
|
136
|
+
process.exit(0)
|
|
137
|
+
})
|
package/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* JSON REST API - REST API plugin for hooked-api with JSON:API compliance
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* This package provides:
|
|
5
5
|
* - REST API plugin that adds query, get, post, put, patch, delete methods to scopes
|
|
6
6
|
* - Connector plugins for various HTTP frameworks (Express, etc.)
|
|
@@ -9,33 +9,33 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
// Core plugins
|
|
12
|
-
export { RestApiPlugin } from './plugins/core/rest-api-plugin.js'
|
|
13
|
-
export { FileHandlingPlugin } from './plugins/core/file-handling-plugin.js'
|
|
14
|
-
export { CorsPlugin } from './plugins/core/rest-api-cors-plugin.js'
|
|
15
|
-
export { AccessPlugin } from './plugins/core/rest-api-access.js'
|
|
12
|
+
export { RestApiPlugin } from './plugins/core/rest-api-plugin.js'
|
|
13
|
+
export { FileHandlingPlugin } from './plugins/core/file-handling-plugin.js'
|
|
14
|
+
export { CorsPlugin } from './plugins/core/rest-api-cors-plugin.js'
|
|
15
|
+
export { AccessPlugin } from './plugins/core/rest-api-access.js'
|
|
16
16
|
|
|
17
17
|
// Database plugins
|
|
18
|
-
export { RestApiKnexPlugin } from './plugins/core/rest-api-knex-plugin.js'
|
|
19
|
-
export { RestApiAnyapiKnexPlugin } from './plugins/core/rest-api-anyapi-knex-plugin.js'
|
|
18
|
+
export { RestApiKnexPlugin } from './plugins/core/rest-api-knex-plugin.js'
|
|
19
|
+
export { RestApiAnyapiKnexPlugin } from './plugins/core/rest-api-anyapi-knex-plugin.js'
|
|
20
20
|
|
|
21
21
|
// Connector plugins
|
|
22
|
-
export { ExpressPlugin } from './plugins/core/connectors/express-plugin.js'
|
|
22
|
+
export { ExpressPlugin } from './plugins/core/connectors/express-plugin.js'
|
|
23
23
|
// Future: export { FastifyPlugin } from './plugins/core/connectors/fastify-plugin.js';
|
|
24
24
|
// Future: export { KoaPlugin } from './plugins/core/connectors/koa-plugin.js';
|
|
25
25
|
|
|
26
26
|
// Storage plugins for file handling
|
|
27
|
-
export { LocalStorage } from './plugins/storage/local-storage.js'
|
|
28
|
-
export { S3Storage } from './plugins/storage/s3-storage.js'
|
|
27
|
+
export { LocalStorage } from './plugins/storage/local-storage.js'
|
|
28
|
+
export { S3Storage } from './plugins/storage/s3-storage.js'
|
|
29
29
|
|
|
30
30
|
// Error classes for consumers who need them
|
|
31
31
|
export {
|
|
32
32
|
RestApiValidationError,
|
|
33
33
|
RestApiResourceError,
|
|
34
34
|
RestApiPayloadError
|
|
35
|
-
} from './lib/rest-api-errors.js'
|
|
35
|
+
} from './lib/rest-api-errors.js'
|
|
36
36
|
|
|
37
37
|
// Re-export everything for backward compatibility
|
|
38
|
-
export * from './lib/rest-api-errors.js'
|
|
38
|
+
export * from './lib/rest-api-errors.js'
|
|
39
39
|
|
|
40
40
|
// URL helper function for generating API URL prefix
|
|
41
|
-
export { getUrlPrefix } from './plugins/core/lib/querying/url-helpers.js'
|
|
41
|
+
export { getUrlPrefix } from './plugins/core/lib/querying/url-helpers.js'
|