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