json-rest-api 1.0.14 → 1.0.16
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/connectors_ideas/fastify.js +66 -68
- package/connectors_ideas/koa-plugin.js +50 -51
- package/eslint.config.js +11 -2
- package/examples/url-override-example.js +63 -63
- package/index.js +13 -13
- package/lib/enhanced-logger.js +51 -51
- package/lib/error-formatter.js +61 -61
- package/lib/rest-api-errors.js +28 -28
- package/package.json +1 -1
- package/plugins/core/bulk-operations-plugin.js +142 -145
- package/plugins/core/connectors/express-plugin.js +210 -217
- package/plugins/core/connectors/lib/busboy-detector.js +84 -84
- package/plugins/core/connectors/lib/formidable-detector.js +50 -50
- package/plugins/core/connectors/lib/request-helpers.js +74 -74
- package/plugins/core/file-handling-plugin.js +125 -125
- package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
- package/plugins/core/lib/anyapi/schema-utils.js +101 -101
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
- package/plugins/core/lib/dbTablesOperations.js +134 -135
- package/plugins/core/lib/fractional-positioning.js +74 -76
- package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
- package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
- package/plugins/core/lib/querying/knex-process-includes.js +50 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
- package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
- package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
- package/plugins/core/lib/querying/knex-window-queries.js +89 -89
- package/plugins/core/lib/querying/url-helpers.js +35 -35
- package/plugins/core/lib/querying-writing/compile-schemas.js +99 -99
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
- package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
- package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
- package/plugins/core/lib/querying-writing/field-utils.js +44 -42
- package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
- package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
- package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
- package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
- package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
- package/plugins/core/lib/storage/storage-adapter.js +68 -68
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
- package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
- package/plugins/core/lib/writing/relationship-processor.js +58 -58
- package/plugins/core/rest-api-access.js +11 -15
- package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
- package/plugins/core/rest-api-cors-plugin.js +170 -171
- package/plugins/core/rest-api-knex-plugin.js +482 -493
- package/plugins/core/rest-api-multihome-plugin.js +247 -251
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
- package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
- package/plugins/core/rest-api-plugin-methods/common.js +295 -297
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
- package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
- package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
- package/plugins/core/rest-api-plugin-methods/get.js +86 -86
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
- package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
- package/plugins/core/rest-api-plugin-methods/post.js +61 -62
- package/plugins/core/rest-api-plugin-methods/put.js +86 -87
- package/plugins/core/rest-api-plugin-methods/query.js +88 -88
- package/plugins/core/rest-api-plugin-methods/release.js +5 -6
- package/plugins/core/rest-api-plugin.js +76 -78
- package/plugins/core/rest-api-positioning-plugin.js +251 -252
- package/plugins/core/socketio-plugin.js +240 -236
- package/plugins/storage/local-storage.js +103 -103
- package/plugins/storage/s3-storage.js +41 -42
- package/quickTest.js +37 -37
- package/scripts/debug-anyapi-review-filter.mjs +18 -18
- package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
- package/scripts/inspect-anyapi-descriptor.mjs +10 -10
- package/scripts/run-docs.js +112 -113
- package/test-debug.js +28 -28
- package/tests/TEST_TEMPLATE.test.js +51 -51
- package/tests/anyapi-basic.test.js +42 -42
- package/tests/anyapi-cursor-pagination.test.js +48 -48
- package/tests/bulk-operations.test.js +208 -208
- package/tests/computed-fields.test.js +204 -204
- package/tests/cors.test.js +412 -413
- package/tests/curl.test.js +275 -276
- package/tests/custom-idproperty-relationships.test.js +266 -266
- package/tests/field-getters.test.js +132 -132
- package/tests/field-setters.test.js +110 -110
- package/tests/fixtures/api-configs.js +699 -699
- package/tests/helpers/storage-mode.js +32 -32
- package/tests/helpers/test-utils.js +76 -76
- package/tests/include-depth-validation.test.js +76 -76
- package/tests/includes.test.js +145 -145
- package/tests/multihome.test.js +175 -175
- package/tests/nested-includes.test.js +255 -255
- package/tests/pagination-cursor-multifield.test.js +164 -164
- package/tests/pagination-enhanced.test.js +225 -225
- package/tests/polymorphic-relationships.test.js +96 -96
- package/tests/polymorphic-simple.test.js +44 -44
- package/tests/positioning-advanced.test.js +226 -226
- package/tests/positioning-extended.test.js +399 -399
- package/tests/positioning-multifilter.test.js +204 -204
- package/tests/positioning.test.js +266 -266
- package/tests/queries.test.js +184 -184
- package/tests/query-limits.test.js +154 -155
- package/tests/relationship-endpoints.test.js +127 -127
- package/tests/relationships.test.js +178 -179
- package/tests/rest-api-access.test.js +86 -86
- package/tests/rest-api.test.js +103 -103
- package/tests/searchschema-merge.test.js +93 -93
- package/tests/socketio.test.js +326 -326
- package/tests/virtual-fields.test.js +164 -164
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { DEFAULT_CANONICAL_CONFIG, SLOT_LIMITS, SLOT_POOLS, TYPE_TO_POOL } from './schema-utils.js'
|
|
1
|
+
import { DEFAULT_CANONICAL_CONFIG, SLOT_LIMITS, SLOT_POOLS, TYPE_TO_POOL } from './schema-utils.js'
|
|
2
2
|
|
|
3
3
|
const clone = (value) => {
|
|
4
4
|
if (typeof structuredClone === 'function') {
|
|
5
|
-
return structuredClone(value)
|
|
5
|
+
return structuredClone(value)
|
|
6
6
|
}
|
|
7
|
-
return JSON.parse(JSON.stringify(value))
|
|
8
|
-
}
|
|
7
|
+
return JSON.parse(JSON.stringify(value))
|
|
8
|
+
}
|
|
9
9
|
|
|
10
|
-
const SUPPORTED_TYPES = new Set([...TYPE_TO_POOL.keys(), 'id'])
|
|
10
|
+
const SUPPORTED_TYPES = new Set([...TYPE_TO_POOL.keys(), 'id'])
|
|
11
11
|
|
|
12
12
|
const BELONGS_TO_ALIAS = (fieldName, fieldDef) => {
|
|
13
|
-
if (fieldDef.as) return fieldDef.as
|
|
13
|
+
if (fieldDef.as) return fieldDef.as
|
|
14
14
|
if (fieldName.endsWith('_id')) {
|
|
15
|
-
return fieldName.slice(0, -3)
|
|
15
|
+
return fieldName.slice(0, -3)
|
|
16
16
|
}
|
|
17
|
-
return fieldName
|
|
18
|
-
}
|
|
17
|
+
return fieldName
|
|
18
|
+
}
|
|
19
19
|
|
|
20
20
|
const SLOT_PATTERNS = {
|
|
21
21
|
string: /^string_(\d+)$/,
|
|
@@ -23,104 +23,104 @@ const SLOT_PATTERNS = {
|
|
|
23
23
|
boolean: /^boolean_(\d+)$/,
|
|
24
24
|
date: /^date_(\d+)$/,
|
|
25
25
|
json: /^json_(\d+)$/,
|
|
26
|
-
}
|
|
26
|
+
}
|
|
27
27
|
|
|
28
|
-
const BELONGS_TO_ID_PATTERN = /^rel_(\d+)_id
|
|
29
|
-
const BELONGS_TO_TYPE_PATTERN = /^rel_(\d+)_type
|
|
28
|
+
const BELONGS_TO_ID_PATTERN = /^rel_(\d+)_id$/
|
|
29
|
+
const BELONGS_TO_TYPE_PATTERN = /^rel_(\d+)_type$/
|
|
30
30
|
|
|
31
31
|
const parseSimpleSlotColumn = (column) => {
|
|
32
32
|
for (const [type, pattern] of Object.entries(SLOT_PATTERNS)) {
|
|
33
|
-
const match = pattern.exec(column)
|
|
33
|
+
const match = pattern.exec(column)
|
|
34
34
|
if (match) {
|
|
35
|
-
return { slotType: type, slotIndex: Number(match[1]) }
|
|
35
|
+
return { slotType: type, slotIndex: Number(match[1]) }
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
-
throw new Error(`Invalid canonical slot column '${column}'`)
|
|
39
|
-
}
|
|
38
|
+
throw new Error(`Invalid canonical slot column '${column}'`)
|
|
39
|
+
}
|
|
40
40
|
|
|
41
41
|
const parseBelongsToSlots = ({ idSlot, typeSlot }) => {
|
|
42
42
|
if (!idSlot || typeof idSlot !== 'string') {
|
|
43
|
-
throw new Error('BelongsTo canonical mapping requires idSlot')
|
|
43
|
+
throw new Error('BelongsTo canonical mapping requires idSlot')
|
|
44
44
|
}
|
|
45
|
-
const idMatch = BELONGS_TO_ID_PATTERN.exec(idSlot)
|
|
45
|
+
const idMatch = BELONGS_TO_ID_PATTERN.exec(idSlot)
|
|
46
46
|
if (!idMatch) {
|
|
47
|
-
throw new Error(`Invalid belongsTo id slot '${idSlot}'`)
|
|
47
|
+
throw new Error(`Invalid belongsTo id slot '${idSlot}'`)
|
|
48
48
|
}
|
|
49
|
-
const index = Number(idMatch[1])
|
|
50
|
-
const expectedTypeSlot = `rel_${index}_type
|
|
51
|
-
const actualTypeSlot = typeSlot || expectedTypeSlot
|
|
52
|
-
const typeMatch = BELONGS_TO_TYPE_PATTERN.exec(actualTypeSlot)
|
|
49
|
+
const index = Number(idMatch[1])
|
|
50
|
+
const expectedTypeSlot = `rel_${index}_type`
|
|
51
|
+
const actualTypeSlot = typeSlot || expectedTypeSlot
|
|
52
|
+
const typeMatch = BELONGS_TO_TYPE_PATTERN.exec(actualTypeSlot)
|
|
53
53
|
if (!typeMatch || Number(typeMatch[1]) !== index) {
|
|
54
|
-
throw new Error(`BelongsTo type slot '${actualTypeSlot}' must correspond to id slot '${idSlot}'`)
|
|
54
|
+
throw new Error(`BelongsTo type slot '${actualTypeSlot}' must correspond to id slot '${idSlot}'`)
|
|
55
55
|
}
|
|
56
|
-
return { slotType: 'belongsTo', slotIndex: index, idColumn: idSlot, typeColumn: actualTypeSlot }
|
|
57
|
-
}
|
|
56
|
+
return { slotType: 'belongsTo', slotIndex: index, idColumn: idSlot, typeColumn: actualTypeSlot }
|
|
57
|
+
}
|
|
58
58
|
|
|
59
59
|
export class AnyapiRegistry {
|
|
60
|
-
constructor({ knex, log } = {}) {
|
|
60
|
+
constructor ({ knex, log } = {}) {
|
|
61
61
|
if (!knex) {
|
|
62
|
-
throw new Error('AnyapiRegistry requires a knex instance')
|
|
62
|
+
throw new Error('AnyapiRegistry requires a knex instance')
|
|
63
63
|
}
|
|
64
|
-
this.knex = knex
|
|
65
|
-
this.log = log || console
|
|
66
|
-
this.cache = new Map()
|
|
64
|
+
this.knex = knex
|
|
65
|
+
this.log = log || console
|
|
66
|
+
this.cache = new Map()
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
#key(tenant, resource) {
|
|
70
|
-
return `${tenant}::${resource}
|
|
69
|
+
#key (tenant, resource) {
|
|
70
|
+
return `${tenant}::${resource}`
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
async registerResource(definition, options = {}) {
|
|
74
|
-
this.#validateDefinition(definition)
|
|
75
|
-
const descriptor = await this.#register(definition, options.transaction)
|
|
76
|
-
const key = this.#key(descriptor.tenant, descriptor.resource)
|
|
77
|
-
this.cache.set(key, descriptor)
|
|
78
|
-
return clone(descriptor)
|
|
73
|
+
async registerResource (definition, options = {}) {
|
|
74
|
+
this.#validateDefinition(definition)
|
|
75
|
+
const descriptor = await this.#register(definition, options.transaction)
|
|
76
|
+
const key = this.#key(descriptor.tenant, descriptor.resource)
|
|
77
|
+
this.cache.set(key, descriptor)
|
|
78
|
+
return clone(descriptor)
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
async allocateField({ tenant, resource, fieldName, definition, canonicalField }, options = {}) {
|
|
81
|
+
async allocateField ({ tenant, resource, fieldName, definition, canonicalField }, options = {}) {
|
|
82
82
|
if (!tenant || !resource || !fieldName || !definition) {
|
|
83
|
-
throw new Error('allocateField requires tenant, resource, fieldName, and definition')
|
|
83
|
+
throw new Error('allocateField requires tenant, resource, fieldName, and definition')
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
const descriptor = await this.getDescriptor(tenant, resource, { bypassCache: true })
|
|
86
|
+
const descriptor = await this.getDescriptor(tenant, resource, { bypassCache: true })
|
|
87
87
|
if (!descriptor) {
|
|
88
|
-
throw new Error(`Resource '${resource}' not registered for tenant '${tenant}'`)
|
|
88
|
+
throw new Error(`Resource '${resource}' not registered for tenant '${tenant}'`)
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
const trx = options.transaction || await this.knex.transaction()
|
|
92
|
-
const managed = !options.transaction
|
|
91
|
+
const trx = options.transaction || await this.knex.transaction()
|
|
92
|
+
const managed = !options.transaction
|
|
93
93
|
|
|
94
|
-
descriptor.belongsTo = descriptor.belongsTo || {}
|
|
95
|
-
descriptor.canonicalFieldMap = descriptor.canonicalFieldMap || {}
|
|
94
|
+
descriptor.belongsTo = descriptor.belongsTo || {}
|
|
95
|
+
descriptor.canonicalFieldMap = descriptor.canonicalFieldMap || {}
|
|
96
96
|
|
|
97
|
-
const usedSlots = new Set()
|
|
97
|
+
const usedSlots = new Set()
|
|
98
98
|
Object.values(descriptor.fields || {}).forEach((info) => {
|
|
99
99
|
if (info?.slot) {
|
|
100
|
-
usedSlots.add(info.slot)
|
|
100
|
+
usedSlots.add(info.slot)
|
|
101
101
|
}
|
|
102
|
-
})
|
|
102
|
+
})
|
|
103
103
|
Object.values(descriptor.belongsTo || {}).forEach((info) => {
|
|
104
104
|
if (info?.typeColumn) {
|
|
105
|
-
usedSlots.add(info.typeColumn)
|
|
105
|
+
usedSlots.add(info.typeColumn)
|
|
106
106
|
}
|
|
107
107
|
if (info?.idColumn) {
|
|
108
|
-
usedSlots.add(info.idColumn)
|
|
108
|
+
usedSlots.add(info.idColumn)
|
|
109
109
|
}
|
|
110
|
-
})
|
|
110
|
+
})
|
|
111
111
|
|
|
112
112
|
try {
|
|
113
113
|
const resourceRow = await trx('any_resource_configs')
|
|
114
114
|
.where({ tenant_id: tenant, resource })
|
|
115
|
-
.first()
|
|
115
|
+
.first()
|
|
116
116
|
|
|
117
117
|
if (!resourceRow) {
|
|
118
|
-
throw new Error(`Resource '${resource}' not found in metadata`)
|
|
118
|
+
throw new Error(`Resource '${resource}' not found in metadata`)
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
if (definition?.belongsToPolymorphic) {
|
|
122
|
-
const alias = definition.as || fieldName
|
|
123
|
-
const { typeField, idField, types = [] } = definition.belongsToPolymorphic
|
|
122
|
+
const alias = definition.as || fieldName
|
|
123
|
+
const { typeField, idField, types = [] } = definition.belongsToPolymorphic
|
|
124
124
|
|
|
125
125
|
await trx('any_relationship_configs').insert({
|
|
126
126
|
resource_config_id: resourceRow.id,
|
|
@@ -136,13 +136,13 @@ export class AnyapiRegistry {
|
|
|
136
136
|
other_key: null,
|
|
137
137
|
alias,
|
|
138
138
|
meta_json: JSON.stringify({ types }),
|
|
139
|
-
})
|
|
139
|
+
})
|
|
140
140
|
|
|
141
141
|
if (managed) {
|
|
142
|
-
await trx.commit()
|
|
142
|
+
await trx.commit()
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
descriptor.polymorphicBelongsTo = descriptor.polymorphicBelongsTo || {}
|
|
145
|
+
descriptor.polymorphicBelongsTo = descriptor.polymorphicBelongsTo || {}
|
|
146
146
|
descriptor.polymorphicBelongsTo[alias] = {
|
|
147
147
|
alias,
|
|
148
148
|
typeField,
|
|
@@ -150,44 +150,44 @@ export class AnyapiRegistry {
|
|
|
150
150
|
types,
|
|
151
151
|
typeColumn: descriptor.fields?.[typeField]?.slot || null,
|
|
152
152
|
idColumn: descriptor.fields?.[idField]?.slot || null,
|
|
153
|
-
}
|
|
154
|
-
this.cache.set(this.#key(tenant, resource), descriptor)
|
|
155
|
-
return clone(descriptor)
|
|
153
|
+
}
|
|
154
|
+
this.cache.set(this.#key(tenant, resource), descriptor)
|
|
155
|
+
return clone(descriptor)
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
let override = null
|
|
158
|
+
let override = null
|
|
159
159
|
if (canonicalField) {
|
|
160
160
|
if (definition.belongsTo) {
|
|
161
161
|
const normalized = typeof canonicalField === 'string'
|
|
162
162
|
? { idSlot: canonicalField }
|
|
163
|
-
: canonicalField
|
|
163
|
+
: canonicalField
|
|
164
164
|
if (!normalized || !normalized.idSlot) {
|
|
165
|
-
throw new Error(`canonicalFieldsMap for belongsTo field '${fieldName}' requires idSlot`)
|
|
165
|
+
throw new Error(`canonicalFieldsMap for belongsTo field '${fieldName}' requires idSlot`)
|
|
166
166
|
}
|
|
167
|
-
const parsed = parseBelongsToSlots(normalized)
|
|
167
|
+
const parsed = parseBelongsToSlots(normalized)
|
|
168
168
|
if (usedSlots.has(parsed.idColumn) || usedSlots.has(parsed.typeColumn)) {
|
|
169
|
-
throw new Error(`Canonical slots '${parsed.idColumn}'/'${parsed.typeColumn}' already in use`)
|
|
169
|
+
throw new Error(`Canonical slots '${parsed.idColumn}'/'${parsed.typeColumn}' already in use`)
|
|
170
170
|
}
|
|
171
|
-
usedSlots.add(parsed.idColumn)
|
|
172
|
-
usedSlots.add(parsed.typeColumn)
|
|
173
|
-
override = normalized
|
|
171
|
+
usedSlots.add(parsed.idColumn)
|
|
172
|
+
usedSlots.add(parsed.typeColumn)
|
|
173
|
+
override = normalized
|
|
174
174
|
} else {
|
|
175
175
|
const slotColumn = typeof canonicalField === 'string'
|
|
176
176
|
? canonicalField
|
|
177
|
-
: canonicalField?.slot || canonicalField?.slotColumn
|
|
177
|
+
: canonicalField?.slot || canonicalField?.slotColumn
|
|
178
178
|
if (!slotColumn) {
|
|
179
|
-
throw new Error(`canonicalFieldsMap for field '${fieldName}' must provide a slot column`)
|
|
179
|
+
throw new Error(`canonicalFieldsMap for field '${fieldName}' must provide a slot column`)
|
|
180
180
|
}
|
|
181
181
|
if (usedSlots.has(slotColumn)) {
|
|
182
|
-
throw new Error(`Canonical slot '${slotColumn}' already in use`)
|
|
182
|
+
throw new Error(`Canonical slot '${slotColumn}' already in use`)
|
|
183
183
|
}
|
|
184
|
-
usedSlots.add(slotColumn)
|
|
185
|
-
override = { slotColumn }
|
|
184
|
+
usedSlots.add(slotColumn)
|
|
185
|
+
override = { slotColumn }
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
const fieldSlot = this.#assignFieldSlot({ ...definition, fieldName }, descriptor.slotState, override)
|
|
190
|
-
const meta = definition.meta ? JSON.stringify(definition.meta) : null
|
|
189
|
+
const fieldSlot = this.#assignFieldSlot({ ...definition, fieldName }, descriptor.slotState, override)
|
|
190
|
+
const meta = definition.meta ? JSON.stringify(definition.meta) : null
|
|
191
191
|
|
|
192
192
|
await trx('any_field_configs').insert({
|
|
193
193
|
resource_config_id: resourceRow.id,
|
|
@@ -200,7 +200,7 @@ export class AnyapiRegistry {
|
|
|
200
200
|
target_resource: definition.belongsTo || null,
|
|
201
201
|
alias: definition.as || null,
|
|
202
202
|
meta_json: meta,
|
|
203
|
-
})
|
|
203
|
+
})
|
|
204
204
|
|
|
205
205
|
if (fieldSlot.relationshipRow) {
|
|
206
206
|
const relationshipRow = {
|
|
@@ -217,12 +217,12 @@ export class AnyapiRegistry {
|
|
|
217
217
|
other_key: fieldSlot.relationshipRow.otherKey,
|
|
218
218
|
alias: fieldSlot.relationshipRow.alias,
|
|
219
219
|
meta_json: fieldSlot.relationshipRow.meta ? JSON.stringify(fieldSlot.relationshipRow.meta) : null,
|
|
220
|
-
}
|
|
221
|
-
await trx('any_relationship_configs').insert(relationshipRow)
|
|
220
|
+
}
|
|
221
|
+
await trx('any_relationship_configs').insert(relationshipRow)
|
|
222
222
|
}
|
|
223
223
|
|
|
224
224
|
if (managed) {
|
|
225
|
-
await trx.commit()
|
|
225
|
+
await trx.commit()
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
descriptor.fields[fieldName] = {
|
|
@@ -233,89 +233,89 @@ export class AnyapiRegistry {
|
|
|
233
233
|
required: definition.required === true,
|
|
234
234
|
target: definition.belongsTo || null,
|
|
235
235
|
alias: definition.as || null,
|
|
236
|
-
}
|
|
236
|
+
}
|
|
237
237
|
|
|
238
238
|
if (fieldSlot.belongsToInfo) {
|
|
239
|
-
descriptor.belongsTo[fieldSlot.belongsToInfo.alias] = fieldSlot.belongsToInfo
|
|
240
|
-
descriptor.canonicalFieldMap = descriptor.canonicalFieldMap || {}
|
|
239
|
+
descriptor.belongsTo[fieldSlot.belongsToInfo.alias] = fieldSlot.belongsToInfo
|
|
240
|
+
descriptor.canonicalFieldMap = descriptor.canonicalFieldMap || {}
|
|
241
241
|
descriptor.canonicalFieldMap[fieldName] = {
|
|
242
242
|
idSlot: fieldSlot.slotColumn,
|
|
243
243
|
typeSlot: fieldSlot.belongsToInfo.typeColumn,
|
|
244
|
-
}
|
|
244
|
+
}
|
|
245
245
|
} else {
|
|
246
|
-
descriptor.canonicalFieldMap = descriptor.canonicalFieldMap || {}
|
|
247
|
-
descriptor.canonicalFieldMap[fieldName] = fieldSlot.slotColumn
|
|
246
|
+
descriptor.canonicalFieldMap = descriptor.canonicalFieldMap || {}
|
|
247
|
+
descriptor.canonicalFieldMap[fieldName] = fieldSlot.slotColumn
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
usedSlots.add(fieldSlot.slotColumn)
|
|
250
|
+
usedSlots.add(fieldSlot.slotColumn)
|
|
251
251
|
if (fieldSlot.relationshipRow?.typeColumn) {
|
|
252
|
-
usedSlots.add(fieldSlot.relationshipRow.typeColumn)
|
|
252
|
+
usedSlots.add(fieldSlot.relationshipRow.typeColumn)
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
-
descriptor.slotState = fieldSlot.updatedState
|
|
256
|
-
this.cache.set(this.#key(tenant, resource), descriptor)
|
|
257
|
-
return clone(descriptor)
|
|
255
|
+
descriptor.slotState = fieldSlot.updatedState
|
|
256
|
+
this.cache.set(this.#key(tenant, resource), descriptor)
|
|
257
|
+
return clone(descriptor)
|
|
258
258
|
} catch (error) {
|
|
259
259
|
if (managed) {
|
|
260
|
-
await trx.rollback()
|
|
260
|
+
await trx.rollback()
|
|
261
261
|
}
|
|
262
|
-
throw error
|
|
262
|
+
throw error
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
async getDescriptor(tenant, resource, options = {}) {
|
|
267
|
-
if (!tenant || !resource) return null
|
|
268
|
-
const key = this.#key(tenant, resource)
|
|
266
|
+
async getDescriptor (tenant, resource, options = {}) {
|
|
267
|
+
if (!tenant || !resource) return null
|
|
268
|
+
const key = this.#key(tenant, resource)
|
|
269
269
|
if (!options.bypassCache && this.cache.has(key)) {
|
|
270
|
-
return clone(this.cache.get(key))
|
|
270
|
+
return clone(this.cache.get(key))
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
const descriptor = await this.#loadDescriptor(tenant, resource, options.transaction)
|
|
274
|
-
if (!descriptor) return null
|
|
273
|
+
const descriptor = await this.#loadDescriptor(tenant, resource, options.transaction)
|
|
274
|
+
if (!descriptor) return null
|
|
275
275
|
|
|
276
|
-
this.cache.set(key, descriptor)
|
|
277
|
-
return clone(descriptor)
|
|
276
|
+
this.cache.set(key, descriptor)
|
|
277
|
+
return clone(descriptor)
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
async listResources(tenant) {
|
|
281
|
-
const query = this.knex('any_resource_configs')
|
|
280
|
+
async listResources (tenant) {
|
|
281
|
+
const query = this.knex('any_resource_configs')
|
|
282
282
|
if (tenant) {
|
|
283
|
-
query.where({ tenant_id: tenant })
|
|
283
|
+
query.where({ tenant_id: tenant })
|
|
284
284
|
}
|
|
285
|
-
const rows = await query.select('tenant_id', 'resource')
|
|
286
|
-
return rows.map((row) => ({ tenant: row.tenant_id, resource: row.resource }))
|
|
285
|
+
const rows = await query.select('tenant_id', 'resource')
|
|
286
|
+
return rows.map((row) => ({ tenant: row.tenant_id, resource: row.resource }))
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
-
invalidateDescriptor(tenant, resource) {
|
|
290
|
-
this.cache.delete(this.#key(tenant, resource))
|
|
289
|
+
invalidateDescriptor (tenant, resource) {
|
|
290
|
+
this.cache.delete(this.#key(tenant, resource))
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
#validateDefinition(definition) {
|
|
294
|
-
const { tenant, resource, schema } = definition || {}
|
|
293
|
+
#validateDefinition (definition) {
|
|
294
|
+
const { tenant, resource, schema } = definition || {}
|
|
295
295
|
if (!tenant || typeof tenant !== 'string') {
|
|
296
|
-
throw new Error('AnyapiRegistry.registerResource requires a tenant string')
|
|
296
|
+
throw new Error('AnyapiRegistry.registerResource requires a tenant string')
|
|
297
297
|
}
|
|
298
298
|
if (!resource || typeof resource !== 'string') {
|
|
299
|
-
throw new Error('AnyapiRegistry.registerResource requires a resource string')
|
|
299
|
+
throw new Error('AnyapiRegistry.registerResource requires a resource string')
|
|
300
300
|
}
|
|
301
301
|
if (!schema || typeof schema !== 'object') {
|
|
302
|
-
throw new Error('AnyapiRegistry.registerResource requires a schema object')
|
|
302
|
+
throw new Error('AnyapiRegistry.registerResource requires a schema object')
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
-
async #register(definition, externalTrx) {
|
|
307
|
-
const { tenant, resource, schema, relationships = {}, canonicalFieldMap = null } = definition
|
|
308
|
-
const trx = externalTrx || await this.knex.transaction()
|
|
309
|
-
const managed = !externalTrx
|
|
306
|
+
async #register (definition, externalTrx) {
|
|
307
|
+
const { tenant, resource, schema, relationships = {}, canonicalFieldMap = null } = definition
|
|
308
|
+
const trx = externalTrx || await this.knex.transaction()
|
|
309
|
+
const managed = !externalTrx
|
|
310
310
|
|
|
311
311
|
try {
|
|
312
|
-
const now = this.knex.fn.now()
|
|
312
|
+
const now = this.knex.fn.now()
|
|
313
313
|
let resourceRow = await trx('any_resource_configs')
|
|
314
314
|
.where({ tenant_id: tenant, resource })
|
|
315
|
-
.first()
|
|
315
|
+
.first()
|
|
316
316
|
|
|
317
|
-
const schemaJson = JSON.stringify(schema)
|
|
318
|
-
const relationshipsJson = JSON.stringify(relationships || {})
|
|
317
|
+
const schemaJson = JSON.stringify(schema)
|
|
318
|
+
const relationshipsJson = JSON.stringify(relationships || {})
|
|
319
319
|
|
|
320
320
|
if (resourceRow) {
|
|
321
321
|
await trx('any_resource_configs')
|
|
@@ -324,7 +324,7 @@ export class AnyapiRegistry {
|
|
|
324
324
|
schema_json: schemaJson,
|
|
325
325
|
relationships_json: relationshipsJson,
|
|
326
326
|
updated_at: now,
|
|
327
|
-
})
|
|
327
|
+
})
|
|
328
328
|
} else {
|
|
329
329
|
const [insertedId] = await trx('any_resource_configs').insert({
|
|
330
330
|
tenant_id: tenant,
|
|
@@ -333,7 +333,7 @@ export class AnyapiRegistry {
|
|
|
333
333
|
relationships_json: relationshipsJson,
|
|
334
334
|
created_at: now,
|
|
335
335
|
updated_at: now,
|
|
336
|
-
})
|
|
336
|
+
})
|
|
337
337
|
|
|
338
338
|
resourceRow = {
|
|
339
339
|
id: insertedId,
|
|
@@ -341,35 +341,35 @@ export class AnyapiRegistry {
|
|
|
341
341
|
resource,
|
|
342
342
|
schema_json: schemaJson,
|
|
343
343
|
relationships_json: relationshipsJson,
|
|
344
|
-
}
|
|
344
|
+
}
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
-
await trx('any_field_configs').where({ resource_config_id: resourceRow.id }).delete()
|
|
348
|
-
await trx('any_relationship_configs').where({ resource_config_id: resourceRow.id }).delete()
|
|
347
|
+
await trx('any_field_configs').where({ resource_config_id: resourceRow.id }).delete()
|
|
348
|
+
await trx('any_relationship_configs').where({ resource_config_id: resourceRow.id }).delete()
|
|
349
349
|
|
|
350
|
-
const slotState = this.#initializeSlotState()
|
|
351
|
-
const usedSlotColumns = new Set()
|
|
352
|
-
let canonicalOverrides = null
|
|
350
|
+
const slotState = this.#initializeSlotState()
|
|
351
|
+
const usedSlotColumns = new Set()
|
|
352
|
+
let canonicalOverrides = null
|
|
353
353
|
if (canonicalFieldMap) {
|
|
354
|
-
canonicalOverrides = new Map(Object.entries(canonicalFieldMap))
|
|
354
|
+
canonicalOverrides = new Map(Object.entries(canonicalFieldMap))
|
|
355
355
|
}
|
|
356
|
-
const fieldInserts = []
|
|
357
|
-
const relationshipInserts = []
|
|
356
|
+
const fieldInserts = []
|
|
357
|
+
const relationshipInserts = []
|
|
358
358
|
|
|
359
359
|
for (const [fieldName, fieldDef] of Object.entries(schema)) {
|
|
360
360
|
if (fieldName === 'id') {
|
|
361
361
|
if (canonicalOverrides) {
|
|
362
|
-
canonicalOverrides.delete(fieldName)
|
|
362
|
+
canonicalOverrides.delete(fieldName)
|
|
363
363
|
}
|
|
364
|
-
continue
|
|
364
|
+
continue
|
|
365
365
|
}
|
|
366
366
|
|
|
367
367
|
if (fieldDef?.belongsToPolymorphic) {
|
|
368
368
|
if (canonicalOverrides?.has(fieldName)) {
|
|
369
|
-
throw new Error(`canonicalFieldsMap should not include polymorphic field '${fieldName}'`)
|
|
369
|
+
throw new Error(`canonicalFieldsMap should not include polymorphic field '${fieldName}'`)
|
|
370
370
|
}
|
|
371
|
-
const alias = fieldDef.as || fieldName
|
|
372
|
-
const { typeField, idField, types = [] } = fieldDef.belongsToPolymorphic
|
|
371
|
+
const alias = fieldDef.as || fieldName
|
|
372
|
+
const { typeField, idField, types = [] } = fieldDef.belongsToPolymorphic
|
|
373
373
|
|
|
374
374
|
relationshipInserts.push({
|
|
375
375
|
resource_config_id: resourceRow.id,
|
|
@@ -387,66 +387,66 @@ export class AnyapiRegistry {
|
|
|
387
387
|
meta_json: JSON.stringify({ types }),
|
|
388
388
|
created_at: this.knex.fn.now(),
|
|
389
389
|
updated_at: this.knex.fn.now(),
|
|
390
|
-
})
|
|
391
|
-
continue
|
|
390
|
+
})
|
|
391
|
+
continue
|
|
392
392
|
}
|
|
393
393
|
|
|
394
394
|
if (fieldDef?.computed) {
|
|
395
395
|
if (canonicalOverrides?.has(fieldName)) {
|
|
396
|
-
throw new Error(`canonicalFieldsMap should not include computed field '${fieldName}'`)
|
|
396
|
+
throw new Error(`canonicalFieldsMap should not include computed field '${fieldName}'`)
|
|
397
397
|
}
|
|
398
|
-
continue
|
|
398
|
+
continue
|
|
399
399
|
}
|
|
400
400
|
|
|
401
401
|
if (fieldDef?.virtual === true) {
|
|
402
402
|
if (canonicalOverrides?.has(fieldName)) {
|
|
403
|
-
throw new Error(`canonicalFieldsMap should not include virtual field '${fieldName}'`)
|
|
403
|
+
throw new Error(`canonicalFieldsMap should not include virtual field '${fieldName}'`)
|
|
404
404
|
}
|
|
405
|
-
continue
|
|
405
|
+
continue
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
-
let override = null
|
|
408
|
+
let override = null
|
|
409
409
|
if (canonicalOverrides) {
|
|
410
410
|
if (!canonicalOverrides.has(fieldName)) {
|
|
411
|
-
throw new Error(`canonicalFieldsMap missing entry for field '${fieldName}'`)
|
|
411
|
+
throw new Error(`canonicalFieldsMap missing entry for field '${fieldName}'`)
|
|
412
412
|
}
|
|
413
|
-
const rawOverride = canonicalOverrides.get(fieldName)
|
|
414
|
-
canonicalOverrides.delete(fieldName)
|
|
413
|
+
const rawOverride = canonicalOverrides.get(fieldName)
|
|
414
|
+
canonicalOverrides.delete(fieldName)
|
|
415
415
|
|
|
416
416
|
if (fieldDef.belongsTo) {
|
|
417
417
|
const normalized = typeof rawOverride === 'string'
|
|
418
418
|
? { idSlot: rawOverride }
|
|
419
|
-
: rawOverride
|
|
419
|
+
: rawOverride
|
|
420
420
|
if (!normalized || !normalized.idSlot) {
|
|
421
|
-
throw new Error(`canonicalFieldsMap for belongsTo field '${fieldName}' requires idSlot`)
|
|
421
|
+
throw new Error(`canonicalFieldsMap for belongsTo field '${fieldName}' requires idSlot`)
|
|
422
422
|
}
|
|
423
|
-
const parsed = parseBelongsToSlots(normalized)
|
|
423
|
+
const parsed = parseBelongsToSlots(normalized)
|
|
424
424
|
if (usedSlotColumns.has(parsed.idColumn) || usedSlotColumns.has(parsed.typeColumn)) {
|
|
425
|
-
throw new Error(`Canonical slots '${parsed.idColumn}'/'${parsed.typeColumn}' already in use`)
|
|
425
|
+
throw new Error(`Canonical slots '${parsed.idColumn}'/'${parsed.typeColumn}' already in use`)
|
|
426
426
|
}
|
|
427
|
-
usedSlotColumns.add(parsed.idColumn)
|
|
428
|
-
usedSlotColumns.add(parsed.typeColumn)
|
|
429
|
-
override = normalized
|
|
427
|
+
usedSlotColumns.add(parsed.idColumn)
|
|
428
|
+
usedSlotColumns.add(parsed.typeColumn)
|
|
429
|
+
override = normalized
|
|
430
430
|
} else {
|
|
431
431
|
const slotColumn = typeof rawOverride === 'string'
|
|
432
432
|
? rawOverride
|
|
433
|
-
: rawOverride?.slot || rawOverride?.slotColumn
|
|
433
|
+
: rawOverride?.slot || rawOverride?.slotColumn
|
|
434
434
|
if (!slotColumn) {
|
|
435
|
-
throw new Error(`canonicalFieldsMap for field '${fieldName}' must provide a slot column`)
|
|
435
|
+
throw new Error(`canonicalFieldsMap for field '${fieldName}' must provide a slot column`)
|
|
436
436
|
}
|
|
437
|
-
const parsed = parseSimpleSlotColumn(slotColumn)
|
|
437
|
+
const parsed = parseSimpleSlotColumn(slotColumn)
|
|
438
438
|
if (usedSlotColumns.has(slotColumn)) {
|
|
439
|
-
throw new Error(`Canonical slot '${slotColumn}' already in use`)
|
|
439
|
+
throw new Error(`Canonical slot '${slotColumn}' already in use`)
|
|
440
440
|
}
|
|
441
|
-
usedSlotColumns.add(slotColumn)
|
|
442
|
-
override = { slotColumn, slotType: parsed.slotType, slotIndex: parsed.slotIndex }
|
|
441
|
+
usedSlotColumns.add(slotColumn)
|
|
442
|
+
override = { slotColumn, slotType: parsed.slotType, slotIndex: parsed.slotIndex }
|
|
443
443
|
}
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
-
const allocation = this.#assignFieldSlot({ ...fieldDef, fieldName }, slotState, override)
|
|
447
|
-
if (!allocation) continue
|
|
446
|
+
const allocation = this.#assignFieldSlot({ ...fieldDef, fieldName }, slotState, override)
|
|
447
|
+
if (!allocation) continue
|
|
448
448
|
|
|
449
|
-
const meta = fieldDef.meta ? JSON.stringify(fieldDef.meta) : null
|
|
449
|
+
const meta = fieldDef.meta ? JSON.stringify(fieldDef.meta) : null
|
|
450
450
|
fieldInserts.push({
|
|
451
451
|
resource_config_id: resourceRow.id,
|
|
452
452
|
field_name: fieldName,
|
|
@@ -460,7 +460,7 @@ export class AnyapiRegistry {
|
|
|
460
460
|
meta_json: meta,
|
|
461
461
|
created_at: this.knex.fn.now(),
|
|
462
462
|
updated_at: this.knex.fn.now(),
|
|
463
|
-
})
|
|
463
|
+
})
|
|
464
464
|
|
|
465
465
|
if (allocation.relationshipRow) {
|
|
466
466
|
const relationshipRow = {
|
|
@@ -479,19 +479,19 @@ export class AnyapiRegistry {
|
|
|
479
479
|
meta_json: allocation.relationshipRow.meta ? JSON.stringify(allocation.relationshipRow.meta) : null,
|
|
480
480
|
created_at: this.knex.fn.now(),
|
|
481
481
|
updated_at: this.knex.fn.now(),
|
|
482
|
-
}
|
|
483
|
-
relationshipInserts.push(relationshipRow)
|
|
482
|
+
}
|
|
483
|
+
relationshipInserts.push(relationshipRow)
|
|
484
484
|
}
|
|
485
485
|
|
|
486
|
-
usedSlotColumns.add(allocation.slotColumn)
|
|
486
|
+
usedSlotColumns.add(allocation.slotColumn)
|
|
487
487
|
if (allocation.relationshipRow?.typeColumn) {
|
|
488
|
-
usedSlotColumns.add(allocation.relationshipRow.typeColumn)
|
|
488
|
+
usedSlotColumns.add(allocation.relationshipRow.typeColumn)
|
|
489
489
|
}
|
|
490
490
|
}
|
|
491
491
|
|
|
492
492
|
if (canonicalOverrides && canonicalOverrides.size > 0) {
|
|
493
|
-
const unknownFields = [...canonicalOverrides.keys()].join(', ')
|
|
494
|
-
throw new Error(`canonicalFieldsMap contains unknown fields: ${unknownFields}`)
|
|
493
|
+
const unknownFields = [...canonicalOverrides.keys()].join(', ')
|
|
494
|
+
throw new Error(`canonicalFieldsMap contains unknown fields: ${unknownFields}`)
|
|
495
495
|
}
|
|
496
496
|
|
|
497
497
|
for (const [relName, relDef] of Object.entries(relationships || {})) {
|
|
@@ -509,54 +509,54 @@ export class AnyapiRegistry {
|
|
|
509
509
|
meta_json: JSON.stringify(relDef.meta || {}),
|
|
510
510
|
created_at: this.knex.fn.now(),
|
|
511
511
|
updated_at: this.knex.fn.now(),
|
|
512
|
-
})
|
|
512
|
+
})
|
|
513
513
|
}
|
|
514
514
|
}
|
|
515
515
|
|
|
516
516
|
if (fieldInserts.length > 0) {
|
|
517
|
-
await trx('any_field_configs').insert(fieldInserts)
|
|
517
|
+
await trx('any_field_configs').insert(fieldInserts)
|
|
518
518
|
}
|
|
519
519
|
if (relationshipInserts.length > 0) {
|
|
520
|
-
await trx('any_relationship_configs').insert(relationshipInserts)
|
|
520
|
+
await trx('any_relationship_configs').insert(relationshipInserts)
|
|
521
521
|
}
|
|
522
522
|
|
|
523
|
-
const descriptor = await this.#loadDescriptor(tenant, resource, trx)
|
|
523
|
+
const descriptor = await this.#loadDescriptor(tenant, resource, trx)
|
|
524
524
|
|
|
525
525
|
if (managed) {
|
|
526
|
-
await trx.commit()
|
|
526
|
+
await trx.commit()
|
|
527
527
|
}
|
|
528
528
|
|
|
529
|
-
return descriptor
|
|
529
|
+
return descriptor
|
|
530
530
|
} catch (error) {
|
|
531
531
|
if (managed) {
|
|
532
|
-
await trx.rollback()
|
|
532
|
+
await trx.rollback()
|
|
533
533
|
}
|
|
534
|
-
throw error
|
|
534
|
+
throw error
|
|
535
535
|
}
|
|
536
536
|
}
|
|
537
537
|
|
|
538
|
-
async #loadDescriptor(tenant, resource, trx) {
|
|
538
|
+
async #loadDescriptor (tenant, resource, trx) {
|
|
539
539
|
const query = (trx || this.knex)('any_resource_configs')
|
|
540
540
|
.where({ tenant_id: tenant, resource })
|
|
541
|
-
.first()
|
|
541
|
+
.first()
|
|
542
542
|
|
|
543
|
-
const resourceRow = await query
|
|
544
|
-
if (!resourceRow) return null
|
|
543
|
+
const resourceRow = await query
|
|
544
|
+
if (!resourceRow) return null
|
|
545
545
|
|
|
546
546
|
const fieldRows = await (trx || this.knex)('any_field_configs')
|
|
547
547
|
.where({ resource_config_id: resourceRow.id })
|
|
548
|
-
.select()
|
|
548
|
+
.select()
|
|
549
549
|
|
|
550
550
|
const relationshipRows = await (trx || this.knex)('any_relationship_configs')
|
|
551
551
|
.where({ resource_config_id: resourceRow.id })
|
|
552
|
-
.select()
|
|
552
|
+
.select()
|
|
553
553
|
|
|
554
|
-
const slotState = this.#initializeSlotState()
|
|
554
|
+
const slotState = this.#initializeSlotState()
|
|
555
555
|
for (const row of fieldRows) {
|
|
556
556
|
if (row.slot_type === 'belongsTo') {
|
|
557
|
-
slotState.belongsTo = Math.max(slotState.belongsTo, row.slot_index)
|
|
557
|
+
slotState.belongsTo = Math.max(slotState.belongsTo, row.slot_index)
|
|
558
558
|
} else if (row.slot_type in slotState) {
|
|
559
|
-
slotState[row.slot_type] = Math.max(slotState[row.slot_type], row.slot_index)
|
|
559
|
+
slotState[row.slot_type] = Math.max(slotState[row.slot_type], row.slot_index)
|
|
560
560
|
}
|
|
561
561
|
}
|
|
562
562
|
|
|
@@ -568,33 +568,33 @@ export class AnyapiRegistry {
|
|
|
568
568
|
slotState,
|
|
569
569
|
fieldRows,
|
|
570
570
|
relationshipRows,
|
|
571
|
-
})
|
|
571
|
+
})
|
|
572
572
|
|
|
573
|
-
const canonicalFieldMap = {}
|
|
573
|
+
const canonicalFieldMap = {}
|
|
574
574
|
for (const [fieldName, fieldInfo] of Object.entries(descriptor.fields || {})) {
|
|
575
575
|
if (fieldInfo.slotType === 'belongsTo') {
|
|
576
|
-
const alias = fieldInfo.alias || BELONGS_TO_ALIAS(fieldName, descriptor.schema?.[fieldName] || {})
|
|
577
|
-
const belongsToInfo = descriptor.belongsTo?.[alias]
|
|
576
|
+
const alias = fieldInfo.alias || BELONGS_TO_ALIAS(fieldName, descriptor.schema?.[fieldName] || {})
|
|
577
|
+
const belongsToInfo = descriptor.belongsTo?.[alias]
|
|
578
578
|
canonicalFieldMap[fieldName] = {
|
|
579
579
|
idSlot: fieldInfo.slot,
|
|
580
580
|
typeSlot: belongsToInfo?.typeColumn || `rel_${fieldInfo.slotIndex}_type`,
|
|
581
|
-
}
|
|
581
|
+
}
|
|
582
582
|
} else {
|
|
583
|
-
canonicalFieldMap[fieldName] = fieldInfo.slot
|
|
583
|
+
canonicalFieldMap[fieldName] = fieldInfo.slot
|
|
584
584
|
}
|
|
585
585
|
}
|
|
586
586
|
|
|
587
|
-
descriptor.canonicalFieldMap = canonicalFieldMap
|
|
587
|
+
descriptor.canonicalFieldMap = canonicalFieldMap
|
|
588
588
|
|
|
589
|
-
return descriptor
|
|
589
|
+
return descriptor
|
|
590
590
|
}
|
|
591
591
|
|
|
592
|
-
#buildDescriptor({ tenant, resource, schema, relationships, slotState, fieldRows, relationshipRows }) {
|
|
593
|
-
const fields = {}
|
|
594
|
-
const reverseAttributes = {}
|
|
595
|
-
const belongsTo = {}
|
|
596
|
-
const manyToMany = {}
|
|
597
|
-
const polymorphicBelongsTo = {}
|
|
592
|
+
#buildDescriptor ({ tenant, resource, schema, relationships, slotState, fieldRows, relationshipRows }) {
|
|
593
|
+
const fields = {}
|
|
594
|
+
const reverseAttributes = {}
|
|
595
|
+
const belongsTo = {}
|
|
596
|
+
const manyToMany = {}
|
|
597
|
+
const polymorphicBelongsTo = {}
|
|
598
598
|
|
|
599
599
|
for (const row of fieldRows) {
|
|
600
600
|
fields[row.field_name] = {
|
|
@@ -606,14 +606,14 @@ export class AnyapiRegistry {
|
|
|
606
606
|
target: row.target_resource,
|
|
607
607
|
alias: row.alias,
|
|
608
608
|
meta: row.meta_json ? JSON.parse(row.meta_json) : undefined,
|
|
609
|
-
}
|
|
609
|
+
}
|
|
610
610
|
|
|
611
|
-
reverseAttributes[row.slot_column] = row.field_name
|
|
611
|
+
reverseAttributes[row.slot_column] = row.field_name
|
|
612
612
|
}
|
|
613
613
|
|
|
614
614
|
for (const row of relationshipRows) {
|
|
615
615
|
if (row.relationship_type === 'belongsTo') {
|
|
616
|
-
const alias = row.alias || row.relationship_name
|
|
616
|
+
const alias = row.alias || row.relationship_name
|
|
617
617
|
belongsTo[alias] = {
|
|
618
618
|
alias,
|
|
619
619
|
field: fields[row.relationship_name]?.field || row.relationship_name,
|
|
@@ -621,7 +621,7 @@ export class AnyapiRegistry {
|
|
|
621
621
|
slotIndex: row.slot_index,
|
|
622
622
|
idColumn: row.id_column,
|
|
623
623
|
typeColumn: row.type_column,
|
|
624
|
-
}
|
|
624
|
+
}
|
|
625
625
|
} else if (row.relationship_type === 'manyToMany') {
|
|
626
626
|
manyToMany[row.relationship_name] = {
|
|
627
627
|
alias: row.alias || row.relationship_name,
|
|
@@ -631,17 +631,17 @@ export class AnyapiRegistry {
|
|
|
631
631
|
foreignKey: row.foreign_key,
|
|
632
632
|
otherKey: row.other_key,
|
|
633
633
|
meta: row.meta_json ? JSON.parse(row.meta_json) : undefined,
|
|
634
|
-
}
|
|
634
|
+
}
|
|
635
635
|
}
|
|
636
636
|
}
|
|
637
637
|
|
|
638
|
-
const schemaObj = schema || {}
|
|
638
|
+
const schemaObj = schema || {}
|
|
639
639
|
for (const [fieldName, fieldDef] of Object.entries(schemaObj)) {
|
|
640
|
-
if (!fieldDef?.belongsToPolymorphic) continue
|
|
641
|
-
const alias = fieldDef.as || fieldName
|
|
642
|
-
const { typeField, idField, types = [] } = fieldDef.belongsToPolymorphic
|
|
643
|
-
const typeEntry = fields[typeField] || null
|
|
644
|
-
const idEntry = fields[idField] || null
|
|
640
|
+
if (!fieldDef?.belongsToPolymorphic) continue
|
|
641
|
+
const alias = fieldDef.as || fieldName
|
|
642
|
+
const { typeField, idField, types = [] } = fieldDef.belongsToPolymorphic
|
|
643
|
+
const typeEntry = fields[typeField] || null
|
|
644
|
+
const idEntry = fields[idField] || null
|
|
645
645
|
polymorphicBelongsTo[alias] = {
|
|
646
646
|
alias,
|
|
647
647
|
typeField,
|
|
@@ -649,15 +649,15 @@ export class AnyapiRegistry {
|
|
|
649
649
|
typeColumn: typeEntry?.slot || null,
|
|
650
650
|
idColumn: idEntry?.slot || null,
|
|
651
651
|
types,
|
|
652
|
-
}
|
|
652
|
+
}
|
|
653
653
|
}
|
|
654
654
|
|
|
655
655
|
for (const [relName, relDef] of Object.entries(relationships || {})) {
|
|
656
|
-
if (!relDef?.belongsToPolymorphic) continue
|
|
657
|
-
const alias = relDef.as || relName
|
|
658
|
-
const { typeField, idField, types = [] } = relDef.belongsToPolymorphic
|
|
659
|
-
const typeEntry = fields[typeField] || null
|
|
660
|
-
const idEntry = fields[idField] || null
|
|
656
|
+
if (!relDef?.belongsToPolymorphic) continue
|
|
657
|
+
const alias = relDef.as || relName
|
|
658
|
+
const { typeField, idField, types = [] } = relDef.belongsToPolymorphic
|
|
659
|
+
const typeEntry = fields[typeField] || null
|
|
660
|
+
const idEntry = fields[idField] || null
|
|
661
661
|
polymorphicBelongsTo[alias] = {
|
|
662
662
|
alias,
|
|
663
663
|
typeField,
|
|
@@ -665,7 +665,7 @@ export class AnyapiRegistry {
|
|
|
665
665
|
typeColumn: typeEntry?.slot || null,
|
|
666
666
|
idColumn: idEntry?.slot || null,
|
|
667
667
|
types,
|
|
668
|
-
}
|
|
668
|
+
}
|
|
669
669
|
}
|
|
670
670
|
|
|
671
671
|
return {
|
|
@@ -680,10 +680,10 @@ export class AnyapiRegistry {
|
|
|
680
680
|
polymorphicBelongsTo,
|
|
681
681
|
reverseAttributes,
|
|
682
682
|
slotState,
|
|
683
|
-
}
|
|
683
|
+
}
|
|
684
684
|
}
|
|
685
685
|
|
|
686
|
-
#initializeSlotState() {
|
|
686
|
+
#initializeSlotState () {
|
|
687
687
|
return {
|
|
688
688
|
string: 0,
|
|
689
689
|
number: 0,
|
|
@@ -691,75 +691,75 @@ export class AnyapiRegistry {
|
|
|
691
691
|
date: 0,
|
|
692
692
|
json: 0,
|
|
693
693
|
belongsTo: 0,
|
|
694
|
-
}
|
|
694
|
+
}
|
|
695
695
|
}
|
|
696
696
|
|
|
697
|
-
#assignFieldSlot(fieldDef, slotState, canonicalOverride = null) {
|
|
697
|
+
#assignFieldSlot (fieldDef, slotState, canonicalOverride = null) {
|
|
698
698
|
if (fieldDef.belongsTo) {
|
|
699
|
-
return this.#assignBelongsTo(fieldDef, slotState, canonicalOverride)
|
|
699
|
+
return this.#assignBelongsTo(fieldDef, slotState, canonicalOverride)
|
|
700
700
|
}
|
|
701
701
|
|
|
702
|
-
const { type } = fieldDef
|
|
702
|
+
const { type } = fieldDef
|
|
703
703
|
if (!type || type === 'id') {
|
|
704
|
-
return null
|
|
704
|
+
return null
|
|
705
705
|
}
|
|
706
706
|
if (!SUPPORTED_TYPES.has(type)) {
|
|
707
|
-
throw new Error(`Unsupported field type '${type}'`)
|
|
707
|
+
throw new Error(`Unsupported field type '${type}'`)
|
|
708
708
|
}
|
|
709
709
|
|
|
710
|
-
const pool = TYPE_TO_POOL.get(type)
|
|
710
|
+
const pool = TYPE_TO_POOL.get(type)
|
|
711
711
|
if (!pool) {
|
|
712
|
-
throw new Error(`No slot pool defined for type '${type}'`)
|
|
712
|
+
throw new Error(`No slot pool defined for type '${type}'`)
|
|
713
713
|
}
|
|
714
714
|
|
|
715
715
|
if (canonicalOverride) {
|
|
716
716
|
const slotColumn = typeof canonicalOverride === 'string'
|
|
717
717
|
? canonicalOverride
|
|
718
|
-
: canonicalOverride.slot || canonicalOverride.slotColumn
|
|
718
|
+
: canonicalOverride.slot || canonicalOverride.slotColumn
|
|
719
719
|
if (!slotColumn) {
|
|
720
|
-
throw new Error(`Canonical mapping for field '${fieldDef.fieldName}' must provide a slot column`)
|
|
720
|
+
throw new Error(`Canonical mapping for field '${fieldDef.fieldName}' must provide a slot column`)
|
|
721
721
|
}
|
|
722
|
-
const { slotType, slotIndex } = parseSimpleSlotColumn(slotColumn)
|
|
722
|
+
const { slotType, slotIndex } = parseSimpleSlotColumn(slotColumn)
|
|
723
723
|
if (slotType !== pool) {
|
|
724
724
|
throw new Error(
|
|
725
|
-
`Canonical slot '${slotColumn}' does not match expected type '${pool}' for field '${fieldDef.fieldName}'
|
|
726
|
-
)
|
|
725
|
+
`Canonical slot '${slotColumn}' does not match expected type '${pool}' for field '${fieldDef.fieldName}'`
|
|
726
|
+
)
|
|
727
727
|
}
|
|
728
|
-
slotState[pool] = Math.max(slotState[pool], slotIndex)
|
|
728
|
+
slotState[pool] = Math.max(slotState[pool], slotIndex)
|
|
729
729
|
return {
|
|
730
730
|
slotType: pool,
|
|
731
731
|
slotIndex,
|
|
732
732
|
slotColumn,
|
|
733
733
|
updatedState: slotState,
|
|
734
|
-
}
|
|
734
|
+
}
|
|
735
735
|
}
|
|
736
736
|
|
|
737
|
-
const nextIndex = slotState[pool] + 1
|
|
737
|
+
const nextIndex = slotState[pool] + 1
|
|
738
738
|
if (nextIndex > SLOT_LIMITS[pool]) {
|
|
739
|
-
throw new Error(`No available ${pool} slots remaining`)
|
|
739
|
+
throw new Error(`No available ${pool} slots remaining`)
|
|
740
740
|
}
|
|
741
741
|
|
|
742
|
-
slotState[pool] = nextIndex
|
|
743
|
-
const slotColumn = SLOT_POOLS[pool][nextIndex - 1]
|
|
742
|
+
slotState[pool] = nextIndex
|
|
743
|
+
const slotColumn = SLOT_POOLS[pool][nextIndex - 1]
|
|
744
744
|
|
|
745
745
|
return {
|
|
746
746
|
slotType: pool,
|
|
747
747
|
slotIndex: nextIndex,
|
|
748
748
|
slotColumn,
|
|
749
749
|
updatedState: slotState,
|
|
750
|
-
}
|
|
750
|
+
}
|
|
751
751
|
}
|
|
752
752
|
|
|
753
|
-
#assignBelongsTo(fieldDef, slotState, canonicalOverride = null) {
|
|
754
|
-
const alias = BELONGS_TO_ALIAS(fieldDef.fieldName || '', fieldDef)
|
|
755
|
-
const relationshipKey = fieldDef.relationshipKey || null
|
|
753
|
+
#assignBelongsTo (fieldDef, slotState, canonicalOverride = null) {
|
|
754
|
+
const alias = BELONGS_TO_ALIAS(fieldDef.fieldName || '', fieldDef)
|
|
755
|
+
const relationshipKey = fieldDef.relationshipKey || null
|
|
756
756
|
|
|
757
757
|
if (canonicalOverride) {
|
|
758
758
|
const normalized = typeof canonicalOverride === 'string'
|
|
759
759
|
? { idSlot: canonicalOverride }
|
|
760
|
-
: canonicalOverride
|
|
761
|
-
const { slotIndex, idColumn, typeColumn } = parseBelongsToSlots(normalized || {})
|
|
762
|
-
slotState.belongsTo = Math.max(slotState.belongsTo, slotIndex)
|
|
760
|
+
: canonicalOverride
|
|
761
|
+
const { slotIndex, idColumn, typeColumn } = parseBelongsToSlots(normalized || {})
|
|
762
|
+
slotState.belongsTo = Math.max(slotState.belongsTo, slotIndex)
|
|
763
763
|
return {
|
|
764
764
|
slotType: 'belongsTo',
|
|
765
765
|
slotIndex,
|
|
@@ -783,15 +783,15 @@ export class AnyapiRegistry {
|
|
|
783
783
|
nullable: fieldDef.nullable === true,
|
|
784
784
|
},
|
|
785
785
|
updatedState: slotState,
|
|
786
|
-
}
|
|
786
|
+
}
|
|
787
787
|
}
|
|
788
788
|
|
|
789
|
-
const nextIndex = slotState.belongsTo + 1
|
|
789
|
+
const nextIndex = slotState.belongsTo + 1
|
|
790
790
|
if (nextIndex > SLOT_LIMITS.belongsTo) {
|
|
791
|
-
throw new Error('No available belongsTo slots remaining')
|
|
791
|
+
throw new Error('No available belongsTo slots remaining')
|
|
792
792
|
}
|
|
793
793
|
|
|
794
|
-
slotState.belongsTo = nextIndex
|
|
794
|
+
slotState.belongsTo = nextIndex
|
|
795
795
|
|
|
796
796
|
return {
|
|
797
797
|
slotType: 'belongsTo',
|
|
@@ -816,6 +816,6 @@ export class AnyapiRegistry {
|
|
|
816
816
|
nullable: fieldDef.nullable === true,
|
|
817
817
|
},
|
|
818
818
|
updatedState: slotState,
|
|
819
|
-
}
|
|
819
|
+
}
|
|
820
820
|
}
|
|
821
821
|
}
|