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.
Files changed (88) hide show
  1. package/CLAUDE.md +1 -2
  2. package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
  3. package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
  4. package/docs/API.md +51 -14
  5. package/docs/COMPARISON.md +2 -2
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +28 -1
  7. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
  8. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
  9. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
  10. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
  11. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
  12. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
  13. package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
  14. package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
  15. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +250 -0
  16. package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
  17. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
  18. package/docs/GUIDE/index.md +18 -9
  19. package/docs/QUICKSTART.md +5 -1
  20. package/docs/index.md +1 -1
  21. package/index.js +3 -2
  22. package/package.json +4 -4
  23. package/plugins/core/connectors/express-plugin.js +97 -184
  24. package/plugins/core/connectors/fastify-plugin.js +206 -0
  25. package/plugins/core/connectors/lib/connector-core.js +195 -0
  26. package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
  27. package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
  28. package/plugins/core/lib/dbIntrospection.js +1078 -0
  29. package/plugins/core/lib/dbTablesOperations.js +1104 -160
  30. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -2
  31. package/plugins/core/lib/querying/knex-process-includes.js +3 -4
  32. package/plugins/core/lib/querying/knex-query-helpers.js +17 -71
  33. package/plugins/core/lib/querying/knex-relationship-includes.js +466 -206
  34. package/plugins/core/lib/querying/query-builder-utils.js +19 -0
  35. package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
  36. package/plugins/core/lib/querying/sort-helpers.js +33 -0
  37. package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
  38. package/plugins/core/lib/querying/url-helpers.js +8 -2
  39. package/plugins/core/lib/querying-writing/compile-schemas.js +12 -1
  40. package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -13
  41. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
  42. package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
  43. package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
  44. package/plugins/core/lib/storage/canonical-storage-mapping.js +113 -0
  45. package/plugins/core/lib/storage/storage-adapter.js +79 -2
  46. package/plugins/core/lib/storage/storage-mapping.js +204 -0
  47. package/plugins/core/rest-api-anyapi-knex-plugin.js +355 -228
  48. package/plugins/core/rest-api-autofilter-plugin.js +414 -0
  49. package/plugins/core/rest-api-knex-plugin.js +249 -211
  50. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
  51. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
  52. package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
  53. package/plugins/core/rest-api-plugin-methods/common.js +121 -83
  54. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
  55. package/plugins/core/rest-api-plugin-methods/get.js +23 -14
  56. package/plugins/core/rest-api-plugin-methods/patch.js +28 -12
  57. package/plugins/core/rest-api-plugin-methods/post.js +13 -8
  58. package/plugins/core/rest-api-plugin-methods/put.js +45 -21
  59. package/plugins/core/rest-api-plugin-methods/query.js +15 -53
  60. package/plugins/core/rest-api-plugin.js +2 -0
  61. package/plugins/core/rest-api-positioning-plugin.js +1 -1
  62. package/plugins/core/rest-api-query-projections-plugin.js +130 -0
  63. package/plugins/core/socketio-plugin.js +1 -3
  64. package/tests/autofilter.test.js +463 -0
  65. package/tests/custom-id-simplified-post.test.js +75 -0
  66. package/tests/custom-idproperty-relationships.test.js +33 -0
  67. package/tests/db-introspection.test.js +518 -0
  68. package/tests/db-table-operations.test.js +781 -0
  69. package/tests/fastify-plugin.test.js +141 -0
  70. package/tests/field-getters.test.js +1 -2
  71. package/tests/field-setters.test.js +0 -4
  72. package/tests/fixtures/api-configs.js +188 -219
  73. package/tests/helpers/fake-fastify.js +99 -0
  74. package/tests/http-connectors-parity.test.js +235 -0
  75. package/tests/projected-fields.test.js +319 -0
  76. package/tests/relationships.test.js +3 -3
  77. package/tests/searchschema-knex-behavior.test.js +187 -0
  78. package/tests/searchschema-merge.test.js +21 -69
  79. package/tests/storage-mapping.test.js +210 -0
  80. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
  81. package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
  82. package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
  83. package/plugins/core/rest-api-access.js +0 -459
  84. package/plugins/core/rest-api-multihome-plugin.js +0 -314
  85. package/tests/multihome.test.js +0 -553
  86. package/tests/multihome.test.js.bak +0 -550
  87. package/tests/ownership-optout.test.js +0 -76
  88. package/tests/rest-api-access.test.js +0 -206
