json-rest-api 1.0.24 → 1.0.26
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 +605 -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/query/anyapi-query-adapter.js +36 -26
- 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/storage-adapter-utils.js +8 -8
- 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 +383 -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 +489 -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,14 @@
|
|
|
1
1
|
import { Api } from 'hooked-api'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
AutoFilterPlugin,
|
|
4
|
+
FileHandlingPlugin,
|
|
5
|
+
QueryProjectionsPlugin,
|
|
6
|
+
RestApiAnyapiKnexPlugin,
|
|
7
|
+
RestApiKnexPlugin,
|
|
8
|
+
RestApiPlugin,
|
|
9
|
+
RowPolicyPlugin,
|
|
10
|
+
SocketIOPlugin
|
|
11
|
+
} from '../../index.js'
|
|
3
12
|
import { ExpressPlugin } from '../../plugins/core/connectors/express-plugin.js'
|
|
4
13
|
import express from 'express'
|
|
5
14
|
import { createServer } from 'http'
|
|
@@ -198,6 +207,133 @@ export async function createBasicApi (knex, pluginOptions = {}) {
|
|
|
198
207
|
}
|
|
199
208
|
}
|
|
200
209
|
|
|
210
|
+
export async function createReturnRecordApi (knex, pluginOptions = {}) {
|
|
211
|
+
const apiName = pluginOptions.apiName || 'return-record-test-api'
|
|
212
|
+
const tablePrefix = pluginOptions.tablePrefix || 'return'
|
|
213
|
+
if (storageMode.isAnyApi()) {
|
|
214
|
+
storageMode.clearRegistry()
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const api = new Api({
|
|
218
|
+
name: apiName,
|
|
219
|
+
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
const previousTenant = storageMode.currentTenant
|
|
223
|
+
const tenantId = storageMode.isAnyApi()
|
|
224
|
+
? (pluginOptions.tenantId || `${tablePrefix}_tenant`)
|
|
225
|
+
: storageMode.defaultTenant
|
|
226
|
+
if (storageMode.isAnyApi()) {
|
|
227
|
+
storageMode.setCurrentTenant(tenantId)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
await api.use(RestApiPlugin, {
|
|
231
|
+
simplifiedApi: false,
|
|
232
|
+
simplifiedTransport: false,
|
|
233
|
+
returnRecordApi: {
|
|
234
|
+
post: true,
|
|
235
|
+
put: false,
|
|
236
|
+
patch: false
|
|
237
|
+
},
|
|
238
|
+
returnRecordTransport: {
|
|
239
|
+
post: false,
|
|
240
|
+
put: true,
|
|
241
|
+
patch: 'minimal'
|
|
242
|
+
},
|
|
243
|
+
sortableFields: ['id', 'name'],
|
|
244
|
+
...pluginOptions['rest-api']
|
|
245
|
+
})
|
|
246
|
+
await useStoragePlugin(api, knex, { tenantId })
|
|
247
|
+
await resetAnyApiTables(knex)
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
await api.addResource('global_items', {
|
|
251
|
+
schema: {
|
|
252
|
+
id: { type: 'id' },
|
|
253
|
+
name: { type: 'string', required: true, max: 100 }
|
|
254
|
+
},
|
|
255
|
+
tableName: `${tablePrefix}_global_items`
|
|
256
|
+
})
|
|
257
|
+
await api.resources.global_items.createKnexTable()
|
|
258
|
+
mapTable(`${tablePrefix}_global_items`, 'global_items')
|
|
259
|
+
|
|
260
|
+
await api.addResource('scope_items', {
|
|
261
|
+
schema: {
|
|
262
|
+
id: { type: 'id' },
|
|
263
|
+
name: { type: 'string', required: true, max: 100 }
|
|
264
|
+
},
|
|
265
|
+
tableName: `${tablePrefix}_scope_items`,
|
|
266
|
+
returnRecordApi: {
|
|
267
|
+
post: false,
|
|
268
|
+
put: true,
|
|
269
|
+
patch: 'minimal'
|
|
270
|
+
},
|
|
271
|
+
returnRecordTransport: true
|
|
272
|
+
})
|
|
273
|
+
await api.resources.scope_items.createKnexTable()
|
|
274
|
+
mapTable(`${tablePrefix}_scope_items`, 'scope_items')
|
|
275
|
+
} finally {
|
|
276
|
+
if (storageMode.isAnyApi()) {
|
|
277
|
+
storageMode.setCurrentTenant(previousTenant)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return api
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Creates a small API that uses logical camelCase belongsTo fields with default snake_case storage mapping.
|
|
286
|
+
* This reproduces the full JSON:API linkage path where storage column names differ from logical field names.
|
|
287
|
+
*/
|
|
288
|
+
export async function createCamelCaseBelongsToApi (knex, pluginOptions = {}) {
|
|
289
|
+
const apiName = pluginOptions.apiName || 'camelcase-belongsto-test-api'
|
|
290
|
+
const tablePrefix = pluginOptions.tablePrefix || 'camel_fk'
|
|
291
|
+
|
|
292
|
+
const api = new Api({
|
|
293
|
+
name: apiName,
|
|
294
|
+
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
await api.use(RestApiPlugin, {
|
|
298
|
+
simplifiedApi: false,
|
|
299
|
+
simplifiedTransport: false,
|
|
300
|
+
returnRecordApi: {
|
|
301
|
+
post: true,
|
|
302
|
+
put: false,
|
|
303
|
+
patch: false
|
|
304
|
+
},
|
|
305
|
+
returnRecordTransport: {
|
|
306
|
+
post: 'full',
|
|
307
|
+
put: 'no',
|
|
308
|
+
patch: 'full'
|
|
309
|
+
},
|
|
310
|
+
...(pluginOptions['rest-api'] || {})
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
await api.use(RestApiKnexPlugin, { knex })
|
|
314
|
+
|
|
315
|
+
await api.addResource('countries', {
|
|
316
|
+
schema: {
|
|
317
|
+
id: { type: 'id' },
|
|
318
|
+
name: { type: 'string', required: true, max: 100 }
|
|
319
|
+
},
|
|
320
|
+
tableName: `${tablePrefix}_countries`
|
|
321
|
+
})
|
|
322
|
+
await api.resources.countries.createKnexTable()
|
|
323
|
+
|
|
324
|
+
await api.addResource('publishers', {
|
|
325
|
+
schema: {
|
|
326
|
+
id: { type: 'id' },
|
|
327
|
+
name: { type: 'string', required: true, max: 200 },
|
|
328
|
+
countryId: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' }
|
|
329
|
+
},
|
|
330
|
+
tableName: `${tablePrefix}_publishers`
|
|
331
|
+
})
|
|
332
|
+
await api.resources.publishers.createKnexTable()
|
|
333
|
+
|
|
334
|
+
return api
|
|
335
|
+
}
|
|
336
|
+
|
|
201
337
|
/**
|
|
202
338
|
* Creates a small API focused on resource ID normalization behavior.
|
|
203
339
|
*/
|
|
@@ -721,7 +857,6 @@ export async function createPaginationApi (knex, options = {}) {
|
|
|
721
857
|
* Creates an API with WebSocket/Socket.IO support for testing
|
|
722
858
|
*/
|
|
723
859
|
export async function createWebSocketApi (knex, pluginOptions = {}) {
|
|
724
|
-
const { SocketIOPlugin } = await import('../../plugins/core/socketio-plugin.js')
|
|
725
860
|
const { jwtVerify } = await import('jose')
|
|
726
861
|
|
|
727
862
|
const api = await createBasicApi(knex, {
|
|
@@ -1558,6 +1693,168 @@ export async function createAutoFilterApi (knex, pluginOptions = {}) {
|
|
|
1558
1693
|
return api
|
|
1559
1694
|
}
|
|
1560
1695
|
|
|
1696
|
+
export async function createRowPolicyApi (knex, pluginOptions = {}) {
|
|
1697
|
+
const api = new Api({
|
|
1698
|
+
name: 'row-policy-test-api',
|
|
1699
|
+
})
|
|
1700
|
+
|
|
1701
|
+
const tenantId = storageMode.isAnyApi() ? 'row_policy_tenant' : storageMode.defaultTenant
|
|
1702
|
+
const previousTenant = storageMode.currentTenant
|
|
1703
|
+
if (storageMode.isAnyApi()) {
|
|
1704
|
+
storageMode.setCurrentTenant(tenantId)
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
await api.use(RestApiPlugin, {
|
|
1708
|
+
simplifiedApi: false,
|
|
1709
|
+
simplifiedTransport: false,
|
|
1710
|
+
returnRecordApi: {
|
|
1711
|
+
post: true,
|
|
1712
|
+
put: false,
|
|
1713
|
+
patch: false
|
|
1714
|
+
},
|
|
1715
|
+
sortableFields: ['id', 'name', 'title', 'access_group', 'project_id']
|
|
1716
|
+
})
|
|
1717
|
+
|
|
1718
|
+
await useStoragePlugin(api, knex, { tenantId })
|
|
1719
|
+
await resetAnyApiTables(knex)
|
|
1720
|
+
|
|
1721
|
+
const rowPolicyOptions = pluginOptions.rowPolicy || {}
|
|
1722
|
+
await api.use(RowPolicyPlugin, {
|
|
1723
|
+
...rowPolicyOptions,
|
|
1724
|
+
policies: {
|
|
1725
|
+
groupVisibility: ({ query, context, column, value, scopeName, queryPurpose }) => {
|
|
1726
|
+
pluginOptions.onPolicy?.({ scopeName, queryPurpose })
|
|
1727
|
+
|
|
1728
|
+
if (context.visibility?.all === true) {
|
|
1729
|
+
return true
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
const groups = context.visibility?.groups
|
|
1733
|
+
if (!Array.isArray(groups) || groups.length === 0) {
|
|
1734
|
+
return false
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
query.where(function visibilityGrants () {
|
|
1738
|
+
this.whereRaw('1 = 0')
|
|
1739
|
+
this.orWhere(function groupVisibilityGrant () {
|
|
1740
|
+
this.whereIn(
|
|
1741
|
+
column('access_group'),
|
|
1742
|
+
groups.map((group) => value('access_group', group))
|
|
1743
|
+
)
|
|
1744
|
+
})
|
|
1745
|
+
})
|
|
1746
|
+
return true
|
|
1747
|
+
},
|
|
1748
|
+
...(rowPolicyOptions.policies || {})
|
|
1749
|
+
}
|
|
1750
|
+
})
|
|
1751
|
+
|
|
1752
|
+
await api.use(AutoFilterPlugin, {
|
|
1753
|
+
resolvers: {
|
|
1754
|
+
workspace: ({ context }) => context.scopeValues?.workspaceId
|
|
1755
|
+
},
|
|
1756
|
+
presets: {
|
|
1757
|
+
workspace: {
|
|
1758
|
+
filters: [{ field: 'workspace_id', resolver: 'workspace' }]
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
})
|
|
1762
|
+
|
|
1763
|
+
await api.addResource('policy_projects', {
|
|
1764
|
+
schema: {
|
|
1765
|
+
id: { type: 'id' },
|
|
1766
|
+
name: { type: 'string', required: true, max: 200 },
|
|
1767
|
+
access_group: { type: 'string', required: true },
|
|
1768
|
+
workspace_id: { type: 'string', required: true }
|
|
1769
|
+
},
|
|
1770
|
+
relationships: {
|
|
1771
|
+
tasks: { type: 'hasMany', target: 'policy_tasks', foreignKey: 'project_id' },
|
|
1772
|
+
shared_tasks: {
|
|
1773
|
+
type: 'manyToMany',
|
|
1774
|
+
target: 'policy_tasks',
|
|
1775
|
+
through: 'policy_project_tasks',
|
|
1776
|
+
foreignKey: 'project_id',
|
|
1777
|
+
otherKey: 'task_id'
|
|
1778
|
+
}
|
|
1779
|
+
},
|
|
1780
|
+
autofilter: 'workspace',
|
|
1781
|
+
rowPolicy: 'groupVisibility',
|
|
1782
|
+
tableName: 'row_policy_projects'
|
|
1783
|
+
})
|
|
1784
|
+
await api.resources.policy_projects.createKnexTable()
|
|
1785
|
+
mapTable('row_policy_projects', 'policy_projects')
|
|
1786
|
+
|
|
1787
|
+
await api.addResource('policy_tasks', {
|
|
1788
|
+
schema: {
|
|
1789
|
+
id: { type: 'id' },
|
|
1790
|
+
title: { type: 'string', required: true, max: 200, search: true },
|
|
1791
|
+
access_group: { type: 'string', required: true },
|
|
1792
|
+
workspace_id: { type: 'string', required: true },
|
|
1793
|
+
project_id: {
|
|
1794
|
+
type: 'number',
|
|
1795
|
+
nullable: true,
|
|
1796
|
+
belongsTo: 'policy_projects',
|
|
1797
|
+
as: 'project',
|
|
1798
|
+
search: true
|
|
1799
|
+
}
|
|
1800
|
+
},
|
|
1801
|
+
relationships: {
|
|
1802
|
+
shared_projects: {
|
|
1803
|
+
type: 'manyToMany',
|
|
1804
|
+
target: 'policy_projects',
|
|
1805
|
+
through: 'policy_project_tasks',
|
|
1806
|
+
foreignKey: 'task_id',
|
|
1807
|
+
otherKey: 'project_id'
|
|
1808
|
+
}
|
|
1809
|
+
},
|
|
1810
|
+
autofilter: 'workspace',
|
|
1811
|
+
rowPolicy: 'groupVisibility',
|
|
1812
|
+
tableName: 'row_policy_tasks'
|
|
1813
|
+
})
|
|
1814
|
+
await api.resources.policy_tasks.createKnexTable()
|
|
1815
|
+
mapTable('row_policy_tasks', 'policy_tasks')
|
|
1816
|
+
|
|
1817
|
+
await api.addResource('policy_project_tasks', {
|
|
1818
|
+
schema: {
|
|
1819
|
+
id: { type: 'id' },
|
|
1820
|
+
project_id: { type: 'number', required: true, belongsTo: 'policy_projects', as: 'project' },
|
|
1821
|
+
task_id: { type: 'number', required: true, belongsTo: 'policy_tasks', as: 'task' }
|
|
1822
|
+
},
|
|
1823
|
+
tableName: 'row_policy_project_tasks'
|
|
1824
|
+
})
|
|
1825
|
+
await api.resources.policy_project_tasks.createKnexTable()
|
|
1826
|
+
mapTable('row_policy_project_tasks', 'policy_project_tasks')
|
|
1827
|
+
|
|
1828
|
+
if (storageMode.isAnyApi()) {
|
|
1829
|
+
const projectsDescriptor = await api.anyapi.registry.getDescriptor(tenantId, 'policy_projects')
|
|
1830
|
+
const tasksDescriptor = await api.anyapi.registry.getDescriptor(tenantId, 'policy_tasks')
|
|
1831
|
+
storageMode.registerLink(
|
|
1832
|
+
'row_policy_project_tasks',
|
|
1833
|
+
'policy_projects',
|
|
1834
|
+
'shared_tasks',
|
|
1835
|
+
projectsDescriptor?.manyToMany?.shared_tasks?.relationship,
|
|
1836
|
+
tasksDescriptor?.manyToMany?.shared_projects?.relationship
|
|
1837
|
+
)
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
await api.addResource('policy_broken', {
|
|
1841
|
+
schema: {
|
|
1842
|
+
id: { type: 'id' },
|
|
1843
|
+
name: { type: 'string', required: true }
|
|
1844
|
+
},
|
|
1845
|
+
rowPolicy: () => undefined,
|
|
1846
|
+
tableName: 'row_policy_broken'
|
|
1847
|
+
})
|
|
1848
|
+
await api.resources.policy_broken.createKnexTable()
|
|
1849
|
+
mapTable('row_policy_broken', 'policy_broken')
|
|
1850
|
+
|
|
1851
|
+
if (storageMode.isAnyApi()) {
|
|
1852
|
+
storageMode.setCurrentTenant(previousTenant)
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
return api
|
|
1856
|
+
}
|
|
1857
|
+
|
|
1561
1858
|
/**
|
|
1562
1859
|
* Creates an API with positioning support for testing
|
|
1563
1860
|
*/
|
|
@@ -1847,7 +2144,7 @@ export async function createCursorPaginationApi (knex) {
|
|
|
1847
2144
|
put: 'full',
|
|
1848
2145
|
patch: 'minimal'
|
|
1849
2146
|
},
|
|
1850
|
-
sortableFields: ['id', 'name', 'category', 'brand', 'price', 'sku', '
|
|
2147
|
+
sortableFields: ['id', 'name', 'category', 'brand', 'price', 'sku', 'createdAt', 'code', 'status', 'type'],
|
|
1851
2148
|
queryDefaultLimit: 20,
|
|
1852
2149
|
queryMaxLimit: 100,
|
|
1853
2150
|
returnBasePath: 'https://api.example.com'
|
|
@@ -1867,11 +2164,12 @@ export async function createCursorPaginationApi (knex) {
|
|
|
1867
2164
|
price: { type: 'number', required: true },
|
|
1868
2165
|
sku: { type: 'string', required: true, max: 50, unique: true },
|
|
1869
2166
|
status: { type: 'string', max: 50, default: 'active' },
|
|
1870
|
-
|
|
2167
|
+
createdAt: { type: 'dateTime', default: 'now()' }
|
|
1871
2168
|
},
|
|
1872
2169
|
tableName: 'cursor_products'
|
|
1873
2170
|
})
|
|
1874
2171
|
await api.resources.products.createKnexTable()
|
|
2172
|
+
mapTable('cursor_products', 'products')
|
|
1875
2173
|
|
|
1876
2174
|
// Create items table with custom ID property
|
|
1877
2175
|
await api.addResource('items', {
|
|
@@ -1886,6 +2184,7 @@ export async function createCursorPaginationApi (knex) {
|
|
|
1886
2184
|
tableName: 'cursor_items'
|
|
1887
2185
|
})
|
|
1888
2186
|
await api.resources.items.createKnexTable()
|
|
2187
|
+
mapTable('cursor_items', 'items')
|
|
1889
2188
|
|
|
1890
2189
|
return api
|
|
1891
2190
|
}
|
|
@@ -2048,3 +2347,83 @@ export async function createSearchSchemaMergeApi (knex, pluginOptions = {}) {
|
|
|
2048
2347
|
|
|
2049
2348
|
return api
|
|
2050
2349
|
}
|
|
2350
|
+
|
|
2351
|
+
export async function createFileUploadApi (knex, pluginOptions = {}) {
|
|
2352
|
+
const apiName = pluginOptions.apiName || 'file-upload-test-api'
|
|
2353
|
+
const tableName = pluginOptions.tableName || 'file_documents'
|
|
2354
|
+
const tablePrefix = pluginOptions.tablePrefix || 'file'
|
|
2355
|
+
const storage = pluginOptions.storage
|
|
2356
|
+
const detectorState = pluginOptions.detectorState || { payload: null }
|
|
2357
|
+
|
|
2358
|
+
if (!storage) {
|
|
2359
|
+
throw new Error('createFileUploadApi requires a storage adapter')
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
if (storageMode.isAnyApi()) {
|
|
2363
|
+
storageMode.clearRegistry()
|
|
2364
|
+
}
|
|
2365
|
+
|
|
2366
|
+
const api = new Api({
|
|
2367
|
+
name: apiName,
|
|
2368
|
+
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
2369
|
+
})
|
|
2370
|
+
|
|
2371
|
+
const previousTenant = storageMode.currentTenant
|
|
2372
|
+
const tenantId = storageMode.isAnyApi()
|
|
2373
|
+
? (pluginOptions.tenantId || `${tablePrefix}_tenant`)
|
|
2374
|
+
: storageMode.defaultTenant
|
|
2375
|
+
|
|
2376
|
+
if (storageMode.isAnyApi()) {
|
|
2377
|
+
storageMode.setCurrentTenant(tenantId)
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
try {
|
|
2381
|
+
await api.use(RestApiPlugin, {
|
|
2382
|
+
simplifiedApi: false,
|
|
2383
|
+
simplifiedTransport: false,
|
|
2384
|
+
returnRecordApi: {
|
|
2385
|
+
post: true,
|
|
2386
|
+
put: false,
|
|
2387
|
+
patch: false
|
|
2388
|
+
},
|
|
2389
|
+
returnRecordTransport: {
|
|
2390
|
+
post: 'full',
|
|
2391
|
+
put: 'no',
|
|
2392
|
+
patch: 'minimal'
|
|
2393
|
+
},
|
|
2394
|
+
sortableFields: ['id', 'title'],
|
|
2395
|
+
...pluginOptions['rest-api']
|
|
2396
|
+
})
|
|
2397
|
+
await useStoragePlugin(api, knex, { tenantId })
|
|
2398
|
+
await resetAnyApiTables(knex)
|
|
2399
|
+
await api.use(FileHandlingPlugin)
|
|
2400
|
+
|
|
2401
|
+
api.rest.registerFileDetector({
|
|
2402
|
+
name: 'test-file-detector',
|
|
2403
|
+
detect: () => Boolean(detectorState.payload),
|
|
2404
|
+
parse: async () => detectorState.payload
|
|
2405
|
+
})
|
|
2406
|
+
|
|
2407
|
+
await api.addResource('documents', {
|
|
2408
|
+
schema: {
|
|
2409
|
+
id: { type: 'id' },
|
|
2410
|
+
title: { type: 'string', required: true },
|
|
2411
|
+
attachment: {
|
|
2412
|
+
type: 'file',
|
|
2413
|
+
storage,
|
|
2414
|
+
accepts: ['image/png'],
|
|
2415
|
+
maxSize: '1mb'
|
|
2416
|
+
}
|
|
2417
|
+
},
|
|
2418
|
+
tableName
|
|
2419
|
+
})
|
|
2420
|
+
await api.resources.documents.createKnexTable()
|
|
2421
|
+
mapTable(tableName, 'documents')
|
|
2422
|
+
|
|
2423
|
+
return api
|
|
2424
|
+
} finally {
|
|
2425
|
+
if (storageMode.isAnyApi()) {
|
|
2426
|
+
storageMode.setCurrentTenant(previousTenant)
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
@@ -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
|
+
})
|