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
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, it, beforeEach } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { Api } from 'hooked-api'
|
|
4
|
+
import { RestApiPlugin, FastifyPlugin } from '../index.js'
|
|
5
|
+
import {
|
|
6
|
+
FakeFastifyApp,
|
|
7
|
+
FakeReply,
|
|
8
|
+
findFastifyRoute
|
|
9
|
+
} from './helpers/fake-fastify.js'
|
|
10
|
+
|
|
11
|
+
async function createFastifyConnectorApi () {
|
|
12
|
+
const app = new FakeFastifyApp()
|
|
13
|
+
const api = new Api({
|
|
14
|
+
name: 'fastify-connector-test-api',
|
|
15
|
+
log: { level: process.env.LOG_LEVEL || 'silent' }
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
await api.use(RestApiPlugin, {
|
|
19
|
+
simplifiedApi: false,
|
|
20
|
+
simplifiedTransport: false
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
await api.use(FastifyPlugin, {
|
|
24
|
+
app,
|
|
25
|
+
mountPath: '/api'
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
await api.addResource('users', {
|
|
29
|
+
schema: {
|
|
30
|
+
id: { type: 'id' },
|
|
31
|
+
name: { type: 'string', required: true }
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
await api.addResource('articles', {
|
|
36
|
+
schema: {
|
|
37
|
+
id: { type: 'id' },
|
|
38
|
+
title: { type: 'string', required: true },
|
|
39
|
+
body: { type: 'string' },
|
|
40
|
+
author_id: { type: 'id', belongsTo: 'users', as: 'author' },
|
|
41
|
+
internal_summary: { type: 'string', computed: true }
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return { api, app }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('Fastify Plugin', () => {
|
|
49
|
+
let api
|
|
50
|
+
let app
|
|
51
|
+
|
|
52
|
+
beforeEach(async () => {
|
|
53
|
+
({ api, app } = await createFastifyConnectorApi())
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('registers Fastify write routes with schema-backed JSON:API body validation', () => {
|
|
57
|
+
const postRoute = findFastifyRoute(app, 'POST', '/api/articles')
|
|
58
|
+
const putRoute = findFastifyRoute(app, 'PUT', '/api/articles/:id')
|
|
59
|
+
const patchRoute = findFastifyRoute(app, 'PATCH', '/api/articles/:id')
|
|
60
|
+
const getRoute = findFastifyRoute(app, 'GET', '/api/articles/:id')
|
|
61
|
+
const postRelationshipRoute = findFastifyRoute(app, 'POST', '/api/articles/:id/relationships/:relationshipName')
|
|
62
|
+
const patchRelationshipRoute = findFastifyRoute(app, 'PATCH', '/api/articles/:id/relationships/:relationshipName')
|
|
63
|
+
const deleteRelationshipRoute = findFastifyRoute(app, 'DELETE', '/api/articles/:id/relationships/:relationshipName')
|
|
64
|
+
|
|
65
|
+
assert.ok(postRoute?.schema?.body, 'POST route should have a body schema')
|
|
66
|
+
assert.ok(putRoute?.schema?.body, 'PUT route should have a body schema')
|
|
67
|
+
assert.ok(patchRoute?.schema?.body, 'PATCH route should have a body schema')
|
|
68
|
+
assert.equal(getRoute?.schema, undefined, 'GET route should not have a body schema')
|
|
69
|
+
assert.ok(postRelationshipRoute?.schema?.body, 'POST relationship route should have a body schema')
|
|
70
|
+
assert.ok(patchRelationshipRoute?.schema?.body, 'PATCH relationship route should have a body schema')
|
|
71
|
+
assert.ok(deleteRelationshipRoute?.schema?.body, 'DELETE relationship route should have a body schema')
|
|
72
|
+
|
|
73
|
+
const postDataSchema = postRoute.schema.body.properties.data
|
|
74
|
+
const postAttributes = postDataSchema.properties.attributes
|
|
75
|
+
|
|
76
|
+
assert.deepEqual(postDataSchema.required, ['type'])
|
|
77
|
+
assert.equal(postAttributes.properties.title.type, 'string')
|
|
78
|
+
assert.equal(postAttributes.properties.author_id, undefined, 'belongsTo foreign key should not be accepted in attributes')
|
|
79
|
+
assert.equal(postAttributes.properties.internal_summary, undefined, 'computed fields should not be accepted in attributes')
|
|
80
|
+
assert.equal(postAttributes.properties.id, undefined, 'resource ids should not appear in attributes')
|
|
81
|
+
assert.ok(postDataSchema.properties.relationships.properties.author, 'declared belongsTo relationships should be represented in the transport schema')
|
|
82
|
+
|
|
83
|
+
const putDataSchema = putRoute.schema.body.properties.data
|
|
84
|
+
assert.ok(putDataSchema.required.includes('id'), 'PUT body schema should require data.id')
|
|
85
|
+
|
|
86
|
+
const patchDataSchema = patchRoute.schema.body.properties.data
|
|
87
|
+
assert.ok(Array.isArray(patchDataSchema.anyOf), 'PATCH body schema should require at least one writable section')
|
|
88
|
+
assert.deepEqual(patchDataSchema.anyOf, [
|
|
89
|
+
{ required: ['attributes'] },
|
|
90
|
+
{ required: ['relationships'] }
|
|
91
|
+
])
|
|
92
|
+
|
|
93
|
+
assert.equal(postRelationshipRoute.schema.body.properties.data.type, 'array')
|
|
94
|
+
assert.ok(Array.isArray(patchRelationshipRoute.schema.body.properties.data.anyOf))
|
|
95
|
+
assert.equal(deleteRelationshipRoute.schema.body.properties.data.type, 'array')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('registers the JSON:API content-type parser and rejects unsupported write content types before the route handler', async () => {
|
|
99
|
+
const parser = app.parsers.find((entry) => entry.contentType === 'application/vnd.api+json')
|
|
100
|
+
assert.ok(parser, 'Fastify connector should register the JSON:API parser')
|
|
101
|
+
assert.deepEqual(parser.options, { parseAs: 'string' })
|
|
102
|
+
|
|
103
|
+
const postRoute = findFastifyRoute(app, 'POST', '/api/articles')
|
|
104
|
+
const reply = new FakeReply()
|
|
105
|
+
|
|
106
|
+
await postRoute.preValidation(
|
|
107
|
+
{ headers: { 'content-type': 'text/plain' } },
|
|
108
|
+
reply
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
assert.equal(reply.statusCode, 415)
|
|
112
|
+
assert.equal(reply.contentType, 'application/vnd.api+json')
|
|
113
|
+
assert.deepEqual(reply.payload, {
|
|
114
|
+
errors: [{
|
|
115
|
+
status: '415',
|
|
116
|
+
title: 'Unsupported Media Type',
|
|
117
|
+
detail: 'Content-Type must be application/vnd.api+json or application/json'
|
|
118
|
+
}]
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const multipartReply = new FakeReply()
|
|
122
|
+
await postRoute.preValidation(
|
|
123
|
+
{ headers: { 'content-type': 'multipart/form-data; boundary=test' } },
|
|
124
|
+
multipartReply
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
assert.equal(multipartReply.statusCode, 415)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('leaves custom routes unschematized when they do not provide route metadata', async () => {
|
|
131
|
+
await api.addRoute({
|
|
132
|
+
method: 'POST',
|
|
133
|
+
path: '/api/custom-endpoint',
|
|
134
|
+
handler: async () => ({ ok: true })
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const customRoute = findFastifyRoute(app, 'POST', '/api/custom-endpoint')
|
|
138
|
+
assert.ok(customRoute)
|
|
139
|
+
assert.equal(customRoute.schema, undefined)
|
|
140
|
+
})
|
|
141
|
+
})
|
|
@@ -62,14 +62,13 @@ describe('Field Getters', () => {
|
|
|
62
62
|
it('should handle null and undefined values', async () => {
|
|
63
63
|
const user = await api.resources.users.post({
|
|
64
64
|
email: null,
|
|
65
|
-
name: undefined,
|
|
66
65
|
phone: null,
|
|
67
66
|
metadata_json: null,
|
|
68
67
|
tags_csv: null
|
|
69
68
|
})
|
|
70
69
|
|
|
71
70
|
assert.equal(user.email, undefined) // null?.toLowerCase().trim() returns undefined
|
|
72
|
-
assert.equal(user.name, undefined) //
|
|
71
|
+
assert.equal(user.name, undefined) // omitted nullable field remains unset
|
|
73
72
|
assert.equal(user.phone, null) // getter explicitly returns null for falsy values
|
|
74
73
|
assert.deepEqual(user.metadata_json, {})
|
|
75
74
|
assert.deepEqual(user.tags_csv, [])
|
|
@@ -132,9 +132,6 @@ describe('Field Setters', () => {
|
|
|
132
132
|
data: 'sensitive information'
|
|
133
133
|
})
|
|
134
134
|
|
|
135
|
-
console.log('**********************************************')
|
|
136
|
-
console.log(record)
|
|
137
|
-
// Check if record has id
|
|
138
135
|
assert.ok(record.id, 'Record should have an id')
|
|
139
136
|
|
|
140
137
|
// Get the record to verify async setters were applied
|
|
@@ -222,7 +219,6 @@ describe('Field Setters', () => {
|
|
|
222
219
|
it('should handle null and undefined values in setters', async () => {
|
|
223
220
|
const data = await api.resources.nullable_data.post({
|
|
224
221
|
field1: null,
|
|
225
|
-
field2: undefined,
|
|
226
222
|
field3: '',
|
|
227
223
|
field4: 0
|
|
228
224
|
})
|