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,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) // undefined is passed, getter returns 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
  })