json-rest-api 1.0.29 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +744 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +733 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +103 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
- package/docs/GUIDE/33-migrating-to-v2.md +3838 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +37 -0
- package/index.js +3 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +70 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -177
- package/plugins/core/lib/anyapi/anyapi-registry.js +586 -418
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +120 -56
- package/plugins/core/lib/anyapi/stored-definition.js +109 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +13 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +128 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1077
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -395
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +36 -68
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +37 -81
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/anyapi.d.ts +30 -0
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +65 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -148
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -155
- package/tests/fixtures/api-configs.js +0 -2566
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-boundaries.test.js +0 -154
- package/tests/pagination-contract.test.js +0 -122
- package/tests/pagination-cursor-multifield.test.js +0 -641
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/temporal-boundaries.test.js +0 -208
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -1,213 +1,88 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @import { FieldSelection, SelectedQuery, SelectionContext, SelectionQueryRequest, SelectionScope } from './field-selection-types.js' */
|
|
3
|
+
/** @import { StorageFieldDefinition } from '../storage/storage-types.js' */
|
|
1
4
|
import { buildQuerySelection } from '../querying/knex-query-helpers-base.js'
|
|
2
5
|
import { createSelectTranslator } from '../storage/storage-adapter.js'
|
|
3
|
-
import { buildQueryFieldRuntimes } from './query-field-helpers.js'
|
|
6
|
+
import { buildQueryFieldRuntimes, normalizeStructuredSelect } from './query-field-helpers.js'
|
|
4
7
|
import { RestApiFieldsetError } from '../../../../lib/rest-api-errors.js'
|
|
5
|
-
|
|
8
|
+
import { getResourceFieldset, parseFieldset } from './field-utils.js'
|
|
9
|
+
import { databaseIdentityExpression } from './database-value-normalizers.js'
|
|
10
|
+
import { findRelationshipDefinition } from './relationship-contracts.js'
|
|
11
|
+
import { wrapUnexpectedError } from '../../../../lib/error-context.js'
|
|
12
|
+
import { getFieldDependencyClosure } from './schema-helpers.js'
|
|
13
|
+
import { resolveSortField } from '../querying/query-field-sort-helpers.js'
|
|
14
|
+
|
|
15
|
+
/** @param {SelectionContext} context @param {Record<string, SelectionScope>} scopes */
|
|
6
16
|
export const validateRequestedFieldsets = async (context, scopes) => {
|
|
7
17
|
for (const scopeName of Object.keys(context.queryParams?.fields || {})) {
|
|
8
|
-
if (!scopes[scopeName]?.vars?.schemaInfo)
|
|
18
|
+
if (!Object.hasOwn(scopes, scopeName) || !scopes[scopeName]?.vars?.schemaInfo) {
|
|
19
|
+
throw new RestApiFieldsetError({ resourceType: scopeName })
|
|
20
|
+
}
|
|
9
21
|
await buildFieldSelection(scopes[scopeName], {
|
|
10
|
-
context: { scopeName, queryParams: { fields: context.queryParams
|
|
22
|
+
context: { scopeName, queryParams: { fields: context.queryParams?.fields } }
|
|
11
23
|
})
|
|
12
24
|
}
|
|
13
25
|
}
|
|
14
26
|
|
|
15
27
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* @
|
|
23
|
-
*
|
|
24
|
-
* const schemaInfo = {
|
|
25
|
-
* computed: { profit_margin: { (definition) } },
|
|
26
|
-
* schemaStructure: { price: { type: 'number' } }
|
|
27
|
-
* };
|
|
28
|
-
* isNonDatabaseField('profit_margin', schemaInfo);
|
|
29
|
-
* // Output: true (it's computed, not stored)
|
|
30
|
-
*
|
|
31
|
-
* @example
|
|
32
|
-
* // Input: Check virtual field
|
|
33
|
-
* const schemaInfo = {
|
|
34
|
-
* computed: {},
|
|
35
|
-
* schemaStructure: {
|
|
36
|
-
* temp_password: { type: 'string', virtual: true }
|
|
37
|
-
* }
|
|
38
|
-
* };
|
|
39
|
-
* isNonDatabaseField('temp_password', schemaInfo);
|
|
40
|
-
* // Output: true (it's virtual, not stored)
|
|
41
|
-
*
|
|
42
|
-
* @description
|
|
43
|
-
* Used by:
|
|
44
|
-
* - buildFieldSelection to exclude from SELECT queries
|
|
45
|
-
* - dataPost/dataPatch to skip non-database fields
|
|
46
|
-
* - enrichAttributes to identify computed fields
|
|
47
|
-
*
|
|
48
|
-
* Purpose:
|
|
49
|
-
* - Prevents SQL errors by excluding non-existent columns
|
|
50
|
-
* - Identifies fields that need computation or special handling
|
|
51
|
-
* - Supports virtual fields for temporary/input-only data
|
|
52
|
-
*/
|
|
53
|
-
export const isNonDatabaseField = (fieldName, schemaInfo) => {
|
|
54
|
-
const { computed = {}, schemaStructure = {} } = schemaInfo
|
|
55
|
-
if (fieldName in computed) return true
|
|
56
|
-
const fieldDef = schemaStructure[fieldName]
|
|
57
|
-
return fieldDef && fieldDef.virtual === true
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Builds the field selection list for database queries with sparse fieldset support
|
|
62
|
-
*
|
|
63
|
-
* @param {Object} scope - Resource scope with schema information
|
|
64
|
-
* @param {Object} deps - Dependencies object
|
|
65
|
-
* @param {Object} deps.context - Request context with queryParams and schemaInfo
|
|
66
|
-
* @returns {Promise<Object>} Field selection information
|
|
67
|
-
*
|
|
68
|
-
* @example
|
|
69
|
-
* // Input: Basic sparse fieldset request
|
|
70
|
-
* const deps = {
|
|
71
|
-
* context: {
|
|
72
|
-
* scopeName: 'articles',
|
|
73
|
-
* queryParams: { fields: { articles: 'title,body' } },
|
|
74
|
-
* schemaInfo: { idProperty: 'id', ... }
|
|
75
|
-
* }
|
|
76
|
-
* };
|
|
77
|
-
* const result = await buildFieldSelection(scope, deps);
|
|
78
|
-
*
|
|
79
|
-
* // Output: Includes requested fields plus required fields
|
|
80
|
-
* // {
|
|
81
|
-
* // fieldsToSelect: ['id', 'title', 'body', 'author_id', 'category_id'],
|
|
82
|
-
* // requestedFields: ['title', 'body'],
|
|
83
|
-
* // computedDependencies: [],
|
|
84
|
-
* // idProperty: 'id'
|
|
85
|
-
* // }
|
|
86
|
-
* // Note: Foreign keys (author_id, category_id) always included for relationships
|
|
87
|
-
*
|
|
88
|
-
* @example
|
|
89
|
-
* // Input: Computed field with hidden dependencies
|
|
90
|
-
* const schemaInfo = {
|
|
91
|
-
* computed: {
|
|
92
|
-
* profit_margin: {
|
|
93
|
-
* compute: (record) => (record.price - record.cost) / record.price,
|
|
94
|
-
* dependencies: ['price', 'cost']
|
|
95
|
-
* }
|
|
96
|
-
* },
|
|
97
|
-
* schemaStructure: {
|
|
98
|
-
* name: { type: 'string' },
|
|
99
|
-
* price: { type: 'number' },
|
|
100
|
-
* cost: { type: 'number', normallyHidden: true }
|
|
101
|
-
* }
|
|
102
|
-
* };
|
|
103
|
-
* const deps = {
|
|
104
|
-
* context: {
|
|
105
|
-
* queryParams: { fields: { products: 'name,profit_margin' } }
|
|
106
|
-
* }
|
|
107
|
-
* };
|
|
108
|
-
*
|
|
109
|
-
* // Output: Fetches hidden dependency for computation
|
|
110
|
-
* // {
|
|
111
|
-
* // fieldsToSelect: ['id', 'name', 'price', 'cost'],
|
|
112
|
-
* // requestedFields: ['name', 'profit_margin'],
|
|
113
|
-
* // computedDependencies: ['cost'], // Will be removed after computation
|
|
114
|
-
* // idProperty: 'id'
|
|
115
|
-
* // }
|
|
116
|
-
*
|
|
117
|
-
* @example
|
|
118
|
-
* // Input: No sparse fieldsets (returns all visible fields)
|
|
119
|
-
* const deps = {
|
|
120
|
-
* context: {
|
|
121
|
-
* queryParams: {}, // No fields parameter
|
|
122
|
-
* schemaInfo: { ... }
|
|
123
|
-
* }
|
|
124
|
-
* };
|
|
125
|
-
*
|
|
126
|
-
* // Output: All non-hidden, non-virtual fields
|
|
127
|
-
* // {
|
|
128
|
-
* // fieldsToSelect: ['id', 'title', 'body', 'published_at', 'author_id'],
|
|
129
|
-
* // requestedFields: null,
|
|
130
|
-
* // computedDependencies: [],
|
|
131
|
-
* // idProperty: 'id'
|
|
132
|
-
* // }
|
|
133
|
-
*
|
|
134
|
-
* @description
|
|
135
|
-
* Used by:
|
|
136
|
-
* - dataGet to build SELECT query
|
|
137
|
-
* - dataQuery to build SELECT query
|
|
138
|
-
* - buildQuerySelection as the main field resolver
|
|
139
|
-
*
|
|
140
|
-
* Purpose:
|
|
141
|
-
* - Implements JSON:API sparse fieldsets specification
|
|
142
|
-
* - Ensures relationships work by including foreign keys
|
|
143
|
-
* - Handles computed field dependencies intelligently
|
|
144
|
-
* - Respects field visibility rules (hidden/normallyHidden)
|
|
145
|
-
* - Supports custom ID column names
|
|
146
|
-
*
|
|
147
|
-
* Data flow:
|
|
148
|
-
* 1. Always includes ID (with aliasing if needed)
|
|
149
|
-
* 2. Parses requested fields from query string
|
|
150
|
-
* 3. Validates requested fields exist in schema
|
|
151
|
-
* 4. Adds dependencies for computed fields
|
|
152
|
-
* 5. Includes all foreign keys for relationships
|
|
153
|
-
* 6. Returns complete field list for SQL SELECT
|
|
28
|
+
* Resolve public fieldsets to logical storage/projection names and read dependencies.
|
|
29
|
+
* Always fetch the logical ID and visible relationship backing fields. Fetch sort
|
|
30
|
+
* values needed by cursors and hidden dependencies needed by selected callbacks;
|
|
31
|
+
* response formatting owns removal of values that were only read dependencies.
|
|
32
|
+
* This prepares selection metadata without executing a query or completing a transaction.
|
|
33
|
+
* @param {SelectionScope | null | undefined} scope
|
|
34
|
+
* @param {{ context: SelectionContext }} deps
|
|
35
|
+
* @returns {Promise<FieldSelection>}
|
|
154
36
|
*/
|
|
155
37
|
export const buildFieldSelection = async (scope, deps) => {
|
|
38
|
+
/** @type {Set<string>} */
|
|
156
39
|
const fieldsToSelect = new Set()
|
|
40
|
+
/** @type {Set<string>} */
|
|
157
41
|
const computedDependencies = new Set()
|
|
158
42
|
|
|
159
43
|
// Extract values from scope
|
|
160
44
|
const scopeSchemaInfo = scope?.vars?.schemaInfo || {}
|
|
161
45
|
const {
|
|
162
|
-
|
|
46
|
+
schemaStructure = {},
|
|
163
47
|
computed: computedFields = {},
|
|
164
48
|
idProperty = 'id'
|
|
165
49
|
} = scopeSchemaInfo
|
|
166
|
-
|
|
167
|
-
const queryFields = scope?.vars?.queryFields || {}
|
|
50
|
+
const queryFields = scope?.vars?.schemaInfo?.queryFields || {}
|
|
168
51
|
|
|
169
52
|
// Extract values from deps
|
|
170
53
|
const { context } = deps
|
|
171
54
|
const scopeName = context.scopeName
|
|
172
|
-
const requestedFields = context.queryParams?.fields
|
|
55
|
+
const requestedFields = getResourceFieldset(context.queryParams?.fields, scopeName)
|
|
173
56
|
|
|
174
57
|
// Always include the logical ID field - the storage adapter translates it.
|
|
175
58
|
fieldsToSelect.add('id')
|
|
176
59
|
|
|
177
|
-
// Handle both Schema objects and plain objects
|
|
178
|
-
if (!schemaStructure) {
|
|
179
|
-
schemaStructure = schemaInstance?.structure || schemaInstance || {}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
60
|
// Get computed fields and virtual fields from schema
|
|
183
61
|
const computedFieldNames = new Set(Object.keys(computedFields))
|
|
184
62
|
const queryFieldNames = new Set(Object.keys(queryFields))
|
|
63
|
+
/** @type {Set<string>} */
|
|
185
64
|
const queryFieldsToSelect = new Set()
|
|
186
65
|
|
|
187
66
|
// Find fields marked as virtual in the schema
|
|
188
|
-
|
|
67
|
+
/** @type {Map<string, StorageFieldDefinition>} */
|
|
68
|
+
const virtualFields = new Map()
|
|
189
69
|
Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
|
|
190
70
|
if (fieldDef.virtual === true) {
|
|
191
|
-
|
|
71
|
+
virtualFields.set(fieldName, fieldDef)
|
|
192
72
|
}
|
|
193
73
|
})
|
|
194
74
|
|
|
195
|
-
const nonDatabaseFields = new Set([...computedFieldNames, ...
|
|
75
|
+
const nonDatabaseFields = new Set([...computedFieldNames, ...virtualFields.keys()])
|
|
196
76
|
|
|
197
77
|
// Parse requested fields
|
|
198
|
-
const requested = requestedFields
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
? requestedFields.split(',').map(f => f.trim()).filter(f => f)
|
|
202
|
-
: requestedFields
|
|
203
|
-
)
|
|
204
|
-
: null
|
|
205
|
-
|
|
206
|
-
if (requested && requested.length > 0) {
|
|
78
|
+
const requested = parseFieldset(requestedFields)
|
|
79
|
+
|
|
80
|
+
if (requested !== null) {
|
|
207
81
|
// Sparse fieldsets requested - only select specified fields
|
|
208
82
|
// Example: ?fields[products]=name,price,profit_margin
|
|
209
83
|
requested.forEach(field => {
|
|
210
84
|
if (field === 'id' || field === idProperty) return
|
|
85
|
+
if (findRelationshipDefinition(scopeSchemaInfo, field)) return
|
|
211
86
|
|
|
212
87
|
if (queryFieldNames.has(field)) {
|
|
213
88
|
if (queryFields[field]?.hidden === true) return
|
|
@@ -238,43 +113,6 @@ export const buildFieldSelection = async (scope, deps) => {
|
|
|
238
113
|
|
|
239
114
|
fieldsToSelect.add(field)
|
|
240
115
|
})
|
|
241
|
-
|
|
242
|
-
// Handle computed field dependencies - fetch fields needed for calculations
|
|
243
|
-
// Example: User requests 'profit_margin' which depends on 'price' and 'cost'
|
|
244
|
-
// We need to fetch price and cost from DB even if not explicitly requested
|
|
245
|
-
const requestedComputedFields = requested.filter(f => computedFieldNames.has(f))
|
|
246
|
-
for (const computedField of requestedComputedFields) {
|
|
247
|
-
const fieldDef = computedFields[computedField]
|
|
248
|
-
if (fieldDef.dependencies) {
|
|
249
|
-
for (const dep of fieldDef.dependencies) {
|
|
250
|
-
const depFieldDef = schemaStructure[dep]
|
|
251
|
-
// Only add dependency if it exists and isn't hidden
|
|
252
|
-
if (depFieldDef && depFieldDef.hidden !== true) {
|
|
253
|
-
fieldsToSelect.add(dep)
|
|
254
|
-
// Track dependencies that weren't explicitly requested
|
|
255
|
-
// These will be removed from the final response
|
|
256
|
-
if (!requested.includes(dep)) {
|
|
257
|
-
computedDependencies.add(dep)
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Still handle normallyHidden fields for backward compatibility
|
|
265
|
-
if (requestedComputedFields.length > 0) {
|
|
266
|
-
Object.entries(schemaStructure).forEach(([field, fieldDef]) => {
|
|
267
|
-
if (fieldDef.normallyHidden === true && fieldDef.hidden !== true) {
|
|
268
|
-
// Only add if not already handled by dependencies
|
|
269
|
-
if (!fieldsToSelect.has(field)) {
|
|
270
|
-
fieldsToSelect.add(field)
|
|
271
|
-
if (!requested.includes(field)) {
|
|
272
|
-
computedDependencies.add(field)
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
})
|
|
277
|
-
}
|
|
278
116
|
} else {
|
|
279
117
|
// No sparse fieldsets - return all visible fields
|
|
280
118
|
// This is the default behavior when no ?fields parameter is provided
|
|
@@ -298,26 +136,6 @@ export const buildFieldSelection = async (scope, deps) => {
|
|
|
298
136
|
fieldsToSelect.add(field)
|
|
299
137
|
})
|
|
300
138
|
|
|
301
|
-
// When no sparse fieldsets, we compute all computed fields
|
|
302
|
-
// So we need to include their dependencies even if normallyHidden
|
|
303
|
-
// Example: profit_margin depends on 'cost' which is normallyHidden
|
|
304
|
-
// We fetch 'cost' for calculation but don't return it in response
|
|
305
|
-
for (const [, fieldDef] of Object.entries(computedFields)) {
|
|
306
|
-
if (fieldDef.dependencies) {
|
|
307
|
-
for (const dep of fieldDef.dependencies) {
|
|
308
|
-
const depFieldDef = schemaStructure[dep]
|
|
309
|
-
if (depFieldDef && depFieldDef.hidden !== true) {
|
|
310
|
-
fieldsToSelect.add(dep)
|
|
311
|
-
// Track normallyHidden dependencies for later removal
|
|
312
|
-
// These are fetched for computation but not returned
|
|
313
|
-
if (depFieldDef.normallyHidden === true) {
|
|
314
|
-
computedDependencies.add(dep)
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
139
|
for (const [fieldName, fieldDef] of Object.entries(queryFields)) {
|
|
322
140
|
if (fieldDef.hidden === true || fieldDef.normallyHidden === true) continue
|
|
323
141
|
queryFieldsToSelect.add(fieldName)
|
|
@@ -330,7 +148,7 @@ export const buildFieldSelection = async (scope, deps) => {
|
|
|
330
148
|
|
|
331
149
|
for (const sortField of sortFields) {
|
|
332
150
|
if (typeof sortField !== 'string') continue
|
|
333
|
-
const normalizedField = sortField.startsWith('-') ? sortField.slice(1) : sortField
|
|
151
|
+
const normalizedField = resolveSortField(sortField.startsWith('-') ? sortField.slice(1) : sortField, scopeSchemaInfo)
|
|
334
152
|
|
|
335
153
|
if (queryFieldNames.has(normalizedField)) {
|
|
336
154
|
queryFieldsToSelect.add(normalizedField)
|
|
@@ -351,15 +169,35 @@ export const buildFieldSelection = async (scope, deps) => {
|
|
|
351
169
|
|
|
352
170
|
// Always include polymorphic type and id fields from relationships
|
|
353
171
|
try {
|
|
354
|
-
const relationships =
|
|
172
|
+
const relationships = scopeSchemaInfo.schemaRelationships
|
|
355
173
|
Object.entries(relationships || {}).forEach(([relName, relDef]) => {
|
|
356
174
|
if (relDef.belongsToPolymorphic) {
|
|
357
|
-
|
|
358
|
-
|
|
175
|
+
fieldsToSelect.add(relDef.belongsToPolymorphic.typeField)
|
|
176
|
+
fieldsToSelect.add(relDef.belongsToPolymorphic.idField)
|
|
359
177
|
}
|
|
360
178
|
})
|
|
361
|
-
} catch (
|
|
362
|
-
|
|
179
|
+
} catch (error) {
|
|
180
|
+
throw wrapUnexpectedError(error, {
|
|
181
|
+
message: `Failed to select relationship fields for '${scopeName}'`,
|
|
182
|
+
context: { scopeName, phase: 'relationshipMetadata' }
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const selectedFields = new Set([
|
|
187
|
+
...fieldsToSelect,
|
|
188
|
+
...queryFieldsToSelect,
|
|
189
|
+
...getRequestedComputedFields(scopeName, requestedFields, computedFields)
|
|
190
|
+
])
|
|
191
|
+
for (const [field, definition] of virtualFields) {
|
|
192
|
+
if (definition.hidden !== true && (requested === null ? definition.normallyHidden !== true : requested.includes(field))) {
|
|
193
|
+
selectedFields.add(field)
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (const field of getFieldDependencyClosure(scopeSchemaInfo, selectedFields)) {
|
|
197
|
+
if (field === 'id' || field === idProperty || nonDatabaseFields.has(field)) continue
|
|
198
|
+
if (queryFieldNames.has(field)) queryFieldsToSelect.add(field)
|
|
199
|
+
else if (Object.hasOwn(schemaStructure, field)) fieldsToSelect.add(field)
|
|
200
|
+
if (!selectedFields.has(field)) computedDependencies.add(field)
|
|
363
201
|
}
|
|
364
202
|
|
|
365
203
|
// Return detailed information about field selection
|
|
@@ -376,6 +214,7 @@ export const buildFieldSelection = async (scope, deps) => {
|
|
|
376
214
|
}
|
|
377
215
|
}
|
|
378
216
|
|
|
217
|
+
/** @param {SelectionQueryRequest} options @returns {Promise<SelectedQuery>} */
|
|
379
218
|
export const applyFieldSelectionToQuery = async ({
|
|
380
219
|
query,
|
|
381
220
|
scope,
|
|
@@ -388,18 +227,34 @@ export const applyFieldSelectionToQuery = async ({
|
|
|
388
227
|
scopeName = ''
|
|
389
228
|
}) => {
|
|
390
229
|
const selectTranslator = createSelectTranslator(storageAdapter)
|
|
230
|
+
const schemaInfo = scope?.vars?.schemaInfo || {}
|
|
231
|
+
const foreignKeyFields = schemaInfo.foreignKeyFields
|
|
232
|
+
/** @param {string} field @param {string | null} [alias] */
|
|
233
|
+
const translateColumn = (field, alias) => {
|
|
234
|
+
const translated = selectTranslator?.(field, alias) || (alias ? `${alias}.${field}` : field)
|
|
235
|
+
if (db && !storageAdapter?.isCanonical() && (field === 'id' || foreignKeyFields?.has(field))) {
|
|
236
|
+
const column = storageAdapter?.translateColumn(field) || field
|
|
237
|
+
const outputName = field === 'id' ? 'id' : column
|
|
238
|
+
const qualifiedColumn = alias ? `${alias}.${column}` : column
|
|
239
|
+
return { [outputName]: databaseIdentityExpression(db, qualifiedColumn) }
|
|
240
|
+
}
|
|
241
|
+
const expression = normalizeStructuredSelect(translated, scope?.vars?.schemaInfo?.schemaStructure?.[field], db)
|
|
242
|
+
if (expression === translated) return translated
|
|
243
|
+
const outputName = storageAdapter?.translateColumn(field) || field
|
|
244
|
+
return { [outputName]: expression }
|
|
245
|
+
}
|
|
391
246
|
|
|
392
247
|
const selectedQuery = buildQuerySelection(
|
|
393
248
|
query,
|
|
394
249
|
tableName,
|
|
395
250
|
fieldSelectionInfo?.fieldsToSelect || [],
|
|
396
251
|
useTablePrefix,
|
|
397
|
-
|
|
252
|
+
{ translateColumn }
|
|
398
253
|
)
|
|
399
254
|
|
|
400
255
|
const queryFieldRuntimeByField = await buildQueryFieldRuntimes({
|
|
401
256
|
queryFieldNames: fieldSelectionInfo?.queryFieldsToSelect || [],
|
|
402
|
-
queryFields: scope?.vars?.queryFields || {},
|
|
257
|
+
queryFields: scope?.vars?.schemaInfo?.queryFields || {},
|
|
403
258
|
schemaInfo: scope?.vars?.schemaInfo || {},
|
|
404
259
|
tableName,
|
|
405
260
|
storageAdapter,
|
|
@@ -419,72 +274,27 @@ export const applyFieldSelectionToQuery = async ({
|
|
|
419
274
|
}
|
|
420
275
|
|
|
421
276
|
/**
|
|
422
|
-
*
|
|
423
|
-
*
|
|
424
|
-
* @param {string} scopeName
|
|
425
|
-
* @param {
|
|
426
|
-
* @param {
|
|
427
|
-
* @returns {
|
|
428
|
-
*
|
|
429
|
-
* @example
|
|
430
|
-
* // Input: No sparse fieldsets (calculate all computed fields)
|
|
431
|
-
* const computed = {
|
|
432
|
-
* full_name: { compute: (r) => `${r.first} ${r.last}` },
|
|
433
|
-
* age: { compute: (r) => new Date().getFullYear() - r.birth_year }
|
|
434
|
-
* };
|
|
435
|
-
* getRequestedComputedFields('users', null, computed);
|
|
436
|
-
* // Output: ['full_name', 'age'] (all computed fields)
|
|
437
|
-
*
|
|
438
|
-
* @example
|
|
439
|
-
* // Input: Sparse fieldsets with some computed fields
|
|
440
|
-
* const requestedFields = 'first_name,full_name,email';
|
|
441
|
-
* const computed = {
|
|
442
|
-
* full_name: { compute: (r) => `${r.first} ${r.last}` },
|
|
443
|
-
* age: { compute: (r) => new Date().getFullYear() - r.birth_year }
|
|
444
|
-
* };
|
|
445
|
-
* getRequestedComputedFields('users', requestedFields, computed);
|
|
446
|
-
* // Output: ['full_name'] (only requested computed field)
|
|
447
|
-
*
|
|
448
|
-
* @example
|
|
449
|
-
* // Input: Sparse fieldsets with no computed fields
|
|
450
|
-
* const requestedFields = ['first_name', 'email'];
|
|
451
|
-
* const computed = {
|
|
452
|
-
* full_name: { compute: (r) => `${r.first} ${r.last}` }
|
|
453
|
-
* };
|
|
454
|
-
* getRequestedComputedFields('users', requestedFields, computed);
|
|
455
|
-
* // Output: [] (no computed fields requested)
|
|
456
|
-
*
|
|
457
|
-
* @description
|
|
458
|
-
* Used by:
|
|
459
|
-
* - enrichAttributes to determine which computations to run
|
|
460
|
-
* - Applied after fetching data from database
|
|
461
|
-
*
|
|
462
|
-
* Purpose:
|
|
463
|
-
* - Optimizes performance by only computing requested fields
|
|
464
|
-
* - Supports JSON:API sparse fieldsets for computed fields
|
|
465
|
-
* - Handles both string and array input formats
|
|
466
|
-
*
|
|
467
|
-
* Data flow:
|
|
468
|
-
* 1. Checks if sparse fieldsets are specified
|
|
469
|
-
* 2. If not, returns all computed field names
|
|
470
|
-
* 3. If yes, filters to only requested computed fields
|
|
471
|
-
* 4. Normalizes string input to array format
|
|
277
|
+
* Select visible computed fields in request order, or declaration order when
|
|
278
|
+
* no fieldset is supplied. Explicit selection includes normally-hidden fields.
|
|
279
|
+
* @param {string} scopeName
|
|
280
|
+
* @param {string | readonly string[] | null | undefined} requestedFields
|
|
281
|
+
* @param {Record<string, { hidden?: boolean, normallyHidden?: boolean }> | undefined} computedFields
|
|
282
|
+
* @returns {string[]}
|
|
472
283
|
*/
|
|
473
284
|
export const getRequestedComputedFields = (scopeName, requestedFields, computedFields) => {
|
|
474
285
|
if (!computedFields) return []
|
|
475
286
|
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
287
|
+
const requested = parseFieldset(requestedFields)
|
|
288
|
+
if (requested === null) {
|
|
289
|
+
return Object.entries(computedFields).filter(([, definition]) => {
|
|
290
|
+
return definition.hidden !== true && definition.normallyHidden !== true
|
|
291
|
+
}).map(([field]) => field)
|
|
481
292
|
}
|
|
482
293
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
return requested.filter(field => allComputedFields.includes(field))
|
|
294
|
+
return requested.filter(field => {
|
|
295
|
+
if (!Object.hasOwn(computedFields, field)) return false
|
|
296
|
+
const definition = computedFields[field]
|
|
297
|
+
if (!definition) throw new Error(`Missing compiled computed definition '${scopeName}.${field}'`)
|
|
298
|
+
return definition.hidden !== true
|
|
299
|
+
})
|
|
490
300
|
}
|