json-rest-api 1.0.29 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +744 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +634 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +100 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
- package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +36 -0
- package/index.js +1 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +64 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -177
- package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +25 -16
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +13 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +128 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1077
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -395
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +36 -68
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +37 -81
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +53 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -148
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -155
- package/tests/fixtures/api-configs.js +0 -2566
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-boundaries.test.js +0 -154
- package/tests/pagination-contract.test.js +0 -122
- package/tests/pagination-cursor-multifield.test.js +0 -641
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/temporal-boundaries.test.js +0 -208
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -1,70 +1,26 @@
|
|
|
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/querying-writing/
|
|
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 { RestApiResourceError } from '../../lib/rest-api-errors.js'
|
|
9
|
+
import { compileSchemas } from './lib/querying-writing/compile-schemas.js'
|
|
10
|
+
import { snapshotResourceConfiguration } from './lib/querying-writing/schema-helpers.js'
|
|
11
|
+
import { assertFieldNameMap } from './lib/querying-writing/field-utils.js'
|
|
12
|
+
import { polymorphicFiltersHook, crossTableFiltersHook, basicFiltersHook } from './lib/querying/knex-query-helpers.js'
|
|
13
|
+
import { createStorageAdapterLookup } from './lib/storage/storage-adapter.js'
|
|
14
|
+
import { getDatabaseCapabilities } from './lib/querying-writing/database-capabilities.js'
|
|
59
15
|
|
|
60
16
|
const DEFAULT_TENANT = 'default'
|
|
61
|
-
const LINKS_TABLE = 'any_links'
|
|
62
17
|
|
|
63
18
|
export const RestApiAnyapiKnexPlugin = {
|
|
64
19
|
name: 'rest-api-anyapi-knex',
|
|
65
20
|
dependencies: ['rest-api'],
|
|
66
21
|
|
|
67
|
-
async install ({ helpers,
|
|
22
|
+
async install ({ helpers, pluginOptions, api, log, addHook, addResourceMethod, scopes }) {
|
|
23
|
+
log = createEnhancedLogger(log)
|
|
68
24
|
const options = pluginOptions || {}
|
|
69
25
|
const knex = options.knex
|
|
70
26
|
const tenantId = options.tenantId || DEFAULT_TENANT
|
|
@@ -83,1984 +39,75 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
83
39
|
api.knex = {
|
|
84
40
|
instance: knex,
|
|
85
41
|
helpers: {},
|
|
42
|
+
capabilities: await getDatabaseCapabilities(knex, log),
|
|
86
43
|
}
|
|
87
44
|
|
|
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
|
-
}
|
|
108
|
-
|
|
109
|
-
api.knex.helpers.getStorageAdapter = getScopeStorageAdapter
|
|
110
|
-
helpers.getStorageAdapter = getScopeStorageAdapter
|
|
111
|
-
|
|
112
|
-
const scopeOptionsRegistry = new Map()
|
|
113
|
-
|
|
114
|
-
helpers.newTransaction = async () => knex.transaction()
|
|
115
|
-
|
|
116
|
-
addHook('release', 'anyapi-knex-release', {}, async ({ api }) => {
|
|
117
|
-
if (api.knex?.instance) {
|
|
118
|
-
await api.knex.instance.destroy()
|
|
119
|
-
}
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
const getDescriptor = async (scopeName) => {
|
|
123
|
-
const descriptor = await registry.getDescriptor(tenantId, scopeName)
|
|
124
|
-
if (!descriptor) {
|
|
125
|
-
throw new Error(`Descriptor not found for resource '${scopeName}'`)
|
|
126
|
-
}
|
|
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
|
-
return descriptor
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const decorateResourceLinks = ({ resource, scope, scopeName, context }) => {
|
|
139
|
-
if (!resource || !scope || !scopeName || !context) return
|
|
140
|
-
|
|
141
|
-
resource.links = resource.links || {}
|
|
142
|
-
resource.links.self = buildResourceUrl(context, scope, scopeName, resource.id)
|
|
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
|
-
}
|
|
240
|
-
|
|
241
|
-
const applySortingToQuery = ({ query, sort, descriptor, scope, adapter, before = false, queryFieldRuntimeByField = new Map() }) => {
|
|
242
|
-
const effectiveSort = buildEffectiveSortList(sort, {
|
|
243
|
-
defaultSort: scope?.vars?.defaultSort,
|
|
244
|
-
idField: 'id'
|
|
245
|
-
})
|
|
246
|
-
const descriptors = []
|
|
247
|
-
|
|
248
|
-
for (const entry of effectiveSort) {
|
|
249
|
-
const { field, direction } = parseSortEntry(entry)
|
|
250
|
-
const queryDirection = before ? (direction === 'asc' ? 'desc' : 'asc') : direction
|
|
251
|
-
const queryFieldRuntime = queryFieldRuntimeByField.get(field)
|
|
252
|
-
if (queryFieldRuntime) {
|
|
253
|
-
applyQueryFieldOrder(query, queryFieldRuntime, queryDirection.toUpperCase(), before ? 'first' : 'last')
|
|
254
|
-
descriptors.push({
|
|
255
|
-
field,
|
|
256
|
-
direction,
|
|
257
|
-
definition: queryFieldRuntime.definition,
|
|
258
|
-
isRelationship: false,
|
|
259
|
-
queryFieldRuntime,
|
|
260
|
-
})
|
|
261
|
-
continue
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const fieldInfo = resolveFieldInfo(descriptor, field)
|
|
265
|
-
if (!fieldInfo?.column) continue
|
|
266
|
-
query.orderByRaw(`?? IS NULL ${before ? 'DESC' : 'ASC'}`, [adapter.translateColumn(fieldInfo.column)])
|
|
267
|
-
query.orderBy(fieldInfo.column, queryDirection)
|
|
268
|
-
descriptors.push({
|
|
269
|
-
field,
|
|
270
|
-
column: fieldInfo.column,
|
|
271
|
-
direction,
|
|
272
|
-
definition: fieldInfo.definition || null,
|
|
273
|
-
isRelationship: fieldInfo.isRelationship || false,
|
|
274
|
-
})
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (descriptors.length === 0) {
|
|
278
|
-
descriptors.push({
|
|
279
|
-
field: 'id',
|
|
280
|
-
column: 'id',
|
|
281
|
-
direction: 'asc',
|
|
282
|
-
definition: { type: 'id' },
|
|
283
|
-
isRelationship: false,
|
|
284
|
-
})
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return descriptors
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const applyPaginationToQuery = ({
|
|
291
|
-
query,
|
|
292
|
-
scope,
|
|
293
|
-
queryParams,
|
|
294
|
-
sortDescriptors,
|
|
295
|
-
adapter,
|
|
296
|
-
}) => {
|
|
297
|
-
const pageParams = queryParams?.page || {}
|
|
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
|
-
}],
|
|
361
|
-
})
|
|
362
|
-
}
|
|
363
|
-
return Math.min(Math.trunc(requestedSize), maxLimit)
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
if (hasCursorParam || (hasPageSize && !hasPageNumber)) {
|
|
367
|
-
if (pageParams.after && pageParams.before) {
|
|
368
|
-
throw new RestApiValidationError(
|
|
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
|
-
}
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
const buildLinkIdentity = (canonicalRow) => ({
|
|
466
|
-
tenant_id: canonicalRow.tenant_id,
|
|
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
|
|
478
|
-
try {
|
|
479
|
-
targetDescriptor = await getDescriptor(relInfo.target)
|
|
480
|
-
} catch (error) {
|
|
481
|
-
return null
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
const entries = Object.entries(targetDescriptor.manyToMany || {})
|
|
485
|
-
for (const [candidateName, candidateInfo] of entries) {
|
|
486
|
-
const candidateTarget = candidateInfo.target || descriptor.resource
|
|
487
|
-
if (candidateTarget !== descriptor.resource) continue
|
|
488
|
-
|
|
489
|
-
if (relInfo.through && candidateInfo.through && relInfo.through !== candidateInfo.through) {
|
|
490
|
-
continue
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
if (
|
|
494
|
-
relInfo.foreignKey && relInfo.otherKey &&
|
|
495
|
-
candidateInfo.foreignKey && candidateInfo.otherKey
|
|
496
|
-
) {
|
|
497
|
-
const foreignMatches = relInfo.foreignKey === candidateInfo.otherKey
|
|
498
|
-
const otherMatches = relInfo.otherKey === candidateInfo.foreignKey
|
|
499
|
-
if (!foreignMatches || !otherMatches) continue
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
const relationshipKey = candidateInfo.relationship ||
|
|
503
|
-
`${targetDescriptor.tenant}:${targetDescriptor.resource}:${candidateName}`
|
|
504
|
-
|
|
505
|
-
return {
|
|
506
|
-
descriptor: targetDescriptor,
|
|
507
|
-
relName: candidateName,
|
|
508
|
-
relInfo: candidateInfo,
|
|
509
|
-
relationshipKey,
|
|
510
|
-
}
|
|
511
|
-
}
|
|
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
|
-
}
|
|
525
|
-
|
|
526
|
-
const loadLinkRowsForResource = async ({ descriptor, relationshipKey, resourceId, db }) => {
|
|
527
|
-
const ownerId = normalizeId(resourceId)
|
|
528
|
-
if (ownerId == null) return new Map()
|
|
529
|
-
|
|
530
|
-
const rows = await db(LINKS_TABLE)
|
|
531
|
-
.where('tenant_id', descriptor.tenant)
|
|
532
|
-
.andWhere((builder) => {
|
|
533
|
-
builder
|
|
534
|
-
.where((q) => {
|
|
535
|
-
q.where('relationship', relationshipKey)
|
|
536
|
-
.andWhere('left_resource', descriptor.resource)
|
|
537
|
-
.andWhere('left_id', ownerId)
|
|
538
|
-
})
|
|
539
|
-
.orWhere((q) => {
|
|
540
|
-
q.where('inverse_relationship', relationshipKey)
|
|
541
|
-
.andWhere('right_resource', descriptor.resource)
|
|
542
|
-
.andWhere('right_id', ownerId)
|
|
543
|
-
})
|
|
544
|
-
})
|
|
545
|
-
.select('id', 'relationship', 'inverse_relationship', 'left_resource', 'left_id', 'right_resource', 'right_id')
|
|
546
|
-
|
|
547
|
-
const map = new Map()
|
|
548
|
-
|
|
549
|
-
for (const row of rows) {
|
|
550
|
-
let relatedResource
|
|
551
|
-
let relatedId
|
|
552
|
-
let otherKey = null
|
|
553
|
-
|
|
554
|
-
if (row.relationship === relationshipKey && row.left_resource === descriptor.resource) {
|
|
555
|
-
relatedResource = row.right_resource
|
|
556
|
-
relatedId = normalizeId(row.right_id)
|
|
557
|
-
otherKey = row.inverse_relationship || null
|
|
558
|
-
} else if (row.inverse_relationship === relationshipKey && row.right_resource === descriptor.resource) {
|
|
559
|
-
relatedResource = row.left_resource
|
|
560
|
-
relatedId = normalizeId(row.left_id)
|
|
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
|
-
})
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
return map
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const fetchLinksForParents = async ({ descriptor, relationshipKey, parentIds, db }) => {
|
|
592
|
-
const normalizedIds = parentIds
|
|
593
|
-
.map((id) => normalizeId(id))
|
|
594
|
-
.filter((id) => id !== null)
|
|
595
|
-
|
|
596
|
-
if (normalizedIds.length === 0) return []
|
|
597
|
-
|
|
598
|
-
const rows = await db(LINKS_TABLE)
|
|
599
|
-
.where('tenant_id', descriptor.tenant)
|
|
600
|
-
.andWhere((builder) => {
|
|
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
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
const applyTargetScopeFilters = async ({
|
|
643
|
-
query,
|
|
644
|
-
resourceName,
|
|
645
|
-
tableName,
|
|
646
|
-
context,
|
|
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
|
-
|
|
673
|
-
return {
|
|
674
|
-
query: unwrapQueryBuilderState(queryState, query)
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
const filterVisibleIdentifiers = async ({ identifiers, context }) => {
|
|
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
|
-
}
|
|
722
|
-
|
|
723
|
-
const ensureTargetsExist = async ({ relInfo, relData, context }) => {
|
|
724
|
-
const relArray = Array.isArray(relData) ? relData : []
|
|
725
|
-
if (relArray.length === 0) return
|
|
726
|
-
const targetScope = api.resources[relInfo.target]
|
|
727
|
-
if (!targetScope) {
|
|
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
|
-
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
const attachLinks = async ({ descriptor, relInfo, relationshipKey, inverseRelationshipKey, leftId, relData, db }) => {
|
|
746
|
-
const relArray = Array.isArray(relData) ? relData : []
|
|
747
|
-
if (relArray.length === 0) return
|
|
748
|
-
|
|
749
|
-
for (const identifier of relArray) {
|
|
750
|
-
const rightId = normalizeId(identifier.id)
|
|
751
|
-
if (rightId == null) continue
|
|
752
|
-
|
|
753
|
-
const canonical = canonicalizeLinkPair({
|
|
754
|
-
tenantId: descriptor.tenant,
|
|
755
|
-
relationshipKey,
|
|
756
|
-
inverseRelationshipKey,
|
|
757
|
-
leftResource: descriptor.resource,
|
|
758
|
-
leftId,
|
|
759
|
-
rightResource: relInfo.target,
|
|
760
|
-
rightId,
|
|
761
|
-
})
|
|
762
|
-
|
|
763
|
-
if (!canonical) continue
|
|
764
|
-
|
|
765
|
-
const identity = buildLinkIdentity(canonical)
|
|
766
|
-
|
|
767
|
-
const existing = await db(LINKS_TABLE)
|
|
768
|
-
.where(identity)
|
|
769
|
-
.first()
|
|
770
|
-
|
|
771
|
-
if (existing) {
|
|
772
|
-
if (!existing.inverse_relationship && canonical.inverse_relationship) {
|
|
773
|
-
await db(LINKS_TABLE)
|
|
774
|
-
.where({ id: existing.id })
|
|
775
|
-
.update({
|
|
776
|
-
inverse_relationship: canonical.inverse_relationship,
|
|
777
|
-
updated_at: db.fn.now(),
|
|
778
|
-
})
|
|
779
|
-
}
|
|
780
|
-
continue
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
await db(LINKS_TABLE).insert({
|
|
784
|
-
...canonical,
|
|
785
|
-
payload: null,
|
|
786
|
-
created_at: db.fn.now(),
|
|
787
|
-
updated_at: db.fn.now(),
|
|
788
|
-
})
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
api.anyapi.links = {
|
|
793
|
-
attachMany: async ({ context, scopeName, relName, relDef, relData }) => {
|
|
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
|
-
)
|
|
905
|
-
|
|
906
|
-
const existingIds = new Set(existingMap.keys())
|
|
907
|
-
|
|
908
|
-
const toAdd = [...desired].filter((id) => !existingIds.has(id))
|
|
909
|
-
const toRemove = [...existingIds].filter((id) => !desired.has(id))
|
|
910
|
-
|
|
911
|
-
if (toAdd.length > 0) {
|
|
912
|
-
const data = toAdd.map((id) => ({ id }))
|
|
913
|
-
await attachLinks({
|
|
914
|
-
descriptor,
|
|
915
|
-
relInfo,
|
|
916
|
-
relationshipKey,
|
|
917
|
-
inverseRelationshipKey,
|
|
918
|
-
leftId,
|
|
919
|
-
relData: data,
|
|
920
|
-
db,
|
|
921
|
-
})
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
if (toRemove.length > 0) {
|
|
925
|
-
for (const id of toRemove) {
|
|
926
|
-
const entry = existingMap.get(id)
|
|
927
|
-
if (!entry?.canonical) continue
|
|
928
|
-
await db(LINKS_TABLE)
|
|
929
|
-
.where(buildLinkIdentity(entry.canonical))
|
|
930
|
-
.delete()
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
const removeLinks = async ({ descriptor, relInfo, relationshipKey, inverseRelationshipKey, leftId, relData, db }) => {
|
|
936
|
-
const relArray = Array.isArray(relData) ? relData : []
|
|
937
|
-
if (relArray.length === 0) return
|
|
938
|
-
|
|
939
|
-
const existingMap = await loadLinkRowsForResource({
|
|
940
|
-
descriptor,
|
|
941
|
-
relationshipKey,
|
|
942
|
-
resourceId: leftId,
|
|
943
|
-
db,
|
|
944
|
-
})
|
|
945
|
-
|
|
946
|
-
for (const identifier of relArray) {
|
|
947
|
-
const relatedId = normalizeId(identifier.id)
|
|
948
|
-
if (relatedId == null) continue
|
|
949
|
-
|
|
950
|
-
const entry = existingMap.get(relatedId)
|
|
951
|
-
if (entry?.canonical) {
|
|
952
|
-
await db(LINKS_TABLE)
|
|
953
|
-
.where(buildLinkIdentity(entry.canonical))
|
|
954
|
-
.delete()
|
|
955
|
-
continue
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
const canonical = canonicalizeLinkPair({
|
|
959
|
-
tenantId: descriptor.tenant,
|
|
960
|
-
relationshipKey,
|
|
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,
|
|
982
|
-
})
|
|
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
|
-
})
|
|
45
|
+
const getScopeStorageAdapter = createStorageAdapterLookup({
|
|
46
|
+
knex,
|
|
47
|
+
getResource: scopeName => api.resources?.[scopeName] || scopes?.[scopeName]
|
|
48
|
+
})
|
|
1913
49
|
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
descriptor,
|
|
1917
|
-
context,
|
|
1918
|
-
})
|
|
50
|
+
api.knex.helpers.getStorageAdapter = getScopeStorageAdapter
|
|
51
|
+
helpers.getStorageAdapter = getScopeStorageAdapter
|
|
1919
52
|
|
|
1920
|
-
|
|
1921
|
-
parentResources: [data],
|
|
1922
|
-
descriptor,
|
|
1923
|
-
context,
|
|
1924
|
-
})
|
|
53
|
+
const scopeOptionsRegistry = new Map()
|
|
1925
54
|
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
response.included = included
|
|
1929
|
-
}
|
|
55
|
+
/** @type {import('../../lib/transaction-types.js').TransactionFactory} */
|
|
56
|
+
helpers.newTransaction = context => createKnexTransaction(knex, context)
|
|
1930
57
|
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
58
|
+
addHook('release', 'anyapi-knex-release', {}, async ({ api }) => {
|
|
59
|
+
if (api.knex?.instance) {
|
|
60
|
+
await api.knex.instance.destroy()
|
|
1934
61
|
}
|
|
62
|
+
})
|
|
1935
63
|
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
decorateResourceLinks({
|
|
1941
|
-
resource: includeResource,
|
|
1942
|
-
scope: targetScope,
|
|
1943
|
-
scopeName: includeResource.type,
|
|
1944
|
-
context,
|
|
1945
|
-
})
|
|
1946
|
-
}
|
|
64
|
+
const getDescriptor = (scopeName) => {
|
|
65
|
+
const descriptor = api.resources?.[scopeName]?.vars?.schemaInfo?.descriptor
|
|
66
|
+
if (!descriptor) {
|
|
67
|
+
throw new Error(`Descriptor not found for resource '${scopeName}'`)
|
|
1947
68
|
}
|
|
1948
|
-
|
|
1949
|
-
return response
|
|
69
|
+
return descriptor
|
|
1950
70
|
}
|
|
1951
71
|
|
|
1952
|
-
const
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
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
|
-
}
|
|
72
|
+
const linkStore = createCanonicalLinkStore({ api, getDescriptor, getScopeStorageAdapter })
|
|
73
|
+
api.anyapi.links = linkStore.methods
|
|
74
|
+
helpers.dataRelatedIdsQuery = linkStore.dataRelatedIdsQuery
|
|
75
|
+
const { buildIncludes, attachReverseRelationships, attachManyToManyRelationships } = createCanonicalRelationshipReader({
|
|
76
|
+
api, knex, getDescriptor, getScopeStorageAdapter, linkStore
|
|
77
|
+
})
|
|
1967
78
|
|
|
79
|
+
const refreshStorageDescriptor = async (scopeName, compiledSchemaInfo) => {
|
|
80
|
+
const scope = api.resources[scopeName]
|
|
81
|
+
const schemaInfo = compiledSchemaInfo || scope.vars.schemaInfo
|
|
1968
82
|
const descriptor = await registry.getDescriptor(tenantId, scopeName)
|
|
1969
|
-
if (!descriptor)
|
|
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
|
-
|
|
83
|
+
if (!descriptor) throw new Error(`Descriptor not found for resource '${scopeName}'`)
|
|
84
|
+
descriptor.idProperty = schemaInfo.idProperty
|
|
2016
85
|
scope.vars.schemaInfo = {
|
|
2017
|
-
...
|
|
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,
|
|
86
|
+
...schemaInfo,
|
|
2030
87
|
descriptor,
|
|
2031
|
-
canonicalFieldMap: descriptor.canonicalFieldMap
|
|
88
|
+
canonicalFieldMap: descriptor.canonicalFieldMap,
|
|
2032
89
|
}
|
|
2033
|
-
|
|
2034
90
|
getScopeStorageAdapter(scopeName)
|
|
2035
|
-
|
|
2036
|
-
scope.scopeOptions = {
|
|
2037
|
-
...(scope.scopeOptions || {}),
|
|
2038
|
-
canonicalFieldsMap: descriptor.canonicalFieldMap || scope.scopeOptions?.canonicalFieldsMap || {},
|
|
2039
|
-
}
|
|
2040
|
-
|
|
2041
91
|
scopeOptionsRegistry.set(scopeName, {
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
searchSchema: scope.scopeOptions?.searchSchema || storedOptions.searchSchema || null,
|
|
2045
|
-
tableName,
|
|
2046
|
-
idProperty,
|
|
2047
|
-
canonicalFieldsMap: descriptor.canonicalFieldMap || {},
|
|
92
|
+
...scopeOptionsRegistry.get(scopeName),
|
|
93
|
+
canonicalFieldsMap: descriptor.canonicalFieldMap,
|
|
2048
94
|
})
|
|
2049
95
|
}
|
|
2050
96
|
|
|
2051
|
-
const
|
|
2052
|
-
const
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
97
|
+
const buildSearchMembershipQuery = ({ sourceScopeName, relationshipName, targetScopeName, foreignKey, otherKey, db }) => {
|
|
98
|
+
const info = linkStore.getManyToManyInfo(sourceScopeName, relationshipName)
|
|
99
|
+
if (!info) throw new RestApiResourceError(`Relationship '${sourceScopeName}.${relationshipName}' not found`, { subtype: 'related_type_not_found' })
|
|
100
|
+
const links = linkStore.buildRelatedLinkQuery({ ...info, db, allParents: true })
|
|
101
|
+
const pivotAdapter = getScopeStorageAdapter(targetScopeName)
|
|
102
|
+
return {
|
|
103
|
+
query: db.select({
|
|
104
|
+
[pivotAdapter.translateColumn(foreignKey)]: 'parentId',
|
|
105
|
+
[pivotAdapter.translateColumn(otherKey)]: 'childId'
|
|
106
|
+
}).from(links.as('search_membership'))
|
|
2059
107
|
}
|
|
2060
|
-
return { resourceToTableName, tableNameToResource }
|
|
2061
108
|
}
|
|
2062
109
|
|
|
2063
|
-
const queryHookDependencies = { log, scopes: api.resources, knex, getStorageAdapter: getScopeStorageAdapter }
|
|
110
|
+
const queryHookDependencies = { log, scopes: api.resources, knex, getStorageAdapter: getScopeStorageAdapter, buildSearchMembershipQuery }
|
|
2064
111
|
|
|
2065
112
|
const applyBuiltInAnyApiQueryFilters = async (context) => {
|
|
2066
113
|
const hookParams = { context }
|
|
@@ -2069,496 +116,80 @@ export const RestApiAnyapiKnexPlugin = {
|
|
|
2069
116
|
await basicFiltersHook(hookParams, queryHookDependencies)
|
|
2070
117
|
}
|
|
2071
118
|
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
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
|
-
}
|
|
119
|
+
Object.assign(helpers, createCanonicalDataHelpers({
|
|
120
|
+
api,
|
|
121
|
+
knex,
|
|
122
|
+
getScopeStorageAdapter,
|
|
123
|
+
getDescriptor,
|
|
124
|
+
linkStore,
|
|
125
|
+
buildIncludes,
|
|
126
|
+
attachReverseRelationships,
|
|
127
|
+
attachManyToManyRelationships,
|
|
128
|
+
applyBuiltInAnyApiQueryFilters
|
|
129
|
+
}))
|
|
2385
130
|
|
|
2386
|
-
addHook('
|
|
131
|
+
addHook('resource:added', 'anyapi-register-resource', {}, async ({ context }) => {
|
|
2387
132
|
const { scopeName, scopeOptions = {} } = context
|
|
2388
|
-
const scope = api.
|
|
2389
|
-
const
|
|
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
|
-
|
|
133
|
+
const scope = api.resources[scopeName]
|
|
134
|
+
const { schemaStructure, computed, schemaRelationships, idProperty } = scope.vars.schemaInfo
|
|
2412
135
|
const descriptor = await registry.registerResource({
|
|
2413
136
|
tenant: tenantId,
|
|
2414
137
|
resource: scopeName,
|
|
2415
|
-
schema,
|
|
2416
|
-
relationships,
|
|
2417
|
-
canonicalFieldMap: canonicalFieldsMap,
|
|
2418
|
-
idProperty
|
|
138
|
+
schema: { ...schemaStructure, ...computed },
|
|
139
|
+
relationships: schemaRelationships,
|
|
140
|
+
canonicalFieldMap: scopeOptions.canonicalFieldsMap || null,
|
|
141
|
+
idProperty
|
|
2419
142
|
})
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
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)
|
|
143
|
+
scopeOptionsRegistry.set(scopeName, snapshotResourceConfiguration({
|
|
144
|
+
...scopeOptions,
|
|
145
|
+
idProperty,
|
|
146
|
+
canonicalFieldsMap: descriptor.canonicalFieldMap
|
|
147
|
+
}))
|
|
148
|
+
await refreshStorageDescriptor(scopeName)
|
|
2438
149
|
})
|
|
2439
150
|
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
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)
|
|
151
|
+
addResourceMethod('createKnexTable', async ({ scopeName }) => {
|
|
152
|
+
await ensureAnyApiSchema(knex)
|
|
153
|
+
await refreshStorageDescriptor(scopeName)
|
|
2501
154
|
})
|
|
2502
155
|
|
|
2503
|
-
|
|
2504
|
-
if (!params?.fields)
|
|
2505
|
-
|
|
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
|
-
}
|
|
156
|
+
addResourceMethod('addKnexFields', async ({ scopeName, params }) => {
|
|
157
|
+
if (!params?.fields || typeof params.fields !== 'object' || Array.isArray(params.fields)) {
|
|
158
|
+
throw new Error('fields parameter is required for addKnexFields')
|
|
2516
159
|
}
|
|
160
|
+
assertFieldNameMap(params.fields, `added fields in '${scopeName}'`)
|
|
161
|
+
assertFieldNameMap(params.searchSchema, `added search schema in '${scopeName}'`)
|
|
162
|
+
assertFieldNameMap(params.canonicalFieldsMap, `added canonical fields in '${scopeName}'`)
|
|
163
|
+
const scope = api.resources[scopeName]
|
|
2517
164
|
const storedBefore = scopeOptionsRegistry.get(scopeName) || {}
|
|
2518
|
-
const
|
|
2519
|
-
|
|
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(', ')
|
|
165
|
+
const unknown = Object.keys(params.canonicalFieldsMap || {}).filter(field => !Object.hasOwn(params.fields, field))
|
|
166
|
+
if (unknown.length) {
|
|
2538
167
|
throw new Error(`canonicalFieldsMap contains unknown fields for addKnexFields: ${unknown}`)
|
|
2539
168
|
}
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
169
|
+
const updatedStored = snapshotResourceConfiguration({
|
|
170
|
+
...storedBefore,
|
|
171
|
+
schema: { ...storedBefore.schema, ...params.fields },
|
|
172
|
+
searchSchema: { ...storedBefore.searchSchema, ...params.searchSchema }
|
|
173
|
+
})
|
|
174
|
+
const compiled = { scopeOptions: updatedStored, vars: { idProperty: scope.vars.idProperty } }
|
|
175
|
+
await compileSchemas(compiled, { context: { scopeName }, runHooks: (name, context) => api.runHooks(name, context) })
|
|
176
|
+
await api.transaction(async transaction => {
|
|
177
|
+
for (const fieldName of Object.keys(params.fields)) {
|
|
178
|
+
await registry.allocateField({
|
|
179
|
+
tenant: tenantId,
|
|
180
|
+
resource: scopeName,
|
|
181
|
+
fieldName,
|
|
182
|
+
definition: compiled.vars.schemaInfo.schemaStructure[fieldName] || compiled.vars.schemaInfo.computed[fieldName],
|
|
183
|
+
canonicalField: params.canonicalFieldsMap?.[fieldName]
|
|
184
|
+
}, { transaction })
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
registry.invalidateDescriptor(tenantId, scopeName)
|
|
2557
188
|
scopeOptionsRegistry.set(scopeName, updatedStored)
|
|
2558
|
-
await
|
|
189
|
+
await refreshStorageDescriptor(scopeName, compiled.vars.schemaInfo)
|
|
2559
190
|
})
|
|
2560
191
|
|
|
2561
|
-
|
|
192
|
+
addResourceMethod('alterKnexFields', async () => {
|
|
2562
193
|
throw new Error('alterKnexFields is not supported by AnyAPI Knex plugin yet')
|
|
2563
194
|
})
|
|
2564
195
|
},
|