json-rest-api 1.0.21 → 1.0.23
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/CLAUDE.md +1 -2
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +180 -0
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +172 -0
- package/docs/API.md +51 -14
- package/docs/COMPARISON.md +2 -2
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +28 -1
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +5 -1
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +3 -1
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +12 -60
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +2 -2
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -1
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +2 -0
- package/docs/GUIDE/GUIDE_X_Autofiltering.md +355 -0
- package/docs/GUIDE/GUIDE_X_Fastify.md +78 -0
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +250 -0
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +159 -0
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +199 -0
- package/docs/GUIDE/index.md +18 -9
- package/docs/QUICKSTART.md +5 -1
- package/docs/index.md +1 -1
- package/index.js +3 -2
- package/package.json +4 -4
- package/plugins/core/connectors/express-plugin.js +97 -184
- package/plugins/core/connectors/fastify-plugin.js +206 -0
- package/plugins/core/connectors/lib/connector-core.js +195 -0
- package/plugins/core/connectors/lib/transport-http-helpers.js +122 -0
- package/plugins/core/connectors/lib/transport-route-schemas.js +92 -0
- package/plugins/core/lib/dbIntrospection.js +1078 -0
- package/plugins/core/lib/dbTablesOperations.js +1104 -160
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +5 -2
- package/plugins/core/lib/querying/knex-process-includes.js +3 -4
- package/plugins/core/lib/querying/knex-query-helpers.js +17 -71
- package/plugins/core/lib/querying/knex-relationship-includes.js +466 -206
- package/plugins/core/lib/querying/query-builder-utils.js +19 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +135 -0
- package/plugins/core/lib/querying/sort-helpers.js +33 -0
- package/plugins/core/lib/querying/storage-adapter-utils.js +51 -0
- package/plugins/core/lib/querying/url-helpers.js +8 -2
- package/plugins/core/lib/querying-writing/compile-schemas.js +12 -1
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +83 -13
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +13 -12
- package/plugins/core/lib/querying-writing/query-field-helpers.js +88 -0
- package/plugins/core/lib/querying-writing/request-contracts.js +590 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +113 -0
- package/plugins/core/lib/storage/storage-adapter.js +79 -2
- package/plugins/core/lib/storage/storage-mapping.js +204 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +355 -228
- package/plugins/core/rest-api-autofilter-plugin.js +414 -0
- package/plugins/core/rest-api-knex-plugin.js +249 -211
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +10 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +12 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +41 -0
- package/plugins/core/rest-api-plugin-methods/common.js +121 -83
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +6 -1
- package/plugins/core/rest-api-plugin-methods/get.js +23 -14
- package/plugins/core/rest-api-plugin-methods/patch.js +28 -12
- package/plugins/core/rest-api-plugin-methods/post.js +13 -8
- package/plugins/core/rest-api-plugin-methods/put.js +45 -21
- package/plugins/core/rest-api-plugin-methods/query.js +15 -53
- package/plugins/core/rest-api-plugin.js +2 -0
- package/plugins/core/rest-api-positioning-plugin.js +1 -1
- package/plugins/core/rest-api-query-projections-plugin.js +130 -0
- package/plugins/core/socketio-plugin.js +1 -3
- package/tests/autofilter.test.js +463 -0
- package/tests/custom-id-simplified-post.test.js +75 -0
- package/tests/custom-idproperty-relationships.test.js +33 -0
- package/tests/db-introspection.test.js +518 -0
- package/tests/db-table-operations.test.js +781 -0
- package/tests/fastify-plugin.test.js +141 -0
- package/tests/field-getters.test.js +1 -2
- package/tests/field-setters.test.js +0 -4
- package/tests/fixtures/api-configs.js +188 -219
- package/tests/helpers/fake-fastify.js +99 -0
- package/tests/http-connectors-parity.test.js +235 -0
- package/tests/projected-fields.test.js +319 -0
- package/tests/relationships.test.js +3 -3
- package/tests/searchschema-knex-behavior.test.js +187 -0
- package/tests/searchschema-merge.test.js +21 -69
- package/tests/storage-mapping.test.js +210 -0
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +0 -707
- package/docs/GUIDE/GUIDE_X_Multihome.md +0 -666
- package/plugins/core/lib/querying-writing/payload-validators.js +0 -1314
- package/plugins/core/rest-api-access.js +0 -459
- package/plugins/core/rest-api-multihome-plugin.js +0 -314
- package/tests/multihome.test.js +0 -553
- package/tests/multihome.test.js.bak +0 -550
- package/tests/ownership-optout.test.js +0 -76
- package/tests/rest-api-access.test.js +0 -206
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Api } from 'hooked-api'
|
|
2
|
-
import { RestApiPlugin, RestApiKnexPlugin, RestApiAnyapiKnexPlugin } from '../../index.js'
|
|
3
|
-
import { AccessPlugin } from '../../plugins/core/rest-api-access.js'
|
|
2
|
+
import { RestApiPlugin, RestApiKnexPlugin, RestApiAnyapiKnexPlugin, QueryProjectionsPlugin } from '../../index.js'
|
|
4
3
|
import { ExpressPlugin } from '../../plugins/core/connectors/express-plugin.js'
|
|
5
4
|
import express from 'express'
|
|
6
5
|
import { createServer } from 'http'
|
|
@@ -126,7 +125,8 @@ export async function createBasicApi (knex, pluginOptions = {}) {
|
|
|
126
125
|
country_id: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' }
|
|
127
126
|
},
|
|
128
127
|
relationships: {
|
|
129
|
-
books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' }
|
|
128
|
+
books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' },
|
|
129
|
+
authors: { type: 'hasMany', target: 'authors', foreignKey: 'publisher_id' }
|
|
130
130
|
},
|
|
131
131
|
tableName: `${tablePrefix}_publishers`
|
|
132
132
|
})
|
|
@@ -137,7 +137,8 @@ export async function createBasicApi (knex, pluginOptions = {}) {
|
|
|
137
137
|
await api.addResource('authors', {
|
|
138
138
|
schema: {
|
|
139
139
|
id: { type: 'id' },
|
|
140
|
-
name: { type: 'string', required: true, max: 200 }
|
|
140
|
+
name: { type: 'string', required: true, max: 200 },
|
|
141
|
+
publisher_id: { type: 'number', nullable: true, belongsTo: 'publishers', as: 'publisher' }
|
|
141
142
|
},
|
|
142
143
|
relationships: {
|
|
143
144
|
books: { type: 'manyToMany', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' }
|
|
@@ -197,113 +198,6 @@ export async function createBasicApi (knex, pluginOptions = {}) {
|
|
|
197
198
|
}
|
|
198
199
|
}
|
|
199
200
|
|
|
200
|
-
export async function createAccessControlApi (knex, pluginOptions = {}) {
|
|
201
|
-
const apiName = pluginOptions.apiName || 'access-test-api'
|
|
202
|
-
const tablePrefix = pluginOptions.tablePrefix || 'access'
|
|
203
|
-
if (storageMode.isAnyApi()) {
|
|
204
|
-
storageMode.clearRegistry()
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const api = new Api({
|
|
208
|
-
name: apiName,
|
|
209
|
-
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
const previousTenant = storageMode.currentTenant
|
|
213
|
-
const tenantId = storageMode.isAnyApi()
|
|
214
|
-
? (pluginOptions.tenantId || `${tablePrefix}_tenant`)
|
|
215
|
-
: storageMode.defaultTenant
|
|
216
|
-
if (storageMode.isAnyApi()) {
|
|
217
|
-
storageMode.setCurrentTenant(tenantId)
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
await api.use(RestApiPlugin, {
|
|
221
|
-
simplifiedApi: false,
|
|
222
|
-
simplifiedTransport: false,
|
|
223
|
-
returnRecordApi: {
|
|
224
|
-
post: true,
|
|
225
|
-
put: false,
|
|
226
|
-
patch: false
|
|
227
|
-
},
|
|
228
|
-
returnRecordTransport: {
|
|
229
|
-
post: 'full',
|
|
230
|
-
put: 'no',
|
|
231
|
-
patch: 'minimal'
|
|
232
|
-
},
|
|
233
|
-
sortableFields: ['id', 'name', 'user_id']
|
|
234
|
-
})
|
|
235
|
-
await useStoragePlugin(api, knex, { tenantId })
|
|
236
|
-
await resetAnyApiTables(knex)
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
await api.use(AccessPlugin, pluginOptions.access || {})
|
|
240
|
-
|
|
241
|
-
await api.addResource('users', {
|
|
242
|
-
schema: {
|
|
243
|
-
id: { type: 'id' },
|
|
244
|
-
email: { type: 'string', required: true },
|
|
245
|
-
display_name: { type: 'string', nullable: true }
|
|
246
|
-
},
|
|
247
|
-
tableName: `${tablePrefix}_users`
|
|
248
|
-
})
|
|
249
|
-
await api.resources.users.createKnexTable()
|
|
250
|
-
mapTable(`${tablePrefix}_users`, 'users')
|
|
251
|
-
|
|
252
|
-
await api.addResource('projects', {
|
|
253
|
-
schema: {
|
|
254
|
-
id: { type: 'id' },
|
|
255
|
-
name: { type: 'string', required: true },
|
|
256
|
-
status: { type: 'string', nullable: true },
|
|
257
|
-
user_id: { type: 'number', belongsTo: 'users', as: 'user', nullable: true }
|
|
258
|
-
},
|
|
259
|
-
relationships: {
|
|
260
|
-
user: { type: 'belongsTo', target: 'users', foreignKey: 'user_id' }
|
|
261
|
-
},
|
|
262
|
-
auth: {
|
|
263
|
-
query: ['authenticated'],
|
|
264
|
-
get: ['owns'],
|
|
265
|
-
post: ['authenticated'],
|
|
266
|
-
patch: ['owns'],
|
|
267
|
-
delete: ['owns']
|
|
268
|
-
},
|
|
269
|
-
ownership: true,
|
|
270
|
-
tableName: `${tablePrefix}_projects`
|
|
271
|
-
})
|
|
272
|
-
await api.resources.projects.createKnexTable()
|
|
273
|
-
mapTable(`${tablePrefix}_projects`, 'projects')
|
|
274
|
-
|
|
275
|
-
await api.addResource('notes', {
|
|
276
|
-
schema: {
|
|
277
|
-
id: { type: 'id' },
|
|
278
|
-
project_id: { type: 'number', belongsTo: 'projects', as: 'project', nullable: true },
|
|
279
|
-
body: { type: 'string', required: true },
|
|
280
|
-
user_id: { type: 'number', belongsTo: 'users', as: 'user', nullable: true }
|
|
281
|
-
},
|
|
282
|
-
relationships: {
|
|
283
|
-
project: { type: 'belongsTo', target: 'projects', foreignKey: 'project_id' },
|
|
284
|
-
user: { type: 'belongsTo', target: 'users', foreignKey: 'user_id' }
|
|
285
|
-
},
|
|
286
|
-
auth: {
|
|
287
|
-
query: ['authenticated'],
|
|
288
|
-
get: ['owns'],
|
|
289
|
-
post: ['authenticated'],
|
|
290
|
-
patch: ['owns'],
|
|
291
|
-
delete: ['owns']
|
|
292
|
-
},
|
|
293
|
-
ownership: true,
|
|
294
|
-
tableName: `${tablePrefix}_notes`
|
|
295
|
-
})
|
|
296
|
-
await api.resources.notes.createKnexTable()
|
|
297
|
-
mapTable(`${tablePrefix}_notes`, 'notes')
|
|
298
|
-
|
|
299
|
-
return api
|
|
300
|
-
} finally {
|
|
301
|
-
if (storageMode.isAnyApi()) {
|
|
302
|
-
storageMode.setCurrentTenant(previousTenant)
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
201
|
/**
|
|
308
202
|
* Creates a basic API with bulk operations enabled
|
|
309
203
|
*/
|
|
@@ -1071,6 +965,92 @@ export async function createFieldGettersApi (knex, pluginOptions = {}) {
|
|
|
1071
965
|
return api
|
|
1072
966
|
}
|
|
1073
967
|
|
|
968
|
+
export async function createProjectedFieldsApi (knex, pluginOptions = {}) {
|
|
969
|
+
const api = new Api({
|
|
970
|
+
name: 'projected-fields-test-api',
|
|
971
|
+
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
972
|
+
})
|
|
973
|
+
|
|
974
|
+
const tenantId = storageMode.isAnyApi() ? 'projected_fields_tenant' : storageMode.defaultTenant
|
|
975
|
+
const previousTenant = storageMode.currentTenant
|
|
976
|
+
if (storageMode.isAnyApi()) {
|
|
977
|
+
storageMode.setCurrentTenant(tenantId)
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
await api.use(RestApiPlugin, {
|
|
981
|
+
simplifiedApi: false,
|
|
982
|
+
simplifiedTransport: false,
|
|
983
|
+
returnRecordApi: {
|
|
984
|
+
post: true,
|
|
985
|
+
put: false,
|
|
986
|
+
patch: false
|
|
987
|
+
},
|
|
988
|
+
returnRecordTransport: {
|
|
989
|
+
post: 'full',
|
|
990
|
+
put: 'no',
|
|
991
|
+
patch: 'minimal'
|
|
992
|
+
},
|
|
993
|
+
sortableFields: ['id', 'first_name', 'last_name'],
|
|
994
|
+
...pluginOptions['rest-api']
|
|
995
|
+
})
|
|
996
|
+
await api.use(QueryProjectionsPlugin)
|
|
997
|
+
|
|
998
|
+
await useStoragePlugin(api, knex, { tenantId })
|
|
999
|
+
await resetAnyApiTables(knex)
|
|
1000
|
+
|
|
1001
|
+
await api.addResource('authors', {
|
|
1002
|
+
schema: {
|
|
1003
|
+
id: { type: 'id' },
|
|
1004
|
+
first_name: { type: 'string', required: true, max: 100 },
|
|
1005
|
+
last_name: { type: 'string', required: true, max: 100 }
|
|
1006
|
+
},
|
|
1007
|
+
queryFields: {
|
|
1008
|
+
full_name: {
|
|
1009
|
+
type: 'string',
|
|
1010
|
+
sortable: true,
|
|
1011
|
+
select: ({ knex, column }) => knex.raw(
|
|
1012
|
+
"trim(coalesce(??, '') || ' ' || coalesce(??, ''))",
|
|
1013
|
+
[column('first_name'), column('last_name')]
|
|
1014
|
+
)
|
|
1015
|
+
},
|
|
1016
|
+
sort_name: {
|
|
1017
|
+
type: 'string',
|
|
1018
|
+
sortable: true,
|
|
1019
|
+
normallyHidden: true,
|
|
1020
|
+
select: ({ knex, column }) => knex.raw(
|
|
1021
|
+
"trim(coalesce(??, '') || ' ' || coalesce(??, ''))",
|
|
1022
|
+
[column('first_name'), column('last_name')]
|
|
1023
|
+
)
|
|
1024
|
+
}
|
|
1025
|
+
},
|
|
1026
|
+
relationships: {
|
|
1027
|
+
books: { type: 'hasMany', target: 'books', foreignKey: 'author_id' }
|
|
1028
|
+
},
|
|
1029
|
+
sortableFields: ['id', 'first_name', 'last_name'],
|
|
1030
|
+
defaultSort: ['full_name'],
|
|
1031
|
+
tableName: 'projected_authors'
|
|
1032
|
+
})
|
|
1033
|
+
await api.resources.authors.createKnexTable()
|
|
1034
|
+
mapTable('projected_authors', 'authors')
|
|
1035
|
+
|
|
1036
|
+
await api.addResource('books', {
|
|
1037
|
+
schema: {
|
|
1038
|
+
id: { type: 'id' },
|
|
1039
|
+
title: { type: 'string', required: true, max: 200 },
|
|
1040
|
+
author_id: { type: 'id', required: true, belongsTo: 'authors', as: 'author' }
|
|
1041
|
+
},
|
|
1042
|
+
tableName: 'projected_books'
|
|
1043
|
+
})
|
|
1044
|
+
await api.resources.books.createKnexTable()
|
|
1045
|
+
mapTable('projected_books', 'books')
|
|
1046
|
+
|
|
1047
|
+
if (storageMode.isAnyApi()) {
|
|
1048
|
+
storageMode.setCurrentTenant(previousTenant)
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
return api
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1074
1054
|
/**
|
|
1075
1055
|
* Creates an API with field setters for testing
|
|
1076
1056
|
*/
|
|
@@ -1288,14 +1268,14 @@ export async function createFieldSettersApi (knex, pluginOptions = {}) {
|
|
|
1288
1268
|
return api
|
|
1289
1269
|
}
|
|
1290
1270
|
|
|
1291
|
-
export async function
|
|
1292
|
-
const {
|
|
1271
|
+
export async function createAutoFilterApi (knex, pluginOptions = {}) {
|
|
1272
|
+
const { AutoFilterPlugin } = await import('../../plugins/core/rest-api-autofilter-plugin.js')
|
|
1293
1273
|
|
|
1294
1274
|
const api = new Api({
|
|
1295
|
-
name: '
|
|
1275
|
+
name: 'autofilter-test-api',
|
|
1296
1276
|
})
|
|
1297
1277
|
|
|
1298
|
-
const tenantId = storageMode.isAnyApi() ? '
|
|
1278
|
+
const tenantId = storageMode.isAnyApi() ? 'autofilter_tenant' : storageMode.defaultTenant
|
|
1299
1279
|
const previousTenant = storageMode.currentTenant
|
|
1300
1280
|
if (storageMode.isAnyApi()) {
|
|
1301
1281
|
storageMode.setCurrentTenant(tenantId)
|
|
@@ -1314,7 +1294,7 @@ export async function createMultiHomeApi (knex, pluginOptions = {}) {
|
|
|
1314
1294
|
put: 'no',
|
|
1315
1295
|
patch: 'minimal'
|
|
1316
1296
|
},
|
|
1317
|
-
sortableFields: ['id', 'title', 'name', '
|
|
1297
|
+
sortableFields: ['id', 'title', 'name', 'workspace_id', 'user_id']
|
|
1318
1298
|
})
|
|
1319
1299
|
|
|
1320
1300
|
await useStoragePlugin(api, knex, { tenantId })
|
|
@@ -1325,34 +1305,77 @@ export async function createMultiHomeApi (knex, pluginOptions = {}) {
|
|
|
1325
1305
|
await api.use(ExpressPlugin, pluginOptions.express || {})
|
|
1326
1306
|
}
|
|
1327
1307
|
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1308
|
+
const autofilterOptions = pluginOptions.autofilter || {}
|
|
1309
|
+
const workspaceField = pluginOptions.workspaceField || 'workspace_id'
|
|
1310
|
+
const userField = pluginOptions.userField || 'user_id'
|
|
1311
|
+
|
|
1312
|
+
await api.use(AutoFilterPlugin, {
|
|
1313
|
+
...autofilterOptions,
|
|
1314
|
+
resolvers: {
|
|
1315
|
+
workspace: ({ context }) => context.scopeValues?.workspaceId,
|
|
1316
|
+
user: ({ context }) => context.scopeValues?.userId,
|
|
1317
|
+
currentProject: ({ context }) => context.scopeValues?.projectId ?? null,
|
|
1318
|
+
...(autofilterOptions.resolvers || {})
|
|
1319
|
+
},
|
|
1320
|
+
presets: {
|
|
1321
|
+
public: { filters: [] },
|
|
1322
|
+
workspace: {
|
|
1323
|
+
filters: [{ field: workspaceField, resolver: 'workspace' }]
|
|
1324
|
+
},
|
|
1325
|
+
user: {
|
|
1326
|
+
filters: [{ field: userField, resolver: 'user' }]
|
|
1327
|
+
},
|
|
1328
|
+
workspace_user: {
|
|
1329
|
+
filters: [
|
|
1330
|
+
{ field: workspaceField, resolver: 'workspace' },
|
|
1331
|
+
{ field: userField, resolver: 'user' }
|
|
1332
|
+
]
|
|
1333
|
+
},
|
|
1334
|
+
...(autofilterOptions.presets || {})
|
|
1335
|
+
}
|
|
1338
1336
|
})
|
|
1339
1337
|
|
|
1340
|
-
|
|
1338
|
+
await api.addResource('workspace_reports', {
|
|
1339
|
+
schema: {
|
|
1340
|
+
id: { type: 'id' },
|
|
1341
|
+
title: { type: 'string', required: true, max: 200 },
|
|
1342
|
+
workspace_id: { type: 'string', required: true }
|
|
1343
|
+
},
|
|
1344
|
+
autofilter: 'workspace',
|
|
1345
|
+
tableName: 'autofilter_workspace_reports'
|
|
1346
|
+
})
|
|
1347
|
+
await api.resources.workspace_reports.createKnexTable()
|
|
1348
|
+
mapTable('autofilter_workspace_reports', 'workspace_reports')
|
|
1349
|
+
|
|
1350
|
+
await api.addResource('user_notes', {
|
|
1351
|
+
schema: {
|
|
1352
|
+
id: { type: 'id' },
|
|
1353
|
+
body: { type: 'string', required: true, max: 500 },
|
|
1354
|
+
user_id: { type: 'number', required: true }
|
|
1355
|
+
},
|
|
1356
|
+
autofilter: 'user',
|
|
1357
|
+
tableName: 'autofilter_user_notes'
|
|
1358
|
+
})
|
|
1359
|
+
await api.resources.user_notes.createKnexTable()
|
|
1360
|
+
mapTable('autofilter_user_notes', 'user_notes')
|
|
1361
|
+
|
|
1341
1362
|
await api.addResource('projects', {
|
|
1342
1363
|
schema: {
|
|
1343
1364
|
id: { type: 'id' },
|
|
1344
1365
|
name: { type: 'string', required: true, max: 200 },
|
|
1345
1366
|
description: { type: 'string', max: 1000 },
|
|
1346
1367
|
status: { type: 'string', defaultTo: 'active' },
|
|
1347
|
-
|
|
1368
|
+
workspace_id: { type: 'string', required: true },
|
|
1369
|
+
user_id: { type: 'number', required: true }
|
|
1348
1370
|
},
|
|
1349
1371
|
relationships: {
|
|
1350
1372
|
tasks: { type: 'hasMany', target: 'tasks', foreignKey: 'project_id' }
|
|
1351
1373
|
},
|
|
1352
|
-
|
|
1374
|
+
autofilter: 'workspace_user',
|
|
1375
|
+
tableName: 'autofilter_projects'
|
|
1353
1376
|
})
|
|
1354
1377
|
await api.resources.projects.createKnexTable()
|
|
1355
|
-
mapTable('
|
|
1378
|
+
mapTable('autofilter_projects', 'projects')
|
|
1356
1379
|
|
|
1357
1380
|
await api.addResource('tasks', {
|
|
1358
1381
|
schema: {
|
|
@@ -1360,37 +1383,40 @@ export async function createMultiHomeApi (knex, pluginOptions = {}) {
|
|
|
1360
1383
|
title: { type: 'string', required: true, max: 200 },
|
|
1361
1384
|
completed: { type: 'boolean', defaultTo: false },
|
|
1362
1385
|
project_id: { type: 'number', belongsTo: 'projects', as: 'project' },
|
|
1363
|
-
|
|
1386
|
+
workspace_id: { type: 'string', required: true },
|
|
1387
|
+
user_id: { type: 'number', required: true }
|
|
1364
1388
|
},
|
|
1365
|
-
|
|
1389
|
+
autofilter: 'workspace_user',
|
|
1390
|
+
tableName: 'autofilter_tasks'
|
|
1366
1391
|
})
|
|
1367
1392
|
await api.resources.tasks.createKnexTable()
|
|
1368
|
-
mapTable('
|
|
1393
|
+
mapTable('autofilter_tasks', 'tasks')
|
|
1369
1394
|
|
|
1370
|
-
await api.addResource('
|
|
1395
|
+
await api.addResource('optional_tasks', {
|
|
1371
1396
|
schema: {
|
|
1372
1397
|
id: { type: 'id' },
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1398
|
+
title: { type: 'string', required: true, max: 200 },
|
|
1399
|
+
project_id: { type: 'number', belongsTo: 'projects', as: 'project', nullable: true }
|
|
1400
|
+
},
|
|
1401
|
+
autofilter: {
|
|
1402
|
+
filters: [{ field: 'project_id', resolver: 'currentProject' }]
|
|
1377
1403
|
},
|
|
1378
|
-
tableName: '
|
|
1404
|
+
tableName: 'autofilter_optional_tasks'
|
|
1379
1405
|
})
|
|
1380
|
-
await api.resources.
|
|
1381
|
-
mapTable('
|
|
1406
|
+
await api.resources.optional_tasks.createKnexTable()
|
|
1407
|
+
mapTable('autofilter_optional_tasks', 'optional_tasks')
|
|
1382
1408
|
|
|
1383
|
-
// Global resource (excluded from multihome)
|
|
1384
1409
|
await api.addResource('system_settings', {
|
|
1385
1410
|
schema: {
|
|
1386
1411
|
id: { type: 'id' },
|
|
1387
1412
|
key: { type: 'string', required: true, unique: true },
|
|
1388
1413
|
value: { type: 'string', required: true }
|
|
1389
1414
|
},
|
|
1390
|
-
|
|
1415
|
+
autofilter: 'public',
|
|
1416
|
+
tableName: 'autofilter_system_settings'
|
|
1391
1417
|
})
|
|
1392
1418
|
await api.resources.system_settings.createKnexTable()
|
|
1393
|
-
mapTable('
|
|
1419
|
+
mapTable('autofilter_system_settings', 'system_settings')
|
|
1394
1420
|
|
|
1395
1421
|
if (storageMode.isAnyApi()) {
|
|
1396
1422
|
storageMode.setCurrentTenant(previousTenant)
|
|
@@ -1786,7 +1812,9 @@ export async function createVirtualFieldsApi (knex, pluginOptions = {}) {
|
|
|
1786
1812
|
}
|
|
1787
1813
|
|
|
1788
1814
|
/**
|
|
1789
|
-
* Creates an API configuration for testing searchSchema merge behavior
|
|
1815
|
+
* Creates an API configuration for testing searchSchema merge behavior.
|
|
1816
|
+
* Shared suites assert merge/validation behavior across storage backends;
|
|
1817
|
+
* backend-specific execution cases live in storage-specific tests.
|
|
1790
1818
|
*/
|
|
1791
1819
|
export async function createSearchSchemaMergeApi (knex, pluginOptions = {}) {
|
|
1792
1820
|
const api = new Api({
|
|
@@ -1825,12 +1853,27 @@ export async function createSearchSchemaMergeApi (knex, pluginOptions = {}) {
|
|
|
1825
1853
|
type: 'array',
|
|
1826
1854
|
filterOperator: 'between'
|
|
1827
1855
|
},
|
|
1856
|
+
// Arbitrary public filter name that searches across multiple fields.
|
|
1857
|
+
// The key `term` is part of the API contract even though there is no `term` column.
|
|
1858
|
+
term: {
|
|
1859
|
+
type: 'string',
|
|
1860
|
+
oneOf: ['name', 'description'],
|
|
1861
|
+
filterOperator: 'like'
|
|
1862
|
+
},
|
|
1828
1863
|
// This is a virtual field
|
|
1829
1864
|
category_name: {
|
|
1830
1865
|
type: 'string',
|
|
1831
1866
|
actualField: 'category.name',
|
|
1832
1867
|
filterOperator: 'like'
|
|
1833
1868
|
},
|
|
1869
|
+
// Arbitrary public filter name with fully custom server-side meaning.
|
|
1870
|
+
// The Knex query layer decides what `availability` means.
|
|
1871
|
+
availability: {
|
|
1872
|
+
type: 'boolean',
|
|
1873
|
+
applyFilter: (query, value) => {
|
|
1874
|
+
query.where('status', value ? 'active' : 'inactive')
|
|
1875
|
+
}
|
|
1876
|
+
},
|
|
1834
1877
|
// This is an explicit field not marked with search:true
|
|
1835
1878
|
// 'in' operator requires array type for validation
|
|
1836
1879
|
status: {
|
|
@@ -1872,77 +1915,3 @@ export async function createSearchSchemaMergeApi (knex, pluginOptions = {}) {
|
|
|
1872
1915
|
|
|
1873
1916
|
return api
|
|
1874
1917
|
}
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
/**
|
|
1878
|
-
* Creates an API with resources that OPT OUT of ownership
|
|
1879
|
-
* - public_items_ownerfield: uses ownerField: false
|
|
1880
|
-
* - public_items_ownership: uses ownership: false
|
|
1881
|
-
* Both tables intentionally DO NOT have a user_id column.
|
|
1882
|
-
*/
|
|
1883
|
-
export async function createOwnershipOptOutApi (knex, pluginOptions = {}) {
|
|
1884
|
-
const apiName = pluginOptions.apiName || 'ownership-optout-api'
|
|
1885
|
-
const tablePrefix = pluginOptions.tablePrefix || 'ownopt'
|
|
1886
|
-
if (storageMode.isAnyApi()) {
|
|
1887
|
-
storageMode.clearRegistry()
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
const api = new Api({
|
|
1891
|
-
name: apiName,
|
|
1892
|
-
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
1893
|
-
})
|
|
1894
|
-
|
|
1895
|
-
const previousTenant = storageMode.currentTenant
|
|
1896
|
-
const tenantId = storageMode.isAnyApi()
|
|
1897
|
-
? (pluginOptions.tenantId || `${tablePrefix}_tenant`)
|
|
1898
|
-
: storageMode.defaultTenant
|
|
1899
|
-
if (storageMode.isAnyApi()) {
|
|
1900
|
-
storageMode.setCurrentTenant(tenantId)
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
await api.use(RestApiPlugin, {
|
|
1904
|
-
simplifiedApi: false,
|
|
1905
|
-
simplifiedTransport: false,
|
|
1906
|
-
returnRecordApi: { post: true, put: false, patch: false },
|
|
1907
|
-
returnRecordTransport: { post: 'full', put: 'no', patch: 'minimal' },
|
|
1908
|
-
sortableFields: ['id', 'name']
|
|
1909
|
-
})
|
|
1910
|
-
await useStoragePlugin(api, knex, { tenantId })
|
|
1911
|
-
await resetAnyApiTables(knex)
|
|
1912
|
-
|
|
1913
|
-
try {
|
|
1914
|
-
// Access plugin (defaults: ownership enabled, filterByOwner true)
|
|
1915
|
-
await api.use(AccessPlugin, pluginOptions.access || {})
|
|
1916
|
-
|
|
1917
|
-
// Resource A: disable via ownerField:false
|
|
1918
|
-
await api.addResource('public_items_ownerfield', {
|
|
1919
|
-
schema: {
|
|
1920
|
-
id: { type: 'id' },
|
|
1921
|
-
name: { type: 'string', required: true }
|
|
1922
|
-
},
|
|
1923
|
-
// Important: no user_id column in schema; ownerField disables ownership logic
|
|
1924
|
-
ownerField: false,
|
|
1925
|
-
tableName: `${tablePrefix}_public_items_of`
|
|
1926
|
-
})
|
|
1927
|
-
await api.resources.public_items_ownerfield.createKnexTable()
|
|
1928
|
-
mapTable(`${tablePrefix}_public_items_of`, 'public_items_ownerfield')
|
|
1929
|
-
|
|
1930
|
-
// Resource B: disable via ownership:false
|
|
1931
|
-
await api.addResource('public_items_ownership', {
|
|
1932
|
-
schema: {
|
|
1933
|
-
id: { type: 'id' },
|
|
1934
|
-
name: { type: 'string', required: true }
|
|
1935
|
-
},
|
|
1936
|
-
ownership: false,
|
|
1937
|
-
tableName: `${tablePrefix}_public_items_own`
|
|
1938
|
-
})
|
|
1939
|
-
await api.resources.public_items_ownership.createKnexTable()
|
|
1940
|
-
mapTable(`${tablePrefix}_public_items_own`, 'public_items_ownership')
|
|
1941
|
-
|
|
1942
|
-
return api
|
|
1943
|
-
} finally {
|
|
1944
|
-
if (storageMode.isAnyApi()) {
|
|
1945
|
-
storageMode.setCurrentTenant(previousTenant)
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
export class FakeFastifyApp {
|
|
2
|
+
constructor () {
|
|
3
|
+
this.routes = []
|
|
4
|
+
this.parsers = []
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
route (definition) {
|
|
8
|
+
this.routes.push(definition)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
addContentTypeParser (contentType, options, parser) {
|
|
12
|
+
this.parsers.push({ contentType, options, parser })
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
hasContentTypeParser (contentType) {
|
|
16
|
+
return this.parsers.some((entry) => entry.contentType === contentType)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class FakeReply {
|
|
21
|
+
constructor () {
|
|
22
|
+
this.statusCode = null
|
|
23
|
+
this.contentType = null
|
|
24
|
+
this.headers = {}
|
|
25
|
+
this.payload = undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
code (statusCode) {
|
|
29
|
+
this.statusCode = statusCode
|
|
30
|
+
return this
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type (contentType) {
|
|
34
|
+
this.contentType = contentType
|
|
35
|
+
return this
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
header (name, value) {
|
|
39
|
+
this.headers[name] = value
|
|
40
|
+
return this
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
send (payload) {
|
|
44
|
+
this.payload = payload
|
|
45
|
+
return this
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function findFastifyRoute (app, method, url) {
|
|
50
|
+
return app.routes.find((route) => route.method === method && route.url === url)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function invokeFastifyRoute (
|
|
54
|
+
app,
|
|
55
|
+
{
|
|
56
|
+
method,
|
|
57
|
+
routeUrl,
|
|
58
|
+
requestUrl = routeUrl,
|
|
59
|
+
headers = {},
|
|
60
|
+
body,
|
|
61
|
+
params = {},
|
|
62
|
+
query = {}
|
|
63
|
+
}
|
|
64
|
+
) {
|
|
65
|
+
const route = findFastifyRoute(app, method, routeUrl)
|
|
66
|
+
if (!route) {
|
|
67
|
+
throw new Error(`Fastify route not found: ${method} ${routeUrl}`)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const reply = new FakeReply()
|
|
71
|
+
const request = {
|
|
72
|
+
method,
|
|
73
|
+
url: requestUrl,
|
|
74
|
+
raw: { url: requestUrl },
|
|
75
|
+
headers,
|
|
76
|
+
body,
|
|
77
|
+
params,
|
|
78
|
+
query
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (typeof route.preValidation === 'function') {
|
|
82
|
+
await route.preValidation(request, reply)
|
|
83
|
+
if (reply.statusCode !== null || reply.payload !== undefined) {
|
|
84
|
+
return { route, request, reply }
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
await route.handler(request, reply)
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (typeof route.errorHandler === 'function') {
|
|
92
|
+
await route.errorHandler(error, request, reply)
|
|
93
|
+
} else {
|
|
94
|
+
throw error
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { route, request, reply }
|
|
99
|
+
}
|