json-rest-api 1.0.24 → 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 +2 -1
- package/docs/API.md +12 -9
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +5 -2
- 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_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/package.json +1 -1
- package/plugins/core/bulk-operations-plugin.js +5 -4
- package/plugins/core/connectors/express-plugin.js +6 -1
- package/plugins/core/connectors/fastify-plugin.js +5 -1
- 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 +1 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +7 -1
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +20 -15
- package/plugins/core/lib/querying/knex-pagination-helpers.js +38 -84
- package/plugins/core/lib/querying/knex-relationship-includes.js +29 -28
- 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/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 +6 -1
- 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/return-record-settings.js +47 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +34 -41
- package/plugins/core/lib/storage/canonical-storage-mapping.js +2 -1
- package/plugins/core/lib/storage/storage-adapter.js +74 -13
- package/plugins/core/lib/writing/many-to-many-manipulations.js +4 -3
- package/plugins/core/lib/writing/relationship-processor.js +10 -1
- package/plugins/core/rest-api-anyapi-knex-plugin.js +210 -97
- package/plugins/core/rest-api-knex-plugin.js +70 -60
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +8 -2
- package/plugins/core/rest-api-plugin-methods/common.js +85 -63
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +23 -13
- package/plugins/core/rest-api-plugin-methods/delete.js +2 -6
- package/plugins/core/rest-api-plugin-methods/get-related.js +15 -13
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +1 -1
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +17 -6
- package/plugins/core/rest-api-plugin-methods/patch.js +3 -6
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +24 -14
- package/plugins/core/rest-api-plugin-methods/post.js +4 -7
- package/plugins/core/rest-api-plugin-methods/put.js +4 -7
- package/plugins/core/rest-api-plugin.js +6 -36
- package/plugins/core/rest-api-row-policy-plugin.js +157 -0
- package/plugins/storage/local-storage.js +44 -19
- package/plugins/storage/s3-storage.js +21 -55
- package/tests/README.md +1 -1
- package/tests/anyapi-cursor-pagination.test.js +7 -1
- package/tests/anyapi-custom-idproperty.test.js +163 -0
- package/tests/fastify-plugin.test.js +28 -0
- package/tests/field-setters.test.js +10 -10
- package/tests/file-handling.test.js +118 -0
- package/tests/fixtures/api-configs.js +362 -4
- package/tests/full-jsonapi-belongsto-linkage.test.js +150 -0
- package/tests/http-connectors-parity.test.js +165 -5
- package/tests/local-storage.test.js +96 -0
- package/tests/pagination-cursor-multifield.test.js +121 -0
- 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 +1 -0
- 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,133 @@ 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
|
+
|
|
201
336
|
/**
|
|
202
337
|
* Creates a small API focused on resource ID normalization behavior.
|
|
203
338
|
*/
|
|
@@ -721,7 +856,6 @@ export async function createPaginationApi (knex, options = {}) {
|
|
|
721
856
|
* Creates an API with WebSocket/Socket.IO support for testing
|
|
722
857
|
*/
|
|
723
858
|
export async function createWebSocketApi (knex, pluginOptions = {}) {
|
|
724
|
-
const { SocketIOPlugin } = await import('../../plugins/core/socketio-plugin.js')
|
|
725
859
|
const { jwtVerify } = await import('jose')
|
|
726
860
|
|
|
727
861
|
const api = await createBasicApi(knex, {
|
|
@@ -1558,6 +1692,148 @@ export async function createAutoFilterApi (knex, pluginOptions = {}) {
|
|
|
1558
1692
|
return api
|
|
1559
1693
|
}
|
|
1560
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
|
+
|
|
1561
1837
|
/**
|
|
1562
1838
|
* Creates an API with positioning support for testing
|
|
1563
1839
|
*/
|
|
@@ -1847,7 +2123,7 @@ export async function createCursorPaginationApi (knex) {
|
|
|
1847
2123
|
put: 'full',
|
|
1848
2124
|
patch: 'minimal'
|
|
1849
2125
|
},
|
|
1850
|
-
sortableFields: ['id', 'name', 'category', 'brand', 'price', 'sku', '
|
|
2126
|
+
sortableFields: ['id', 'name', 'category', 'brand', 'price', 'sku', 'createdAt', 'code', 'status', 'type'],
|
|
1851
2127
|
queryDefaultLimit: 20,
|
|
1852
2128
|
queryMaxLimit: 100,
|
|
1853
2129
|
returnBasePath: 'https://api.example.com'
|
|
@@ -1867,11 +2143,12 @@ export async function createCursorPaginationApi (knex) {
|
|
|
1867
2143
|
price: { type: 'number', required: true },
|
|
1868
2144
|
sku: { type: 'string', required: true, max: 50, unique: true },
|
|
1869
2145
|
status: { type: 'string', max: 50, default: 'active' },
|
|
1870
|
-
|
|
2146
|
+
createdAt: { type: 'dateTime', default: 'now()' }
|
|
1871
2147
|
},
|
|
1872
2148
|
tableName: 'cursor_products'
|
|
1873
2149
|
})
|
|
1874
2150
|
await api.resources.products.createKnexTable()
|
|
2151
|
+
mapTable('cursor_products', 'products')
|
|
1875
2152
|
|
|
1876
2153
|
// Create items table with custom ID property
|
|
1877
2154
|
await api.addResource('items', {
|
|
@@ -1886,6 +2163,7 @@ export async function createCursorPaginationApi (knex) {
|
|
|
1886
2163
|
tableName: 'cursor_items'
|
|
1887
2164
|
})
|
|
1888
2165
|
await api.resources.items.createKnexTable()
|
|
2166
|
+
mapTable('cursor_items', 'items')
|
|
1889
2167
|
|
|
1890
2168
|
return api
|
|
1891
2169
|
}
|
|
@@ -2048,3 +2326,83 @@ export async function createSearchSchemaMergeApi (knex, pluginOptions = {}) {
|
|
|
2048
2326
|
|
|
2049
2327
|
return api
|
|
2050
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
|
+
})
|