json-rest-api 1.0.29 → 2.1.0
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/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +744 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +733 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +103 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
- package/docs/GUIDE/33-migrating-to-v2.md +3838 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +37 -0
- package/index.js +3 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +70 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -177
- package/plugins/core/lib/anyapi/anyapi-registry.js +586 -418
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +120 -56
- package/plugins/core/lib/anyapi/stored-definition.js +109 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +13 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +128 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1077
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -395
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +36 -68
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +37 -81
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/anyapi.d.ts +30 -0
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +65 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -148
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -155
- package/tests/fixtures/api-configs.js +0 -2566
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-boundaries.test.js +0 -154
- package/tests/pagination-contract.test.js +0 -122
- package/tests/pagination-cursor-multifield.test.js +0 -641
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/temporal-boundaries.test.js +0 -208
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -1,2566 +0,0 @@
|
|
|
1
|
-
import { Api } from 'hooked-api'
|
|
2
|
-
import {
|
|
3
|
-
AutoFilterPlugin,
|
|
4
|
-
FileHandlingPlugin,
|
|
5
|
-
QueryProjectionsPlugin,
|
|
6
|
-
RestApiAnyapiKnexPlugin,
|
|
7
|
-
RestApiKnexPlugin,
|
|
8
|
-
RestApiPlugin,
|
|
9
|
-
RowPolicyPlugin,
|
|
10
|
-
SocketIOPlugin
|
|
11
|
-
} from '../../index.js'
|
|
12
|
-
import { ExpressPlugin } from '../../plugins/core/connectors/express-plugin.js'
|
|
13
|
-
import express from 'express'
|
|
14
|
-
import { createServer } from 'http'
|
|
15
|
-
import { ensureAnyApiSchema } from '../../plugins/core/lib/anyapi/schema-utils.js'
|
|
16
|
-
import { storageMode } from '../helpers/storage-mode.js'
|
|
17
|
-
|
|
18
|
-
async function resetAnyApiTables (knex) {
|
|
19
|
-
if (!storageMode.isAnyApi()) return
|
|
20
|
-
await knex('any_links').delete().catch(() => {})
|
|
21
|
-
await knex('any_records').delete().catch(() => {})
|
|
22
|
-
await knex('any_relationship_configs').delete().catch(() => {})
|
|
23
|
-
await knex('any_field_configs').delete().catch(() => {})
|
|
24
|
-
await knex('any_resource_configs').delete().catch(() => {})
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function useStoragePlugin (api, knex, { tenantId } = {}) {
|
|
28
|
-
if (storageMode.isAnyApi()) {
|
|
29
|
-
await ensureAnyApiSchema(knex)
|
|
30
|
-
const sanitizedName = api?.name ? api.name.replace(/[^A-Za-z0-9]+/g, '_').toLowerCase() : 'tenant'
|
|
31
|
-
const effectiveTenantId = tenantId || api?.anyapi?.tenantId || `${sanitizedName}_tenant`
|
|
32
|
-
api.anyapi = api.anyapi || {}
|
|
33
|
-
api.anyapi.tenantId = effectiveTenantId
|
|
34
|
-
await api.use(RestApiAnyapiKnexPlugin, { knex, tenantId: effectiveTenantId })
|
|
35
|
-
storageMode.setCurrentTenant(effectiveTenantId)
|
|
36
|
-
} else {
|
|
37
|
-
await api.use(RestApiKnexPlugin, { knex })
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function mapTable (tableName, resourceName) {
|
|
42
|
-
if (storageMode.isAnyApi()) {
|
|
43
|
-
storageMode.registerTable(tableName, resourceName)
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async function withTenantContext (tenantId, fn) {
|
|
48
|
-
if (!storageMode.isAnyApi()) {
|
|
49
|
-
return await fn()
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const previousTenant = storageMode.currentTenant
|
|
53
|
-
storageMode.setCurrentTenant(tenantId || storageMode.defaultTenant)
|
|
54
|
-
try {
|
|
55
|
-
return await fn()
|
|
56
|
-
} finally {
|
|
57
|
-
storageMode.setCurrentTenant(previousTenant)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export async function createTemporalBoundaryApi (knex) {
|
|
62
|
-
const api = new Api({ name: 'temporal-boundaries', log: { level: 'error' } })
|
|
63
|
-
await api.use(RestApiPlugin, {
|
|
64
|
-
simplifiedApi: false,
|
|
65
|
-
simplifiedTransport: false,
|
|
66
|
-
returnRecordApi: { post: 'full', put: 'full', patch: 'full' },
|
|
67
|
-
queryDefaultLimit: 2,
|
|
68
|
-
queryMaxLimit: 3
|
|
69
|
-
})
|
|
70
|
-
await api.use(QueryProjectionsPlugin)
|
|
71
|
-
await useStoragePlugin(api, knex)
|
|
72
|
-
const producedAt = {
|
|
73
|
-
type: 'dateTime', temporalPrecision: 0, computed: true,
|
|
74
|
-
compute: () => new Date('2026-09-08T01:02:03.456Z')
|
|
75
|
-
}
|
|
76
|
-
await api.addResource('people', {
|
|
77
|
-
schema: { id: { type: 'id' }, name: { type: 'string' }, producedAt },
|
|
78
|
-
relationships: { events: { type: 'hasMany', target: 'events', foreignKey: 'personId' } },
|
|
79
|
-
tableName: 'temporal_people'
|
|
80
|
-
})
|
|
81
|
-
await api.resources.people.createKnexTable()
|
|
82
|
-
mapTable('temporal_people', 'people')
|
|
83
|
-
await api.addResource('events', {
|
|
84
|
-
sortableFields: ['id', 'name', 'occurredAt', 'day', 'atTime', 'observedAtMs'],
|
|
85
|
-
schema: {
|
|
86
|
-
id: { type: 'id' },
|
|
87
|
-
name: { type: 'string', required: true },
|
|
88
|
-
occurredAt: { type: 'dateTime', temporalPrecision: 6, nullable: true, search: true },
|
|
89
|
-
day: { type: 'date', nullable: true, search: true },
|
|
90
|
-
atTime: { type: 'time', temporalPrecision: 3, nullable: true, search: true },
|
|
91
|
-
observedAtMs: { type: 'epochMilliseconds', nullable: true, search: true },
|
|
92
|
-
observedAtSeconds: { type: 'epochSeconds', nullable: true, search: true },
|
|
93
|
-
serializedAt: {
|
|
94
|
-
type: 'dateTime', temporalPrecision: 6, nullable: true, search: true,
|
|
95
|
-
storage: { serialize: value => value == null ? null : value.replace('T', ' ').replace(/Z$/, '') }
|
|
96
|
-
},
|
|
97
|
-
personId: { type: 'id', belongsTo: 'people', as: 'person', nullable: true },
|
|
98
|
-
subjectId: { type: 'number', nullable: true },
|
|
99
|
-
subjectType: { type: 'string', nullable: true },
|
|
100
|
-
producedAt
|
|
101
|
-
},
|
|
102
|
-
queryFields: {
|
|
103
|
-
projectedAt: {
|
|
104
|
-
type: 'dateTime', temporalPrecision: 3, sortable: true,
|
|
105
|
-
select: ({ knex, column }) => knex.raw('??', [column('occurredAt')])
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
relationships: {
|
|
109
|
-
subject: { belongsToPolymorphic: { types: ['people'], typeField: 'subjectType', idField: 'subjectId' } }
|
|
110
|
-
},
|
|
111
|
-
tableName: 'temporal_events'
|
|
112
|
-
})
|
|
113
|
-
await api.resources.events.createKnexTable()
|
|
114
|
-
mapTable('temporal_events', 'events')
|
|
115
|
-
return api
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Creates a basic API configuration with Countries, Publishers, Authors, Books
|
|
120
|
-
*/
|
|
121
|
-
export async function createBasicApi (knex, pluginOptions = {}) {
|
|
122
|
-
const apiName = pluginOptions.apiName || 'basic-test-api'
|
|
123
|
-
const tablePrefix = pluginOptions.tablePrefix || 'basic'
|
|
124
|
-
if (storageMode.isAnyApi()) {
|
|
125
|
-
storageMode.clearRegistry()
|
|
126
|
-
}
|
|
127
|
-
const api = new Api({
|
|
128
|
-
name: apiName,
|
|
129
|
-
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
const previousTenant = storageMode.currentTenant
|
|
133
|
-
const tenantId = storageMode.isAnyApi()
|
|
134
|
-
? (pluginOptions.tenantId || `${tablePrefix}_tenant`)
|
|
135
|
-
: storageMode.defaultTenant
|
|
136
|
-
if (storageMode.isAnyApi()) {
|
|
137
|
-
storageMode.setCurrentTenant(tenantId)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const restApiOptions = {
|
|
141
|
-
simplifiedApi: false,
|
|
142
|
-
simplifiedTransport: false,
|
|
143
|
-
returnRecordApi: {
|
|
144
|
-
post: true, // Need to return record to get ID for tests
|
|
145
|
-
put: false,
|
|
146
|
-
patch: false
|
|
147
|
-
},
|
|
148
|
-
returnRecordTransport: {
|
|
149
|
-
post: 'full', // Tests expect full JSON:API response
|
|
150
|
-
put: 'no',
|
|
151
|
-
patch: 'minimal' // Tests expect minimal response with ID
|
|
152
|
-
},
|
|
153
|
-
sortableFields: ['id', 'title', 'country_id', 'publisher_id', 'name', 'code'],
|
|
154
|
-
...pluginOptions['rest-api'] // Merge any custom options for rest-api plugin
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
await api.use(RestApiPlugin, restApiOptions)
|
|
158
|
-
await useStoragePlugin(api, knex, { tenantId })
|
|
159
|
-
await resetAnyApiTables(knex)
|
|
160
|
-
|
|
161
|
-
// Add Express plugin if requested
|
|
162
|
-
if (pluginOptions.includeExpress) {
|
|
163
|
-
await api.use(ExpressPlugin, {
|
|
164
|
-
mountPath: '/api', // Default mount path for tests
|
|
165
|
-
...(pluginOptions.express || {})
|
|
166
|
-
})
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
// Countries table
|
|
171
|
-
await api.addResource('countries', {
|
|
172
|
-
schema: {
|
|
173
|
-
id: { type: 'id' },
|
|
174
|
-
name: { type: 'string', required: true, max: 100, search: true },
|
|
175
|
-
code: { type: 'string', max: 2, unique: true }
|
|
176
|
-
},
|
|
177
|
-
relationships: {
|
|
178
|
-
publishers: { type: 'hasMany', target: 'publishers', foreignKey: 'country_id' },
|
|
179
|
-
books: { type: 'hasMany', target: 'books', foreignKey: 'country_id' }
|
|
180
|
-
},
|
|
181
|
-
tableName: `${tablePrefix}_countries`
|
|
182
|
-
})
|
|
183
|
-
await api.resources.countries.createKnexTable()
|
|
184
|
-
mapTable(`${tablePrefix}_countries`, 'countries')
|
|
185
|
-
|
|
186
|
-
// Publishers table
|
|
187
|
-
await api.addResource('publishers', {
|
|
188
|
-
schema: {
|
|
189
|
-
id: { type: 'id' },
|
|
190
|
-
name: { type: 'string', required: true, max: 200 },
|
|
191
|
-
country_id: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' }
|
|
192
|
-
},
|
|
193
|
-
relationships: {
|
|
194
|
-
books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' },
|
|
195
|
-
authors: { type: 'hasMany', target: 'authors', foreignKey: 'publisher_id' }
|
|
196
|
-
},
|
|
197
|
-
tableName: `${tablePrefix}_publishers`
|
|
198
|
-
})
|
|
199
|
-
await api.resources.publishers.createKnexTable()
|
|
200
|
-
mapTable(`${tablePrefix}_publishers`, 'publishers')
|
|
201
|
-
|
|
202
|
-
// Authors table
|
|
203
|
-
await api.addResource('authors', {
|
|
204
|
-
schema: {
|
|
205
|
-
id: { type: 'id' },
|
|
206
|
-
name: { type: 'string', required: true, max: 200 },
|
|
207
|
-
publisher_id: { type: 'number', nullable: true, belongsTo: 'publishers', as: 'publisher' }
|
|
208
|
-
},
|
|
209
|
-
relationships: {
|
|
210
|
-
books: { type: 'manyToMany', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' }
|
|
211
|
-
},
|
|
212
|
-
tableName: `${tablePrefix}_authors`
|
|
213
|
-
})
|
|
214
|
-
await api.resources.authors.createKnexTable()
|
|
215
|
-
mapTable(`${tablePrefix}_authors`, 'authors')
|
|
216
|
-
|
|
217
|
-
// Books table
|
|
218
|
-
await api.addResource('books', {
|
|
219
|
-
schema: {
|
|
220
|
-
id: { type: 'id' },
|
|
221
|
-
title: { type: 'string', required: true, max: 300, search: true },
|
|
222
|
-
country_id: { type: 'number', required: true, belongsTo: 'countries', as: 'country', search: true },
|
|
223
|
-
publisher_id: { type: 'number', belongsTo: 'publishers', as: 'publisher', search: true }
|
|
224
|
-
},
|
|
225
|
-
relationships: {
|
|
226
|
-
authors: { type: 'manyToMany', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' }
|
|
227
|
-
},
|
|
228
|
-
tableName: `${tablePrefix}_books`
|
|
229
|
-
})
|
|
230
|
-
await api.resources.books.createKnexTable()
|
|
231
|
-
mapTable(`${tablePrefix}_books`, 'books')
|
|
232
|
-
|
|
233
|
-
// Book-Authors pivot table
|
|
234
|
-
await api.addResource('book_authors', {
|
|
235
|
-
schema: {
|
|
236
|
-
id: { type: 'id' },
|
|
237
|
-
book_id: { type: 'number', required: true, belongsTo: 'books', as: 'book' },
|
|
238
|
-
author_id: { type: 'number', required: true, belongsTo: 'authors', as: 'author' }
|
|
239
|
-
},
|
|
240
|
-
tableName: `${tablePrefix}_book_authors`
|
|
241
|
-
})
|
|
242
|
-
await api.resources.book_authors.createKnexTable()
|
|
243
|
-
mapTable(`${tablePrefix}_book_authors`, 'book_authors')
|
|
244
|
-
if (storageMode.isAnyApi()) {
|
|
245
|
-
const descriptorTenant = api.anyapi?.tenantId || tenantId
|
|
246
|
-
const booksDescriptor = await api.anyapi.registry.getDescriptor(descriptorTenant, 'books')
|
|
247
|
-
const authorsDescriptor = await api.anyapi.registry.getDescriptor(descriptorTenant, 'authors')
|
|
248
|
-
const relationshipKey = booksDescriptor?.manyToMany?.authors?.relationship
|
|
249
|
-
const inverseRelationshipKey = authorsDescriptor?.manyToMany?.books?.relationship
|
|
250
|
-
storageMode.registerLink(
|
|
251
|
-
`${tablePrefix}_book_authors`,
|
|
252
|
-
'books',
|
|
253
|
-
'authors',
|
|
254
|
-
relationshipKey,
|
|
255
|
-
inverseRelationshipKey
|
|
256
|
-
)
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return api
|
|
260
|
-
} finally {
|
|
261
|
-
if (storageMode.isAnyApi()) {
|
|
262
|
-
storageMode.setCurrentTenant(previousTenant)
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
export async function createReturnRecordApi (knex, pluginOptions = {}) {
|
|
268
|
-
const apiName = pluginOptions.apiName || 'return-record-test-api'
|
|
269
|
-
const tablePrefix = pluginOptions.tablePrefix || 'return'
|
|
270
|
-
if (storageMode.isAnyApi()) {
|
|
271
|
-
storageMode.clearRegistry()
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const api = new Api({
|
|
275
|
-
name: apiName,
|
|
276
|
-
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
const previousTenant = storageMode.currentTenant
|
|
280
|
-
const tenantId = storageMode.isAnyApi()
|
|
281
|
-
? (pluginOptions.tenantId || `${tablePrefix}_tenant`)
|
|
282
|
-
: storageMode.defaultTenant
|
|
283
|
-
if (storageMode.isAnyApi()) {
|
|
284
|
-
storageMode.setCurrentTenant(tenantId)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
await api.use(RestApiPlugin, {
|
|
288
|
-
simplifiedApi: false,
|
|
289
|
-
simplifiedTransport: false,
|
|
290
|
-
returnRecordApi: {
|
|
291
|
-
post: true,
|
|
292
|
-
put: false,
|
|
293
|
-
patch: false
|
|
294
|
-
},
|
|
295
|
-
returnRecordTransport: {
|
|
296
|
-
post: false,
|
|
297
|
-
put: true,
|
|
298
|
-
patch: 'minimal'
|
|
299
|
-
},
|
|
300
|
-
sortableFields: ['id', 'name'],
|
|
301
|
-
...pluginOptions['rest-api']
|
|
302
|
-
})
|
|
303
|
-
await useStoragePlugin(api, knex, { tenantId })
|
|
304
|
-
await resetAnyApiTables(knex)
|
|
305
|
-
|
|
306
|
-
try {
|
|
307
|
-
await api.addResource('global_items', {
|
|
308
|
-
schema: {
|
|
309
|
-
id: { type: 'id' },
|
|
310
|
-
name: { type: 'string', required: true, max: 100 }
|
|
311
|
-
},
|
|
312
|
-
tableName: `${tablePrefix}_global_items`
|
|
313
|
-
})
|
|
314
|
-
await api.resources.global_items.createKnexTable()
|
|
315
|
-
mapTable(`${tablePrefix}_global_items`, 'global_items')
|
|
316
|
-
|
|
317
|
-
await api.addResource('scope_items', {
|
|
318
|
-
schema: {
|
|
319
|
-
id: { type: 'id' },
|
|
320
|
-
name: { type: 'string', required: true, max: 100 }
|
|
321
|
-
},
|
|
322
|
-
tableName: `${tablePrefix}_scope_items`,
|
|
323
|
-
returnRecordApi: {
|
|
324
|
-
post: false,
|
|
325
|
-
put: true,
|
|
326
|
-
patch: 'minimal'
|
|
327
|
-
},
|
|
328
|
-
returnRecordTransport: true
|
|
329
|
-
})
|
|
330
|
-
await api.resources.scope_items.createKnexTable()
|
|
331
|
-
mapTable(`${tablePrefix}_scope_items`, 'scope_items')
|
|
332
|
-
} finally {
|
|
333
|
-
if (storageMode.isAnyApi()) {
|
|
334
|
-
storageMode.setCurrentTenant(previousTenant)
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
return api
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Creates a small API that uses logical camelCase belongsTo fields with default snake_case storage mapping.
|
|
343
|
-
* This reproduces the full JSON:API linkage path where storage column names differ from logical field names.
|
|
344
|
-
*/
|
|
345
|
-
export async function createCamelCaseBelongsToApi (knex, pluginOptions = {}) {
|
|
346
|
-
const apiName = pluginOptions.apiName || 'camelcase-belongsto-test-api'
|
|
347
|
-
const tablePrefix = pluginOptions.tablePrefix || 'camel_fk'
|
|
348
|
-
|
|
349
|
-
const api = new Api({
|
|
350
|
-
name: apiName,
|
|
351
|
-
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
await api.use(RestApiPlugin, {
|
|
355
|
-
simplifiedApi: false,
|
|
356
|
-
simplifiedTransport: false,
|
|
357
|
-
returnRecordApi: {
|
|
358
|
-
post: true,
|
|
359
|
-
put: false,
|
|
360
|
-
patch: false
|
|
361
|
-
},
|
|
362
|
-
returnRecordTransport: {
|
|
363
|
-
post: 'full',
|
|
364
|
-
put: 'no',
|
|
365
|
-
patch: 'full'
|
|
366
|
-
},
|
|
367
|
-
...(pluginOptions['rest-api'] || {})
|
|
368
|
-
})
|
|
369
|
-
|
|
370
|
-
await api.use(RestApiKnexPlugin, { knex })
|
|
371
|
-
|
|
372
|
-
await api.addResource('countries', {
|
|
373
|
-
schema: {
|
|
374
|
-
id: { type: 'id' },
|
|
375
|
-
name: { type: 'string', required: true, max: 100 }
|
|
376
|
-
},
|
|
377
|
-
tableName: `${tablePrefix}_countries`
|
|
378
|
-
})
|
|
379
|
-
await api.resources.countries.createKnexTable()
|
|
380
|
-
|
|
381
|
-
await api.addResource('publishers', {
|
|
382
|
-
schema: {
|
|
383
|
-
id: { type: 'id' },
|
|
384
|
-
name: { type: 'string', required: true, max: 200 },
|
|
385
|
-
countryId: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' }
|
|
386
|
-
},
|
|
387
|
-
tableName: `${tablePrefix}_publishers`
|
|
388
|
-
})
|
|
389
|
-
await api.resources.publishers.createKnexTable()
|
|
390
|
-
|
|
391
|
-
return api
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
export async function createRelationshipIncludeStorageAdapterApi (knex) {
|
|
395
|
-
const api = new Api({
|
|
396
|
-
name: 'relationship-include-storage-adapter-test',
|
|
397
|
-
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
await api.use(RestApiPlugin, {
|
|
401
|
-
simplifiedApi: false,
|
|
402
|
-
simplifiedTransport: false
|
|
403
|
-
})
|
|
404
|
-
await api.use(RestApiKnexPlugin, { knex })
|
|
405
|
-
|
|
406
|
-
await knex.schema.createTable('uninitialized_pets', table => {
|
|
407
|
-
table.increments('id').primary()
|
|
408
|
-
table.string('first_name').notNullable()
|
|
409
|
-
table.string('last_name').notNullable()
|
|
410
|
-
})
|
|
411
|
-
await knex.schema.createTable('uninitialized_bookings', table => {
|
|
412
|
-
table.increments('id').primary()
|
|
413
|
-
table.string('reference').notNullable()
|
|
414
|
-
table.integer('pet_id').notNullable()
|
|
415
|
-
})
|
|
416
|
-
|
|
417
|
-
await api.addResource('pets', {
|
|
418
|
-
schema: {
|
|
419
|
-
id: { type: 'id' },
|
|
420
|
-
firstName: { type: 'string', required: true },
|
|
421
|
-
lastName: { type: 'string', required: true }
|
|
422
|
-
},
|
|
423
|
-
tableName: 'uninitialized_pets'
|
|
424
|
-
})
|
|
425
|
-
|
|
426
|
-
await api.addResource('bookings', {
|
|
427
|
-
schema: {
|
|
428
|
-
id: { type: 'id' },
|
|
429
|
-
reference: { type: 'string', required: true },
|
|
430
|
-
petId: {
|
|
431
|
-
type: 'id',
|
|
432
|
-
required: true,
|
|
433
|
-
belongsTo: 'pets',
|
|
434
|
-
as: 'pet'
|
|
435
|
-
}
|
|
436
|
-
},
|
|
437
|
-
tableName: 'uninitialized_bookings'
|
|
438
|
-
})
|
|
439
|
-
|
|
440
|
-
return api
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
export async function seedRelationshipIncludeStorageAdapterApi (knex) {
|
|
444
|
-
await knex('uninitialized_pets').insert({
|
|
445
|
-
id: 7,
|
|
446
|
-
first_name: 'Coco',
|
|
447
|
-
last_name: 'Spaniel'
|
|
448
|
-
})
|
|
449
|
-
await knex('uninitialized_bookings').insert({
|
|
450
|
-
id: 41,
|
|
451
|
-
reference: 'BOOK-41',
|
|
452
|
-
pet_id: 7
|
|
453
|
-
})
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* Creates a small API focused on resource ID normalization behavior.
|
|
458
|
-
*/
|
|
459
|
-
export async function createIdNormalizationApi (knex, pluginOptions = {}) {
|
|
460
|
-
const apiName = pluginOptions.apiName || 'id-normalization-test-api'
|
|
461
|
-
const tablePrefix = pluginOptions.tablePrefix || 'id_norm'
|
|
462
|
-
if (storageMode.isAnyApi()) {
|
|
463
|
-
storageMode.clearRegistry()
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
const api = new Api({
|
|
467
|
-
name: apiName,
|
|
468
|
-
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
469
|
-
})
|
|
470
|
-
|
|
471
|
-
const previousTenant = storageMode.currentTenant
|
|
472
|
-
const tenantId = storageMode.isAnyApi()
|
|
473
|
-
? (pluginOptions.tenantId || `${tablePrefix}_tenant`)
|
|
474
|
-
: storageMode.defaultTenant
|
|
475
|
-
if (storageMode.isAnyApi()) {
|
|
476
|
-
storageMode.setCurrentTenant(tenantId)
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
await api.use(RestApiPlugin, {
|
|
480
|
-
simplifiedApi: false,
|
|
481
|
-
simplifiedTransport: false,
|
|
482
|
-
returnRecordApi: {
|
|
483
|
-
post: true,
|
|
484
|
-
put: false,
|
|
485
|
-
patch: false
|
|
486
|
-
},
|
|
487
|
-
returnRecordTransport: {
|
|
488
|
-
post: 'full',
|
|
489
|
-
put: 'no',
|
|
490
|
-
patch: 'minimal'
|
|
491
|
-
},
|
|
492
|
-
...(pluginOptions['rest-api'] || {})
|
|
493
|
-
})
|
|
494
|
-
|
|
495
|
-
try {
|
|
496
|
-
await withTenantContext(tenantId, async () => {
|
|
497
|
-
await useStoragePlugin(api, knex, { tenantId })
|
|
498
|
-
await resetAnyApiTables(knex)
|
|
499
|
-
|
|
500
|
-
await api.addResource('countries', {
|
|
501
|
-
schema: {
|
|
502
|
-
id: { type: 'string', required: true },
|
|
503
|
-
name: { type: 'string', required: true, max: 100 }
|
|
504
|
-
},
|
|
505
|
-
tableName: `${tablePrefix}_countries`,
|
|
506
|
-
...(pluginOptions.countryResourceOptions || {})
|
|
507
|
-
})
|
|
508
|
-
await knex.schema.dropTableIfExists(`${tablePrefix}_countries`)
|
|
509
|
-
await knex.schema.createTable(`${tablePrefix}_countries`, (table) => {
|
|
510
|
-
table.string('id').primary()
|
|
511
|
-
table.string('name').notNullable()
|
|
512
|
-
})
|
|
513
|
-
mapTable(`${tablePrefix}_countries`, 'countries')
|
|
514
|
-
|
|
515
|
-
await api.addResource('publishers', {
|
|
516
|
-
schema: {
|
|
517
|
-
id: { type: 'string', required: true },
|
|
518
|
-
name: { type: 'string', required: true, max: 100 },
|
|
519
|
-
country_id: { type: 'string', nullable: true, belongsTo: 'countries', as: 'country' }
|
|
520
|
-
},
|
|
521
|
-
relationships: {
|
|
522
|
-
tags: { type: 'manyToMany', through: 'publisher_tags', foreignKey: 'publisher_id', otherKey: 'tag_id' }
|
|
523
|
-
},
|
|
524
|
-
tableName: `${tablePrefix}_publishers`,
|
|
525
|
-
...(pluginOptions.publisherResourceOptions || {})
|
|
526
|
-
})
|
|
527
|
-
await knex.schema.dropTableIfExists(`${tablePrefix}_publishers`)
|
|
528
|
-
await knex.schema.createTable(`${tablePrefix}_publishers`, (table) => {
|
|
529
|
-
table.string('id').primary()
|
|
530
|
-
table.string('name').notNullable()
|
|
531
|
-
table.string('country_id').nullable()
|
|
532
|
-
})
|
|
533
|
-
mapTable(`${tablePrefix}_publishers`, 'publishers')
|
|
534
|
-
|
|
535
|
-
await api.addResource('tags', {
|
|
536
|
-
schema: {
|
|
537
|
-
id: { type: 'string', required: true },
|
|
538
|
-
name: { type: 'string', required: true, max: 100 }
|
|
539
|
-
},
|
|
540
|
-
tableName: `${tablePrefix}_tags`,
|
|
541
|
-
...(pluginOptions.tagResourceOptions || {})
|
|
542
|
-
})
|
|
543
|
-
await knex.schema.dropTableIfExists(`${tablePrefix}_tags`)
|
|
544
|
-
await knex.schema.createTable(`${tablePrefix}_tags`, (table) => {
|
|
545
|
-
table.string('id').primary()
|
|
546
|
-
table.string('name').notNullable()
|
|
547
|
-
})
|
|
548
|
-
mapTable(`${tablePrefix}_tags`, 'tags')
|
|
549
|
-
|
|
550
|
-
await api.addResource('publisher_tags', {
|
|
551
|
-
schema: {
|
|
552
|
-
id: { type: 'id' },
|
|
553
|
-
publisher_id: { type: 'string', required: true, belongsTo: 'publishers', as: 'publisher' },
|
|
554
|
-
tag_id: { type: 'string', required: true, belongsTo: 'tags', as: 'tag' }
|
|
555
|
-
},
|
|
556
|
-
tableName: `${tablePrefix}_publisher_tags`
|
|
557
|
-
})
|
|
558
|
-
await knex.schema.dropTableIfExists(`${tablePrefix}_publisher_tags`)
|
|
559
|
-
await knex.schema.createTable(`${tablePrefix}_publisher_tags`, (table) => {
|
|
560
|
-
table.increments('id').primary()
|
|
561
|
-
table.string('publisher_id').notNullable()
|
|
562
|
-
table.string('tag_id').notNullable()
|
|
563
|
-
})
|
|
564
|
-
mapTable(`${tablePrefix}_publisher_tags`, 'publisher_tags')
|
|
565
|
-
if (storageMode.isAnyApi()) {
|
|
566
|
-
const descriptorTenant = api.anyapi?.tenantId || tenantId
|
|
567
|
-
const publishersDescriptor = await api.anyapi.registry.getDescriptor(descriptorTenant, 'publishers')
|
|
568
|
-
const tagsDescriptor = await api.anyapi.registry.getDescriptor(descriptorTenant, 'tags')
|
|
569
|
-
const relationshipKey = publishersDescriptor?.manyToMany?.tags?.relationship
|
|
570
|
-
const inverseRelationshipKey = tagsDescriptor?.manyToMany?.publishers?.relationship
|
|
571
|
-
storageMode.registerLink(
|
|
572
|
-
`${tablePrefix}_publisher_tags`,
|
|
573
|
-
'publishers',
|
|
574
|
-
'tags',
|
|
575
|
-
relationshipKey,
|
|
576
|
-
inverseRelationshipKey
|
|
577
|
-
)
|
|
578
|
-
}
|
|
579
|
-
})
|
|
580
|
-
|
|
581
|
-
return api
|
|
582
|
-
} finally {
|
|
583
|
-
if (storageMode.isAnyApi()) {
|
|
584
|
-
storageMode.setCurrentTenant(previousTenant)
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
/**
|
|
590
|
-
* Creates a basic API with bulk operations enabled
|
|
591
|
-
*/
|
|
592
|
-
export async function createBulkOperationsApi (knex, pluginOptions = {}) {
|
|
593
|
-
const { BulkOperationsPlugin } = await import('../../plugins/core/bulk-operations-plugin.js')
|
|
594
|
-
|
|
595
|
-
const api = await createBasicApi(knex, {
|
|
596
|
-
...pluginOptions,
|
|
597
|
-
tenantId: pluginOptions.tenantId || 'bulk_ops_tenant'
|
|
598
|
-
})
|
|
599
|
-
|
|
600
|
-
// Add bulk operations plugin
|
|
601
|
-
await api.use(BulkOperationsPlugin, {
|
|
602
|
-
'bulk-operations': {
|
|
603
|
-
maxBulkOperations: 100,
|
|
604
|
-
defaultAtomic: true,
|
|
605
|
-
batchSize: 10,
|
|
606
|
-
enableOptimizations: true,
|
|
607
|
-
...pluginOptions['bulk-operations']
|
|
608
|
-
}
|
|
609
|
-
})
|
|
610
|
-
|
|
611
|
-
return api
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
/**
|
|
615
|
-
* Creates an extended API with additional fields for more complex testing
|
|
616
|
-
*/
|
|
617
|
-
export async function createExtendedApi (knex) {
|
|
618
|
-
const api = new Api({
|
|
619
|
-
name: 'extended-test-api',
|
|
620
|
-
})
|
|
621
|
-
|
|
622
|
-
const tenantId = storageMode.isAnyApi() ? 'extended_tenant' : storageMode.defaultTenant
|
|
623
|
-
|
|
624
|
-
await api.use(RestApiPlugin, {
|
|
625
|
-
simplifiedApi: false,
|
|
626
|
-
simplifiedTransport: false,
|
|
627
|
-
returnRecordApi: {
|
|
628
|
-
post: true, // Need to return record to get ID for tests
|
|
629
|
-
put: false,
|
|
630
|
-
patch: false
|
|
631
|
-
},
|
|
632
|
-
returnRecordTransport: {
|
|
633
|
-
post: 'full', // Tests expect full JSON:API response
|
|
634
|
-
put: 'no',
|
|
635
|
-
patch: 'minimal' // Tests expect minimal response with ID
|
|
636
|
-
},
|
|
637
|
-
sortableFields: ['id', 'title', 'country_id', 'publisher_id', 'price', 'language', 'population', 'name', 'code']
|
|
638
|
-
})
|
|
639
|
-
await withTenantContext(tenantId, async () => {
|
|
640
|
-
await useStoragePlugin(api, knex, { tenantId })
|
|
641
|
-
await resetAnyApiTables(knex)
|
|
642
|
-
|
|
643
|
-
// Countries with extended fields
|
|
644
|
-
await api.addResource('countries', {
|
|
645
|
-
schema: {
|
|
646
|
-
id: { type: 'id' },
|
|
647
|
-
name: { type: 'string', required: true, max: 100 },
|
|
648
|
-
code: { type: 'string', max: 2, unique: true },
|
|
649
|
-
capital: { type: 'string', max: 100 },
|
|
650
|
-
population: { type: 'number' },
|
|
651
|
-
currency: { type: 'string', max: 3 }
|
|
652
|
-
},
|
|
653
|
-
relationships: {
|
|
654
|
-
publishers: { type: 'hasMany', target: 'publishers', foreignKey: 'country_id' },
|
|
655
|
-
books: { type: 'hasMany', target: 'books', foreignKey: 'country_id' },
|
|
656
|
-
authors: { type: 'hasMany', target: 'authors', foreignKey: 'nationality_id' }
|
|
657
|
-
},
|
|
658
|
-
tableName: 'ext_countries'
|
|
659
|
-
})
|
|
660
|
-
await api.resources.countries.createKnexTable()
|
|
661
|
-
mapTable('ext_countries', 'countries')
|
|
662
|
-
|
|
663
|
-
// Publishers with extended fields
|
|
664
|
-
await api.addResource('publishers', {
|
|
665
|
-
schema: {
|
|
666
|
-
id: { type: 'id' },
|
|
667
|
-
name: { type: 'string', required: true, max: 200 },
|
|
668
|
-
country_id: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' },
|
|
669
|
-
founded_year: { type: 'number' },
|
|
670
|
-
website: { type: 'string', max: 255 },
|
|
671
|
-
active: { type: 'boolean', default: true }
|
|
672
|
-
},
|
|
673
|
-
relationships: {
|
|
674
|
-
books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' },
|
|
675
|
-
reviews: {
|
|
676
|
-
type: 'hasMany',
|
|
677
|
-
target: 'reviews',
|
|
678
|
-
via: 'reviewable'
|
|
679
|
-
}
|
|
680
|
-
},
|
|
681
|
-
tableName: 'ext_publishers'
|
|
682
|
-
})
|
|
683
|
-
await api.resources.publishers.createKnexTable()
|
|
684
|
-
mapTable('ext_publishers', 'publishers')
|
|
685
|
-
|
|
686
|
-
// Authors with extended fields
|
|
687
|
-
await api.addResource('authors', {
|
|
688
|
-
schema: {
|
|
689
|
-
id: { type: 'id' },
|
|
690
|
-
name: { type: 'string', required: true, max: 200 },
|
|
691
|
-
birth_date: { type: 'date' },
|
|
692
|
-
biography: { type: 'string', max: 5000 },
|
|
693
|
-
nationality_id: { type: 'number', belongsTo: 'countries', as: 'nationality' }
|
|
694
|
-
},
|
|
695
|
-
relationships: {
|
|
696
|
-
books: { type: 'manyToMany', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' },
|
|
697
|
-
reviews: {
|
|
698
|
-
type: 'hasMany',
|
|
699
|
-
target: 'reviews',
|
|
700
|
-
via: 'reviewable'
|
|
701
|
-
}
|
|
702
|
-
},
|
|
703
|
-
tableName: 'ext_authors'
|
|
704
|
-
})
|
|
705
|
-
await api.resources.authors.createKnexTable()
|
|
706
|
-
mapTable('ext_authors', 'authors')
|
|
707
|
-
|
|
708
|
-
// Books with extended fields
|
|
709
|
-
await api.addResource('books', {
|
|
710
|
-
schema: {
|
|
711
|
-
id: { type: 'id' },
|
|
712
|
-
title: { type: 'string', required: true, max: 300, search: true },
|
|
713
|
-
isbn: { type: 'string', max: 13 },
|
|
714
|
-
pages: { type: 'number' },
|
|
715
|
-
price: { type: 'number', search: true }, // Store price as string for decimal precision
|
|
716
|
-
published_date: { type: 'date' },
|
|
717
|
-
language: { type: 'string', max: 2, default: 'en', search: true },
|
|
718
|
-
country_id: { type: 'number', required: true, belongsTo: 'countries', as: 'country', search: true },
|
|
719
|
-
publisher_id: { type: 'number', belongsTo: 'publishers', as: 'publisher', search: true }
|
|
720
|
-
},
|
|
721
|
-
relationships: {
|
|
722
|
-
authors: { type: 'manyToMany', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' },
|
|
723
|
-
reviews: {
|
|
724
|
-
type: 'hasMany',
|
|
725
|
-
target: 'reviews',
|
|
726
|
-
via: 'reviewable'
|
|
727
|
-
}
|
|
728
|
-
},
|
|
729
|
-
tableName: 'ext_books'
|
|
730
|
-
})
|
|
731
|
-
await api.resources.books.createKnexTable()
|
|
732
|
-
mapTable('ext_books', 'books')
|
|
733
|
-
|
|
734
|
-
// Book-Authors pivot with extended fields
|
|
735
|
-
await api.addResource('book_authors', {
|
|
736
|
-
schema: {
|
|
737
|
-
id: { type: 'id' },
|
|
738
|
-
book_id: { type: 'number', required: true, belongsTo: 'books', as: 'book' },
|
|
739
|
-
author_id: { type: 'number', required: true, belongsTo: 'authors', as: 'author' },
|
|
740
|
-
contribution_type: { type: 'string', max: 50 },
|
|
741
|
-
order: { type: 'number' }
|
|
742
|
-
},
|
|
743
|
-
tableName: 'ext_book_authors'
|
|
744
|
-
})
|
|
745
|
-
await api.resources.book_authors.createKnexTable()
|
|
746
|
-
mapTable('ext_book_authors', 'book_authors')
|
|
747
|
-
if (storageMode.isAnyApi()) {
|
|
748
|
-
const booksDescriptor = await api.anyapi.registry.getDescriptor(tenantId, 'books')
|
|
749
|
-
const authorsDescriptor = await api.anyapi.registry.getDescriptor(tenantId, 'authors')
|
|
750
|
-
const relationshipKey = booksDescriptor?.manyToMany?.authors?.relationship
|
|
751
|
-
const inverseRelationshipKey = authorsDescriptor?.manyToMany?.books?.relationship
|
|
752
|
-
storageMode.registerLink('ext_book_authors', 'books', 'authors', relationshipKey, inverseRelationshipKey)
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
// Polymorphic reviews (can go on authors, books and publishers)
|
|
756
|
-
await api.addResource('reviews', {
|
|
757
|
-
schema: {
|
|
758
|
-
id: { type: 'id' },
|
|
759
|
-
rating: { type: 'number', required: true, min: 1, max: 5 },
|
|
760
|
-
title: { type: 'string', max: 200 },
|
|
761
|
-
content: { type: 'string', required: true, max: 5000 },
|
|
762
|
-
reviewer_name: { type: 'string', required: true, max: 100 },
|
|
763
|
-
review_date: { type: 'dateTime', temporalPrecision: 3, defaultTo: () => new Date().toISOString() },
|
|
764
|
-
helpful_count: { type: 'number', default: 0 },
|
|
765
|
-
reviewable_type: { type: 'string', required: true },
|
|
766
|
-
reviewable_id: { type: 'number', required: true }
|
|
767
|
-
},
|
|
768
|
-
relationships: {
|
|
769
|
-
reviewable: {
|
|
770
|
-
belongsToPolymorphic: {
|
|
771
|
-
types: ['books', 'authors', 'publishers'],
|
|
772
|
-
typeField: 'reviewable_type',
|
|
773
|
-
idField: 'reviewable_id'
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
},
|
|
777
|
-
tableName: 'ext_reviews'
|
|
778
|
-
})
|
|
779
|
-
await api.resources.reviews.createKnexTable()
|
|
780
|
-
mapTable('ext_reviews', 'reviews')
|
|
781
|
-
})
|
|
782
|
-
|
|
783
|
-
return api
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
/**
|
|
787
|
-
* Creates an API with limited include depth for testing depth validation
|
|
788
|
-
* Uses 'limited_' prefix for all tables to avoid conflicts
|
|
789
|
-
*/
|
|
790
|
-
export async function createLimitedDepthApi (knex) {
|
|
791
|
-
const api = new Api({
|
|
792
|
-
name: 'limited-depth-api',
|
|
793
|
-
})
|
|
794
|
-
|
|
795
|
-
const tenantId = storageMode.isAnyApi() ? 'limited_depth_tenant' : storageMode.defaultTenant
|
|
796
|
-
|
|
797
|
-
await api.use(RestApiPlugin, {
|
|
798
|
-
simplifiedApi: false,
|
|
799
|
-
simplifiedTransport: false,
|
|
800
|
-
returnRecordApi: {
|
|
801
|
-
post: true,
|
|
802
|
-
put: false,
|
|
803
|
-
patch: false
|
|
804
|
-
},
|
|
805
|
-
returnRecordTransport: {
|
|
806
|
-
post: 'full',
|
|
807
|
-
put: 'no',
|
|
808
|
-
patch: 'minimal'
|
|
809
|
-
},
|
|
810
|
-
sortableFields: ['id', 'title', 'country_id', 'publisher_id', 'name', 'code'],
|
|
811
|
-
includeDepthLimit: 2 // Key difference: limit is 2 instead of default 3
|
|
812
|
-
})
|
|
813
|
-
await withTenantContext(tenantId, async () => {
|
|
814
|
-
await useStoragePlugin(api, knex, { tenantId })
|
|
815
|
-
|
|
816
|
-
// Use different table names with 'limited_' prefix to avoid conflicts
|
|
817
|
-
await api.addResource('countries', {
|
|
818
|
-
schema: {
|
|
819
|
-
id: { type: 'id' },
|
|
820
|
-
name: { type: 'string', required: true, max: 100 },
|
|
821
|
-
code: { type: 'string', max: 2, unique: true }
|
|
822
|
-
},
|
|
823
|
-
relationships: {
|
|
824
|
-
publishers: { type: 'hasMany', target: 'publishers', foreignKey: 'country_id' },
|
|
825
|
-
books: { type: 'hasMany', target: 'books', foreignKey: 'country_id' }
|
|
826
|
-
},
|
|
827
|
-
tableName: 'limited_countries',
|
|
828
|
-
})
|
|
829
|
-
await api.resources.countries.createKnexTable()
|
|
830
|
-
mapTable('limited_countries', 'countries')
|
|
831
|
-
|
|
832
|
-
await api.addResource('publishers', {
|
|
833
|
-
schema: {
|
|
834
|
-
id: { type: 'id' },
|
|
835
|
-
name: { type: 'string', required: true, max: 200 },
|
|
836
|
-
country_id: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' }
|
|
837
|
-
},
|
|
838
|
-
relationships: {
|
|
839
|
-
books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' },
|
|
840
|
-
authors: { type: 'hasMany', target: 'authors', foreignKey: 'publisher_id' }
|
|
841
|
-
},
|
|
842
|
-
tableName: 'limited_publishers'
|
|
843
|
-
})
|
|
844
|
-
await api.resources.publishers.createKnexTable()
|
|
845
|
-
mapTable('limited_publishers', 'publishers')
|
|
846
|
-
|
|
847
|
-
await api.addResource('authors', {
|
|
848
|
-
schema: {
|
|
849
|
-
id: { type: 'id' },
|
|
850
|
-
name: { type: 'string', required: true, max: 200 },
|
|
851
|
-
publisher_id: { type: 'number', nullable: true, belongsTo: 'publishers', as: 'publisher' }
|
|
852
|
-
},
|
|
853
|
-
relationships: {
|
|
854
|
-
books: { type: 'manyToMany', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' }
|
|
855
|
-
},
|
|
856
|
-
tableName: 'limited_authors'
|
|
857
|
-
})
|
|
858
|
-
await api.resources.authors.createKnexTable()
|
|
859
|
-
|
|
860
|
-
await api.addResource('books', {
|
|
861
|
-
schema: {
|
|
862
|
-
id: { type: 'id' },
|
|
863
|
-
title: { type: 'string', required: true, max: 300, search: true },
|
|
864
|
-
country_id: { type: 'number', required: true, belongsTo: 'countries', as: 'country', search: true },
|
|
865
|
-
publisher_id: { type: 'number', belongsTo: 'publishers', as: 'publisher', search: true }
|
|
866
|
-
},
|
|
867
|
-
relationships: {
|
|
868
|
-
authors: { type: 'manyToMany', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' }
|
|
869
|
-
},
|
|
870
|
-
tableName: 'limited_books'
|
|
871
|
-
})
|
|
872
|
-
await api.resources.books.createKnexTable()
|
|
873
|
-
|
|
874
|
-
await api.addResource('book_authors', {
|
|
875
|
-
schema: {
|
|
876
|
-
id: { type: 'id' },
|
|
877
|
-
book_id: { type: 'number', required: true, belongsTo: 'books', as: 'book' },
|
|
878
|
-
author_id: { type: 'number', required: true, belongsTo: 'authors', as: 'author' }
|
|
879
|
-
},
|
|
880
|
-
tableName: 'limited_book_authors'
|
|
881
|
-
})
|
|
882
|
-
await api.resources.book_authors.createKnexTable()
|
|
883
|
-
mapTable('limited_book_authors', 'book_authors')
|
|
884
|
-
if (storageMode.isAnyApi()) {
|
|
885
|
-
const booksDescriptor = await api.anyapi.registry.getDescriptor(tenantId, 'books')
|
|
886
|
-
const authorsDescriptor = await api.anyapi.registry.getDescriptor(tenantId, 'authors')
|
|
887
|
-
const relationshipKey = booksDescriptor?.manyToMany?.authors?.relationship
|
|
888
|
-
const inverseRelationshipKey = authorsDescriptor?.manyToMany?.books?.relationship
|
|
889
|
-
storageMode.registerLink('limited_book_authors', 'books', 'authors', relationshipKey, inverseRelationshipKey)
|
|
890
|
-
}
|
|
891
|
-
})
|
|
892
|
-
|
|
893
|
-
return api
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
/**
|
|
897
|
-
* Creates an API configuration for pagination testing
|
|
898
|
-
*/
|
|
899
|
-
export async function createPaginationApi (knex, options = {}) {
|
|
900
|
-
const api = new Api({
|
|
901
|
-
name: 'pagination-test-api',
|
|
902
|
-
})
|
|
903
|
-
|
|
904
|
-
const tenantId = storageMode.isAnyApi() ? 'pagination_tenant' : storageMode.defaultTenant
|
|
905
|
-
|
|
906
|
-
const restApiOptions = {
|
|
907
|
-
simplifiedApi: false,
|
|
908
|
-
simplifiedTransport: false,
|
|
909
|
-
returnRecordApi: {
|
|
910
|
-
post: true,
|
|
911
|
-
put: false,
|
|
912
|
-
patch: false
|
|
913
|
-
},
|
|
914
|
-
returnRecordTransport: {
|
|
915
|
-
post: 'full',
|
|
916
|
-
put: 'no',
|
|
917
|
-
patch: 'minimal'
|
|
918
|
-
},
|
|
919
|
-
sortableFields: ['id', 'title', 'country_id', 'publisher_id', 'name', 'code'],
|
|
920
|
-
...options // Allow overriding options likereturnBasePath, enablePaginationCounts
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
await api.use(RestApiPlugin, restApiOptions)
|
|
924
|
-
await withTenantContext(tenantId, async () => {
|
|
925
|
-
await useStoragePlugin(api, knex, { tenantId })
|
|
926
|
-
|
|
927
|
-
// Countries table
|
|
928
|
-
await api.addResource('countries', {
|
|
929
|
-
schema: {
|
|
930
|
-
id: { type: 'id' },
|
|
931
|
-
name: { type: 'string', required: true, max: 100 },
|
|
932
|
-
code: { type: 'string', max: 2, unique: true }
|
|
933
|
-
},
|
|
934
|
-
relationships: {
|
|
935
|
-
publishers: { type: 'hasMany', target: 'publishers', foreignKey: 'country_id' },
|
|
936
|
-
books: { type: 'hasMany', target: 'books', foreignKey: 'country_id' }
|
|
937
|
-
},
|
|
938
|
-
tableName: 'pagination_countries'
|
|
939
|
-
})
|
|
940
|
-
await api.resources.countries.createKnexTable()
|
|
941
|
-
mapTable('pagination_countries', 'countries')
|
|
942
|
-
|
|
943
|
-
// Publishers table
|
|
944
|
-
await api.addResource('publishers', {
|
|
945
|
-
schema: {
|
|
946
|
-
id: { type: 'id' },
|
|
947
|
-
name: { type: 'string', required: true, max: 200 },
|
|
948
|
-
country_id: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' }
|
|
949
|
-
},
|
|
950
|
-
relationships: {
|
|
951
|
-
books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' }
|
|
952
|
-
},
|
|
953
|
-
tableName: 'pagination_publishers'
|
|
954
|
-
})
|
|
955
|
-
await api.resources.publishers.createKnexTable()
|
|
956
|
-
mapTable('pagination_publishers', 'publishers')
|
|
957
|
-
|
|
958
|
-
// Books table
|
|
959
|
-
await api.addResource('books', {
|
|
960
|
-
schema: {
|
|
961
|
-
id: { type: 'id' },
|
|
962
|
-
title: { type: 'string', required: true, max: 300, search: true },
|
|
963
|
-
country_id: { type: 'number', required: true, belongsTo: 'countries', as: 'country', search: true },
|
|
964
|
-
publisher_id: { type: 'number', belongsTo: 'publishers', as: 'publisher', search: true }
|
|
965
|
-
},
|
|
966
|
-
tableName: 'pagination_books'
|
|
967
|
-
})
|
|
968
|
-
await api.resources.books.createKnexTable()
|
|
969
|
-
mapTable('pagination_books', 'books')
|
|
970
|
-
})
|
|
971
|
-
|
|
972
|
-
return api
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
/**
|
|
976
|
-
* Creates an API with WebSocket/Socket.IO support for testing
|
|
977
|
-
*/
|
|
978
|
-
export async function createWebSocketApi (knex, pluginOptions = {}) {
|
|
979
|
-
const { jwtVerify } = await import('jose')
|
|
980
|
-
|
|
981
|
-
const api = await createBasicApi(knex, {
|
|
982
|
-
...pluginOptions,
|
|
983
|
-
tenantId: pluginOptions.tenantId || 'socketio_tenant',
|
|
984
|
-
includeExpress: true,
|
|
985
|
-
express: {
|
|
986
|
-
port: 0 // Let OS assign a port
|
|
987
|
-
}
|
|
988
|
-
})
|
|
989
|
-
|
|
990
|
-
const socketioOptions = { ...(pluginOptions['socketio'] || {}) }
|
|
991
|
-
const authOptions = { ...(socketioOptions.auth || {}) }
|
|
992
|
-
const sharedSecret = authOptions.sharedSecret || socketioOptions.sharedSecret || 'test-secret-key'
|
|
993
|
-
|
|
994
|
-
if (!authOptions.authenticate) {
|
|
995
|
-
authOptions.authenticate = async ({ socket }) => {
|
|
996
|
-
const token = socket.handshake.auth?.token
|
|
997
|
-
if (!token) {
|
|
998
|
-
throw new Error('Authentication required')
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
const encoder = new TextEncoder()
|
|
1002
|
-
const key = encoder.encode(sharedSecret)
|
|
1003
|
-
const { payload } = await jwtVerify(token, key)
|
|
1004
|
-
|
|
1005
|
-
const roles = Array.isArray(payload.roles)
|
|
1006
|
-
? payload.roles
|
|
1007
|
-
: payload.role ? [payload.role] : []
|
|
1008
|
-
|
|
1009
|
-
const authContext = {
|
|
1010
|
-
userId: payload.userId || payload.sub,
|
|
1011
|
-
roles,
|
|
1012
|
-
token: payload
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
if (!authContext.userId) {
|
|
1016
|
-
throw new Error('Token is missing required user identifier')
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
return authContext
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
if (authOptions.requireAuth === undefined) {
|
|
1024
|
-
authOptions.requireAuth = true
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
socketioOptions.auth = authOptions
|
|
1028
|
-
|
|
1029
|
-
await api.use(SocketIOPlugin, socketioOptions)
|
|
1030
|
-
|
|
1031
|
-
// Create and start Express server
|
|
1032
|
-
const app = express()
|
|
1033
|
-
api.http.express.app = app
|
|
1034
|
-
|
|
1035
|
-
// Mount the API routes
|
|
1036
|
-
app.use('/api', api.http.express.router)
|
|
1037
|
-
|
|
1038
|
-
// Create HTTP server
|
|
1039
|
-
const server = createServer(app)
|
|
1040
|
-
|
|
1041
|
-
// Start Socket.IO server
|
|
1042
|
-
await api.startSocketServer(server)
|
|
1043
|
-
|
|
1044
|
-
// Start listening
|
|
1045
|
-
await new Promise((resolve) => {
|
|
1046
|
-
server.listen(0, () => {
|
|
1047
|
-
resolve()
|
|
1048
|
-
})
|
|
1049
|
-
})
|
|
1050
|
-
|
|
1051
|
-
return { api, server }
|
|
1052
|
-
}
|
|
1053
|
-
|
|
1054
|
-
/**
|
|
1055
|
-
* Creates an API with computed fields for testing
|
|
1056
|
-
*/
|
|
1057
|
-
export async function createComputedFieldsApi (knex, pluginOptions = {}) {
|
|
1058
|
-
const api = new Api({
|
|
1059
|
-
name: 'computed-fields-test-api',
|
|
1060
|
-
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
1061
|
-
})
|
|
1062
|
-
|
|
1063
|
-
const tenantId = storageMode.isAnyApi() ? 'computed_fields_tenant' : storageMode.defaultTenant
|
|
1064
|
-
|
|
1065
|
-
await api.use(RestApiPlugin, {
|
|
1066
|
-
simplifiedApi: true,
|
|
1067
|
-
simplifiedTransport: true
|
|
1068
|
-
})
|
|
1069
|
-
|
|
1070
|
-
await withTenantContext(tenantId, async () => {
|
|
1071
|
-
await useStoragePlugin(api, knex, { tenantId })
|
|
1072
|
-
await resetAnyApiTables(knex)
|
|
1073
|
-
|
|
1074
|
-
// Products resource with computed fields
|
|
1075
|
-
await api.addResource('products', {
|
|
1076
|
-
schema: {
|
|
1077
|
-
id: { type: 'id' },
|
|
1078
|
-
name: { type: 'string', required: true, max: 255 },
|
|
1079
|
-
price: { type: 'number', required: true, min: 0 },
|
|
1080
|
-
cost: { type: 'number', required: true, min: 0, normallyHidden: true },
|
|
1081
|
-
internal_notes: { type: 'string', normallyHidden: true },
|
|
1082
|
-
profit_margin: {
|
|
1083
|
-
type: 'number',
|
|
1084
|
-
computed: true,
|
|
1085
|
-
dependencies: ['price', 'cost'],
|
|
1086
|
-
compute: ({ attributes }) => {
|
|
1087
|
-
if (!attributes.price || attributes.price === 0) return 0
|
|
1088
|
-
return Number(((attributes.price - attributes.cost) / attributes.price * 100).toFixed(2))
|
|
1089
|
-
}
|
|
1090
|
-
},
|
|
1091
|
-
profit_amount: {
|
|
1092
|
-
type: 'number',
|
|
1093
|
-
computed: true,
|
|
1094
|
-
dependencies: ['price', 'cost'],
|
|
1095
|
-
compute: ({ attributes }) => {
|
|
1096
|
-
return Number((attributes.price - attributes.cost).toFixed(2))
|
|
1097
|
-
}
|
|
1098
|
-
},
|
|
1099
|
-
calculated_at: {
|
|
1100
|
-
type: 'dateTime',
|
|
1101
|
-
temporalPrecision: 0,
|
|
1102
|
-
computed: true,
|
|
1103
|
-
compute: () => new Date('2026-08-25T23:45:01.987Z')
|
|
1104
|
-
},
|
|
1105
|
-
calculated_time: {
|
|
1106
|
-
type: 'time',
|
|
1107
|
-
temporalPrecision: 3,
|
|
1108
|
-
computed: true,
|
|
1109
|
-
compute: () => new Date('2026-08-25T23:45:01.987Z')
|
|
1110
|
-
}
|
|
1111
|
-
},
|
|
1112
|
-
relationships: {
|
|
1113
|
-
reviews: { type: 'hasMany', target: 'reviews', foreignKey: 'product_id' }
|
|
1114
|
-
},
|
|
1115
|
-
tableName: 'test_products'
|
|
1116
|
-
})
|
|
1117
|
-
await api.resources.products.createKnexTable()
|
|
1118
|
-
mapTable('searchmerge_products', 'products')
|
|
1119
|
-
mapTable('test_products', 'products')
|
|
1120
|
-
|
|
1121
|
-
// Reviews resource with computed fields
|
|
1122
|
-
await api.addResource('reviews', {
|
|
1123
|
-
schema: {
|
|
1124
|
-
id: { type: 'id' },
|
|
1125
|
-
product_id: { type: 'number', required: true, belongsTo: 'products', as: 'product' },
|
|
1126
|
-
reviewer_name: { type: 'string', required: true },
|
|
1127
|
-
rating: { type: 'number', required: true, min: 1, max: 5 },
|
|
1128
|
-
comment: { type: 'string', max: 1000 },
|
|
1129
|
-
helpful_votes: { type: 'number', default: 0 },
|
|
1130
|
-
total_votes: { type: 'number', default: 0 },
|
|
1131
|
-
spam_score: { type: 'number', default: 0, normallyHidden: true },
|
|
1132
|
-
helpfulness_score: {
|
|
1133
|
-
type: 'number',
|
|
1134
|
-
computed: true,
|
|
1135
|
-
dependencies: ['helpful_votes', 'total_votes'],
|
|
1136
|
-
compute: ({ attributes }) => {
|
|
1137
|
-
if (attributes.total_votes === 0) return null
|
|
1138
|
-
return Number(((attributes.helpful_votes / attributes.total_votes) * 100).toFixed(0))
|
|
1139
|
-
}
|
|
1140
|
-
},
|
|
1141
|
-
is_helpful: {
|
|
1142
|
-
type: 'boolean',
|
|
1143
|
-
computed: true,
|
|
1144
|
-
dependencies: ['helpful_votes', 'total_votes', 'spam_score'],
|
|
1145
|
-
compute: ({ attributes }) => {
|
|
1146
|
-
if (attributes.total_votes < 10) return null
|
|
1147
|
-
const helpfulnessScore = (attributes.helpful_votes / attributes.total_votes) * 100
|
|
1148
|
-
return helpfulnessScore > 70 && attributes.spam_score < 0.5
|
|
1149
|
-
}
|
|
1150
|
-
},
|
|
1151
|
-
calculated_at: {
|
|
1152
|
-
type: 'dateTime',
|
|
1153
|
-
temporalPrecision: 0,
|
|
1154
|
-
computed: true,
|
|
1155
|
-
compute: () => new Date('2026-08-25T23:46:02.654Z')
|
|
1156
|
-
}
|
|
1157
|
-
},
|
|
1158
|
-
tableName: 'test_reviews'
|
|
1159
|
-
})
|
|
1160
|
-
await api.resources.reviews.createKnexTable()
|
|
1161
|
-
mapTable('test_reviews', 'reviews')
|
|
1162
|
-
})
|
|
1163
|
-
|
|
1164
|
-
return api
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
/**
|
|
1168
|
-
* Creates an API with field getters for testing
|
|
1169
|
-
*/
|
|
1170
|
-
export async function createFieldGettersApi (knex, pluginOptions = {}) {
|
|
1171
|
-
const api = new Api({
|
|
1172
|
-
name: 'field-getters-test-api',
|
|
1173
|
-
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
1174
|
-
})
|
|
1175
|
-
|
|
1176
|
-
const tenantId = storageMode.isAnyApi() ? 'field_getters_tenant' : storageMode.defaultTenant
|
|
1177
|
-
const previousTenant = storageMode.currentTenant
|
|
1178
|
-
if (storageMode.isAnyApi()) {
|
|
1179
|
-
storageMode.setCurrentTenant(tenantId)
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
await api.use(RestApiPlugin, {
|
|
1183
|
-
simplifiedApi: true,
|
|
1184
|
-
simplifiedTransport: true
|
|
1185
|
-
})
|
|
1186
|
-
|
|
1187
|
-
await useStoragePlugin(api, knex, { tenantId })
|
|
1188
|
-
await resetAnyApiTables(knex)
|
|
1189
|
-
|
|
1190
|
-
// Users resource with basic getters
|
|
1191
|
-
await api.addResource('users', {
|
|
1192
|
-
schema: {
|
|
1193
|
-
id: { type: 'id' },
|
|
1194
|
-
email: {
|
|
1195
|
-
type: 'string',
|
|
1196
|
-
nullable: true,
|
|
1197
|
-
getter: (value) => value?.toLowerCase().trim()
|
|
1198
|
-
},
|
|
1199
|
-
name: {
|
|
1200
|
-
type: 'string',
|
|
1201
|
-
nullable: true,
|
|
1202
|
-
getter: (value) => value?.trim()
|
|
1203
|
-
},
|
|
1204
|
-
phone: {
|
|
1205
|
-
type: 'string',
|
|
1206
|
-
nullable: true,
|
|
1207
|
-
getter: (value) => {
|
|
1208
|
-
if (!value) return null
|
|
1209
|
-
// Remove all non-digits and format
|
|
1210
|
-
const digits = value.replace(/\D/g, '')
|
|
1211
|
-
if (digits.length === 10) {
|
|
1212
|
-
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`
|
|
1213
|
-
}
|
|
1214
|
-
return value
|
|
1215
|
-
}
|
|
1216
|
-
},
|
|
1217
|
-
metadata_json: {
|
|
1218
|
-
type: 'string',
|
|
1219
|
-
nullable: true,
|
|
1220
|
-
getter: (value) => {
|
|
1221
|
-
if (!value) return {}
|
|
1222
|
-
try {
|
|
1223
|
-
return JSON.parse(value)
|
|
1224
|
-
} catch {
|
|
1225
|
-
return {}
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
},
|
|
1229
|
-
tags_csv: {
|
|
1230
|
-
type: 'string',
|
|
1231
|
-
nullable: true,
|
|
1232
|
-
getter: (value) => {
|
|
1233
|
-
if (!value) return []
|
|
1234
|
-
return value.split(',').map(t => t.trim()).filter(t => t)
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
},
|
|
1238
|
-
tableName: 'getter_users'
|
|
1239
|
-
})
|
|
1240
|
-
await api.resources.users.createKnexTable()
|
|
1241
|
-
mapTable('searchmerge_users', 'users')
|
|
1242
|
-
mapTable('getter_users', 'users')
|
|
1243
|
-
|
|
1244
|
-
// Products resource with getters and computed fields
|
|
1245
|
-
await api.addResource('products', {
|
|
1246
|
-
schema: {
|
|
1247
|
-
id: { type: 'id' },
|
|
1248
|
-
name: {
|
|
1249
|
-
type: 'string',
|
|
1250
|
-
getter: (value) => value?.toUpperCase()
|
|
1251
|
-
},
|
|
1252
|
-
description: {
|
|
1253
|
-
type: 'string',
|
|
1254
|
-
getter: (value) => {
|
|
1255
|
-
// Truncate long descriptions
|
|
1256
|
-
if (!value) return ''
|
|
1257
|
-
return value.length > 50 ? value.substring(0, 47) + '...' : value
|
|
1258
|
-
}
|
|
1259
|
-
},
|
|
1260
|
-
price_str: {
|
|
1261
|
-
type: 'string',
|
|
1262
|
-
getter: (value) => parseFloat(value) || 0
|
|
1263
|
-
},
|
|
1264
|
-
tax_rate_str: {
|
|
1265
|
-
type: 'string',
|
|
1266
|
-
getter: (value) => parseFloat(value) || 0
|
|
1267
|
-
},
|
|
1268
|
-
total_price: {
|
|
1269
|
-
type: 'number',
|
|
1270
|
-
computed: true,
|
|
1271
|
-
dependencies: ['price_str', 'tax_rate_str'],
|
|
1272
|
-
compute: ({ attributes }) => {
|
|
1273
|
-
// Should see numbers from getters, not strings
|
|
1274
|
-
return attributes.price_str * (1 + attributes.tax_rate_str)
|
|
1275
|
-
}
|
|
1276
|
-
}
|
|
1277
|
-
},
|
|
1278
|
-
relationships: {
|
|
1279
|
-
reviews: { type: 'hasMany', target: 'reviews', foreignKey: 'product_id' }
|
|
1280
|
-
},
|
|
1281
|
-
tableName: 'getter_products'
|
|
1282
|
-
})
|
|
1283
|
-
await api.resources.products.createKnexTable()
|
|
1284
|
-
mapTable('getter_products', 'products')
|
|
1285
|
-
|
|
1286
|
-
// Reviews resource with getters
|
|
1287
|
-
await api.addResource('reviews', {
|
|
1288
|
-
schema: {
|
|
1289
|
-
id: { type: 'id' },
|
|
1290
|
-
product_id: { type: 'id', belongsTo: 'products', as: 'product' },
|
|
1291
|
-
content: {
|
|
1292
|
-
type: 'string',
|
|
1293
|
-
getter: (value) => `[REVIEW] ${value}`
|
|
1294
|
-
},
|
|
1295
|
-
rating: { type: 'number' }
|
|
1296
|
-
},
|
|
1297
|
-
tableName: 'getter_reviews'
|
|
1298
|
-
})
|
|
1299
|
-
await api.resources.reviews.createKnexTable()
|
|
1300
|
-
mapTable('getter_reviews', 'reviews')
|
|
1301
|
-
|
|
1302
|
-
// Resource with getter dependencies
|
|
1303
|
-
await api.addResource('formatted_data', {
|
|
1304
|
-
schema: {
|
|
1305
|
-
id: { type: 'id' },
|
|
1306
|
-
step1: {
|
|
1307
|
-
type: 'string',
|
|
1308
|
-
nullable: true,
|
|
1309
|
-
getter: (value) => value?.trim()
|
|
1310
|
-
},
|
|
1311
|
-
step2: {
|
|
1312
|
-
type: 'string',
|
|
1313
|
-
nullable: true,
|
|
1314
|
-
getter: (value, { attributes }) => {
|
|
1315
|
-
if (!value) return value // Return null/undefined as-is
|
|
1316
|
-
// Depends on step1 being trimmed first
|
|
1317
|
-
return `${value} [step1: ${attributes.step1}]`
|
|
1318
|
-
},
|
|
1319
|
-
runGetterAfter: ['step1']
|
|
1320
|
-
},
|
|
1321
|
-
step3: {
|
|
1322
|
-
type: 'string',
|
|
1323
|
-
getter: (value, { attributes }) => {
|
|
1324
|
-
if (!value) return value // Return null/undefined as-is
|
|
1325
|
-
// Depends on step2
|
|
1326
|
-
return `${value} [step2: ${attributes.step2}]`
|
|
1327
|
-
},
|
|
1328
|
-
runGetterAfter: ['step2']
|
|
1329
|
-
}
|
|
1330
|
-
},
|
|
1331
|
-
tableName: 'getter_formatted'
|
|
1332
|
-
})
|
|
1333
|
-
await api.resources.formatted_data.createKnexTable()
|
|
1334
|
-
mapTable('getter_formatted', 'formatted_data')
|
|
1335
|
-
|
|
1336
|
-
// Async getters example
|
|
1337
|
-
await api.addResource('encrypted_data', {
|
|
1338
|
-
schema: {
|
|
1339
|
-
id: { type: 'id' },
|
|
1340
|
-
secret: {
|
|
1341
|
-
type: 'string',
|
|
1342
|
-
nullable: true,
|
|
1343
|
-
getter: async (value) => {
|
|
1344
|
-
if (!value) return null
|
|
1345
|
-
// Simulate async decryption
|
|
1346
|
-
await new Promise(resolve => setTimeout(resolve, 5))
|
|
1347
|
-
return Buffer.from(value, 'base64').toString()
|
|
1348
|
-
}
|
|
1349
|
-
},
|
|
1350
|
-
data: {
|
|
1351
|
-
type: 'string',
|
|
1352
|
-
nullable: true,
|
|
1353
|
-
getter: async (value, context) => {
|
|
1354
|
-
if (!value) return null
|
|
1355
|
-
await new Promise(resolve => setTimeout(resolve, 5))
|
|
1356
|
-
const decrypted = Buffer.from(value, 'base64').toString()
|
|
1357
|
-
return `[${context.scopeName}] ${decrypted}`
|
|
1358
|
-
}
|
|
1359
|
-
}
|
|
1360
|
-
},
|
|
1361
|
-
tableName: 'getter_encrypted'
|
|
1362
|
-
})
|
|
1363
|
-
await api.resources.encrypted_data.createKnexTable()
|
|
1364
|
-
mapTable('getter_encrypted', 'encrypted_data')
|
|
1365
|
-
|
|
1366
|
-
if (storageMode.isAnyApi()) {
|
|
1367
|
-
storageMode.setCurrentTenant(previousTenant)
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
return api
|
|
1371
|
-
}
|
|
1372
|
-
|
|
1373
|
-
export async function createProjectedFieldsApi (knex, pluginOptions = {}) {
|
|
1374
|
-
const api = new Api({
|
|
1375
|
-
name: 'projected-fields-test-api',
|
|
1376
|
-
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
1377
|
-
})
|
|
1378
|
-
|
|
1379
|
-
const tenantId = storageMode.isAnyApi() ? 'projected_fields_tenant' : storageMode.defaultTenant
|
|
1380
|
-
const previousTenant = storageMode.currentTenant
|
|
1381
|
-
if (storageMode.isAnyApi()) {
|
|
1382
|
-
storageMode.setCurrentTenant(tenantId)
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
await api.use(RestApiPlugin, {
|
|
1386
|
-
simplifiedApi: false,
|
|
1387
|
-
simplifiedTransport: false,
|
|
1388
|
-
returnRecordApi: {
|
|
1389
|
-
post: true,
|
|
1390
|
-
put: false,
|
|
1391
|
-
patch: false
|
|
1392
|
-
},
|
|
1393
|
-
returnRecordTransport: {
|
|
1394
|
-
post: 'full',
|
|
1395
|
-
put: 'no',
|
|
1396
|
-
patch: 'minimal'
|
|
1397
|
-
},
|
|
1398
|
-
sortableFields: ['id', 'first_name', 'last_name'],
|
|
1399
|
-
...pluginOptions['rest-api']
|
|
1400
|
-
})
|
|
1401
|
-
await api.use(QueryProjectionsPlugin)
|
|
1402
|
-
|
|
1403
|
-
await useStoragePlugin(api, knex, { tenantId })
|
|
1404
|
-
await resetAnyApiTables(knex)
|
|
1405
|
-
|
|
1406
|
-
await api.addResource('authors', {
|
|
1407
|
-
schema: {
|
|
1408
|
-
id: { type: 'id' },
|
|
1409
|
-
first_name: { type: 'string', required: true, max: 100 },
|
|
1410
|
-
last_name: { type: 'string', required: true, max: 100 }
|
|
1411
|
-
},
|
|
1412
|
-
queryFields: {
|
|
1413
|
-
full_name: {
|
|
1414
|
-
type: 'string',
|
|
1415
|
-
sortable: true,
|
|
1416
|
-
select: ({ knex, column }) => knex.raw(
|
|
1417
|
-
"trim(coalesce(??, '') || ' ' || coalesce(??, ''))",
|
|
1418
|
-
[column('first_name'), column('last_name')]
|
|
1419
|
-
)
|
|
1420
|
-
},
|
|
1421
|
-
sort_name: {
|
|
1422
|
-
type: 'string',
|
|
1423
|
-
sortable: true,
|
|
1424
|
-
normallyHidden: true,
|
|
1425
|
-
select: ({ knex, column }) => knex.raw(
|
|
1426
|
-
"trim(coalesce(??, '') || ' ' || coalesce(??, ''))",
|
|
1427
|
-
[column('first_name'), column('last_name')]
|
|
1428
|
-
)
|
|
1429
|
-
}
|
|
1430
|
-
},
|
|
1431
|
-
relationships: {
|
|
1432
|
-
books: { type: 'hasMany', target: 'books', foreignKey: 'author_id' }
|
|
1433
|
-
},
|
|
1434
|
-
sortableFields: ['id', 'first_name', 'last_name'],
|
|
1435
|
-
defaultSort: ['full_name'],
|
|
1436
|
-
tableName: 'projected_authors'
|
|
1437
|
-
})
|
|
1438
|
-
await api.resources.authors.createKnexTable()
|
|
1439
|
-
mapTable('projected_authors', 'authors')
|
|
1440
|
-
|
|
1441
|
-
await api.addResource('books', {
|
|
1442
|
-
schema: {
|
|
1443
|
-
id: { type: 'id' },
|
|
1444
|
-
title: { type: 'string', required: true, max: 200 },
|
|
1445
|
-
author_id: { type: 'id', required: true, belongsTo: 'authors', as: 'author' }
|
|
1446
|
-
},
|
|
1447
|
-
tableName: 'projected_books'
|
|
1448
|
-
})
|
|
1449
|
-
await api.resources.books.createKnexTable()
|
|
1450
|
-
mapTable('projected_books', 'books')
|
|
1451
|
-
|
|
1452
|
-
if (storageMode.isAnyApi()) {
|
|
1453
|
-
storageMode.setCurrentTenant(previousTenant)
|
|
1454
|
-
}
|
|
1455
|
-
|
|
1456
|
-
return api
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
/**
|
|
1460
|
-
* Creates an API with field setters for testing
|
|
1461
|
-
*/
|
|
1462
|
-
export async function createFieldSettersApi (knex, pluginOptions = {}) {
|
|
1463
|
-
const api = new Api({
|
|
1464
|
-
name: 'field-setters-test-api',
|
|
1465
|
-
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
1466
|
-
})
|
|
1467
|
-
|
|
1468
|
-
const tenantId = storageMode.isAnyApi() ? 'field_setters_tenant' : storageMode.defaultTenant
|
|
1469
|
-
const previousTenant = storageMode.currentTenant
|
|
1470
|
-
if (storageMode.isAnyApi()) {
|
|
1471
|
-
storageMode.setCurrentTenant(tenantId)
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
await api.use(RestApiPlugin, {
|
|
1475
|
-
simplifiedApi: true,
|
|
1476
|
-
simplifiedTransport: true,
|
|
1477
|
-
returnRecordApi: {
|
|
1478
|
-
post: true,
|
|
1479
|
-
put: true,
|
|
1480
|
-
patch: true
|
|
1481
|
-
}
|
|
1482
|
-
})
|
|
1483
|
-
|
|
1484
|
-
await useStoragePlugin(api, knex, { tenantId })
|
|
1485
|
-
await resetAnyApiTables(knex)
|
|
1486
|
-
|
|
1487
|
-
// Users resource with basic setters
|
|
1488
|
-
await api.addResource('users', {
|
|
1489
|
-
schema: {
|
|
1490
|
-
id: { type: 'id' },
|
|
1491
|
-
email: {
|
|
1492
|
-
type: 'string',
|
|
1493
|
-
required: true,
|
|
1494
|
-
setter: (value) => value?.toLowerCase().trim()
|
|
1495
|
-
},
|
|
1496
|
-
username: {
|
|
1497
|
-
type: 'string',
|
|
1498
|
-
required: true,
|
|
1499
|
-
setter: (value) => value?.toLowerCase().replace(/\s+/g, '')
|
|
1500
|
-
},
|
|
1501
|
-
tags: {
|
|
1502
|
-
type: 'string', // Accepts string, stored as string
|
|
1503
|
-
setter: (value) => value // Pass through
|
|
1504
|
-
},
|
|
1505
|
-
preferences: {
|
|
1506
|
-
type: 'string', // Accepts string, stored as string
|
|
1507
|
-
setter: (value) => value // Pass through
|
|
1508
|
-
}
|
|
1509
|
-
},
|
|
1510
|
-
tableName: 'setter_users'
|
|
1511
|
-
})
|
|
1512
|
-
await api.resources.users.createKnexTable()
|
|
1513
|
-
|
|
1514
|
-
// Products with type conversion setters
|
|
1515
|
-
await api.addResource('products', {
|
|
1516
|
-
schema: {
|
|
1517
|
-
id: { type: 'id' },
|
|
1518
|
-
name: { type: 'string', required: true },
|
|
1519
|
-
price: {
|
|
1520
|
-
type: 'number',
|
|
1521
|
-
required: true,
|
|
1522
|
-
min: 0,
|
|
1523
|
-
setter: (value) => {
|
|
1524
|
-
// Convert to cents and round
|
|
1525
|
-
return Math.round(value * 100)
|
|
1526
|
-
}
|
|
1527
|
-
},
|
|
1528
|
-
discount_percent: {
|
|
1529
|
-
type: 'number',
|
|
1530
|
-
min: 0,
|
|
1531
|
-
max: 100,
|
|
1532
|
-
setter: (value) => Math.round(value)
|
|
1533
|
-
},
|
|
1534
|
-
metadata: {
|
|
1535
|
-
type: 'object',
|
|
1536
|
-
setter: (value) => JSON.stringify(value || {})
|
|
1537
|
-
}
|
|
1538
|
-
},
|
|
1539
|
-
tableName: 'setter_products'
|
|
1540
|
-
})
|
|
1541
|
-
await api.resources.products.createKnexTable()
|
|
1542
|
-
|
|
1543
|
-
// Secure data with async setters
|
|
1544
|
-
await api.addResource('secure_data', {
|
|
1545
|
-
schema: {
|
|
1546
|
-
id: { type: 'id' },
|
|
1547
|
-
password: {
|
|
1548
|
-
type: 'string',
|
|
1549
|
-
required: true,
|
|
1550
|
-
min: 8,
|
|
1551
|
-
setter: async (value) => {
|
|
1552
|
-
// Simulate async password hashing
|
|
1553
|
-
await new Promise(resolve => setTimeout(resolve, 5))
|
|
1554
|
-
return `hashed:${value}`
|
|
1555
|
-
}
|
|
1556
|
-
},
|
|
1557
|
-
api_key: {
|
|
1558
|
-
type: 'string',
|
|
1559
|
-
setter: async (value) => {
|
|
1560
|
-
if (!value) return null
|
|
1561
|
-
// Simulate encryption
|
|
1562
|
-
await new Promise(resolve => setTimeout(resolve, 5))
|
|
1563
|
-
return Buffer.from(value).toString('base64')
|
|
1564
|
-
}
|
|
1565
|
-
},
|
|
1566
|
-
data: {
|
|
1567
|
-
type: 'string',
|
|
1568
|
-
setter: async (value) => {
|
|
1569
|
-
if (!value) return null
|
|
1570
|
-
return Buffer.from(value).toString('base64')
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1573
|
-
},
|
|
1574
|
-
tableName: 'setter_secure'
|
|
1575
|
-
})
|
|
1576
|
-
await api.resources.secure_data.createKnexTable()
|
|
1577
|
-
|
|
1578
|
-
// Resource with setter dependencies
|
|
1579
|
-
await api.addResource('computed_data', {
|
|
1580
|
-
schema: {
|
|
1581
|
-
id: { type: 'id' },
|
|
1582
|
-
base_value: {
|
|
1583
|
-
type: 'number',
|
|
1584
|
-
setter: (value) => value || 0
|
|
1585
|
-
},
|
|
1586
|
-
multiplier: {
|
|
1587
|
-
type: 'number',
|
|
1588
|
-
setter: (value) => value || 1
|
|
1589
|
-
},
|
|
1590
|
-
adjustment: {
|
|
1591
|
-
type: 'number',
|
|
1592
|
-
setter: (value) => value || 0
|
|
1593
|
-
},
|
|
1594
|
-
calculated_value: {
|
|
1595
|
-
type: 'number',
|
|
1596
|
-
setter: (value, { attributes }) => {
|
|
1597
|
-
// Calculate based on other fields
|
|
1598
|
-
return (attributes.base_value || 0) * (attributes.multiplier || 1)
|
|
1599
|
-
},
|
|
1600
|
-
runSetterAfter: ['base_value', 'multiplier']
|
|
1601
|
-
},
|
|
1602
|
-
final_value: {
|
|
1603
|
-
type: 'number',
|
|
1604
|
-
setter: (value, { attributes }) => {
|
|
1605
|
-
// Depends on calculated_value
|
|
1606
|
-
return (attributes.calculated_value || 0) + (attributes.adjustment || 0)
|
|
1607
|
-
},
|
|
1608
|
-
runSetterAfter: ['calculated_value', 'adjustment']
|
|
1609
|
-
}
|
|
1610
|
-
},
|
|
1611
|
-
tableName: 'setter_computed'
|
|
1612
|
-
})
|
|
1613
|
-
await api.resources.computed_data.createKnexTable()
|
|
1614
|
-
|
|
1615
|
-
// Nullable fields handling
|
|
1616
|
-
await api.addResource('nullable_data', {
|
|
1617
|
-
schema: {
|
|
1618
|
-
id: { type: 'id' },
|
|
1619
|
-
field1: {
|
|
1620
|
-
type: 'string',
|
|
1621
|
-
nullable: true,
|
|
1622
|
-
setter: (value) => value === undefined ? null : value
|
|
1623
|
-
},
|
|
1624
|
-
field2: {
|
|
1625
|
-
type: 'string',
|
|
1626
|
-
nullable: true,
|
|
1627
|
-
setter: (value) => value === undefined ? null : value
|
|
1628
|
-
},
|
|
1629
|
-
field3: {
|
|
1630
|
-
type: 'string',
|
|
1631
|
-
setter: (value) => value === '' ? 'empty' : value
|
|
1632
|
-
},
|
|
1633
|
-
field4: {
|
|
1634
|
-
type: 'number',
|
|
1635
|
-
setter: (value) => value === 0 ? -1 : value
|
|
1636
|
-
}
|
|
1637
|
-
},
|
|
1638
|
-
tableName: 'setter_nullable'
|
|
1639
|
-
})
|
|
1640
|
-
await api.resources.nullable_data.createKnexTable()
|
|
1641
|
-
|
|
1642
|
-
// Validated data
|
|
1643
|
-
await api.addResource('validated_data', {
|
|
1644
|
-
schema: {
|
|
1645
|
-
id: { type: 'id' },
|
|
1646
|
-
email: {
|
|
1647
|
-
type: 'string',
|
|
1648
|
-
required: true,
|
|
1649
|
-
format: 'email',
|
|
1650
|
-
setter: (value) => value.toLowerCase().trim()
|
|
1651
|
-
},
|
|
1652
|
-
age: {
|
|
1653
|
-
type: 'number',
|
|
1654
|
-
min: 0,
|
|
1655
|
-
max: 120,
|
|
1656
|
-
setter: (value) => value // No transformation, just to verify it's a number
|
|
1657
|
-
},
|
|
1658
|
-
score: {
|
|
1659
|
-
type: 'number',
|
|
1660
|
-
min: 0,
|
|
1661
|
-
max: 100,
|
|
1662
|
-
setter: (value) => Math.ceil(value) // Round up
|
|
1663
|
-
}
|
|
1664
|
-
},
|
|
1665
|
-
tableName: 'setter_validated'
|
|
1666
|
-
})
|
|
1667
|
-
await api.resources.validated_data.createKnexTable()
|
|
1668
|
-
|
|
1669
|
-
if (storageMode.isAnyApi()) {
|
|
1670
|
-
storageMode.setCurrentTenant(previousTenant)
|
|
1671
|
-
}
|
|
1672
|
-
|
|
1673
|
-
return api
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
export async function createAutoFilterApi (knex, pluginOptions = {}) {
|
|
1677
|
-
const { AutoFilterPlugin } = await import('../../plugins/core/rest-api-autofilter-plugin.js')
|
|
1678
|
-
|
|
1679
|
-
const api = new Api({
|
|
1680
|
-
name: 'autofilter-test-api',
|
|
1681
|
-
})
|
|
1682
|
-
|
|
1683
|
-
const tenantId = storageMode.isAnyApi() ? 'autofilter_tenant' : storageMode.defaultTenant
|
|
1684
|
-
const previousTenant = storageMode.currentTenant
|
|
1685
|
-
if (storageMode.isAnyApi()) {
|
|
1686
|
-
storageMode.setCurrentTenant(tenantId)
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
await api.use(RestApiPlugin, {
|
|
1690
|
-
simplifiedApi: false,
|
|
1691
|
-
simplifiedTransport: false,
|
|
1692
|
-
returnRecordApi: {
|
|
1693
|
-
post: true,
|
|
1694
|
-
put: false,
|
|
1695
|
-
patch: false
|
|
1696
|
-
},
|
|
1697
|
-
returnRecordTransport: {
|
|
1698
|
-
post: 'full',
|
|
1699
|
-
put: 'no',
|
|
1700
|
-
patch: 'minimal'
|
|
1701
|
-
},
|
|
1702
|
-
sortableFields: ['id', 'title', 'name', 'workspace_id', 'user_id']
|
|
1703
|
-
})
|
|
1704
|
-
|
|
1705
|
-
await useStoragePlugin(api, knex, { tenantId })
|
|
1706
|
-
await resetAnyApiTables(knex)
|
|
1707
|
-
|
|
1708
|
-
// Add Express plugin if requested for transport testing
|
|
1709
|
-
if (pluginOptions.includeExpress) {
|
|
1710
|
-
await api.use(ExpressPlugin, pluginOptions.express || {})
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
const autofilterOptions = pluginOptions.autofilter || {}
|
|
1714
|
-
const workspaceField = pluginOptions.workspaceField || 'workspace_id'
|
|
1715
|
-
const userField = pluginOptions.userField || 'user_id'
|
|
1716
|
-
|
|
1717
|
-
await api.use(AutoFilterPlugin, {
|
|
1718
|
-
...autofilterOptions,
|
|
1719
|
-
resolvers: {
|
|
1720
|
-
workspace: ({ context }) => context.scopeValues?.workspaceId,
|
|
1721
|
-
user: ({ context }) => context.scopeValues?.userId,
|
|
1722
|
-
currentProject: ({ context }) => context.scopeValues?.projectId ?? null,
|
|
1723
|
-
...(autofilterOptions.resolvers || {})
|
|
1724
|
-
},
|
|
1725
|
-
presets: {
|
|
1726
|
-
public: { filters: [] },
|
|
1727
|
-
workspace: {
|
|
1728
|
-
filters: [{ field: workspaceField, resolver: 'workspace' }]
|
|
1729
|
-
},
|
|
1730
|
-
user: {
|
|
1731
|
-
filters: [{ field: userField, resolver: 'user' }]
|
|
1732
|
-
},
|
|
1733
|
-
workspace_user: {
|
|
1734
|
-
filters: [
|
|
1735
|
-
{ field: workspaceField, resolver: 'workspace' },
|
|
1736
|
-
{ field: userField, resolver: 'user' }
|
|
1737
|
-
]
|
|
1738
|
-
},
|
|
1739
|
-
...(autofilterOptions.presets || {})
|
|
1740
|
-
}
|
|
1741
|
-
})
|
|
1742
|
-
|
|
1743
|
-
await api.addResource('workspace_reports', {
|
|
1744
|
-
schema: {
|
|
1745
|
-
id: { type: 'id' },
|
|
1746
|
-
title: { type: 'string', required: true, max: 200 },
|
|
1747
|
-
workspace_id: { type: 'string', required: true }
|
|
1748
|
-
},
|
|
1749
|
-
autofilter: 'workspace',
|
|
1750
|
-
tableName: 'autofilter_workspace_reports'
|
|
1751
|
-
})
|
|
1752
|
-
await api.resources.workspace_reports.createKnexTable()
|
|
1753
|
-
mapTable('autofilter_workspace_reports', 'workspace_reports')
|
|
1754
|
-
|
|
1755
|
-
await api.addResource('user_notes', {
|
|
1756
|
-
schema: {
|
|
1757
|
-
id: { type: 'id' },
|
|
1758
|
-
body: { type: 'string', required: true, max: 500 },
|
|
1759
|
-
user_id: { type: 'number', required: true }
|
|
1760
|
-
},
|
|
1761
|
-
autofilter: 'user',
|
|
1762
|
-
tableName: 'autofilter_user_notes'
|
|
1763
|
-
})
|
|
1764
|
-
await api.resources.user_notes.createKnexTable()
|
|
1765
|
-
mapTable('autofilter_user_notes', 'user_notes')
|
|
1766
|
-
|
|
1767
|
-
await api.addResource('projects', {
|
|
1768
|
-
schema: {
|
|
1769
|
-
id: { type: 'id' },
|
|
1770
|
-
name: { type: 'string', required: true, max: 200 },
|
|
1771
|
-
description: { type: 'string', max: 1000 },
|
|
1772
|
-
status: { type: 'string', defaultTo: 'active' },
|
|
1773
|
-
workspace_id: { type: 'string', required: true },
|
|
1774
|
-
user_id: { type: 'number', required: true }
|
|
1775
|
-
},
|
|
1776
|
-
relationships: {
|
|
1777
|
-
tasks: { type: 'hasMany', target: 'tasks', foreignKey: 'project_id' }
|
|
1778
|
-
},
|
|
1779
|
-
autofilter: 'workspace_user',
|
|
1780
|
-
tableName: 'autofilter_projects'
|
|
1781
|
-
})
|
|
1782
|
-
await api.resources.projects.createKnexTable()
|
|
1783
|
-
mapTable('autofilter_projects', 'projects')
|
|
1784
|
-
|
|
1785
|
-
await api.addResource('tasks', {
|
|
1786
|
-
schema: {
|
|
1787
|
-
id: { type: 'id' },
|
|
1788
|
-
title: { type: 'string', required: true, max: 200 },
|
|
1789
|
-
completed: { type: 'boolean', defaultTo: false },
|
|
1790
|
-
project_id: { type: 'number', belongsTo: 'projects', as: 'project' },
|
|
1791
|
-
workspace_id: { type: 'string', required: true },
|
|
1792
|
-
user_id: { type: 'number', required: true }
|
|
1793
|
-
},
|
|
1794
|
-
autofilter: 'workspace_user',
|
|
1795
|
-
tableName: 'autofilter_tasks'
|
|
1796
|
-
})
|
|
1797
|
-
await api.resources.tasks.createKnexTable()
|
|
1798
|
-
mapTable('autofilter_tasks', 'tasks')
|
|
1799
|
-
|
|
1800
|
-
await api.addResource('optional_tasks', {
|
|
1801
|
-
schema: {
|
|
1802
|
-
id: { type: 'id' },
|
|
1803
|
-
title: { type: 'string', required: true, max: 200 },
|
|
1804
|
-
project_id: { type: 'number', belongsTo: 'projects', as: 'project', nullable: true }
|
|
1805
|
-
},
|
|
1806
|
-
autofilter: {
|
|
1807
|
-
filters: [{ field: 'project_id', resolver: 'currentProject' }]
|
|
1808
|
-
},
|
|
1809
|
-
tableName: 'autofilter_optional_tasks'
|
|
1810
|
-
})
|
|
1811
|
-
await api.resources.optional_tasks.createKnexTable()
|
|
1812
|
-
mapTable('autofilter_optional_tasks', 'optional_tasks')
|
|
1813
|
-
|
|
1814
|
-
await api.addResource('system_settings', {
|
|
1815
|
-
schema: {
|
|
1816
|
-
id: { type: 'id' },
|
|
1817
|
-
key: { type: 'string', required: true, unique: true },
|
|
1818
|
-
value: { type: 'string', required: true }
|
|
1819
|
-
},
|
|
1820
|
-
autofilter: 'public',
|
|
1821
|
-
tableName: 'autofilter_system_settings'
|
|
1822
|
-
})
|
|
1823
|
-
await api.resources.system_settings.createKnexTable()
|
|
1824
|
-
mapTable('autofilter_system_settings', 'system_settings')
|
|
1825
|
-
|
|
1826
|
-
if (storageMode.isAnyApi()) {
|
|
1827
|
-
storageMode.setCurrentTenant(previousTenant)
|
|
1828
|
-
}
|
|
1829
|
-
|
|
1830
|
-
return api
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
export async function createRowPolicyApi (knex, pluginOptions = {}) {
|
|
1834
|
-
const api = new Api({
|
|
1835
|
-
name: 'row-policy-test-api',
|
|
1836
|
-
})
|
|
1837
|
-
|
|
1838
|
-
const tenantId = storageMode.isAnyApi() ? 'row_policy_tenant' : storageMode.defaultTenant
|
|
1839
|
-
const previousTenant = storageMode.currentTenant
|
|
1840
|
-
if (storageMode.isAnyApi()) {
|
|
1841
|
-
storageMode.setCurrentTenant(tenantId)
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
await api.use(RestApiPlugin, {
|
|
1845
|
-
simplifiedApi: false,
|
|
1846
|
-
simplifiedTransport: false,
|
|
1847
|
-
returnRecordApi: {
|
|
1848
|
-
post: true,
|
|
1849
|
-
put: false,
|
|
1850
|
-
patch: false
|
|
1851
|
-
},
|
|
1852
|
-
sortableFields: ['id', 'name', 'title', 'access_group', 'project_id']
|
|
1853
|
-
})
|
|
1854
|
-
|
|
1855
|
-
await useStoragePlugin(api, knex, { tenantId })
|
|
1856
|
-
await resetAnyApiTables(knex)
|
|
1857
|
-
|
|
1858
|
-
const rowPolicyOptions = pluginOptions.rowPolicy || {}
|
|
1859
|
-
await api.use(RowPolicyPlugin, {
|
|
1860
|
-
...rowPolicyOptions,
|
|
1861
|
-
policies: {
|
|
1862
|
-
groupVisibility: ({ query, context, column, value, scopeName, queryPurpose }) => {
|
|
1863
|
-
pluginOptions.onPolicy?.({ scopeName, queryPurpose })
|
|
1864
|
-
|
|
1865
|
-
if (context.visibility?.all === true) {
|
|
1866
|
-
return true
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
const groups = context.visibility?.groups
|
|
1870
|
-
if (!Array.isArray(groups) || groups.length === 0) {
|
|
1871
|
-
return false
|
|
1872
|
-
}
|
|
1873
|
-
|
|
1874
|
-
query.where(function visibilityGrants () {
|
|
1875
|
-
this.whereRaw('1 = 0')
|
|
1876
|
-
this.orWhere(function groupVisibilityGrant () {
|
|
1877
|
-
this.whereIn(
|
|
1878
|
-
column('access_group'),
|
|
1879
|
-
groups.map((group) => value('access_group', group))
|
|
1880
|
-
)
|
|
1881
|
-
})
|
|
1882
|
-
})
|
|
1883
|
-
return true
|
|
1884
|
-
},
|
|
1885
|
-
...(rowPolicyOptions.policies || {})
|
|
1886
|
-
}
|
|
1887
|
-
})
|
|
1888
|
-
|
|
1889
|
-
await api.use(AutoFilterPlugin, {
|
|
1890
|
-
resolvers: {
|
|
1891
|
-
workspace: ({ context }) => context.scopeValues?.workspaceId
|
|
1892
|
-
},
|
|
1893
|
-
presets: {
|
|
1894
|
-
workspace: {
|
|
1895
|
-
filters: [{ field: 'workspace_id', resolver: 'workspace' }]
|
|
1896
|
-
}
|
|
1897
|
-
}
|
|
1898
|
-
})
|
|
1899
|
-
|
|
1900
|
-
await api.addResource('policy_projects', {
|
|
1901
|
-
schema: {
|
|
1902
|
-
id: { type: 'id' },
|
|
1903
|
-
name: { type: 'string', required: true, max: 200 },
|
|
1904
|
-
access_group: { type: 'string', required: true },
|
|
1905
|
-
workspace_id: { type: 'string', required: true }
|
|
1906
|
-
},
|
|
1907
|
-
relationships: {
|
|
1908
|
-
tasks: { type: 'hasMany', target: 'policy_tasks', foreignKey: 'project_id' },
|
|
1909
|
-
shared_tasks: {
|
|
1910
|
-
type: 'manyToMany',
|
|
1911
|
-
target: 'policy_tasks',
|
|
1912
|
-
through: 'policy_project_tasks',
|
|
1913
|
-
foreignKey: 'project_id',
|
|
1914
|
-
otherKey: 'task_id'
|
|
1915
|
-
}
|
|
1916
|
-
},
|
|
1917
|
-
autofilter: 'workspace',
|
|
1918
|
-
rowPolicy: 'groupVisibility',
|
|
1919
|
-
tableName: 'row_policy_projects'
|
|
1920
|
-
})
|
|
1921
|
-
await api.resources.policy_projects.createKnexTable()
|
|
1922
|
-
mapTable('row_policy_projects', 'policy_projects')
|
|
1923
|
-
|
|
1924
|
-
await api.addResource('policy_tasks', {
|
|
1925
|
-
schema: {
|
|
1926
|
-
id: { type: 'id' },
|
|
1927
|
-
title: { type: 'string', required: true, max: 200, search: true },
|
|
1928
|
-
access_group: { type: 'string', required: true },
|
|
1929
|
-
workspace_id: { type: 'string', required: true },
|
|
1930
|
-
project_id: {
|
|
1931
|
-
type: 'number',
|
|
1932
|
-
nullable: true,
|
|
1933
|
-
belongsTo: 'policy_projects',
|
|
1934
|
-
as: 'project',
|
|
1935
|
-
search: true
|
|
1936
|
-
}
|
|
1937
|
-
},
|
|
1938
|
-
relationships: {
|
|
1939
|
-
shared_projects: {
|
|
1940
|
-
type: 'manyToMany',
|
|
1941
|
-
target: 'policy_projects',
|
|
1942
|
-
through: 'policy_project_tasks',
|
|
1943
|
-
foreignKey: 'task_id',
|
|
1944
|
-
otherKey: 'project_id'
|
|
1945
|
-
}
|
|
1946
|
-
},
|
|
1947
|
-
autofilter: 'workspace',
|
|
1948
|
-
rowPolicy: 'groupVisibility',
|
|
1949
|
-
tableName: 'row_policy_tasks'
|
|
1950
|
-
})
|
|
1951
|
-
await api.resources.policy_tasks.createKnexTable()
|
|
1952
|
-
mapTable('row_policy_tasks', 'policy_tasks')
|
|
1953
|
-
|
|
1954
|
-
await api.addResource('policy_project_tasks', {
|
|
1955
|
-
schema: {
|
|
1956
|
-
id: { type: 'id' },
|
|
1957
|
-
project_id: { type: 'number', required: true, belongsTo: 'policy_projects', as: 'project' },
|
|
1958
|
-
task_id: { type: 'number', required: true, belongsTo: 'policy_tasks', as: 'task' }
|
|
1959
|
-
},
|
|
1960
|
-
tableName: 'row_policy_project_tasks'
|
|
1961
|
-
})
|
|
1962
|
-
await api.resources.policy_project_tasks.createKnexTable()
|
|
1963
|
-
mapTable('row_policy_project_tasks', 'policy_project_tasks')
|
|
1964
|
-
|
|
1965
|
-
if (storageMode.isAnyApi()) {
|
|
1966
|
-
const projectsDescriptor = await api.anyapi.registry.getDescriptor(tenantId, 'policy_projects')
|
|
1967
|
-
const tasksDescriptor = await api.anyapi.registry.getDescriptor(tenantId, 'policy_tasks')
|
|
1968
|
-
storageMode.registerLink(
|
|
1969
|
-
'row_policy_project_tasks',
|
|
1970
|
-
'policy_projects',
|
|
1971
|
-
'shared_tasks',
|
|
1972
|
-
projectsDescriptor?.manyToMany?.shared_tasks?.relationship,
|
|
1973
|
-
tasksDescriptor?.manyToMany?.shared_projects?.relationship
|
|
1974
|
-
)
|
|
1975
|
-
}
|
|
1976
|
-
|
|
1977
|
-
await api.addResource('policy_broken', {
|
|
1978
|
-
schema: {
|
|
1979
|
-
id: { type: 'id' },
|
|
1980
|
-
name: { type: 'string', required: true }
|
|
1981
|
-
},
|
|
1982
|
-
rowPolicy: () => undefined,
|
|
1983
|
-
tableName: 'row_policy_broken'
|
|
1984
|
-
})
|
|
1985
|
-
await api.resources.policy_broken.createKnexTable()
|
|
1986
|
-
mapTable('row_policy_broken', 'policy_broken')
|
|
1987
|
-
|
|
1988
|
-
if (storageMode.isAnyApi()) {
|
|
1989
|
-
storageMode.setCurrentTenant(previousTenant)
|
|
1990
|
-
}
|
|
1991
|
-
|
|
1992
|
-
return api
|
|
1993
|
-
}
|
|
1994
|
-
|
|
1995
|
-
/**
|
|
1996
|
-
* Creates an API with positioning support for testing
|
|
1997
|
-
*/
|
|
1998
|
-
export async function createPositioningApi (knex, pluginOptions = {}) {
|
|
1999
|
-
const apiName = pluginOptions.apiName || 'positioning-test-api'
|
|
2000
|
-
const tablePrefix = pluginOptions.tablePrefix || 'positioning'
|
|
2001
|
-
const api = new Api({
|
|
2002
|
-
name: apiName,
|
|
2003
|
-
})
|
|
2004
|
-
|
|
2005
|
-
const restApiOptions = {
|
|
2006
|
-
simplifiedApi: true, // Changed to true to allow simplified API calls in tests
|
|
2007
|
-
simplifiedTransport: false,
|
|
2008
|
-
returnRecordApi: {
|
|
2009
|
-
post: true,
|
|
2010
|
-
put: true,
|
|
2011
|
-
patch: true
|
|
2012
|
-
},
|
|
2013
|
-
returnRecordTransport: {
|
|
2014
|
-
post: 'full',
|
|
2015
|
-
put: 'full',
|
|
2016
|
-
patch: 'full'
|
|
2017
|
-
},
|
|
2018
|
-
sortableFields: ['id', 'title', 'name', 'position', 'sort_order', 'category_id', 'project_id', 'status'],
|
|
2019
|
-
...pluginOptions['rest-api']
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
|
-
await api.use(RestApiPlugin, restApiOptions)
|
|
2023
|
-
await useStoragePlugin(api, knex)
|
|
2024
|
-
await resetAnyApiTables(knex)
|
|
2025
|
-
|
|
2026
|
-
// Categories (for grouping tasks)
|
|
2027
|
-
await api.addResource('categories', {
|
|
2028
|
-
schema: {
|
|
2029
|
-
id: { type: 'id' },
|
|
2030
|
-
name: { type: 'string', required: true, max: 100 }
|
|
2031
|
-
},
|
|
2032
|
-
tableName: `${tablePrefix}_categories`
|
|
2033
|
-
})
|
|
2034
|
-
await api.resources.categories.createKnexTable()
|
|
2035
|
-
mapTable(`${tablePrefix}_categories`, 'categories')
|
|
2036
|
-
|
|
2037
|
-
// Tasks (main positioning test resource)
|
|
2038
|
-
await api.addResource('tasks', {
|
|
2039
|
-
schema: {
|
|
2040
|
-
id: { type: 'id' },
|
|
2041
|
-
title: { type: 'string', required: true, max: 200 },
|
|
2042
|
-
category_id: { type: 'number', nullable: true, belongsTo: 'categories', as: 'category', search: true },
|
|
2043
|
-
position: { type: 'string', max: 255, nullable: true },
|
|
2044
|
-
beforeId: { type: 'string', virtual: true }, // Virtual field for positioning
|
|
2045
|
-
deleted_at: { type: 'dateTime', nullable: true, search: true }, // For soft delete tests
|
|
2046
|
-
version: { type: 'number', defaultTo: 1, search: true } // For versioning tests
|
|
2047
|
-
},
|
|
2048
|
-
relationships: {
|
|
2049
|
-
category: { belongsTo: 'categories' }
|
|
2050
|
-
},
|
|
2051
|
-
tableName: `${tablePrefix}_tasks`
|
|
2052
|
-
})
|
|
2053
|
-
await api.resources.tasks.createKnexTable()
|
|
2054
|
-
mapTable(`${tablePrefix}_tasks`, 'tasks')
|
|
2055
|
-
|
|
2056
|
-
// Projects (for multi-filter testing)
|
|
2057
|
-
await api.addResource('projects', {
|
|
2058
|
-
schema: {
|
|
2059
|
-
id: { type: 'id' },
|
|
2060
|
-
name: { type: 'string', required: true, max: 100 }
|
|
2061
|
-
},
|
|
2062
|
-
tableName: `${tablePrefix}_projects`
|
|
2063
|
-
})
|
|
2064
|
-
await api.resources.projects.createKnexTable()
|
|
2065
|
-
mapTable(`${tablePrefix}_projects`, 'projects')
|
|
2066
|
-
|
|
2067
|
-
// Items (flexible resource for various positioning tests)
|
|
2068
|
-
await api.addResource('items', {
|
|
2069
|
-
schema: {
|
|
2070
|
-
id: { type: 'id' },
|
|
2071
|
-
name: { type: 'string', required: true, max: 200 },
|
|
2072
|
-
project_id: { type: 'number', nullable: true, belongsTo: 'projects', as: 'project', search: true },
|
|
2073
|
-
status: { type: 'string', defaultTo: 'active', nullable: true, search: true },
|
|
2074
|
-
position: { type: 'string', max: 255, nullable: true },
|
|
2075
|
-
sort_order: { type: 'string', max: 255, nullable: true }, // Alternative position field
|
|
2076
|
-
beforeId: { type: 'string', virtual: true },
|
|
2077
|
-
priority: { type: 'string', defaultTo: 'medium', search: true } // For multi-filter tests
|
|
2078
|
-
},
|
|
2079
|
-
relationships: {
|
|
2080
|
-
project: { belongsTo: 'projects' }
|
|
2081
|
-
},
|
|
2082
|
-
tableName: `${tablePrefix}_items`
|
|
2083
|
-
})
|
|
2084
|
-
await api.resources.items.createKnexTable()
|
|
2085
|
-
mapTable(`${tablePrefix}_items`, 'items')
|
|
2086
|
-
|
|
2087
|
-
return api
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
|
-
/**
|
|
2091
|
-
* Creates an API with custom idProperty for all resources to test idProperty functionality
|
|
2092
|
-
* Uses 'custom_id_' prefix for all tables to avoid conflicts
|
|
2093
|
-
*/
|
|
2094
|
-
export async function createCustomIdPropertyApi (knex, pluginOptions = {}) {
|
|
2095
|
-
const apiName = pluginOptions.apiName || 'custom-id-test-api'
|
|
2096
|
-
const tablePrefix = pluginOptions.tablePrefix || 'custom_id'
|
|
2097
|
-
const api = new Api({
|
|
2098
|
-
name: apiName,
|
|
2099
|
-
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
2100
|
-
})
|
|
2101
|
-
|
|
2102
|
-
if (storageMode.isAnyApi()) {
|
|
2103
|
-
storageMode.clearRegistry()
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
const restApiOptions = {
|
|
2107
|
-
simplifiedApi: false,
|
|
2108
|
-
simplifiedTransport: false,
|
|
2109
|
-
returnRecordApi: {
|
|
2110
|
-
post: 'full',
|
|
2111
|
-
put: 'full',
|
|
2112
|
-
patch: 'full'
|
|
2113
|
-
},
|
|
2114
|
-
returnRecordTransport: {
|
|
2115
|
-
post: 'full',
|
|
2116
|
-
put: 'full',
|
|
2117
|
-
patch: 'full'
|
|
2118
|
-
},
|
|
2119
|
-
sortableFields: ['country_id', 'publisher_id', 'author_id', 'book_id', 'title', 'name', 'code'],
|
|
2120
|
-
...pluginOptions['rest-api'] // Merge any custom options for rest-api plugin
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
|
-
await api.use(RestApiPlugin, restApiOptions)
|
|
2124
|
-
await useStoragePlugin(api, knex)
|
|
2125
|
-
await resetAnyApiTables(knex)
|
|
2126
|
-
|
|
2127
|
-
// Countries table with custom idProperty
|
|
2128
|
-
await api.addResource('countries', {
|
|
2129
|
-
schema: {
|
|
2130
|
-
// NO id: { type: 'id' } - this is key!
|
|
2131
|
-
name: { type: 'string', required: true, max: 100, search: true },
|
|
2132
|
-
code: { type: 'string', max: 2, unique: true }
|
|
2133
|
-
},
|
|
2134
|
-
relationships: {
|
|
2135
|
-
publishers: { type: 'hasMany', target: 'publishers', foreignKey: 'country_id' },
|
|
2136
|
-
books: { type: 'hasMany', target: 'books', foreignKey: 'country_id' }
|
|
2137
|
-
},
|
|
2138
|
-
tableName: `${tablePrefix}_countries`,
|
|
2139
|
-
idProperty: 'country_id' // Custom ID property
|
|
2140
|
-
})
|
|
2141
|
-
await api.resources.countries.createKnexTable()
|
|
2142
|
-
mapTable(`${tablePrefix}_countries`, 'countries')
|
|
2143
|
-
|
|
2144
|
-
// Publishers table with custom idProperty
|
|
2145
|
-
await api.addResource('publishers', {
|
|
2146
|
-
schema: {
|
|
2147
|
-
name: { type: 'string', required: true, max: 200 },
|
|
2148
|
-
country_id: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' }
|
|
2149
|
-
},
|
|
2150
|
-
relationships: {
|
|
2151
|
-
books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' },
|
|
2152
|
-
reviews: {
|
|
2153
|
-
type: 'hasMany',
|
|
2154
|
-
target: 'reviews',
|
|
2155
|
-
via: 'reviewable'
|
|
2156
|
-
}
|
|
2157
|
-
},
|
|
2158
|
-
tableName: `${tablePrefix}_publishers`,
|
|
2159
|
-
idProperty: 'publisher_id'
|
|
2160
|
-
})
|
|
2161
|
-
await api.resources.publishers.createKnexTable()
|
|
2162
|
-
mapTable(`${tablePrefix}_publishers`, 'publishers')
|
|
2163
|
-
|
|
2164
|
-
// Authors table with custom idProperty
|
|
2165
|
-
await api.addResource('authors', {
|
|
2166
|
-
schema: {
|
|
2167
|
-
name: { type: 'string', required: true, max: 200 },
|
|
2168
|
-
biography: { type: 'string', max: 5000 },
|
|
2169
|
-
country_id: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' }
|
|
2170
|
-
},
|
|
2171
|
-
relationships: {
|
|
2172
|
-
books: { type: 'manyToMany', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' },
|
|
2173
|
-
reviews: {
|
|
2174
|
-
type: 'hasMany',
|
|
2175
|
-
target: 'reviews',
|
|
2176
|
-
via: 'reviewable'
|
|
2177
|
-
}
|
|
2178
|
-
},
|
|
2179
|
-
tableName: `${tablePrefix}_authors`,
|
|
2180
|
-
idProperty: 'author_id'
|
|
2181
|
-
})
|
|
2182
|
-
await api.resources.authors.createKnexTable()
|
|
2183
|
-
mapTable(`${tablePrefix}_authors`, 'authors')
|
|
2184
|
-
|
|
2185
|
-
// Books table with custom idProperty
|
|
2186
|
-
await api.addResource('books', {
|
|
2187
|
-
schema: {
|
|
2188
|
-
title: { type: 'string', required: true, max: 300, search: true },
|
|
2189
|
-
isbn: { type: 'string', max: 13 },
|
|
2190
|
-
pages: { type: 'number' },
|
|
2191
|
-
published_date: { type: 'date' },
|
|
2192
|
-
country_id: { type: 'number', required: true, belongsTo: 'countries', as: 'country', search: true },
|
|
2193
|
-
publisher_id: { type: 'number', nullable: true, belongsTo: 'publishers', as: 'publisher', search: true }
|
|
2194
|
-
},
|
|
2195
|
-
relationships: {
|
|
2196
|
-
authors: { type: 'manyToMany', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' },
|
|
2197
|
-
reviews: {
|
|
2198
|
-
type: 'hasMany',
|
|
2199
|
-
target: 'reviews',
|
|
2200
|
-
via: 'reviewable'
|
|
2201
|
-
}
|
|
2202
|
-
},
|
|
2203
|
-
tableName: `${tablePrefix}_books`,
|
|
2204
|
-
idProperty: 'book_id'
|
|
2205
|
-
})
|
|
2206
|
-
await api.resources.books.createKnexTable()
|
|
2207
|
-
mapTable(`${tablePrefix}_books`, 'books')
|
|
2208
|
-
|
|
2209
|
-
// Book-Authors pivot table with custom idProperty
|
|
2210
|
-
await api.addResource('book_authors', {
|
|
2211
|
-
schema: {
|
|
2212
|
-
book_id: { type: 'number', required: true, belongsTo: 'books', as: 'book' },
|
|
2213
|
-
author_id: { type: 'number', required: true, belongsTo: 'authors', as: 'author' },
|
|
2214
|
-
contribution_type: { type: 'string', max: 50 },
|
|
2215
|
-
order: { type: 'number' }
|
|
2216
|
-
},
|
|
2217
|
-
tableName: `${tablePrefix}_book_authors`,
|
|
2218
|
-
idProperty: 'book_author_id'
|
|
2219
|
-
})
|
|
2220
|
-
await api.resources.book_authors.createKnexTable()
|
|
2221
|
-
mapTable(`${tablePrefix}_book_authors`, 'book_authors')
|
|
2222
|
-
|
|
2223
|
-
// Polymorphic reviews with custom idProperty
|
|
2224
|
-
await api.addResource('reviews', {
|
|
2225
|
-
schema: {
|
|
2226
|
-
rating: { type: 'number', required: true, min: 1, max: 5 },
|
|
2227
|
-
title: { type: 'string', max: 200 },
|
|
2228
|
-
content: { type: 'string', required: true, max: 5000 },
|
|
2229
|
-
reviewer_name: { type: 'string', required: true, max: 100 },
|
|
2230
|
-
review_date: { type: 'dateTime', temporalPrecision: 3, defaultTo: () => new Date().toISOString() },
|
|
2231
|
-
reviewable_type: { type: 'string', required: true, search: true },
|
|
2232
|
-
reviewable_id: { type: 'number', required: true },
|
|
2233
|
-
// Define the polymorphic field in schema
|
|
2234
|
-
reviewable: {
|
|
2235
|
-
belongsToPolymorphic: {
|
|
2236
|
-
types: ['books', 'authors', 'publishers'],
|
|
2237
|
-
typeField: 'reviewable_type',
|
|
2238
|
-
idField: 'reviewable_id'
|
|
2239
|
-
},
|
|
2240
|
-
as: 'reviewable'
|
|
2241
|
-
}
|
|
2242
|
-
},
|
|
2243
|
-
relationships: {},
|
|
2244
|
-
tableName: `${tablePrefix}_reviews`,
|
|
2245
|
-
idProperty: 'review_id'
|
|
2246
|
-
})
|
|
2247
|
-
await api.resources.reviews.createKnexTable()
|
|
2248
|
-
mapTable(`${tablePrefix}_reviews`, 'reviews')
|
|
2249
|
-
|
|
2250
|
-
if (storageMode.isAnyApi()) {
|
|
2251
|
-
const tenant = storageMode.defaultTenant || 'default'
|
|
2252
|
-
const booksDescriptor = await api.anyapi.registry.getDescriptor(tenant, 'books')
|
|
2253
|
-
const authorsDescriptor = await api.anyapi.registry.getDescriptor(tenant, 'authors')
|
|
2254
|
-
const relationshipKey = booksDescriptor?.manyToMany?.authors?.relationship
|
|
2255
|
-
const inverseRelationshipKey = authorsDescriptor?.manyToMany?.books?.relationship
|
|
2256
|
-
storageMode.registerLink(
|
|
2257
|
-
`${tablePrefix}_book_authors`,
|
|
2258
|
-
'books',
|
|
2259
|
-
'authors',
|
|
2260
|
-
relationshipKey,
|
|
2261
|
-
inverseRelationshipKey
|
|
2262
|
-
)
|
|
2263
|
-
}
|
|
2264
|
-
|
|
2265
|
-
return api
|
|
2266
|
-
}
|
|
2267
|
-
|
|
2268
|
-
/**
|
|
2269
|
-
* Creates an API configuration for multi-field cursor pagination testing
|
|
2270
|
-
* Uses 'cursor_' prefix for all tables to avoid conflicts
|
|
2271
|
-
*/
|
|
2272
|
-
export async function createCursorPaginationApi (knex) {
|
|
2273
|
-
const api = new Api({
|
|
2274
|
-
name: 'cursor-pagination-test-api',
|
|
2275
|
-
})
|
|
2276
|
-
|
|
2277
|
-
const restApiOptions = {
|
|
2278
|
-
simplified: false,
|
|
2279
|
-
responseFormat: {
|
|
2280
|
-
post: 'full',
|
|
2281
|
-
put: 'full',
|
|
2282
|
-
patch: 'minimal'
|
|
2283
|
-
},
|
|
2284
|
-
sortableFields: ['id', 'name', 'category', 'brand', 'price', 'sku', 'createdAt', 'code', 'status', 'type'],
|
|
2285
|
-
queryDefaultLimit: 20,
|
|
2286
|
-
queryMaxLimit: 100,
|
|
2287
|
-
returnBasePath: 'https://api.example.com'
|
|
2288
|
-
}
|
|
2289
|
-
|
|
2290
|
-
await api.use(RestApiPlugin, restApiOptions)
|
|
2291
|
-
await useStoragePlugin(api, knex)
|
|
2292
|
-
await resetAnyApiTables(knex)
|
|
2293
|
-
|
|
2294
|
-
// Create products table with fields suitable for multi-field sorting
|
|
2295
|
-
await api.addResource('products', {
|
|
2296
|
-
schema: {
|
|
2297
|
-
id: { type: 'id' },
|
|
2298
|
-
name: { type: 'string', required: true, max: 200, search: true },
|
|
2299
|
-
category: { type: 'string', required: true, max: 100, search: true, indexed: true },
|
|
2300
|
-
brand: { type: 'string', required: true, max: 100, search: true, indexed: true },
|
|
2301
|
-
price: { type: 'number', required: true },
|
|
2302
|
-
sku: { type: 'string', required: true, max: 50, unique: true },
|
|
2303
|
-
status: { type: 'string', max: 50, default: 'active' },
|
|
2304
|
-
createdAt: { type: 'dateTime', temporalPrecision: 3, defaultTo: () => new Date().toISOString() }
|
|
2305
|
-
},
|
|
2306
|
-
tableName: 'cursor_products'
|
|
2307
|
-
})
|
|
2308
|
-
await api.resources.products.createKnexTable()
|
|
2309
|
-
mapTable('cursor_products', 'products')
|
|
2310
|
-
|
|
2311
|
-
// Create items table with custom ID property
|
|
2312
|
-
await api.addResource('items', {
|
|
2313
|
-
schema: {
|
|
2314
|
-
item_id: { type: 'id' },
|
|
2315
|
-
code: { type: 'string', required: true, max: 10, unique: true },
|
|
2316
|
-
name: { type: 'string', required: true, max: 200 },
|
|
2317
|
-
category: { type: 'string', required: true, max: 100 },
|
|
2318
|
-
type: { type: 'string', required: true, max: 50 }
|
|
2319
|
-
},
|
|
2320
|
-
idProperty: 'item_id',
|
|
2321
|
-
tableName: 'cursor_items'
|
|
2322
|
-
})
|
|
2323
|
-
await api.resources.items.createKnexTable()
|
|
2324
|
-
mapTable('cursor_items', 'items')
|
|
2325
|
-
|
|
2326
|
-
return api
|
|
2327
|
-
}
|
|
2328
|
-
|
|
2329
|
-
/**
|
|
2330
|
-
* Creates an API configuration for testing virtual fields validation
|
|
2331
|
-
*/
|
|
2332
|
-
export async function createVirtualFieldsApi (knex, pluginOptions = {}) {
|
|
2333
|
-
const apiName = pluginOptions.apiName || 'virtual-fields-test-api'
|
|
2334
|
-
const api = new Api({
|
|
2335
|
-
name: apiName,
|
|
2336
|
-
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
2337
|
-
})
|
|
2338
|
-
|
|
2339
|
-
const restApiOptions = {
|
|
2340
|
-
simplifiedApi: false,
|
|
2341
|
-
simplifiedTransport: false,
|
|
2342
|
-
returnRecordApi: {
|
|
2343
|
-
post: true,
|
|
2344
|
-
put: false,
|
|
2345
|
-
patch: false
|
|
2346
|
-
},
|
|
2347
|
-
returnRecordTransport: {
|
|
2348
|
-
post: 'full',
|
|
2349
|
-
put: 'no',
|
|
2350
|
-
patch: 'minimal'
|
|
2351
|
-
},
|
|
2352
|
-
...pluginOptions['rest-api']
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
|
-
await api.use(RestApiPlugin, restApiOptions)
|
|
2356
|
-
await useStoragePlugin(api, knex)
|
|
2357
|
-
await resetAnyApiTables(knex)
|
|
2358
|
-
|
|
2359
|
-
// Add a test resource with virtual fields
|
|
2360
|
-
await api.addResource('users', {
|
|
2361
|
-
schema: {
|
|
2362
|
-
// Regular fields
|
|
2363
|
-
username: { type: 'string', required: true },
|
|
2364
|
-
email: { type: 'string', required: true },
|
|
2365
|
-
age: { type: 'number' },
|
|
2366
|
-
|
|
2367
|
-
// Virtual fields with validation rules
|
|
2368
|
-
password: { type: 'string', virtual: true, minLength: 8 },
|
|
2369
|
-
passwordConfirmation: { type: 'string', virtual: true, minLength: 8 },
|
|
2370
|
-
termsAccepted: { type: 'boolean', virtual: true, required: true },
|
|
2371
|
-
captchaScore: { type: 'number', virtual: true, min: 0, max: 1 },
|
|
2372
|
-
tags: { type: 'array', virtual: true },
|
|
2373
|
-
metadata: { type: 'object', virtual: true }
|
|
2374
|
-
},
|
|
2375
|
-
tableName: 'virtual_users'
|
|
2376
|
-
})
|
|
2377
|
-
|
|
2378
|
-
await api.resources.users.createKnexTable()
|
|
2379
|
-
|
|
2380
|
-
return api
|
|
2381
|
-
}
|
|
2382
|
-
|
|
2383
|
-
/**
|
|
2384
|
-
* Creates an API configuration for testing searchSchema merge behavior.
|
|
2385
|
-
* Shared suites assert merge/validation behavior across storage backends;
|
|
2386
|
-
* backend-specific execution cases live in storage-specific tests.
|
|
2387
|
-
*/
|
|
2388
|
-
export async function createSearchSchemaMergeApi (knex, pluginOptions = {}) {
|
|
2389
|
-
const api = new Api({
|
|
2390
|
-
name: 'searchschema-merge-test-api',
|
|
2391
|
-
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
2392
|
-
})
|
|
2393
|
-
|
|
2394
|
-
await api.use(RestApiPlugin, {
|
|
2395
|
-
simplifiedApi: false,
|
|
2396
|
-
simplifiedTransport: false,
|
|
2397
|
-
returnRecordApi: {
|
|
2398
|
-
post: true,
|
|
2399
|
-
put: false,
|
|
2400
|
-
patch: false
|
|
2401
|
-
},
|
|
2402
|
-
...pluginOptions['rest-api']
|
|
2403
|
-
})
|
|
2404
|
-
|
|
2405
|
-
await useStoragePlugin(api, knex)
|
|
2406
|
-
await resetAnyApiTables(knex)
|
|
2407
|
-
|
|
2408
|
-
// Test resource with both search:true and searchSchema
|
|
2409
|
-
await api.addResource('products', {
|
|
2410
|
-
schema: {
|
|
2411
|
-
name: { type: 'string', search: true }, // Should be added to searchSchema
|
|
2412
|
-
description: { type: 'string', search: true }, // Should be added to searchSchema
|
|
2413
|
-
price: { type: 'number', search: true }, // Should be overridden by searchSchema
|
|
2414
|
-
category: { type: 'number' }, // Not searchable
|
|
2415
|
-
sku: { type: 'string', search: true }, // Should be added to searchSchema
|
|
2416
|
-
status: { type: 'string' } // Not searchable but added via searchSchema
|
|
2417
|
-
},
|
|
2418
|
-
searchSchema: {
|
|
2419
|
-
// This should override the price field from search:true
|
|
2420
|
-
// 'between' operator requires array type for validation [min, max]
|
|
2421
|
-
price: {
|
|
2422
|
-
type: 'array',
|
|
2423
|
-
filterOperator: 'between'
|
|
2424
|
-
},
|
|
2425
|
-
// Arbitrary public filter name that searches across multiple fields.
|
|
2426
|
-
// The key `term` is part of the API contract even though there is no `term` column.
|
|
2427
|
-
term: {
|
|
2428
|
-
type: 'string',
|
|
2429
|
-
oneOf: ['name', 'description'],
|
|
2430
|
-
filterOperator: 'like'
|
|
2431
|
-
},
|
|
2432
|
-
// This is a virtual field
|
|
2433
|
-
category_name: {
|
|
2434
|
-
type: 'string',
|
|
2435
|
-
actualField: 'category.name',
|
|
2436
|
-
filterOperator: 'like'
|
|
2437
|
-
},
|
|
2438
|
-
// Arbitrary public filter name with fully custom server-side meaning.
|
|
2439
|
-
// The Knex query layer decides what `availability` means.
|
|
2440
|
-
availability: {
|
|
2441
|
-
type: 'boolean',
|
|
2442
|
-
applyFilter: (query, value) => {
|
|
2443
|
-
query.where('status', value ? 'active' : 'inactive')
|
|
2444
|
-
}
|
|
2445
|
-
},
|
|
2446
|
-
// This is an explicit field not marked with search:true
|
|
2447
|
-
// 'in' operator requires array type for validation
|
|
2448
|
-
status: {
|
|
2449
|
-
type: 'array',
|
|
2450
|
-
filterOperator: 'in'
|
|
2451
|
-
}
|
|
2452
|
-
},
|
|
2453
|
-
tableName: 'searchmerge_products'
|
|
2454
|
-
})
|
|
2455
|
-
await api.resources.products.createKnexTable()
|
|
2456
|
-
|
|
2457
|
-
// Test resource with only search:true fields
|
|
2458
|
-
await api.addResource('users', {
|
|
2459
|
-
schema: {
|
|
2460
|
-
username: { type: 'string', search: true },
|
|
2461
|
-
email: { type: 'string', search: true },
|
|
2462
|
-
age: { type: 'number' },
|
|
2463
|
-
bio: { type: 'string', search: { filterOperator: 'like' } }
|
|
2464
|
-
},
|
|
2465
|
-
tableName: 'searchmerge_users'
|
|
2466
|
-
})
|
|
2467
|
-
await api.resources.users.createKnexTable()
|
|
2468
|
-
|
|
2469
|
-
// Test resource with only explicit searchSchema
|
|
2470
|
-
await api.addResource('orders', {
|
|
2471
|
-
schema: {
|
|
2472
|
-
order_number: { type: 'string' },
|
|
2473
|
-
total: { type: 'number' },
|
|
2474
|
-
status: { type: 'string' }
|
|
2475
|
-
},
|
|
2476
|
-
searchSchema: {
|
|
2477
|
-
order_number: { type: 'string', filterOperator: '=' },
|
|
2478
|
-
status: { type: 'array', filterOperator: 'in' } // 'in' operator requires array type
|
|
2479
|
-
},
|
|
2480
|
-
tableName: 'searchmerge_orders'
|
|
2481
|
-
})
|
|
2482
|
-
await api.resources.orders.createKnexTable()
|
|
2483
|
-
mapTable('searchmerge_orders', 'orders')
|
|
2484
|
-
|
|
2485
|
-
return api
|
|
2486
|
-
}
|
|
2487
|
-
|
|
2488
|
-
export async function createFileUploadApi (knex, pluginOptions = {}) {
|
|
2489
|
-
const apiName = pluginOptions.apiName || 'file-upload-test-api'
|
|
2490
|
-
const tableName = pluginOptions.tableName || 'file_documents'
|
|
2491
|
-
const tablePrefix = pluginOptions.tablePrefix || 'file'
|
|
2492
|
-
const storage = pluginOptions.storage
|
|
2493
|
-
const detectorState = pluginOptions.detectorState || { payload: null }
|
|
2494
|
-
|
|
2495
|
-
if (!storage) {
|
|
2496
|
-
throw new Error('createFileUploadApi requires a storage adapter')
|
|
2497
|
-
}
|
|
2498
|
-
|
|
2499
|
-
if (storageMode.isAnyApi()) {
|
|
2500
|
-
storageMode.clearRegistry()
|
|
2501
|
-
}
|
|
2502
|
-
|
|
2503
|
-
const api = new Api({
|
|
2504
|
-
name: apiName,
|
|
2505
|
-
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
2506
|
-
})
|
|
2507
|
-
|
|
2508
|
-
const previousTenant = storageMode.currentTenant
|
|
2509
|
-
const tenantId = storageMode.isAnyApi()
|
|
2510
|
-
? (pluginOptions.tenantId || `${tablePrefix}_tenant`)
|
|
2511
|
-
: storageMode.defaultTenant
|
|
2512
|
-
|
|
2513
|
-
if (storageMode.isAnyApi()) {
|
|
2514
|
-
storageMode.setCurrentTenant(tenantId)
|
|
2515
|
-
}
|
|
2516
|
-
|
|
2517
|
-
try {
|
|
2518
|
-
await api.use(RestApiPlugin, {
|
|
2519
|
-
simplifiedApi: false,
|
|
2520
|
-
simplifiedTransport: false,
|
|
2521
|
-
returnRecordApi: {
|
|
2522
|
-
post: true,
|
|
2523
|
-
put: false,
|
|
2524
|
-
patch: false
|
|
2525
|
-
},
|
|
2526
|
-
returnRecordTransport: {
|
|
2527
|
-
post: 'full',
|
|
2528
|
-
put: 'no',
|
|
2529
|
-
patch: 'minimal'
|
|
2530
|
-
},
|
|
2531
|
-
sortableFields: ['id', 'title'],
|
|
2532
|
-
...pluginOptions['rest-api']
|
|
2533
|
-
})
|
|
2534
|
-
await useStoragePlugin(api, knex, { tenantId })
|
|
2535
|
-
await resetAnyApiTables(knex)
|
|
2536
|
-
await api.use(FileHandlingPlugin)
|
|
2537
|
-
|
|
2538
|
-
api.rest.registerFileDetector({
|
|
2539
|
-
name: 'test-file-detector',
|
|
2540
|
-
detect: () => Boolean(detectorState.payload),
|
|
2541
|
-
parse: async () => detectorState.payload
|
|
2542
|
-
})
|
|
2543
|
-
|
|
2544
|
-
await api.addResource('documents', {
|
|
2545
|
-
schema: {
|
|
2546
|
-
id: { type: 'id' },
|
|
2547
|
-
title: { type: 'string', required: true },
|
|
2548
|
-
attachment: {
|
|
2549
|
-
type: 'file',
|
|
2550
|
-
storage,
|
|
2551
|
-
accepts: ['image/png'],
|
|
2552
|
-
maxSize: '1mb'
|
|
2553
|
-
}
|
|
2554
|
-
},
|
|
2555
|
-
tableName
|
|
2556
|
-
})
|
|
2557
|
-
await api.resources.documents.createKnexTable()
|
|
2558
|
-
mapTable(tableName, 'documents')
|
|
2559
|
-
|
|
2560
|
-
return api
|
|
2561
|
-
} finally {
|
|
2562
|
-
if (storageMode.isAnyApi()) {
|
|
2563
|
-
storageMode.setCurrentTenant(previousTenant)
|
|
2564
|
-
}
|
|
2565
|
-
}
|
|
2566
|
-
}
|