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