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.
- package/README.md +24 -2
- package/docs/API.md +157 -33
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +88 -6
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +3 -3
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +2 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1 -1
- package/docs/GUIDE/GUIDE_X_Autofiltering.md +63 -0
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +17 -12
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +122 -2
- package/docs/GUIDE/GUIDE_X_Row_Policies.md +603 -0
- package/docs/GUIDE/GUIDE_X_SocketIO.md +15 -10
- package/docs/GUIDE/GUIDE_X_URL_Management.md +93 -254
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +2 -0
- package/docs/GUIDE/index.md +4 -1
- package/fixing_plan.md +112 -0
- package/index.js +2 -0
- package/lib/enhanced-logger.js +1 -1
- package/package.json +6 -2
- package/plugins/core/bulk-operations-plugin.js +31 -38
- package/plugins/core/connectors/express-plugin.js +6 -7
- package/plugins/core/connectors/fastify-plugin.js +5 -1
- package/plugins/core/connectors/lib/busboy-detector.js +4 -4
- package/plugins/core/connectors/lib/connector-core.js +17 -11
- package/plugins/core/connectors/lib/transport-route-schemas.js +12 -60
- package/plugins/core/file-handling-plugin.js +126 -96
- package/plugins/core/lib/anyapi/anyapi-registry.js +21 -3
- package/plugins/core/lib/anyapi/schema-utils.js +44 -4
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +11 -2
- package/plugins/core/lib/dbIntrospection.js +1 -1
- package/plugins/core/lib/dbTablesOperations.js +10 -3
- package/plugins/core/lib/querying/knex-cross-table-search.js +0 -7
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +20 -16
- package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
- package/plugins/core/lib/querying/knex-query-helpers.js +5 -6
- package/plugins/core/lib/querying/knex-relationship-includes.js +40 -46
- package/plugins/core/lib/querying/knex-window-queries.js +10 -77
- package/plugins/core/lib/querying/url-helpers.js +9 -21
- package/plugins/core/lib/querying-writing/compile-schemas.js +10 -6
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +51 -0
- package/plugins/core/lib/querying-writing/database-capabilities.js +1 -1
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +18 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +7 -3
- package/plugins/core/lib/querying-writing/relationship-contracts.js +56 -0
- package/plugins/core/lib/querying-writing/request-contracts.js +53 -4
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +131 -0
- package/plugins/core/lib/querying-writing/return-record-settings.js +47 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +34 -43
- package/plugins/core/lib/querying-writing/simplified-helpers.js +4 -11
- package/plugins/core/lib/storage/canonical-storage-mapping.js +24 -3
- package/plugins/core/lib/storage/storage-adapter.js +76 -14
- package/plugins/core/lib/storage/storage-mapping.js +86 -16
- package/plugins/core/lib/writing/many-to-many-manipulations.js +8 -5
- package/plugins/core/lib/writing/relationship-processor.js +10 -1
- package/plugins/core/rest-api-anyapi-knex-plugin.js +402 -197
- package/plugins/core/rest-api-knex-plugin.js +93 -76
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +9 -2
- package/plugins/core/rest-api-plugin-methods/common.js +112 -72
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +34 -15
- package/plugins/core/rest-api-plugin-methods/delete.js +8 -7
- package/plugins/core/rest-api-plugin-methods/get-related.js +22 -15
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +8 -3
- package/plugins/core/rest-api-plugin-methods/get.js +7 -1
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +26 -8
- package/plugins/core/rest-api-plugin-methods/patch.js +34 -9
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +35 -16
- package/plugins/core/rest-api-plugin-methods/post.js +12 -7
- package/plugins/core/rest-api-plugin-methods/put.js +35 -10
- package/plugins/core/rest-api-plugin-methods/release.js +0 -2
- package/plugins/core/rest-api-plugin.js +11 -37
- package/plugins/core/rest-api-positioning-plugin.js +1 -4
- package/plugins/core/rest-api-row-policy-plugin.js +157 -0
- package/plugins/core/socketio-plugin.js +0 -1
- package/plugins/storage/local-storage.js +45 -20
- package/plugins/storage/s3-storage.js +23 -54
- package/quickTest.js +1 -0
- package/scripts/run-docs.js +80 -84
- package/test-debug.js +1 -1
- package/tests/README.md +8 -1
- package/tests/anyapi-basic.test.js +3 -0
- package/tests/anyapi-cursor-pagination.test.js +7 -1
- package/tests/anyapi-custom-idproperty.test.js +163 -0
- package/tests/custom-id-mysql-returning-fallback.test.js +128 -0
- package/tests/db-introspection.test.js +4 -1
- package/tests/db-table-operations.test.js +25 -20
- package/tests/fastify-plugin.test.js +49 -6
- package/tests/field-setters.test.js +10 -10
- package/tests/file-handling.test.js +118 -0
- package/tests/fixtures/api-configs.js +495 -4
- package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
- package/tests/http-connectors-parity.test.js +165 -5
- package/tests/id-normalization.test.js +444 -0
- package/tests/local-storage.test.js +96 -0
- package/tests/logical-id-write-validation.test.js +151 -0
- package/tests/pagination-cursor-multifield.test.js +132 -11
- package/tests/pagination-enhanced.test.js +66 -15
- package/tests/post-commit-hooks.test.js +120 -0
- package/tests/public-exports.test.js +15 -0
- package/tests/put-create-parity.test.js +216 -0
- package/tests/relationship-endpoints.test.js +259 -0
- package/tests/return-record-settings.test.js +184 -0
- package/tests/row-policy.test.js +433 -0
- package/tests/s3-storage.test.js +38 -0
- package/tests/searchschema-merge.test.js +17 -0
- package/tests/socketio.test.js +62 -0
- package/tests/storage-mapping.test.js +56 -6
- package/tests/test-development-guide.md +5 -5
package/tests/socketio.test.js
CHANGED
|
@@ -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,
|
|
74
|
-
loginCount: { type: 'number', defaultTo: 0
|
|
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:
|
|
68
|
-
put:
|
|
69
|
-
patch:
|
|
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:
|
|
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.
|