json-rest-api 1.0.29 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +744 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +733 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +103 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
- package/docs/GUIDE/33-migrating-to-v2.md +3838 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +37 -0
- package/index.js +3 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +70 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -177
- package/plugins/core/lib/anyapi/anyapi-registry.js +586 -418
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +120 -56
- package/plugins/core/lib/anyapi/stored-definition.js +109 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +13 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +128 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1077
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -395
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +36 -68
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +37 -81
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/anyapi.d.ts +30 -0
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +65 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -148
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -155
- package/tests/fixtures/api-configs.js +0 -2566
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-boundaries.test.js +0 -154
- package/tests/pagination-contract.test.js +0 -122
- package/tests/pagination-cursor-multifield.test.js +0 -641
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/temporal-boundaries.test.js +0 -208
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -0,0 +1,710 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @import { DataMinimalRequest, DataReadContext, DataRelatedIdsQuery, DataResource, DataSortDescriptor, DataSortRequest, OrdinaryDataDependencies, OrdinaryDataReadHelpers, OrdinaryDataWriteHelpers, StorageRow } from './storage-types.js' */
|
|
3
|
+
import { prepareReferenceSortColumns } from '../querying/knex-query-helpers.js'
|
|
4
|
+
import { applySortDescriptorOrder, buildEffectiveSortList, parseSortEntry, resolveSortField } from '../querying/query-field-sort-helpers.js'
|
|
5
|
+
import { deletePivotReferences, invalidateDeletedPivotTargets } from '../writing/many-to-many-manipulations.js'
|
|
6
|
+
import { applyFieldSelectionToQuery, buildFieldSelection } from '../querying-writing/knex-field-helpers.js'
|
|
7
|
+
import { buildJsonApiResponse } from '../querying/knex-json-api-transformers-querying.js'
|
|
8
|
+
import { toJsonApiRecordWithBelongsTo } from '../querying-writing/knex-json-api-transformers.js'
|
|
9
|
+
import { processIncludes } from '../querying/knex-process-includes.js'
|
|
10
|
+
import { loadRelationshipIdentifiers } from '../querying/relationship-identifiers.js'
|
|
11
|
+
import { applyQueryConstraint } from '../querying/query-constraint.js'
|
|
12
|
+
import { RestApiResourceError } from '../../../../lib/rest-api-errors.js'
|
|
13
|
+
import { getOperationDiagnosticContext } from '../../../../lib/error-context.js'
|
|
14
|
+
import { applyInsertReturning } from '../querying-writing/database-capabilities.js'
|
|
15
|
+
import { ERROR_SUBTYPES } from '../querying-writing/knex-constants.js'
|
|
16
|
+
import { calculatePaginationMeta, generatePaginationLinks, generateCursorPaginationLinks, buildCursorMeta, applyPaginationToQuery } from '../querying/knex-pagination-helpers.js'
|
|
17
|
+
import { getUrlPrefix } from '../querying/url-helpers.js'
|
|
18
|
+
import { unwrapQueryBuilderState, withQueryFilteringContext } from '../querying/query-builder-utils.js'
|
|
19
|
+
import { serializeJsonApiQuery } from '../querying-writing/connectors-query-parser.js'
|
|
20
|
+
import { applyDatabaseReadOptions, databaseIdentityExpression } from '../querying-writing/database-value-normalizers.js'
|
|
21
|
+
|
|
22
|
+
/** @param {OrdinaryDataDependencies} dependencies */
|
|
23
|
+
export function createOrdinaryDataHelpers ({ api, scopes, knex, log, getScopeStorageAdapter: findStorageAdapter }) {
|
|
24
|
+
/** @param {string} scopeName */
|
|
25
|
+
const getScopeStorageAdapter = scopeName => {
|
|
26
|
+
const adapter = findStorageAdapter(scopeName)
|
|
27
|
+
if (!adapter) throw new Error(`Storage adapter for resource '${scopeName}' not found`)
|
|
28
|
+
return adapter
|
|
29
|
+
}
|
|
30
|
+
/** @param {string} scopeName */
|
|
31
|
+
const getScope = scopeName => {
|
|
32
|
+
const scope = api.resources[scopeName]
|
|
33
|
+
if (!scope) throw new Error(`Scope '${scopeName}' not found in api.resources`)
|
|
34
|
+
return scope
|
|
35
|
+
}
|
|
36
|
+
/** @param {DataSortRequest} request @returns {Promise<DataSortDescriptor[]>} */
|
|
37
|
+
const buildSortDescriptors = async ({
|
|
38
|
+
query,
|
|
39
|
+
sort,
|
|
40
|
+
schemaInfo,
|
|
41
|
+
sortableFields,
|
|
42
|
+
storageAdapter,
|
|
43
|
+
defaultSort,
|
|
44
|
+
scopeName,
|
|
45
|
+
context,
|
|
46
|
+
before = false,
|
|
47
|
+
queryFieldRuntimeByField = new Map()
|
|
48
|
+
}) => {
|
|
49
|
+
const effectiveSort = buildEffectiveSortList(sort, { defaultSort, idField: 'id', schemaInfo })
|
|
50
|
+
/** @type {DataSortDescriptor[]} */
|
|
51
|
+
const descriptors = []
|
|
52
|
+
const referenceColumns = await prepareReferenceSortColumns({
|
|
53
|
+
query,
|
|
54
|
+
fields: effectiveSort.map(entry => parseSortEntry(entry).field).filter(field =>
|
|
55
|
+
!queryFieldRuntimeByField.has(field) && (field === 'id' || !sortableFields?.length || sortableFields.includes(field))),
|
|
56
|
+
scopeName,
|
|
57
|
+
tableAlias: storageAdapter.getTableName(),
|
|
58
|
+
context
|
|
59
|
+
}, { scopes, knex, getStorageAdapter: getScopeStorageAdapter })
|
|
60
|
+
|
|
61
|
+
for (const sortEntry of effectiveSort) {
|
|
62
|
+
const { field, direction, sqlDirection } = parseSortEntry(sortEntry)
|
|
63
|
+
|
|
64
|
+
if (field !== 'id' && sortableFields && sortableFields.length > 0 && !sortableFields.includes(field)) {
|
|
65
|
+
try {
|
|
66
|
+
await log.warn('Ignoring non-sortable field', {
|
|
67
|
+
...getOperationDiagnosticContext(context, { phase: 'querySort', scopeName, backend: knex.client.config.client }), field
|
|
68
|
+
})
|
|
69
|
+
} catch { /* Diagnostics must preserve the skipped sort. */ }
|
|
70
|
+
continue
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const queryFieldRuntime = queryFieldRuntimeByField.get(field)
|
|
74
|
+
if (queryFieldRuntime) {
|
|
75
|
+
applySortDescriptorOrder(query, { queryFieldRuntime, direction }, { before })
|
|
76
|
+
descriptors.push({
|
|
77
|
+
field,
|
|
78
|
+
direction: sqlDirection,
|
|
79
|
+
queryFieldRuntime,
|
|
80
|
+
definition: queryFieldRuntime.definition,
|
|
81
|
+
isRelationship: false
|
|
82
|
+
})
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const dbField = resolveSortField(field, schemaInfo)
|
|
87
|
+
const searchField = schemaInfo.searchSchemaStructure?.[field]
|
|
88
|
+
|
|
89
|
+
const storageColumn = storageAdapter.translateColumn(dbField)
|
|
90
|
+
const reference = referenceColumns.get(field)
|
|
91
|
+
const translatedField = reference?.column || (storageColumn.includes('.')
|
|
92
|
+
? storageColumn
|
|
93
|
+
: `${storageAdapter.getTableName()}.${storageColumn}`)
|
|
94
|
+
|
|
95
|
+
applySortDescriptorOrder(query, { column: translatedField, direction }, { before })
|
|
96
|
+
descriptors.push({
|
|
97
|
+
field,
|
|
98
|
+
direction: sqlDirection,
|
|
99
|
+
column: translatedField,
|
|
100
|
+
actualField: dbField,
|
|
101
|
+
resultColumn: field === 'id' ? 'id' : storageColumn,
|
|
102
|
+
...(reference || {}),
|
|
103
|
+
definition: schemaInfo.schemaStructure?.[dbField] || { type: field === 'id' ? 'id' : undefined },
|
|
104
|
+
isRelationship: Boolean(schemaInfo.schemaStructure?.[dbField]?.belongsTo || (dbField !== field && searchField?.isRelationship))
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (descriptors.length === 0) {
|
|
109
|
+
query.orderBy('id', 'asc')
|
|
110
|
+
descriptors.push({
|
|
111
|
+
field: 'id',
|
|
112
|
+
direction: 'ASC',
|
|
113
|
+
column: 'id',
|
|
114
|
+
definition: { type: 'id' },
|
|
115
|
+
isRelationship: false
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return descriptors
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** @type {DataRelatedIdsQuery} */
|
|
123
|
+
const dataRelatedIdsQuery = async ({ context, relDef }) => {
|
|
124
|
+
const pivotScope = scopes[relDef.through]
|
|
125
|
+
const pivotAdapter = getScopeStorageAdapter(relDef.through)
|
|
126
|
+
if (!pivotScope || !relDef.foreignKey || !relDef.otherKey) {
|
|
127
|
+
throw new RestApiResourceError('Invalid many-to-many pivot definition', { subtype: 'pivot_table_not_found' })
|
|
128
|
+
}
|
|
129
|
+
const tableName = pivotAdapter.getTableName()
|
|
130
|
+
const pivotContext = {
|
|
131
|
+
...context,
|
|
132
|
+
method: 'query',
|
|
133
|
+
scopeName: relDef.through,
|
|
134
|
+
schemaInfo: pivotScope.vars.schemaInfo,
|
|
135
|
+
storageAdapter: pivotAdapter,
|
|
136
|
+
queryParams: {}
|
|
137
|
+
}
|
|
138
|
+
await pivotScope.checkPermissions({ method: 'query', originalContext: pivotContext })
|
|
139
|
+
const query = pivotAdapter.buildBaseQuery({ transaction: context.transaction })
|
|
140
|
+
.where({ [`${tableName}.${pivotAdapter.translateColumn(relDef.foreignKey)}`]: pivotAdapter.translateFilterValue(relDef.foreignKey, context.id) })
|
|
141
|
+
const state = await pivotScope.applyQueryFilters({
|
|
142
|
+
query,
|
|
143
|
+
filters: undefined,
|
|
144
|
+
scopeName: relDef.through,
|
|
145
|
+
tableName,
|
|
146
|
+
db: context.db,
|
|
147
|
+
schemaInfo: pivotScope.vars.schemaInfo,
|
|
148
|
+
storageAdapter: pivotAdapter,
|
|
149
|
+
queryPurpose: 'collection'
|
|
150
|
+
}, pivotContext)
|
|
151
|
+
return {
|
|
152
|
+
query: unwrapQueryBuilderState(state, query).clearSelect().select(`${tableName}.${pivotAdapter.translateColumn(relDef.otherKey)}`)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** @type {OrdinaryDataWriteHelpers['dataExists']} */
|
|
157
|
+
const dataExists = async ({ scopeName, context }) => {
|
|
158
|
+
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
159
|
+
context.storageAdapter = storageAdapter
|
|
160
|
+
|
|
161
|
+
const id = context.id
|
|
162
|
+
|
|
163
|
+
const tableName = storageAdapter.getTableName()
|
|
164
|
+
const idProperty = storageAdapter.getIdColumn()
|
|
165
|
+
const db = context.db || api.knex.instance
|
|
166
|
+
|
|
167
|
+
log.debug(`[Knex] EXISTS ${tableName}/${id}`)
|
|
168
|
+
|
|
169
|
+
const selectClause = idProperty !== 'id' ? `${idProperty} as id` : 'id'
|
|
170
|
+
|
|
171
|
+
const record = await db(tableName)
|
|
172
|
+
.where(idProperty, id)
|
|
173
|
+
.select(selectClause)
|
|
174
|
+
.first()
|
|
175
|
+
|
|
176
|
+
return !!record
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** @type {OrdinaryDataReadHelpers['dataGet']} */
|
|
180
|
+
const dataGet = async ({ scopeName, context }) => {
|
|
181
|
+
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
182
|
+
context.storageAdapter = storageAdapter
|
|
183
|
+
const scope = getScope(scopeName)
|
|
184
|
+
const id = context.id
|
|
185
|
+
const tableName = storageAdapter.getTableName()
|
|
186
|
+
const idProperty = storageAdapter.getIdColumn()
|
|
187
|
+
const db = context.db || api.knex.instance
|
|
188
|
+
|
|
189
|
+
log.debug(`[Knex] GET ${tableName}/${id}`)
|
|
190
|
+
|
|
191
|
+
const fieldSelectionInfo = await buildFieldSelection(scope, { context })
|
|
192
|
+
// Enrichment removes fetched dependencies that the caller did not request.
|
|
193
|
+
context.computedDependencies = fieldSelectionInfo.computedDependencies
|
|
194
|
+
|
|
195
|
+
// The preceding minimal read has already applied permission and row filters.
|
|
196
|
+
let query = db(tableName).where(idProperty, id)
|
|
197
|
+
|
|
198
|
+
const selectionState = await applyFieldSelectionToQuery({
|
|
199
|
+
query,
|
|
200
|
+
scope,
|
|
201
|
+
fieldSelectionInfo,
|
|
202
|
+
tableName,
|
|
203
|
+
useTablePrefix: false,
|
|
204
|
+
storageAdapter,
|
|
205
|
+
db,
|
|
206
|
+
context,
|
|
207
|
+
scopeName
|
|
208
|
+
})
|
|
209
|
+
query = selectionState.query
|
|
210
|
+
|
|
211
|
+
const record = await applyDatabaseReadOptions(query).first()
|
|
212
|
+
|
|
213
|
+
if (!record) {
|
|
214
|
+
throw new RestApiResourceError(
|
|
215
|
+
'Resource not found',
|
|
216
|
+
{
|
|
217
|
+
subtype: ERROR_SUBTYPES.NOT_FOUND,
|
|
218
|
+
resourceType: scopeName,
|
|
219
|
+
resourceId: id
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const records = [record]
|
|
225
|
+
await loadRelationshipIdentifiers(records, scopeName, scopes, db, context)
|
|
226
|
+
|
|
227
|
+
const included = await processIncludes(scope, records, {
|
|
228
|
+
log,
|
|
229
|
+
scopes,
|
|
230
|
+
knex,
|
|
231
|
+
context,
|
|
232
|
+
api
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
return buildJsonApiResponse(scope, records, included, true, scopeName, context)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Supplying ids returns minimal records for the caller's bounded validation batch.
|
|
239
|
+
/** @overload @param {DataMinimalRequest & { ids: Array<string | number> }} request @returns {Promise<DataResource[]>} */
|
|
240
|
+
/** @overload @param {DataMinimalRequest & { ids?: undefined, context: DataReadContext & {id: string | number} }} request @returns {Promise<DataResource | null>} */
|
|
241
|
+
/** @param {DataMinimalRequest & { ids?: Array<string | number> }} request */
|
|
242
|
+
async function dataGetMinimal ({
|
|
243
|
+
scopeName,
|
|
244
|
+
context,
|
|
245
|
+
ids,
|
|
246
|
+
runHooks,
|
|
247
|
+
applyQueryFilters,
|
|
248
|
+
filters = context.queryParams?.filters,
|
|
249
|
+
queryPurpose = 'single'
|
|
250
|
+
}) {
|
|
251
|
+
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
252
|
+
context.storageAdapter = storageAdapter
|
|
253
|
+
const scope = getScope(scopeName)
|
|
254
|
+
const id = context.id
|
|
255
|
+
const tableName = storageAdapter.getTableName()
|
|
256
|
+
const idProperty = storageAdapter.getIdColumn()
|
|
257
|
+
const db = context.db || api.knex.instance
|
|
258
|
+
|
|
259
|
+
log.debug(`[Knex] GET_MINIMAL ${tableName}/${id}`)
|
|
260
|
+
|
|
261
|
+
// Knex types exclude unknown bindings; the configured driver still validates serializer results.
|
|
262
|
+
let query = ids === undefined
|
|
263
|
+
? db(tableName).where(idProperty, id)
|
|
264
|
+
: db(tableName).whereIn(idProperty, /** @type {import('knex').Knex.Value[]} */ (ids.map(value => storageAdapter.translateFilterValue('id', value))))
|
|
265
|
+
.distinct(`${tableName}.${idProperty}`)
|
|
266
|
+
|
|
267
|
+
if (typeof applyQueryFilters === 'function') {
|
|
268
|
+
const scopedQueryState = await applyQueryFilters({
|
|
269
|
+
query,
|
|
270
|
+
filters,
|
|
271
|
+
tableName,
|
|
272
|
+
db,
|
|
273
|
+
scopeName,
|
|
274
|
+
queryPurpose,
|
|
275
|
+
storageAdapter
|
|
276
|
+
})
|
|
277
|
+
query = unwrapQueryBuilderState(scopedQueryState, query)
|
|
278
|
+
} else if (runHooks) {
|
|
279
|
+
const filteredState = await withQueryFilteringContext(context, {
|
|
280
|
+
query,
|
|
281
|
+
filters,
|
|
282
|
+
schemaInfo: context.schemaInfo,
|
|
283
|
+
scopeName,
|
|
284
|
+
tableName,
|
|
285
|
+
db,
|
|
286
|
+
queryPurpose,
|
|
287
|
+
adapter: storageAdapter,
|
|
288
|
+
storageAdapter,
|
|
289
|
+
}, async () => {
|
|
290
|
+
await runHooks('knexQueryFiltering')
|
|
291
|
+
})
|
|
292
|
+
query = filteredState.query
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const identityFields = new Set([...context.schemaInfo.foreignKeyFields, 'id'])
|
|
296
|
+
const identities = Object.fromEntries([...identityFields].map(field => {
|
|
297
|
+
const column = storageAdapter.translateColumn(field)
|
|
298
|
+
return [field === 'id' ? 'id' : column, databaseIdentityExpression(db, `${tableName}.${column}`)]
|
|
299
|
+
}))
|
|
300
|
+
if (ids !== undefined) {
|
|
301
|
+
// Filtering owns predicates; the validation batch owns its identity projection.
|
|
302
|
+
query.clearSelect().distinct(`${tableName}.${idProperty}`)
|
|
303
|
+
// Deduplicate policy joins by ID, without comparing JSON or other row values.
|
|
304
|
+
/** @type {StorageRow[]} */
|
|
305
|
+
const records = await applyDatabaseReadOptions(storageAdapter.buildBaseQuery({ transaction: db }).whereIn(idProperty, query).select(`${tableName}.*`, identities))
|
|
306
|
+
return records.map(record => toJsonApiRecordWithBelongsTo(scope, record, scopeName))
|
|
307
|
+
}
|
|
308
|
+
const record = await applyDatabaseReadOptions(query.select(`${tableName}.*`, identities)).first()
|
|
309
|
+
|
|
310
|
+
if (!record) {
|
|
311
|
+
return null
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return toJsonApiRecordWithBelongsTo(scope, record, scopeName)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** @type {OrdinaryDataReadHelpers['dataQuery']} */
|
|
318
|
+
const dataQuery = async ({ scopeName, context, runHooks }) => {
|
|
319
|
+
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
320
|
+
context.storageAdapter = storageAdapter
|
|
321
|
+
const scope = getScope(scopeName)
|
|
322
|
+
const tableName = storageAdapter.getTableName()
|
|
323
|
+
const schemaInfo = context.schemaInfo
|
|
324
|
+
const queryParams = context.queryParams
|
|
325
|
+
const db = context.db || api.knex.instance
|
|
326
|
+
const sortableFields = context.sortableFields
|
|
327
|
+
|
|
328
|
+
log.trace('[DATA-QUERY] Starting dataQuery', { scopeName })
|
|
329
|
+
log.debug(`[Knex] QUERY ${tableName}`)
|
|
330
|
+
|
|
331
|
+
const fieldSelectionInfo = await buildFieldSelection(scope, { context })
|
|
332
|
+
context.computedDependencies = fieldSelectionInfo.computedDependencies
|
|
333
|
+
|
|
334
|
+
let query = db(tableName)
|
|
335
|
+
applyQueryConstraint({ query, context, scopeName, storageAdapter, tableName })
|
|
336
|
+
|
|
337
|
+
const selectionState = await applyFieldSelectionToQuery({
|
|
338
|
+
query,
|
|
339
|
+
scope,
|
|
340
|
+
fieldSelectionInfo,
|
|
341
|
+
tableName,
|
|
342
|
+
useTablePrefix: true,
|
|
343
|
+
storageAdapter,
|
|
344
|
+
db,
|
|
345
|
+
context,
|
|
346
|
+
scopeName
|
|
347
|
+
})
|
|
348
|
+
query = selectionState.query
|
|
349
|
+
const queryFieldRuntimeByField = selectionState.queryFieldRuntimeByField
|
|
350
|
+
|
|
351
|
+
log.trace('[DATA-QUERY] Calling knexQueryFiltering hook', { hasQuery: !!query, hasFilters: !!queryParams.filters, scopeName, tableName })
|
|
352
|
+
|
|
353
|
+
const filteredState = await withQueryFilteringContext(context, {
|
|
354
|
+
query,
|
|
355
|
+
filters: queryParams.filters,
|
|
356
|
+
schemaInfo,
|
|
357
|
+
scopeName,
|
|
358
|
+
tableName,
|
|
359
|
+
db,
|
|
360
|
+
queryPurpose: 'collection',
|
|
361
|
+
adapter: storageAdapter,
|
|
362
|
+
storageAdapter,
|
|
363
|
+
}, async () => {
|
|
364
|
+
await runHooks('knexQueryFiltering')
|
|
365
|
+
})
|
|
366
|
+
query = filteredState.query
|
|
367
|
+
|
|
368
|
+
log.trace('[DATA-QUERY] Finished knexQueryFiltering hook')
|
|
369
|
+
|
|
370
|
+
const sortDescriptors = await buildSortDescriptors({
|
|
371
|
+
query,
|
|
372
|
+
sort: queryParams.sort,
|
|
373
|
+
schemaInfo,
|
|
374
|
+
sortableFields,
|
|
375
|
+
defaultSort: scope.vars.defaultSort,
|
|
376
|
+
scopeName,
|
|
377
|
+
context,
|
|
378
|
+
before: Boolean(queryParams.page?.before),
|
|
379
|
+
storageAdapter,
|
|
380
|
+
queryFieldRuntimeByField
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
const paginationInfo = applyPaginationToQuery({
|
|
384
|
+
query,
|
|
385
|
+
page: queryParams.page,
|
|
386
|
+
vars: scope.vars,
|
|
387
|
+
sortDescriptors,
|
|
388
|
+
storageAdapter
|
|
389
|
+
})
|
|
390
|
+
const { pageSize } = paginationInfo
|
|
391
|
+
|
|
392
|
+
/** @type {StorageRow[]} */
|
|
393
|
+
const records = await applyDatabaseReadOptions(query)
|
|
394
|
+
|
|
395
|
+
context.returnMeta = context.returnMeta || {}
|
|
396
|
+
const queryString = serializeJsonApiQuery(queryParams)
|
|
397
|
+
context.returnMeta.queryString = queryString ? `?${queryString}` : ''
|
|
398
|
+
|
|
399
|
+
// Execute count query for pagination if offset-based pagination is used
|
|
400
|
+
if (paginationInfo.mode === 'offset') {
|
|
401
|
+
const { page } = paginationInfo
|
|
402
|
+
|
|
403
|
+
// Only execute count query if enabled
|
|
404
|
+
if (scope.vars.enablePaginationCounts) {
|
|
405
|
+
// Build count query with same filters as main query
|
|
406
|
+
let countQuery = db(tableName)
|
|
407
|
+
applyQueryConstraint({ query: countQuery, context, scopeName, storageAdapter, tableName })
|
|
408
|
+
|
|
409
|
+
// Mandatory server-side filters must also run when the client supplied no filters.
|
|
410
|
+
// Otherwise pagination metadata can disclose or count rows excluded from the page.
|
|
411
|
+
const filteredState = await withQueryFilteringContext(context, {
|
|
412
|
+
query: countQuery,
|
|
413
|
+
filters: queryParams.filters,
|
|
414
|
+
scopeName,
|
|
415
|
+
tableName,
|
|
416
|
+
schemaInfo,
|
|
417
|
+
db,
|
|
418
|
+
queryPurpose: 'count',
|
|
419
|
+
adapter: storageAdapter,
|
|
420
|
+
storageAdapter
|
|
421
|
+
}, async () => {
|
|
422
|
+
await runHooks('knexQueryFiltering')
|
|
423
|
+
})
|
|
424
|
+
countQuery = filteredState.query
|
|
425
|
+
|
|
426
|
+
// Get total count
|
|
427
|
+
const countResult = await countQuery.clearSelect().clearOrder()
|
|
428
|
+
.countDistinct({ total: `${tableName}.${storageAdapter.getIdColumn()}` }).first()
|
|
429
|
+
const total = Number(countResult?.total ?? 0)
|
|
430
|
+
|
|
431
|
+
// Calculate pagination metadata with total
|
|
432
|
+
context.returnMeta.paginationMeta = calculatePaginationMeta(total, page, pageSize)
|
|
433
|
+
} else {
|
|
434
|
+
// Without count, we can still provide basic pagination info
|
|
435
|
+
context.returnMeta.paginationMeta = {
|
|
436
|
+
page,
|
|
437
|
+
pageSize
|
|
438
|
+
// No total, pageCount, or hasMore when counts are disabled
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Generate links
|
|
443
|
+
const urlPrefix = getUrlPrefix(context, scope)
|
|
444
|
+
context.returnMeta.paginationLinks = generatePaginationLinks(
|
|
445
|
+
urlPrefix,
|
|
446
|
+
scopeName,
|
|
447
|
+
queryParams,
|
|
448
|
+
context.returnMeta.paginationMeta
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const referenceSortDescriptors = sortDescriptors.filter(descriptor => descriptor.referenceField)
|
|
453
|
+
if (paginationInfo.mode === 'cursor') {
|
|
454
|
+
// Pagination fetches one extra row to detect a following page.
|
|
455
|
+
const hasMore = records.length > pageSize
|
|
456
|
+
|
|
457
|
+
if (hasMore) {
|
|
458
|
+
records.pop()
|
|
459
|
+
}
|
|
460
|
+
if (paginationInfo.before) records.reverse()
|
|
461
|
+
|
|
462
|
+
const sortFields = sortDescriptors.map((descriptor) => descriptor.field)
|
|
463
|
+
const cursorRecords = records.map(record => {
|
|
464
|
+
const cursorRecord = { ...record }
|
|
465
|
+
for (const { field, resultColumn } of sortDescriptors) {
|
|
466
|
+
if (resultColumn) cursorRecord[field] = record[resultColumn]
|
|
467
|
+
}
|
|
468
|
+
return cursorRecord
|
|
469
|
+
})
|
|
470
|
+
const cursorOptions = {
|
|
471
|
+
schemaInfo,
|
|
472
|
+
definitions: Object.fromEntries(sortDescriptors.map(({ field, definition }) => [field, definition])),
|
|
473
|
+
before: paginationInfo.before
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
context.returnMeta.paginationMeta = buildCursorMeta(cursorRecords, pageSize, hasMore, sortFields, cursorOptions)
|
|
477
|
+
const urlPrefix = getUrlPrefix(context, scope)
|
|
478
|
+
context.returnMeta.paginationLinks = generateCursorPaginationLinks(
|
|
479
|
+
urlPrefix,
|
|
480
|
+
scopeName,
|
|
481
|
+
queryParams,
|
|
482
|
+
cursorRecords,
|
|
483
|
+
pageSize,
|
|
484
|
+
hasMore,
|
|
485
|
+
sortFields,
|
|
486
|
+
cursorOptions
|
|
487
|
+
)
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
for (const record of records) {
|
|
491
|
+
for (const { resultColumn } of referenceSortDescriptors) {
|
|
492
|
+
if (resultColumn) delete record[resultColumn]
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
await loadRelationshipIdentifiers(records, scopeName, scopes, db, context)
|
|
496
|
+
|
|
497
|
+
const included = await processIncludes(scope, records, {
|
|
498
|
+
log,
|
|
499
|
+
scopes,
|
|
500
|
+
knex,
|
|
501
|
+
context,
|
|
502
|
+
api
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
return buildJsonApiResponse(scope, records, included, false, scopeName, context)
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/** @type {OrdinaryDataWriteHelpers['dataPost']} */
|
|
509
|
+
const dataPost = async ({ scopeName, context }) => {
|
|
510
|
+
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
511
|
+
context.storageAdapter = storageAdapter
|
|
512
|
+
const tableName = storageAdapter.getTableName()
|
|
513
|
+
const idProperty = storageAdapter.getIdColumn()
|
|
514
|
+
const db = context.db || api.knex.instance
|
|
515
|
+
const inputRecord = context.inputRecord
|
|
516
|
+
|
|
517
|
+
log.debug(`[Knex] POST ${tableName}`)
|
|
518
|
+
|
|
519
|
+
// Extract attributes from JSON:API format.
|
|
520
|
+
// POST may legitimately provide a resource id when the table uses a custom primary key
|
|
521
|
+
// (for example, one-to-one rows keyed by user_id/workspace_id). Keep that id in the
|
|
522
|
+
// attribute bag so storage translation can write it to the configured id column.
|
|
523
|
+
const attributes = {
|
|
524
|
+
...(inputRecord.data.attributes || {}),
|
|
525
|
+
...(inputRecord.data.id !== undefined ? { id: inputRecord.data.id } : {})
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Strip non-database fields (computed and virtual) before insert
|
|
529
|
+
/** @type {StorageRow} */
|
|
530
|
+
const dbAttributes = storageAdapter.toStorageRow(attributes, {
|
|
531
|
+
context,
|
|
532
|
+
operation: 'post'
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
const explicitId = dbAttributes[idProperty] ?? inputRecord.data.id
|
|
536
|
+
|
|
537
|
+
// Insert and get the new ID
|
|
538
|
+
const result = await applyDatabaseReadOptions(applyInsertReturning(db(tableName).insert(dbAttributes), { [idProperty]: databaseIdentityExpression(db, idProperty) }))
|
|
539
|
+
|
|
540
|
+
// Extract the ID value. Some dialects ignore .returning() for inserts and can
|
|
541
|
+
// yield 0 even when the caller explicitly supplied the primary key.
|
|
542
|
+
const returnedId = Array.isArray(result)
|
|
543
|
+
? (result[0]?.[idProperty] ?? result[0])
|
|
544
|
+
: (result?.[idProperty] ?? result)
|
|
545
|
+
|
|
546
|
+
if (
|
|
547
|
+
explicitId !== undefined &&
|
|
548
|
+
explicitId !== null &&
|
|
549
|
+
(returnedId === undefined || returnedId === null || returnedId === 0 || returnedId === '0')
|
|
550
|
+
) {
|
|
551
|
+
return explicitId
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return returnedId
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** @type {OrdinaryDataWriteHelpers['dataPut']} */
|
|
558
|
+
const dataPut = async ({ scopeName, context }) => {
|
|
559
|
+
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
560
|
+
context.storageAdapter = storageAdapter
|
|
561
|
+
const id = context.id
|
|
562
|
+
const tableName = storageAdapter.getTableName()
|
|
563
|
+
const idProperty = storageAdapter.getIdColumn()
|
|
564
|
+
const db = context.db || api.knex.instance
|
|
565
|
+
const inputRecord = context.inputRecord
|
|
566
|
+
const isCreate = context.isCreate
|
|
567
|
+
|
|
568
|
+
log.debug(`[Knex] PUT ${tableName}/${id} (isCreate: ${context.isCreate})`)
|
|
569
|
+
|
|
570
|
+
// Core has already merged relationship values and applied setters.
|
|
571
|
+
const attributes = inputRecord.data.attributes || {}
|
|
572
|
+
|
|
573
|
+
// Strip non-database fields (computed and virtual) before database operation
|
|
574
|
+
const finalAttributes = storageAdapter.toStorageRow(attributes, {
|
|
575
|
+
context,
|
|
576
|
+
operation: 'put'
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
if (isCreate) {
|
|
580
|
+
await db(tableName).insert({
|
|
581
|
+
...finalAttributes,
|
|
582
|
+
[idProperty]: id
|
|
583
|
+
})
|
|
584
|
+
return
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const exists = await db(tableName)
|
|
588
|
+
.where(idProperty, id)
|
|
589
|
+
.first()
|
|
590
|
+
|
|
591
|
+
if (!exists) {
|
|
592
|
+
throw new RestApiResourceError(
|
|
593
|
+
'Resource not found',
|
|
594
|
+
{
|
|
595
|
+
subtype: ERROR_SUBTYPES.NOT_FOUND,
|
|
596
|
+
resourceType: scopeName,
|
|
597
|
+
resourceId: id
|
|
598
|
+
}
|
|
599
|
+
)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Updates cannot replace the primary key.
|
|
603
|
+
delete finalAttributes[idProperty]
|
|
604
|
+
if (Object.keys(finalAttributes).length > 0) {
|
|
605
|
+
await db(tableName)
|
|
606
|
+
.where(idProperty, id)
|
|
607
|
+
.update(finalAttributes)
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/** @type {OrdinaryDataWriteHelpers['dataPatch']} */
|
|
612
|
+
const dataPatch = async ({ scopeName, context }) => {
|
|
613
|
+
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
614
|
+
context.storageAdapter = storageAdapter
|
|
615
|
+
const id = context.id
|
|
616
|
+
const tableName = storageAdapter.getTableName()
|
|
617
|
+
const idProperty = storageAdapter.getIdColumn()
|
|
618
|
+
const db = context.db || api.knex.instance
|
|
619
|
+
const inputRecord = context.inputRecord
|
|
620
|
+
|
|
621
|
+
log.debug(`[Knex] PATCH ${tableName}/${id}`)
|
|
622
|
+
|
|
623
|
+
// Check if record exists
|
|
624
|
+
const exists = await db(tableName)
|
|
625
|
+
.where(idProperty, id)
|
|
626
|
+
.first()
|
|
627
|
+
|
|
628
|
+
if (!exists) {
|
|
629
|
+
throw new RestApiResourceError(
|
|
630
|
+
'Resource not found',
|
|
631
|
+
{
|
|
632
|
+
subtype: ERROR_SUBTYPES.NOT_FOUND,
|
|
633
|
+
resourceType: scopeName,
|
|
634
|
+
resourceId: id
|
|
635
|
+
}
|
|
636
|
+
)
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Core has already merged relationship values and applied setters.
|
|
640
|
+
const attributes = inputRecord.data.attributes || {}
|
|
641
|
+
|
|
642
|
+
// Strip non-database fields (computed and virtual) before database operation
|
|
643
|
+
const finalAttributes = storageAdapter.toStorageRow(attributes, {
|
|
644
|
+
context,
|
|
645
|
+
operation: 'patch'
|
|
646
|
+
})
|
|
647
|
+
|
|
648
|
+
log.debug(`[Knex] PATCH attributes prepared for ${tableName}`)
|
|
649
|
+
|
|
650
|
+
// Remove the idProperty from attributes to prevent updating the primary key
|
|
651
|
+
delete finalAttributes[idProperty]
|
|
652
|
+
|
|
653
|
+
// Update only if there are changes
|
|
654
|
+
if (Object.keys(finalAttributes).length > 0) {
|
|
655
|
+
await db(tableName)
|
|
656
|
+
.where(idProperty, id)
|
|
657
|
+
.update(finalAttributes)
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/** @type {OrdinaryDataWriteHelpers['dataDelete']} */
|
|
662
|
+
const dataDelete = async ({ scopeName, context }) => {
|
|
663
|
+
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
664
|
+
context.storageAdapter = storageAdapter
|
|
665
|
+
const id = context.id
|
|
666
|
+
|
|
667
|
+
const tableName = storageAdapter.getTableName()
|
|
668
|
+
const idProperty = storageAdapter.getIdColumn()
|
|
669
|
+
const db = context.db || api.knex.instance
|
|
670
|
+
|
|
671
|
+
log.debug(`[Knex] DELETE ${tableName}/${id}`)
|
|
672
|
+
|
|
673
|
+
// Check if record exists
|
|
674
|
+
const exists = await db(tableName)
|
|
675
|
+
.where(idProperty, id)
|
|
676
|
+
.first()
|
|
677
|
+
|
|
678
|
+
if (!exists) {
|
|
679
|
+
throw new RestApiResourceError(
|
|
680
|
+
'Resource not found',
|
|
681
|
+
{
|
|
682
|
+
subtype: ERROR_SUBTYPES.NOT_FOUND,
|
|
683
|
+
resourceType: scopeName,
|
|
684
|
+
resourceId: id
|
|
685
|
+
}
|
|
686
|
+
)
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Delete the record
|
|
690
|
+
await invalidateDeletedPivotTargets(api, scopeName, context)
|
|
691
|
+
await db(tableName)
|
|
692
|
+
.where(idProperty, id)
|
|
693
|
+
.delete()
|
|
694
|
+
await deletePivotReferences(api, scopeName, context)
|
|
695
|
+
|
|
696
|
+
return { success: true }
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
return {
|
|
700
|
+
dataRelatedIdsQuery,
|
|
701
|
+
dataExists,
|
|
702
|
+
dataGet,
|
|
703
|
+
dataQuery,
|
|
704
|
+
dataPost,
|
|
705
|
+
dataPut,
|
|
706
|
+
dataPatch,
|
|
707
|
+
dataDelete,
|
|
708
|
+
dataGetMinimal
|
|
709
|
+
}
|
|
710
|
+
}
|