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
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { Api } from 'hooked-api'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
FileHandlingPlugin,
|
|
4
|
+
QueryProjectionsPlugin,
|
|
5
|
+
RestApiAnyapiKnexPlugin,
|
|
6
|
+
RestApiKnexPlugin,
|
|
7
|
+
RestApiPlugin,
|
|
8
|
+
RowPolicyPlugin,
|
|
9
|
+
SocketIOPlugin
|
|
10
|
+
} from '../../index.js'
|
|
3
11
|
import { ExpressPlugin } from '../../plugins/core/connectors/express-plugin.js'
|
|
4
12
|
import express from 'express'
|
|
5
13
|
import { createServer } from 'http'
|
|
@@ -198,6 +206,266 @@ export async function createBasicApi (knex, pluginOptions = {}) {
|
|
|
198
206
|
}
|
|
199
207
|
}
|
|
200
208
|
|
|
209
|
+
export async function createReturnRecordApi (knex, pluginOptions = {}) {
|
|
210
|
+
const apiName = pluginOptions.apiName || 'return-record-test-api'
|
|
211
|
+
const tablePrefix = pluginOptions.tablePrefix || 'return'
|
|
212
|
+
if (storageMode.isAnyApi()) {
|
|
213
|
+
storageMode.clearRegistry()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const api = new Api({
|
|
217
|
+
name: apiName,
|
|
218
|
+
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
const previousTenant = storageMode.currentTenant
|
|
222
|
+
const tenantId = storageMode.isAnyApi()
|
|
223
|
+
? (pluginOptions.tenantId || `${tablePrefix}_tenant`)
|
|
224
|
+
: storageMode.defaultTenant
|
|
225
|
+
if (storageMode.isAnyApi()) {
|
|
226
|
+
storageMode.setCurrentTenant(tenantId)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
await api.use(RestApiPlugin, {
|
|
230
|
+
simplifiedApi: false,
|
|
231
|
+
simplifiedTransport: false,
|
|
232
|
+
returnRecordApi: {
|
|
233
|
+
post: true,
|
|
234
|
+
put: false,
|
|
235
|
+
patch: false
|
|
236
|
+
},
|
|
237
|
+
returnRecordTransport: {
|
|
238
|
+
post: false,
|
|
239
|
+
put: true,
|
|
240
|
+
patch: 'minimal'
|
|
241
|
+
},
|
|
242
|
+
sortableFields: ['id', 'name'],
|
|
243
|
+
...pluginOptions['rest-api']
|
|
244
|
+
})
|
|
245
|
+
await useStoragePlugin(api, knex, { tenantId })
|
|
246
|
+
await resetAnyApiTables(knex)
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
await api.addResource('global_items', {
|
|
250
|
+
schema: {
|
|
251
|
+
id: { type: 'id' },
|
|
252
|
+
name: { type: 'string', required: true, max: 100 }
|
|
253
|
+
},
|
|
254
|
+
tableName: `${tablePrefix}_global_items`
|
|
255
|
+
})
|
|
256
|
+
await api.resources.global_items.createKnexTable()
|
|
257
|
+
mapTable(`${tablePrefix}_global_items`, 'global_items')
|
|
258
|
+
|
|
259
|
+
await api.addResource('scope_items', {
|
|
260
|
+
schema: {
|
|
261
|
+
id: { type: 'id' },
|
|
262
|
+
name: { type: 'string', required: true, max: 100 }
|
|
263
|
+
},
|
|
264
|
+
tableName: `${tablePrefix}_scope_items`,
|
|
265
|
+
returnRecordApi: {
|
|
266
|
+
post: false,
|
|
267
|
+
put: true,
|
|
268
|
+
patch: 'minimal'
|
|
269
|
+
},
|
|
270
|
+
returnRecordTransport: true
|
|
271
|
+
})
|
|
272
|
+
await api.resources.scope_items.createKnexTable()
|
|
273
|
+
mapTable(`${tablePrefix}_scope_items`, 'scope_items')
|
|
274
|
+
} finally {
|
|
275
|
+
if (storageMode.isAnyApi()) {
|
|
276
|
+
storageMode.setCurrentTenant(previousTenant)
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return api
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Creates a small API that uses logical camelCase belongsTo fields with default snake_case storage mapping.
|
|
285
|
+
* This reproduces the full JSON:API linkage path where storage column names differ from logical field names.
|
|
286
|
+
*/
|
|
287
|
+
export async function createCamelCaseBelongsToApi (knex, pluginOptions = {}) {
|
|
288
|
+
const apiName = pluginOptions.apiName || 'camelcase-belongsto-test-api'
|
|
289
|
+
const tablePrefix = pluginOptions.tablePrefix || 'camel_fk'
|
|
290
|
+
|
|
291
|
+
const api = new Api({
|
|
292
|
+
name: apiName,
|
|
293
|
+
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
await api.use(RestApiPlugin, {
|
|
297
|
+
simplifiedApi: false,
|
|
298
|
+
simplifiedTransport: false,
|
|
299
|
+
returnRecordApi: {
|
|
300
|
+
post: true,
|
|
301
|
+
put: false,
|
|
302
|
+
patch: false
|
|
303
|
+
},
|
|
304
|
+
returnRecordTransport: {
|
|
305
|
+
post: 'full',
|
|
306
|
+
put: 'no',
|
|
307
|
+
patch: 'full'
|
|
308
|
+
},
|
|
309
|
+
...(pluginOptions['rest-api'] || {})
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
await api.use(RestApiKnexPlugin, { knex })
|
|
313
|
+
|
|
314
|
+
await api.addResource('countries', {
|
|
315
|
+
schema: {
|
|
316
|
+
id: { type: 'id' },
|
|
317
|
+
name: { type: 'string', required: true, max: 100 }
|
|
318
|
+
},
|
|
319
|
+
tableName: `${tablePrefix}_countries`
|
|
320
|
+
})
|
|
321
|
+
await api.resources.countries.createKnexTable()
|
|
322
|
+
|
|
323
|
+
await api.addResource('publishers', {
|
|
324
|
+
schema: {
|
|
325
|
+
id: { type: 'id' },
|
|
326
|
+
name: { type: 'string', required: true, max: 200 },
|
|
327
|
+
countryId: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' }
|
|
328
|
+
},
|
|
329
|
+
tableName: `${tablePrefix}_publishers`
|
|
330
|
+
})
|
|
331
|
+
await api.resources.publishers.createKnexTable()
|
|
332
|
+
|
|
333
|
+
return api
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Creates a small API focused on resource ID normalization behavior.
|
|
338
|
+
*/
|
|
339
|
+
export async function createIdNormalizationApi (knex, pluginOptions = {}) {
|
|
340
|
+
const apiName = pluginOptions.apiName || 'id-normalization-test-api'
|
|
341
|
+
const tablePrefix = pluginOptions.tablePrefix || 'id_norm'
|
|
342
|
+
if (storageMode.isAnyApi()) {
|
|
343
|
+
storageMode.clearRegistry()
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const api = new Api({
|
|
347
|
+
name: apiName,
|
|
348
|
+
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
const previousTenant = storageMode.currentTenant
|
|
352
|
+
const tenantId = storageMode.isAnyApi()
|
|
353
|
+
? (pluginOptions.tenantId || `${tablePrefix}_tenant`)
|
|
354
|
+
: storageMode.defaultTenant
|
|
355
|
+
if (storageMode.isAnyApi()) {
|
|
356
|
+
storageMode.setCurrentTenant(tenantId)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
await api.use(RestApiPlugin, {
|
|
360
|
+
simplifiedApi: false,
|
|
361
|
+
simplifiedTransport: false,
|
|
362
|
+
returnRecordApi: {
|
|
363
|
+
post: true,
|
|
364
|
+
put: false,
|
|
365
|
+
patch: false
|
|
366
|
+
},
|
|
367
|
+
returnRecordTransport: {
|
|
368
|
+
post: 'full',
|
|
369
|
+
put: 'no',
|
|
370
|
+
patch: 'minimal'
|
|
371
|
+
},
|
|
372
|
+
...(pluginOptions['rest-api'] || {})
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
try {
|
|
376
|
+
await withTenantContext(tenantId, async () => {
|
|
377
|
+
await useStoragePlugin(api, knex, { tenantId })
|
|
378
|
+
await resetAnyApiTables(knex)
|
|
379
|
+
|
|
380
|
+
await api.addResource('countries', {
|
|
381
|
+
schema: {
|
|
382
|
+
id: { type: 'string', required: true },
|
|
383
|
+
name: { type: 'string', required: true, max: 100 }
|
|
384
|
+
},
|
|
385
|
+
tableName: `${tablePrefix}_countries`,
|
|
386
|
+
...(pluginOptions.countryResourceOptions || {})
|
|
387
|
+
})
|
|
388
|
+
await knex.schema.dropTableIfExists(`${tablePrefix}_countries`)
|
|
389
|
+
await knex.schema.createTable(`${tablePrefix}_countries`, (table) => {
|
|
390
|
+
table.string('id').primary()
|
|
391
|
+
table.string('name').notNullable()
|
|
392
|
+
})
|
|
393
|
+
mapTable(`${tablePrefix}_countries`, 'countries')
|
|
394
|
+
|
|
395
|
+
await api.addResource('publishers', {
|
|
396
|
+
schema: {
|
|
397
|
+
id: { type: 'string', required: true },
|
|
398
|
+
name: { type: 'string', required: true, max: 100 },
|
|
399
|
+
country_id: { type: 'string', nullable: true, belongsTo: 'countries', as: 'country' }
|
|
400
|
+
},
|
|
401
|
+
relationships: {
|
|
402
|
+
tags: { type: 'manyToMany', through: 'publisher_tags', foreignKey: 'publisher_id', otherKey: 'tag_id' }
|
|
403
|
+
},
|
|
404
|
+
tableName: `${tablePrefix}_publishers`,
|
|
405
|
+
...(pluginOptions.publisherResourceOptions || {})
|
|
406
|
+
})
|
|
407
|
+
await knex.schema.dropTableIfExists(`${tablePrefix}_publishers`)
|
|
408
|
+
await knex.schema.createTable(`${tablePrefix}_publishers`, (table) => {
|
|
409
|
+
table.string('id').primary()
|
|
410
|
+
table.string('name').notNullable()
|
|
411
|
+
table.string('country_id').nullable()
|
|
412
|
+
})
|
|
413
|
+
mapTable(`${tablePrefix}_publishers`, 'publishers')
|
|
414
|
+
|
|
415
|
+
await api.addResource('tags', {
|
|
416
|
+
schema: {
|
|
417
|
+
id: { type: 'string', required: true },
|
|
418
|
+
name: { type: 'string', required: true, max: 100 }
|
|
419
|
+
},
|
|
420
|
+
tableName: `${tablePrefix}_tags`,
|
|
421
|
+
...(pluginOptions.tagResourceOptions || {})
|
|
422
|
+
})
|
|
423
|
+
await knex.schema.dropTableIfExists(`${tablePrefix}_tags`)
|
|
424
|
+
await knex.schema.createTable(`${tablePrefix}_tags`, (table) => {
|
|
425
|
+
table.string('id').primary()
|
|
426
|
+
table.string('name').notNullable()
|
|
427
|
+
})
|
|
428
|
+
mapTable(`${tablePrefix}_tags`, 'tags')
|
|
429
|
+
|
|
430
|
+
await api.addResource('publisher_tags', {
|
|
431
|
+
schema: {
|
|
432
|
+
id: { type: 'id' },
|
|
433
|
+
publisher_id: { type: 'string', required: true, belongsTo: 'publishers', as: 'publisher' },
|
|
434
|
+
tag_id: { type: 'string', required: true, belongsTo: 'tags', as: 'tag' }
|
|
435
|
+
},
|
|
436
|
+
tableName: `${tablePrefix}_publisher_tags`
|
|
437
|
+
})
|
|
438
|
+
await knex.schema.dropTableIfExists(`${tablePrefix}_publisher_tags`)
|
|
439
|
+
await knex.schema.createTable(`${tablePrefix}_publisher_tags`, (table) => {
|
|
440
|
+
table.increments('id').primary()
|
|
441
|
+
table.string('publisher_id').notNullable()
|
|
442
|
+
table.string('tag_id').notNullable()
|
|
443
|
+
})
|
|
444
|
+
mapTable(`${tablePrefix}_publisher_tags`, 'publisher_tags')
|
|
445
|
+
if (storageMode.isAnyApi()) {
|
|
446
|
+
const descriptorTenant = api.anyapi?.tenantId || tenantId
|
|
447
|
+
const publishersDescriptor = await api.anyapi.registry.getDescriptor(descriptorTenant, 'publishers')
|
|
448
|
+
const tagsDescriptor = await api.anyapi.registry.getDescriptor(descriptorTenant, 'tags')
|
|
449
|
+
const relationshipKey = publishersDescriptor?.manyToMany?.tags?.relationship
|
|
450
|
+
const inverseRelationshipKey = tagsDescriptor?.manyToMany?.publishers?.relationship
|
|
451
|
+
storageMode.registerLink(
|
|
452
|
+
`${tablePrefix}_publisher_tags`,
|
|
453
|
+
'publishers',
|
|
454
|
+
'tags',
|
|
455
|
+
relationshipKey,
|
|
456
|
+
inverseRelationshipKey
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
return api
|
|
462
|
+
} finally {
|
|
463
|
+
if (storageMode.isAnyApi()) {
|
|
464
|
+
storageMode.setCurrentTenant(previousTenant)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
201
469
|
/**
|
|
202
470
|
* Creates a basic API with bulk operations enabled
|
|
203
471
|
*/
|
|
@@ -588,7 +856,6 @@ export async function createPaginationApi (knex, options = {}) {
|
|
|
588
856
|
* Creates an API with WebSocket/Socket.IO support for testing
|
|
589
857
|
*/
|
|
590
858
|
export async function createWebSocketApi (knex, pluginOptions = {}) {
|
|
591
|
-
const { SocketIOPlugin } = await import('../../plugins/core/socketio-plugin.js')
|
|
592
859
|
const { jwtVerify } = await import('jose')
|
|
593
860
|
|
|
594
861
|
const api = await createBasicApi(knex, {
|
|
@@ -1425,6 +1692,148 @@ export async function createAutoFilterApi (knex, pluginOptions = {}) {
|
|
|
1425
1692
|
return api
|
|
1426
1693
|
}
|
|
1427
1694
|
|
|
1695
|
+
export async function createRowPolicyApi (knex, pluginOptions = {}) {
|
|
1696
|
+
const api = new Api({
|
|
1697
|
+
name: 'row-policy-test-api',
|
|
1698
|
+
})
|
|
1699
|
+
|
|
1700
|
+
const tenantId = storageMode.isAnyApi() ? 'row_policy_tenant' : storageMode.defaultTenant
|
|
1701
|
+
const previousTenant = storageMode.currentTenant
|
|
1702
|
+
if (storageMode.isAnyApi()) {
|
|
1703
|
+
storageMode.setCurrentTenant(tenantId)
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
await api.use(RestApiPlugin, {
|
|
1707
|
+
simplifiedApi: false,
|
|
1708
|
+
simplifiedTransport: false,
|
|
1709
|
+
returnRecordApi: {
|
|
1710
|
+
post: true,
|
|
1711
|
+
put: false,
|
|
1712
|
+
patch: false
|
|
1713
|
+
},
|
|
1714
|
+
sortableFields: ['id', 'name', 'title', 'access_group', 'project_id']
|
|
1715
|
+
})
|
|
1716
|
+
|
|
1717
|
+
await useStoragePlugin(api, knex, { tenantId })
|
|
1718
|
+
await resetAnyApiTables(knex)
|
|
1719
|
+
|
|
1720
|
+
const rowPolicyOptions = pluginOptions.rowPolicy || {}
|
|
1721
|
+
await api.use(RowPolicyPlugin, {
|
|
1722
|
+
...rowPolicyOptions,
|
|
1723
|
+
policies: {
|
|
1724
|
+
groupVisibility: ({ query, context, column, value, scopeName, queryPurpose }) => {
|
|
1725
|
+
pluginOptions.onPolicy?.({ scopeName, queryPurpose })
|
|
1726
|
+
|
|
1727
|
+
if (context.visibility?.all === true) {
|
|
1728
|
+
return true
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
const groups = context.visibility?.groups
|
|
1732
|
+
if (!Array.isArray(groups) || groups.length === 0) {
|
|
1733
|
+
return false
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
query.whereIn(
|
|
1737
|
+
column('access_group'),
|
|
1738
|
+
groups.map((group) => value('access_group', group))
|
|
1739
|
+
)
|
|
1740
|
+
return true
|
|
1741
|
+
},
|
|
1742
|
+
...(rowPolicyOptions.policies || {})
|
|
1743
|
+
}
|
|
1744
|
+
})
|
|
1745
|
+
|
|
1746
|
+
await api.addResource('policy_projects', {
|
|
1747
|
+
schema: {
|
|
1748
|
+
id: { type: 'id' },
|
|
1749
|
+
name: { type: 'string', required: true, max: 200 },
|
|
1750
|
+
access_group: { type: 'string', required: true }
|
|
1751
|
+
},
|
|
1752
|
+
relationships: {
|
|
1753
|
+
tasks: { type: 'hasMany', target: 'policy_tasks', foreignKey: 'project_id' },
|
|
1754
|
+
shared_tasks: {
|
|
1755
|
+
type: 'manyToMany',
|
|
1756
|
+
target: 'policy_tasks',
|
|
1757
|
+
through: 'policy_project_tasks',
|
|
1758
|
+
foreignKey: 'project_id',
|
|
1759
|
+
otherKey: 'task_id'
|
|
1760
|
+
}
|
|
1761
|
+
},
|
|
1762
|
+
rowPolicy: 'groupVisibility',
|
|
1763
|
+
tableName: 'row_policy_projects'
|
|
1764
|
+
})
|
|
1765
|
+
await api.resources.policy_projects.createKnexTable()
|
|
1766
|
+
mapTable('row_policy_projects', 'policy_projects')
|
|
1767
|
+
|
|
1768
|
+
await api.addResource('policy_tasks', {
|
|
1769
|
+
schema: {
|
|
1770
|
+
id: { type: 'id' },
|
|
1771
|
+
title: { type: 'string', required: true, max: 200, search: true },
|
|
1772
|
+
access_group: { type: 'string', required: true },
|
|
1773
|
+
project_id: {
|
|
1774
|
+
type: 'number',
|
|
1775
|
+
nullable: true,
|
|
1776
|
+
belongsTo: 'policy_projects',
|
|
1777
|
+
as: 'project',
|
|
1778
|
+
search: true
|
|
1779
|
+
}
|
|
1780
|
+
},
|
|
1781
|
+
relationships: {
|
|
1782
|
+
shared_projects: {
|
|
1783
|
+
type: 'manyToMany',
|
|
1784
|
+
target: 'policy_projects',
|
|
1785
|
+
through: 'policy_project_tasks',
|
|
1786
|
+
foreignKey: 'task_id',
|
|
1787
|
+
otherKey: 'project_id'
|
|
1788
|
+
}
|
|
1789
|
+
},
|
|
1790
|
+
rowPolicy: 'groupVisibility',
|
|
1791
|
+
tableName: 'row_policy_tasks'
|
|
1792
|
+
})
|
|
1793
|
+
await api.resources.policy_tasks.createKnexTable()
|
|
1794
|
+
mapTable('row_policy_tasks', 'policy_tasks')
|
|
1795
|
+
|
|
1796
|
+
await api.addResource('policy_project_tasks', {
|
|
1797
|
+
schema: {
|
|
1798
|
+
id: { type: 'id' },
|
|
1799
|
+
project_id: { type: 'number', required: true, belongsTo: 'policy_projects', as: 'project' },
|
|
1800
|
+
task_id: { type: 'number', required: true, belongsTo: 'policy_tasks', as: 'task' }
|
|
1801
|
+
},
|
|
1802
|
+
tableName: 'row_policy_project_tasks'
|
|
1803
|
+
})
|
|
1804
|
+
await api.resources.policy_project_tasks.createKnexTable()
|
|
1805
|
+
mapTable('row_policy_project_tasks', 'policy_project_tasks')
|
|
1806
|
+
|
|
1807
|
+
if (storageMode.isAnyApi()) {
|
|
1808
|
+
const projectsDescriptor = await api.anyapi.registry.getDescriptor(tenantId, 'policy_projects')
|
|
1809
|
+
const tasksDescriptor = await api.anyapi.registry.getDescriptor(tenantId, 'policy_tasks')
|
|
1810
|
+
storageMode.registerLink(
|
|
1811
|
+
'row_policy_project_tasks',
|
|
1812
|
+
'policy_projects',
|
|
1813
|
+
'shared_tasks',
|
|
1814
|
+
projectsDescriptor?.manyToMany?.shared_tasks?.relationship,
|
|
1815
|
+
tasksDescriptor?.manyToMany?.shared_projects?.relationship
|
|
1816
|
+
)
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
await api.addResource('policy_broken', {
|
|
1820
|
+
schema: {
|
|
1821
|
+
id: { type: 'id' },
|
|
1822
|
+
name: { type: 'string', required: true }
|
|
1823
|
+
},
|
|
1824
|
+
rowPolicy: () => undefined,
|
|
1825
|
+
tableName: 'row_policy_broken'
|
|
1826
|
+
})
|
|
1827
|
+
await api.resources.policy_broken.createKnexTable()
|
|
1828
|
+
mapTable('row_policy_broken', 'policy_broken')
|
|
1829
|
+
|
|
1830
|
+
if (storageMode.isAnyApi()) {
|
|
1831
|
+
storageMode.setCurrentTenant(previousTenant)
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
return api
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1428
1837
|
/**
|
|
1429
1838
|
* Creates an API with positioning support for testing
|
|
1430
1839
|
*/
|
|
@@ -1714,7 +2123,7 @@ export async function createCursorPaginationApi (knex) {
|
|
|
1714
2123
|
put: 'full',
|
|
1715
2124
|
patch: 'minimal'
|
|
1716
2125
|
},
|
|
1717
|
-
sortableFields: ['id', 'name', 'category', 'brand', 'price', 'sku', '
|
|
2126
|
+
sortableFields: ['id', 'name', 'category', 'brand', 'price', 'sku', 'createdAt', 'code', 'status', 'type'],
|
|
1718
2127
|
queryDefaultLimit: 20,
|
|
1719
2128
|
queryMaxLimit: 100,
|
|
1720
2129
|
returnBasePath: 'https://api.example.com'
|
|
@@ -1734,11 +2143,12 @@ export async function createCursorPaginationApi (knex) {
|
|
|
1734
2143
|
price: { type: 'number', required: true },
|
|
1735
2144
|
sku: { type: 'string', required: true, max: 50, unique: true },
|
|
1736
2145
|
status: { type: 'string', max: 50, default: 'active' },
|
|
1737
|
-
|
|
2146
|
+
createdAt: { type: 'dateTime', default: 'now()' }
|
|
1738
2147
|
},
|
|
1739
2148
|
tableName: 'cursor_products'
|
|
1740
2149
|
})
|
|
1741
2150
|
await api.resources.products.createKnexTable()
|
|
2151
|
+
mapTable('cursor_products', 'products')
|
|
1742
2152
|
|
|
1743
2153
|
// Create items table with custom ID property
|
|
1744
2154
|
await api.addResource('items', {
|
|
@@ -1753,6 +2163,7 @@ export async function createCursorPaginationApi (knex) {
|
|
|
1753
2163
|
tableName: 'cursor_items'
|
|
1754
2164
|
})
|
|
1755
2165
|
await api.resources.items.createKnexTable()
|
|
2166
|
+
mapTable('cursor_items', 'items')
|
|
1756
2167
|
|
|
1757
2168
|
return api
|
|
1758
2169
|
}
|
|
@@ -1915,3 +2326,83 @@ export async function createSearchSchemaMergeApi (knex, pluginOptions = {}) {
|
|
|
1915
2326
|
|
|
1916
2327
|
return api
|
|
1917
2328
|
}
|
|
2329
|
+
|
|
2330
|
+
export async function createFileUploadApi (knex, pluginOptions = {}) {
|
|
2331
|
+
const apiName = pluginOptions.apiName || 'file-upload-test-api'
|
|
2332
|
+
const tableName = pluginOptions.tableName || 'file_documents'
|
|
2333
|
+
const tablePrefix = pluginOptions.tablePrefix || 'file'
|
|
2334
|
+
const storage = pluginOptions.storage
|
|
2335
|
+
const detectorState = pluginOptions.detectorState || { payload: null }
|
|
2336
|
+
|
|
2337
|
+
if (!storage) {
|
|
2338
|
+
throw new Error('createFileUploadApi requires a storage adapter')
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
if (storageMode.isAnyApi()) {
|
|
2342
|
+
storageMode.clearRegistry()
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
const api = new Api({
|
|
2346
|
+
name: apiName,
|
|
2347
|
+
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
2348
|
+
})
|
|
2349
|
+
|
|
2350
|
+
const previousTenant = storageMode.currentTenant
|
|
2351
|
+
const tenantId = storageMode.isAnyApi()
|
|
2352
|
+
? (pluginOptions.tenantId || `${tablePrefix}_tenant`)
|
|
2353
|
+
: storageMode.defaultTenant
|
|
2354
|
+
|
|
2355
|
+
if (storageMode.isAnyApi()) {
|
|
2356
|
+
storageMode.setCurrentTenant(tenantId)
|
|
2357
|
+
}
|
|
2358
|
+
|
|
2359
|
+
try {
|
|
2360
|
+
await api.use(RestApiPlugin, {
|
|
2361
|
+
simplifiedApi: false,
|
|
2362
|
+
simplifiedTransport: false,
|
|
2363
|
+
returnRecordApi: {
|
|
2364
|
+
post: true,
|
|
2365
|
+
put: false,
|
|
2366
|
+
patch: false
|
|
2367
|
+
},
|
|
2368
|
+
returnRecordTransport: {
|
|
2369
|
+
post: 'full',
|
|
2370
|
+
put: 'no',
|
|
2371
|
+
patch: 'minimal'
|
|
2372
|
+
},
|
|
2373
|
+
sortableFields: ['id', 'title'],
|
|
2374
|
+
...pluginOptions['rest-api']
|
|
2375
|
+
})
|
|
2376
|
+
await useStoragePlugin(api, knex, { tenantId })
|
|
2377
|
+
await resetAnyApiTables(knex)
|
|
2378
|
+
await api.use(FileHandlingPlugin)
|
|
2379
|
+
|
|
2380
|
+
api.rest.registerFileDetector({
|
|
2381
|
+
name: 'test-file-detector',
|
|
2382
|
+
detect: () => Boolean(detectorState.payload),
|
|
2383
|
+
parse: async () => detectorState.payload
|
|
2384
|
+
})
|
|
2385
|
+
|
|
2386
|
+
await api.addResource('documents', {
|
|
2387
|
+
schema: {
|
|
2388
|
+
id: { type: 'id' },
|
|
2389
|
+
title: { type: 'string', required: true },
|
|
2390
|
+
attachment: {
|
|
2391
|
+
type: 'file',
|
|
2392
|
+
storage,
|
|
2393
|
+
accepts: ['image/png'],
|
|
2394
|
+
maxSize: '1mb'
|
|
2395
|
+
}
|
|
2396
|
+
},
|
|
2397
|
+
tableName
|
|
2398
|
+
})
|
|
2399
|
+
await api.resources.documents.createKnexTable()
|
|
2400
|
+
mapTable(tableName, 'documents')
|
|
2401
|
+
|
|
2402
|
+
return api
|
|
2403
|
+
} finally {
|
|
2404
|
+
if (storageMode.isAnyApi()) {
|
|
2405
|
+
storageMode.setCurrentTenant(previousTenant)
|
|
2406
|
+
}
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { after, before, beforeEach, describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import knexLib from 'knex'
|
|
4
|
+
import { createCamelCaseBelongsToApi } from './fixtures/api-configs.js'
|
|
5
|
+
import {
|
|
6
|
+
createJsonApiDocument,
|
|
7
|
+
createRelationship,
|
|
8
|
+
resourceIdentifier,
|
|
9
|
+
assertResourceRelationship
|
|
10
|
+
} from './helpers/test-utils.js'
|
|
11
|
+
|
|
12
|
+
const knex = knexLib({
|
|
13
|
+
client: 'better-sqlite3',
|
|
14
|
+
connection: {
|
|
15
|
+
filename: ':memory:'
|
|
16
|
+
},
|
|
17
|
+
useNullAsDefault: true
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
let api
|
|
21
|
+
|
|
22
|
+
describe('Full JSON:API belongsTo linkage', () => {
|
|
23
|
+
before(async () => {
|
|
24
|
+
api = await createCamelCaseBelongsToApi(knex)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
after(async () => {
|
|
28
|
+
await knex.destroy()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
beforeEach(async () => {
|
|
32
|
+
await knex('camel_fk_publishers').delete()
|
|
33
|
+
await knex('camel_fk_countries').delete()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const createCountry = async () => {
|
|
37
|
+
return api.resources.countries.post({
|
|
38
|
+
inputRecord: createJsonApiDocument('countries', {
|
|
39
|
+
name: 'United States'
|
|
40
|
+
}),
|
|
41
|
+
simplified: false
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const createPublisher = async (countryId = null) => {
|
|
46
|
+
return api.resources.publishers.post({
|
|
47
|
+
inputRecord: createJsonApiDocument(
|
|
48
|
+
'publishers',
|
|
49
|
+
{ name: 'Mapped Publisher' },
|
|
50
|
+
{
|
|
51
|
+
country: createRelationship(
|
|
52
|
+
countryId == null ? null : resourceIdentifier('countries', countryId)
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
),
|
|
56
|
+
simplified: false
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
it('emits belongsTo linkage on primary resources without include for single and collection responses', async () => {
|
|
61
|
+
const country = await createCountry()
|
|
62
|
+
const publisher = await createPublisher(country.data.id)
|
|
63
|
+
|
|
64
|
+
assertResourceRelationship(
|
|
65
|
+
publisher.data,
|
|
66
|
+
'country',
|
|
67
|
+
resourceIdentifier('countries', country.data.id)
|
|
68
|
+
)
|
|
69
|
+
assert.equal(Object.hasOwn(publisher.data.attributes, 'countryId'), false)
|
|
70
|
+
|
|
71
|
+
const single = await api.resources.publishers.get({
|
|
72
|
+
id: publisher.data.id,
|
|
73
|
+
simplified: false
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
assertResourceRelationship(
|
|
77
|
+
single.data,
|
|
78
|
+
'country',
|
|
79
|
+
resourceIdentifier('countries', country.data.id)
|
|
80
|
+
)
|
|
81
|
+
assert.equal(Object.hasOwn(single.data.attributes, 'countryId'), false)
|
|
82
|
+
|
|
83
|
+
const collection = await api.resources.publishers.query({
|
|
84
|
+
simplified: false
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
assert.equal(collection.data.length, 1)
|
|
88
|
+
assertResourceRelationship(
|
|
89
|
+
collection.data[0],
|
|
90
|
+
'country',
|
|
91
|
+
resourceIdentifier('countries', country.data.id)
|
|
92
|
+
)
|
|
93
|
+
assert.equal(Object.hasOwn(collection.data[0].attributes, 'countryId'), false)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('keeps primary belongsTo linkage when include is requested and adds included resources', async () => {
|
|
97
|
+
const country = await createCountry()
|
|
98
|
+
const publisher = await createPublisher(country.data.id)
|
|
99
|
+
|
|
100
|
+
const result = await api.resources.publishers.get({
|
|
101
|
+
id: publisher.data.id,
|
|
102
|
+
queryParams: {
|
|
103
|
+
include: ['country']
|
|
104
|
+
},
|
|
105
|
+
simplified: false,
|
|
106
|
+
returnFullRecord: 'full'
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
assertResourceRelationship(
|
|
110
|
+
result.data,
|
|
111
|
+
'country',
|
|
112
|
+
resourceIdentifier('countries', country.data.id)
|
|
113
|
+
)
|
|
114
|
+
assert.equal(Object.hasOwn(result.data.attributes, 'countryId'), false)
|
|
115
|
+
assert.equal(result.included?.length, 1)
|
|
116
|
+
assert.equal(result.included?.[0]?.type, 'countries')
|
|
117
|
+
assert.equal(result.included?.[0]?.id, country.data.id)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('emits explicit null belongsTo linkage without exposing the foreign key attribute', async () => {
|
|
121
|
+
const country = await createCountry()
|
|
122
|
+
const publisher = await createPublisher(country.data.id)
|
|
123
|
+
|
|
124
|
+
const patched = await api.resources.publishers.patch({
|
|
125
|
+
id: publisher.data.id,
|
|
126
|
+
inputRecord: {
|
|
127
|
+
data: {
|
|
128
|
+
type: 'publishers',
|
|
129
|
+
id: publisher.data.id,
|
|
130
|
+
relationships: {
|
|
131
|
+
country: { data: null }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
simplified: false,
|
|
136
|
+
returnFullRecord: 'full'
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
assert.equal(patched.data.relationships?.country?.data, null)
|
|
140
|
+
assert.equal(Object.hasOwn(patched.data.attributes, 'countryId'), false)
|
|
141
|
+
|
|
142
|
+
const single = await api.resources.publishers.get({
|
|
143
|
+
id: publisher.data.id,
|
|
144
|
+
simplified: false
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
assert.equal(single.data.relationships?.country?.data, null)
|
|
148
|
+
assert.equal(Object.hasOwn(single.data.attributes, 'countryId'), false)
|
|
149
|
+
})
|
|
150
|
+
})
|