json-rest-api 1.0.29 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +744 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +733 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +103 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
- package/docs/GUIDE/33-migrating-to-v2.md +3838 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +37 -0
- package/index.js +3 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +70 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -177
- package/plugins/core/lib/anyapi/anyapi-registry.js +586 -418
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +120 -56
- package/plugins/core/lib/anyapi/stored-definition.js +109 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +13 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +128 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1077
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -395
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +36 -68
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +37 -81
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/anyapi.d.ts +30 -0
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +65 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -148
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -155
- package/tests/fixtures/api-configs.js +0 -2566
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-boundaries.test.js +0 -154
- package/tests/pagination-contract.test.js +0 -122
- package/tests/pagination-cursor-multifield.test.js +0 -641
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/temporal-boundaries.test.js +0 -208
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -1,17 +1,42 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
import { hasKnexTableIndex } from '../dbIntrospection.js'
|
|
2
|
+
|
|
3
|
+
// Default layout of a new any_records table. The registry reads the actual
|
|
4
|
+
// columns, so a migration that adds slots raises the limits for every server.
|
|
5
|
+
export const SLOT_LIMITS = Object.freeze({
|
|
6
|
+
string: 10,
|
|
7
|
+
number: 10,
|
|
8
|
+
boolean: 5,
|
|
9
|
+
date: 5,
|
|
10
|
+
json: 5,
|
|
11
|
+
belongsTo: 5,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const SLOT_COLUMNS = {
|
|
15
|
+
string: index => [`string_${index}`],
|
|
16
|
+
number: index => [`number_${index}`],
|
|
17
|
+
boolean: index => [`boolean_${index}`],
|
|
18
|
+
date: index => [`date_${index}`],
|
|
19
|
+
json: index => [`json_${index}`],
|
|
20
|
+
belongsTo: index => [`rel_${index}_id`, `rel_${index}_type`],
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const addSlotColumn = (table, column) => {
|
|
24
|
+
if (column.startsWith('number_')) table.double(column)
|
|
25
|
+
else if (column.startsWith('boolean_')) table.boolean(column)
|
|
26
|
+
else if (column.startsWith('date_')) table.dateTime(column, { precision: 6 })
|
|
27
|
+
else if (column.startsWith('rel_')) table.string(column)
|
|
28
|
+
else table.text(column)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const resolveSlotCounts = (slots = {}) => {
|
|
32
|
+
if (!slots || typeof slots !== 'object' || Array.isArray(slots)) {
|
|
33
|
+
throw new Error('AnyAPI slots must be an object of slot counts')
|
|
34
|
+
}
|
|
35
|
+
for (const [pool, count] of Object.entries(slots)) {
|
|
36
|
+
if (!Object.hasOwn(SLOT_LIMITS, pool)) throw new Error(`Unknown AnyAPI slot pool '${pool}'`)
|
|
37
|
+
if (!Number.isInteger(count) || count < 1) throw new Error(`AnyAPI ${pool} slot count must be a positive integer`)
|
|
38
|
+
}
|
|
39
|
+
return { ...SLOT_LIMITS, ...slots }
|
|
15
40
|
}
|
|
16
41
|
|
|
17
42
|
export const DEFAULT_CANONICAL_CONFIG = {
|
|
@@ -48,46 +73,31 @@ const ensureColumn = async (knex, tableName, columnName, columnBuilder) => {
|
|
|
48
73
|
const ensureUniqueIndex = async (knex, tableName, columns, indexName) => {
|
|
49
74
|
const exists = await knex.schema.hasTable(tableName)
|
|
50
75
|
if (!exists) return
|
|
51
|
-
const hasIndex = await knex
|
|
76
|
+
const hasIndex = await hasKnexTableIndex(knex, tableName, indexName)
|
|
52
77
|
if (!hasIndex) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
})
|
|
57
|
-
} catch (error) {
|
|
58
|
-
if (!String(error?.message || '').includes('already exists')) {
|
|
59
|
-
throw error
|
|
60
|
-
}
|
|
61
|
-
}
|
|
78
|
+
await knex.schema.table(tableName, (table) => {
|
|
79
|
+
table.unique(columns, indexName)
|
|
80
|
+
})
|
|
62
81
|
}
|
|
63
82
|
}
|
|
64
83
|
|
|
65
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Create or upgrade the shared canonical tables. Run it from a migration, or let
|
|
86
|
+
* the plugin run it at start-up. `slots` only adds missing slot columns; lowering
|
|
87
|
+
* a count never drops columns that may hold data.
|
|
88
|
+
* @param {import('knex').Knex} knex
|
|
89
|
+
* @param {{ slots?: Partial<Record<keyof typeof SLOT_LIMITS, number>> }} [options]
|
|
90
|
+
*/
|
|
91
|
+
export const ensureAnyApiSchema = async (knex, { slots } = {}) => {
|
|
92
|
+
const counts = resolveSlotCounts(slots)
|
|
66
93
|
await ensureTable(knex, 'any_records', (table) => {
|
|
67
94
|
table.increments('id').primary()
|
|
68
95
|
table.string('logical_id')
|
|
69
96
|
table.string('tenant_id').notNullable()
|
|
70
97
|
table.string('resource').notNullable()
|
|
71
98
|
|
|
72
|
-
for (
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
for (let index = 1; index <= NUMBER_SLOT_COUNT; index += 1) {
|
|
76
|
-
table.double(`number_${index}`)
|
|
77
|
-
}
|
|
78
|
-
for (let index = 1; index <= BOOLEAN_SLOT_COUNT; index += 1) {
|
|
79
|
-
table.boolean(`boolean_${index}`)
|
|
80
|
-
}
|
|
81
|
-
for (let index = 1; index <= DATE_SLOT_COUNT; index += 1) {
|
|
82
|
-
table.dateTime(`date_${index}`)
|
|
83
|
-
}
|
|
84
|
-
for (let index = 1; index <= JSON_SLOT_COUNT; index += 1) {
|
|
85
|
-
table.text(`json_${index}`)
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
for (let index = 1; index <= BELONGS_TO_SLOT_COUNT; index += 1) {
|
|
89
|
-
table.string(`rel_${index}_id`)
|
|
90
|
-
table.string(`rel_${index}_type`)
|
|
99
|
+
for (const [pool, count] of Object.entries(counts)) {
|
|
100
|
+
for (let index = 1; index <= count; index += 1) SLOT_COLUMNS[pool](index).forEach(column => addSlotColumn(table, column))
|
|
91
101
|
}
|
|
92
102
|
|
|
93
103
|
table.dateTime('created_at').defaultTo(knex.fn.now()).notNullable()
|
|
@@ -99,6 +109,28 @@ export const ensureAnyApiSchema = async (knex) => {
|
|
|
99
109
|
table.index(['resource'])
|
|
100
110
|
})
|
|
101
111
|
|
|
112
|
+
if (slots) {
|
|
113
|
+
const existing = new Set(Object.keys(await knex('any_records').columnInfo()))
|
|
114
|
+
const missing = Object.entries(counts).flatMap(([pool, count]) => Array.from({ length: count }, (_, index) => SLOT_COLUMNS[pool](index + 1)))
|
|
115
|
+
.flat()
|
|
116
|
+
.filter(column => !existing.has(column))
|
|
117
|
+
if (missing.length) {
|
|
118
|
+
await knex.schema.alterTable('any_records', (table) => missing.forEach(column => addSlotColumn(table, column)))
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const client = knex.client.config.client
|
|
123
|
+
if (['pg', 'postgresql', 'mysql', 'mysql2'].includes(client)) {
|
|
124
|
+
const columns = (await knex('information_schema.columns')
|
|
125
|
+
.select({ column: 'column_name', precision: 'datetime_precision' })
|
|
126
|
+
.where('table_schema', knex.raw(client === 'pg' || client === 'postgresql' ? 'current_schema()' : 'DATABASE()'))
|
|
127
|
+
.where('table_name', 'any_records'))
|
|
128
|
+
.filter(column => /^date_\d+$/.test(column.column))
|
|
129
|
+
if (!columns.length || columns.some(column => Number(column.precision) !== 6)) {
|
|
130
|
+
throw new Error('AnyAPI temporal storage migration required: every any_records date slot must use timestamp/datetime precision 6. See 33-migrating-to-v2.md.')
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
102
134
|
await ensureColumn(knex, 'any_records', 'logical_id', (table) => {
|
|
103
135
|
table.string('logical_id')
|
|
104
136
|
})
|
|
@@ -146,15 +178,24 @@ export const ensureAnyApiSchema = async (knex) => {
|
|
|
146
178
|
table.string('resource').notNullable()
|
|
147
179
|
table.text('schema_json').notNullable()
|
|
148
180
|
table.text('relationships_json').notNullable()
|
|
181
|
+
// Holds the search schema and runtime additions too: MySQL TEXT stops at 64 KB.
|
|
182
|
+
table.text('definition_json', 'longtext')
|
|
183
|
+
table.integer('schema_version').notNullable().defaultTo(1)
|
|
149
184
|
table.dateTime('created_at').defaultTo(knex.fn.now()).notNullable()
|
|
150
185
|
table.dateTime('updated_at').defaultTo(knex.fn.now()).notNullable()
|
|
151
186
|
|
|
152
187
|
table.unique(['tenant_id', 'resource'])
|
|
153
188
|
})
|
|
189
|
+
await ensureColumn(knex, 'any_resource_configs', 'definition_json', (table) => {
|
|
190
|
+
table.text('definition_json', 'longtext')
|
|
191
|
+
})
|
|
192
|
+
await ensureColumn(knex, 'any_resource_configs', 'schema_version', (table) => {
|
|
193
|
+
table.integer('schema_version').notNullable().defaultTo(1)
|
|
194
|
+
})
|
|
154
195
|
|
|
155
196
|
await ensureTable(knex, 'any_field_configs', (table) => {
|
|
156
197
|
table.increments('id').primary()
|
|
157
|
-
table.integer('resource_config_id').notNullable()
|
|
198
|
+
table.integer('resource_config_id').unsigned().notNullable()
|
|
158
199
|
.references('id').inTable('any_resource_configs').onDelete('CASCADE')
|
|
159
200
|
table.string('field_name').notNullable()
|
|
160
201
|
table.string('slot_type').notNullable()
|
|
@@ -169,12 +210,24 @@ export const ensureAnyApiSchema = async (knex) => {
|
|
|
169
210
|
table.dateTime('updated_at').defaultTo(knex.fn.now()).notNullable()
|
|
170
211
|
|
|
171
212
|
table.unique(['resource_config_id', 'field_name'])
|
|
213
|
+
table.unique(['resource_config_id', 'slot_column'], 'any_field_configs_resource_slot_unique')
|
|
172
214
|
table.index(['resource_config_id'])
|
|
173
215
|
})
|
|
216
|
+
if (!await hasKnexTableIndex(knex, 'any_field_configs', 'any_field_configs_resource_slot_unique')) {
|
|
217
|
+
const shared = await knex('any_field_configs')
|
|
218
|
+
.select('resource_config_id', 'slot_column')
|
|
219
|
+
.groupBy('resource_config_id', 'slot_column')
|
|
220
|
+
.havingRaw('count(*) > 1')
|
|
221
|
+
.first()
|
|
222
|
+
if (shared) {
|
|
223
|
+
throw new Error(`AnyAPI slot metadata repair required: several fields of resource config ${shared.resource_config_id} share ${shared.slot_column}. Reassign them before upgrading.`)
|
|
224
|
+
}
|
|
225
|
+
await ensureUniqueIndex(knex, 'any_field_configs', ['resource_config_id', 'slot_column'], 'any_field_configs_resource_slot_unique')
|
|
226
|
+
}
|
|
174
227
|
|
|
175
228
|
await ensureTable(knex, 'any_relationship_configs', (table) => {
|
|
176
229
|
table.increments('id').primary()
|
|
177
|
-
table.integer('resource_config_id').notNullable()
|
|
230
|
+
table.integer('resource_config_id').unsigned().notNullable()
|
|
178
231
|
.references('id').inTable('any_resource_configs').onDelete('CASCADE')
|
|
179
232
|
table.string('relationship_name').notNullable()
|
|
180
233
|
table.string('relationship_type').notNullable()
|
|
@@ -191,18 +244,28 @@ export const ensureAnyApiSchema = async (knex) => {
|
|
|
191
244
|
table.dateTime('created_at').defaultTo(knex.fn.now()).notNullable()
|
|
192
245
|
table.dateTime('updated_at').defaultTo(knex.fn.now()).notNullable()
|
|
193
246
|
|
|
194
|
-
table.unique(['resource_config_id', 'relationship_name'])
|
|
247
|
+
table.unique(['resource_config_id', 'relationship_name'], 'any_relationship_configs_resource_relationship_unique')
|
|
195
248
|
table.index(['resource_config_id'])
|
|
196
249
|
})
|
|
197
250
|
}
|
|
198
251
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
252
|
+
const buildSlotPools = (hasColumns) => Object.fromEntries(Object.keys(SLOT_LIMITS).map(pool => {
|
|
253
|
+
const indexes = []
|
|
254
|
+
for (let index = 1; hasColumns(pool, index); index += 1) indexes.push(index)
|
|
255
|
+
return [pool, pool === 'belongsTo' ? indexes : indexes.map(index => SLOT_COLUMNS[pool](index)[0])]
|
|
256
|
+
}))
|
|
257
|
+
|
|
258
|
+
export const SLOT_POOLS = buildSlotPools((pool, index) => index <= SLOT_LIMITS[pool])
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Read the slot columns any_records actually has. Slots are numbered from 1
|
|
262
|
+
* without gaps; a pool ends at its first missing column.
|
|
263
|
+
* @param {import('knex').Knex | import('knex').Knex.Transaction} db
|
|
264
|
+
*/
|
|
265
|
+
export const readSlotPools = async (db) => {
|
|
266
|
+
const columns = new Set(Object.keys(await db('any_records').columnInfo()))
|
|
267
|
+
if (!columns.size) throw new Error('AnyAPI storage tables are missing; run ensureAnyApiSchema in a migration')
|
|
268
|
+
return buildSlotPools((pool, index) => SLOT_COLUMNS[pool](index).every(column => columns.has(column)))
|
|
206
269
|
}
|
|
207
270
|
|
|
208
271
|
export const TYPE_TO_POOL = new Map([
|
|
@@ -216,12 +279,13 @@ export const TYPE_TO_POOL = new Map([
|
|
|
216
279
|
['float', 'number'],
|
|
217
280
|
['decimal', 'number'],
|
|
218
281
|
['boolean', 'boolean'],
|
|
219
|
-
['date', '
|
|
282
|
+
['date', 'string'],
|
|
220
283
|
['datetime', 'date'],
|
|
221
284
|
['dateTime', 'date'],
|
|
222
|
-
['time', '
|
|
285
|
+
['time', 'string'],
|
|
223
286
|
['epochMilliseconds', 'number'],
|
|
224
287
|
['epochSeconds', 'number'],
|
|
225
288
|
['json', 'json'],
|
|
226
289
|
['object', 'json'],
|
|
290
|
+
['array', 'json'],
|
|
227
291
|
])
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Stored resource definitions keep JSON values only. The paths of values JSON
|
|
2
|
+
// cannot hold (callbacks, class instances, RegExps) are recorded, so a server
|
|
3
|
+
// loading the definition knows which fields need code it may not have.
|
|
4
|
+
|
|
5
|
+
const isPlainObject = value => {
|
|
6
|
+
const prototype = Object.getPrototypeOf(value)
|
|
7
|
+
return prototype === Object.prototype || prototype === null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @param {unknown} value
|
|
12
|
+
* @returns {{ json: any, callbacks: Array<Array<string|number>> }}
|
|
13
|
+
*/
|
|
14
|
+
export const toStoredJson = (value) => {
|
|
15
|
+
const callbacks = []
|
|
16
|
+
const visit = (current, path) => {
|
|
17
|
+
if (current === undefined) return undefined
|
|
18
|
+
if (current === null || typeof current === 'string' || typeof current === 'boolean') return current
|
|
19
|
+
if (typeof current === 'number' && Number.isFinite(current)) return current
|
|
20
|
+
if (Array.isArray(current)) return current.map((item, index) => visit(item, [...path, index]) ?? null)
|
|
21
|
+
if (typeof current === 'object' && isPlainObject(current)) {
|
|
22
|
+
const result = {}
|
|
23
|
+
for (const [key, item] of Object.entries(current)) {
|
|
24
|
+
const stored = visit(item, [...path, key])
|
|
25
|
+
if (stored !== undefined) result[key] = stored
|
|
26
|
+
}
|
|
27
|
+
return result
|
|
28
|
+
}
|
|
29
|
+
callbacks.push(path)
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
return { json: visit(value, []), callbacks }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const targetsOf = definition => definition?.oneOf || [definition?.actualField].filter(Boolean)
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Combine a resource's declaration with the fields other servers added at run
|
|
39
|
+
* time. Declared fields win; runtime fields this server added keep its local
|
|
40
|
+
* callbacks. A field whose callbacks exist in no available code is hidden
|
|
41
|
+
* rather than served without them, because a missing serializer or setter
|
|
42
|
+
* would read or write the wrong values.
|
|
43
|
+
* @param {{ declaration?: Record<string, any>, stored?: Record<string, any> | null, local?: { schema?: Record<string, any>, searchSchema?: Record<string, any> }, fromStorage?: boolean }} input
|
|
44
|
+
*/
|
|
45
|
+
export const composeResourceOptions = ({ declaration = {}, stored = null, local = {}, fromStorage = false }) => {
|
|
46
|
+
const hidden = new Set()
|
|
47
|
+
const hiddenFilters = new Set()
|
|
48
|
+
const declaredSchema = declaration.schema || {}
|
|
49
|
+
const declaredSearch = declaration.searchSchema || {}
|
|
50
|
+
const schema = { ...declaredSchema }
|
|
51
|
+
const searchSchema = { ...declaredSearch }
|
|
52
|
+
|
|
53
|
+
if (fromStorage) {
|
|
54
|
+
for (const path of stored?.callbacks || []) {
|
|
55
|
+
if (path[0] === 'schema' && path.length > 1) hidden.add(path[1])
|
|
56
|
+
else if (path[0] === 'searchSchema' && path.length > 1) hiddenFilters.add(path[1])
|
|
57
|
+
else throw new Error(`Resource option '${path.join('.')}' cannot be loaded from storage; declare the resource in code`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const runtime = stored?.runtime || {}
|
|
62
|
+
const needsCode = new Set((runtime.callbacks || []).map(path => `${path[0]}.${path[1]}`))
|
|
63
|
+
const merge = (kind, target, declared, localDefinitions = {}, hide) => {
|
|
64
|
+
for (const [name, definition] of Object.entries(runtime[kind] || {})) {
|
|
65
|
+
if (Object.hasOwn(declared, name)) continue
|
|
66
|
+
if (Object.hasOwn(localDefinitions, name)) target[name] = localDefinitions[name]
|
|
67
|
+
else if (needsCode.has(`${kind}.${name}`)) hide.add(name)
|
|
68
|
+
else target[name] = definition
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
merge('schema', schema, declaredSchema, local.schema, hidden)
|
|
72
|
+
merge('searchSchema', searchSchema, declaredSearch, local.searchSchema, hiddenFilters)
|
|
73
|
+
|
|
74
|
+
for (const name of hidden) delete schema[name]
|
|
75
|
+
for (const [name, definition] of Object.entries(searchSchema)) {
|
|
76
|
+
if (hiddenFilters.has(name) || targetsOf(definition).some(target => hidden.has(target))) {
|
|
77
|
+
hiddenFilters.add(name)
|
|
78
|
+
delete searchSchema[name]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const options = { ...declaration, schema }
|
|
83
|
+
if (Object.keys(searchSchema).length || declaration.searchSchema) options.searchSchema = searchSchema
|
|
84
|
+
if (hidden.size && Array.isArray(declaration.sortableFields)) {
|
|
85
|
+
options.sortableFields = declaration.sortableFields.filter(field => !hidden.has(String(field).replace(/^-/, '')))
|
|
86
|
+
}
|
|
87
|
+
return { options, hidden: [...hidden], hiddenFilters: [...hiddenFilters] }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resources a stored declaration refers to. Polymorphic targets must be
|
|
92
|
+
* registered before the resource itself; the others can follow.
|
|
93
|
+
* @param {Record<string, any>} options
|
|
94
|
+
*/
|
|
95
|
+
export const relatedResources = (options = {}) => {
|
|
96
|
+
const before = new Set()
|
|
97
|
+
const after = new Set()
|
|
98
|
+
for (const definition of Object.values(options.schema || {})) {
|
|
99
|
+
if (definition?.belongsTo) after.add(definition.belongsTo)
|
|
100
|
+
}
|
|
101
|
+
for (const [name, definition] of Object.entries(options.relationships || {})) {
|
|
102
|
+
for (const type of definition?.belongsToPolymorphic?.types || []) before.add(type)
|
|
103
|
+
if (definition?.type === 'hasMany' || definition?.type === 'manyToMany' || definition?.hasMany) {
|
|
104
|
+
after.add(definition.target || definition.hasMany || name)
|
|
105
|
+
}
|
|
106
|
+
if (definition?.through) after.add(definition.through)
|
|
107
|
+
}
|
|
108
|
+
return { before: [...before], after: [...after].filter(name => !before.has(name)) }
|
|
109
|
+
}
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { RestApiValidationError } from '../../../../../lib/rest-api-errors.js'
|
|
2
|
-
import { normalizeValueForDatabaseStorage } from '../../querying-writing/database-value-normalizers.js'
|
|
3
|
-
|
|
4
1
|
export const normalizeId = (value) => (value === null || value === undefined ? null : String(value))
|
|
5
2
|
|
|
6
3
|
export const findSchemaFieldByAlias = (descriptor, alias) => {
|
|
@@ -61,64 +58,3 @@ export const resolveFieldInfo = (descriptor, field) => {
|
|
|
61
58
|
|
|
62
59
|
return null
|
|
63
60
|
}
|
|
64
|
-
|
|
65
|
-
export const coerceValueForDefinition = (value, definition, { isRelationship } = {}) => {
|
|
66
|
-
if (value === null || value === undefined) return null
|
|
67
|
-
|
|
68
|
-
if (isRelationship) {
|
|
69
|
-
return normalizeId(value)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const type = definition?.type || definition?.dataType
|
|
73
|
-
if (!type) {
|
|
74
|
-
return value
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (['number', 'integer', 'float', 'decimal'].includes(type)) {
|
|
78
|
-
const numeric = Number(value)
|
|
79
|
-
return Number.isNaN(numeric) ? value : numeric
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (type === 'boolean') {
|
|
83
|
-
if (typeof value === 'boolean') return value
|
|
84
|
-
if (typeof value === 'string') {
|
|
85
|
-
if (value.toLowerCase() === 'true') return true
|
|
86
|
-
if (value.toLowerCase() === 'false') return false
|
|
87
|
-
}
|
|
88
|
-
return Boolean(value)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (['string', 'text', 'uuid', 'email'].includes(type)) {
|
|
92
|
-
return String(value)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (['date', 'dateTime', 'time'].includes(type)) {
|
|
96
|
-
return normalizeValueForDatabaseStorage(value, type, {
|
|
97
|
-
temporalPrecision: definition?.temporalPrecision
|
|
98
|
-
})
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
return value
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export const normalizeFilterValues = (rawValue, definition, options) => {
|
|
105
|
-
if (Array.isArray(rawValue)) {
|
|
106
|
-
return rawValue.map((item) => coerceValueForDefinition(item, definition, options))
|
|
107
|
-
}
|
|
108
|
-
return [coerceValueForDefinition(rawValue, definition, options)]
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export const ensureFilterableField = (descriptor, field) => {
|
|
112
|
-
const fieldInfo = resolveFieldInfo(descriptor, field)
|
|
113
|
-
if (!fieldInfo?.column) {
|
|
114
|
-
throw new RestApiValidationError('Invalid filter field', {
|
|
115
|
-
fields: [`filters.${field}`],
|
|
116
|
-
violations: [{
|
|
117
|
-
field: `filters.${field}`,
|
|
118
|
-
rule: 'unknown_field',
|
|
119
|
-
message: `Filter on '${field}' is not supported in AnyAPI mode`,
|
|
120
|
-
}],
|
|
121
|
-
})
|
|
122
|
-
}
|
|
123
|
-
return fieldInfo
|
|
124
|
-
}
|