json-rest-api 1.0.23 → 1.0.25

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 (106) hide show
  1. package/README.md +24 -2
  2. package/docs/API.md +157 -33
  3. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +88 -6
  4. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +3 -3
  5. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +2 -0
  6. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1 -1
  7. package/docs/GUIDE/GUIDE_X_Autofiltering.md +63 -0
  8. package/docs/GUIDE/GUIDE_X_File_Uploads.md +17 -12
  9. package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +122 -2
  10. package/docs/GUIDE/GUIDE_X_Row_Policies.md +603 -0
  11. package/docs/GUIDE/GUIDE_X_SocketIO.md +15 -10
  12. package/docs/GUIDE/GUIDE_X_URL_Management.md +93 -254
  13. package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +2 -0
  14. package/docs/GUIDE/index.md +4 -1
  15. package/fixing_plan.md +112 -0
  16. package/index.js +2 -0
  17. package/lib/enhanced-logger.js +1 -1
  18. package/package.json +6 -2
  19. package/plugins/core/bulk-operations-plugin.js +31 -38
  20. package/plugins/core/connectors/express-plugin.js +6 -7
  21. package/plugins/core/connectors/fastify-plugin.js +5 -1
  22. package/plugins/core/connectors/lib/busboy-detector.js +4 -4
  23. package/plugins/core/connectors/lib/connector-core.js +17 -11
  24. package/plugins/core/connectors/lib/transport-route-schemas.js +12 -60
  25. package/plugins/core/file-handling-plugin.js +126 -96
  26. package/plugins/core/lib/anyapi/anyapi-registry.js +21 -3
  27. package/plugins/core/lib/anyapi/schema-utils.js +44 -4
  28. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +11 -2
  29. package/plugins/core/lib/dbIntrospection.js +1 -1
  30. package/plugins/core/lib/dbTablesOperations.js +10 -3
  31. package/plugins/core/lib/querying/knex-cross-table-search.js +0 -7
  32. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +20 -16
  33. package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
  34. package/plugins/core/lib/querying/knex-query-helpers.js +5 -6
  35. package/plugins/core/lib/querying/knex-relationship-includes.js +40 -46
  36. package/plugins/core/lib/querying/knex-window-queries.js +10 -77
  37. package/plugins/core/lib/querying/url-helpers.js +9 -21
  38. package/plugins/core/lib/querying-writing/compile-schemas.js +10 -6
  39. package/plugins/core/lib/querying-writing/connectors-query-parser.js +51 -0
  40. package/plugins/core/lib/querying-writing/database-capabilities.js +1 -1
  41. package/plugins/core/lib/querying-writing/database-value-normalizers.js +18 -9
  42. package/plugins/core/lib/querying-writing/knex-field-helpers.js +7 -3
  43. package/plugins/core/lib/querying-writing/relationship-contracts.js +56 -0
  44. package/plugins/core/lib/querying-writing/request-contracts.js +53 -4
  45. package/plugins/core/lib/querying-writing/resource-id-normalization.js +131 -0
  46. package/plugins/core/lib/querying-writing/return-record-settings.js +47 -0
  47. package/plugins/core/lib/querying-writing/schema-helpers.js +34 -43
  48. package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
  49. package/plugins/core/lib/storage/canonical-storage-mapping.js +24 -3
  50. package/plugins/core/lib/storage/storage-adapter.js +76 -14
  51. package/plugins/core/lib/storage/storage-mapping.js +86 -16
  52. package/plugins/core/lib/writing/many-to-many-manipulations.js +8 -5
  53. package/plugins/core/lib/writing/relationship-processor.js +10 -1
  54. package/plugins/core/rest-api-anyapi-knex-plugin.js +402 -197
  55. package/plugins/core/rest-api-knex-plugin.js +93 -76
  56. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +9 -2
  57. package/plugins/core/rest-api-plugin-methods/common.js +112 -72
  58. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +34 -15
  59. package/plugins/core/rest-api-plugin-methods/delete.js +8 -7
  60. package/plugins/core/rest-api-plugin-methods/get-related.js +22 -15
  61. package/plugins/core/rest-api-plugin-methods/get-relationship.js +8 -3
  62. package/plugins/core/rest-api-plugin-methods/get.js +7 -1
  63. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +26 -8
  64. package/plugins/core/rest-api-plugin-methods/patch.js +34 -9
  65. package/plugins/core/rest-api-plugin-methods/post-relationship.js +35 -16
  66. package/plugins/core/rest-api-plugin-methods/post.js +12 -7
  67. package/plugins/core/rest-api-plugin-methods/put.js +35 -10
  68. package/plugins/core/rest-api-plugin-methods/release.js +0 -2
  69. package/plugins/core/rest-api-plugin.js +11 -37
  70. package/plugins/core/rest-api-positioning-plugin.js +1 -4
  71. package/plugins/core/rest-api-row-policy-plugin.js +157 -0
  72. package/plugins/core/socketio-plugin.js +0 -1
  73. package/plugins/storage/local-storage.js +45 -20
  74. package/plugins/storage/s3-storage.js +23 -54
  75. package/quickTest.js +1 -0
  76. package/scripts/run-docs.js +80 -84
  77. package/test-debug.js +1 -1
  78. package/tests/README.md +8 -1
  79. package/tests/anyapi-basic.test.js +3 -0
  80. package/tests/anyapi-cursor-pagination.test.js +7 -1
  81. package/tests/anyapi-custom-idproperty.test.js +163 -0
  82. package/tests/custom-id-mysql-returning-fallback.test.js +128 -0
  83. package/tests/db-introspection.test.js +4 -1
  84. package/tests/db-table-operations.test.js +25 -20
  85. package/tests/fastify-plugin.test.js +49 -6
  86. package/tests/field-setters.test.js +10 -10
  87. package/tests/file-handling.test.js +118 -0
  88. package/tests/fixtures/api-configs.js +495 -4
  89. package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
  90. package/tests/http-connectors-parity.test.js +165 -5
  91. package/tests/id-normalization.test.js +444 -0
  92. package/tests/local-storage.test.js +96 -0
  93. package/tests/logical-id-write-validation.test.js +151 -0
  94. package/tests/pagination-cursor-multifield.test.js +132 -11
  95. package/tests/pagination-enhanced.test.js +66 -15
  96. package/tests/post-commit-hooks.test.js +120 -0
  97. package/tests/public-exports.test.js +15 -0
  98. package/tests/put-create-parity.test.js +216 -0
  99. package/tests/relationship-endpoints.test.js +259 -0
  100. package/tests/return-record-settings.test.js +184 -0
  101. package/tests/row-policy.test.js +433 -0
  102. package/tests/s3-storage.test.js +38 -0
  103. package/tests/searchschema-merge.test.js +17 -0
  104. package/tests/socketio.test.js +62 -0
  105. package/tests/storage-mapping.test.js +56 -6
  106. package/tests/test-development-guide.md +5 -5