@@ -0,0 +1,195 @@
1
+ import { getUrlPrefix } from '../../lib/querying/url-helpers.js'
2
+ import {
3
+ determineResponseStatus,
4
+ mapRestApiErrorToHttp
5
+ } from './transport-http-helpers.js'
6
+
7
+ export function extractQueryString (url = '') {
8
+ return String(url || '').split('?')[1] || ''
9
+ }
10
+
11
+ export function buildTransportRequestData ({
12
+ method,
13
+ url,
14
+ path,
15
+ headers,
16
+ body,
17
+ params,
18
+ query
19
+ }) {
20
+ return {
21
+ method,
22
+ url,
23
+ path,
24
+ headers,
25
+ body,
26
+ params,
27
+ query
28
+ }
29
+ }
30
+
31
+ export function buildTransportData (requestData) {
32
+ return {
33
+ request: requestData,
34
+ response: {
35
+ headers: {},
36
+ status: null
37
+ }
38
+ }
39
+ }
40
+
41
+ export function createConnectorContext ({
42
+ request,
43
+ reply,
44
+ source,
45
+ mountPath = '',
46
+ requestData,
47
+ createContext,
48
+ urlPrefixOverride
49
+ }) {
50
+ const context = createContext(request, reply, source)
51
+
52
+ if (urlPrefixOverride) {
53
+ context.urlPrefixOverride = urlPrefixOverride
54
+ }
55
+
56
+ context.urlPrefix = getUrlPrefix(
57
+ context,
58
+ { vars: { transport: { mountPath } } },
59
+ request
60
+ )
61
+
62
+ const transportData = buildTransportData(requestData)
63
+ context.transport = transportData
64
+
65
+ return { context, transportData }
66
+ }
67
+
68
+ export async function runTransportRequestLifecycle ({ context, runHooks }) {
69
+ await runHooks('transport:request', context)
70
+
71
+ return {
72
+ rejected: !!context.rejection,
73
+ handled: !!context.handled
74
+ }
75
+ }
76
+
77
+ export function buildTransportRejectionBody (context) {
78
+ return {
79
+ errors: [{
80
+ status: String(context.rejection?.status || 500),
81
+ title: context.rejection?.title || 'Request Rejected',
82
+ detail: context.rejection?.message
83
+ }]
84
+ }
85
+ }
86
+
87
+ export async function applyTransportResponseLifecycle ({
88
+ context,
89
+ transportData,
90
+ status,
91
+ body,
92
+ runHooks
93
+ }) {
94
+ if (!context || !transportData) {
95
+ return {}
96
+ }
97
+
98
+ transportData.response.status = status
99
+ transportData.response.body = body
100
+ context.transport = transportData
101
+
102
+ await runHooks('transport:response', context)
103
+
104
+ return transportData.response.headers || {}
105
+ }
106
+
107
+ export function getConnectorResponseBody (result) {
108
+ return result && result.body !== undefined ? result.body : result
109
+ }
110
+
111
+ export function getConnectorLocationHeader ({
112
+ method,
113
+ context,
114
+ routeMeta,
115
+ helpers,
116
+ mountPath = ''
117
+ }) {
118
+ if (String(method || '').toUpperCase() !== 'POST') return null
119
+ if (!context?.id || !routeMeta?.scopeName || !helpers?.getLocation) return null
120
+
121
+ const location = helpers.getLocation({
122
+ scopeName: routeMeta.scopeName,
123
+ id: context.id
124
+ })
125
+
126
+ const baseUrl = context.urlPrefix || mountPath
127
+ return `${baseUrl}${location}`
128
+ }
129
+
130
+ export async function executeConnectorRoute ({
131
+ method,
132
+ handler,
133
+ queryString,
134
+ headers,
135
+ params,
136
+ body,
137
+ context,
138
+ transportData,
139
+ routeMeta,
140
+ helpers,
141
+ mountPath = '',
142
+ runHooks
143
+ }) {
144
+ const result = await handler({
145
+ queryString,
146
+ headers,
147
+ params,
148
+ body,
149
+ context
150
+ })
151
+
152
+ const status = determineResponseStatus(method, result)
153
+ const transportHeaders = await applyTransportResponseLifecycle({
154
+ context,
155
+ transportData,
156
+ status,
157
+ body: result,
158
+ runHooks
159
+ })
160
+
161
+ return {
162
+ status,
163
+ body: status === 204 ? undefined : getConnectorResponseBody(result),
164
+ headers: {
165
+ ...(result?.headers || {}),
166
+ ...transportHeaders
167
+ },
168
+ location: getConnectorLocationHeader({
169
+ method,
170
+ context,
171
+ routeMeta,
172
+ helpers,
173
+ mountPath
174
+ }),
175
+ result
176
+ }
177
+ }
178
+
179
+ export async function handleConnectorError ({
180
+ error,
181
+ context,
182
+ transportData,
183
+ runHooks
184
+ }) {
185
+ const { status, body } = mapRestApiErrorToHttp(error)
186
+ const headers = await applyTransportResponseLifecycle({
187
+ context,
188
+ transportData,
189
+ status,
190
+ body,
191
+ runHooks
192
+ })
193
+
194
+ return { status, body, headers }
195
+ }
@@ -0,0 +1,122 @@
1
+ const JSON_API_WRITE_CONTENT_TYPES = [
2
+ 'application/vnd.api+json',
3
+ 'application/json'
4
+ ]
5
+
6
+ export function isWriteMethod (method) {
7
+ return ['POST', 'PUT', 'PATCH', 'DELETE'].includes(String(method || '').toUpperCase())
8
+ }
9
+
10
+ export function getAllowedWriteContentTypes ({ allowMultipart = false } = {}) {
11
+ return allowMultipart
12
+ ? [...JSON_API_WRITE_CONTENT_TYPES, 'multipart/form-data']
13
+ : [...JSON_API_WRITE_CONTENT_TYPES]
14
+ }
15
+
16
+ export function isAllowedWriteContentType (contentType, { allowMultipart = false } = {}) {
17
+ if (!contentType) return true
18
+
19
+ return getAllowedWriteContentTypes({ allowMultipart })
20
+ .some((allowedType) => contentType.includes(allowedType))
21
+ }
22
+
23
+ export function getUnsupportedMediaTypeErrorBody ({ allowMultipart = false } = {}) {
24
+ const acceptedTypes = getAllowedWriteContentTypes({ allowMultipart }).join(' or ')
25
+
26
+ return {
27
+ errors: [{
28
+ status: '415',
29
+ title: 'Unsupported Media Type',
30
+ detail: `Content-Type must be ${acceptedTypes}`
31
+ }]
32
+ }
33
+ }
34
+
35
+ export function determineResponseStatus (method, result) {
36
+ const upperMethod = String(method || '').toUpperCase()
37
+
38
+ if (result && typeof result.statusCode === 'number') {
39
+ return result.statusCode
40
+ }
41
+
42
+ if (upperMethod === 'POST' && result) return 201
43
+ if (upperMethod === 'POST' && !result) return 204
44
+ if (upperMethod === 'DELETE') return 204
45
+ if ((upperMethod === 'PUT' || upperMethod === 'PATCH') && !result) return 204
46
+
47
+ return 200
48
+ }
49
+
50
+ export function mapRestApiErrorToHttp (error) {
51
+ let status = 500
52
+ let errors = [{
53
+ status: '500',
54
+ title: 'Internal Server Error',
55
+ detail: error.message
56
+ }]
57
+
58
+ if (error?.validation) {
59
+ status = 400
60
+ errors = error.validation.map((issue) => ({
61
+ status: '400',
62
+ title: 'Bad Request',
63
+ detail: issue.message,
64
+ ...(issue.instancePath ? { source: { pointer: issue.instancePath } } : {})
65
+ }))
66
+ } else if (error.code === 'REST_API_VALIDATION') {
67
+ status = 422
68
+ if (error.details?.violations?.length) {
69
+ errors = error.details.violations.map((violation) => ({
70
+ status: '422',
71
+ title: 'Validation Error',
72
+ detail: violation.message,
73
+ source: { pointer: violation.field }
74
+ }))
75
+ } else {
76
+ errors = [{
77
+ status: '422',
78
+ title: 'Validation Error',
79
+ detail: error.message,
80
+ ...(error.details ? { source: error.details } : {})
81
+ }]
82
+ }
83
+ } else if (error.code === 'REST_API_RESOURCE') {
84
+ switch (error.subtype) {
85
+ case 'not_found':
86
+ status = 404
87
+ errors = [{ status: '404', title: 'Not Found', detail: error.message }]
88
+ break
89
+ case 'conflict':
90
+ status = 409
91
+ errors = [{ status: '409', title: 'Conflict', detail: error.message }]
92
+ break
93
+ case 'forbidden':
94
+ status = 403
95
+ errors = [{ status: '403', title: 'Forbidden', detail: error.message }]
96
+ break
97
+ default:
98
+ status = 400
99
+ errors = [{ status: '400', title: 'Bad Request', detail: error.message }]
100
+ }
101
+ } else if (error.code === 'REST_API_PAYLOAD') {
102
+ status = 400
103
+ errors = [{
104
+ status: '400',
105
+ title: 'Bad Request',
106
+ detail: error.message,
107
+ ...(error.path ? { source: { pointer: error.path } } : {})
108
+ }]
109
+ } else if (typeof error.statusCode === 'number' && error.statusCode >= 400 && error.statusCode < 500) {
110
+ status = error.statusCode
111
+ errors = [{
112
+ status: String(status),
113
+ title: status === 415 ? 'Unsupported Media Type' : 'Bad Request',
114
+ detail: error.message
115
+ }]
116
+ }
117
+
118
+ return {
119
+ status,
120
+ body: { errors }
121
+ }
122
+ }
@@ -0,0 +1,92 @@
1
+ import { getRequestContracts } from '../../lib/querying-writing/request-contracts.js'
2
+
3
+ const STRING_OR_NUMBER_SCHEMA = {
4
+ anyOf: [
5
+ { type: 'string' },
6
+ { type: 'number' }
7
+ ]
8
+ }
9
+
10
+ function buildResourceIdentifierSchema () {
11
+ return {
12
+ type: 'object',
13
+ additionalProperties: false,
14
+ required: ['type', 'id'],
15
+ properties: {
16
+ type: { type: 'string', minLength: 1 },
17
+ id: STRING_OR_NUMBER_SCHEMA
18
+ }
19
+ }
20
+ }
21
+
22
+ function buildRelationshipValueSchema (operation) {
23
+ if (operation === 'patchRelationship') {
24
+ return {
25
+ anyOf: [
26
+ { type: 'null' },
27
+ buildResourceIdentifierSchema(),
28
+ {
29
+ type: 'array',
30
+ items: buildResourceIdentifierSchema()
31
+ }
32
+ ]
33
+ }
34
+ }
35
+
36
+ return {
37
+ type: 'array',
38
+ items: buildResourceIdentifierSchema()
39
+ }
40
+ }
41
+
42
+ function buildRelationshipRouteBodySchema (operation) {
43
+ if (!['postRelationship', 'patchRelationship', 'deleteRelationship'].includes(operation)) {
44
+ return null
45
+ }
46
+
47
+ return {
48
+ type: 'object',
49
+ additionalProperties: false,
50
+ required: ['data'],
51
+ properties: {
52
+ data: buildRelationshipValueSchema(operation)
53
+ }
54
+ }
55
+ }
56
+
57
+ export function buildTransportRouteSchema ({ routeMeta, api }) {
58
+ if (!routeMeta) {
59
+ return null
60
+ }
61
+
62
+ if (routeMeta.kind === 'relationship') {
63
+ const relationshipBodySchema = buildRelationshipRouteBodySchema(routeMeta.operation)
64
+ return relationshipBodySchema ? { body: relationshipBodySchema } : null
65
+ }
66
+
67
+ if (routeMeta.kind !== 'resource' || !['post', 'put', 'patch'].includes(routeMeta.operation)) {
68
+ return null
69
+ }
70
+
71
+ const scope = api.resources?.[routeMeta.scopeName]
72
+ const schemaInfo = scope?.vars?.schemaInfo
73
+ if (!schemaInfo?.schemaInstance) {
74
+ return null
75
+ }
76
+
77
+ const requestContracts = getRequestContracts({
78
+ scopeName: routeMeta.scopeName,
79
+ schemaInfo,
80
+ includeDepthLimit: scope?.vars?.includeDepthLimit,
81
+ sortableFields: scope?.vars?.sortableFields
82
+ })
83
+
84
+ const contract = requestContracts[routeMeta.operation]
85
+ if (!contract?.schema) {
86
+ return null
87
+ }
88
+
89
+ return {
90
+ body: contract.schema.toJsonSchema({ mode: contract.mode, additionalProperties: false })
91
+ }
92
+ }