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,81 +1,54 @@
|
|
|
1
|
+
import { createCanonicalDataHelpers } from './lib/storage/canonical-data-helpers.js'
|
|
2
|
+
import { createCanonicalLinkStore } from './lib/anyapi/canonical-link-store.js'
|
|
3
|
+
import { createCanonicalRelationshipReader } from './lib/anyapi/canonical-relationship-reader.js'
|
|
4
|
+
import { createKnexTransaction } from '../../lib/knex-transaction.js'
|
|
5
|
+
import { createEnhancedLogger } from '../../lib/enhanced-logger.js'
|
|
1
6
|
import { ensureAnyApiSchema } from './lib/anyapi/schema-utils.js'
|
|
2
7
|
import { AnyapiRegistry } from './lib/anyapi/anyapi-registry.js'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from './lib/
|
|
10
|
-
import {
|
|
11
|
-
calculatePaginationMeta,
|
|
12
|
-
generatePaginationLinks,
|
|
13
|
-
generateCursorPaginationLinks,
|
|
14
|
-
buildCursorMeta,
|
|
15
|
-
parseCursor,
|
|
16
|
-
} from './lib/querying/knex-pagination-helpers.js'
|
|
17
|
-
import { getUrlPrefix, buildResourceUrl, buildRelationshipUrl } from './lib/querying/url-helpers.js'
|
|
18
|
-
import {
|
|
19
|
-
normalizeId,
|
|
20
|
-
resolveFieldInfo,
|
|
21
|
-
normalizeFilterValues,
|
|
22
|
-
ensureFilterableField,
|
|
23
|
-
} from './lib/anyapi/utils/descriptor-helpers.js'
|
|
24
|
-
import {
|
|
25
|
-
AnyapiQueryAdapter,
|
|
26
|
-
preloadRelatedDescriptors,
|
|
27
|
-
} from './lib/anyapi/query/anyapi-query-adapter.js'
|
|
28
|
-
import {
|
|
29
|
-
ensureSearchFieldsAreIndexed,
|
|
30
|
-
generateSearchSchemaFromSchema,
|
|
31
|
-
} from './lib/querying-writing/schema-helpers.js'
|
|
32
|
-
import { applyFieldSelectionToQuery, buildFieldSelection } from './lib/querying-writing/knex-field-helpers.js'
|
|
33
|
-
import {
|
|
34
|
-
polymorphicFiltersHook,
|
|
35
|
-
crossTableFiltersHook,
|
|
36
|
-
basicFiltersHook,
|
|
37
|
-
resolveSearchOperator,
|
|
38
|
-
applyWhereForOperator,
|
|
39
|
-
} from './lib/querying/knex-query-helpers.js'
|
|
40
|
-
import {
|
|
41
|
-
createStorageAdapter,
|
|
42
|
-
} from './lib/storage/storage-adapter.js'
|
|
43
|
-
import {
|
|
44
|
-
translateCanonicalAttributesForStorage,
|
|
45
|
-
getCanonicalResourceId,
|
|
46
|
-
getCanonicalResourceIdColumn,
|
|
47
|
-
translateCanonicalRecordFromStorage,
|
|
48
|
-
} from './lib/storage/canonical-storage-mapping.js'
|
|
49
|
-
import {
|
|
50
|
-
applyCursorPredicate,
|
|
51
|
-
validateCursorValues,
|
|
52
|
-
applyQueryFieldOrder,
|
|
53
|
-
buildEffectiveSortList,
|
|
54
|
-
parseSortEntry
|
|
55
|
-
} from './lib/querying/query-field-sort-helpers.js'
|
|
56
|
-
import { unwrapQueryBuilderState } from './lib/querying/query-builder-utils.js'
|
|
57
|
-
import { serializeJsonApiQuery } from './lib/querying-writing/connectors-query-parser.js'
|
|
58
|
-
import { normalizeValueForDatabaseStorage } from './lib/querying-writing/database-value-normalizers.js'
|
|
8
|
+
import { composeResourceOptions, relatedResources } from './lib/anyapi/stored-definition.js'
|
|
9
|
+
import { RestApiResourceError } from '../../lib/rest-api-errors.js'
|
|
10
|
+
import { compileSchemas } from './lib/querying-writing/compile-schemas.js'
|
|
11
|
+
import { snapshotResourceConfiguration } from './lib/querying-writing/schema-helpers.js'
|
|
12
|
+
import { assertFieldNameMap } from './lib/querying-writing/field-utils.js'
|
|
13
|
+
import { polymorphicFiltersHook, crossTableFiltersHook, basicFiltersHook } from './lib/querying/knex-query-helpers.js'
|
|
14
|
+
import { createStorageAdapterLookup } from './lib/storage/storage-adapter.js'
|
|
59
15
|
|
|
60
16
|
const DEFAULT_TENANT = 'default'
|
|
61
|
-
|
|
17
|
+
// Methods that read a published schema; schema checks wrap these.
|
|
18
|
+
const REQUEST_METHODS = ['query', 'get', 'post', 'put', 'patch', 'delete', 'getRelated', 'getRelationship', 'postRelationship', 'patchRelationship', 'deleteRelationship']
|
|
19
|
+
|
|
20
|
+
const optionsSignature = options => JSON.stringify([Object.keys(options.schema || {}).sort(), Object.keys(options.searchSchema || {}).sort()])
|
|
21
|
+
|
|
22
|
+
// A validation failure may come from a schema another server already changed.
|
|
23
|
+
// Writes that may have committed are never repeated.
|
|
24
|
+
const isStaleSchemaRejection = error => error?.type === 'rest_api_validation' && !['committed', 'unknown'].includes(error.transactionOutcome)
|
|
62
25
|
|
|
63
26
|
export const RestApiAnyapiKnexPlugin = {
|
|
64
27
|
name: 'rest-api-anyapi-knex',
|
|
65
28
|
dependencies: ['rest-api'],
|
|
66
29
|
|
|
67
|
-
async install ({ helpers,
|
|
30
|
+
async install ({ helpers, pluginOptions, api, log, addHook, addResourceMethod, scopes }) {
|
|
31
|
+
log = createEnhancedLogger(log)
|
|
68
32
|
const options = pluginOptions || {}
|
|
69
33
|
const knex = options.knex
|
|
70
34
|
const tenantId = options.tenantId || DEFAULT_TENANT
|
|
35
|
+
const { ensureSchema = true, ownsKnex = true, onSchemaChanged, schemaCheckInterval, slots } = options
|
|
71
36
|
|
|
72
37
|
if (!knex) {
|
|
73
38
|
throw new Error('AnyapiKnexPlugin requires a knex instance')
|
|
74
39
|
}
|
|
40
|
+
if (options.registry !== undefined && (!(options.registry instanceof AnyapiRegistry) || options.registry.knex !== knex)) {
|
|
41
|
+
throw new Error('AnyapiKnexPlugin registry must be an AnyapiRegistry for the same knex instance')
|
|
42
|
+
}
|
|
43
|
+
if (onSchemaChanged !== undefined && typeof onSchemaChanged !== 'function') {
|
|
44
|
+
throw new Error('AnyapiKnexPlugin onSchemaChanged must be a function')
|
|
45
|
+
}
|
|
46
|
+
if (schemaCheckInterval !== undefined && !(Number.isFinite(schemaCheckInterval) && schemaCheckInterval >= 0)) {
|
|
47
|
+
throw new Error('AnyapiKnexPlugin schemaCheckInterval must be a non-negative number of milliseconds')
|
|
48
|
+
}
|
|
75
49
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const registry = new AnyapiRegistry({ knex, log })
|
|
50
|
+
const registry = options.registry || new AnyapiRegistry({ knex, log })
|
|
51
|
+
if (ensureSchema) await registry.ensureSchema({ slots })
|
|
79
52
|
|
|
80
53
|
api.anyapi = api.anyapi || {}
|
|
81
54
|
api.anyapi.registry = registry
|
|
@@ -83,2482 +56,324 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
83
56
|
api.knex = {
|
|
84
57
|
instance: knex,
|
|
85
58
|
helpers: {},
|
|
59
|
+
capabilities: await registry.getCapabilities(log),
|
|
86
60
|
}
|
|
87
61
|
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const resource = api.resources?.[scopeName] || scopes?.[scopeName]
|
|
93
|
-
const schemaInfo = resource?.vars?.schemaInfo
|
|
94
|
-
if (!schemaInfo) return null
|
|
95
|
-
|
|
96
|
-
const cached = storageAdapterCache.get(scopeName)
|
|
97
|
-
if (cached && cached.schemaInfo === schemaInfo) {
|
|
98
|
-
return cached.adapter
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const adapter = createStorageAdapter({ knex, schemaInfo })
|
|
102
|
-
storageAdapterCache.set(scopeName, { adapter, schemaInfo })
|
|
103
|
-
if (resource?.vars) {
|
|
104
|
-
resource.vars.storageAdapter = adapter
|
|
105
|
-
}
|
|
106
|
-
return adapter
|
|
107
|
-
}
|
|
62
|
+
const getScopeStorageAdapter = createStorageAdapterLookup({
|
|
63
|
+
knex,
|
|
64
|
+
getResource: scopeName => api.resources?.[scopeName] || scopes?.[scopeName]
|
|
65
|
+
})
|
|
108
66
|
|
|
109
67
|
api.knex.helpers.getStorageAdapter = getScopeStorageAdapter
|
|
110
68
|
helpers.getStorageAdapter = getScopeStorageAdapter
|
|
111
69
|
|
|
112
|
-
|
|
70
|
+
// Owned copies of what this server declared and added, callbacks included.
|
|
71
|
+
const declarations = new Map()
|
|
72
|
+
const runtimeDeclarations = new Map()
|
|
73
|
+
const storageLoaded = new Set()
|
|
74
|
+
const compiledSignatures = new Map()
|
|
75
|
+
const lastChecked = new Map()
|
|
76
|
+
const reloads = new Map()
|
|
77
|
+
const checks = new Map()
|
|
78
|
+
const loading = new Map()
|
|
113
79
|
|
|
114
|
-
|
|
80
|
+
/** @type {import('../../lib/transaction-types.js').TransactionFactory} */
|
|
81
|
+
helpers.newTransaction = context => createKnexTransaction(knex, context)
|
|
115
82
|
|
|
116
83
|
addHook('release', 'anyapi-knex-release', {}, async ({ api }) => {
|
|
117
|
-
if (api.knex?.instance) {
|
|
84
|
+
if (ownsKnex && api.knex?.instance) {
|
|
118
85
|
await api.knex.instance.destroy()
|
|
119
86
|
}
|
|
120
87
|
})
|
|
121
88
|
|
|
122
|
-
const getDescriptor =
|
|
123
|
-
const descriptor =
|
|
89
|
+
const getDescriptor = (scopeName) => {
|
|
90
|
+
const descriptor = api.resources?.[scopeName]?.vars?.schemaInfo?.descriptor
|
|
124
91
|
if (!descriptor) {
|
|
125
92
|
throw new Error(`Descriptor not found for resource '${scopeName}'`)
|
|
126
93
|
}
|
|
127
|
-
const scope = api.resources?.[scopeName] || scopes?.[scopeName]
|
|
128
|
-
const stored = scopeOptionsRegistry.get(scopeName) || {}
|
|
129
|
-
descriptor.idProperty = stored.idProperty ||
|
|
130
|
-
scope?.vars?.schemaInfo?.idProperty ||
|
|
131
|
-
scope?.scopeOptions?.idProperty ||
|
|
132
|
-
descriptor.idProperty ||
|
|
133
|
-
vars.idProperty ||
|
|
134
|
-
'id'
|
|
135
94
|
return descriptor
|
|
136
95
|
}
|
|
137
96
|
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (!resource.relationships || typeof resource.relationships !== 'object') {
|
|
145
|
-
return
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
for (const [relationshipName, relationshipObject] of Object.entries(resource.relationships)) {
|
|
149
|
-
if (!relationshipObject || typeof relationshipObject !== 'object') {
|
|
150
|
-
continue
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
relationshipObject.links = {
|
|
154
|
-
self: buildRelationshipUrl(context, scope, scopeName, resource.id, relationshipName, true),
|
|
155
|
-
related: buildRelationshipUrl(context, scope, scopeName, resource.id, relationshipName, false),
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const getAllowedQueryFieldNames = (scopeName) => (
|
|
161
|
-
Object.keys(api.resources?.[scopeName]?.vars?.queryFields || {})
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
const getLogicalResourceIdColumn = (descriptor) => getCanonicalResourceIdColumn(descriptor)
|
|
165
|
-
|
|
166
|
-
const getLogicalResourceId = (row, descriptor) => getCanonicalResourceId(row, descriptor)
|
|
167
|
-
|
|
168
|
-
const applyFiltersToQuery = ({ query, filters, descriptor, searchSchema, adapter }) => {
|
|
169
|
-
if (!filters || Object.keys(filters).length === 0) return
|
|
170
|
-
|
|
171
|
-
const normalizeValue = (field, value) => {
|
|
172
|
-
if (!adapter?.translateFilterValue) {
|
|
173
|
-
return value
|
|
174
|
-
}
|
|
175
|
-
if (Array.isArray(value)) {
|
|
176
|
-
return value.map((entry) => adapter.translateFilterValue(field, entry))
|
|
177
|
-
}
|
|
178
|
-
return adapter.translateFilterValue(field, value)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
for (const [filterKey, rawValue] of Object.entries(filters)) {
|
|
182
|
-
const fieldDef = searchSchema?.[filterKey]
|
|
183
|
-
|
|
184
|
-
if (fieldDef?.polymorphicField) {
|
|
185
|
-
continue
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (fieldDef?.actualField?.includes('.')) {
|
|
189
|
-
continue
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (fieldDef?.oneOf?.some((field) => field.includes('.'))) {
|
|
193
|
-
continue
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (fieldDef?.oneOf && Array.isArray(fieldDef.oneOf)) {
|
|
197
|
-
continue
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (fieldDef?.applyFilter && typeof fieldDef.applyFilter === 'function') {
|
|
201
|
-
continue
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const actualField = fieldDef?.actualField || filterKey
|
|
205
|
-
const fieldInfo = ensureFilterableField(descriptor, filterKey)
|
|
206
|
-
const columnRef = adapter ? adapter.translateColumn(actualField) : fieldInfo.column
|
|
207
|
-
const operator = resolveSearchOperator(fieldDef || fieldInfo.definition)
|
|
208
|
-
const values = normalizeFilterValues(rawValue, fieldInfo.definition, {
|
|
209
|
-
isRelationship: fieldInfo.isRelationship,
|
|
210
|
-
})
|
|
211
|
-
const normalizedValues = values.map((value) => normalizeValue(actualField, value))
|
|
212
|
-
const nonNullValues = normalizedValues.filter((value) => value !== null)
|
|
213
|
-
const hasNull = normalizedValues.length !== nonNullValues.length
|
|
214
|
-
|
|
215
|
-
if (nonNullValues.length === 0 && hasNull) {
|
|
216
|
-
query.whereNull(columnRef)
|
|
217
|
-
continue
|
|
218
|
-
}
|
|
219
|
-
if (nonNullValues.length === 1 && !hasNull) {
|
|
220
|
-
applyWhereForOperator({
|
|
221
|
-
builder: query,
|
|
222
|
-
columnRef,
|
|
223
|
-
operator,
|
|
224
|
-
value: nonNullValues[0],
|
|
225
|
-
knex,
|
|
226
|
-
})
|
|
227
|
-
continue
|
|
228
|
-
}
|
|
229
|
-
if (nonNullValues.length > 1 && !hasNull) {
|
|
230
|
-
query.whereIn(columnRef, nonNullValues)
|
|
231
|
-
continue
|
|
232
|
-
}
|
|
233
|
-
if (nonNullValues.length > 0 && hasNull) {
|
|
234
|
-
query.where(function anyApiNullFilterGroup () {
|
|
235
|
-
this.whereIn(columnRef, nonNullValues).orWhereNull(columnRef)
|
|
236
|
-
})
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
97
|
+
const linkStore = createCanonicalLinkStore({ api, getDescriptor, getScopeStorageAdapter })
|
|
98
|
+
api.anyapi.links = linkStore.methods
|
|
99
|
+
helpers.dataRelatedIdsQuery = linkStore.dataRelatedIdsQuery
|
|
100
|
+
const { buildIncludes, attachReverseRelationships, attachManyToManyRelationships } = createCanonicalRelationshipReader({
|
|
101
|
+
api, knex, getDescriptor, getScopeStorageAdapter, linkStore
|
|
102
|
+
})
|
|
240
103
|
|
|
241
|
-
const
|
|
242
|
-
const effectiveSort = buildEffectiveSortList(sort, {
|
|
243
|
-
defaultSort: scope?.vars?.defaultSort,
|
|
244
|
-
idField: 'id'
|
|
245
|
-
})
|
|
246
|
-
const descriptors = []
|
|
104
|
+
const publishedVersion = scopeName => api.resources[scopeName]?.vars?.schemaInfo?.descriptor?.schemaVersion ?? 0
|
|
247
105
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
descriptors.push({
|
|
255
|
-
field,
|
|
256
|
-
direction,
|
|
257
|
-
definition: queryFieldRuntime.definition,
|
|
258
|
-
isRelationship: false,
|
|
259
|
-
queryFieldRuntime,
|
|
260
|
-
})
|
|
261
|
-
continue
|
|
262
|
-
}
|
|
106
|
+
const composeOptions = (scopeName, descriptor) => composeResourceOptions({
|
|
107
|
+
declaration: declarations.get(scopeName),
|
|
108
|
+
stored: descriptor.definition,
|
|
109
|
+
local: runtimeDeclarations.get(scopeName),
|
|
110
|
+
fromStorage: storageLoaded.has(scopeName)
|
|
111
|
+
})
|
|
263
112
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
113
|
+
const compileOptions = async (scopeName, scopeOptions) => {
|
|
114
|
+
const compiled = { scopeOptions, vars: { idProperty: api.resources[scopeName].vars.idProperty } }
|
|
115
|
+
await compileSchemas(compiled, { context: { scopeName }, runHooks: (name, context) => api.runHooks(name, context) })
|
|
116
|
+
return compiled.vars.schemaInfo
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Publish a descriptor with the schema it implies. Recompile only when the
|
|
120
|
+
// field set changed, so schema hooks do not rerun on every refresh.
|
|
121
|
+
const publish = async (scopeName, descriptor, { compiled } = {}) => {
|
|
122
|
+
const scope = api.resources[scopeName]
|
|
123
|
+
if (publishedVersion(scopeName) > descriptor.schemaVersion) return
|
|
124
|
+
const { options: scopeOptions, hidden, hiddenFilters } = composeOptions(scopeName, descriptor)
|
|
125
|
+
if (hidden.length || hiddenFilters.length) {
|
|
126
|
+
await log.warn(`AnyAPI resource '${scopeName}' hides stored fields whose callbacks this server does not declare`, {
|
|
127
|
+
tenant: tenantId, scopeName, fields: hidden, filters: hiddenFilters
|
|
274
128
|
})
|
|
275
129
|
}
|
|
130
|
+
const signature = optionsSignature(scopeOptions)
|
|
131
|
+
let schemaInfo = scope.vars.schemaInfo
|
|
132
|
+
if (compiled?.signature === signature) schemaInfo = compiled.schemaInfo
|
|
133
|
+
else if (compiledSignatures.get(scopeName) !== signature) schemaInfo = await compileOptions(scopeName, scopeOptions)
|
|
134
|
+
compiledSignatures.set(scopeName, signature)
|
|
276
135
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
definition: { type: 'id' },
|
|
283
|
-
isRelationship: false,
|
|
284
|
-
})
|
|
136
|
+
descriptor.idProperty = schemaInfo.idProperty
|
|
137
|
+
scope.vars.schemaInfo = {
|
|
138
|
+
...schemaInfo,
|
|
139
|
+
descriptor,
|
|
140
|
+
canonicalFieldMap: descriptor.canonicalFieldMap,
|
|
285
141
|
}
|
|
286
|
-
|
|
287
|
-
|
|
142
|
+
getScopeStorageAdapter(scopeName)
|
|
143
|
+
lastChecked.set(scopeName, Date.now())
|
|
288
144
|
}
|
|
289
145
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const scopeVars = scope?.vars || {}
|
|
299
|
-
const defaultLimit = scopeVars.queryDefaultLimit || DEFAULT_QUERY_LIMIT
|
|
300
|
-
const maxLimit = scopeVars.queryMaxLimit || DEFAULT_MAX_QUERY_LIMIT
|
|
301
|
-
|
|
302
|
-
const descriptors = (sortDescriptors && sortDescriptors.length > 0)
|
|
303
|
-
? sortDescriptors
|
|
304
|
-
: [{
|
|
305
|
-
field: 'id',
|
|
306
|
-
column: 'id',
|
|
307
|
-
direction: 'asc',
|
|
308
|
-
definition: { type: 'id' },
|
|
309
|
-
isRelationship: false,
|
|
310
|
-
}]
|
|
311
|
-
|
|
312
|
-
const hasCursorParam = pageParams.after !== undefined || pageParams.before !== undefined
|
|
313
|
-
const hasPageSize = pageParams.size !== undefined
|
|
314
|
-
const hasPageNumber = pageParams.number !== undefined
|
|
315
|
-
|
|
316
|
-
const parseCursorOrThrow = (rawCursor, paramName) => {
|
|
317
|
-
try {
|
|
318
|
-
return parseCursor(rawCursor)
|
|
319
|
-
} catch (error) {
|
|
320
|
-
throw new RestApiValidationError(
|
|
321
|
-
`Invalid cursor format in ${paramName} parameter`,
|
|
322
|
-
{
|
|
323
|
-
fields: [`page.${paramName}`],
|
|
324
|
-
violations: [{
|
|
325
|
-
field: `page.${paramName}`,
|
|
326
|
-
rule: 'invalid_cursor',
|
|
327
|
-
message: error.message,
|
|
328
|
-
}],
|
|
329
|
-
}
|
|
330
|
-
)
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
const applyRawCursorPredicate = (cursorValues, operatorSelector) => {
|
|
335
|
-
applyCursorPredicate(
|
|
336
|
-
query,
|
|
337
|
-
descriptors,
|
|
338
|
-
cursorValues,
|
|
339
|
-
operatorSelector,
|
|
340
|
-
(builder, descriptor, operator, value) => builder.where(
|
|
341
|
-
descriptor.column,
|
|
342
|
-
operator,
|
|
343
|
-
normalizeValueForDatabaseStorage(
|
|
344
|
-
value, descriptor.isRelationship ? null : descriptor.definition?.type,
|
|
345
|
-
{ temporalPrecision: descriptor.definition?.temporalPrecision }
|
|
346
|
-
)
|
|
347
|
-
)
|
|
348
|
-
)
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const ensurePageSize = () => {
|
|
352
|
-
const requestedSize = Number(pageParams.size ?? defaultLimit)
|
|
353
|
-
if (!Number.isFinite(requestedSize) || requestedSize <= 0) {
|
|
354
|
-
throw new RestApiValidationError('Page size must be greater than 0', {
|
|
355
|
-
fields: ['page.size'],
|
|
356
|
-
violations: [{
|
|
357
|
-
field: 'page.size',
|
|
358
|
-
rule: 'min_value',
|
|
359
|
-
message: 'Page size must be a positive number',
|
|
360
|
-
}],
|
|
146
|
+
// Reloads of one resource run in order, so an older read never wins.
|
|
147
|
+
const reload = (scopeName) => {
|
|
148
|
+
const run = (reloads.get(scopeName) || Promise.resolve()).catch(() => {}).then(async () => {
|
|
149
|
+
registry.invalidateDescriptor(tenantId, scopeName)
|
|
150
|
+
const descriptor = await registry.getDescriptor(tenantId, scopeName)
|
|
151
|
+
if (!descriptor) {
|
|
152
|
+
throw new RestApiResourceError(`Resource '${scopeName}' is not registered in AnyAPI storage`, {
|
|
153
|
+
subtype: 'not_found', resourceType: 'scope', resourceId: scopeName
|
|
361
154
|
})
|
|
362
155
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
'page[after] and page[before] cannot be used together',
|
|
370
|
-
{
|
|
371
|
-
fields: ['page.after', 'page.before'],
|
|
372
|
-
violations: [{
|
|
373
|
-
field: 'page.after',
|
|
374
|
-
rule: 'conflict',
|
|
375
|
-
message: 'Provide either page[after] or page[before], not both',
|
|
376
|
-
}],
|
|
377
|
-
}
|
|
378
|
-
)
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
const pageSize = ensurePageSize()
|
|
382
|
-
query.limit(pageSize + 1)
|
|
383
|
-
|
|
384
|
-
if (pageParams.after) {
|
|
385
|
-
const cursorMap = parseCursorOrThrow(pageParams.after, 'after')
|
|
386
|
-
const cursorValues = validateCursorValues(descriptors, cursorMap, 'after')
|
|
387
|
-
applyRawCursorPredicate(cursorValues, (direction) => (direction === 'desc' ? '<' : '>'))
|
|
388
|
-
} else if (pageParams.before) {
|
|
389
|
-
const cursorMap = parseCursorOrThrow(pageParams.before, 'before')
|
|
390
|
-
const cursorValues = validateCursorValues(descriptors, cursorMap, 'before')
|
|
391
|
-
applyRawCursorPredicate(cursorValues, (direction) => (direction === 'desc' ? '>' : '<'))
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
return {
|
|
395
|
-
mode: 'cursor',
|
|
396
|
-
pageSize,
|
|
397
|
-
sortDescriptors: descriptors,
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
if (hasPageSize || hasPageNumber) {
|
|
402
|
-
const pageSize = ensurePageSize()
|
|
403
|
-
const pageNumber = Math.trunc(Number(pageParams.number ?? 1))
|
|
404
|
-
const safePageNumber = Number.isFinite(pageNumber) && pageNumber > 0 ? pageNumber : 1
|
|
405
|
-
const offset = (safePageNumber - 1) * pageSize
|
|
406
|
-
|
|
407
|
-
query.limit(pageSize).offset(offset)
|
|
408
|
-
|
|
409
|
-
return {
|
|
410
|
-
mode: 'offset',
|
|
411
|
-
page: safePageNumber,
|
|
412
|
-
pageSize,
|
|
413
|
-
sortDescriptors: descriptors,
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
query.limit(Math.min(defaultLimit, maxLimit))
|
|
418
|
-
return {
|
|
419
|
-
mode: 'default',
|
|
420
|
-
page: 1,
|
|
421
|
-
pageSize: Math.min(defaultLimit, maxLimit),
|
|
422
|
-
sortDescriptors: descriptors,
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const canonicalizeLinkPair = ({
|
|
427
|
-
tenantId,
|
|
428
|
-
relationshipKey,
|
|
429
|
-
inverseRelationshipKey,
|
|
430
|
-
leftResource,
|
|
431
|
-
leftId,
|
|
432
|
-
rightResource,
|
|
433
|
-
rightId,
|
|
434
|
-
}) => {
|
|
435
|
-
const normalizedLeftId = normalizeId(leftId)
|
|
436
|
-
const normalizedRightId = normalizeId(rightId)
|
|
437
|
-
|
|
438
|
-
if (normalizedLeftId == null || normalizedRightId == null) {
|
|
439
|
-
return null
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
if (inverseRelationshipKey && inverseRelationshipKey < relationshipKey) {
|
|
443
|
-
return {
|
|
444
|
-
tenant_id: tenantId,
|
|
445
|
-
relationship: inverseRelationshipKey,
|
|
446
|
-
inverse_relationship: relationshipKey,
|
|
447
|
-
left_resource: rightResource,
|
|
448
|
-
left_id: normalizedRightId,
|
|
449
|
-
right_resource: leftResource,
|
|
450
|
-
right_id: normalizedLeftId,
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
return {
|
|
455
|
-
tenant_id: tenantId,
|
|
456
|
-
relationship: relationshipKey,
|
|
457
|
-
inverse_relationship: inverseRelationshipKey || null,
|
|
458
|
-
left_resource: leftResource,
|
|
459
|
-
left_id: normalizedLeftId,
|
|
460
|
-
right_resource: rightResource,
|
|
461
|
-
right_id: normalizedRightId,
|
|
462
|
-
}
|
|
156
|
+
await publish(scopeName, descriptor)
|
|
157
|
+
return publishedVersion(scopeName)
|
|
158
|
+
})
|
|
159
|
+
reloads.set(scopeName, run)
|
|
160
|
+
run.finally(() => { if (reloads.get(scopeName) === run) reloads.delete(scopeName) }).catch(() => {})
|
|
161
|
+
return run
|
|
463
162
|
}
|
|
464
163
|
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
relationship: canonicalRow.relationship,
|
|
468
|
-
left_resource: canonicalRow.left_resource,
|
|
469
|
-
left_id: canonicalRow.left_id,
|
|
470
|
-
right_resource: canonicalRow.right_resource,
|
|
471
|
-
right_id: canonicalRow.right_id,
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
const findInverseManyToMany = async ({ descriptor, relInfo }) => {
|
|
475
|
-
if (!relInfo?.target) return null
|
|
476
|
-
|
|
477
|
-
let targetDescriptor
|
|
164
|
+
const notifySchemaChanged = async (scopeName, version) => {
|
|
165
|
+
if (!onSchemaChanged) return
|
|
478
166
|
try {
|
|
479
|
-
|
|
167
|
+
await onSchemaChanged({ tenant: tenantId, resource: scopeName, version })
|
|
480
168
|
} catch (error) {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
)
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
169
|
+
// The change is committed; other servers still catch up through schema checks.
|
|
170
|
+
await log.logError('AnyAPI onSchemaChanged failed', error, { tenant: tenantId, scopeName, version })
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const ensureFresh = (scopeName, maxAge) => {
|
|
175
|
+
if (maxAge > 0 && Date.now() - (lastChecked.get(scopeName) ?? 0) < maxAge) return false
|
|
176
|
+
if (!checks.has(scopeName)) {
|
|
177
|
+
const check = (async () => {
|
|
178
|
+
lastChecked.set(scopeName, Date.now())
|
|
179
|
+
const version = await registry.getSchemaVersion(tenantId, scopeName)
|
|
180
|
+
if (version === null || version <= publishedVersion(scopeName)) return false
|
|
181
|
+
await reload(scopeName)
|
|
182
|
+
return true
|
|
183
|
+
})()
|
|
184
|
+
checks.set(scopeName, check)
|
|
185
|
+
check.finally(() => checks.delete(scopeName)).catch(() => {})
|
|
186
|
+
}
|
|
187
|
+
return checks.get(scopeName)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const watchSchema = (scope) => {
|
|
191
|
+
for (const name of REQUEST_METHODS) {
|
|
192
|
+
const method = scope[name]
|
|
193
|
+
if (typeof method !== 'function') continue
|
|
194
|
+
scope[name] = async function (params = {}, context = {}) {
|
|
195
|
+
// A caller transaction may hold uncommitted metadata; never publish from it.
|
|
196
|
+
if (params?.transaction) return method.call(this, params, context)
|
|
197
|
+
await ensureFresh(scope.name, schemaCheckInterval)
|
|
198
|
+
try {
|
|
199
|
+
return await method.call(this, params, context)
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (!isStaleSchemaRejection(error)) throw error
|
|
202
|
+
let reloaded = false
|
|
203
|
+
try { reloaded = await ensureFresh(scope.name, 0) } catch { throw error }
|
|
204
|
+
if (!reloaded) throw error
|
|
205
|
+
return method.call(this, params, context)
|
|
206
|
+
}
|
|
510
207
|
}
|
|
511
208
|
}
|
|
512
|
-
|
|
513
|
-
return null
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
const getManyToManyInfo = async (scopeName, relName) => {
|
|
517
|
-
const descriptor = await getDescriptor(scopeName)
|
|
518
|
-
const relInfo = descriptor.manyToMany?.[relName]
|
|
519
|
-
if (!relInfo) return null
|
|
520
|
-
const relationshipKey = relInfo.relationship || `${descriptor.tenant}:${descriptor.resource}:${relName}`
|
|
521
|
-
const inverse = await findInverseManyToMany({ descriptor, relInfo })
|
|
522
|
-
const inverseRelationshipKey = inverse?.relationshipKey || null
|
|
523
|
-
return { descriptor, relInfo, relationshipKey, inverseRelationshipKey }
|
|
524
209
|
}
|
|
525
210
|
|
|
526
|
-
const
|
|
527
|
-
|
|
528
|
-
if (
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
.
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
otherKey = row.relationship || null
|
|
562
|
-
} else {
|
|
563
|
-
continue
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
if (relatedId == null) continue
|
|
567
|
-
|
|
568
|
-
const canonical = canonicalizeLinkPair({
|
|
569
|
-
tenantId: descriptor.tenant,
|
|
570
|
-
relationshipKey,
|
|
571
|
-
inverseRelationshipKey: otherKey,
|
|
572
|
-
leftResource: descriptor.resource,
|
|
573
|
-
leftId: ownerId,
|
|
574
|
-
rightResource: relatedResource,
|
|
575
|
-
rightId: relatedId,
|
|
576
|
-
})
|
|
577
|
-
|
|
578
|
-
if (!canonical) continue
|
|
579
|
-
|
|
580
|
-
map.set(relatedId, {
|
|
581
|
-
rowId: row.id,
|
|
582
|
-
relatedResource,
|
|
583
|
-
canonical,
|
|
584
|
-
otherKey,
|
|
585
|
-
})
|
|
211
|
+
const loadResource = async (name, chain = new Set()) => {
|
|
212
|
+
if (typeof name !== 'string' || !name) throw new TypeError('loadResource requires a resource name')
|
|
213
|
+
if (Object.hasOwn(api.resources, name)) return api.resources[name]
|
|
214
|
+
if (chain.has(name)) return null
|
|
215
|
+
if (!loading.has(name)) {
|
|
216
|
+
const load = (async () => {
|
|
217
|
+
const descriptor = await registry.getDescriptor(tenantId, name)
|
|
218
|
+
if (!descriptor) return null
|
|
219
|
+
if (!descriptor.definition?.options) {
|
|
220
|
+
throw new Error(`AnyAPI resource '${name}' has no stored definition; declare it in code or register it again with this version`)
|
|
221
|
+
}
|
|
222
|
+
const { options: declaration } = composeResourceOptions({
|
|
223
|
+
declaration: descriptor.definition.options,
|
|
224
|
+
stored: { callbacks: descriptor.definition.callbacks },
|
|
225
|
+
fromStorage: true
|
|
226
|
+
})
|
|
227
|
+
const { before, after } = relatedResources(declaration)
|
|
228
|
+
const nextChain = new Set([...chain, name])
|
|
229
|
+
for (const target of before) await loadResource(target, nextChain)
|
|
230
|
+
if (Object.hasOwn(api.resources, name)) return api.resources[name]
|
|
231
|
+
storageLoaded.add(name)
|
|
232
|
+
try {
|
|
233
|
+
await api.addResource(name, declaration)
|
|
234
|
+
} catch (error) {
|
|
235
|
+
// A failed registration must not leave a half-published resource behind.
|
|
236
|
+
storageLoaded.delete(name)
|
|
237
|
+
declarations.delete(name)
|
|
238
|
+
delete api.resources[name]
|
|
239
|
+
throw error
|
|
240
|
+
}
|
|
241
|
+
for (const target of after) await loadResource(target, nextChain)
|
|
242
|
+
return api.resources[name]
|
|
243
|
+
})()
|
|
244
|
+
loading.set(name, load)
|
|
245
|
+
load.finally(() => loading.delete(name)).catch(() => {})
|
|
586
246
|
}
|
|
587
|
-
|
|
588
|
-
return map
|
|
247
|
+
return loading.get(name)
|
|
589
248
|
}
|
|
590
249
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
builder
|
|
602
|
-
.where((q) => {
|
|
603
|
-
q.where('relationship', relationshipKey)
|
|
604
|
-
.andWhere('left_resource', descriptor.resource)
|
|
605
|
-
.whereIn('left_id', normalizedIds)
|
|
606
|
-
})
|
|
607
|
-
.orWhere((q) => {
|
|
608
|
-
q.where('inverse_relationship', relationshipKey)
|
|
609
|
-
.andWhere('right_resource', descriptor.resource)
|
|
610
|
-
.whereIn('right_id', normalizedIds)
|
|
611
|
-
})
|
|
612
|
-
})
|
|
613
|
-
.select('relationship', 'inverse_relationship', 'left_resource', 'left_id', 'right_resource', 'right_id')
|
|
614
|
-
|
|
615
|
-
const results = []
|
|
616
|
-
|
|
617
|
-
for (const row of rows) {
|
|
618
|
-
let parentId
|
|
619
|
-
let childId
|
|
620
|
-
let childType
|
|
621
|
-
|
|
622
|
-
if (row.relationship === relationshipKey && row.left_resource === descriptor.resource) {
|
|
623
|
-
parentId = normalizeId(row.left_id)
|
|
624
|
-
childId = normalizeId(row.right_id)
|
|
625
|
-
childType = row.right_resource
|
|
626
|
-
} else if (row.inverse_relationship === relationshipKey && row.right_resource === descriptor.resource) {
|
|
627
|
-
parentId = normalizeId(row.right_id)
|
|
628
|
-
childId = normalizeId(row.left_id)
|
|
629
|
-
childType = row.left_resource
|
|
630
|
-
} else {
|
|
631
|
-
continue
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
if (parentId == null || childId == null) continue
|
|
635
|
-
|
|
636
|
-
results.push({ parentId, childId, childType })
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
return results
|
|
250
|
+
api.anyapi.loadResource = name => loadResource(name)
|
|
251
|
+
api.anyapi.loadResources = async () => {
|
|
252
|
+
for (const { resource } of await registry.listResources(tenantId)) await loadResource(resource)
|
|
253
|
+
return api.resources
|
|
254
|
+
}
|
|
255
|
+
api.anyapi.handleSchemaChange = async ({ tenant, resource, version } = {}) => {
|
|
256
|
+
if ((tenant !== undefined && tenant !== tenantId) || typeof resource !== 'string' || !Object.hasOwn(api.resources, resource)) return false
|
|
257
|
+
if (version !== undefined && publishedVersion(resource) >= version) return false
|
|
258
|
+
await reload(resource)
|
|
259
|
+
return true
|
|
640
260
|
}
|
|
641
261
|
|
|
642
|
-
const
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
filters,
|
|
648
|
-
queryPurpose = 'include'
|
|
649
|
-
}) => {
|
|
650
|
-
const targetScope = api.resources?.[resourceName]
|
|
651
|
-
if (!targetScope?.applyQueryFilters) {
|
|
652
|
-
return { query }
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
const storageAdapter = getScopeStorageAdapter(resourceName)
|
|
656
|
-
const queryState = await targetScope.applyQueryFilters({
|
|
657
|
-
query,
|
|
658
|
-
filters,
|
|
659
|
-
schemaInfo: targetScope.vars?.schemaInfo,
|
|
660
|
-
scopeName: resourceName,
|
|
661
|
-
tableName,
|
|
662
|
-
db: context.db || context.transaction || api.knex.instance,
|
|
663
|
-
isAnyApi: true,
|
|
664
|
-
queryPurpose,
|
|
665
|
-
storageAdapter
|
|
666
|
-
}, {
|
|
667
|
-
...context,
|
|
668
|
-
scopeName: resourceName,
|
|
669
|
-
schemaInfo: targetScope.vars?.schemaInfo,
|
|
670
|
-
storageAdapter
|
|
671
|
-
})
|
|
672
|
-
|
|
262
|
+
const buildSearchMembershipQuery = ({ sourceScopeName, relationshipName, targetScopeName, foreignKey, otherKey, db }) => {
|
|
263
|
+
const info = linkStore.getManyToManyInfo(sourceScopeName, relationshipName)
|
|
264
|
+
if (!info) throw new RestApiResourceError(`Relationship '${sourceScopeName}.${relationshipName}' not found`, { subtype: 'related_type_not_found' })
|
|
265
|
+
const links = linkStore.buildRelatedLinkQuery({ ...info, db, allParents: true })
|
|
266
|
+
const pivotAdapter = getScopeStorageAdapter(targetScopeName)
|
|
673
267
|
return {
|
|
674
|
-
query:
|
|
268
|
+
query: db.select({
|
|
269
|
+
[pivotAdapter.translateColumn(foreignKey)]: 'parentId',
|
|
270
|
+
[pivotAdapter.translateColumn(otherKey)]: 'childId'
|
|
271
|
+
}).from(links.as('search_membership'))
|
|
675
272
|
}
|
|
676
273
|
}
|
|
677
274
|
|
|
678
|
-
const
|
|
679
|
-
if (!identifiers || identifiers.length === 0) return []
|
|
680
|
-
|
|
681
|
-
const db = context.db || context.transaction || api.knex.instance
|
|
682
|
-
const identifiersByType = new Map()
|
|
683
|
-
|
|
684
|
-
for (const identifier of identifiers) {
|
|
685
|
-
if (!identifier?.type || identifier.id === undefined || identifier.id === null) continue
|
|
686
|
-
if (!identifiersByType.has(identifier.type)) {
|
|
687
|
-
identifiersByType.set(identifier.type, new Set())
|
|
688
|
-
}
|
|
689
|
-
identifiersByType.get(identifier.type).add(normalizeId(identifier.id))
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
const visibleKeys = new Set()
|
|
693
|
-
|
|
694
|
-
for (const [resourceName, idSet] of identifiersByType.entries()) {
|
|
695
|
-
const descriptor = await getDescriptor(resourceName)
|
|
696
|
-
const idColumn = getLogicalResourceIdColumn(descriptor)
|
|
697
|
-
let query = db(descriptor.canonical.tableName)
|
|
698
|
-
.where(descriptor.canonical.tenantColumn, descriptor.tenant)
|
|
699
|
-
.where(descriptor.canonical.resourceColumn, descriptor.resource)
|
|
700
|
-
.whereIn(idColumn, [...idSet])
|
|
701
|
-
.select(idColumn)
|
|
702
|
-
|
|
703
|
-
const scopedQueryState = await applyTargetScopeFilters({
|
|
704
|
-
query,
|
|
705
|
-
resourceName,
|
|
706
|
-
tableName: descriptor.canonical.tableName,
|
|
707
|
-
context,
|
|
708
|
-
queryPurpose: 'relationship-identifiers'
|
|
709
|
-
})
|
|
710
|
-
query = scopedQueryState.query
|
|
711
|
-
|
|
712
|
-
const rows = await query
|
|
713
|
-
for (const row of rows) {
|
|
714
|
-
visibleKeys.add(`${resourceName}:${String(getLogicalResourceId(row, descriptor))}`)
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
return identifiers.filter((identifier) => (
|
|
719
|
-
visibleKeys.has(`${identifier.type}:${String(identifier.id)}`)
|
|
720
|
-
))
|
|
721
|
-
}
|
|
275
|
+
const queryHookDependencies = { log, scopes: api.resources, knex, getStorageAdapter: getScopeStorageAdapter, buildSearchMembershipQuery }
|
|
722
276
|
|
|
723
|
-
const
|
|
724
|
-
const
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
throw new Error(`Target resource '${relInfo.target}' not found`)
|
|
729
|
-
}
|
|
730
|
-
for (const identifier of relArray) {
|
|
731
|
-
if (identifier?.id === undefined || identifier.id === null) {
|
|
732
|
-
throw new Error('Relationship data requires resource identifier with id')
|
|
733
|
-
}
|
|
734
|
-
if (identifier.type && identifier.type !== relInfo.target) {
|
|
735
|
-
throw new Error(`Relationship expects type '${relInfo.target}' but received '${identifier.type}'`)
|
|
736
|
-
}
|
|
737
|
-
await targetScope.get({
|
|
738
|
-
id: identifier.id,
|
|
739
|
-
transaction: context.transaction,
|
|
740
|
-
simplified: false
|
|
741
|
-
}, { ...context })
|
|
742
|
-
}
|
|
277
|
+
const applyBuiltInAnyApiQueryFilters = async (context) => {
|
|
278
|
+
const hookParams = { context }
|
|
279
|
+
await polymorphicFiltersHook(hookParams, queryHookDependencies)
|
|
280
|
+
await crossTableFiltersHook(hookParams, queryHookDependencies)
|
|
281
|
+
await basicFiltersHook(hookParams, queryHookDependencies)
|
|
743
282
|
}
|
|
744
283
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
inverseRelationshipKey,
|
|
757
|
-
leftResource: descriptor.resource,
|
|
758
|
-
leftId,
|
|
759
|
-
rightResource: relInfo.target,
|
|
760
|
-
rightId,
|
|
761
|
-
})
|
|
762
|
-
|
|
763
|
-
if (!canonical) continue
|
|
284
|
+
Object.assign(helpers, createCanonicalDataHelpers({
|
|
285
|
+
api,
|
|
286
|
+
knex,
|
|
287
|
+
getScopeStorageAdapter,
|
|
288
|
+
getDescriptor,
|
|
289
|
+
linkStore,
|
|
290
|
+
buildIncludes,
|
|
291
|
+
attachReverseRelationships,
|
|
292
|
+
attachManyToManyRelationships,
|
|
293
|
+
applyBuiltInAnyApiQueryFilters
|
|
294
|
+
}))
|
|
764
295
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
updated_at: db.fn.now(),
|
|
788
|
-
})
|
|
296
|
+
addHook('resource:added', 'anyapi-register-resource', {}, async ({ context }) => {
|
|
297
|
+
const { scopeName, scopeOptions = {} } = context
|
|
298
|
+
const scope = api.resources[scopeName]
|
|
299
|
+
const declaration = snapshotResourceConfiguration(scopeOptions)
|
|
300
|
+
declarations.set(scopeName, declaration)
|
|
301
|
+
compiledSignatures.set(scopeName, optionsSignature(declaration))
|
|
302
|
+
let descriptor
|
|
303
|
+
let changed = false
|
|
304
|
+
if (storageLoaded.has(scopeName)) {
|
|
305
|
+
descriptor = await registry.getDescriptor(tenantId, scopeName)
|
|
306
|
+
if (!descriptor) throw new Error(`Descriptor not found for resource '${scopeName}'`)
|
|
307
|
+
} else {
|
|
308
|
+
const { schemaStructure, computed, schemaRelationships, idProperty } = scope.vars.schemaInfo
|
|
309
|
+
;({ descriptor, changed } = await registry.register({
|
|
310
|
+
tenant: tenantId,
|
|
311
|
+
resource: scopeName,
|
|
312
|
+
schema: { ...schemaStructure, ...computed },
|
|
313
|
+
relationships: schemaRelationships,
|
|
314
|
+
canonicalFieldMap: scopeOptions.canonicalFieldsMap || null,
|
|
315
|
+
idProperty,
|
|
316
|
+
declaration
|
|
317
|
+
}))
|
|
789
318
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
const info = await getManyToManyInfo(scopeName, relName)
|
|
795
|
-
if (!info) {
|
|
796
|
-
throw new Error(`Many-to-many relationship '${relName}' not found on '${scopeName}'`)
|
|
797
|
-
}
|
|
798
|
-
const db = context.transaction || context.db || api.knex.instance
|
|
799
|
-
const leftId = normalizeId(context.id)
|
|
800
|
-
await ensureTargetsExist({ relInfo: info.relInfo, relData, context })
|
|
801
|
-
await attachLinks({
|
|
802
|
-
descriptor: info.descriptor,
|
|
803
|
-
relInfo: info.relInfo,
|
|
804
|
-
relationshipKey: info.relationshipKey,
|
|
805
|
-
inverseRelationshipKey: info.inverseRelationshipKey,
|
|
806
|
-
leftId,
|
|
807
|
-
relData,
|
|
808
|
-
db,
|
|
809
|
-
})
|
|
810
|
-
},
|
|
811
|
-
syncMany: async ({ context, scopeName, relName, relDef, relData, isUpdate }) => {
|
|
812
|
-
const info = await getManyToManyInfo(scopeName, relName)
|
|
813
|
-
if (!info) {
|
|
814
|
-
throw new Error(`Many-to-many relationship '${relName}' not found on '${scopeName}'`)
|
|
815
|
-
}
|
|
816
|
-
const db = context.transaction || context.db || api.knex.instance
|
|
817
|
-
const leftId = normalizeId(context.id)
|
|
818
|
-
await ensureTargetsExist({ relInfo: info.relInfo, relData, context })
|
|
819
|
-
if (isUpdate) {
|
|
820
|
-
await syncLinks({
|
|
821
|
-
descriptor: info.descriptor,
|
|
822
|
-
relInfo: info.relInfo,
|
|
823
|
-
relationshipKey: info.relationshipKey,
|
|
824
|
-
inverseRelationshipKey: info.inverseRelationshipKey,
|
|
825
|
-
leftId,
|
|
826
|
-
relData,
|
|
827
|
-
db,
|
|
828
|
-
})
|
|
829
|
-
} else {
|
|
830
|
-
await attachLinks({
|
|
831
|
-
descriptor: info.descriptor,
|
|
832
|
-
relInfo: info.relInfo,
|
|
833
|
-
relationshipKey: info.relationshipKey,
|
|
834
|
-
inverseRelationshipKey: info.inverseRelationshipKey,
|
|
835
|
-
leftId,
|
|
836
|
-
relData,
|
|
837
|
-
db,
|
|
838
|
-
})
|
|
839
|
-
}
|
|
840
|
-
},
|
|
841
|
-
removeMany: async ({ context, scopeName, relName, relData }) => {
|
|
842
|
-
const info = await getManyToManyInfo(scopeName, relName)
|
|
843
|
-
if (!info) {
|
|
844
|
-
throw new Error(`Many-to-many relationship '${relName}' not found on '${scopeName}'`)
|
|
845
|
-
}
|
|
846
|
-
const db = context.transaction || context.db || api.knex.instance
|
|
847
|
-
const leftId = normalizeId(context.id)
|
|
848
|
-
await removeLinks({
|
|
849
|
-
descriptor: info.descriptor,
|
|
850
|
-
relInfo: info.relInfo,
|
|
851
|
-
relationshipKey: info.relationshipKey,
|
|
852
|
-
inverseRelationshipKey: info.inverseRelationshipKey,
|
|
853
|
-
leftId,
|
|
854
|
-
relData,
|
|
855
|
-
db,
|
|
856
|
-
})
|
|
857
|
-
},
|
|
858
|
-
listMany: async ({ context, scopeName, relName }) => {
|
|
859
|
-
const info = await getManyToManyInfo(scopeName, relName)
|
|
860
|
-
if (!info) {
|
|
861
|
-
throw new Error(`Many-to-many relationship '${relName}' not found on '${scopeName}'`)
|
|
862
|
-
}
|
|
863
|
-
const db = context.transaction || context.db || api.knex.instance
|
|
864
|
-
const leftId = normalizeId(context.id)
|
|
865
|
-
const identifiers = await listLinks({
|
|
866
|
-
descriptor: info.descriptor,
|
|
867
|
-
relInfo: info.relInfo,
|
|
868
|
-
relationshipKey: info.relationshipKey,
|
|
869
|
-
inverseRelationshipKey: info.inverseRelationshipKey,
|
|
870
|
-
leftId,
|
|
871
|
-
db,
|
|
872
|
-
})
|
|
873
|
-
return filterVisibleIdentifiers({
|
|
874
|
-
identifiers,
|
|
875
|
-
context
|
|
876
|
-
})
|
|
877
|
-
},
|
|
878
|
-
fetchManyToManyRows: async ({ scopeName, relName, parentIds, context }) => {
|
|
879
|
-
const info = await getManyToManyInfo(scopeName, relName)
|
|
880
|
-
if (!info) return []
|
|
881
|
-
const db = context.db || context.transaction || api.knex.instance
|
|
882
|
-
return fetchLinksForParents({
|
|
883
|
-
descriptor: info.descriptor,
|
|
884
|
-
relationshipKey: info.relationshipKey,
|
|
885
|
-
parentIds,
|
|
886
|
-
db,
|
|
887
|
-
})
|
|
888
|
-
},
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
const syncLinks = async ({ descriptor, relInfo, relationshipKey, inverseRelationshipKey, leftId, relData, db }) => {
|
|
892
|
-
const relArray = Array.isArray(relData) ? relData : []
|
|
893
|
-
const existingMap = await loadLinkRowsForResource({
|
|
894
|
-
descriptor,
|
|
895
|
-
relationshipKey,
|
|
896
|
-
resourceId: leftId,
|
|
897
|
-
db,
|
|
898
|
-
})
|
|
899
|
-
|
|
900
|
-
const desired = new Set(
|
|
901
|
-
relArray
|
|
902
|
-
.map((identifier) => normalizeId(identifier.id))
|
|
903
|
-
.filter((id) => id !== null)
|
|
904
|
-
)
|
|
319
|
+
await publish(scopeName, descriptor)
|
|
320
|
+
if (schemaCheckInterval !== undefined) watchSchema(scope)
|
|
321
|
+
if (changed) await notifySchemaChanged(scopeName, descriptor.schemaVersion)
|
|
322
|
+
})
|
|
905
323
|
|
|
906
|
-
|
|
324
|
+
addResourceMethod('createKnexTable', async ({ scopeName }) => {
|
|
325
|
+
if (ensureSchema) await ensureAnyApiSchema(knex, { slots })
|
|
326
|
+
const descriptor = await registry.getDescriptor(tenantId, scopeName)
|
|
327
|
+
if (!descriptor) throw new Error(`Descriptor not found for resource '${scopeName}'`)
|
|
328
|
+
await publish(scopeName, descriptor)
|
|
329
|
+
})
|
|
907
330
|
|
|
908
|
-
|
|
909
|
-
const toRemove = [...existingIds].filter((id) => !desired.has(id))
|
|
331
|
+
addResourceMethod('reloadSchema', async ({ scopeName }) => reload(scopeName))
|
|
910
332
|
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
descriptor,
|
|
915
|
-
relInfo,
|
|
916
|
-
relationshipKey,
|
|
917
|
-
inverseRelationshipKey,
|
|
918
|
-
leftId,
|
|
919
|
-
relData: data,
|
|
920
|
-
db,
|
|
921
|
-
})
|
|
333
|
+
addResourceMethod('addKnexFields', async ({ scopeName, params }) => {
|
|
334
|
+
if (!params?.fields || typeof params.fields !== 'object' || Array.isArray(params.fields)) {
|
|
335
|
+
throw new Error('fields parameter is required for addKnexFields')
|
|
922
336
|
}
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
.delete()
|
|
931
|
-
}
|
|
337
|
+
assertFieldNameMap(params.fields, `added fields in '${scopeName}'`)
|
|
338
|
+
assertFieldNameMap(params.searchSchema, `added search schema in '${scopeName}'`)
|
|
339
|
+
assertFieldNameMap(params.canonicalFieldsMap, `added canonical fields in '${scopeName}'`)
|
|
340
|
+
const scope = api.resources[scopeName]
|
|
341
|
+
const unknown = Object.keys(params.canonicalFieldsMap || {}).filter(field => !Object.hasOwn(params.fields, field))
|
|
342
|
+
if (unknown.length) {
|
|
343
|
+
throw new Error(`canonicalFieldsMap contains unknown fields for addKnexFields: ${unknown}`)
|
|
932
344
|
}
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
descriptor,
|
|
941
|
-
relationshipKey,
|
|
942
|
-
resourceId: leftId,
|
|
943
|
-
db,
|
|
345
|
+
const fields = snapshotResourceConfiguration(params.fields)
|
|
346
|
+
const searchSchema = snapshotResourceConfiguration(params.searchSchema || {})
|
|
347
|
+
const current = composeOptions(scopeName, scope.vars.schemaInfo.descriptor).options
|
|
348
|
+
const candidate = snapshotResourceConfiguration({
|
|
349
|
+
...current,
|
|
350
|
+
schema: { ...current.schema, ...fields },
|
|
351
|
+
searchSchema: { ...current.searchSchema, ...searchSchema }
|
|
944
352
|
})
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
inverseRelationshipKey,
|
|
962
|
-
leftResource: descriptor.resource,
|
|
963
|
-
leftId,
|
|
964
|
-
rightResource: relInfo.target,
|
|
965
|
-
rightId: relatedId,
|
|
966
|
-
})
|
|
967
|
-
|
|
968
|
-
if (!canonical) continue
|
|
969
|
-
|
|
970
|
-
await db(LINKS_TABLE)
|
|
971
|
-
.where(buildLinkIdentity(canonical))
|
|
972
|
-
.delete()
|
|
973
|
-
}
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
const listLinks = async ({ descriptor, relInfo, relationshipKey, leftId, db }) => {
|
|
977
|
-
const existingMap = await loadLinkRowsForResource({
|
|
978
|
-
descriptor,
|
|
979
|
-
relationshipKey,
|
|
980
|
-
resourceId: leftId,
|
|
981
|
-
db,
|
|
353
|
+
const schemaInfo = await compileOptions(scopeName, candidate)
|
|
354
|
+
await api.transaction(transaction => registry.allocateFields({
|
|
355
|
+
tenant: tenantId,
|
|
356
|
+
resource: scopeName,
|
|
357
|
+
fields: Object.fromEntries(Object.keys(fields).map(fieldName => [fieldName, {
|
|
358
|
+
definition: schemaInfo.schemaStructure[fieldName] || schemaInfo.computed[fieldName],
|
|
359
|
+
declaration: fields[fieldName],
|
|
360
|
+
canonicalField: params.canonicalFieldsMap?.[fieldName]
|
|
361
|
+
}])),
|
|
362
|
+
searchSchema
|
|
363
|
+
}, { transaction }))
|
|
364
|
+
registry.invalidateDescriptor(tenantId, scopeName)
|
|
365
|
+
const local = runtimeDeclarations.get(scopeName) || {}
|
|
366
|
+
runtimeDeclarations.set(scopeName, {
|
|
367
|
+
schema: { ...local.schema, ...fields },
|
|
368
|
+
searchSchema: { ...local.searchSchema, ...searchSchema }
|
|
982
369
|
})
|
|
983
|
-
|
|
984
|
-
return [...existingMap.entries()].map(([relatedId, entry]) => ({
|
|
985
|
-
type: entry.relatedResource || relInfo.target,
|
|
986
|
-
id: relatedId,
|
|
987
|
-
}))
|
|
988
|
-
}
|
|
989
|
-
|
|
990
|
-
helpers.dataExists = async ({ scopeName, context }) => {
|
|
991
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
992
|
-
if (storageAdapter) {
|
|
993
|
-
context.storageAdapter = storageAdapter
|
|
994
|
-
}
|
|
995
|
-
const descriptor = await getDescriptor(scopeName)
|
|
996
|
-
const { canonical } = descriptor
|
|
997
|
-
const id = context.id
|
|
998
|
-
const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
|
|
999
|
-
|
|
1000
|
-
const row = await context.db(canonical.tableName)
|
|
1001
|
-
.select('id')
|
|
1002
|
-
.where(canonical.tenantColumn, descriptor.tenant)
|
|
1003
|
-
.where(canonical.resourceColumn, descriptor.resource)
|
|
1004
|
-
.where(logicalIdColumn, id)
|
|
1005
|
-
.first()
|
|
1006
|
-
|
|
1007
|
-
return !!row
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
helpers.dataPost = async ({ scopeName, context }) => {
|
|
1011
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
1012
|
-
if (storageAdapter) {
|
|
1013
|
-
context.storageAdapter = storageAdapter
|
|
1014
|
-
}
|
|
1015
|
-
const descriptor = await getDescriptor(scopeName)
|
|
1016
|
-
const { canonical } = descriptor
|
|
1017
|
-
const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
|
|
1018
|
-
const explicitId = context.inputRecord?.data?.id
|
|
1019
|
-
const attributes = context.inputRecord?.data?.attributes || {}
|
|
1020
|
-
const row = storageAdapter?.toStorageRow
|
|
1021
|
-
? storageAdapter.toStorageRow(attributes)
|
|
1022
|
-
: translateCanonicalAttributesForStorage(attributes, descriptor)
|
|
1023
|
-
|
|
1024
|
-
if (explicitId !== undefined && explicitId !== null) {
|
|
1025
|
-
row[logicalIdColumn] = explicitId
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
|
-
row[canonical.tenantColumn] = descriptor.tenant
|
|
1029
|
-
row[canonical.resourceColumn] = descriptor.resource
|
|
1030
|
-
|
|
1031
|
-
const result = await context.db(canonical.tableName)
|
|
1032
|
-
.insert(row)
|
|
1033
|
-
.returning('id')
|
|
1034
|
-
|
|
1035
|
-
const inserted = Array.isArray(result) ? result[0] : result
|
|
1036
|
-
const insertedRowId = inserted && typeof inserted === 'object' && 'id' in inserted
|
|
1037
|
-
? inserted.id
|
|
1038
|
-
: inserted
|
|
1039
|
-
|
|
1040
|
-
if (explicitId !== undefined && explicitId !== null) {
|
|
1041
|
-
return explicitId
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
|
-
if (insertedRowId === undefined || insertedRowId === null) {
|
|
1045
|
-
return insertedRowId
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
const generatedLogicalId = String(insertedRowId)
|
|
1049
|
-
|
|
1050
|
-
await context.db(canonical.tableName)
|
|
1051
|
-
.where({ id: insertedRowId })
|
|
1052
|
-
.update({ [logicalIdColumn]: generatedLogicalId })
|
|
1053
|
-
|
|
1054
|
-
return generatedLogicalId
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
helpers.dataPut = async ({ scopeName, context }) => {
|
|
1058
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
1059
|
-
if (storageAdapter) {
|
|
1060
|
-
context.storageAdapter = storageAdapter
|
|
1061
|
-
}
|
|
1062
|
-
const descriptor = await getDescriptor(scopeName)
|
|
1063
|
-
const { canonical } = descriptor
|
|
1064
|
-
const id = context.id
|
|
1065
|
-
const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
|
|
1066
|
-
const attributes = context.inputRecord?.data?.attributes || {}
|
|
1067
|
-
const row = storageAdapter?.toStorageRow
|
|
1068
|
-
? storageAdapter.toStorageRow(attributes)
|
|
1069
|
-
: translateCanonicalAttributesForStorage(attributes, descriptor)
|
|
1070
|
-
|
|
1071
|
-
row[canonical.resourceColumn] = descriptor.resource
|
|
1072
|
-
row[canonical.tenantColumn] = descriptor.tenant
|
|
1073
|
-
|
|
1074
|
-
const writeRow = Object.fromEntries(
|
|
1075
|
-
Object.entries(row).filter(([, value]) => value !== undefined)
|
|
1076
|
-
)
|
|
1077
|
-
|
|
1078
|
-
if (context.isCreate) {
|
|
1079
|
-
await context.db(canonical.tableName).insert({
|
|
1080
|
-
...writeRow,
|
|
1081
|
-
[logicalIdColumn]: id,
|
|
1082
|
-
})
|
|
1083
|
-
return 1
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
if (Object.keys(writeRow).length === 0) {
|
|
1087
|
-
return 0
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
const result = await context.db(canonical.tableName)
|
|
1091
|
-
.where(logicalIdColumn, id)
|
|
1092
|
-
.where(canonical.resourceColumn, descriptor.resource)
|
|
1093
|
-
.where(canonical.tenantColumn, descriptor.tenant)
|
|
1094
|
-
.update(writeRow)
|
|
1095
|
-
|
|
1096
|
-
return result
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
helpers.dataPatch = async ({ scopeName, context }) => {
|
|
1100
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
1101
|
-
if (storageAdapter) {
|
|
1102
|
-
context.storageAdapter = storageAdapter
|
|
1103
|
-
}
|
|
1104
|
-
const descriptor = await getDescriptor(scopeName)
|
|
1105
|
-
const { canonical } = descriptor
|
|
1106
|
-
const id = context.id
|
|
1107
|
-
const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
|
|
1108
|
-
const attributes = context.inputRecord?.data?.attributes || {}
|
|
1109
|
-
const row = storageAdapter?.toStorageRow
|
|
1110
|
-
? storageAdapter.toStorageRow(attributes)
|
|
1111
|
-
: translateCanonicalAttributesForStorage(attributes, descriptor)
|
|
1112
|
-
|
|
1113
|
-
const updateRow = Object.fromEntries(
|
|
1114
|
-
Object.entries(row).filter(([, value]) => value !== undefined)
|
|
1115
|
-
)
|
|
1116
|
-
|
|
1117
|
-
if (Object.keys(updateRow).length === 0) {
|
|
1118
|
-
return 0
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
const result = await context.db(canonical.tableName)
|
|
1122
|
-
.where(logicalIdColumn, id)
|
|
1123
|
-
.where(canonical.resourceColumn, descriptor.resource)
|
|
1124
|
-
.where(canonical.tenantColumn, descriptor.tenant)
|
|
1125
|
-
.update(updateRow)
|
|
1126
|
-
|
|
1127
|
-
return result
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
helpers.dataDelete = async ({ scopeName, context }) => {
|
|
1131
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
1132
|
-
if (storageAdapter) {
|
|
1133
|
-
context.storageAdapter = storageAdapter
|
|
1134
|
-
}
|
|
1135
|
-
const descriptor = await getDescriptor(scopeName)
|
|
1136
|
-
const { canonical } = descriptor
|
|
1137
|
-
const id = context.id
|
|
1138
|
-
const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
|
|
1139
|
-
|
|
1140
|
-
const result = await context.db(canonical.tableName)
|
|
1141
|
-
.where(logicalIdColumn, id)
|
|
1142
|
-
.where(canonical.resourceColumn, descriptor.resource)
|
|
1143
|
-
.where(canonical.tenantColumn, descriptor.tenant)
|
|
1144
|
-
.delete()
|
|
1145
|
-
|
|
1146
|
-
return result
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
helpers.dataGetMinimal = async ({
|
|
1150
|
-
scopeName,
|
|
1151
|
-
context,
|
|
1152
|
-
runHooks,
|
|
1153
|
-
applyQueryFilters,
|
|
1154
|
-
filters = context.queryParams?.filters,
|
|
1155
|
-
queryPurpose = 'single'
|
|
1156
|
-
}) => {
|
|
1157
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
1158
|
-
if (storageAdapter) {
|
|
1159
|
-
context.storageAdapter = storageAdapter
|
|
1160
|
-
}
|
|
1161
|
-
const descriptor = await getDescriptor(scopeName)
|
|
1162
|
-
const { canonical } = descriptor
|
|
1163
|
-
const id = context.id
|
|
1164
|
-
const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
|
|
1165
|
-
const scope = api.resources?.[scopeName]
|
|
1166
|
-
|
|
1167
|
-
let query = context.db(canonical.tableName)
|
|
1168
|
-
.where(logicalIdColumn, id)
|
|
1169
|
-
.where(canonical.resourceColumn, descriptor.resource)
|
|
1170
|
-
.where(canonical.tenantColumn, descriptor.tenant)
|
|
1171
|
-
|
|
1172
|
-
if (typeof applyQueryFilters === 'function') {
|
|
1173
|
-
const scopedQueryState = await applyQueryFilters({
|
|
1174
|
-
query,
|
|
1175
|
-
filters,
|
|
1176
|
-
tableName: canonical.tableName,
|
|
1177
|
-
db: context.db,
|
|
1178
|
-
scopeName,
|
|
1179
|
-
queryPurpose,
|
|
1180
|
-
storageAdapter,
|
|
1181
|
-
isAnyApi: true
|
|
1182
|
-
})
|
|
1183
|
-
query = unwrapQueryBuilderState(scopedQueryState, query)
|
|
1184
|
-
} else if (runHooks) {
|
|
1185
|
-
const previousKnexQuery = context.knexQuery
|
|
1186
|
-
context.knexQuery = {
|
|
1187
|
-
query,
|
|
1188
|
-
filters,
|
|
1189
|
-
schemaInfo: context.schemaInfo,
|
|
1190
|
-
scopeName,
|
|
1191
|
-
tableName: canonical.tableName,
|
|
1192
|
-
db: context.db,
|
|
1193
|
-
queryPurpose,
|
|
1194
|
-
isAnyApi: true,
|
|
1195
|
-
adapter: storageAdapter,
|
|
1196
|
-
storageAdapter,
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
try {
|
|
1200
|
-
await applyBuiltInAnyApiQueryFilters(context)
|
|
1201
|
-
await runHooks('knexQueryFiltering')
|
|
1202
|
-
query = context.knexQuery?.query || query
|
|
1203
|
-
} finally {
|
|
1204
|
-
if (previousKnexQuery === undefined) {
|
|
1205
|
-
delete context.knexQuery
|
|
1206
|
-
} else {
|
|
1207
|
-
context.knexQuery = previousKnexQuery
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
|
|
1212
|
-
const row = await query.first()
|
|
1213
|
-
|
|
1214
|
-
if (!row) return null
|
|
1215
|
-
const translated = translateCanonicalRecordFromStorage(
|
|
1216
|
-
row,
|
|
1217
|
-
descriptor,
|
|
1218
|
-
{ allowedExtraFields: getAllowedQueryFieldNames(scopeName) }
|
|
1219
|
-
)
|
|
1220
|
-
const resourceId = getLogicalResourceId(row, descriptor)
|
|
1221
|
-
const minimal = {
|
|
1222
|
-
type: descriptor.resource,
|
|
1223
|
-
id: resourceId,
|
|
1224
|
-
attributes: translated.attributes,
|
|
1225
|
-
relationships: translated.relationships,
|
|
1226
|
-
}
|
|
1227
|
-
if (scope) {
|
|
1228
|
-
decorateResourceLinks({ resource: minimal, scope, scopeName, context })
|
|
1229
|
-
}
|
|
1230
|
-
return minimal
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
const parseIncludeTree = (includeParam) => {
|
|
1234
|
-
const tree = {}
|
|
1235
|
-
if (!includeParam) return tree
|
|
1236
|
-
|
|
1237
|
-
const pushPath = (raw) => {
|
|
1238
|
-
if (!raw) return
|
|
1239
|
-
const segments = raw.split('.').map((segment) => segment.trim()).filter(Boolean)
|
|
1240
|
-
if (segments.length === 0) return
|
|
1241
|
-
let node = tree
|
|
1242
|
-
for (const segment of segments) {
|
|
1243
|
-
node[segment] = node[segment] || {}
|
|
1244
|
-
node = node[segment]
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
if (Array.isArray(includeParam)) {
|
|
1249
|
-
for (const entry of includeParam) {
|
|
1250
|
-
String(entry).split(',').forEach((part) => pushPath(part.trim()))
|
|
1251
|
-
}
|
|
1252
|
-
} else {
|
|
1253
|
-
String(includeParam).split(',').forEach((part) => pushPath(part.trim()))
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
return tree
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
const applyIncludeFieldSelection = async ({ query, resourceName, tableName, context }) => {
|
|
1260
|
-
const targetScope = api.resources?.[resourceName]
|
|
1261
|
-
if (!targetScope) {
|
|
1262
|
-
return { query, fieldSelectionInfo: null }
|
|
1263
|
-
}
|
|
1264
|
-
|
|
1265
|
-
const storageAdapter = getScopeStorageAdapter(resourceName)
|
|
1266
|
-
const fieldSelectionInfo = await buildFieldSelection(targetScope, {
|
|
1267
|
-
context: {
|
|
1268
|
-
...context,
|
|
1269
|
-
scopeName: resourceName,
|
|
1270
|
-
schemaInfo: targetScope.vars.schemaInfo,
|
|
1271
|
-
storageAdapter,
|
|
1272
|
-
}
|
|
1273
|
-
})
|
|
1274
|
-
|
|
1275
|
-
const selectionState = await applyFieldSelectionToQuery({
|
|
1276
|
-
query,
|
|
1277
|
-
scope: targetScope,
|
|
1278
|
-
fieldSelectionInfo,
|
|
1279
|
-
tableName,
|
|
1280
|
-
useTablePrefix: false,
|
|
1281
|
-
storageAdapter,
|
|
1282
|
-
db: context.db || context.transaction || api.knex.instance,
|
|
1283
|
-
context,
|
|
1284
|
-
scopeName: resourceName
|
|
1285
|
-
})
|
|
1286
|
-
|
|
1287
|
-
return {
|
|
1288
|
-
query: selectionState.query,
|
|
1289
|
-
fieldSelectionInfo
|
|
1290
|
-
}
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
const buildIncludedResource = ({ row, descriptor, context, fieldSelectionInfo = null }) => {
|
|
1294
|
-
const translated = translateCanonicalRecordFromStorage(
|
|
1295
|
-
row,
|
|
1296
|
-
descriptor,
|
|
1297
|
-
{ allowedExtraFields: getAllowedQueryFieldNames(descriptor.resource) }
|
|
1298
|
-
)
|
|
1299
|
-
const resourceId = getLogicalResourceId(row, descriptor)
|
|
1300
|
-
const includeResource = {
|
|
1301
|
-
type: descriptor.resource,
|
|
1302
|
-
id: resourceId,
|
|
1303
|
-
attributes: translated.attributes,
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
if (translated.relationships && Object.keys(translated.relationships).length > 0) {
|
|
1307
|
-
includeResource.relationships = translated.relationships
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
if (fieldSelectionInfo?.computedDependencies?.length) {
|
|
1311
|
-
includeResource[COMPUTED_DEPENDENCIES_KEY] = fieldSelectionInfo.computedDependencies
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
const targetScope = api.resources?.[descriptor.resource]
|
|
1315
|
-
if (targetScope) {
|
|
1316
|
-
decorateResourceLinks({
|
|
1317
|
-
resource: includeResource,
|
|
1318
|
-
scope: targetScope,
|
|
1319
|
-
scopeName: descriptor.resource,
|
|
1320
|
-
context,
|
|
1321
|
-
})
|
|
1322
|
-
}
|
|
1323
|
-
|
|
1324
|
-
return includeResource
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
const collectIncludes = async ({ descriptor, resources, includeTree, context, includes, seen }) => {
|
|
1328
|
-
const entries = Object.entries(includeTree || {})
|
|
1329
|
-
if (entries.length === 0 || !resources || resources.length === 0) return
|
|
1330
|
-
|
|
1331
|
-
const db = context.db || context.transaction || api.knex.instance
|
|
1332
|
-
|
|
1333
|
-
for (const [relName, childTree] of entries) {
|
|
1334
|
-
const childKeys = Object.keys(childTree || {})
|
|
1335
|
-
|
|
1336
|
-
const belongsToInfo = descriptor.belongsTo?.[relName]
|
|
1337
|
-
if (belongsToInfo) {
|
|
1338
|
-
const ids = [...new Set(resources
|
|
1339
|
-
.map((resource) => resource.relationships?.[relName]?.data?.id)
|
|
1340
|
-
.filter((id) => id !== undefined && id !== null))]
|
|
1341
|
-
|
|
1342
|
-
if (ids.length === 0) continue
|
|
1343
|
-
|
|
1344
|
-
const targetDescriptor = await registry.getDescriptor(tenantId, belongsToInfo.target)
|
|
1345
|
-
if (!targetDescriptor) continue
|
|
1346
|
-
|
|
1347
|
-
let query = db(targetDescriptor.canonical.tableName)
|
|
1348
|
-
.where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
|
|
1349
|
-
.where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
|
|
1350
|
-
.whereIn(getLogicalResourceIdColumn(targetDescriptor), ids)
|
|
1351
|
-
const selectionState = await applyIncludeFieldSelection({
|
|
1352
|
-
query,
|
|
1353
|
-
resourceName: targetDescriptor.resource,
|
|
1354
|
-
tableName: targetDescriptor.canonical.tableName,
|
|
1355
|
-
context
|
|
1356
|
-
})
|
|
1357
|
-
query = selectionState.query
|
|
1358
|
-
const scopedQueryState = await applyTargetScopeFilters({
|
|
1359
|
-
query,
|
|
1360
|
-
resourceName: targetDescriptor.resource,
|
|
1361
|
-
tableName: targetDescriptor.canonical.tableName,
|
|
1362
|
-
context
|
|
1363
|
-
})
|
|
1364
|
-
query = scopedQueryState.query
|
|
1365
|
-
const rows = await query
|
|
1366
|
-
|
|
1367
|
-
const newResources = []
|
|
1368
|
-
for (const row of rows) {
|
|
1369
|
-
const includeId = getLogicalResourceId(row, targetDescriptor)
|
|
1370
|
-
const includeKey = `${targetDescriptor.resource}:${includeId}`
|
|
1371
|
-
if (seen.has(includeKey)) continue
|
|
1372
|
-
seen.add(includeKey)
|
|
1373
|
-
const includeResource = buildIncludedResource({
|
|
1374
|
-
row,
|
|
1375
|
-
descriptor: targetDescriptor,
|
|
1376
|
-
context,
|
|
1377
|
-
fieldSelectionInfo: selectionState.fieldSelectionInfo
|
|
1378
|
-
})
|
|
1379
|
-
includes.push(includeResource)
|
|
1380
|
-
newResources.push(includeResource)
|
|
1381
|
-
}
|
|
1382
|
-
|
|
1383
|
-
if (childKeys.length > 0 && newResources.length > 0) {
|
|
1384
|
-
await attachHasManyRelationships({ resources: newResources, descriptor: targetDescriptor, context })
|
|
1385
|
-
await attachManyToManyRelationships({ resources: newResources, descriptor: targetDescriptor, context })
|
|
1386
|
-
await collectIncludes({
|
|
1387
|
-
descriptor: targetDescriptor,
|
|
1388
|
-
resources: newResources,
|
|
1389
|
-
includeTree: childTree,
|
|
1390
|
-
context,
|
|
1391
|
-
includes,
|
|
1392
|
-
seen,
|
|
1393
|
-
})
|
|
1394
|
-
}
|
|
1395
|
-
continue
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
const polymorphicInfo = descriptor.polymorphicBelongsTo?.[relName]
|
|
1399
|
-
if (polymorphicInfo) {
|
|
1400
|
-
const typeToIds = new Map()
|
|
1401
|
-
for (const resource of resources) {
|
|
1402
|
-
const relData = resource.relationships?.[relName]?.data
|
|
1403
|
-
if (!relData?.type || relData?.id == null) continue
|
|
1404
|
-
const targetType = String(relData.type)
|
|
1405
|
-
const targetId = normalizeId(relData.id)
|
|
1406
|
-
if (targetId == null) continue
|
|
1407
|
-
if (!typeToIds.has(targetType)) {
|
|
1408
|
-
typeToIds.set(targetType, new Set())
|
|
1409
|
-
}
|
|
1410
|
-
typeToIds.get(targetType).add(targetId)
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
const newResources = []
|
|
1414
|
-
for (const [targetType, idSet] of typeToIds.entries()) {
|
|
1415
|
-
if (idSet.size === 0) continue
|
|
1416
|
-
let targetDescriptor
|
|
1417
|
-
try {
|
|
1418
|
-
targetDescriptor = await getDescriptor(targetType)
|
|
1419
|
-
} catch (error) {
|
|
1420
|
-
continue
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
let query = db(targetDescriptor.canonical.tableName)
|
|
1424
|
-
.where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
|
|
1425
|
-
.where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
|
|
1426
|
-
.whereIn(getLogicalResourceIdColumn(targetDescriptor), [...idSet])
|
|
1427
|
-
const selectionState = await applyIncludeFieldSelection({
|
|
1428
|
-
query,
|
|
1429
|
-
resourceName: targetDescriptor.resource,
|
|
1430
|
-
tableName: targetDescriptor.canonical.tableName,
|
|
1431
|
-
context
|
|
1432
|
-
})
|
|
1433
|
-
query = selectionState.query
|
|
1434
|
-
const scopedQueryState = await applyTargetScopeFilters({
|
|
1435
|
-
query,
|
|
1436
|
-
resourceName: targetDescriptor.resource,
|
|
1437
|
-
tableName: targetDescriptor.canonical.tableName,
|
|
1438
|
-
context
|
|
1439
|
-
})
|
|
1440
|
-
query = scopedQueryState.query
|
|
1441
|
-
const rows = await query
|
|
1442
|
-
|
|
1443
|
-
for (const row of rows) {
|
|
1444
|
-
const includeId = getLogicalResourceId(row, targetDescriptor)
|
|
1445
|
-
const includeKey = `${targetDescriptor.resource}:${includeId}`
|
|
1446
|
-
if (seen.has(includeKey)) continue
|
|
1447
|
-
seen.add(includeKey)
|
|
1448
|
-
const includeResource = buildIncludedResource({
|
|
1449
|
-
row,
|
|
1450
|
-
descriptor: targetDescriptor,
|
|
1451
|
-
context,
|
|
1452
|
-
fieldSelectionInfo: selectionState.fieldSelectionInfo
|
|
1453
|
-
})
|
|
1454
|
-
includes.push(includeResource)
|
|
1455
|
-
newResources.push(includeResource)
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
|
|
1459
|
-
if (childKeys.length > 0 && newResources.length > 0) {
|
|
1460
|
-
const resourcesByType = new Map()
|
|
1461
|
-
for (const includeResource of newResources) {
|
|
1462
|
-
if (!resourcesByType.has(includeResource.type)) {
|
|
1463
|
-
resourcesByType.set(includeResource.type, [])
|
|
1464
|
-
}
|
|
1465
|
-
resourcesByType.get(includeResource.type).push(includeResource)
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
for (const [targetType, groupedResources] of resourcesByType.entries()) {
|
|
1469
|
-
let targetDescriptor
|
|
1470
|
-
try {
|
|
1471
|
-
targetDescriptor = await getDescriptor(targetType)
|
|
1472
|
-
} catch (error) {
|
|
1473
|
-
continue
|
|
1474
|
-
}
|
|
1475
|
-
|
|
1476
|
-
await attachHasManyRelationships({ resources: groupedResources, descriptor: targetDescriptor, context })
|
|
1477
|
-
await attachManyToManyRelationships({ resources: groupedResources, descriptor: targetDescriptor, context })
|
|
1478
|
-
await collectIncludes({
|
|
1479
|
-
descriptor: targetDescriptor,
|
|
1480
|
-
resources: groupedResources,
|
|
1481
|
-
includeTree: childTree,
|
|
1482
|
-
context,
|
|
1483
|
-
includes,
|
|
1484
|
-
seen,
|
|
1485
|
-
})
|
|
1486
|
-
}
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
continue
|
|
1490
|
-
}
|
|
1491
|
-
|
|
1492
|
-
const hasManyInfo = descriptor.relationships?.[relName]
|
|
1493
|
-
if (hasManyInfo?.type === 'hasMany' && hasManyInfo.target && (hasManyInfo.foreignKey || hasManyInfo.via)) {
|
|
1494
|
-
let targetDescriptor
|
|
1495
|
-
try {
|
|
1496
|
-
targetDescriptor = await getDescriptor(hasManyInfo.target)
|
|
1497
|
-
} catch (error) {
|
|
1498
|
-
continue
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
const parentIds = resources
|
|
1502
|
-
.map((resource) => normalizeId(resource.id))
|
|
1503
|
-
.filter((id) => id !== null)
|
|
1504
|
-
|
|
1505
|
-
if (parentIds.length === 0) continue
|
|
1506
|
-
|
|
1507
|
-
const queryIds = Array.from(new Set([
|
|
1508
|
-
...parentIds,
|
|
1509
|
-
...parentIds
|
|
1510
|
-
.map((id) => {
|
|
1511
|
-
const numeric = Number(id)
|
|
1512
|
-
return Number.isFinite(numeric) ? numeric : null
|
|
1513
|
-
})
|
|
1514
|
-
.filter((value) => value !== null),
|
|
1515
|
-
]))
|
|
1516
|
-
|
|
1517
|
-
let rows = []
|
|
1518
|
-
let groupingColumn = null
|
|
1519
|
-
let fieldSelectionInfo = null
|
|
1520
|
-
|
|
1521
|
-
if (hasManyInfo.foreignKey) {
|
|
1522
|
-
const foreignField = targetDescriptor.fields?.[hasManyInfo.foreignKey]
|
|
1523
|
-
if (!foreignField?.slot) {
|
|
1524
|
-
continue
|
|
1525
|
-
}
|
|
1526
|
-
groupingColumn = foreignField.slot
|
|
1527
|
-
let query = db(targetDescriptor.canonical.tableName)
|
|
1528
|
-
.where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
|
|
1529
|
-
.where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
|
|
1530
|
-
.whereIn(groupingColumn, parentIds)
|
|
1531
|
-
const selectionState = await applyIncludeFieldSelection({
|
|
1532
|
-
query,
|
|
1533
|
-
resourceName: targetDescriptor.resource,
|
|
1534
|
-
tableName: targetDescriptor.canonical.tableName,
|
|
1535
|
-
context
|
|
1536
|
-
})
|
|
1537
|
-
query = selectionState.query
|
|
1538
|
-
const scopedQueryState = await applyTargetScopeFilters({
|
|
1539
|
-
query,
|
|
1540
|
-
resourceName: targetDescriptor.resource,
|
|
1541
|
-
tableName: targetDescriptor.canonical.tableName,
|
|
1542
|
-
context
|
|
1543
|
-
})
|
|
1544
|
-
query = scopedQueryState.query
|
|
1545
|
-
fieldSelectionInfo = selectionState.fieldSelectionInfo
|
|
1546
|
-
rows = await query
|
|
1547
|
-
} else if (hasManyInfo.via) {
|
|
1548
|
-
const polyInfo = targetDescriptor.polymorphicBelongsTo?.[hasManyInfo.via]
|
|
1549
|
-
if (!polyInfo?.idColumn || !polyInfo?.typeColumn) {
|
|
1550
|
-
continue
|
|
1551
|
-
}
|
|
1552
|
-
groupingColumn = polyInfo.idColumn
|
|
1553
|
-
let query = db(targetDescriptor.canonical.tableName)
|
|
1554
|
-
.where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
|
|
1555
|
-
.where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
|
|
1556
|
-
.where(polyInfo.typeColumn, descriptor.resource)
|
|
1557
|
-
.whereIn(groupingColumn, queryIds)
|
|
1558
|
-
const selectionState = await applyIncludeFieldSelection({
|
|
1559
|
-
query,
|
|
1560
|
-
resourceName: targetDescriptor.resource,
|
|
1561
|
-
tableName: targetDescriptor.canonical.tableName,
|
|
1562
|
-
context
|
|
1563
|
-
})
|
|
1564
|
-
query = selectionState.query
|
|
1565
|
-
const scopedQueryState = await applyTargetScopeFilters({
|
|
1566
|
-
query,
|
|
1567
|
-
resourceName: targetDescriptor.resource,
|
|
1568
|
-
tableName: targetDescriptor.canonical.tableName,
|
|
1569
|
-
context
|
|
1570
|
-
})
|
|
1571
|
-
query = scopedQueryState.query
|
|
1572
|
-
fieldSelectionInfo = selectionState.fieldSelectionInfo
|
|
1573
|
-
rows = await query
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
if (!groupingColumn || rows.length === 0) {
|
|
1577
|
-
continue
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
const newResources = []
|
|
1581
|
-
for (const row of rows) {
|
|
1582
|
-
const includeId = getLogicalResourceId(row, targetDescriptor)
|
|
1583
|
-
const includeKey = `${targetDescriptor.resource}:${includeId}`
|
|
1584
|
-
if (seen.has(includeKey)) continue
|
|
1585
|
-
seen.add(includeKey)
|
|
1586
|
-
const includeResource = buildIncludedResource({
|
|
1587
|
-
row,
|
|
1588
|
-
descriptor: targetDescriptor,
|
|
1589
|
-
context,
|
|
1590
|
-
fieldSelectionInfo
|
|
1591
|
-
})
|
|
1592
|
-
includes.push(includeResource)
|
|
1593
|
-
newResources.push(includeResource)
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
if (childKeys.length > 0 && newResources.length > 0) {
|
|
1597
|
-
await attachHasManyRelationships({ resources: newResources, descriptor: targetDescriptor, context })
|
|
1598
|
-
await attachManyToManyRelationships({ resources: newResources, descriptor: targetDescriptor, context })
|
|
1599
|
-
await collectIncludes({
|
|
1600
|
-
descriptor: targetDescriptor,
|
|
1601
|
-
resources: newResources,
|
|
1602
|
-
includeTree: childTree,
|
|
1603
|
-
context,
|
|
1604
|
-
includes,
|
|
1605
|
-
seen,
|
|
1606
|
-
})
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
continue
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
|
-
const manyInfo = descriptor.manyToMany?.[relName]
|
|
1613
|
-
if (manyInfo) {
|
|
1614
|
-
const info = await getManyToManyInfo(descriptor.resource, relName)
|
|
1615
|
-
if (!info) continue
|
|
1616
|
-
const linkRows = await fetchLinksForParents({
|
|
1617
|
-
descriptor: info.descriptor,
|
|
1618
|
-
relationshipKey: info.relationshipKey,
|
|
1619
|
-
parentIds: resources.map((resource) => resource.id),
|
|
1620
|
-
db,
|
|
1621
|
-
})
|
|
1622
|
-
|
|
1623
|
-
const childIds = [...new Set(linkRows.map((row) => row.childId))]
|
|
1624
|
-
if (childIds.length === 0) continue
|
|
1625
|
-
|
|
1626
|
-
const targetDescriptor = await registry.getDescriptor(tenantId, info.relInfo.target)
|
|
1627
|
-
if (!targetDescriptor) continue
|
|
1628
|
-
|
|
1629
|
-
let query = db(targetDescriptor.canonical.tableName)
|
|
1630
|
-
.where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
|
|
1631
|
-
.where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
|
|
1632
|
-
.whereIn(getLogicalResourceIdColumn(targetDescriptor), childIds)
|
|
1633
|
-
const selectionState = await applyIncludeFieldSelection({
|
|
1634
|
-
query,
|
|
1635
|
-
resourceName: targetDescriptor.resource,
|
|
1636
|
-
tableName: targetDescriptor.canonical.tableName,
|
|
1637
|
-
context
|
|
1638
|
-
})
|
|
1639
|
-
query = selectionState.query
|
|
1640
|
-
const scopedQueryState = await applyTargetScopeFilters({
|
|
1641
|
-
query,
|
|
1642
|
-
resourceName: targetDescriptor.resource,
|
|
1643
|
-
tableName: targetDescriptor.canonical.tableName,
|
|
1644
|
-
context
|
|
1645
|
-
})
|
|
1646
|
-
query = scopedQueryState.query
|
|
1647
|
-
const rows = await query
|
|
1648
|
-
|
|
1649
|
-
const newResources = []
|
|
1650
|
-
for (const row of rows) {
|
|
1651
|
-
const includeId = getLogicalResourceId(row, targetDescriptor)
|
|
1652
|
-
const includeKey = `${targetDescriptor.resource}:${includeId}`
|
|
1653
|
-
if (seen.has(includeKey)) continue
|
|
1654
|
-
seen.add(includeKey)
|
|
1655
|
-
const includeResource = buildIncludedResource({
|
|
1656
|
-
row,
|
|
1657
|
-
descriptor: targetDescriptor,
|
|
1658
|
-
context,
|
|
1659
|
-
fieldSelectionInfo: selectionState.fieldSelectionInfo
|
|
1660
|
-
})
|
|
1661
|
-
includes.push(includeResource)
|
|
1662
|
-
newResources.push(includeResource)
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
if (childKeys.length > 0 && newResources.length > 0) {
|
|
1666
|
-
await attachHasManyRelationships({ resources: newResources, descriptor: targetDescriptor, context })
|
|
1667
|
-
await attachManyToManyRelationships({ resources: newResources, descriptor: targetDescriptor, context })
|
|
1668
|
-
await collectIncludes({
|
|
1669
|
-
descriptor: targetDescriptor,
|
|
1670
|
-
resources: newResources,
|
|
1671
|
-
includeTree: childTree,
|
|
1672
|
-
context,
|
|
1673
|
-
includes,
|
|
1674
|
-
seen,
|
|
1675
|
-
})
|
|
1676
|
-
}
|
|
1677
|
-
}
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
|
|
1681
|
-
const buildIncludes = async ({ parentResources, descriptor, context }) => {
|
|
1682
|
-
const includeTree = parseIncludeTree(context.queryParams?.include)
|
|
1683
|
-
if (Object.keys(includeTree).length === 0) return []
|
|
1684
|
-
|
|
1685
|
-
const includes = []
|
|
1686
|
-
const seen = new Set()
|
|
1687
|
-
|
|
1688
|
-
await collectIncludes({
|
|
1689
|
-
descriptor,
|
|
1690
|
-
resources: parentResources,
|
|
1691
|
-
includeTree,
|
|
1692
|
-
context,
|
|
1693
|
-
includes,
|
|
1694
|
-
seen,
|
|
1695
|
-
})
|
|
1696
|
-
|
|
1697
|
-
return includes
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
const attachHasManyRelationships = async ({ resources, descriptor, context }) => {
|
|
1701
|
-
if (!resources || resources.length === 0) return
|
|
1702
|
-
const hasManyEntries = Object.entries(descriptor.relationships || {})
|
|
1703
|
-
.filter(([, relDef]) => relDef?.type === 'hasMany' && relDef.target && (relDef.foreignKey || relDef.via))
|
|
1704
|
-
|
|
1705
|
-
if (hasManyEntries.length === 0) return
|
|
1706
|
-
|
|
1707
|
-
const db = context.db || context.transaction || api.knex.instance
|
|
1708
|
-
const parentIds = resources.map((resource) => resource.id)
|
|
1709
|
-
const normalizedParentIds = parentIds
|
|
1710
|
-
.map((id) => normalizeId(id))
|
|
1711
|
-
.filter((id) => id !== null)
|
|
1712
|
-
|
|
1713
|
-
if (normalizedParentIds.length === 0) return
|
|
1714
|
-
|
|
1715
|
-
for (const [relName, relDef] of hasManyEntries) {
|
|
1716
|
-
let targetDescriptor
|
|
1717
|
-
try {
|
|
1718
|
-
targetDescriptor = await getDescriptor(relDef.target)
|
|
1719
|
-
} catch (error) {
|
|
1720
|
-
continue
|
|
1721
|
-
}
|
|
1722
|
-
|
|
1723
|
-
if (relDef.foreignKey) {
|
|
1724
|
-
const foreignField = targetDescriptor.fields?.[relDef.foreignKey]
|
|
1725
|
-
if (!foreignField?.slot) continue
|
|
1726
|
-
const column = foreignField.slot
|
|
1727
|
-
|
|
1728
|
-
let query = db(targetDescriptor.canonical.tableName)
|
|
1729
|
-
.where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
|
|
1730
|
-
.where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
|
|
1731
|
-
.whereIn(column, normalizedParentIds)
|
|
1732
|
-
const scopedQueryState = await applyTargetScopeFilters({
|
|
1733
|
-
query,
|
|
1734
|
-
resourceName: targetDescriptor.resource,
|
|
1735
|
-
tableName: targetDescriptor.canonical.tableName,
|
|
1736
|
-
context,
|
|
1737
|
-
queryPurpose: 'relationship-identifiers'
|
|
1738
|
-
})
|
|
1739
|
-
query = scopedQueryState.query
|
|
1740
|
-
const rows = await query
|
|
1741
|
-
|
|
1742
|
-
const grouped = rows.reduce((acc, row) => {
|
|
1743
|
-
const parentId = row[column]
|
|
1744
|
-
if (parentId == null) return acc
|
|
1745
|
-
const key = String(parentId)
|
|
1746
|
-
acc[key] = acc[key] || []
|
|
1747
|
-
acc[key].push({ type: targetDescriptor.resource, id: getLogicalResourceId(row, targetDescriptor) })
|
|
1748
|
-
return acc
|
|
1749
|
-
}, {})
|
|
1750
|
-
|
|
1751
|
-
for (const resource of resources) {
|
|
1752
|
-
resource.relationships = resource.relationships || {}
|
|
1753
|
-
const related = grouped[String(resource.id)] || []
|
|
1754
|
-
resource.relationships[relName] = { data: related }
|
|
1755
|
-
}
|
|
1756
|
-
|
|
1757
|
-
continue
|
|
1758
|
-
}
|
|
1759
|
-
|
|
1760
|
-
if (relDef.via) {
|
|
1761
|
-
const polyInfo = targetDescriptor.polymorphicBelongsTo?.[relDef.via]
|
|
1762
|
-
if (!polyInfo?.idColumn || !polyInfo?.typeColumn) continue
|
|
1763
|
-
|
|
1764
|
-
const queryIds = Array.from(new Set([
|
|
1765
|
-
...normalizedParentIds,
|
|
1766
|
-
...normalizedParentIds
|
|
1767
|
-
.map((id) => {
|
|
1768
|
-
const numeric = Number(id)
|
|
1769
|
-
return Number.isFinite(numeric) ? numeric : null
|
|
1770
|
-
})
|
|
1771
|
-
.filter((value) => value !== null),
|
|
1772
|
-
]))
|
|
1773
|
-
|
|
1774
|
-
let query = db(targetDescriptor.canonical.tableName)
|
|
1775
|
-
.where(targetDescriptor.canonical.tenantColumn, targetDescriptor.tenant)
|
|
1776
|
-
.where(targetDescriptor.canonical.resourceColumn, targetDescriptor.resource)
|
|
1777
|
-
.where(polyInfo.typeColumn, descriptor.resource)
|
|
1778
|
-
.whereIn(polyInfo.idColumn, queryIds)
|
|
1779
|
-
const scopedQueryState = await applyTargetScopeFilters({
|
|
1780
|
-
query,
|
|
1781
|
-
resourceName: targetDescriptor.resource,
|
|
1782
|
-
tableName: targetDescriptor.canonical.tableName,
|
|
1783
|
-
context,
|
|
1784
|
-
queryPurpose: 'relationship-identifiers'
|
|
1785
|
-
})
|
|
1786
|
-
query = scopedQueryState.query
|
|
1787
|
-
const rows = await query
|
|
1788
|
-
|
|
1789
|
-
const grouped = rows.reduce((acc, row) => {
|
|
1790
|
-
const parentId = row[polyInfo.idColumn]
|
|
1791
|
-
if (parentId == null) return acc
|
|
1792
|
-
const key = String(parentId)
|
|
1793
|
-
acc[key] = acc[key] || []
|
|
1794
|
-
acc[key].push({ type: targetDescriptor.resource, id: getLogicalResourceId(row, targetDescriptor) })
|
|
1795
|
-
return acc
|
|
1796
|
-
}, {})
|
|
1797
|
-
|
|
1798
|
-
for (const resource of resources) {
|
|
1799
|
-
resource.relationships = resource.relationships || {}
|
|
1800
|
-
const related = grouped[String(resource.id)] || []
|
|
1801
|
-
resource.relationships[relName] = { data: related }
|
|
1802
|
-
}
|
|
1803
|
-
}
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
const attachManyToManyRelationships = async ({ resources, descriptor, context }) => {
|
|
1808
|
-
if (!resources || resources.length === 0) return
|
|
1809
|
-
const manyEntries = Object.entries(descriptor.manyToMany || {})
|
|
1810
|
-
if (manyEntries.length === 0) return
|
|
1811
|
-
|
|
1812
|
-
const db = context.db || context.transaction || api.knex.instance
|
|
1813
|
-
const parentIds = resources.map((resource) => resource.id)
|
|
1814
|
-
|
|
1815
|
-
for (const [relName] of manyEntries) {
|
|
1816
|
-
const info = await getManyToManyInfo(descriptor.resource, relName)
|
|
1817
|
-
if (!info) continue
|
|
1818
|
-
let rows = await fetchLinksForParents({
|
|
1819
|
-
descriptor: info.descriptor,
|
|
1820
|
-
relationshipKey: info.relationshipKey,
|
|
1821
|
-
parentIds,
|
|
1822
|
-
db,
|
|
1823
|
-
})
|
|
1824
|
-
|
|
1825
|
-
const visibleIdentifiers = await filterVisibleIdentifiers({
|
|
1826
|
-
identifiers: rows.map((row) => ({ type: row.childType, id: row.childId })),
|
|
1827
|
-
context
|
|
1828
|
-
})
|
|
1829
|
-
const visibleKeys = new Set(
|
|
1830
|
-
visibleIdentifiers.map((identifier) => `${identifier.type}:${String(identifier.id)}`)
|
|
1831
|
-
)
|
|
1832
|
-
rows = rows.filter((row) => visibleKeys.has(`${row.childType}:${String(row.childId)}`))
|
|
1833
|
-
|
|
1834
|
-
const grouped = rows.reduce((acc, row) => {
|
|
1835
|
-
const key = String(row.parentId)
|
|
1836
|
-
acc[key] = acc[key] || []
|
|
1837
|
-
acc[key].push({ type: row.childType, id: String(row.childId) })
|
|
1838
|
-
return acc
|
|
1839
|
-
}, {})
|
|
1840
|
-
|
|
1841
|
-
for (const resource of resources) {
|
|
1842
|
-
const related = grouped[resource.id] || []
|
|
1843
|
-
resource.relationships = resource.relationships || {}
|
|
1844
|
-
resource.relationships[relName] = { data: related }
|
|
1845
|
-
}
|
|
1846
|
-
}
|
|
1847
|
-
}
|
|
1848
|
-
|
|
1849
|
-
helpers.dataGet = async ({ scopeName, context }) => {
|
|
1850
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
1851
|
-
if (storageAdapter) {
|
|
1852
|
-
context.storageAdapter = storageAdapter
|
|
1853
|
-
}
|
|
1854
|
-
const descriptor = await getDescriptor(scopeName)
|
|
1855
|
-
const { canonical } = descriptor
|
|
1856
|
-
const id = context.id
|
|
1857
|
-
const logicalIdColumn = getLogicalResourceIdColumn(descriptor)
|
|
1858
|
-
const scope = api.resources?.[scopeName]
|
|
1859
|
-
|
|
1860
|
-
let fieldSelectionInfo = null
|
|
1861
|
-
if (scope) {
|
|
1862
|
-
fieldSelectionInfo = await buildFieldSelection(scope, { context })
|
|
1863
|
-
context.computedDependencies = fieldSelectionInfo.computedDependencies
|
|
1864
|
-
} else {
|
|
1865
|
-
context.computedDependencies = []
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
let query = context.db(canonical.tableName)
|
|
1869
|
-
.where(logicalIdColumn, id)
|
|
1870
|
-
.where(canonical.resourceColumn, descriptor.resource)
|
|
1871
|
-
.where(canonical.tenantColumn, descriptor.tenant)
|
|
1872
|
-
|
|
1873
|
-
if (scope && fieldSelectionInfo) {
|
|
1874
|
-
const selectionState = await applyFieldSelectionToQuery({
|
|
1875
|
-
query,
|
|
1876
|
-
scope,
|
|
1877
|
-
fieldSelectionInfo,
|
|
1878
|
-
tableName: canonical.tableName,
|
|
1879
|
-
useTablePrefix: false,
|
|
1880
|
-
storageAdapter,
|
|
1881
|
-
db: context.db,
|
|
1882
|
-
context,
|
|
1883
|
-
scopeName
|
|
1884
|
-
})
|
|
1885
|
-
query = selectionState.query
|
|
1886
|
-
}
|
|
1887
|
-
|
|
1888
|
-
const row = await query.first()
|
|
1889
|
-
|
|
1890
|
-
if (!row) return null
|
|
1891
|
-
|
|
1892
|
-
const record = translateCanonicalRecordFromStorage(
|
|
1893
|
-
row,
|
|
1894
|
-
descriptor,
|
|
1895
|
-
{ allowedExtraFields: getAllowedQueryFieldNames(scopeName) }
|
|
1896
|
-
)
|
|
1897
|
-
const resourceId = getLogicalResourceId(row, descriptor)
|
|
1898
|
-
const data = {
|
|
1899
|
-
type: descriptor.resource,
|
|
1900
|
-
id: resourceId,
|
|
1901
|
-
attributes: record.attributes,
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
if (record.relationships && Object.keys(record.relationships).length > 0) {
|
|
1905
|
-
data.relationships = record.relationships
|
|
1906
|
-
}
|
|
1907
|
-
|
|
1908
|
-
await attachHasManyRelationships({
|
|
1909
|
-
resources: [data],
|
|
1910
|
-
descriptor,
|
|
1911
|
-
context,
|
|
1912
|
-
})
|
|
1913
|
-
|
|
1914
|
-
await attachManyToManyRelationships({
|
|
1915
|
-
resources: [data],
|
|
1916
|
-
descriptor,
|
|
1917
|
-
context,
|
|
1918
|
-
})
|
|
1919
|
-
|
|
1920
|
-
const included = await buildIncludes({
|
|
1921
|
-
parentResources: [data],
|
|
1922
|
-
descriptor,
|
|
1923
|
-
context,
|
|
1924
|
-
})
|
|
1925
|
-
|
|
1926
|
-
const response = { data }
|
|
1927
|
-
if (included.length > 0) {
|
|
1928
|
-
response.included = included
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
if (scope) {
|
|
1932
|
-
decorateResourceLinks({ resource: data, scope, scopeName, context })
|
|
1933
|
-
response.links = { self: data.links.self }
|
|
1934
|
-
}
|
|
1935
|
-
|
|
1936
|
-
if (included.length > 0) {
|
|
1937
|
-
for (const includeResource of included) {
|
|
1938
|
-
const targetScope = api.resources?.[includeResource.type]
|
|
1939
|
-
if (!targetScope) continue
|
|
1940
|
-
decorateResourceLinks({
|
|
1941
|
-
resource: includeResource,
|
|
1942
|
-
scope: targetScope,
|
|
1943
|
-
scopeName: includeResource.type,
|
|
1944
|
-
context,
|
|
1945
|
-
})
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
return response
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
const rehydrateSchemaInfo = async (scopeName) => {
|
|
1953
|
-
const scope = api.resources?.[scopeName] || scopes?.[scopeName] || {}
|
|
1954
|
-
const storedOptions = scopeOptionsRegistry.get(scopeName) || {}
|
|
1955
|
-
const rawSchema = scope.scopeOptions?.schema || storedOptions.schema || {}
|
|
1956
|
-
|
|
1957
|
-
if (!scope.scopeOptions) {
|
|
1958
|
-
scope.scopeOptions = {
|
|
1959
|
-
schema: rawSchema,
|
|
1960
|
-
relationships: storedOptions.relationships || {},
|
|
1961
|
-
searchSchema: storedOptions.searchSchema || null,
|
|
1962
|
-
tableName: storedOptions.tableName || scopeName,
|
|
1963
|
-
idProperty: storedOptions.idProperty || scope?.vars?.idProperty || vars.idProperty || 'id',
|
|
1964
|
-
canonicalFieldsMap: storedOptions.canonicalFieldsMap || {},
|
|
1965
|
-
}
|
|
1966
|
-
}
|
|
1967
|
-
|
|
370
|
+
// The fresh descriptor also carries fields other servers added meanwhile.
|
|
1968
371
|
const descriptor = await registry.getDescriptor(tenantId, scopeName)
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
const existingInfo = scope.vars?.schemaInfo || {}
|
|
1972
|
-
|
|
1973
|
-
const mergedSchema = { ...(existingInfo.schemaInstance?.structure || {}) }
|
|
1974
|
-
const computed = { ...(existingInfo.computed || {}) }
|
|
1975
|
-
for (const [fieldName, definition] of Object.entries(descriptor.schema || {})) {
|
|
1976
|
-
if (definition?.computed) {
|
|
1977
|
-
if (!computed[fieldName]) {
|
|
1978
|
-
computed[fieldName] = { ...definition }
|
|
1979
|
-
}
|
|
1980
|
-
continue
|
|
1981
|
-
}
|
|
1982
|
-
mergedSchema[fieldName] = {
|
|
1983
|
-
...(mergedSchema[fieldName] || {}),
|
|
1984
|
-
...definition,
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
|
|
1988
|
-
if (!mergedSchema.id) {
|
|
1989
|
-
mergedSchema.id = { type: 'id' }
|
|
1990
|
-
}
|
|
1991
|
-
|
|
1992
|
-
const schemaInstance = createSchema(mergedSchema)
|
|
1993
|
-
|
|
1994
|
-
const rawSearchFields = generateSearchSchemaFromSchema(
|
|
1995
|
-
mergedSchema,
|
|
1996
|
-
scope.scopeOptions?.searchSchema || storedOptions.searchSchema || null
|
|
1997
|
-
)
|
|
1998
|
-
if (rawSearchFields) {
|
|
1999
|
-
ensureSearchFieldsAreIndexed(rawSearchFields)
|
|
2000
|
-
}
|
|
2001
|
-
const searchSchemaInstance = createSchema(rawSearchFields || {})
|
|
2002
|
-
|
|
2003
|
-
const tableName = storedOptions.tableName ||
|
|
2004
|
-
existingInfo.tableName ||
|
|
2005
|
-
scope.scopeOptions?.tableName ||
|
|
2006
|
-
scopeName
|
|
2007
|
-
|
|
2008
|
-
const idProperty = storedOptions.idProperty ||
|
|
2009
|
-
existingInfo.idProperty ||
|
|
2010
|
-
scope.scopeOptions?.idProperty ||
|
|
2011
|
-
scope.vars?.idProperty ||
|
|
2012
|
-
vars.idProperty ||
|
|
2013
|
-
'id'
|
|
2014
|
-
descriptor.idProperty = idProperty
|
|
2015
|
-
|
|
2016
|
-
scope.vars.schemaInfo = {
|
|
2017
|
-
...existingInfo,
|
|
2018
|
-
schemaInstance,
|
|
2019
|
-
schemaStructure: schemaInstance.structure,
|
|
2020
|
-
searchSchemaInstance,
|
|
2021
|
-
searchSchemaStructure: searchSchemaInstance.structure,
|
|
2022
|
-
schemaRelationships: descriptor.relationships ||
|
|
2023
|
-
scope.scopeOptions?.relationships ||
|
|
2024
|
-
storedOptions.relationships ||
|
|
2025
|
-
existingInfo.schemaRelationships ||
|
|
2026
|
-
{},
|
|
2027
|
-
computed,
|
|
2028
|
-
tableName,
|
|
2029
|
-
idProperty,
|
|
2030
|
-
descriptor,
|
|
2031
|
-
canonicalFieldMap: descriptor.canonicalFieldMap || {},
|
|
2032
|
-
}
|
|
2033
|
-
|
|
2034
|
-
getScopeStorageAdapter(scopeName)
|
|
2035
|
-
|
|
2036
|
-
scope.scopeOptions = {
|
|
2037
|
-
...(scope.scopeOptions || {}),
|
|
2038
|
-
canonicalFieldsMap: descriptor.canonicalFieldMap || scope.scopeOptions?.canonicalFieldsMap || {},
|
|
2039
|
-
}
|
|
2040
|
-
|
|
2041
|
-
scopeOptionsRegistry.set(scopeName, {
|
|
2042
|
-
schema: storedOptions.schema || scope.scopeOptions?.schema || {},
|
|
2043
|
-
relationships: storedOptions.relationships || scope.scopeOptions?.relationships || {},
|
|
2044
|
-
searchSchema: scope.scopeOptions?.searchSchema || storedOptions.searchSchema || null,
|
|
2045
|
-
tableName,
|
|
2046
|
-
idProperty,
|
|
2047
|
-
canonicalFieldsMap: descriptor.canonicalFieldMap || {},
|
|
2048
|
-
})
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
const buildTableNameMaps = () => {
|
|
2052
|
-
const resourceToTableName = new Map()
|
|
2053
|
-
const tableNameToResource = new Map()
|
|
2054
|
-
for (const [resourceName, resourceScope] of Object.entries(api.resources || {})) {
|
|
2055
|
-
const tableName = resourceScope?.vars?.schemaInfo?.tableName
|
|
2056
|
-
if (!tableName) continue
|
|
2057
|
-
resourceToTableName.set(resourceName, tableName)
|
|
2058
|
-
tableNameToResource.set(tableName, resourceName)
|
|
2059
|
-
}
|
|
2060
|
-
return { resourceToTableName, tableNameToResource }
|
|
2061
|
-
}
|
|
2062
|
-
|
|
2063
|
-
const queryHookDependencies = { log, scopes: api.resources, knex, getStorageAdapter: getScopeStorageAdapter }
|
|
2064
|
-
|
|
2065
|
-
const applyBuiltInAnyApiQueryFilters = async (context) => {
|
|
2066
|
-
const hookParams = { context }
|
|
2067
|
-
await polymorphicFiltersHook(hookParams, queryHookDependencies)
|
|
2068
|
-
await crossTableFiltersHook(hookParams, queryHookDependencies)
|
|
2069
|
-
await basicFiltersHook(hookParams, queryHookDependencies)
|
|
2070
|
-
}
|
|
2071
|
-
|
|
2072
|
-
helpers.dataQuery = async ({ scopeName, context, runHooks }) => {
|
|
2073
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
2074
|
-
if (storageAdapter) {
|
|
2075
|
-
context.storageAdapter = storageAdapter
|
|
2076
|
-
}
|
|
2077
|
-
const descriptor = await getDescriptor(scopeName)
|
|
2078
|
-
const db = context.db || context.transaction || api.knex.instance
|
|
2079
|
-
const scope = api.resources?.[scopeName]
|
|
2080
|
-
const queryParams = context.queryParams || {}
|
|
2081
|
-
|
|
2082
|
-
let fieldSelectionInfo = null
|
|
2083
|
-
if (scope) {
|
|
2084
|
-
fieldSelectionInfo = await buildFieldSelection(scope, { context })
|
|
2085
|
-
context.computedDependencies = fieldSelectionInfo.computedDependencies
|
|
2086
|
-
} else {
|
|
2087
|
-
context.computedDependencies = []
|
|
2088
|
-
}
|
|
2089
|
-
|
|
2090
|
-
const { resourceToTableName, tableNameToResource } = buildTableNameMaps()
|
|
2091
|
-
context.resourceToTableName = resourceToTableName
|
|
2092
|
-
const descriptorsMap = await preloadRelatedDescriptors({ registry, descriptor })
|
|
2093
|
-
const adapter = new AnyapiQueryAdapter({
|
|
2094
|
-
descriptor,
|
|
2095
|
-
db,
|
|
2096
|
-
registry,
|
|
2097
|
-
descriptorsMap,
|
|
2098
|
-
resourceToTableName,
|
|
2099
|
-
tableNameToResource,
|
|
2100
|
-
log,
|
|
2101
|
-
})
|
|
2102
|
-
const queryBuilder = adapter.query
|
|
2103
|
-
|
|
2104
|
-
const schemaInfo = scope?.vars?.schemaInfo
|
|
2105
|
-
const tableNameForHooks = schemaInfo?.tableName || adapter.tableAlias
|
|
2106
|
-
|
|
2107
|
-
applyFiltersToQuery({
|
|
2108
|
-
query: queryBuilder,
|
|
2109
|
-
filters: queryParams.filters,
|
|
2110
|
-
descriptor,
|
|
2111
|
-
searchSchema: schemaInfo?.searchSchemaStructure,
|
|
2112
|
-
adapter,
|
|
2113
|
-
})
|
|
2114
|
-
|
|
2115
|
-
context.knexQuery = {
|
|
2116
|
-
query: queryBuilder,
|
|
2117
|
-
filters: queryParams.filters,
|
|
2118
|
-
schemaInfo,
|
|
2119
|
-
scopeName,
|
|
2120
|
-
tableName: tableNameForHooks,
|
|
2121
|
-
db,
|
|
2122
|
-
queryPurpose: 'collection',
|
|
2123
|
-
isAnyApi: true,
|
|
2124
|
-
adapter,
|
|
2125
|
-
storageAdapter,
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
await applyBuiltInAnyApiQueryFilters(context)
|
|
2129
|
-
|
|
2130
|
-
if (runHooks) {
|
|
2131
|
-
await runHooks('knexQueryFiltering')
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
|
-
delete context.knexQuery
|
|
2135
|
-
|
|
2136
|
-
const countQuery = queryBuilder.clone()
|
|
2137
|
-
|
|
2138
|
-
let selectionState = { query: queryBuilder, queryFieldRuntimeByField: new Map() }
|
|
2139
|
-
if (scope && fieldSelectionInfo) {
|
|
2140
|
-
selectionState = await applyFieldSelectionToQuery({
|
|
2141
|
-
query: queryBuilder,
|
|
2142
|
-
scope,
|
|
2143
|
-
fieldSelectionInfo,
|
|
2144
|
-
tableName: adapter.tableAlias,
|
|
2145
|
-
useTablePrefix: false,
|
|
2146
|
-
storageAdapter,
|
|
2147
|
-
db,
|
|
2148
|
-
context,
|
|
2149
|
-
scopeName
|
|
2150
|
-
})
|
|
2151
|
-
}
|
|
2152
|
-
|
|
2153
|
-
const sortDescriptors = applySortingToQuery({
|
|
2154
|
-
query: selectionState.query,
|
|
2155
|
-
sort: queryParams.sort,
|
|
2156
|
-
descriptor,
|
|
2157
|
-
scope,
|
|
2158
|
-
adapter,
|
|
2159
|
-
before: Boolean(queryParams.page?.before),
|
|
2160
|
-
queryFieldRuntimeByField: selectionState.queryFieldRuntimeByField
|
|
2161
|
-
})
|
|
2162
|
-
|
|
2163
|
-
const paginationInfo = applyPaginationToQuery({
|
|
2164
|
-
query: queryBuilder,
|
|
2165
|
-
scope,
|
|
2166
|
-
queryParams,
|
|
2167
|
-
sortDescriptors,
|
|
2168
|
-
adapter,
|
|
2169
|
-
})
|
|
2170
|
-
|
|
2171
|
-
let rows = await queryBuilder
|
|
2172
|
-
let cursorRecords = null
|
|
2173
|
-
let hasMore = false
|
|
2174
|
-
|
|
2175
|
-
if (paginationInfo.mode === 'cursor') {
|
|
2176
|
-
const { pageSize, sortDescriptors: cursorDescriptors } = paginationInfo
|
|
2177
|
-
cursorRecords = rows.map((row) => {
|
|
2178
|
-
const record = {}
|
|
2179
|
-
for (const descriptorEntry of cursorDescriptors) {
|
|
2180
|
-
if (descriptorEntry.field === 'id') {
|
|
2181
|
-
record.id = getLogicalResourceId(row, descriptor)
|
|
2182
|
-
} else if (descriptorEntry.queryFieldRuntime) {
|
|
2183
|
-
record[descriptorEntry.field] = row[descriptorEntry.field]
|
|
2184
|
-
} else if (descriptorEntry.column) {
|
|
2185
|
-
record[descriptorEntry.field] = row[descriptorEntry.column]
|
|
2186
|
-
}
|
|
2187
|
-
}
|
|
2188
|
-
return record
|
|
2189
|
-
})
|
|
2190
|
-
|
|
2191
|
-
if (rows.length > pageSize) {
|
|
2192
|
-
hasMore = true
|
|
2193
|
-
rows = rows.slice(0, pageSize)
|
|
2194
|
-
cursorRecords = cursorRecords.slice(0, pageSize)
|
|
2195
|
-
}
|
|
2196
|
-
if (queryParams.page?.before) {
|
|
2197
|
-
rows.reverse()
|
|
2198
|
-
cursorRecords.reverse()
|
|
2199
|
-
}
|
|
2200
|
-
}
|
|
2201
|
-
|
|
2202
|
-
const data = rows.map((row) => {
|
|
2203
|
-
const translated = translateCanonicalRecordFromStorage(
|
|
2204
|
-
row,
|
|
2205
|
-
descriptor,
|
|
2206
|
-
{ allowedExtraFields: getAllowedQueryFieldNames(scopeName) }
|
|
2207
|
-
)
|
|
2208
|
-
const resourceId = getLogicalResourceId(row, descriptor)
|
|
2209
|
-
const resource = {
|
|
2210
|
-
type: descriptor.resource,
|
|
2211
|
-
id: resourceId,
|
|
2212
|
-
attributes: translated.attributes,
|
|
2213
|
-
}
|
|
2214
|
-
if (translated.relationships && Object.keys(translated.relationships).length > 0) {
|
|
2215
|
-
resource.relationships = translated.relationships
|
|
2216
|
-
}
|
|
2217
|
-
const resourceScope = scope
|
|
2218
|
-
if (resourceScope) {
|
|
2219
|
-
decorateResourceLinks({ resource, scope: resourceScope, scopeName, context })
|
|
2220
|
-
}
|
|
2221
|
-
if (fieldSelectionInfo?.computedDependencies?.length) {
|
|
2222
|
-
resource.__$jsonrestapi_computed_deps$__ = fieldSelectionInfo.computedDependencies
|
|
2223
|
-
}
|
|
2224
|
-
return resource
|
|
2225
|
-
})
|
|
2226
|
-
|
|
2227
|
-
await attachHasManyRelationships({
|
|
2228
|
-
resources: data,
|
|
2229
|
-
descriptor,
|
|
2230
|
-
context,
|
|
2231
|
-
})
|
|
2232
|
-
|
|
2233
|
-
await attachManyToManyRelationships({
|
|
2234
|
-
resources: data,
|
|
2235
|
-
descriptor,
|
|
2236
|
-
context,
|
|
2237
|
-
})
|
|
2238
|
-
|
|
2239
|
-
const included = await buildIncludes({
|
|
2240
|
-
parentResources: data,
|
|
2241
|
-
descriptor,
|
|
2242
|
-
context,
|
|
2243
|
-
})
|
|
2244
|
-
|
|
2245
|
-
const response = { data }
|
|
2246
|
-
if (included.length > 0) {
|
|
2247
|
-
response.included = included
|
|
2248
|
-
}
|
|
2249
|
-
|
|
2250
|
-
if (included.length > 0) {
|
|
2251
|
-
for (const includeResource of included) {
|
|
2252
|
-
const targetScope = api.resources?.[includeResource.type]
|
|
2253
|
-
if (!targetScope) continue
|
|
2254
|
-
decorateResourceLinks({
|
|
2255
|
-
resource: includeResource,
|
|
2256
|
-
scope: targetScope,
|
|
2257
|
-
scopeName: includeResource.type,
|
|
2258
|
-
context,
|
|
2259
|
-
})
|
|
2260
|
-
}
|
|
2261
|
-
}
|
|
2262
|
-
|
|
2263
|
-
context.returnMeta = context.returnMeta || {}
|
|
2264
|
-
const queryString = serializeJsonApiQuery(queryParams)
|
|
2265
|
-
context.returnMeta.queryString = queryString ? `?${queryString}` : ''
|
|
2266
|
-
delete context.returnMeta.paginationMeta
|
|
2267
|
-
delete context.returnMeta.paginationLinks
|
|
2268
|
-
|
|
2269
|
-
if (paginationInfo.mode === 'offset') {
|
|
2270
|
-
const { page, pageSize } = paginationInfo
|
|
2271
|
-
let paginationMeta
|
|
2272
|
-
|
|
2273
|
-
if (scope?.vars?.enablePaginationCounts) {
|
|
2274
|
-
const countResult = await countQuery.count({ count: '*' }).first()
|
|
2275
|
-
const total = Number(countResult?.count ?? countResult?.total ?? 0)
|
|
2276
|
-
paginationMeta = calculatePaginationMeta(total, page, pageSize)
|
|
2277
|
-
} else {
|
|
2278
|
-
paginationMeta = { page, pageSize }
|
|
2279
|
-
}
|
|
2280
|
-
|
|
2281
|
-
context.returnMeta.paginationMeta = paginationMeta
|
|
2282
|
-
const urlPrefix = getUrlPrefix(context, scope)
|
|
2283
|
-
context.returnMeta.paginationLinks = generatePaginationLinks(
|
|
2284
|
-
urlPrefix,
|
|
2285
|
-
scopeName,
|
|
2286
|
-
queryParams,
|
|
2287
|
-
paginationMeta
|
|
2288
|
-
)
|
|
2289
|
-
} else if (paginationInfo.mode === 'cursor' && cursorRecords) {
|
|
2290
|
-
const cursorFields = paginationInfo.sortDescriptors.map((descriptorEntry) => descriptorEntry.field)
|
|
2291
|
-
const cursorOptions = {
|
|
2292
|
-
schemaInfo,
|
|
2293
|
-
definitions: Object.fromEntries(paginationInfo.sortDescriptors.map(({ field, definition }) => [field, definition])),
|
|
2294
|
-
before: Boolean(queryParams.page?.before)
|
|
2295
|
-
}
|
|
2296
|
-
const paginationMeta = buildCursorMeta(
|
|
2297
|
-
cursorRecords,
|
|
2298
|
-
paginationInfo.pageSize,
|
|
2299
|
-
hasMore,
|
|
2300
|
-
cursorFields,
|
|
2301
|
-
cursorOptions
|
|
2302
|
-
)
|
|
2303
|
-
context.returnMeta.paginationMeta = paginationMeta
|
|
2304
|
-
const urlPrefix = getUrlPrefix(context, scope)
|
|
2305
|
-
context.returnMeta.paginationLinks = generateCursorPaginationLinks(
|
|
2306
|
-
urlPrefix,
|
|
2307
|
-
scopeName,
|
|
2308
|
-
queryParams,
|
|
2309
|
-
cursorRecords,
|
|
2310
|
-
paginationInfo.pageSize,
|
|
2311
|
-
hasMore,
|
|
2312
|
-
cursorFields,
|
|
2313
|
-
cursorOptions
|
|
2314
|
-
)
|
|
2315
|
-
}
|
|
2316
|
-
|
|
2317
|
-
if (context.returnMeta.paginationMeta) {
|
|
2318
|
-
response.meta = {
|
|
2319
|
-
pagination: context.returnMeta.paginationMeta,
|
|
2320
|
-
}
|
|
2321
|
-
}
|
|
2322
|
-
|
|
2323
|
-
if (context.returnMeta.paginationLinks) {
|
|
2324
|
-
response.links = context.returnMeta.paginationLinks
|
|
2325
|
-
} else if (scope) {
|
|
2326
|
-
const urlPrefix = getUrlPrefix(context, scope)
|
|
2327
|
-
response.links = {
|
|
2328
|
-
self: `${urlPrefix}/${scopeName}${context.returnMeta.queryString || ''}`,
|
|
2329
|
-
}
|
|
2330
|
-
}
|
|
2331
|
-
|
|
2332
|
-
return response
|
|
2333
|
-
}
|
|
2334
|
-
|
|
2335
|
-
helpers.dataQueryCount = async ({ scopeName, context }) => {
|
|
2336
|
-
const storageAdapter = getScopeStorageAdapter(scopeName)
|
|
2337
|
-
if (storageAdapter) {
|
|
2338
|
-
context.storageAdapter = storageAdapter
|
|
2339
|
-
}
|
|
2340
|
-
const descriptor = await getDescriptor(scopeName)
|
|
2341
|
-
const db = context.db || context.transaction || api.knex.instance
|
|
2342
|
-
const { resourceToTableName, tableNameToResource } = buildTableNameMaps()
|
|
2343
|
-
const descriptorsMap = await preloadRelatedDescriptors({ registry, descriptor })
|
|
2344
|
-
const adapter = new AnyapiQueryAdapter({
|
|
2345
|
-
descriptor,
|
|
2346
|
-
db,
|
|
2347
|
-
registry,
|
|
2348
|
-
descriptorsMap,
|
|
2349
|
-
resourceToTableName,
|
|
2350
|
-
tableNameToResource,
|
|
2351
|
-
log,
|
|
2352
|
-
})
|
|
2353
|
-
|
|
2354
|
-
// Apply the same filter hooks as dataQuery for consistent counts
|
|
2355
|
-
const scope = api.resources?.[scopeName]
|
|
2356
|
-
const schemaInfo = scope?.vars?.schemaInfo
|
|
2357
|
-
const tableNameForHooks = schemaInfo?.tableName || adapter.tableAlias
|
|
2358
|
-
|
|
2359
|
-
const countContext = {
|
|
2360
|
-
...context,
|
|
2361
|
-
knexQuery: {
|
|
2362
|
-
query: adapter.query,
|
|
2363
|
-
filters: context.queryParams?.filters,
|
|
2364
|
-
schemaInfo,
|
|
2365
|
-
scopeName,
|
|
2366
|
-
tableName: tableNameForHooks,
|
|
2367
|
-
db,
|
|
2368
|
-
queryPurpose: 'count',
|
|
2369
|
-
isAnyApi: true,
|
|
2370
|
-
adapter,
|
|
2371
|
-
storageAdapter,
|
|
2372
|
-
}
|
|
2373
|
-
}
|
|
2374
|
-
|
|
2375
|
-
const hookParams = { context: countContext }
|
|
2376
|
-
await polymorphicFiltersHook(hookParams, queryHookDependencies)
|
|
2377
|
-
await crossTableFiltersHook(hookParams, queryHookDependencies)
|
|
2378
|
-
await basicFiltersHook(hookParams, queryHookDependencies)
|
|
2379
|
-
await api.runHooks('knexQueryFiltering', countContext)
|
|
2380
|
-
|
|
2381
|
-
const countQuery = countContext.knexQuery?.query || adapter.query
|
|
2382
|
-
const [{ count }] = await countQuery.count({ count: '*' })
|
|
2383
|
-
return Number(count)
|
|
2384
|
-
}
|
|
2385
|
-
|
|
2386
|
-
addHook('scope:added', 'anyapi-register-resource', { sequence: 50 }, async ({ context }) => {
|
|
2387
|
-
const { scopeName, scopeOptions = {} } = context
|
|
2388
|
-
const scope = api.scopes?.[scopeName] || scopes?.[scopeName]
|
|
2389
|
-
const stored = scopeOptionsRegistry.get(scopeName) || {}
|
|
2390
|
-
|
|
2391
|
-
const baseSchema = scopeOptions.schema || scope?.scopeOptions?.schema || stored.schema || {}
|
|
2392
|
-
const relationships = scopeOptions.relationships || scope?.scopeOptions?.relationships || stored.relationships || {}
|
|
2393
|
-
const searchSchemaDef = scopeOptions.searchSchema || scope?.scopeOptions?.searchSchema || stored.searchSchema || null
|
|
2394
|
-
const tableName = scopeOptions.tableName || scope?.scopeOptions?.tableName || stored.tableName || scopeName
|
|
2395
|
-
const idProperty = scopeOptions.idProperty || scope?.scopeOptions?.idProperty || stored.idProperty || scope?.vars?.idProperty || vars.idProperty || 'id'
|
|
2396
|
-
const canonicalFieldsMap = scope?.scopeOptions?.canonicalFieldsMap || stored.canonicalFieldsMap || null
|
|
2397
|
-
|
|
2398
|
-
const schema = { ...baseSchema }
|
|
2399
|
-
if (idProperty && idProperty !== 'id' && !schema[idProperty]) {
|
|
2400
|
-
schema[idProperty] = { type: 'number' }
|
|
2401
|
-
}
|
|
2402
|
-
|
|
2403
|
-
scopeOptionsRegistry.set(scopeName, {
|
|
2404
|
-
schema,
|
|
2405
|
-
relationships,
|
|
2406
|
-
searchSchema: searchSchemaDef,
|
|
2407
|
-
tableName,
|
|
2408
|
-
idProperty,
|
|
2409
|
-
canonicalFieldsMap,
|
|
2410
|
-
})
|
|
2411
|
-
|
|
2412
|
-
const descriptor = await registry.registerResource({
|
|
2413
|
-
tenant: tenantId,
|
|
2414
|
-
resource: scopeName,
|
|
2415
|
-
schema,
|
|
2416
|
-
relationships,
|
|
2417
|
-
canonicalFieldMap: canonicalFieldsMap,
|
|
2418
|
-
idProperty,
|
|
2419
|
-
})
|
|
2420
|
-
|
|
2421
|
-
const updatedStored = scopeOptionsRegistry.get(scopeName) || {}
|
|
2422
|
-
updatedStored.canonicalFieldsMap = descriptor.canonicalFieldMap || updatedStored.canonicalFieldsMap || null
|
|
2423
|
-
scopeOptionsRegistry.set(scopeName, updatedStored)
|
|
2424
|
-
|
|
2425
|
-
if (scope) {
|
|
2426
|
-
scope.scopeOptions = {
|
|
2427
|
-
...(scope.scopeOptions || {}),
|
|
2428
|
-
schema,
|
|
2429
|
-
relationships,
|
|
2430
|
-
searchSchema: searchSchemaDef || undefined,
|
|
2431
|
-
tableName,
|
|
2432
|
-
idProperty,
|
|
2433
|
-
canonicalFieldsMap: descriptor.canonicalFieldMap || scope.scopeOptions?.canonicalFieldsMap || {},
|
|
2434
|
-
}
|
|
2435
|
-
}
|
|
2436
|
-
|
|
2437
|
-
await rehydrateSchemaInfo(scopeName)
|
|
2438
|
-
})
|
|
2439
|
-
|
|
2440
|
-
addScopeMethod('createKnexTable', async ({ scopeName, scopeOptions }) => {
|
|
2441
|
-
const scope = api.resources?.[scopeName] || scopes?.[scopeName]
|
|
2442
|
-
const schemaInput = scopeOptions?.schema || scope?.scopeOptions?.schema || {}
|
|
2443
|
-
const relationshipInput = scopeOptions?.relationships || scope?.scopeOptions?.relationships || {}
|
|
2444
|
-
const storedBefore = scopeOptionsRegistry.get(scopeName) || {}
|
|
2445
|
-
const idProperty = scopeOptions?.idProperty ||
|
|
2446
|
-
scope?.scopeOptions?.idProperty ||
|
|
2447
|
-
storedBefore.idProperty ||
|
|
2448
|
-
scope?.vars?.schemaInfo?.idProperty ||
|
|
2449
|
-
vars.idProperty ||
|
|
2450
|
-
'id'
|
|
2451
|
-
|
|
2452
|
-
const effectiveSchema = { ...schemaInput }
|
|
2453
|
-
if (idProperty && idProperty !== 'id' && !effectiveSchema[idProperty]) {
|
|
2454
|
-
effectiveSchema[idProperty] = { type: 'number' }
|
|
2455
|
-
}
|
|
2456
|
-
|
|
2457
|
-
if (scope) {
|
|
2458
|
-
const existingOptions = scope.scopeOptions || {}
|
|
2459
|
-
scope.scopeOptions = {
|
|
2460
|
-
...existingOptions,
|
|
2461
|
-
schema: {
|
|
2462
|
-
...(existingOptions.schema || {}),
|
|
2463
|
-
...schemaInput,
|
|
2464
|
-
...(idProperty && idProperty !== 'id' && !schemaInput[idProperty] ? { [idProperty]: { type: 'number' } } : {}),
|
|
2465
|
-
},
|
|
2466
|
-
relationships: {
|
|
2467
|
-
...(existingOptions.relationships || {}),
|
|
2468
|
-
...relationshipInput,
|
|
2469
|
-
},
|
|
2470
|
-
searchSchema: existingOptions.searchSchema || scopeOptions?.searchSchema,
|
|
2471
|
-
}
|
|
2472
|
-
}
|
|
2473
|
-
scopeOptionsRegistry.set(scopeName, {
|
|
2474
|
-
schema: scope?.scopeOptions?.schema || effectiveSchema || storedBefore.schema || {},
|
|
2475
|
-
relationships: scope?.scopeOptions?.relationships || relationshipInput || storedBefore.relationships || {},
|
|
2476
|
-
searchSchema: scope?.scopeOptions?.searchSchema || scopeOptions?.searchSchema || storedBefore.searchSchema || null,
|
|
2477
|
-
tableName: scope?.scopeOptions?.tableName || scopeOptions?.tableName || storedBefore.tableName || scopeName,
|
|
2478
|
-
idProperty: scope?.scopeOptions?.idProperty || scopeOptions?.idProperty || storedBefore.idProperty || scope?.vars?.idProperty || vars.idProperty || 'id',
|
|
2479
|
-
canonicalFieldsMap: storedBefore.canonicalFieldsMap || null,
|
|
2480
|
-
})
|
|
2481
|
-
const descriptor = await registry.registerResource({
|
|
2482
|
-
tenant: tenantId,
|
|
2483
|
-
resource: scopeName,
|
|
2484
|
-
schema: effectiveSchema,
|
|
2485
|
-
relationships: relationshipInput,
|
|
2486
|
-
canonicalFieldMap: scopeOptions?.canonicalFieldsMap ||
|
|
2487
|
-
scope?.scopeOptions?.canonicalFieldsMap ||
|
|
2488
|
-
null,
|
|
2489
|
-
idProperty,
|
|
2490
|
-
})
|
|
2491
|
-
if (scope) {
|
|
2492
|
-
scope.scopeOptions = {
|
|
2493
|
-
...(scope.scopeOptions || {}),
|
|
2494
|
-
canonicalFieldsMap: descriptor.canonicalFieldMap || scope.scopeOptions?.canonicalFieldsMap || {},
|
|
2495
|
-
}
|
|
2496
|
-
}
|
|
2497
|
-
const storedAfter = scopeOptionsRegistry.get(scopeName) || {}
|
|
2498
|
-
storedAfter.canonicalFieldsMap = descriptor.canonicalFieldMap || storedAfter.canonicalFieldsMap || null
|
|
2499
|
-
scopeOptionsRegistry.set(scopeName, storedAfter)
|
|
2500
|
-
await rehydrateSchemaInfo(scopeName)
|
|
2501
|
-
})
|
|
2502
|
-
|
|
2503
|
-
addScopeMethod('addKnexFields', async ({ scopeName, params }) => {
|
|
2504
|
-
if (!params?.fields) return
|
|
2505
|
-
const scope = api.resources?.[scopeName] || scopes?.[scopeName]
|
|
2506
|
-
if (scope) {
|
|
2507
|
-
const existingOptions = scope.scopeOptions || {}
|
|
2508
|
-
scope.scopeOptions = {
|
|
2509
|
-
...existingOptions,
|
|
2510
|
-
schema: {
|
|
2511
|
-
...(existingOptions.schema || {}),
|
|
2512
|
-
},
|
|
2513
|
-
relationships: existingOptions.relationships || {},
|
|
2514
|
-
searchSchema: existingOptions.searchSchema,
|
|
2515
|
-
}
|
|
2516
|
-
}
|
|
2517
|
-
const storedBefore = scopeOptionsRegistry.get(scopeName) || {}
|
|
2518
|
-
const canonicalEntries = params.canonicalFieldsMap ? { ...params.canonicalFieldsMap } : null
|
|
2519
|
-
let latestDescriptor = null
|
|
2520
|
-
for (const [fieldName, definition] of Object.entries(params.fields)) {
|
|
2521
|
-
if (scope) {
|
|
2522
|
-
scope.scopeOptions.schema[fieldName] = definition
|
|
2523
|
-
}
|
|
2524
|
-
const canonicalEntry = canonicalEntries ? canonicalEntries[fieldName] : null
|
|
2525
|
-
if (canonicalEntries) {
|
|
2526
|
-
delete canonicalEntries[fieldName]
|
|
2527
|
-
}
|
|
2528
|
-
latestDescriptor = await registry.allocateField({
|
|
2529
|
-
tenant: tenantId,
|
|
2530
|
-
resource: scopeName,
|
|
2531
|
-
fieldName,
|
|
2532
|
-
definition,
|
|
2533
|
-
canonicalField: canonicalEntry,
|
|
2534
|
-
})
|
|
2535
|
-
}
|
|
2536
|
-
if (canonicalEntries && Object.keys(canonicalEntries).length > 0) {
|
|
2537
|
-
const unknown = Object.keys(canonicalEntries).join(', ')
|
|
2538
|
-
throw new Error(`canonicalFieldsMap contains unknown fields for addKnexFields: ${unknown}`)
|
|
2539
|
-
}
|
|
2540
|
-
if (scope && latestDescriptor?.canonicalFieldMap) {
|
|
2541
|
-
scope.scopeOptions.canonicalFieldsMap = latestDescriptor.canonicalFieldMap
|
|
2542
|
-
}
|
|
2543
|
-
if (scope && params.searchSchema) {
|
|
2544
|
-
scope.scopeOptions.searchSchema = {
|
|
2545
|
-
...(scope.scopeOptions.searchSchema || {}),
|
|
2546
|
-
...params.searchSchema,
|
|
2547
|
-
}
|
|
2548
|
-
}
|
|
2549
|
-
const updatedStored = {
|
|
2550
|
-
schema: scope?.scopeOptions?.schema || storedBefore.schema || {},
|
|
2551
|
-
relationships: scope?.scopeOptions?.relationships || storedBefore.relationships || {},
|
|
2552
|
-
searchSchema: scope?.scopeOptions?.searchSchema || storedBefore.searchSchema || null,
|
|
2553
|
-
tableName: scope?.scopeOptions?.tableName || storedBefore.tableName || scopeName,
|
|
2554
|
-
idProperty: scope?.scopeOptions?.idProperty || storedBefore.idProperty || scope?.vars?.idProperty || vars.idProperty || 'id',
|
|
2555
|
-
canonicalFieldsMap: scope?.scopeOptions?.canonicalFieldsMap || storedBefore.canonicalFieldsMap || null,
|
|
2556
|
-
}
|
|
2557
|
-
scopeOptionsRegistry.set(scopeName, updatedStored)
|
|
2558
|
-
await rehydrateSchemaInfo(scopeName)
|
|
372
|
+
await publish(scopeName, descriptor, { compiled: { signature: optionsSignature(candidate), schemaInfo } })
|
|
373
|
+
await notifySchemaChanged(scopeName, descriptor.schemaVersion)
|
|
2559
374
|
})
|
|
2560
375
|
|
|
2561
|
-
|
|
376
|
+
addResourceMethod('alterKnexFields', async () => {
|
|
2562
377
|
throw new Error('alterKnexFields is not supported by AnyAPI Knex plugin yet')
|
|
2563
378
|
})
|
|
2564
379
|
},
|