@@ -453,6 +453,68 @@ describe('WebSocket/Socket.IO Plugin', () => {
453
453
  socket.close()
454
454
  }
455
455
  })
456
+
457
+ it('should broadcast deferred patch notifications from relationship PATCH', async () => {
458
+ const token = await createToken({ userId: 'test-user', role: 'user' }, 'test-secret-key')
459
+
460
+ const socket = ioClient(`http://localhost:${server.address().port}`, {
461
+ path: '/api/socket.io',
462
+ auth: { token }
463
+ })
464
+
465
+ try {
466
+ await new Promise((resolve, reject) => {
467
+ socket.on('connect', resolve)
468
+ socket.on('connect_error', reject)
469
+ setTimeout(() => reject(new Error('Connection timeout')), 5000)
470
+ })
471
+
472
+ const country = await api.resources.countries.post({
473
+ inputRecord: createJsonApiDocument('countries', {
474
+ name: 'Original Country',
475
+ code: 'OC'
476
+ }),
477
+ simplified: false
478
+ })
479
+
480
+ const nextCountry = await api.resources.countries.post({
481
+ inputRecord: createJsonApiDocument('countries', {
482
+ name: 'Next Country',
483
+ code: 'NC'
484
+ }),
485
+ simplified: false
486
+ })
487
+
488
+ const book = await api.resources.books.post({
489
+ inputRecord: createJsonApiDocument(
490
+ 'books',
491
+ { title: 'Relationship Patch Broadcast' },
492
+ { country: createRelationship(resourceIdentifier('countries', country.data.id)) }
493
+ ),
494
+ simplified: false
495
+ })
496
+
497
+ await new Promise((resolve) => {
498
+ socket.emit('subscribe', { resource: 'books' }, resolve)
499
+ })
500
+
501
+ const updatePromise = waitForSocketEvent(socket, 'subscription.update', 1000)
502
+
503
+ await api.resources.books.patchRelationship({
504
+ id: book.data.id,
505
+ relationshipName: 'country',
506
+ relationshipData: resourceIdentifier('countries', nextCountry.data.id)
507
+ })
508
+
509
+ const notification = await updatePromise
510
+ assert.equal(notification.type, 'resource.patchd')
511
+ assert.equal(notification.resource, 'books')
512
+ assert.equal(String(notification.id), String(book.data.id))
513
+ assert.equal(notification.action, 'patch')
514
+ } finally {
515
+ socket.close()
516
+ }
517
+ })
456
518
  })
