json-rest-api 1.0.28 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +746 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +634 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +100 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +90 -31
- package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +36 -0
- package/index.js +1 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +64 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -174
- package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +25 -16
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +67 -330
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +205 -30
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +20 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +238 -187
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +111 -287
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +109 -44
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +142 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2509
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1069
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -385
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -145
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +38 -69
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +39 -82
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +53 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1238
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -150
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -118
- package/tests/fixtures/api-configs.js +0 -2509
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-cursor-multifield.test.js +0 -643
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
import { createKnexTransaction } from '../../../../lib/knex-transaction.js'
|
|
2
|
+
import { applyInsertReturning } from '../querying-writing/database-capabilities.js'
|
|
1
3
|
import { DEFAULT_CANONICAL_CONFIG, SLOT_LIMITS, SLOT_POOLS, TYPE_TO_POOL } from './schema-utils.js'
|
|
4
|
+
import { beginWriteTransaction, commitTransaction, endWriteOperation, getOperationDiagnosticContext, rollbackAfterError, wrapUnexpectedError, wrapWriteError } from '../../../../lib/error-context.js'
|
|
5
|
+
import { createEnhancedLogger } from '../../../../lib/enhanced-logger.js'
|
|
6
|
+
import { assertFieldName, assertFieldNameMap } from '../querying-writing/field-utils.js'
|
|
7
|
+
import { validateRelationshipFieldNames, validateSchemaFieldNames } from '../querying-writing/scope-validations.js'
|
|
2
8
|
|
|
3
9
|
const clone = (value) => {
|
|
4
10
|
if (typeof structuredClone === 'function') {
|
|
@@ -8,6 +14,7 @@ const clone = (value) => {
|
|
|
8
14
|
}
|
|
9
15
|
|
|
10
16
|
const SUPPORTED_TYPES = new Set([...TYPE_TO_POOL.keys(), 'id'])
|
|
17
|
+
const MAX_CACHED_DESCRIPTORS = 100
|
|
11
18
|
|
|
12
19
|
const resolveIdProperty = (schema = {}, explicitIdProperty = null) => {
|
|
13
20
|
if (explicitIdProperty) return explicitIdProperty
|
|
@@ -41,12 +48,14 @@ const SLOT_PATTERNS = {
|
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
const BELONGS_TO_ID_PATTERN = /^rel_(\d+)_id$/
|
|
44
|
-
const BELONGS_TO_TYPE_PATTERN = /^rel_(\d+)_type$/
|
|
45
51
|
|
|
46
52
|
const parseSimpleSlotColumn = (column) => {
|
|
47
53
|
for (const [type, pattern] of Object.entries(SLOT_PATTERNS)) {
|
|
48
54
|
const match = pattern.exec(column)
|
|
49
55
|
if (match) {
|
|
56
|
+
if (!SLOT_POOLS[type].includes(column)) {
|
|
57
|
+
throw new Error(`Canonical slot '${column}' is out of range`)
|
|
58
|
+
}
|
|
50
59
|
return { slotType: type, slotIndex: Number(match[1]) }
|
|
51
60
|
}
|
|
52
61
|
}
|
|
@@ -62,10 +71,12 @@ const parseBelongsToSlots = ({ idSlot, typeSlot }) => {
|
|
|
62
71
|
throw new Error(`Invalid belongsTo id slot '${idSlot}'`)
|
|
63
72
|
}
|
|
64
73
|
const index = Number(idMatch[1])
|
|
74
|
+
if (index < 1 || index > SLOT_LIMITS.belongsTo || idSlot !== `rel_${index}_id`) {
|
|
75
|
+
throw new Error(`BelongsTo slot '${idSlot}' is out of range`)
|
|
76
|
+
}
|
|
65
77
|
const expectedTypeSlot = `rel_${index}_type`
|
|
66
78
|
const actualTypeSlot = typeSlot || expectedTypeSlot
|
|
67
|
-
|
|
68
|
-
if (!typeMatch || Number(typeMatch[1]) !== index) {
|
|
79
|
+
if (actualTypeSlot !== expectedTypeSlot) {
|
|
69
80
|
throw new Error(`BelongsTo type slot '${actualTypeSlot}' must correspond to id slot '${idSlot}'`)
|
|
70
81
|
}
|
|
71
82
|
return { slotType: 'belongsTo', slotIndex: index, idColumn: idSlot, typeColumn: actualTypeSlot }
|
|
@@ -82,49 +93,56 @@ export class AnyapiRegistry {
|
|
|
82
93
|
}
|
|
83
94
|
|
|
84
95
|
#key (tenant, resource) {
|
|
85
|
-
return
|
|
96
|
+
return JSON.stringify([tenant, resource])
|
|
86
97
|
}
|
|
87
98
|
|
|
88
|
-
|
|
89
|
-
this.#validateDefinition(definition)
|
|
90
|
-
const descriptor = await this.#register(definition, options.transaction)
|
|
99
|
+
#rememberDescriptor (descriptor) {
|
|
91
100
|
const key = this.#key(descriptor.tenant, descriptor.resource)
|
|
101
|
+
this.cache.delete(key)
|
|
92
102
|
this.cache.set(key, descriptor)
|
|
93
|
-
|
|
103
|
+
if (this.cache.size > MAX_CACHED_DESCRIPTORS) this.cache.delete(this.cache.keys().next().value)
|
|
94
104
|
}
|
|
95
105
|
|
|
96
|
-
async
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
106
|
+
async registerResource (definition, options = {}) {
|
|
107
|
+
const context = { method: 'registerResource', tenant: definition?.tenant, resource: definition?.resource }
|
|
108
|
+
try {
|
|
109
|
+
this.#validateDefinition(definition)
|
|
110
|
+
const descriptor = await this.#register(definition, options.transaction, context)
|
|
111
|
+
if (!options.transaction) this.#rememberDescriptor(descriptor)
|
|
112
|
+
return clone(descriptor)
|
|
113
|
+
} catch (error) {
|
|
114
|
+
throw wrapWriteError(error, context, context.transaction || options.transaction)
|
|
115
|
+
} finally {
|
|
116
|
+
endWriteOperation(context)
|
|
104
117
|
}
|
|
118
|
+
}
|
|
105
119
|
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (info?.slot) {
|
|
115
|
-
usedSlots.add(info.slot)
|
|
120
|
+
async allocateField (params, options = {}) {
|
|
121
|
+
const context = { method: 'allocateField' }
|
|
122
|
+
let trx; let managed = false
|
|
123
|
+
try {
|
|
124
|
+
const { tenant, resource, fieldName, definition, canonicalField } = params || {}
|
|
125
|
+
Object.assign(context, { tenant, resource })
|
|
126
|
+
if (!tenant || !resource || !fieldName || !definition) {
|
|
127
|
+
throw new Error('allocateField requires tenant, resource, fieldName, and definition')
|
|
116
128
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
129
|
+
validateSchemaFieldNames({ [fieldName]: definition }, resource)
|
|
130
|
+
|
|
131
|
+
managed = !options.transaction
|
|
132
|
+
trx = await beginWriteTransaction(context, options.transaction, ownerContext => createKnexTransaction(this.knex, ownerContext))
|
|
133
|
+
const descriptor = await this.#loadDescriptor(tenant, resource, trx)
|
|
134
|
+
if (!descriptor) throw new Error(`Resource '${resource}' not registered for tenant '${tenant}'`)
|
|
135
|
+
if (fieldName === descriptor.idProperty || Object.hasOwn(descriptor.schema, fieldName)) {
|
|
136
|
+
throw new Error(`Field '${resource}.${fieldName}' already exists`)
|
|
137
|
+
}
|
|
138
|
+
if (canonicalField && (definition.belongsToPolymorphic || definition.computed || definition.virtual)) {
|
|
139
|
+
throw new Error(`canonicalFieldsMap should not include non-stored field '${fieldName}'`)
|
|
121
140
|
}
|
|
122
|
-
|
|
141
|
+
const usedSlots = new Set(Object.values(descriptor.fields).map(info => info.slot))
|
|
142
|
+
for (const info of Object.values(descriptor.belongsTo)) {
|
|
123
143
|
usedSlots.add(info.idColumn)
|
|
144
|
+
usedSlots.add(info.typeColumn)
|
|
124
145
|
}
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
try {
|
|
128
146
|
const resourceRow = await trx('any_resource_configs')
|
|
129
147
|
.where({ tenant_id: tenant, resource })
|
|
130
148
|
.first()
|
|
@@ -152,144 +170,116 @@ export class AnyapiRegistry {
|
|
|
152
170
|
alias,
|
|
153
171
|
meta_json: JSON.stringify({ types }),
|
|
154
172
|
})
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
173
|
+
} else if (!definition.computed && !definition.virtual) {
|
|
174
|
+
let override = null
|
|
175
|
+
if (canonicalField) {
|
|
176
|
+
if (definition.belongsTo) {
|
|
177
|
+
const normalized = typeof canonicalField === 'string'
|
|
178
|
+
? { idSlot: canonicalField }
|
|
179
|
+
: canonicalField
|
|
180
|
+
if (!normalized || !normalized.idSlot) {
|
|
181
|
+
throw new Error(`canonicalFieldsMap for belongsTo field '${fieldName}' requires idSlot`)
|
|
182
|
+
}
|
|
183
|
+
const parsed = parseBelongsToSlots(normalized)
|
|
184
|
+
if (usedSlots.has(parsed.idColumn) || usedSlots.has(parsed.typeColumn)) {
|
|
185
|
+
throw new Error(`Canonical slots '${parsed.idColumn}'/'${parsed.typeColumn}' already in use`)
|
|
186
|
+
}
|
|
187
|
+
usedSlots.add(parsed.idColumn)
|
|
188
|
+
usedSlots.add(parsed.typeColumn)
|
|
189
|
+
override = normalized
|
|
190
|
+
} else {
|
|
191
|
+
const slotColumn = typeof canonicalField === 'string'
|
|
192
|
+
? canonicalField
|
|
193
|
+
: canonicalField?.slot || canonicalField?.slotColumn
|
|
194
|
+
if (!slotColumn) {
|
|
195
|
+
throw new Error(`canonicalFieldsMap for field '${fieldName}' must provide a slot column`)
|
|
196
|
+
}
|
|
197
|
+
if (usedSlots.has(slotColumn)) {
|
|
198
|
+
throw new Error(`Canonical slot '${slotColumn}' already in use`)
|
|
199
|
+
}
|
|
200
|
+
usedSlots.add(slotColumn)
|
|
201
|
+
override = { slotColumn }
|
|
202
|
+
}
|
|
158
203
|
}
|
|
204
|
+
const fieldSlot = this.#assignFieldSlot({ ...definition, fieldName }, descriptor.slotState, override)
|
|
205
|
+
if (!fieldSlot) throw new Error(`Field '${resource}.${fieldName}' requires a supported type`)
|
|
206
|
+
const meta = definition.meta ? JSON.stringify(definition.meta) : null
|
|
159
207
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
208
|
+
await trx('any_field_configs').insert({
|
|
209
|
+
resource_config_id: resourceRow.id,
|
|
210
|
+
field_name: fieldName,
|
|
211
|
+
slot_type: fieldSlot.slotType,
|
|
212
|
+
slot_index: fieldSlot.slotIndex,
|
|
213
|
+
slot_column: fieldSlot.slotColumn,
|
|
214
|
+
nullable: definition.nullable === true,
|
|
215
|
+
required: definition.required === true,
|
|
216
|
+
target_resource: definition.belongsTo || null,
|
|
217
|
+
alias: definition.as || null,
|
|
218
|
+
meta_json: meta,
|
|
219
|
+
})
|
|
172
220
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
:
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
override = normalized
|
|
189
|
-
} else {
|
|
190
|
-
const slotColumn = typeof canonicalField === 'string'
|
|
191
|
-
? canonicalField
|
|
192
|
-
: canonicalField?.slot || canonicalField?.slotColumn
|
|
193
|
-
if (!slotColumn) {
|
|
194
|
-
throw new Error(`canonicalFieldsMap for field '${fieldName}' must provide a slot column`)
|
|
195
|
-
}
|
|
196
|
-
if (usedSlots.has(slotColumn)) {
|
|
197
|
-
throw new Error(`Canonical slot '${slotColumn}' already in use`)
|
|
221
|
+
if (fieldSlot.relationshipRow) {
|
|
222
|
+
const relationshipRow = {
|
|
223
|
+
resource_config_id: resourceRow.id,
|
|
224
|
+
relationship_name: fieldSlot.relationshipRow.name,
|
|
225
|
+
relationship_type: fieldSlot.relationshipRow.type,
|
|
226
|
+
target_resource: fieldSlot.relationshipRow.target,
|
|
227
|
+
slot_index: fieldSlot.relationshipRow.slotIndex,
|
|
228
|
+
id_column: fieldSlot.relationshipRow.idColumn,
|
|
229
|
+
type_column: fieldSlot.relationshipRow.typeColumn,
|
|
230
|
+
relationship_key: fieldSlot.relationshipRow.relationshipKey,
|
|
231
|
+
through: fieldSlot.relationshipRow.through,
|
|
232
|
+
foreign_key: fieldSlot.relationshipRow.foreignKey,
|
|
233
|
+
other_key: fieldSlot.relationshipRow.otherKey,
|
|
234
|
+
alias: fieldSlot.relationshipRow.alias,
|
|
235
|
+
meta_json: fieldSlot.relationshipRow.meta ? JSON.stringify(fieldSlot.relationshipRow.meta) : null,
|
|
198
236
|
}
|
|
199
|
-
|
|
200
|
-
override = { slotColumn }
|
|
237
|
+
await trx('any_relationship_configs').insert(relationshipRow)
|
|
201
238
|
}
|
|
202
239
|
}
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
const meta = definition.meta ? JSON.stringify(definition.meta) : null
|
|
206
|
-
|
|
207
|
-
await trx('any_field_configs').insert({
|
|
208
|
-
resource_config_id: resourceRow.id,
|
|
209
|
-
field_name: fieldName,
|
|
210
|
-
slot_type: fieldSlot.slotType,
|
|
211
|
-
slot_index: fieldSlot.slotIndex,
|
|
212
|
-
slot_column: fieldSlot.slotColumn,
|
|
213
|
-
nullable: definition.nullable === true,
|
|
214
|
-
required: definition.required === true,
|
|
215
|
-
target_resource: definition.belongsTo || null,
|
|
216
|
-
alias: definition.as || null,
|
|
217
|
-
meta_json: meta,
|
|
240
|
+
await trx('any_resource_configs').where({ id: resourceRow.id }).update({
|
|
241
|
+
schema_json: JSON.stringify({ ...descriptor.schema, [fieldName]: definition })
|
|
218
242
|
})
|
|
219
|
-
|
|
220
|
-
if (fieldSlot.relationshipRow) {
|
|
221
|
-
const relationshipRow = {
|
|
222
|
-
resource_config_id: resourceRow.id,
|
|
223
|
-
relationship_name: fieldSlot.relationshipRow.name,
|
|
224
|
-
relationship_type: fieldSlot.relationshipRow.type,
|
|
225
|
-
target_resource: fieldSlot.relationshipRow.target,
|
|
226
|
-
slot_index: fieldSlot.relationshipRow.slotIndex,
|
|
227
|
-
id_column: fieldSlot.relationshipRow.idColumn,
|
|
228
|
-
type_column: fieldSlot.relationshipRow.typeColumn,
|
|
229
|
-
relationship_key: fieldSlot.relationshipRow.relationshipKey,
|
|
230
|
-
through: fieldSlot.relationshipRow.through,
|
|
231
|
-
foreign_key: fieldSlot.relationshipRow.foreignKey,
|
|
232
|
-
other_key: fieldSlot.relationshipRow.otherKey,
|
|
233
|
-
alias: fieldSlot.relationshipRow.alias,
|
|
234
|
-
meta_json: fieldSlot.relationshipRow.meta ? JSON.stringify(fieldSlot.relationshipRow.meta) : null,
|
|
235
|
-
}
|
|
236
|
-
await trx('any_relationship_configs').insert(relationshipRow)
|
|
237
|
-
}
|
|
238
|
-
|
|
243
|
+
const updated = await this.#loadDescriptor(tenant, resource, trx)
|
|
239
244
|
if (managed) {
|
|
240
|
-
await trx
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
descriptor.fields[fieldName] = {
|
|
244
|
-
slot: fieldSlot.slotColumn,
|
|
245
|
-
slotType: fieldSlot.slotType,
|
|
246
|
-
slotIndex: fieldSlot.slotIndex,
|
|
247
|
-
nullable: definition.nullable === true,
|
|
248
|
-
required: definition.required === true,
|
|
249
|
-
target: definition.belongsTo || null,
|
|
250
|
-
alias: definition.as || null,
|
|
245
|
+
await commitTransaction(trx, context)
|
|
246
|
+
this.#rememberDescriptor(updated)
|
|
251
247
|
}
|
|
252
|
-
|
|
253
|
-
if (fieldSlot.belongsToInfo) {
|
|
254
|
-
descriptor.belongsTo[fieldSlot.belongsToInfo.alias] = fieldSlot.belongsToInfo
|
|
255
|
-
descriptor.canonicalFieldMap = descriptor.canonicalFieldMap || {}
|
|
256
|
-
descriptor.canonicalFieldMap[fieldName] = {
|
|
257
|
-
idSlot: fieldSlot.slotColumn,
|
|
258
|
-
typeSlot: fieldSlot.belongsToInfo.typeColumn,
|
|
259
|
-
}
|
|
260
|
-
} else {
|
|
261
|
-
descriptor.canonicalFieldMap = descriptor.canonicalFieldMap || {}
|
|
262
|
-
descriptor.canonicalFieldMap[fieldName] = fieldSlot.slotColumn
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
usedSlots.add(fieldSlot.slotColumn)
|
|
266
|
-
if (fieldSlot.relationshipRow?.typeColumn) {
|
|
267
|
-
usedSlots.add(fieldSlot.relationshipRow.typeColumn)
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
descriptor.slotState = fieldSlot.updatedState
|
|
271
|
-
this.cache.set(this.#key(tenant, resource), descriptor)
|
|
272
|
-
return clone(descriptor)
|
|
248
|
+
return clone(updated)
|
|
273
249
|
} catch (error) {
|
|
274
250
|
if (managed) {
|
|
275
|
-
await trx.
|
|
251
|
+
await this.#handleWriteFailure(error, trx || context.transaction, context)
|
|
276
252
|
}
|
|
277
|
-
throw error
|
|
253
|
+
throw wrapWriteError(error, context, context.transaction || options.transaction)
|
|
254
|
+
} finally {
|
|
255
|
+
endWriteOperation(context)
|
|
278
256
|
}
|
|
279
257
|
}
|
|
280
258
|
|
|
281
259
|
async getDescriptor (tenant, resource, options = {}) {
|
|
282
260
|
if (!tenant || !resource) return null
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
261
|
+
try {
|
|
262
|
+
const key = this.#key(tenant, resource)
|
|
263
|
+
if (!options.transaction && !options.bypassCache && this.cache.has(key)) {
|
|
264
|
+
const descriptor = this.cache.get(key)
|
|
265
|
+
this.#rememberDescriptor(descriptor)
|
|
266
|
+
return clone(descriptor)
|
|
267
|
+
}
|
|
287
268
|
|
|
288
|
-
|
|
289
|
-
|
|
269
|
+
const descriptor = await this.#loadDescriptor(tenant, resource, options.transaction)
|
|
270
|
+
if (!descriptor) {
|
|
271
|
+
if (!options.transaction) this.cache.delete(key)
|
|
272
|
+
return null
|
|
273
|
+
}
|
|
290
274
|
|
|
291
|
-
|
|
292
|
-
|
|
275
|
+
if (!options.transaction) this.#rememberDescriptor(descriptor)
|
|
276
|
+
return clone(descriptor)
|
|
277
|
+
} catch (error) {
|
|
278
|
+
throw wrapUnexpectedError(error, {
|
|
279
|
+
message: `Failed to load descriptor for resource '${resource}'`,
|
|
280
|
+
context: { tenant, scopeName: resource, phase: 'descriptor' }
|
|
281
|
+
})
|
|
282
|
+
}
|
|
293
283
|
}
|
|
294
284
|
|
|
295
285
|
async listResources (tenant) {
|
|
@@ -305,6 +295,28 @@ export class AnyapiRegistry {
|
|
|
305
295
|
this.cache.delete(this.#key(tenant, resource))
|
|
306
296
|
}
|
|
307
297
|
|
|
298
|
+
async #handleWriteFailure (error, transaction, context) {
|
|
299
|
+
// A rejected commit can still have changed persisted metadata.
|
|
300
|
+
this.invalidateDescriptor(context.tenant, context.resource)
|
|
301
|
+
await rollbackAfterError(error, context, transaction)
|
|
302
|
+
if (context.cleanupErrors.length) {
|
|
303
|
+
try {
|
|
304
|
+
await createEnhancedLogger(this.log).error('AnyAPI registry rollback failed', {
|
|
305
|
+
...getOperationDiagnosticContext(context, {
|
|
306
|
+
phase: 'registryRollback',
|
|
307
|
+
scopeName: context.resource,
|
|
308
|
+
backend: this.knex.client.config.client
|
|
309
|
+
}),
|
|
310
|
+
tenant: context.tenant,
|
|
311
|
+
error: context.error,
|
|
312
|
+
cleanupErrors: context.cleanupErrors
|
|
313
|
+
})
|
|
314
|
+
} catch (error) {
|
|
315
|
+
context.cleanupErrors.push({ phase: 'logging', error })
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
308
320
|
#validateDefinition (definition) {
|
|
309
321
|
const { tenant, resource, schema } = definition || {}
|
|
310
322
|
if (!tenant || typeof tenant !== 'string') {
|
|
@@ -316,15 +328,19 @@ export class AnyapiRegistry {
|
|
|
316
328
|
if (!schema || typeof schema !== 'object') {
|
|
317
329
|
throw new Error('AnyapiRegistry.registerResource requires a schema object')
|
|
318
330
|
}
|
|
331
|
+
validateSchemaFieldNames(schema, resource)
|
|
332
|
+
validateRelationshipFieldNames(definition.relationships, resource)
|
|
333
|
+
assertFieldNameMap(definition.canonicalFieldMap, `canonical fields in '${resource}'`)
|
|
319
334
|
}
|
|
320
335
|
|
|
321
|
-
async #register (definition, externalTrx) {
|
|
336
|
+
async #register (definition, externalTrx, context) {
|
|
322
337
|
const { tenant, resource, schema, relationships = {}, canonicalFieldMap = null } = definition
|
|
323
338
|
const idProperty = resolveIdProperty(schema, definition.idProperty)
|
|
324
|
-
|
|
339
|
+
let trx
|
|
325
340
|
const managed = !externalTrx
|
|
326
341
|
|
|
327
342
|
try {
|
|
343
|
+
trx = await beginWriteTransaction(context, externalTrx, ownerContext => createKnexTransaction(this.knex, ownerContext))
|
|
328
344
|
const now = this.knex.fn.now()
|
|
329
345
|
let resourceRow = await trx('any_resource_configs')
|
|
330
346
|
.where({ tenant_id: tenant, resource })
|
|
@@ -332,8 +348,11 @@ export class AnyapiRegistry {
|
|
|
332
348
|
|
|
333
349
|
const schemaJson = JSON.stringify(schema)
|
|
334
350
|
const relationshipsJson = JSON.stringify(relationships || {})
|
|
351
|
+
let previousDescriptor
|
|
335
352
|
|
|
336
353
|
if (resourceRow) {
|
|
354
|
+
// Validate persisted mappings before registration can replace migration evidence.
|
|
355
|
+
previousDescriptor = await this.#loadDescriptor(tenant, resource, trx, { idProperty })
|
|
337
356
|
await trx('any_resource_configs')
|
|
338
357
|
.where({ id: resourceRow.id })
|
|
339
358
|
.update({
|
|
@@ -342,17 +361,17 @@ export class AnyapiRegistry {
|
|
|
342
361
|
updated_at: now,
|
|
343
362
|
})
|
|
344
363
|
} else {
|
|
345
|
-
const [
|
|
364
|
+
const [inserted] = await applyInsertReturning(trx('any_resource_configs').insert({
|
|
346
365
|
tenant_id: tenant,
|
|
347
366
|
resource,
|
|
348
367
|
schema_json: schemaJson,
|
|
349
368
|
relationships_json: relationshipsJson,
|
|
350
369
|
created_at: now,
|
|
351
370
|
updated_at: now,
|
|
352
|
-
})
|
|
371
|
+
}), 'id')
|
|
353
372
|
|
|
354
373
|
resourceRow = {
|
|
355
|
-
id:
|
|
374
|
+
id: inserted && typeof inserted === 'object' ? inserted.id : inserted,
|
|
356
375
|
tenant_id: tenant,
|
|
357
376
|
resource,
|
|
358
377
|
schema_json: schemaJson,
|
|
@@ -372,8 +391,12 @@ export class AnyapiRegistry {
|
|
|
372
391
|
const fieldInserts = []
|
|
373
392
|
const relationshipInserts = []
|
|
374
393
|
|
|
375
|
-
|
|
376
|
-
|
|
394
|
+
// ID attributes previously had no slot; append them without moving existing slots.
|
|
395
|
+
const fields = Object.entries(schema).sort(([, left], [, right]) =>
|
|
396
|
+
Number(left?.type === 'id' && !left.belongsTo) - Number(right?.type === 'id' && !right.belongsTo)
|
|
397
|
+
)
|
|
398
|
+
for (const [fieldName, fieldDef] of fields) {
|
|
399
|
+
if (fieldName === 'id' || (fieldName === idProperty && fieldDef?.type === 'id')) {
|
|
377
400
|
if (canonicalOverrides) {
|
|
378
401
|
canonicalOverrides.delete(fieldName)
|
|
379
402
|
}
|
|
@@ -537,15 +560,32 @@ export class AnyapiRegistry {
|
|
|
537
560
|
}
|
|
538
561
|
|
|
539
562
|
const descriptor = await this.#loadDescriptor(tenant, resource, trx, { idProperty })
|
|
563
|
+
const changedField = Object.keys(previousDescriptor?.fields || {}).find(fieldName => {
|
|
564
|
+
const previous = previousDescriptor.fields[fieldName]
|
|
565
|
+
const current = descriptor.fields[fieldName]
|
|
566
|
+
if (!current || previous.slot !== current.slot || previous.slotType !== current.slotType) return true
|
|
567
|
+
return previous.slotType === 'belongsTo' &&
|
|
568
|
+
previousDescriptor.belongsTo[previous.alias]?.typeColumn !== descriptor.belongsTo[current.alias]?.typeColumn
|
|
569
|
+
})
|
|
570
|
+
if (changedField) {
|
|
571
|
+
const { canonical } = previousDescriptor
|
|
572
|
+
const storedRecord = await trx(canonical.tableName)
|
|
573
|
+
.where(canonical.tenantColumn, tenant)
|
|
574
|
+
.where(canonical.resourceColumn, resource)
|
|
575
|
+
.first()
|
|
576
|
+
if (storedRecord) {
|
|
577
|
+
throw new Error(`Canonical storage migration required for '${resource}.${changedField}': registration cannot remove or remap stored fields while records exist. Preserve canonicalFieldsMap or migrate the stored data before changing the schema.`)
|
|
578
|
+
}
|
|
579
|
+
}
|
|
540
580
|
|
|
541
581
|
if (managed) {
|
|
542
|
-
await trx
|
|
582
|
+
await commitTransaction(trx, context)
|
|
543
583
|
}
|
|
544
584
|
|
|
545
585
|
return descriptor
|
|
546
586
|
} catch (error) {
|
|
547
587
|
if (managed) {
|
|
548
|
-
await trx.
|
|
588
|
+
await this.#handleWriteFailure(error, trx || context.transaction, context)
|
|
549
589
|
}
|
|
550
590
|
throw error
|
|
551
591
|
}
|
|
@@ -607,6 +647,8 @@ export class AnyapiRegistry {
|
|
|
607
647
|
}
|
|
608
648
|
|
|
609
649
|
#buildDescriptor ({ tenant, resource, schema, relationships, slotState, fieldRows, relationshipRows, idProperty }) {
|
|
650
|
+
validateSchemaFieldNames(schema, resource)
|
|
651
|
+
validateRelationshipFieldNames(relationships, resource)
|
|
610
652
|
const fields = {}
|
|
611
653
|
const reverseAttributes = {}
|
|
612
654
|
const belongsTo = {}
|
|
@@ -614,6 +656,11 @@ export class AnyapiRegistry {
|
|
|
614
656
|
const polymorphicBelongsTo = {}
|
|
615
657
|
|
|
616
658
|
for (const row of fieldRows) {
|
|
659
|
+
assertFieldName(row.field_name, `stored field in '${resource}'`)
|
|
660
|
+
assertFieldName(row.alias, `stored relationship alias in '${resource}'`)
|
|
661
|
+
if (['date', 'time'].includes(schema?.[row.field_name]?.type) && row.slot_type !== 'string') {
|
|
662
|
+
throw new Error(`AnyAPI temporal storage migration required for '${resource}.${row.field_name}': calendar dates and times must use string slots. See 33-migrating-to-v2.md.`)
|
|
663
|
+
}
|
|
617
664
|
fields[row.field_name] = {
|
|
618
665
|
slot: row.slot_column,
|
|
619
666
|
slotType: row.slot_type,
|
|
@@ -629,6 +676,8 @@ export class AnyapiRegistry {
|
|
|
629
676
|
}
|
|
630
677
|
|
|
631
678
|
for (const row of relationshipRows) {
|
|
679
|
+
assertFieldName(row.relationship_name, `stored relationship in '${resource}'`)
|
|
680
|
+
assertFieldName(row.alias, `stored relationship alias in '${resource}'`)
|
|
632
681
|
if (row.relationship_type === 'belongsTo') {
|
|
633
682
|
const alias = row.alias || row.relationship_name
|
|
634
683
|
belongsTo[alias] = {
|
|
@@ -718,14 +767,14 @@ export class AnyapiRegistry {
|
|
|
718
767
|
}
|
|
719
768
|
|
|
720
769
|
const { type } = fieldDef
|
|
721
|
-
if (!type
|
|
770
|
+
if (!type) {
|
|
722
771
|
return null
|
|
723
772
|
}
|
|
724
773
|
if (!SUPPORTED_TYPES.has(type)) {
|
|
725
774
|
throw new Error(`Unsupported field type '${type}'`)
|
|
726
775
|
}
|
|
727
776
|
|
|
728
|
-
const pool = TYPE_TO_POOL.get(type)
|
|
777
|
+
const pool = type === 'id' ? 'string' : TYPE_TO_POOL.get(type)
|
|
729
778
|
if (!pool) {
|
|
730
779
|
throw new Error(`No slot pool defined for type '${type}'`)
|
|
731
780
|
}
|