json-rest-api 1.0.28 → 2.0.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 +746 -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 +634 -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 +100 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +90 -31
- package/docs/GUIDE/33-migrating-to-v2.md +3827 -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 +36 -0
- package/index.js +1 -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 +64 -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 -174
- package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
- 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 +25 -16
- 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 +67 -330
- 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 +205 -30
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +20 -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 +238 -187
- 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 +111 -287
- 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 +109 -44
- 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 +142 -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 +116 -2509
- 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 -1069
- 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 -385
- 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 -145
- 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 +38 -69
- 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 +39 -82
- 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/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 +53 -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 -1238
- 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 -150
- 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 -118
- package/tests/fixtures/api-configs.js +0 -2509
- 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-cursor-multifield.test.js +0 -643
- 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/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -1,101 +1,56 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createOrdinaryDataHelpers } from './lib/storage/ordinary-data-helpers.js'
|
|
2
|
+
import { createKnexTransaction } from '../../lib/knex-transaction.js'
|
|
3
|
+
import { createEnhancedLogger } from '../../lib/enhanced-logger.js'
|
|
4
|
+
import { throwMissingPackage } from '../../lib/missing-package.js'
|
|
2
5
|
import { createSchema } from 'json-rest-schema'
|
|
3
|
-
import {
|
|
4
|
-
createKnexTable,
|
|
5
|
-
addKnexFields,
|
|
6
|
-
alterKnexFields,
|
|
7
|
-
generateKnexMigration,
|
|
8
|
-
generateKnexMigrationDiff
|
|
9
|
-
} from './lib/dbTablesOperations.js'
|
|
6
|
+
import { createKnexTable, addKnexFields, alterKnexFields, generateKnexMigration, generateKnexMigrationDiff } from './lib/dbTablesOperations.js'
|
|
10
7
|
import { introspectKnexTableSnapshot } from './lib/dbIntrospection.js'
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import { loadRelationshipIdentifiers } from './lib/querying/knex-relationship-includes.js'
|
|
17
|
-
import {
|
|
18
|
-
polymorphicFiltersHook,
|
|
19
|
-
crossTableFiltersHook,
|
|
20
|
-
basicFiltersHook
|
|
21
|
-
} from './lib/querying/knex-query-helpers.js'
|
|
22
|
-
import { RestApiResourceError, RestApiValidationError } from '../../lib/rest-api-errors.js'
|
|
23
|
-
import { supportsWindowFunctions, getDatabaseInfo } from './lib/querying-writing/database-capabilities.js'
|
|
24
|
-
import { ERROR_SUBTYPES, DEFAULT_QUERY_LIMIT, DEFAULT_MAX_QUERY_LIMIT } from './lib/querying-writing/knex-constants.js'
|
|
25
|
-
import {
|
|
26
|
-
calculatePaginationMeta,
|
|
27
|
-
generatePaginationLinks,
|
|
28
|
-
generateCursorPaginationLinks,
|
|
29
|
-
buildCursorMeta,
|
|
30
|
-
parseCursor
|
|
31
|
-
} from './lib/querying/knex-pagination-helpers.js'
|
|
32
|
-
import { getUrlPrefix } from './lib/querying/url-helpers.js'
|
|
33
|
-
import { createStorageAdapter } from './lib/storage/storage-adapter.js'
|
|
34
|
-
import {
|
|
35
|
-
applyCursorPredicate,
|
|
36
|
-
applyQueryFieldOrder,
|
|
37
|
-
buildEffectiveSortList,
|
|
38
|
-
parseSortEntry
|
|
39
|
-
} from './lib/querying/query-field-sort-helpers.js'
|
|
40
|
-
import { unwrapQueryBuilderState } from './lib/querying/query-builder-utils.js'
|
|
41
|
-
import { serializeJsonApiQuery } from './lib/querying-writing/connectors-query-parser.js'
|
|
8
|
+
import { polymorphicFiltersHook, crossTableFiltersHook, basicFiltersHook } from './lib/querying/knex-query-helpers.js'
|
|
9
|
+
import { getDatabaseCapabilities } from './lib/querying-writing/database-capabilities.js'
|
|
10
|
+
import { createStorageAdapterLookup } from './lib/storage/storage-adapter.js'
|
|
11
|
+
import { assertWritableKnexColumns } from './lib/storage/storage-mapping.js'
|
|
12
|
+
import { assertFieldNameMap } from './lib/querying-writing/field-utils.js'
|
|
42
13
|
|
|
43
14
|
export const RestApiKnexPlugin = {
|
|
44
15
|
name: 'rest-api-knex',
|
|
45
16
|
dependencies: ['rest-api'],
|
|
46
17
|
|
|
47
|
-
async install ({ helpers,
|
|
48
|
-
|
|
18
|
+
async install ({ helpers, pluginOptions, api, log, scopes, addHook, addResourceMethod }) {
|
|
19
|
+
log = createEnhancedLogger(log)
|
|
49
20
|
try {
|
|
50
21
|
await import('knex')
|
|
51
22
|
} catch (e) {
|
|
52
|
-
|
|
23
|
+
throwMissingPackage('knex', 'rest-api-knex',
|
|
53
24
|
'Knex.js is required for database operations. This is a peer dependency that allows you to control the version.')
|
|
54
25
|
}
|
|
55
26
|
|
|
56
|
-
// Get Knex instance from plugin options
|
|
57
27
|
const knexOptions = pluginOptions || {}
|
|
58
28
|
const knex = knexOptions.knex
|
|
59
29
|
|
|
60
|
-
// Expose Knex instance and helpers in a structured way
|
|
61
30
|
api.knex = {
|
|
62
31
|
instance: knex,
|
|
63
32
|
helpers: {}
|
|
64
33
|
}
|
|
65
34
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const resource = api.resources?.[scopeName] || scopes?.[scopeName]
|
|
71
|
-
const schemaInfo = resource?.vars?.schemaInfo
|
|
72
|
-
if (!schemaInfo) return null
|
|
73
|
-
|
|
74
|
-
const cached = storageAdapters.get(scopeName)
|
|
75
|
-
if (cached && cached.schemaInfo === schemaInfo) {
|
|
76
|
-
return cached.adapter
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const adapter = createStorageAdapter({ knex, schemaInfo })
|
|
80
|
-
storageAdapters.set(scopeName, { adapter, schemaInfo })
|
|
81
|
-
if (resource?.vars) {
|
|
82
|
-
resource.vars.storageAdapter = adapter
|
|
83
|
-
}
|
|
84
|
-
return adapter
|
|
85
|
-
}
|
|
35
|
+
const getScopeStorageAdapter = createStorageAdapterLookup({
|
|
36
|
+
knex,
|
|
37
|
+
getResource: scopeName => api.resources?.[scopeName] || scopes?.[scopeName]
|
|
38
|
+
})
|
|
86
39
|
|
|
87
40
|
api.knex.helpers.getStorageAdapter = getScopeStorageAdapter
|
|
88
41
|
helpers.getStorageAdapter = getScopeStorageAdapter
|
|
89
42
|
|
|
43
|
+
addHook('resource:added', 'validate-knex-storage-columns', { afterFunction: 'compileResourceSchemas' }, ({ context }) => {
|
|
44
|
+
assertWritableKnexColumns(context.vars.schemaInfo.storageInfo)
|
|
45
|
+
})
|
|
46
|
+
|
|
90
47
|
const buildScopeTableSchema = (vars = {}) => {
|
|
91
48
|
const schemaStructure = vars.schemaInfo?.schemaStructure || {}
|
|
92
49
|
const filteredSchema = {}
|
|
93
50
|
|
|
94
|
-
|
|
95
|
-
if (!fieldDef.virtual)
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
})
|
|
51
|
+
for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
|
|
52
|
+
if (!fieldDef.virtual) filteredSchema[fieldName] = fieldDef
|
|
53
|
+
}
|
|
99
54
|
|
|
100
55
|
return {
|
|
101
56
|
structure: filteredSchema,
|
|
@@ -106,138 +61,47 @@ export const RestApiKnexPlugin = {
|
|
|
106
61
|
}
|
|
107
62
|
}
|
|
108
63
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
sort,
|
|
112
|
-
schemaInfo,
|
|
113
|
-
sortableFields,
|
|
114
|
-
storageAdapter,
|
|
115
|
-
defaultSort,
|
|
116
|
-
queryFieldRuntimeByField = new Map()
|
|
117
|
-
}) => {
|
|
118
|
-
const effectiveSort = buildEffectiveSortList(sort, { defaultSort, idField: 'id' })
|
|
119
|
-
const descriptors = []
|
|
120
|
-
|
|
121
|
-
for (const sortEntry of effectiveSort) {
|
|
122
|
-
const { field, direction, sqlDirection } = parseSortEntry(sortEntry)
|
|
123
|
-
|
|
124
|
-
if (field !== 'id' && sortableFields?.length > 0 && !sortableFields.includes(field)) {
|
|
125
|
-
log.warn(`Ignoring non-sortable field: ${field}`)
|
|
126
|
-
continue
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const queryFieldRuntime = queryFieldRuntimeByField.get(field)
|
|
130
|
-
if (queryFieldRuntime) {
|
|
131
|
-
applyQueryFieldOrder(query, queryFieldRuntime, sqlDirection)
|
|
132
|
-
descriptors.push({
|
|
133
|
-
field,
|
|
134
|
-
direction: sqlDirection,
|
|
135
|
-
queryFieldRuntime,
|
|
136
|
-
definition: queryFieldRuntime.definition,
|
|
137
|
-
isRelationship: false
|
|
138
|
-
})
|
|
139
|
-
continue
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
let dbField = field
|
|
143
|
-
const searchField = schemaInfo.searchSchemaStructure?.[field]
|
|
144
|
-
if (searchField?.actualField) {
|
|
145
|
-
dbField = searchField.actualField
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const translatedField = storageAdapter?.translateColumn
|
|
149
|
-
? storageAdapter.translateColumn(dbField)
|
|
150
|
-
: dbField
|
|
151
|
-
|
|
152
|
-
query.orderBy(translatedField, direction)
|
|
153
|
-
descriptors.push({
|
|
154
|
-
field,
|
|
155
|
-
direction: sqlDirection,
|
|
156
|
-
column: translatedField,
|
|
157
|
-
definition: schemaInfo.schemaStructure?.[field] || { type: field === 'id' ? 'id' : undefined },
|
|
158
|
-
isRelationship: Boolean(searchField?.isRelationship)
|
|
159
|
-
})
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (descriptors.length === 0) {
|
|
163
|
-
query.orderBy('id', 'asc')
|
|
164
|
-
descriptors.push({
|
|
165
|
-
field: 'id',
|
|
166
|
-
direction: 'ASC',
|
|
167
|
-
column: 'id',
|
|
168
|
-
definition: { type: 'id' },
|
|
169
|
-
isRelationship: false
|
|
170
|
-
})
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return descriptors
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Check database capabilities
|
|
177
|
-
const hasWindowFunctions = await supportsWindowFunctions(knex)
|
|
178
|
-
const dbInfo = await getDatabaseInfo(knex)
|
|
179
|
-
|
|
180
|
-
// Store capabilities in API instance for access throughout
|
|
181
|
-
api.knex.capabilities = {
|
|
182
|
-
windowFunctions: hasWindowFunctions,
|
|
183
|
-
dbInfo
|
|
184
|
-
}
|
|
64
|
+
api.knex.capabilities = await getDatabaseCapabilities(knex, log)
|
|
65
|
+
const { dbInfo, windowFunctions } = api.knex.capabilities
|
|
185
66
|
|
|
186
67
|
log.info('Database capabilities detected:', {
|
|
187
68
|
database: dbInfo.client,
|
|
188
69
|
version: dbInfo.version,
|
|
189
|
-
windowFunctions
|
|
70
|
+
windowFunctions
|
|
190
71
|
})
|
|
191
72
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
/* ╔═════════════════════════════════════════════════════════════════════╗
|
|
195
|
-
* ║ MAIN QUERY FILTERING HOOK ║
|
|
196
|
-
* ║ This is the heart of the filtering system. It processes searchSchema ║
|
|
197
|
-
* ║ filters and builds SQL WHERE conditions with proper JOINs ║
|
|
198
|
-
* ╚═════════════════════════════════════════════════════════════════════╝ */
|
|
73
|
+
const queryHookDependencies = { log, scopes, knex, getStorageAdapter: getScopeStorageAdapter }
|
|
199
74
|
|
|
200
|
-
//
|
|
201
|
-
// Dependencies object for the hooks
|
|
202
|
-
const polymorphicFiltersHookParams = { log, scopes, knex, getStorageAdapter: getScopeStorageAdapter }
|
|
203
|
-
|
|
204
|
-
// Register in specific order: polymorphic → cross-table → basic
|
|
205
|
-
// This ensures proper field qualification when JOINs are present
|
|
206
|
-
|
|
207
|
-
// 1. Polymorphic filters (adds JOINs for polymorphic relationships)
|
|
75
|
+
// Relationship joins must precede basic filters so column names are qualified.
|
|
208
76
|
addHook('knexQueryFiltering', 'polymorphicFiltersHook', {},
|
|
209
|
-
async (hookParams) => polymorphicFiltersHook(hookParams,
|
|
77
|
+
async (hookParams) => polymorphicFiltersHook(hookParams, queryHookDependencies)
|
|
210
78
|
)
|
|
211
79
|
|
|
212
|
-
// 2. Cross-table filters (adds JOINs for cross-table fields)
|
|
213
80
|
addHook('knexQueryFiltering', 'crossTableFiltersHook', {},
|
|
214
|
-
async (hookParams) => crossTableFiltersHook(hookParams,
|
|
81
|
+
async (hookParams) => crossTableFiltersHook(hookParams, queryHookDependencies)
|
|
215
82
|
)
|
|
216
83
|
|
|
217
|
-
// 3. Basic filters (processes simple main table filters)
|
|
218
84
|
addHook('knexQueryFiltering', 'basicFiltersHook', {},
|
|
219
|
-
async (hookParams) => basicFiltersHook(hookParams,
|
|
85
|
+
async (hookParams) => basicFiltersHook(hookParams, queryHookDependencies)
|
|
220
86
|
)
|
|
221
87
|
|
|
222
|
-
// 3. Basic filters (processes simple main table filters)
|
|
223
88
|
addHook('release', 'releaseHook', {},
|
|
224
89
|
async ({ api }) => api.knex.instance.destroy()
|
|
225
90
|
)
|
|
226
91
|
|
|
227
|
-
|
|
228
|
-
addScopeMethod('createKnexTable', async ({ vars, scope, scopeName, scopeOptions, runHooks }) => {
|
|
92
|
+
addResourceMethod('createKnexTable', async ({ vars, scopeOptions }) => {
|
|
229
93
|
const tableSchemaInstance = buildScopeTableSchema(vars)
|
|
230
94
|
await createKnexTable(api.knex.instance, vars.schemaInfo, tableSchemaInstance, scopeOptions)
|
|
231
95
|
})
|
|
232
96
|
|
|
233
|
-
|
|
97
|
+
addResourceMethod('introspectKnexTableSnapshot', async ({ vars }) => {
|
|
234
98
|
return introspectKnexTableSnapshot(api.knex.instance, {
|
|
235
99
|
tableName: vars.schemaInfo.tableName,
|
|
236
100
|
idColumn: vars.schemaInfo.idProperty
|
|
237
101
|
})
|
|
238
102
|
})
|
|
239
103
|
|
|
240
|
-
|
|
104
|
+
addResourceMethod('generateKnexMigration', async ({ vars, scopeOptions }) => {
|
|
241
105
|
return generateKnexMigration(
|
|
242
106
|
vars.schemaInfo.tableName,
|
|
243
107
|
buildScopeTableSchema(vars),
|
|
@@ -249,7 +113,7 @@ export const RestApiKnexPlugin = {
|
|
|
249
113
|
)
|
|
250
114
|
})
|
|
251
115
|
|
|
252
|
-
|
|
116
|
+
addResourceMethod('generateKnexMigrationDiff', async ({ vars, params }) => {
|
|
253
117
|
const snapshot = await introspectKnexTableSnapshot(api.knex.instance, {
|
|
254
118
|
tableName: vars.schemaInfo.tableName,
|
|
255
119
|
idColumn: vars.schemaInfo.idProperty
|
|
@@ -267,9 +131,7 @@ export const RestApiKnexPlugin = {
|
|
|
267
131
|
)
|
|
268
132
|
})
|
|
269
133
|
|
|
270
|
-
|
|
271
|
-
addScopeMethod('alterKnexFields', async ({ vars, scope, scopeName, scopeOptions, runHooks, params }) => {
|
|
272
|
-
// Validate required parameters
|
|
134
|
+
addResourceMethod('alterKnexFields', async ({ vars, params }) => {
|
|
273
135
|
if (!params.fields || typeof params.fields !== 'object') {
|
|
274
136
|
throw new Error('fields parameter is required for alterKnexFields')
|
|
275
137
|
}
|
|
@@ -277,913 +139,27 @@ export const RestApiKnexPlugin = {
|
|
|
277
139
|
await alterKnexFields(
|
|
278
140
|
api.knex.instance,
|
|
279
141
|
vars.schemaInfo.tableName,
|
|
280
|
-
params.fields,
|
|
281
|
-
{ ...params.options, idProperty: vars.schemaInfo.idProperty }
|
|
142
|
+
{ structure: params.fields },
|
|
143
|
+
{ storage: vars.schemaInfo.storage, ...params.options, idProperty: vars.schemaInfo.idProperty }
|
|
282
144
|
)
|
|
283
145
|
})
|
|
284
146
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
// Create schema object from filtered fields
|
|
147
|
+
addResourceMethod('addKnexFields', async ({ vars, scopeName, params }) => {
|
|
148
|
+
assertFieldNameMap(params.fields, `added fields in '${scopeName}'`)
|
|
288
149
|
const partialTableSchema = createSchema(params.fields)
|
|
289
150
|
|
|
290
151
|
await addKnexFields(
|
|
291
152
|
api.knex.instance,
|
|
292
153
|
vars.schemaInfo.tableName,
|
|
293
154
|
partialTableSchema,
|
|
294
|
-
{ idProperty: vars.schemaInfo.idProperty }
|
|
155
|
+
{ storage: vars.schemaInfo.storage, idProperty: vars.schemaInfo.idProperty }
|
|
295
156
|
)
|
|
296
157
|
})
|
|
297
158
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/* ╔═════════════════════════════════════════════════════════════════════╗
|
|
303
|
-
* ║ DATA OPERATION METHODS ║
|
|
304
|
-
* ║ Implementation of the storage interface required by REST API plugin║
|
|
305
|
-
* ╚═════════════════════════════════════════════════════════════════════╝ */
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Checks if a resource exists in the database
|
|
309
|
-
*
|
|
310
|
-
* @param {Object} params - The parameters object
|
|
311
|
-
* @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
312
|
-
* @param {Object} params.context - The context object containing request-specific data
|
|
313
|
-
* @param {string|number} params.context.id - The ID of the resource to check for existence
|
|
314
|
-
* @param {Object} params.context.schemaInfo - Schema information for the resource
|
|
315
|
-
* @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
|
|
316
|
-
* @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
|
|
317
|
-
* @param {Object} params.context.db - Database connection (knex instance or transaction)
|
|
318
|
-
* @returns {Promise<boolean>} True if the resource exists, false otherwise
|
|
319
|
-
*/
|
|
320
|
-
helpers.dataExists = async ({ scopeName, context }) => {
|
|
321
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
322
|
-
if (storageAdapter) {
|
|
323
|
-
context.storageAdapter = storageAdapter
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const id = context.id
|
|
327
|
-
|
|
328
|
-
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
329
|
-
const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
330
|
-
const db = context.db || api.knex.instance
|
|
331
|
-
|
|
332
|
-
log.debug(`[Knex] EXISTS ${tableName}/${id}`)
|
|
333
|
-
|
|
334
|
-
const selectClause = idProperty !== 'id' ? `${idProperty} as id` : 'id'
|
|
335
|
-
|
|
336
|
-
const record = await db(tableName)
|
|
337
|
-
.where(idProperty, id)
|
|
338
|
-
.select(selectClause)
|
|
339
|
-
.first()
|
|
340
|
-
|
|
341
|
-
return !!record
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Retrieves a single resource by ID with support for sparse fieldsets and includes
|
|
346
|
-
*
|
|
347
|
-
* @param {Object} params - The parameters object
|
|
348
|
-
* @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
349
|
-
* @param {Object} params.context - The context object containing request-specific data
|
|
350
|
-
* @param {string|number} params.context.id - The ID of the resource to retrieve
|
|
351
|
-
* @param {Object} params.context.schemaInfo - Schema information for the resource
|
|
352
|
-
* @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
|
|
353
|
-
* @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
|
|
354
|
-
* @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
|
|
355
|
-
* @param {Object} params.context.db - Database connection (knex instance or transaction)
|
|
356
|
-
* @param {Object} [params.context.queryParams] - Query parameters for sparse fieldsets and includes
|
|
357
|
-
* @param {Object} [params.context.queryParams.fields] - Sparse fieldset selections
|
|
358
|
-
* @param {Array<string>} [params.context.queryParams.include] - Related resources to include
|
|
359
|
-
* @param {Object} [params.context.computedDependencies] - Set by function to track computed field dependencies
|
|
360
|
-
* @returns {Promise<Object>} JSON:API formatted response with data and optional included resources
|
|
361
|
-
* @throws {RestApiResourceError} When the resource is not found
|
|
362
|
-
*/
|
|
363
|
-
helpers.dataGet = async ({ scopeName, context, runHooks }) => {
|
|
364
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
365
|
-
if (storageAdapter) {
|
|
366
|
-
context.storageAdapter = storageAdapter
|
|
367
|
-
}
|
|
368
|
-
const scope = api.resources[scopeName]
|
|
369
|
-
if (!scope) {
|
|
370
|
-
log.error('[DATA-GET] scope is undefined!', { scopeName, availableScopes: Object.keys(api.resources || {}) })
|
|
371
|
-
throw new Error(`Scope '${scopeName}' not found in api.resources`)
|
|
372
|
-
}
|
|
373
|
-
if (!scope.scopeName && !scope.name) {
|
|
374
|
-
log.debug('[DATA-GET] Scope structure:', {
|
|
375
|
-
scopeKeys: Object.keys(scope),
|
|
376
|
-
scopeName,
|
|
377
|
-
hasVars: !!scope.vars,
|
|
378
|
-
varKeys: scope.vars ? Object.keys(scope.vars) : []
|
|
379
|
-
})
|
|
380
|
-
}
|
|
381
|
-
const id = context.id
|
|
382
|
-
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
383
|
-
const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
384
|
-
const db = context.db || api.knex.instance
|
|
385
|
-
|
|
386
|
-
log.debug(`[Knex] GET ${tableName}/${id}`)
|
|
387
|
-
|
|
388
|
-
// Build field selection for sparse fieldsets
|
|
389
|
-
// This determines which fields to SELECT from database
|
|
390
|
-
// and tracks dependencies needed for computed fields
|
|
391
|
-
const fieldSelectionInfo = await buildFieldSelection(
|
|
392
|
-
scope,
|
|
393
|
-
{ context }
|
|
394
|
-
)
|
|
395
|
-
|
|
396
|
-
// Store dependency info in context for enrichAttributes
|
|
397
|
-
// Example: If user requests 'profit_margin' (computed), this might contain ['cost']
|
|
398
|
-
// The REST API plugin will use this to remove 'cost' from response if not requested
|
|
399
|
-
context.computedDependencies = fieldSelectionInfo.computedDependencies
|
|
400
|
-
|
|
401
|
-
// Build query - no filtering hooks for single records
|
|
402
|
-
// Permission checks will handle access control
|
|
403
|
-
let query = db(tableName).where(idProperty, id)
|
|
404
|
-
|
|
405
|
-
const selectionState = await applyFieldSelectionToQuery({
|
|
406
|
-
query,
|
|
407
|
-
scope,
|
|
408
|
-
fieldSelectionInfo,
|
|
409
|
-
tableName,
|
|
410
|
-
useTablePrefix: false,
|
|
411
|
-
storageAdapter,
|
|
412
|
-
db,
|
|
413
|
-
context,
|
|
414
|
-
scopeName
|
|
415
|
-
})
|
|
416
|
-
query = selectionState.query
|
|
417
|
-
|
|
418
|
-
const record = await query.first()
|
|
419
|
-
|
|
420
|
-
if (!record) {
|
|
421
|
-
throw new RestApiResourceError(
|
|
422
|
-
'Resource not found',
|
|
423
|
-
{
|
|
424
|
-
subtype: ERROR_SUBTYPES.NOT_FOUND,
|
|
425
|
-
resourceType: scopeName,
|
|
426
|
-
resourceId: id
|
|
427
|
-
}
|
|
428
|
-
)
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Load relationship identifiers for all hasMany relationships
|
|
432
|
-
const records = [record] // Wrap in array for processing
|
|
433
|
-
await loadRelationshipIdentifiers(records, scopeName, scopes, db, context)
|
|
434
|
-
|
|
435
|
-
// Process includes
|
|
436
|
-
const included = await processIncludes(scope, records, {
|
|
437
|
-
log,
|
|
438
|
-
scopes,
|
|
439
|
-
knex,
|
|
440
|
-
context
|
|
441
|
-
})
|
|
442
|
-
|
|
443
|
-
// Build and return response
|
|
444
|
-
return buildJsonApiResponse(scope, records, included, true, scopeName, context)
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Retrieves a single resource by ID with minimal processing (no includes or sparse fieldsets)
|
|
449
|
-
*
|
|
450
|
-
* @param {Object} params - The parameters object
|
|
451
|
-
* @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
452
|
-
* @param {Object} params.context - The context object containing request-specific data
|
|
453
|
-
* @param {string|number} params.context.id - The ID of the resource to retrieve
|
|
454
|
-
* @param {Object} params.context.schemaInfo - Schema information for the resource
|
|
455
|
-
* @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
|
|
456
|
-
* @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
|
|
457
|
-
* @param {Object} params.context.db - Database connection (knex instance or transaction)
|
|
458
|
-
* @returns {Promise<Object|null>} JSON:API formatted resource with belongsTo relationships, or null if not found
|
|
459
|
-
*/
|
|
460
|
-
helpers.dataGetMinimal = async ({
|
|
461
|
-
scopeName,
|
|
462
|
-
context,
|
|
463
|
-
runHooks,
|
|
464
|
-
applyQueryFilters,
|
|
465
|
-
filters = context.queryParams?.filters,
|
|
466
|
-
queryPurpose = 'single'
|
|
467
|
-
}) => {
|
|
468
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
469
|
-
if (storageAdapter) {
|
|
470
|
-
context.storageAdapter = storageAdapter
|
|
471
|
-
}
|
|
472
|
-
const scope = api.resources[scopeName]
|
|
473
|
-
const id = context.id
|
|
474
|
-
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
475
|
-
const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
476
|
-
const db = context.db || api.knex.instance
|
|
477
|
-
|
|
478
|
-
log.debug(`[Knex] GET_MINIMAL ${tableName}/${id}`)
|
|
479
|
-
|
|
480
|
-
// Build query.
|
|
481
|
-
// Single-record lookups still go through knexQueryFiltering when hooks are available,
|
|
482
|
-
// so resource scoping plugins can treat GET/PUT/PATCH/DELETE the same way as collections.
|
|
483
|
-
let query = db(tableName).where(idProperty, id)
|
|
484
|
-
|
|
485
|
-
// Add alias if idProperty is not 'id'
|
|
486
|
-
if (idProperty !== 'id') {
|
|
487
|
-
query = query.select('*', `${idProperty} as id`)
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (typeof applyQueryFilters === 'function') {
|
|
491
|
-
const scopedQueryState = await applyQueryFilters({
|
|
492
|
-
query,
|
|
493
|
-
filters,
|
|
494
|
-
tableName,
|
|
495
|
-
db,
|
|
496
|
-
scopeName,
|
|
497
|
-
queryPurpose,
|
|
498
|
-
storageAdapter
|
|
499
|
-
})
|
|
500
|
-
query = unwrapQueryBuilderState(scopedQueryState, query)
|
|
501
|
-
} else if (runHooks) {
|
|
502
|
-
const previousKnexQuery = context.knexQuery
|
|
503
|
-
context.knexQuery = {
|
|
504
|
-
query,
|
|
505
|
-
filters,
|
|
506
|
-
schemaInfo: context.schemaInfo,
|
|
507
|
-
scopeName,
|
|
508
|
-
tableName,
|
|
509
|
-
db,
|
|
510
|
-
queryPurpose,
|
|
511
|
-
adapter: storageAdapter,
|
|
512
|
-
storageAdapter,
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
try {
|
|
516
|
-
await runHooks('knexQueryFiltering')
|
|
517
|
-
query = context.knexQuery?.query || query
|
|
518
|
-
} finally {
|
|
519
|
-
if (previousKnexQuery === undefined) {
|
|
520
|
-
delete context.knexQuery
|
|
521
|
-
} else {
|
|
522
|
-
context.knexQuery = previousKnexQuery
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
// Execute query
|
|
528
|
-
const record = await query.first()
|
|
529
|
-
|
|
530
|
-
if (!record) {
|
|
531
|
-
return null
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// Transform to JSON:API format with belongsTo relationships
|
|
535
|
-
return toJsonApiRecordWithBelongsTo(scope, record, scopeName)
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
/**
|
|
539
|
-
* Queries resources with support for filtering, sorting, pagination, sparse fieldsets, and includes
|
|
540
|
-
*
|
|
541
|
-
* @param {Object} params - The parameters object
|
|
542
|
-
* @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
543
|
-
* @param {Object} params.context - The context object containing request-specific data
|
|
544
|
-
* @param {Object} params.context.schemaInfo - Schema information for the resource
|
|
545
|
-
* @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
|
|
546
|
-
* @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
|
|
547
|
-
* @param {Object} params.context.schemaInfo.searchSchemaInstance - Search schema for filtering capabilities
|
|
548
|
-
* @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
|
|
549
|
-
* @param {Object} params.context.queryParams - Query parameters object
|
|
550
|
-
* @param {Object} [params.context.queryParams.filters] - Filter conditions
|
|
551
|
-
* @param {Array<string>} [params.context.queryParams.sort] - Sort fields (prefix with - for DESC)
|
|
552
|
-
* @param {Object} [params.context.queryParams.page] - Pagination parameters
|
|
553
|
-
* @param {number} [params.context.queryParams.page.size] - Page size
|
|
554
|
-
* @param {number} [params.context.queryParams.page.number] - Page number (offset pagination)
|
|
555
|
-
* @param {string} [params.context.queryParams.page.after] - Cursor for forward pagination
|
|
556
|
-
* @param {string} [params.context.queryParams.page.before] - Cursor for backward pagination
|
|
557
|
-
* @param {Array<string>} [params.context.queryParams.include] - Related resources to include
|
|
558
|
-
* @param {Object} [params.context.queryParams.fields] - Sparse fieldset selections
|
|
559
|
-
* @param {Object} params.context.db - Database connection (knex instance or transaction)
|
|
560
|
-
* @param {Array<string>} params.context.sortableFields - Array of fields that can be sorted
|
|
561
|
-
* @param {Object} [params.context.knexQuery] - Temporarily set during hooks for query building
|
|
562
|
-
* @param {Object} [params.context.computedDependencies] - Set by function to track computed field dependencies
|
|
563
|
-
* @param {Function} params.runHooks - Function to run hooks (e.g., 'knexQueryFiltering')
|
|
564
|
-
* @returns {Promise<Object>} JSON:API formatted response with data array, optional included resources, and pagination meta/links
|
|
565
|
-
*/
|
|
566
|
-
helpers.dataQuery = async ({ scopeName, context, runHooks }) => {
|
|
567
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
568
|
-
if (storageAdapter) {
|
|
569
|
-
context.storageAdapter = storageAdapter
|
|
570
|
-
}
|
|
571
|
-
const scope = api.resources[scopeName]
|
|
572
|
-
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
573
|
-
const schemaInfo = context.schemaInfo
|
|
574
|
-
const queryParams = context.queryParams
|
|
575
|
-
const db = context.db || api.knex.instance
|
|
576
|
-
const sortableFields = context.sortableFields
|
|
577
|
-
|
|
578
|
-
log.trace('[DATA-QUERY] Starting dataQuery', { scopeName })
|
|
579
|
-
log.debug(`[Knex] QUERY ${tableName}`, queryParams)
|
|
580
|
-
|
|
581
|
-
// Build field selection for sparse fieldsets
|
|
582
|
-
// This determines which fields to SELECT from database
|
|
583
|
-
// and tracks dependencies needed for computed fields
|
|
584
|
-
const fieldSelectionInfo = await buildFieldSelection(
|
|
585
|
-
scope,
|
|
586
|
-
{ context }
|
|
587
|
-
)
|
|
588
|
-
|
|
589
|
-
// Store dependency info in context for enrichAttributes
|
|
590
|
-
// Example: If user requests 'profit_margin' (computed), this might contain ['cost']
|
|
591
|
-
// The REST API plugin will use this to remove 'cost' from response if not requested
|
|
592
|
-
context.computedDependencies = fieldSelectionInfo.computedDependencies
|
|
593
|
-
|
|
594
|
-
// Start building query with table prefix (for JOIN support)
|
|
595
|
-
let query = db(tableName)
|
|
596
|
-
|
|
597
|
-
const selectionState = await applyFieldSelectionToQuery({
|
|
598
|
-
query,
|
|
599
|
-
scope,
|
|
600
|
-
fieldSelectionInfo,
|
|
601
|
-
tableName,
|
|
602
|
-
useTablePrefix: true,
|
|
603
|
-
storageAdapter,
|
|
604
|
-
db,
|
|
605
|
-
context,
|
|
606
|
-
scopeName
|
|
607
|
-
})
|
|
608
|
-
query = selectionState.query
|
|
609
|
-
const queryFieldRuntimeByField = selectionState.queryFieldRuntimeByField
|
|
610
|
-
|
|
611
|
-
/* ═══════════════════════════════════════════════════════════════════
|
|
612
|
-
* FILTERING HOOKS
|
|
613
|
-
* This is where the magic happens. The knexQueryFiltering hook is called
|
|
614
|
-
* to apply all filter conditions. The searchSchemaFilter hook (registered
|
|
615
|
-
* above) will process the searchSchema and apply filters with JOINs.
|
|
616
|
-
*
|
|
617
|
-
* IMPORTANT: Each hook should wrap its conditions in query.where(function() {...})
|
|
618
|
-
* to ensure proper grouping and prevent accidental filter bypass.
|
|
619
|
-
* ═══════════════════════════════════════════════════════════════════ */
|
|
620
|
-
|
|
621
|
-
log.trace('[DATA-QUERY] Calling knexQueryFiltering hook', { hasQuery: !!query, hasFilters: !!queryParams.filters, scopeName, tableName })
|
|
622
|
-
|
|
623
|
-
log.trace('[DATA-QUERY] About to call runHooks', { hookName: 'knexQueryFiltering' })
|
|
624
|
-
|
|
625
|
-
log.trace('[DATA-QUERY] Storing query data in context before calling runHooks')
|
|
626
|
-
|
|
627
|
-
// YOU ARE HERE: pass 'sesrchSchemaInstance' in the context below
|
|
628
|
-
|
|
629
|
-
// Store the query data in context where hooks can access it
|
|
630
|
-
// This is the proper way to share data between methods and hooks
|
|
631
|
-
if (context) {
|
|
632
|
-
context.knexQuery = {
|
|
633
|
-
query,
|
|
634
|
-
filters: queryParams.filters,
|
|
635
|
-
schemaInfo,
|
|
636
|
-
scopeName,
|
|
637
|
-
tableName,
|
|
638
|
-
db,
|
|
639
|
-
queryPurpose: 'collection',
|
|
640
|
-
adapter: storageAdapter,
|
|
641
|
-
storageAdapter,
|
|
642
|
-
}
|
|
159
|
+
/** @type {import('../../lib/transaction-types.js').TransactionFactory} */
|
|
160
|
+
helpers.newTransaction = context => createKnexTransaction(knex, context)
|
|
643
161
|
|
|
644
|
-
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
await runHooks('knexQueryFiltering')
|
|
648
|
-
|
|
649
|
-
// Clean up after hook execution
|
|
650
|
-
if (context && context.knexQuery) {
|
|
651
|
-
delete context.knexQuery
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
log.trace('[DATA-QUERY] Finished knexQueryFiltering hook')
|
|
655
|
-
|
|
656
|
-
const sortDescriptors = buildLegacySortDescriptors({
|
|
657
|
-
query,
|
|
658
|
-
sort: queryParams.sort,
|
|
659
|
-
schemaInfo,
|
|
660
|
-
sortableFields,
|
|
661
|
-
defaultSort: scope.vars.defaultSort,
|
|
662
|
-
storageAdapter,
|
|
663
|
-
queryFieldRuntimeByField
|
|
664
|
-
})
|
|
665
|
-
|
|
666
|
-
// Apply pagination
|
|
667
|
-
// Check if page object has any actual pagination parameters
|
|
668
|
-
const hasPageParams = queryParams.page &&
|
|
669
|
-
(queryParams.page.size !== undefined ||
|
|
670
|
-
queryParams.page.number !== undefined ||
|
|
671
|
-
queryParams.page.after !== undefined ||
|
|
672
|
-
queryParams.page.before !== undefined)
|
|
673
|
-
|
|
674
|
-
if (hasPageParams) {
|
|
675
|
-
const requestedSize = queryParams.page.size || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
|
|
676
|
-
const pageSize = Math.min(
|
|
677
|
-
requestedSize,
|
|
678
|
-
scope.vars.queryMaxLimit || DEFAULT_MAX_QUERY_LIMIT
|
|
679
|
-
)
|
|
680
|
-
|
|
681
|
-
// Validate page size
|
|
682
|
-
if (requestedSize <= 0) {
|
|
683
|
-
throw new RestApiValidationError(
|
|
684
|
-
'Page size must be greater than 0',
|
|
685
|
-
{
|
|
686
|
-
fields: ['page.size'],
|
|
687
|
-
violations: [{
|
|
688
|
-
field: 'page.size',
|
|
689
|
-
rule: 'min_value',
|
|
690
|
-
message: 'Page size must be a positive number'
|
|
691
|
-
}]
|
|
692
|
-
}
|
|
693
|
-
)
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
// Offset-based pagination
|
|
697
|
-
if (queryParams.page.number !== undefined) {
|
|
698
|
-
const pageNumber = queryParams.page.number || 1
|
|
699
|
-
query
|
|
700
|
-
.limit(pageSize)
|
|
701
|
-
.offset((pageNumber - 1) * pageSize)
|
|
702
|
-
} else if (queryParams.page.after || queryParams.page.before) {
|
|
703
|
-
// Fetch one extra record to determine if there are more
|
|
704
|
-
query.limit(pageSize + 1)
|
|
705
|
-
|
|
706
|
-
if (queryParams.page.after) {
|
|
707
|
-
let cursorData
|
|
708
|
-
try {
|
|
709
|
-
cursorData = parseCursor(queryParams.page.after)
|
|
710
|
-
} catch (error) {
|
|
711
|
-
throw new RestApiValidationError(
|
|
712
|
-
'Invalid cursor format in page[after] parameter',
|
|
713
|
-
{
|
|
714
|
-
fields: ['page.after'],
|
|
715
|
-
violations: [{
|
|
716
|
-
field: 'page.after',
|
|
717
|
-
rule: 'invalid_cursor',
|
|
718
|
-
message: 'The cursor value is not valid'
|
|
719
|
-
}]
|
|
720
|
-
}
|
|
721
|
-
)
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
applyCursorPredicate(
|
|
725
|
-
query,
|
|
726
|
-
sortDescriptors,
|
|
727
|
-
cursorData,
|
|
728
|
-
(direction) => (direction === 'DESC' ? '<' : '>'),
|
|
729
|
-
(builder, descriptor, operator, value) => builder.where(
|
|
730
|
-
descriptor.column,
|
|
731
|
-
operator,
|
|
732
|
-
storageAdapter?.translateFilterValue
|
|
733
|
-
? storageAdapter.translateFilterValue(descriptor.field, value)
|
|
734
|
-
: value
|
|
735
|
-
),
|
|
736
|
-
{
|
|
737
|
-
onMissingValue: (fieldName) => log.warn(`Cursor missing value for sort field: ${fieldName}`)
|
|
738
|
-
}
|
|
739
|
-
)
|
|
740
|
-
} else if (queryParams.page.before) {
|
|
741
|
-
let cursorData
|
|
742
|
-
try {
|
|
743
|
-
cursorData = parseCursor(queryParams.page.before)
|
|
744
|
-
} catch (error) {
|
|
745
|
-
throw new RestApiValidationError(
|
|
746
|
-
'Invalid cursor format in page[before] parameter',
|
|
747
|
-
{
|
|
748
|
-
fields: ['page.before'],
|
|
749
|
-
violations: [{
|
|
750
|
-
field: 'page.before',
|
|
751
|
-
rule: 'invalid_cursor',
|
|
752
|
-
message: 'The cursor value is not valid'
|
|
753
|
-
}]
|
|
754
|
-
}
|
|
755
|
-
)
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
applyCursorPredicate(
|
|
759
|
-
query,
|
|
760
|
-
sortDescriptors,
|
|
761
|
-
cursorData,
|
|
762
|
-
(direction) => (direction === 'DESC' ? '>' : '<'),
|
|
763
|
-
(builder, descriptor, operator, value) => builder.where(
|
|
764
|
-
descriptor.column,
|
|
765
|
-
operator,
|
|
766
|
-
storageAdapter?.translateFilterValue
|
|
767
|
-
? storageAdapter.translateFilterValue(descriptor.field, value)
|
|
768
|
-
: value
|
|
769
|
-
),
|
|
770
|
-
{
|
|
771
|
-
onMissingValue: (fieldName) => log.warn(`Cursor missing value for sort field: ${fieldName}`)
|
|
772
|
-
}
|
|
773
|
-
)
|
|
774
|
-
}
|
|
775
|
-
} else {
|
|
776
|
-
// Fetch one extra to detect hasMore
|
|
777
|
-
query.limit(pageSize + 1)
|
|
778
|
-
}
|
|
779
|
-
} else {
|
|
780
|
-
// No pagination params provided - apply default limit
|
|
781
|
-
const defaultLimit = scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
|
|
782
|
-
query.limit(defaultLimit)
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
// Execute query
|
|
786
|
-
const records = await query
|
|
787
|
-
|
|
788
|
-
// Initialize returnMeta namespace for thread-safe metadata
|
|
789
|
-
context.returnMeta = context.returnMeta || {}
|
|
790
|
-
const queryString = serializeJsonApiQuery(queryParams)
|
|
791
|
-
context.returnMeta.queryString = queryString ? `?${queryString}` : ''
|
|
792
|
-
|
|
793
|
-
// Execute count query for pagination if offset-based pagination is used
|
|
794
|
-
if (queryParams.page?.number !== undefined || (queryParams.page?.size !== undefined && !queryParams.page?.after && !queryParams.page?.before)) {
|
|
795
|
-
const page = parseInt(queryParams.page?.number) || 1
|
|
796
|
-
const pageSize = parseInt(queryParams.page?.size) || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
|
|
797
|
-
|
|
798
|
-
// Only execute count query if enabled
|
|
799
|
-
if (scope.vars.enablePaginationCounts) {
|
|
800
|
-
// Build count query with same filters as main query
|
|
801
|
-
let countQuery = db(tableName)
|
|
802
|
-
|
|
803
|
-
// Mandatory server-side filters must also run when the client supplied no filters.
|
|
804
|
-
// Otherwise pagination metadata can disclose or count rows excluded from the page.
|
|
805
|
-
const previousKnexQuery = context.knexQuery
|
|
806
|
-
context.knexQuery = {
|
|
807
|
-
query: countQuery,
|
|
808
|
-
filters: queryParams.filters,
|
|
809
|
-
scopeName,
|
|
810
|
-
tableName,
|
|
811
|
-
schemaInfo,
|
|
812
|
-
db,
|
|
813
|
-
queryPurpose: 'count',
|
|
814
|
-
adapter: storageAdapter,
|
|
815
|
-
storageAdapter
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
try {
|
|
819
|
-
await runHooks('knexQueryFiltering')
|
|
820
|
-
countQuery = context.knexQuery?.query || countQuery
|
|
821
|
-
} finally {
|
|
822
|
-
if (previousKnexQuery === undefined) {
|
|
823
|
-
delete context.knexQuery
|
|
824
|
-
} else {
|
|
825
|
-
context.knexQuery = previousKnexQuery
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
|
|
829
|
-
// Get total count
|
|
830
|
-
const countResult = await countQuery.count('* as total').first()
|
|
831
|
-
const total = parseInt(countResult.total)
|
|
832
|
-
|
|
833
|
-
// Calculate pagination metadata with total
|
|
834
|
-
context.returnMeta.paginationMeta = calculatePaginationMeta(total, page, pageSize)
|
|
835
|
-
} else {
|
|
836
|
-
// Without count, we can still provide basic pagination info
|
|
837
|
-
context.returnMeta.paginationMeta = {
|
|
838
|
-
page,
|
|
839
|
-
pageSize
|
|
840
|
-
// No total, pageCount, or hasMore when counts are disabled
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
// Generate links
|
|
845
|
-
const urlPrefix = getUrlPrefix(context, scope)
|
|
846
|
-
context.returnMeta.paginationLinks = generatePaginationLinks(
|
|
847
|
-
urlPrefix,
|
|
848
|
-
scopeName,
|
|
849
|
-
queryParams,
|
|
850
|
-
context.returnMeta.paginationMeta
|
|
851
|
-
)
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
// Handle cursor-based pagination meta
|
|
855
|
-
// Generate cursor metadata when using cursor parameters OR when only size is specified (no page number)
|
|
856
|
-
if (queryParams.page?.after || queryParams.page?.before ||
|
|
857
|
-
(queryParams.page?.size && queryParams.page?.number === undefined)) {
|
|
858
|
-
const pageSize = parseInt(queryParams.page?.size) || scope.vars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
|
|
859
|
-
|
|
860
|
-
// Check if there are more records
|
|
861
|
-
// We fetched pageSize + 1 records to detect if there are more
|
|
862
|
-
const hasMore = records.length > pageSize
|
|
863
|
-
|
|
864
|
-
// Remove the extra record if present
|
|
865
|
-
if (hasMore) {
|
|
866
|
-
records.pop()
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
const sortFields = sortDescriptors.map((descriptor) => descriptor.field)
|
|
870
|
-
|
|
871
|
-
context.returnMeta.paginationMeta = buildCursorMeta(records, pageSize, hasMore, sortFields, {
|
|
872
|
-
schemaInfo
|
|
873
|
-
})
|
|
874
|
-
const urlPrefix = getUrlPrefix(context, scope)
|
|
875
|
-
context.returnMeta.paginationLinks = generateCursorPaginationLinks(
|
|
876
|
-
urlPrefix,
|
|
877
|
-
scopeName,
|
|
878
|
-
queryParams,
|
|
879
|
-
records,
|
|
880
|
-
pageSize,
|
|
881
|
-
hasMore,
|
|
882
|
-
sortFields,
|
|
883
|
-
{
|
|
884
|
-
schemaInfo
|
|
885
|
-
}
|
|
886
|
-
)
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// Load relationship identifiers for all hasMany relationships
|
|
890
|
-
await loadRelationshipIdentifiers(records, scopeName, scopes, db, context)
|
|
891
|
-
|
|
892
|
-
// Process includes
|
|
893
|
-
const included = await processIncludes(scope, records, {
|
|
894
|
-
log,
|
|
895
|
-
scopes,
|
|
896
|
-
knex,
|
|
897
|
-
context,
|
|
898
|
-
api
|
|
899
|
-
})
|
|
900
|
-
|
|
901
|
-
// Build and return response
|
|
902
|
-
return buildJsonApiResponse(scope, records, included, false, scopeName, context)
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
/**
|
|
906
|
-
* Creates a new resource in the database
|
|
907
|
-
*
|
|
908
|
-
* @param {Object} params - The parameters object
|
|
909
|
-
* @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
910
|
-
* @param {Object} params.context - The context object containing request-specific data
|
|
911
|
-
* @param {Object} params.context.schemaInfo - Schema information for the resource
|
|
912
|
-
* @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
|
|
913
|
-
* @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
|
|
914
|
-
* @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
|
|
915
|
-
* @param {Object} params.context.db - Database connection (knex instance or transaction)
|
|
916
|
-
* @param {Object} params.context.inputRecord - JSON:API formatted input record
|
|
917
|
-
* @param {Object} params.context.inputRecord.data - The resource data
|
|
918
|
-
* @param {Object} params.context.inputRecord.data.attributes - The resource attributes to insert
|
|
919
|
-
* @returns {Promise<string|number>} The ID of the newly created resource
|
|
920
|
-
*/
|
|
921
|
-
helpers.dataPost = async ({ scopeName, context }) => {
|
|
922
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
923
|
-
if (storageAdapter) {
|
|
924
|
-
context.storageAdapter = storageAdapter
|
|
925
|
-
}
|
|
926
|
-
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
927
|
-
const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
928
|
-
const db = context.db || api.knex.instance
|
|
929
|
-
const inputRecord = context.inputRecord
|
|
930
|
-
|
|
931
|
-
log.debug(`[Knex] POST ${tableName}`, inputRecord)
|
|
932
|
-
|
|
933
|
-
// Extract attributes from JSON:API format.
|
|
934
|
-
// POST may legitimately provide a resource id when the table uses a custom primary key
|
|
935
|
-
// (for example, one-to-one rows keyed by user_id/workspace_id). Keep that id in the
|
|
936
|
-
// attribute bag so storage translation can write it to the configured id column.
|
|
937
|
-
const attributes = {
|
|
938
|
-
...(inputRecord.data.attributes || {}),
|
|
939
|
-
...(inputRecord.data.id !== undefined ? { id: inputRecord.data.id } : {})
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
// Strip non-database fields (computed and virtual) before insert
|
|
943
|
-
const dbAttributes = storageAdapter?.toStorageRow
|
|
944
|
-
? storageAdapter.toStorageRow(attributes, {
|
|
945
|
-
context,
|
|
946
|
-
operation: 'post'
|
|
947
|
-
})
|
|
948
|
-
: attributes
|
|
949
|
-
|
|
950
|
-
const explicitId = dbAttributes[idProperty] ?? inputRecord.data.id
|
|
951
|
-
|
|
952
|
-
// Insert and get the new ID
|
|
953
|
-
const result = await db(tableName).insert(dbAttributes).returning(idProperty)
|
|
954
|
-
|
|
955
|
-
// Extract the ID value. Some dialects ignore .returning() for inserts and can
|
|
956
|
-
// yield 0 even when the caller explicitly supplied the primary key.
|
|
957
|
-
const returnedId = Array.isArray(result)
|
|
958
|
-
? (result[0]?.[idProperty] ?? result[0])
|
|
959
|
-
: (result?.[idProperty] ?? result)
|
|
960
|
-
|
|
961
|
-
if (
|
|
962
|
-
explicitId !== undefined &&
|
|
963
|
-
explicitId !== null &&
|
|
964
|
-
(returnedId === undefined || returnedId === null || returnedId === 0 || returnedId === '0')
|
|
965
|
-
) {
|
|
966
|
-
return explicitId
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
return returnedId
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
/**
|
|
973
|
-
* Replaces an entire resource (PUT operation) or creates it with a specific ID
|
|
974
|
-
*
|
|
975
|
-
* @param {Object} params - The parameters object
|
|
976
|
-
* @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
977
|
-
* @param {Object} params.context - The context object containing request-specific data
|
|
978
|
-
* @param {string|number} params.context.id - The ID of the resource to replace or create
|
|
979
|
-
* @param {Object} params.context.schemaInfo - Schema information for the resource
|
|
980
|
-
* @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
|
|
981
|
-
* @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
|
|
982
|
-
* @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
|
|
983
|
-
* @param {Object} params.context.db - Database connection (knex instance or transaction)
|
|
984
|
-
* @param {Object} params.context.inputRecord - JSON:API formatted input record
|
|
985
|
-
* @param {Object} params.context.inputRecord.data - The resource data
|
|
986
|
-
* @param {Object} [params.context.inputRecord.data.attributes] - The resource attributes
|
|
987
|
-
* @param {Object} [params.context.inputRecord.data.relationships] - The resource relationships (processed for foreign keys)
|
|
988
|
-
* @param {boolean} params.context.isCreate - Whether this is a create operation (true) or update (false)
|
|
989
|
-
* @returns {Promise<void>} Resolves when the operation is complete
|
|
990
|
-
* @throws {RestApiResourceError} When updating and the resource is not found
|
|
991
|
-
*/
|
|
992
|
-
helpers.dataPut = async ({ scopeName, context }) => {
|
|
993
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
994
|
-
if (storageAdapter) {
|
|
995
|
-
context.storageAdapter = storageAdapter
|
|
996
|
-
}
|
|
997
|
-
const scope = api.resources[scopeName]
|
|
998
|
-
const id = context.id
|
|
999
|
-
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
1000
|
-
const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
1001
|
-
const db = context.db || api.knex.instance
|
|
1002
|
-
const inputRecord = context.inputRecord
|
|
1003
|
-
const isCreate = context.isCreate
|
|
1004
|
-
|
|
1005
|
-
log.debug(`[Knex] PUT ${tableName}/${id} (isCreate: ${context.isCreate})`)
|
|
1006
|
-
|
|
1007
|
-
// Extract attributes and process relationships using helper
|
|
1008
|
-
const attributes = inputRecord.data.attributes || {}
|
|
1009
|
-
const foreignKeyUpdates = processBelongsToRelationships(scope, { context })
|
|
1010
|
-
const mergedAttributes = { ...attributes, ...foreignKeyUpdates }
|
|
1011
|
-
|
|
1012
|
-
// Strip non-database fields (computed and virtual) before database operation
|
|
1013
|
-
const finalAttributes = storageAdapter?.toStorageRow
|
|
1014
|
-
? storageAdapter.toStorageRow(mergedAttributes, {
|
|
1015
|
-
context,
|
|
1016
|
-
operation: 'put'
|
|
1017
|
-
})
|
|
1018
|
-
: mergedAttributes
|
|
1019
|
-
|
|
1020
|
-
// Map 'id' to actual idProperty if needed (for PUT with specific ID)
|
|
1021
|
-
if (idProperty !== 'id' && inputRecord.data.id) {
|
|
1022
|
-
finalAttributes[idProperty] = inputRecord.data.id
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
if (isCreate) {
|
|
1026
|
-
// Create mode - insert new record with specified ID
|
|
1027
|
-
const recordData = {
|
|
1028
|
-
...finalAttributes,
|
|
1029
|
-
[idProperty]: id
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
await db(tableName).insert(recordData)
|
|
1033
|
-
} else {
|
|
1034
|
-
// Update mode - check if record exists first
|
|
1035
|
-
const exists = await db(tableName)
|
|
1036
|
-
.where(idProperty, id)
|
|
1037
|
-
.first()
|
|
1038
|
-
|
|
1039
|
-
if (!exists) {
|
|
1040
|
-
throw new RestApiResourceError(
|
|
1041
|
-
'Resource not found',
|
|
1042
|
-
{
|
|
1043
|
-
subtype: ERROR_SUBTYPES.NOT_FOUND,
|
|
1044
|
-
resourceType: scopeName,
|
|
1045
|
-
resourceId: id
|
|
1046
|
-
}
|
|
1047
|
-
)
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
// Remove the idProperty from attributes to prevent updating the primary key
|
|
1051
|
-
delete finalAttributes[idProperty]
|
|
1052
|
-
|
|
1053
|
-
// Update the record (replace all fields)
|
|
1054
|
-
if (Object.keys(finalAttributes).length > 0) {
|
|
1055
|
-
await db(tableName)
|
|
1056
|
-
.where(idProperty, id)
|
|
1057
|
-
.update(finalAttributes)
|
|
1058
|
-
}
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
/**
|
|
1063
|
-
* Partially updates a resource (PATCH operation)
|
|
1064
|
-
*
|
|
1065
|
-
* @param {Object} params - The parameters object
|
|
1066
|
-
* @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
1067
|
-
* @param {Object} params.context - The context object containing request-specific data
|
|
1068
|
-
* @param {string|number} params.context.id - The ID of the resource to update
|
|
1069
|
-
* @param {Object} params.context.schemaInfo - Schema information for the resource
|
|
1070
|
-
* @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
|
|
1071
|
-
* @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
|
|
1072
|
-
* @param {Object} params.context.schemaInfo.schemaInstance - The full schema definition for the resource
|
|
1073
|
-
* @param {Object} params.context.db - Database connection (knex instance or transaction)
|
|
1074
|
-
* @param {Object} params.context.inputRecord - JSON:API formatted input record with partial updates
|
|
1075
|
-
* @param {Object} params.context.inputRecord.data - The resource data
|
|
1076
|
-
* @param {Object} [params.context.inputRecord.data.attributes] - The resource attributes to update
|
|
1077
|
-
* @param {Object} [params.context.inputRecord.data.relationships] - The resource relationships (processed for foreign keys)
|
|
1078
|
-
* @returns {Promise<void>} Resolves when the update is complete
|
|
1079
|
-
* @throws {RestApiResourceError} When the resource is not found
|
|
1080
|
-
*/
|
|
1081
|
-
helpers.dataPatch = async ({ scopeName, context }) => {
|
|
1082
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
1083
|
-
if (storageAdapter) {
|
|
1084
|
-
context.storageAdapter = storageAdapter
|
|
1085
|
-
}
|
|
1086
|
-
const scope = api.resources[scopeName]
|
|
1087
|
-
const id = context.id
|
|
1088
|
-
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
1089
|
-
const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
1090
|
-
const db = context.db || api.knex.instance
|
|
1091
|
-
const inputRecord = context.inputRecord
|
|
1092
|
-
|
|
1093
|
-
log.debug(`[Knex] PATCH ${tableName}/${id}`)
|
|
1094
|
-
|
|
1095
|
-
// Check if record exists
|
|
1096
|
-
const exists = await db(tableName)
|
|
1097
|
-
.where(idProperty, id)
|
|
1098
|
-
.first()
|
|
1099
|
-
|
|
1100
|
-
if (!exists) {
|
|
1101
|
-
throw new RestApiResourceError(
|
|
1102
|
-
'Resource not found',
|
|
1103
|
-
{
|
|
1104
|
-
subtype: ERROR_SUBTYPES.NOT_FOUND,
|
|
1105
|
-
resourceType: scopeName,
|
|
1106
|
-
resourceId: id
|
|
1107
|
-
}
|
|
1108
|
-
)
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
// Extract attributes and process relationships using helper
|
|
1112
|
-
const attributes = inputRecord.data.attributes || {}
|
|
1113
|
-
const foreignKeyUpdates = processBelongsToRelationships(scope, { context })
|
|
1114
|
-
const mergedAttributes = { ...attributes, ...foreignKeyUpdates }
|
|
1115
|
-
|
|
1116
|
-
// Strip non-database fields (computed and virtual) before database operation
|
|
1117
|
-
const finalAttributes = storageAdapter?.toStorageRow
|
|
1118
|
-
? storageAdapter.toStorageRow(mergedAttributes, {
|
|
1119
|
-
context,
|
|
1120
|
-
operation: 'patch'
|
|
1121
|
-
})
|
|
1122
|
-
: mergedAttributes
|
|
1123
|
-
|
|
1124
|
-
log.debug('[Knex] PATCH finalAttributes:', finalAttributes)
|
|
1125
|
-
|
|
1126
|
-
// Remove the idProperty from attributes to prevent updating the primary key
|
|
1127
|
-
delete finalAttributes[idProperty]
|
|
1128
|
-
|
|
1129
|
-
// Update only if there are changes
|
|
1130
|
-
if (Object.keys(finalAttributes).length > 0) {
|
|
1131
|
-
await db(tableName)
|
|
1132
|
-
.where(idProperty, id)
|
|
1133
|
-
.update(finalAttributes)
|
|
1134
|
-
}
|
|
1135
|
-
}
|
|
1136
|
-
|
|
1137
|
-
/**
|
|
1138
|
-
* Deletes a resource from the database
|
|
1139
|
-
*
|
|
1140
|
-
* @param {Object} params - The parameters object
|
|
1141
|
-
* @param {string} params.scopeName - The name of the resource scope (e.g., 'books', 'authors')
|
|
1142
|
-
* @param {Object} params.context - The context object containing request-specific data
|
|
1143
|
-
* @param {string|number} params.context.id - The ID of the resource to delete
|
|
1144
|
-
* @param {Object} params.context.schemaInfo - Schema information for the resource
|
|
1145
|
-
* @param {string} params.context.schemaInfo.tableName - The database table name (e.g., 'basic_books')
|
|
1146
|
-
* @param {string} params.context.schemaInfo.idProperty - The primary key field name (e.g., 'id')
|
|
1147
|
-
* @param {Object} params.context.db - Database connection (knex instance or transaction)
|
|
1148
|
-
* @returns {Promise<Object>} Returns { success: true } when deletion is successful
|
|
1149
|
-
* @throws {RestApiResourceError} When the resource is not found
|
|
1150
|
-
*/
|
|
1151
|
-
helpers.dataDelete = async ({ scopeName, context }) => {
|
|
1152
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
1153
|
-
if (storageAdapter) {
|
|
1154
|
-
context.storageAdapter = storageAdapter
|
|
1155
|
-
}
|
|
1156
|
-
const id = context.id
|
|
1157
|
-
|
|
1158
|
-
const tableName = storageAdapter?.getTableName?.() || context.schemaInfo.tableName
|
|
1159
|
-
const idProperty = storageAdapter?.getIdColumn?.() || context.schemaInfo.idProperty
|
|
1160
|
-
const db = context.db || api.knex.instance
|
|
1161
|
-
|
|
1162
|
-
log.debug(`[Knex] DELETE ${tableName}/${id}`)
|
|
1163
|
-
|
|
1164
|
-
// Check if record exists
|
|
1165
|
-
const exists = await db(tableName)
|
|
1166
|
-
.where(idProperty, id)
|
|
1167
|
-
.first()
|
|
1168
|
-
|
|
1169
|
-
if (!exists) {
|
|
1170
|
-
throw new RestApiResourceError(
|
|
1171
|
-
'Resource not found',
|
|
1172
|
-
{
|
|
1173
|
-
subtype: ERROR_SUBTYPES.NOT_FOUND,
|
|
1174
|
-
resourceType: scopeName,
|
|
1175
|
-
resourceId: id
|
|
1176
|
-
}
|
|
1177
|
-
)
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
// Delete the record
|
|
1181
|
-
await db(tableName)
|
|
1182
|
-
.where(idProperty, id)
|
|
1183
|
-
.delete()
|
|
1184
|
-
|
|
1185
|
-
return { success: true }
|
|
1186
|
-
}
|
|
162
|
+
Object.assign(helpers, createOrdinaryDataHelpers({ api, scopes, knex, log, getScopeStorageAdapter }))
|
|
1187
163
|
|
|
1188
164
|
log.info('RestApiKnexPlugin installed - basic CRUD operations ready')
|
|
1189
165
|
}
|