457
519
 
458
520
  describe('Multiple Subscriptions', () => {
@@ -42,6 +42,7 @@ describe('Storage mapping', () => {
42
42
  await api.use(RestApiPlugin, {
43
43
  simplifiedApi: false,
44
44
  simplifiedTransport: false,
45
+ sortableFields: ['id', 'displayName', 'loginCount', 'lastSeenAt', 'countryId', 'externalRef'],
45
46
  returnRecordApi: {
46
47
  post: 'full',
47
48
  put: 'full',
@@ -70,13 +71,12 @@ describe('Storage mapping', () => {
70
71
  await api.addResource('profiles', {
71
72
  schema: {
72
73
  id: { type: 'id' },
73
- displayName: { type: 'string', required: true, storage: { column: 'display_name' } },
74
- loginCount: { type: 'number', defaultTo: 0, storage: { column: 'login_count' } },
74
+ displayName: { type: 'string', required: true, search: true },
75
+ loginCount: { type: 'number', defaultTo: 0 },
75
76
  lastSeenAt: {
76
77
  type: 'dateTime',
77
78
  nullable: true,
78
79
  storage: {
79
- column: 'last_seen_at',
80
80
  serialize: serializeDateTimeForSql
81
81
  }
82
82
  },
@@ -84,14 +84,25 @@ describe('Storage mapping', () => {
84
84
  type: 'id',
85
85
  nullable: true,
86
86
  belongsTo: 'countries',
87
- as: 'country',
88
- storage: { column: 'country_id' }
87
+ as: 'country'
89
88
  },
89
+ externalRef: { type: 'string', nullable: true, search: true, storage: { column: 'legacy_ref' } },
90
90
  confirmationToken: { type: 'string', virtual: true }
91
91
  },
92
92
  tableName: 'mapped_profiles'
93
93
  })
94
94
  await api.resources.profiles.createKnexTable()
95
+
96
+ await api.addResource('verbatim_profiles', {
97
+ storage: { naming: 'exact' },
98
+ schema: {
99
+ id: { type: 'id' },
100
+ displayName: { type: 'string', required: true },
101
+ loginCount: { type: 'number', defaultTo: 0 }
102
+ },
103
+ tableName: 'verbatim_profiles'
104
+ })
105
+ await api.resources.verbatim_profiles.createKnexTable()
95
106
  })
96
107
 
97
108
  after(async () => {
@@ -99,7 +110,7 @@ describe('Storage mapping', () => {
99
110
  })
100
111
 
101
112
  beforeEach(async () => {
102
- await cleanTables(knex, ['mapped_profiles', 'mapped_countries'])
113
+ await cleanTables(knex, ['mapped_profiles', 'mapped_countries', 'verbatim_profiles'])
103
114
  })
104
115
 
105
116
  it('persists mapped columns while exposing logical field names', async () => {
@@ -116,6 +127,7 @@ describe('Storage mapping', () => {
116
127
  displayName: 'Mercury',
117
128
  loginCount: 7,
118
129
  lastSeenAt: '2026-01-02T03:04:05Z',
130
+ externalRef: 'legacy-001',
119
131
  confirmationToken: 'secret-token'
120
132
  },
121
133
  {
@@ -140,6 +152,7 @@ describe('Storage mapping', () => {
140
152
  assert.equal(dbRecord.login_count, 7)
141
153
  assert.equal(dbRecord.last_seen_at, '2026-01-02 03:04:05')
142
154
  assert.equal(String(dbRecord.country_id), country.data.id)
155
+ assert.equal(dbRecord.legacy_ref, 'legacy-001')
143
156
  assert.ok(!('displayName' in dbRecord))
144
157
  assert.ok(!('countryId' in dbRecord))
145
158
  assert.ok(!('confirmationToken' in dbRecord))
@@ -164,6 +177,22 @@ describe('Storage mapping', () => {
164
177
  'country',
165
178
  resourceIdentifier('countries', country.data.id)
166
179
  )
180
+
181
+ const queryResult = await api.resources.profiles.query({
182
+ queryParams: {
183
+ filters: {
184
+ displayName: 'Mercury',
185
+ externalRef: 'legacy-001'
186
+ }
187
+ }
188
+ })
189
+
190
+ validateJsonApiStructure(queryResult, true)
191
+ assert.equal(queryResult.data.length, 1)
192
+ assertResourceAttributes(queryResult.data[0], {
193
+ displayName: 'Mercury',
194
+ externalRef: 'legacy-001'
195
+ })
167
196
  })
168
197
 
169
198
  it('applies storage mapping on PATCH updates and sparse fieldsets', async () => {
@@ -207,4 +236,25 @@ describe('Storage mapping', () => {
207
236
  assert.equal(dbRecord.last_seen_at, '2026-02-03 04:05:06')
208
237
  assert.ok(!('displayName' in dbRecord))
209
238
  })
239
+
240
+ it('can opt out of snake_case translation for an entire resource', async () => {
241
+ const created = await api.resources.verbatim_profiles.post({
242
+ inputRecord: createJsonApiDocument('verbatim_profiles', {
243
+ displayName: 'Exact Mode',
244
+ loginCount: 3
245
+ })
246
+ })
247
+
248
+ validateJsonApiStructure(created)
249
+ assertResourceAttributes(created.data, {
250
+ displayName: 'Exact Mode',
251
+ loginCount: 3
252
+ })
253
+
254
+ const dbRecord = await knex('verbatim_profiles').where('id', created.data.id).first()
255
+ assert.equal(dbRecord.displayName, 'Exact Mode')
256
+ assert.equal(dbRecord.loginCount, 3)
257
+ assert.equal(Object.hasOwn(dbRecord, 'display_name'), false)
258
+ assert.equal(Object.hasOwn(dbRecord, 'login_count'), false)
259
+ })
210
260
  })
@@ -64,9 +64,9 @@ Control what methods return:
64
64
  ```javascript
65
65
  // In API configuration
66
66
  returnFullRecord: {
67
- post: true, // Return created record (needed to get ID)
68
- put: false, // Don't return after replacement
69
- patch: false // Don't return after update
67
+ post: 'full', // Return created record (needed to get ID)
68
+ put: 'no', // Don't return after replacement
69
+ patch: 'no' // Don't return after update
70
70
  }
71
71
 
72
72
  // Override per call if needed
@@ -74,7 +74,7 @@ const result = await api.resources.books.put({
74
74
  id: bookId,
75
75
  inputRecord: putDoc,
76
76
  simplified: false,
77
- returnFullRecord: false // Override to not return
77
+ returnFullRecord: 'no' // Override to not return
78
78
  });
79
79
  ```
80
80
 
@@ -367,4 +367,4 @@ npm test -- tests/relationships.test.js
367
367
  4. **validation.test.js** - Schema validation edge cases
368
368
  5. **polymorphic.test.js** - Polymorphic relationship tests
369
369
 
370
- Each should follow the patterns established in existing tests.
370
+ Each should follow the patterns established in existing tests.