json-rest-api 1.0.29 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +744 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +634 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +100 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
- package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +36 -0
- package/index.js +1 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +64 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -177
- package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +25 -16
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +13 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +128 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1077
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -395
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +36 -68
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +37 -81
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +53 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -148
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -155
- package/tests/fixtures/api-configs.js +0 -2566
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-boundaries.test.js +0 -154
- package/tests/pagination-contract.test.js +0 -122
- package/tests/pagination-cursor-multifield.test.js +0 -641
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/temporal-boundaries.test.js +0 -208
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { createSchema } from 'json-rest-schema'
|
|
2
|
+
import { getForeignKeyFields, assertFieldName, assertFieldNameMap } from './field-utils.js'
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Schema processing utilities for search and field dependencies
|
|
3
6
|
*
|
|
@@ -10,79 +13,15 @@
|
|
|
10
13
|
*/
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* @param {Object} searchSchema - Search schema object to process
|
|
16
|
-
* @returns {void} Modifies the searchSchema in-place
|
|
16
|
+
* Mark object-valued search definitions indexed in place, overriding false.
|
|
17
|
+
* This is search metadata, not a DDL operation or a query-performance guarantee.
|
|
17
18
|
*
|
|
19
|
+
* @param {Object} searchSchema - Search definitions, if available
|
|
20
|
+
* @returns {void}
|
|
18
21
|
* @example
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* status: { type: 'string', filterOperator: '=' },
|
|
23
|
-
* created_at: { type: 'datetime', filterOperator: '>=' }
|
|
24
|
-
* };
|
|
25
|
-
* ensureSearchFieldsAreIndexed(searchSchema);
|
|
26
|
-
*
|
|
27
|
-
* // Output: All fields marked as indexed
|
|
28
|
-
* // {
|
|
29
|
-
* // title: { type: 'string', filterOperator: 'contains', indexed: true },
|
|
30
|
-
* // status: { type: 'string', filterOperator: '=', indexed: true },
|
|
31
|
-
* // created_at: { type: 'datetime', filterOperator: '>=', indexed: true }
|
|
32
|
-
* // }
|
|
33
|
-
*
|
|
34
|
-
* @example
|
|
35
|
-
* // Input: Virtual field for cross-table search
|
|
36
|
-
* const searchSchema = {
|
|
37
|
-
* name: { type: 'string', filterOperator: 'contains' },
|
|
38
|
-
* author_name: { // Virtual field
|
|
39
|
-
* type: 'string',
|
|
40
|
-
* filterOperator: 'contains',
|
|
41
|
-
* virtualField: {
|
|
42
|
-
* joinTo: 'users',
|
|
43
|
-
* joinOn: ['author_id', 'id'],
|
|
44
|
-
* searchField: 'name'
|
|
45
|
-
* }
|
|
46
|
-
* }
|
|
47
|
-
* };
|
|
48
|
-
* ensureSearchFieldsAreIndexed(searchSchema);
|
|
49
|
-
*
|
|
50
|
-
* // Output: Both regular and virtual fields indexed
|
|
51
|
-
* // {
|
|
52
|
-
* // name: { ..., indexed: true },
|
|
53
|
-
* // author_name: { ..., indexed: true } // Enables efficient JOIN
|
|
54
|
-
* // }
|
|
55
|
-
*
|
|
56
|
-
* @example
|
|
57
|
-
* // Input: Already indexed or explicitly false
|
|
58
|
-
* const searchSchema = {
|
|
59
|
-
* email: { type: 'string', indexed: false }, // Explicitly false
|
|
60
|
-
* username: { type: 'string', indexed: true } // Already true
|
|
61
|
-
* };
|
|
62
|
-
* ensureSearchFieldsAreIndexed(searchSchema);
|
|
63
|
-
*
|
|
64
|
-
* // Output: All forced to indexed: true
|
|
65
|
-
* // {
|
|
66
|
-
* // email: { type: 'string', indexed: true }, // Overridden
|
|
67
|
-
* // username: { type: 'string', indexed: true } // Unchanged
|
|
68
|
-
* // }
|
|
69
|
-
*
|
|
70
|
-
* @description
|
|
71
|
-
* Used by:
|
|
72
|
-
* - rest-api-plugin during scope initialization
|
|
73
|
-
* - Applied to all searchSchema fields before storage setup
|
|
74
|
-
*
|
|
75
|
-
* Purpose:
|
|
76
|
-
* - Signals storage plugins which fields need database indexes
|
|
77
|
-
* - Enables efficient filtering without full table scans
|
|
78
|
-
* - Supports cross-table searches via indexed JOINs
|
|
79
|
-
* - Improves query performance for filtered API requests
|
|
80
|
-
*
|
|
81
|
-
* Data flow:
|
|
82
|
-
* 1. Receives searchSchema object (or null)
|
|
83
|
-
* 2. Iterates through each field definition
|
|
84
|
-
* 3. Sets indexed: true on all fields
|
|
85
|
-
* 4. Storage plugins use this flag for index creation
|
|
22
|
+
* const fields = { title: { type: 'string', indexed: false } }
|
|
23
|
+
* ensureSearchFieldsAreIndexed(fields)
|
|
24
|
+
* // fields.title.indexed === true
|
|
86
25
|
*/
|
|
87
26
|
export function ensureSearchFieldsAreIndexed (searchSchema) {
|
|
88
27
|
if (!searchSchema) return
|
|
@@ -118,7 +57,8 @@ function createRelationshipSearchEntries (fieldName, fieldDef, searchEntry) {
|
|
|
118
57
|
|
|
119
58
|
function registerSearchEntries (searchSchema, entries = []) {
|
|
120
59
|
for (const [searchFieldName, searchEntry] of entries) {
|
|
121
|
-
|
|
60
|
+
assertFieldName(searchFieldName, 'generated search schema')
|
|
61
|
+
if (!searchFieldName || Object.hasOwn(searchSchema, searchFieldName)) {
|
|
122
62
|
continue
|
|
123
63
|
}
|
|
124
64
|
|
|
@@ -127,137 +67,21 @@ function registerSearchEntries (searchSchema, entries = []) {
|
|
|
127
67
|
}
|
|
128
68
|
|
|
129
69
|
/**
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* @
|
|
138
|
-
*
|
|
139
|
-
* const schema = {
|
|
140
|
-
* title: { type: 'string', search: true }, // Simple
|
|
141
|
-
* content: { type: 'text' }, // Not searchable
|
|
142
|
-
* status: { type: 'string', search: { filterOperator: '=' } } // Custom
|
|
143
|
-
* };
|
|
144
|
-
* const result = generateSearchSchemaFromSchema(schema, null);
|
|
145
|
-
*
|
|
146
|
-
* // Output: Generated search schema
|
|
147
|
-
* // {
|
|
148
|
-
* // title: { type: 'string' }, // No operator set; resolver applies defaults
|
|
149
|
-
* // status: { type: 'string', filterOperator: '=' } // Specified operator
|
|
150
|
-
* // }
|
|
151
|
-
*
|
|
152
|
-
* @example
|
|
153
|
-
* // Input: Multiple filters from one field
|
|
154
|
-
* const schema = {
|
|
155
|
-
* published_at: {
|
|
156
|
-
* type: 'datetime',
|
|
157
|
-
* search: {
|
|
158
|
-
* published_after: { filterOperator: '>=', type: 'datetime' },
|
|
159
|
-
* published_before: { filterOperator: '<=', type: 'datetime' }
|
|
160
|
-
* }
|
|
161
|
-
* }
|
|
162
|
-
* };
|
|
163
|
-
* const result = generateSearchSchemaFromSchema(schema, null);
|
|
164
|
-
*
|
|
165
|
-
* // Output: Two search fields from one database field
|
|
166
|
-
* // {
|
|
167
|
-
* // published_after: {
|
|
168
|
-
* // type: 'datetime',
|
|
169
|
-
* // actualField: 'published_at', // Maps to real field
|
|
170
|
-
* // filterOperator: '>='
|
|
171
|
-
* // },
|
|
172
|
-
* // published_before: {
|
|
173
|
-
* // type: 'datetime',
|
|
174
|
-
* // actualField: 'published_at', // Same field, different operator
|
|
175
|
-
* // filterOperator: '<='
|
|
176
|
-
* // }
|
|
177
|
-
* // }
|
|
178
|
-
*
|
|
179
|
-
* @example
|
|
180
|
-
* // Input: Merging with explicit searchSchema
|
|
181
|
-
* const schema = {
|
|
182
|
-
* name: { type: 'string', search: true }
|
|
183
|
-
* };
|
|
184
|
-
* const explicitSearchSchema = {
|
|
185
|
-
* email: { type: 'string', filterOperator: '=' },
|
|
186
|
-
* age: { type: 'number', filterOperator: '>=' }
|
|
187
|
-
* };
|
|
188
|
-
* const result = generateSearchSchemaFromSchema(schema, explicitSearchSchema);
|
|
189
|
-
*
|
|
190
|
-
* // Output: Combined from both sources
|
|
191
|
-
* // {
|
|
192
|
-
* // email: { type: 'string', filterOperator: '=' }, // Explicit
|
|
193
|
-
* // age: { type: 'number', filterOperator: '>=' }, // Explicit
|
|
194
|
-
* // name: { type: 'string' } // From schema (no operator set)
|
|
195
|
-
* // }
|
|
196
|
-
*
|
|
70
|
+
* Merge generated field filters into an explicit search schema.
|
|
71
|
+
* Explicit names win. Boolean search copies the field declaration; configured
|
|
72
|
+
* search may define one filter or named filters with actualField mappings.
|
|
73
|
+
* belongsTo aliases and physical-field search entries share the target metadata.
|
|
74
|
+
* Operator defaults are resolved later, not hard-coded by this generator.
|
|
75
|
+
*
|
|
76
|
+
* @param {Object} schema - Resource field definitions
|
|
77
|
+
* @param {Object} explicitSearchSchema - Explicit filters taking precedence
|
|
78
|
+
* @returns {Object|null} Merged definitions, or null when none exist
|
|
197
79
|
* @example
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
* email: { type: 'string', search: true }
|
|
201
|
-
* };
|
|
202
|
-
* const explicitSearchSchema = {
|
|
203
|
-
* email: { type: 'string', filterOperator: 'contains' }
|
|
204
|
-
* };
|
|
205
|
-
* const result = generateSearchSchemaFromSchema(schema, explicitSearchSchema);
|
|
206
|
-
* // Output: Explicit searchSchema wins
|
|
207
|
-
* // {
|
|
208
|
-
* // email: { type: 'string', filterOperator: 'contains' } // Uses explicit definition
|
|
209
|
-
* // }
|
|
210
|
-
*
|
|
211
|
-
* @example
|
|
212
|
-
* // Input: Virtual search fields
|
|
213
|
-
* const schema = {
|
|
214
|
-
* title: { type: 'string' },
|
|
215
|
-
* _virtual: {
|
|
216
|
-
* search: {
|
|
217
|
-
* category_name: { // Doesn't exist in DB
|
|
218
|
-
* type: 'string',
|
|
219
|
-
* filterOperator: 'contains',
|
|
220
|
-
* virtualField: {
|
|
221
|
-
* joinTo: 'categories',
|
|
222
|
-
* joinOn: ['category_id', 'id'], // JOIN condition
|
|
223
|
-
* searchField: 'name' // Field in related table
|
|
224
|
-
* }
|
|
225
|
-
* }
|
|
226
|
-
* }
|
|
227
|
-
* }
|
|
228
|
-
* };
|
|
229
|
-
*
|
|
230
|
-
* // Output: Virtual field for cross-table search
|
|
231
|
-
* // {
|
|
232
|
-
* // category_name: {
|
|
233
|
-
* // type: 'string',
|
|
234
|
-
* // filterOperator: 'contains',
|
|
235
|
-
* // virtualField: { ... join config ... }
|
|
236
|
-
* // }
|
|
237
|
-
* // }
|
|
238
|
-
*
|
|
239
|
-
* @description
|
|
240
|
-
* Used by:
|
|
241
|
-
* - rest-api-plugin during scope initialization
|
|
242
|
-
* - Generates searchSchema from various sources
|
|
243
|
-
*
|
|
244
|
-
* Purpose:
|
|
245
|
-
* - Provides flexible search configuration options
|
|
246
|
-
* - Enables virtual fields for cross-table searches
|
|
247
|
-
* - Supports range queries (before/after patterns)
|
|
248
|
-
* - Merges search:true fields with explicit searchSchema
|
|
249
|
-
* - Explicit searchSchema always takes precedence
|
|
250
|
-
* - Allows storage plugins to optimize queries
|
|
251
|
-
*
|
|
252
|
-
* Data flow:
|
|
253
|
-
* 1. Starts with explicit searchSchema (if any)
|
|
254
|
-
* 2. Processes schema fields with 'search' property
|
|
255
|
-
* 3. Skips fields already in explicit searchSchema (no conflicts)
|
|
256
|
-
* 4. Handles multiple filters from single field
|
|
257
|
-
* 5. Processes virtual search definitions
|
|
258
|
-
* 6. Returns merged searchSchema or null
|
|
80
|
+
* generateSearchSchemaFromSchema({ title: { type: 'string', search: true } }, {})
|
|
81
|
+
* // { title: { type: 'string' } }
|
|
259
82
|
*/
|
|
260
83
|
export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) => {
|
|
84
|
+
assertFieldNameMap(explicitSearchSchema, 'explicit search schema')
|
|
261
85
|
// Start with explicit searchSchema or empty object
|
|
262
86
|
const searchSchema = explicitSearchSchema ? { ...explicitSearchSchema } : {}
|
|
263
87
|
|
|
@@ -278,6 +102,7 @@ export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) =>
|
|
|
278
102
|
// Do not set filterOperator here; centralized resolver will apply sensible defaults
|
|
279
103
|
}))
|
|
280
104
|
} else if (typeof effectiveSearch === 'object') {
|
|
105
|
+
assertFieldNameMap(effectiveSearch, `search declaration for '${fieldName}'`)
|
|
281
106
|
// Check if search defines multiple filter fields
|
|
282
107
|
const hasNestedFilters = Object.values(effectiveSearch).some(
|
|
283
108
|
v => typeof v === 'object' && v.filterOperator
|
|
@@ -287,7 +112,7 @@ export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) =>
|
|
|
287
112
|
// Multiple filters from one field (like published_after/before)
|
|
288
113
|
Object.entries(effectiveSearch).forEach(([filterName, filterDef]) => {
|
|
289
114
|
// Check if filter already exists in explicit searchSchema
|
|
290
|
-
if (searchSchema
|
|
115
|
+
if (Object.hasOwn(searchSchema, filterName)) {
|
|
291
116
|
// Skip - explicit searchSchema takes precedence
|
|
292
117
|
return
|
|
293
118
|
}
|
|
@@ -311,9 +136,10 @@ export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) =>
|
|
|
311
136
|
|
|
312
137
|
// Handle _virtual search definitions
|
|
313
138
|
if (schema._virtual?.search) {
|
|
139
|
+
assertFieldNameMap(schema._virtual.search, 'virtual search schema')
|
|
314
140
|
Object.entries(schema._virtual.search).forEach(([filterName, filterDef]) => {
|
|
315
141
|
// Check if filter already exists in explicit searchSchema
|
|
316
|
-
if (searchSchema
|
|
142
|
+
if (Object.hasOwn(searchSchema, filterName)) {
|
|
317
143
|
// Skip - explicit searchSchema takes precedence
|
|
318
144
|
return
|
|
319
145
|
}
|
|
@@ -326,52 +152,21 @@ export const generateSearchSchemaFromSchema = (schema, explicitSearchSchema) =>
|
|
|
326
152
|
}
|
|
327
153
|
|
|
328
154
|
/**
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
* @
|
|
332
|
-
* @param {
|
|
333
|
-
* @
|
|
334
|
-
* @
|
|
335
|
-
*
|
|
336
|
-
* @example
|
|
337
|
-
* // Input: Simple dependency chain
|
|
338
|
-
* const items = ['a', 'b', 'c'];
|
|
339
|
-
* const deps = { a: ['b'], b: ['c'], c: [] };
|
|
340
|
-
* const sorted = topologicalSort(items, item => deps[item]);
|
|
341
|
-
* // Output: ['c', 'b', 'a']
|
|
342
|
-
* // c first (no deps), then b (depends on c), then a (depends on b)
|
|
343
|
-
*
|
|
344
|
-
* @example
|
|
345
|
-
* // Input: Circular dependency
|
|
346
|
-
* const items = ['a', 'b'];
|
|
347
|
-
* const deps = { a: ['b'], b: ['a'] }; // Circular!
|
|
348
|
-
* topologicalSort(items, item => deps[item]);
|
|
349
|
-
* // Throws: Error "Circular dependency detected: b"
|
|
350
|
-
*
|
|
351
|
-
* @example
|
|
352
|
-
* // Input: Unknown dependency
|
|
353
|
-
* const items = ['a', 'b'];
|
|
354
|
-
* const deps = { a: ['c'], b: [] }; // 'c' not in items
|
|
355
|
-
* topologicalSort(items, item => deps[item]);
|
|
356
|
-
* // Throws: Error "Unknown dependency 'c' for 'a'"
|
|
357
|
-
*
|
|
358
|
-
* @description
|
|
359
|
-
* Used by:
|
|
360
|
-
* - sortFieldsByDependencies for ordering field operations
|
|
361
|
-
* - Any code needing dependency-based ordering
|
|
362
|
-
*
|
|
363
|
-
* Purpose:
|
|
364
|
-
* - Orders items so dependencies come before dependents
|
|
365
|
-
* - Detects circular dependencies early
|
|
366
|
-
* - Validates all dependencies exist
|
|
367
|
-
* - Uses depth-first search algorithm
|
|
155
|
+
* Sort a fixed field graph dependency-first, preserving traversal order for ties.
|
|
156
|
+
* Membership is indexed once; cycles and references outside the graph reject.
|
|
157
|
+
* @template T
|
|
158
|
+
* @param {readonly T[]} items
|
|
159
|
+
* @param {(item: T) => Iterable<T> | null | undefined} getDependencies
|
|
160
|
+
* @returns {T[]}
|
|
368
161
|
*/
|
|
369
162
|
export function topologicalSort (items, getDependencies) {
|
|
163
|
+
const itemNames = new Set(items)
|
|
370
164
|
const sorted = []
|
|
371
165
|
const visited = new Set()
|
|
372
166
|
const visiting = new Set()
|
|
167
|
+
const stack = []
|
|
373
168
|
|
|
374
|
-
function
|
|
169
|
+
function enter (item) {
|
|
375
170
|
if (visited.has(item)) return
|
|
376
171
|
|
|
377
172
|
if (visiting.has(item)) {
|
|
@@ -380,100 +175,184 @@ export function topologicalSort (items, getDependencies) {
|
|
|
380
175
|
|
|
381
176
|
visiting.add(item)
|
|
382
177
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
178
|
+
stack.push({ item, iterator: (getDependencies(item) || [])[Symbol.iterator]() })
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
for (const item of items) {
|
|
183
|
+
enter(item)
|
|
184
|
+
while (stack.length) {
|
|
185
|
+
const frame = stack[stack.length - 1]
|
|
186
|
+
let next
|
|
187
|
+
try {
|
|
188
|
+
next = frame.iterator.next()
|
|
189
|
+
} catch (error) {
|
|
190
|
+
stack.pop()
|
|
191
|
+
throw error
|
|
192
|
+
}
|
|
193
|
+
if (next.done) {
|
|
194
|
+
visiting.delete(frame.item)
|
|
195
|
+
visited.add(frame.item)
|
|
196
|
+
sorted.push(frame.item)
|
|
197
|
+
stack.pop()
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
200
|
+
if (!itemNames.has(next.value)) {
|
|
201
|
+
throw new Error(`Unknown dependency '${next.value}' for '${frame.item}'`)
|
|
202
|
+
}
|
|
203
|
+
enter(next.value)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
// Match nested for-of cleanup while retaining the original graph failure.
|
|
208
|
+
for (let index = stack.length - 1; index >= 0; index--) {
|
|
209
|
+
try { stack[index].iterator.return?.() } catch {}
|
|
210
|
+
}
|
|
211
|
+
throw error
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return sorted
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Validate callback dependencies once; retain direct read edges and execution orders. */
|
|
218
|
+
export function compileFieldDependencies ({ schemaStructure, computedFields, queryFields = {}, schemaRelationships = {}, idProperty = 'id', scopeName }) {
|
|
219
|
+
const inputNames = new Set([...Object.keys(schemaStructure), 'id', idProperty])
|
|
220
|
+
const getterNames = new Set([...inputNames, ...Object.keys(queryFields)])
|
|
221
|
+
const readNames = new Set([...getterNames, ...Object.keys(computedFields)])
|
|
222
|
+
const relationshipFields = getForeignKeyFields(schemaStructure, schemaRelationships)
|
|
223
|
+
const getterDependencies = new Set([...getterNames].filter(name => !relationshipFields.has(name)))
|
|
224
|
+
const computedDependencies = new Set([...readNames].filter(name => !relationshipFields.has(name)))
|
|
225
|
+
const fieldGetters = Object.create(null)
|
|
226
|
+
const fieldSetters = Object.create(null)
|
|
227
|
+
const readDependencies = Object.fromEntries([...readNames].map(name => [name, []]))
|
|
228
|
+
|
|
229
|
+
const dependencies = (name, definition, property, kind, available) => {
|
|
230
|
+
const declared = definition[property]
|
|
231
|
+
if (declared === undefined) return []
|
|
232
|
+
if (!Array.isArray(declared) || declared.some(dep => typeof dep !== 'string' || dep.length === 0)) {
|
|
233
|
+
throw new Error(`Field '${name}' in resource '${scopeName}' must declare ${property} as an array of nonempty field names`)
|
|
234
|
+
}
|
|
235
|
+
for (const dep of declared) {
|
|
236
|
+
if (!available.has(dep)) {
|
|
237
|
+
throw new Error(`Field '${name}' in resource '${scopeName}' has ${kind} dependency '${dep}' that does not exist in schema or is unavailable at that stage`)
|
|
387
238
|
}
|
|
388
|
-
visit(dep)
|
|
389
239
|
}
|
|
240
|
+
return [...new Set(declared)]
|
|
241
|
+
}
|
|
390
242
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
243
|
+
for (const [name, definition] of Object.entries({ ...schemaStructure, ...queryFields })) {
|
|
244
|
+
for (const [callback, property, available, target] of [
|
|
245
|
+
['getter', 'runGetterAfter', getterDependencies, fieldGetters],
|
|
246
|
+
['setter', 'runSetterAfter', inputNames, fieldSetters]
|
|
247
|
+
]) {
|
|
248
|
+
if (callback === 'setter' && Object.hasOwn(queryFields, name)) {
|
|
249
|
+
if (definition.setter !== undefined || definition.runSetterAfter !== undefined) {
|
|
250
|
+
throw new Error(`Query field '${name}' in resource '${scopeName}' cannot declare a setter or runSetterAfter; projections are read-only`)
|
|
251
|
+
}
|
|
252
|
+
continue
|
|
253
|
+
}
|
|
254
|
+
if (definition[callback] !== undefined && typeof definition[callback] !== 'function') {
|
|
255
|
+
throw new Error(`Field '${name}' in resource '${scopeName}' has invalid ${callback} function`)
|
|
256
|
+
}
|
|
257
|
+
if (definition[callback] === undefined) {
|
|
258
|
+
if (definition[property] !== undefined) {
|
|
259
|
+
throw new Error(`Field '${name}' in resource '${scopeName}' declares ${property} without a ${callback} function`)
|
|
260
|
+
}
|
|
261
|
+
continue
|
|
262
|
+
}
|
|
263
|
+
const after = dependencies(name, definition, property, callback, available)
|
|
264
|
+
target[name] = { [callback]: definition[callback], [property]: after, fieldDef: definition }
|
|
265
|
+
if (callback === 'getter') readDependencies[name] = after
|
|
266
|
+
}
|
|
394
267
|
}
|
|
395
268
|
|
|
396
|
-
for (const
|
|
397
|
-
|
|
269
|
+
for (const [name, definition] of Object.entries(computedFields)) {
|
|
270
|
+
readDependencies[name] = dependencies(name, definition, 'dependencies', 'computed', computedDependencies)
|
|
398
271
|
}
|
|
399
272
|
|
|
400
|
-
|
|
273
|
+
let sortedGetterFields, sortedSetterFields, sortedComputedFields
|
|
274
|
+
try {
|
|
275
|
+
sortedGetterFields = topologicalSort([...getterNames], name => fieldGetters[name]?.runGetterAfter || [])
|
|
276
|
+
.filter(name => Object.hasOwn(fieldGetters, name))
|
|
277
|
+
} catch (error) {
|
|
278
|
+
throw new Error(`Invalid getter dependencies in ${scopeName}: ${error.message}`, { cause: error })
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
sortedSetterFields = topologicalSort([...inputNames], name => fieldSetters[name]?.runSetterAfter || [])
|
|
282
|
+
.filter(name => Object.hasOwn(fieldSetters, name))
|
|
283
|
+
} catch (error) {
|
|
284
|
+
throw new Error(`Invalid setter dependencies in ${scopeName}: ${error.message}`, { cause: error })
|
|
285
|
+
}
|
|
286
|
+
try {
|
|
287
|
+
sortedComputedFields = topologicalSort([...readNames], name => readDependencies[name])
|
|
288
|
+
.filter(name => Object.hasOwn(computedFields, name))
|
|
289
|
+
} catch (error) {
|
|
290
|
+
throw new Error(`Invalid computed dependencies in ${scopeName}: ${error.message}`, { cause: error })
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return { fieldGetters, sortedGetterFields, fieldSetters, sortedSetterFields, readDependencies, sortedComputedFields, foreignKeyFields: relationshipFields }
|
|
401
294
|
}
|
|
402
295
|
|
|
403
|
-
/**
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
* @
|
|
407
|
-
* @param {string} dependencyProperty - Property name containing dependencies
|
|
408
|
-
* @returns {Array} Field names sorted by dependencies
|
|
409
|
-
* @throws {Error} If circular dependencies or unknown fields detected
|
|
410
|
-
*
|
|
411
|
-
* @example
|
|
412
|
-
* // Input: Getter dependencies (fullName needs firstName and lastName)
|
|
413
|
-
* const fields = {
|
|
414
|
-
* firstName: { getter: v => v.trim() },
|
|
415
|
-
* lastName: { getter: v => v.trim() },
|
|
416
|
-
* fullName: {
|
|
417
|
-
* getter: (v, ctx) => `${ctx.attributes.firstName} ${ctx.attributes.lastName}`,
|
|
418
|
-
* runGetterAfter: ['firstName', 'lastName']
|
|
419
|
-
* }
|
|
420
|
-
* };
|
|
421
|
-
* const sorted = sortFieldsByDependencies(fields, 'runGetterAfter');
|
|
422
|
-
* // Output: ['firstName', 'lastName', 'fullName']
|
|
423
|
-
* // Ensures firstName/lastName getters run before fullName
|
|
424
|
-
*
|
|
425
|
-
* @example
|
|
426
|
-
* // Input: Complex dependency chain
|
|
427
|
-
* const fields = {
|
|
428
|
-
* a: { runGetterAfter: ['b', 'c'] }, // a needs b and c
|
|
429
|
-
* b: { runGetterAfter: ['d'] }, // b needs d
|
|
430
|
-
* c: { runGetterAfter: ['d'] }, // c needs d
|
|
431
|
-
* d: { runGetterAfter: [] } // d needs nothing
|
|
432
|
-
* };
|
|
433
|
-
* const sorted = sortFieldsByDependencies(fields, 'runGetterAfter');
|
|
434
|
-
* // Output: ['d', 'b', 'c', 'a']
|
|
435
|
-
* // d first, then b/c (both need d), then a (needs b/c)
|
|
436
|
-
*
|
|
437
|
-
* @example
|
|
438
|
-
* // Input: Circular dependency error
|
|
439
|
-
* const fields = {
|
|
440
|
-
* a: { runGetterAfter: ['b'] },
|
|
441
|
-
* b: { runGetterAfter: ['a'] } // Circular!
|
|
442
|
-
* };
|
|
443
|
-
* sortFieldsByDependencies(fields, 'runGetterAfter');
|
|
444
|
-
* // Throws: Error "Circular dependency detected: a in runGetterAfter"
|
|
445
|
-
*
|
|
446
|
-
* @description
|
|
447
|
-
* Used by:
|
|
448
|
-
* - Schema processing for getter/setter ordering
|
|
449
|
-
* - Ensures dependent fields process after dependencies
|
|
450
|
-
*
|
|
451
|
-
* Purpose:
|
|
452
|
-
* - Orders field operations by dependencies
|
|
453
|
-
* - Enables computed fields that depend on other fields
|
|
454
|
-
* - Validates dependency graph is acyclic
|
|
455
|
-
* - Provides clear error messages for debugging
|
|
456
|
-
*
|
|
457
|
-
* Data flow:
|
|
458
|
-
* 1. Extracts field names from object
|
|
459
|
-
* 2. Calls topologicalSort with dependency function
|
|
460
|
-
* 3. Returns ordered field names
|
|
461
|
-
* 4. Enhances error messages with property name
|
|
296
|
+
/** Find only the dependencies needed by this read; ordering is already compiled.
|
|
297
|
+
* @param {{ readDependencies?: Record<string, readonly string[]> }} schemaInfo
|
|
298
|
+
* @param {Iterable<string>} fields
|
|
299
|
+
* @returns {Set<string>}
|
|
462
300
|
*/
|
|
463
|
-
export function
|
|
464
|
-
const
|
|
301
|
+
export function getFieldDependencyClosure (schemaInfo, fields) {
|
|
302
|
+
const selected = new Set(fields)
|
|
303
|
+
const pending = [...selected]
|
|
304
|
+
const dependencies = schemaInfo.readDependencies || {}
|
|
305
|
+
while (pending.length) {
|
|
306
|
+
const name = pending.pop()
|
|
307
|
+
if (!Object.hasOwn(dependencies, name)) continue
|
|
308
|
+
for (const dependency of dependencies[name]) {
|
|
309
|
+
if (selected.has(dependency)) continue
|
|
310
|
+
selected.add(dependency)
|
|
311
|
+
pending.push(dependency)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return selected
|
|
315
|
+
}
|
|
465
316
|
|
|
466
|
-
|
|
317
|
+
/** Copy declaration data without freezing caller-owned values or cloning callbacks. */
|
|
318
|
+
export function snapshotResourceConfiguration (value, seen = new WeakMap()) {
|
|
319
|
+
if (value === null || typeof value !== 'object') return value
|
|
320
|
+
if (seen.has(value)) return seen.get(value)
|
|
467
321
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
|
|
322
|
+
let copy
|
|
323
|
+
if (value instanceof Date) copy = new Date(value.getTime())
|
|
324
|
+
else if (value instanceof RegExp) {
|
|
325
|
+
copy = new RegExp(value.source, value.flags)
|
|
326
|
+
copy.lastIndex = value.lastIndex
|
|
327
|
+
} else if (typeof value.validateWith === 'function' && typeof value.getFieldDefinitions === 'function') {
|
|
328
|
+
const factory = createSchema.createFactory(value)
|
|
329
|
+
copy = factory({}, { operations: value.operations })
|
|
330
|
+
seen.set(value, copy)
|
|
331
|
+
copy.structure = snapshotResourceConfiguration(value.structure, seen)
|
|
332
|
+
return copy
|
|
333
|
+
} else if (Array.isArray(value)) copy = new Array(value.length)
|
|
334
|
+
else {
|
|
335
|
+
const prototype = Object.getPrototypeOf(value)
|
|
336
|
+
if (prototype !== Object.prototype && prototype !== null) return value
|
|
337
|
+
copy = Object.create(prototype)
|
|
338
|
+
}
|
|
339
|
+
seen.set(value, copy)
|
|
340
|
+
if (value instanceof Date || value instanceof RegExp) return copy
|
|
341
|
+
for (const key of Object.keys(value)) {
|
|
342
|
+
// File backends are external handles; their state and method receiver stay intact.
|
|
343
|
+
const isFileBackend = key === 'storage' && value.type === 'file' && typeof value[key]?.upload === 'function'
|
|
344
|
+
let propertyValue
|
|
345
|
+
if (isFileBackend) {
|
|
346
|
+
propertyValue = value[key]
|
|
347
|
+
} else {
|
|
348
|
+
propertyValue = snapshotResourceConfiguration(value[key], seen)
|
|
476
349
|
}
|
|
477
|
-
|
|
350
|
+
Object.defineProperty(copy, key, {
|
|
351
|
+
value: propertyValue,
|
|
352
|
+
enumerable: true,
|
|
353
|
+
configurable: true,
|
|
354
|
+
writable: true
|
|
355
|
+
})
|
|
478
356
|
}
|
|
357
|
+
return copy
|
|
479
358
|
}
|