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
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Schema and migrations"
|
|
3
|
+
chapter: 21
|
|
4
|
+
chapter_label: "21"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# 21. Schema and migrations
|
|
8
|
+
|
|
9
|
+
`RestApiKnexPlugin` exposes a small schema-management surface for table-backed resources.
|
|
10
|
+
|
|
11
|
+
Use it when you want to:
|
|
12
|
+
|
|
13
|
+
- create tables directly from resource definitions
|
|
14
|
+
- add or alter plain columns on existing tables
|
|
15
|
+
- inspect a live table snapshot
|
|
16
|
+
- generate a create migration from a resource
|
|
17
|
+
- generate an additive diff migration from a live table
|
|
18
|
+
|
|
19
|
+
This is a Knex/table feature surface. It does not exist for non-table resources.
|
|
20
|
+
|
|
21
|
+
## Overview
|
|
22
|
+
|
|
23
|
+
Once a resource is backed by `RestApiKnexPlugin`, these scope methods are available:
|
|
24
|
+
|
|
25
|
+
- `createKnexTable()`
|
|
26
|
+
- `addKnexFields({ fields })`
|
|
27
|
+
- `alterKnexFields({ fields })`
|
|
28
|
+
- `introspectKnexTableSnapshot()`
|
|
29
|
+
- `generateKnexMigration()`
|
|
30
|
+
- `generateKnexMigrationDiff()`
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
await api.addResource('memberships', {
|
|
36
|
+
schema: {
|
|
37
|
+
id: { type: 'id' },
|
|
38
|
+
workspaceId: { type: 'id', required: true },
|
|
39
|
+
userId: { type: 'id', required: true },
|
|
40
|
+
role: { type: 'string', enum: ['owner', 'member'], required: true, defaultTo: 'member' }
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
indexes: [
|
|
44
|
+
{
|
|
45
|
+
name: 'uq_memberships_workspace_user',
|
|
46
|
+
unique: true,
|
|
47
|
+
columns: ['workspaceId', 'userId']
|
|
48
|
+
}
|
|
49
|
+
],
|
|
50
|
+
|
|
51
|
+
foreignKeys: [
|
|
52
|
+
{
|
|
53
|
+
name: 'fk_memberships_workspace_user',
|
|
54
|
+
columns: ['workspaceId', 'userId'],
|
|
55
|
+
referencedTableName: 'workspace_users',
|
|
56
|
+
referencedColumns: ['workspace_id', 'user_id'],
|
|
57
|
+
deleteRule: 'CASCADE',
|
|
58
|
+
updateRule: 'RESTRICT'
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
|
|
62
|
+
checkConstraints: [
|
|
63
|
+
{
|
|
64
|
+
name: 'chk_memberships_workspace_positive',
|
|
65
|
+
clause: 'workspace_id > 0'
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
|
|
69
|
+
tableName: 'memberships'
|
|
70
|
+
})
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Default Column Naming
|
|
74
|
+
|
|
75
|
+
Table-backed resources keep logical field names in the resource schema and map them to physical columns for Knex operations.
|
|
76
|
+
|
|
77
|
+
By default, physical columns use snake_case:
|
|
78
|
+
|
|
79
|
+
- `workspaceId` -> `workspace_id`
|
|
80
|
+
- `userId` -> `user_id`
|
|
81
|
+
- `createdAt` -> `created_at`
|
|
82
|
+
|
|
83
|
+
This keeps the API surface expressive without repeating `storage.column` on every camelCase field.
|
|
84
|
+
|
|
85
|
+
You only need `storage.column` when a field should use a non-standard column name:
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
await api.addResource('profiles', {
|
|
89
|
+
schema: {
|
|
90
|
+
id: { type: 'id' },
|
|
91
|
+
displayName: { type: 'string', required: true },
|
|
92
|
+
legacyRef: { type: 'string', storage: { column: 'legacy_profile_ref' } }
|
|
93
|
+
},
|
|
94
|
+
tableName: 'profiles'
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
If you need physical columns to match the logical field names exactly for a whole resource, opt out explicitly:
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
await api.addResource('verbatim_profiles', {
|
|
102
|
+
storage: { naming: 'exact' },
|
|
103
|
+
schema: {
|
|
104
|
+
id: { type: 'id' },
|
|
105
|
+
displayName: { type: 'string', required: true }
|
|
106
|
+
},
|
|
107
|
+
tableName: 'verbatim_profiles'
|
|
108
|
+
})
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Logical IDs and `idProperty`
|
|
112
|
+
|
|
113
|
+
`idProperty` names the physical primary-key column for table-backed resources. The API contract still uses the logical resource id.
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
await api.addResource('profiles', {
|
|
117
|
+
idProperty: 'user_id',
|
|
118
|
+
schema: {
|
|
119
|
+
id: { type: 'id', required: true, storage: { column: 'user_id' } },
|
|
120
|
+
displayName: { type: 'string', required: true },
|
|
121
|
+
loginCount: { type: 'number', defaultTo: 0 }
|
|
122
|
+
},
|
|
123
|
+
tableName: 'profiles'
|
|
124
|
+
})
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
With this definition:
|
|
128
|
+
|
|
129
|
+
- writes send the resource id as `id` or `data.id`
|
|
130
|
+
- reads return the resource id as `id` or `data.id`
|
|
131
|
+
- the primary key is stored in `user_id`
|
|
132
|
+
- `displayName` and `loginCount` are stored in `display_name` and `login_count`
|
|
133
|
+
|
|
134
|
+
The resource id is not part of `attributes`:
|
|
135
|
+
|
|
136
|
+
```js
|
|
137
|
+
await api.resources.profiles.post({
|
|
138
|
+
format: 'jsonapi',
|
|
139
|
+
document: {
|
|
140
|
+
data: {
|
|
141
|
+
type: 'profiles',
|
|
142
|
+
id: '42',
|
|
143
|
+
attributes: {
|
|
144
|
+
displayName: 'Mercury'
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### ID normalization
|
|
152
|
+
|
|
153
|
+
If your primary keys need canonicalization beyond the default trim-and-stringify behavior, configure `normalizeId`.
|
|
154
|
+
|
|
155
|
+
```js
|
|
156
|
+
await api.use(RestApiPlugin, {
|
|
157
|
+
normalizeId: (value) => {
|
|
158
|
+
if (value === null || value === undefined) return null
|
|
159
|
+
|
|
160
|
+
const normalized = String(value).trim()
|
|
161
|
+
return normalized ? normalized.toUpperCase() : null
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
You can override that per resource:
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
await api.addResource('profiles', {
|
|
170
|
+
idProperty: 'user_id',
|
|
171
|
+
normalizeId: (value) => {
|
|
172
|
+
if (value === null || value === undefined) return null
|
|
173
|
+
|
|
174
|
+
const normalized = String(value).trim()
|
|
175
|
+
return normalized ? normalized.toLowerCase() : null
|
|
176
|
+
},
|
|
177
|
+
schema: {
|
|
178
|
+
id: { type: 'id', required: true, storage: { column: 'user_id' } },
|
|
179
|
+
displayName: { type: 'string', required: true }
|
|
180
|
+
},
|
|
181
|
+
tableName: 'profiles'
|
|
182
|
+
})
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
For table-backed resources, `normalizeId` is applied before:
|
|
186
|
+
|
|
187
|
+
- reading a record by id
|
|
188
|
+
- replacing, patching, or deleting a record
|
|
189
|
+
- persisting an explicit `POST` resource id
|
|
190
|
+
- validating or writing relationship identifiers that point at the resource
|
|
191
|
+
|
|
192
|
+
If the normalizer returns an empty value, existing-resource operations fail as `not_found`, while explicit `POST` document ids fail validation on `data.id`.
|
|
193
|
+
|
|
194
|
+
## Create Tables
|
|
195
|
+
|
|
196
|
+
Create the table directly from the resource definition:
|
|
197
|
+
|
|
198
|
+
```js
|
|
199
|
+
await api.resources.memberships.createKnexTable()
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`createKnexTable()` understands:
|
|
203
|
+
|
|
204
|
+
- storage-mapped column names
|
|
205
|
+
- mapped logical id columns
|
|
206
|
+
- top-level `indexes`
|
|
207
|
+
- top-level `foreignKeys`
|
|
208
|
+
- top-level `checkConstraints`
|
|
209
|
+
- enum columns
|
|
210
|
+
- MySQL-style `setValues`
|
|
211
|
+
|
|
212
|
+
This is the same table metadata used by migration generation and diffing.
|
|
213
|
+
|
|
214
|
+
## Field-Only Helpers
|
|
215
|
+
|
|
216
|
+
Two helpers only operate on columns:
|
|
217
|
+
|
|
218
|
+
```js
|
|
219
|
+
await api.resources.memberships.addKnexFields({
|
|
220
|
+
fields: {
|
|
221
|
+
noteCount: { type: 'number', defaultTo: 0, storage: { column: 'note_count' } }
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
await api.resources.memberships.alterKnexFields({
|
|
226
|
+
fields: {
|
|
227
|
+
noteCount: { type: 'number', defaultTo: 7, storage: { column: 'note_count' } }
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Important:
|
|
233
|
+
|
|
234
|
+
- these helpers are field-only
|
|
235
|
+
- they reject top-level `indexes`, `foreignKeys`, and `checkConstraints`
|
|
236
|
+
- use `createKnexTable()`, `generateKnexMigration()`, or `generateKnexMigrationDiff()` for table metadata
|
|
237
|
+
|
|
238
|
+
Both regular-table helpers use the resource's `storage.naming` setting and each
|
|
239
|
+
supplied field's `storage.column`. For example, with `storage: { naming: 'exact' }`,
|
|
240
|
+
adding or altering `loginCount` addresses `loginCount`, not `login_count`.
|
|
241
|
+
Supply the complete intended column definition when altering a field, including
|
|
242
|
+
its explicit mapping, nullability and default. These regular helpers execute DDL;
|
|
243
|
+
keep the resource declaration in app source aligned with the migrated table.
|
|
244
|
+
They do not publish a new compiled resource schema as AnyAPI field additions do.
|
|
245
|
+
|
|
246
|
+
Direct `alterKnexFields` now handles PostgreSQL enum checks separately from the
|
|
247
|
+
column type. PostgreSQL alterations run in a transaction; passing a transaction
|
|
248
|
+
to the deep helper uses a savepoint and leaves the outer commit to its owner.
|
|
249
|
+
An owned PostgreSQL alteration uses the managed completion runner: errors retain
|
|
250
|
+
the original cause and `transactionOutcome`, failed completion prevents reuse of
|
|
251
|
+
its connection, and COMMIT acknowledged as ROLLBACK rejects. A failed or lost
|
|
252
|
+
savepoint RELEASE leaves the outer transaction to its caller; roll back that
|
|
253
|
+
owner rather than assuming the failed alteration has already been undone.
|
|
254
|
+
|
|
255
|
+
An incompatible enum or failed not-null change rolls back the whole field batch.
|
|
256
|
+
The column reader does not impose the full resource snapshot's single-column-ID
|
|
257
|
+
restriction and preserves unrelated indexes.
|
|
258
|
+
|
|
259
|
+
SQLite uses Knex's transactional table rebuild. Changing an enum's allowed
|
|
260
|
+
values, adding an enum to an existing string, or removing an enum throws before
|
|
261
|
+
any field in that call changes; supply a reviewed rebuild migration instead.
|
|
262
|
+
Changing only its default works. For the deep helper, a borrowed SQLite
|
|
263
|
+
transaction must start with `foreign_keys=OFF`, since SQLite cannot disable
|
|
264
|
+
foreign keys inside a transaction. The owner must validate `PRAGMA foreign_key_check`
|
|
265
|
+
before committing and restore enforcement afterward. Standalone calls let Knex
|
|
266
|
+
manage enforcement around its rebuild, preserving referencing rows. The library
|
|
267
|
+
holds that connection lease through error cleanup. If the driver reports an
|
|
268
|
+
unfinished transaction, cleanup rolls it back and restores the original
|
|
269
|
+
foreign-key setting before the connection can be borrowed again. A connection
|
|
270
|
+
whose state cannot be verified or whose cleanup fails is marked disposed.
|
|
271
|
+
Cleanup failure throws an `AggregateError` with the original error as `cause`
|
|
272
|
+
and the original/cleanup errors in order. This does not establish an uncertain
|
|
273
|
+
COMMIT's outcome or retry the alteration.
|
|
274
|
+
|
|
275
|
+
MySQL executes the field batch as one ALTER statement. Passing an existing
|
|
276
|
+
transaction to the deep helper throws before DDL, because MySQL would implicitly
|
|
277
|
+
commit it. Run schema changes separately from application transactions.
|
|
278
|
+
|
|
279
|
+
### AnyAPI field additions
|
|
280
|
+
|
|
281
|
+
AnyAPI allocates canonical slots and refreshes its compiled resource schema:
|
|
282
|
+
|
|
283
|
+
```js
|
|
284
|
+
await api.resources.items.addKnexFields({
|
|
285
|
+
fields: {
|
|
286
|
+
note: { type: 'string', nullable: true, search: true },
|
|
287
|
+
score: { type: 'integer', defaultTo: 0 }
|
|
288
|
+
},
|
|
289
|
+
canonicalFieldsMap: { note: 'string_4' },
|
|
290
|
+
searchSchema: {
|
|
291
|
+
noteContains: { type: 'string', actualField: 'note', filterOperator: 'contains' }
|
|
292
|
+
}
|
|
293
|
+
})
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
This optional map may cover a subset of the new stored fields; remaining fields
|
|
297
|
+
use the existing allocator. An initial resource map must still cover all stored
|
|
298
|
+
fields. Invalid, occupied or unknown slots and existing field names reject the
|
|
299
|
+
addition. Computed and virtual fields need no slots and cannot have overrides.
|
|
300
|
+
AnyAPI `alterKnexFields` remains explicitly unsupported.
|
|
301
|
+
|
|
302
|
+
One call uses one owned metadata transaction. Allocation failure rolls back the
|
|
303
|
+
batch and leaves the runtime schema unchanged. Added getters, setters, defaults,
|
|
304
|
+
serializers, computed fields and filters use the existing schema compiler;
|
|
305
|
+
cached request contracts are refreshed. Ordinary table helpers retain their
|
|
306
|
+
separate DDL behavior.
|
|
307
|
+
|
|
308
|
+
Existing rows are not backfilled: unused slots stay null, and defaults apply to
|
|
309
|
+
subsequent writes. Function defaults do not run during allocation. Backfill old
|
|
310
|
+
rows explicitly when needed.
|
|
311
|
+
|
|
312
|
+
Additions are stored with their search schema, so they survive a restart
|
|
313
|
+
without being declared again. Persisted JSON cannot hold callbacks: getters,
|
|
314
|
+
setters, function defaults, serializers and `compute` functions exist only in the
|
|
315
|
+
process that added them. A server that loads such a field without declaring it
|
|
316
|
+
in code hides the field and logs a warning instead of serving it without its
|
|
317
|
+
callbacks. Declare fields with callbacks in app source on every server; a
|
|
318
|
+
declared field always takes precedence over a stored addition of the same name.
|
|
319
|
+
|
|
320
|
+
Explicit maps now take effect during registration. Repeating `createKnexTable()`
|
|
321
|
+
ensures canonical tables and refreshes schema information without replacing
|
|
322
|
+
field metadata or removing additions.
|
|
323
|
+
|
|
324
|
+
Canonical registration allocates slots from the enriched compiled definitions.
|
|
325
|
+
Stored additions that the declaration does not repeat are compiled from their
|
|
326
|
+
persisted JSON. Schema/search hooks run once per registration, `addKnexFields`
|
|
327
|
+
compilation or reload that changes the field set. Repeated table creation does
|
|
328
|
+
not rerun them. Search hook changes therefore survive descriptor refresh.
|
|
329
|
+
|
|
330
|
+
Registration never moves a stored field: existing fields keep their slots
|
|
331
|
+
whatever the declaration order, and new fields take the first free slot. A field
|
|
332
|
+
the declaration omits keeps its mapping while the resource has records, because
|
|
333
|
+
servers still running older code may use it; this declaration stops publishing
|
|
334
|
+
it. Unpopulated omitted fields are released. Changing a populated field's storage
|
|
335
|
+
type is still rejected and needs an explicit data migration. See the
|
|
336
|
+
[schema enrichment migration steps](33-migrating-to-v2.md#schema-enrichment-and-canonical-registration),
|
|
337
|
+
including the consequences for hooks that were previously overwritten.
|
|
338
|
+
|
|
339
|
+
Registering an unchanged declaration writes nothing. Concurrent schema changes
|
|
340
|
+
to one resource queue on its metadata row, and a unique index stops two fields
|
|
341
|
+
from sharing a slot. Each committed change increments the resource's
|
|
342
|
+
`schema_version`. On MySQL, the registry runs its own transactions at READ
|
|
343
|
+
COMMITTED, because REPEATABLE READ makes concurrent creation of different
|
|
344
|
+
resources or tenants deadlock. A transaction you pass in keeps its own isolation
|
|
345
|
+
level.
|
|
346
|
+
|
|
347
|
+
### AnyAPI across several servers
|
|
348
|
+
|
|
349
|
+
Each server compiles resources into memory. When one server adds fields, the
|
|
350
|
+
others keep their published schema until they reload it:
|
|
351
|
+
|
|
352
|
+
```js
|
|
353
|
+
await api.use(RestApiAnyapiKnexPlugin, {
|
|
354
|
+
knex,
|
|
355
|
+
tenantId,
|
|
356
|
+
// Runs after each committed change; publish it to the other servers.
|
|
357
|
+
onSchemaChanged: change => notifyServers(change),
|
|
358
|
+
// Safety net for missed notifications.
|
|
359
|
+
schemaCheckInterval: 5000
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
// On a server that receives a notification:
|
|
363
|
+
await api.anyapi.handleSchemaChange(change)
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
`handleSchemaChange({ tenant, resource, version })` reloads the resource only when
|
|
367
|
+
the version is newer than the published one, and returns whether it did.
|
|
368
|
+
`api.resources.items.reloadSchema()` reloads unconditionally and resolves to the
|
|
369
|
+
published version. Reloads are read-only: they combine the stored definition
|
|
370
|
+
with this server's declaration and never re-register it.
|
|
371
|
+
|
|
372
|
+
With `schemaCheckInterval`, request methods compare the stored version at most
|
|
373
|
+
once per interval; `0` checks on every request. A request rejected by validation
|
|
374
|
+
also checks the version once and, if the resource changed, reloads and retries.
|
|
375
|
+
A write whose transaction may have committed is never retried. Calls given a
|
|
376
|
+
caller transaction skip both checks, because that transaction may hold
|
|
377
|
+
uncommitted metadata. A rejected `onSchemaChanged` callback is logged; the
|
|
378
|
+
committed change stands.
|
|
379
|
+
|
|
380
|
+
A server can serve resources it never declared. `await api.anyapi.loadResource('items')`
|
|
381
|
+
loads a stored resource, with the resources it relates to, and resolves to it, or to
|
|
382
|
+
`null` when none is stored. `api.anyapi.loadResources()` loads every stored resource
|
|
383
|
+
of the tenant. A stored resource whose non-field options needed callbacks, such as
|
|
384
|
+
`normalizeId`, must be declared in code. To load resources on demand over HTTP,
|
|
385
|
+
call `loadResource` from middleware placed before the API router.
|
|
386
|
+
|
|
387
|
+
### AnyAPI tenant instances
|
|
388
|
+
|
|
389
|
+
One API instance per tenant stays cheap when the instances share their start-up work:
|
|
390
|
+
|
|
391
|
+
```js
|
|
392
|
+
// In a migration:
|
|
393
|
+
await ensureAnyApiSchema(knex, { slots: { string: 60, number: 30, boolean: 20, date: 20, json: 10, belongsTo: 15 } })
|
|
394
|
+
|
|
395
|
+
// In the application:
|
|
396
|
+
const registry = new AnyapiRegistry({ knex, cacheSize: 5000 })
|
|
397
|
+
const tenantApi = async tenantId => {
|
|
398
|
+
const api = new JsonRestApi()
|
|
399
|
+
await api.use(RestApiPlugin)
|
|
400
|
+
await api.use(RestApiAnyapiKnexPlugin, { knex, tenantId, registry, ensureSchema: false, ownsKnex: false })
|
|
401
|
+
await api.anyapi.loadResources()
|
|
402
|
+
return api
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
A shared registry reads database capabilities and the slot layout once, and holds
|
|
407
|
+
one descriptor cache, bounded by `cacheSize` (default 100). `ensureSchema: false`
|
|
408
|
+
skips table checks and the logical ID backfill at start-up and in `createKnexTable()`;
|
|
409
|
+
migrations must then run `ensureAnyApiSchema`. `ownsKnex: false` keeps `api.release()`
|
|
410
|
+
from destroying the shared Knex instance. A dropped instance needs no other cleanup.
|
|
411
|
+
|
|
412
|
+
### AnyAPI slot limits
|
|
413
|
+
|
|
414
|
+
Slot limits belong to the database: every server reads the slot columns that
|
|
415
|
+
`any_records` actually has. The defaults are 10 string, 10 number, 5 boolean, 5
|
|
416
|
+
date, 5 JSON and 5 belongsTo slots. `ensureAnyApiSchema(knex, { slots })`, or the
|
|
417
|
+
plugin's `slots` option when it ensures the schema, adds missing columns. Lower
|
|
418
|
+
counts never drop columns. A server whose pool is exhausted rereads the layout
|
|
419
|
+
once, so a migration that adds slots takes effect without a restart.
|
|
420
|
+
|
|
421
|
+
On PostgreSQL, adding a nullable column without a default does not rewrite the
|
|
422
|
+
table. MySQL can add columns instantly from 8.0.29; earlier versions may copy the
|
|
423
|
+
table. MySQL limits a row to 65,535 bytes and each belongsTo slot adds two
|
|
424
|
+
string columns, so check the row size before raising that pool substantially.
|
|
425
|
+
|
|
426
|
+
Deep `AnyapiRegistry.allocateField` and `allocateFields` calls persist the field
|
|
427
|
+
definitions and their allocation together. `allocateFields` adds a batch under one
|
|
428
|
+
version; pass each field's uncompiled `declaration` to store it for other servers. Borrowed transactions own their reads/writes and bypass the
|
|
429
|
+
global descriptor cache; the registry does not commit, roll back or publish their
|
|
430
|
+
uncommitted descriptors globally. After commit, call
|
|
431
|
+
`invalidateDescriptor(tenant, resource)` before an ordinary cached read. Deep
|
|
432
|
+
registry calls do not refresh an existing API's compiled schema; use the resource
|
|
433
|
+
helper for in-process additions.
|
|
434
|
+
|
|
435
|
+
Owned registry writes preserve their original rejection when rollback also
|
|
436
|
+
fails. The registry passes the original error and secondary `cleanupErrors` to
|
|
437
|
+
its error logger with the tenant and resource. It guards that logging call;
|
|
438
|
+
these diagnostics do not add fields to a frozen error. Registry writes now reject
|
|
439
|
+
with `RestApiWriteError`, preserving that original error as `cause` and carrying
|
|
440
|
+
the transaction outcome. The [migration guide](33-migrating-to-v2.md) explains the
|
|
441
|
+
error-class change and the outcome contract's current verification status.
|
|
442
|
+
|
|
443
|
+
A failed owned write invalidates its descriptor cache entry. In particular, a
|
|
444
|
+
commit call can reject after the database has completed the transaction; the
|
|
445
|
+
registry skips further rollback and reloads metadata on the next uncached read.
|
|
446
|
+
Driver completion alone does not prove commit or rollback, so callers must not
|
|
447
|
+
assume a rejected operation is safe to retry. Descriptor cache keys distinguish
|
|
448
|
+
the full tenant/resource pair, including identifiers containing `::`. This
|
|
449
|
+
in-memory cache correction requires no stored-data migration.
|
|
450
|
+
|
|
451
|
+
Unexpected descriptor read failures retain their original `cause` and context
|
|
452
|
+
identifying the tenant, resource (`scopeName`) and `descriptor` phase. Typed
|
|
453
|
+
API errors retain their identity on reads and become the cause of
|
|
454
|
+
`RestApiWriteError` when propagated through a write. Relationship processing propagates these
|
|
455
|
+
failures instead of returning partial linkage or includes. Direct registry
|
|
456
|
+
lookups return null for absent metadata; operations requiring that relationship
|
|
457
|
+
descriptor reject a missing descriptor. Schema errors and database failures
|
|
458
|
+
are distinct from a valid relationship with no matching records.
|
|
459
|
+
|
|
460
|
+
## Live Table Snapshots
|
|
461
|
+
|
|
462
|
+
Use `introspectKnexTableSnapshot()` to inspect the physical table shape:
|
|
463
|
+
|
|
464
|
+
```js
|
|
465
|
+
const snapshot = await api.resources.memberships.introspectKnexTableSnapshot()
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
Returned shape:
|
|
469
|
+
|
|
470
|
+
```js
|
|
471
|
+
{
|
|
472
|
+
dialect: 'sqlite',
|
|
473
|
+
schemaName: 'main',
|
|
474
|
+
tableName: 'memberships',
|
|
475
|
+
tableCollation: '',
|
|
476
|
+
idColumn: 'id',
|
|
477
|
+
primaryKeyColumns: ['id'],
|
|
478
|
+
hasWorkspaceIdColumn: true,
|
|
479
|
+
hasUserIdColumn: true,
|
|
480
|
+
columns: [...],
|
|
481
|
+
indexes: [...],
|
|
482
|
+
foreignKeys: [...],
|
|
483
|
+
checkConstraints: [...]
|
|
484
|
+
}
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
Column entries are normalized and include information such as:
|
|
488
|
+
|
|
489
|
+
- physical column name
|
|
490
|
+
- logical key
|
|
491
|
+
- type kind
|
|
492
|
+
- nullability
|
|
493
|
+
- default value
|
|
494
|
+
- precision / scale / datetime precision
|
|
495
|
+
- enum values
|
|
496
|
+
- set values
|
|
497
|
+
|
|
498
|
+
Current live introspection support:
|
|
499
|
+
|
|
500
|
+
- SQLite
|
|
501
|
+
- PostgreSQL via `pg` (verified on PostgreSQL 16)
|
|
502
|
+
- MySQL via `mysql2` (verified on MySQL 8; this does not establish MariaDB coverage)
|
|
503
|
+
|
|
504
|
+
PostgreSQL inspection reads the current schema's
|
|
505
|
+
[columns](https://www.postgresql.org/docs/16/infoschema-columns.html),
|
|
506
|
+
[indexes](https://www.postgresql.org/docs/16/catalog-pg-index.html) and
|
|
507
|
+
[constraints](https://www.postgresql.org/docs/16/catalog-pg-constraint.html).
|
|
508
|
+
It recognizes serial/identity allocation, mapped integer/string IDs, native temporal
|
|
509
|
+
precision, JSON/binary types and Knex's inline enum checks. Table and schema
|
|
510
|
+
names are bound in catalog queries.
|
|
511
|
+
|
|
512
|
+
The snapshot requires a single, non-null integer or string primary key.
|
|
513
|
+
Database allocation/default is optional: application-supplied IDs are supported,
|
|
514
|
+
and the snapshot still records the actual `autoIncrement` and default metadata.
|
|
515
|
+
PostgreSQL partial/expression indexes, included
|
|
516
|
+
index columns, nondefault null uniqueness, invalid indexes, exclusion constraints
|
|
517
|
+
and foreign keys into another schema are rejected when they cannot be represented
|
|
518
|
+
by this snapshot. Do not interpret that limited snapshot model as a database
|
|
519
|
+
limitation. Broader schema capabilities remain under verification.
|
|
520
|
+
|
|
521
|
+
## Create Migrations
|
|
522
|
+
|
|
523
|
+
Use `generateKnexMigration()` to emit a Knex migration string from the resource definition:
|
|
524
|
+
|
|
525
|
+
```js
|
|
526
|
+
const migration = await api.resources.memberships.generateKnexMigration()
|
|
527
|
+
console.log(migration)
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
This generates `exports.up` and `exports.down` for full table creation/drop.
|
|
531
|
+
The generated source is CommonJS: save it with a `.cjs` extension in an ESM
|
|
532
|
+
application, and configure Knex's migration loader to include that extension.
|
|
533
|
+
Review the generated migration before running it; generation does not execute DDL.
|
|
534
|
+
|
|
535
|
+
Static defaults retain their value, including empty/quoted/padded strings,
|
|
536
|
+
booleans, SQL null and BigInt literals. A function-valued `defaultTo` belongs to
|
|
537
|
+
runtime validation: table creation and migration generation do not call it or
|
|
538
|
+
freeze its result as a database default.
|
|
539
|
+
|
|
540
|
+
New dateTime/time columns default to precision 6 unless `temporalPrecision` is
|
|
541
|
+
declared. The time declaration explicitly includes its precision because the
|
|
542
|
+
Knex PostgreSQL time builder does not apply the precision argument. SQLite's
|
|
543
|
+
Knex decimal columns are stored as `float`; native decimal precision/scale
|
|
544
|
+
metadata is not available there.
|
|
545
|
+
|
|
546
|
+
It includes the same table metadata as `createKnexTable()`:
|
|
547
|
+
|
|
548
|
+
- mapped columns
|
|
549
|
+
- mapped id column
|
|
550
|
+
- indexes
|
|
551
|
+
- foreign keys
|
|
552
|
+
- check constraints
|
|
553
|
+
- enum columns
|
|
554
|
+
- supported MySQL-specific set columns
|
|
555
|
+
|
|
556
|
+
## Diff Migrations
|
|
557
|
+
|
|
558
|
+
Use `generateKnexMigrationDiff()` to draft a migration by comparing one resource's
|
|
559
|
+
desired schema against its live table snapshot. Review the plan, warnings and
|
|
560
|
+
generated code before placing the file in your application's Knex migrations:
|
|
561
|
+
|
|
562
|
+
```js
|
|
563
|
+
const diff = await api.resources.memberships.generateKnexMigrationDiff()
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
Returned shape:
|
|
567
|
+
|
|
568
|
+
```js
|
|
569
|
+
{
|
|
570
|
+
migration,
|
|
571
|
+
warnings,
|
|
572
|
+
plan
|
|
573
|
+
}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
`plan` is normalized into:
|
|
577
|
+
|
|
578
|
+
- `addColumns`
|
|
579
|
+
- `alterColumns`
|
|
580
|
+
- `dropColumns`
|
|
581
|
+
- `addIndexes`
|
|
582
|
+
- `dropIndexes`
|
|
583
|
+
- `addForeignKeys`
|
|
584
|
+
- `dropForeignKeys`
|
|
585
|
+
- `addCheckConstraints`
|
|
586
|
+
- `dropCheckConstraints` (PostgreSQL inline-enum replacements)
|
|
587
|
+
|
|
588
|
+
The generated migration is intentionally additive-first:
|
|
589
|
+
|
|
590
|
+
- create missing columns
|
|
591
|
+
- alter columns when supported
|
|
592
|
+
- recreate changed indexes when needed
|
|
593
|
+
- add missing foreign keys, or drop and recreate a changed named foreign key
|
|
594
|
+
- add supported check constraints
|
|
595
|
+
|
|
596
|
+
Columns, indexes and foreign keys omitted from the resource schema are preserved
|
|
597
|
+
by default, with warnings. A resource may describe only part of an application's
|
|
598
|
+
database contract. To remove omitted metadata deliberately, pass explicit booleans:
|
|
599
|
+
|
|
600
|
+
```js
|
|
601
|
+
const diff = await api.resources.memberships.generateKnexMigrationDiff({
|
|
602
|
+
options: {
|
|
603
|
+
allowDropColumns: true,
|
|
604
|
+
allowDropIndexes: true,
|
|
605
|
+
allowDropForeignKeys: true
|
|
606
|
+
}
|
|
607
|
+
})
|
|
608
|
+
```
|
|
609
|
+
|
|
610
|
+
Enable only the removals you have reviewed. These flags cover every omitted item
|
|
611
|
+
of that kind in this table, so review each planned removal. A changed definition
|
|
612
|
+
under an existing index or foreign-key name is an explicit replacement and adds
|
|
613
|
+
a warning without needing a drop flag. Dropping a column while retaining its
|
|
614
|
+
index or foreign key rejects generation; remove that dependency explicitly too.
|
|
615
|
+
Necessary MySQL indexes supporting declared foreign keys remain preserved.
|
|
616
|
+
|
|
617
|
+
New not-null columns without a static non-null database default produce a
|
|
618
|
+
backfill warning, even when an application hook or function default would supply
|
|
619
|
+
values for future writes. Bigint-to-integer changes on native databases warn
|
|
620
|
+
about overflow. Generation does not inspect existing row values to prove either
|
|
621
|
+
change will succeed.
|
|
622
|
+
|
|
623
|
+
The diff compares effective storage declarations, including the default string
|
|
624
|
+
length and temporal precision. Native integer/float/binary metadata does not
|
|
625
|
+
produce an alteration merely because it reports implicit size information.
|
|
626
|
+
Boolean and decimal defaults compare by value; textual defaults retain their
|
|
627
|
+
contents. Equivalent check formatting does not propose a duplicate constraint,
|
|
628
|
+
and comparison preserves string-literal case.
|
|
629
|
+
|
|
630
|
+
PostgreSQL enum changes alter the text column and replace its inline check in
|
|
631
|
+
separate statements. `plan.dropCheckConstraints` makes those removals visible.
|
|
632
|
+
Use the normal transactional Knex migration execution: if existing values violate
|
|
633
|
+
the new enum, PostgreSQL can roll back the check removal and column change.
|
|
634
|
+
MySQL uses native enum alteration; its rejected narrowing case preserves the
|
|
635
|
+
old values and definition, but this does not promise transactional MySQL DDL.
|
|
636
|
+
|
|
637
|
+
Changing only native datetime/time precision now produces an executable
|
|
638
|
+
alteration. Reducing precision adds a data-loss warning. Tests execute increasing
|
|
639
|
+
precision and additive changes with existing rows, verify preserved values, then
|
|
640
|
+
require an empty second diff. MySQL non-unique indexes needed by retained foreign
|
|
641
|
+
keys are kept even when they were created implicitly by the database.
|
|
642
|
+
|
|
643
|
+
Enum values and static text/JSON/array defaults preserve apostrophes, backslashes
|
|
644
|
+
and question marks on the tested drivers. PostgreSQL literals avoid Knex's
|
|
645
|
+
parameter-marker rewriting, and MySQL introspection decodes generated JSON
|
|
646
|
+
defaults and escaped enum values without proposing another alteration. Quoted
|
|
647
|
+
constraint identifiers preserve case and spaces; changing `role` does not remove
|
|
648
|
+
a check on PostgreSQL's distinct `Role` column.
|
|
649
|
+
|
|
650
|
+
Direct table helpers require `schema` to contain `structure: fields`, including
|
|
651
|
+
add/alter helpers. Keep table metadata beside `structure`; bare field maps reject
|
|
652
|
+
before SQL. A `json-rest-schema` instance already provides `structure`. Public
|
|
653
|
+
resource calls retain `addKnexFields({ fields })` and `alterKnexFields({ fields })`.
|
|
654
|
+
See [the migration example](33-migrating-to-v2.md#table-helper-schemas-use-an-explicit-structure-wrapper).
|
|
655
|
+
|
|
656
|
+
Standalone `generateKnexMigration(tableName, schema, options)` calls must supply
|
|
657
|
+
`options.dialect` when enum/default values need dialect-specific escaping. The
|
|
658
|
+
resource-level generator already obtains it from the configured Knex client.
|
|
659
|
+
|
|
660
|
+
When a MySQL migration replaces or removes a supporting index, its plan includes
|
|
661
|
+
dropping the dependent foreign key first and restoring it after the new indexes
|
|
662
|
+
exist. This also allows removing an unwanted unique index while retaining
|
|
663
|
+
referential integrity. MySQL may create a replacement non-unique supporting
|
|
664
|
+
index. Composite keys retain their declared column order and referential actions.
|
|
665
|
+
These migrations use multiple DDL statements: pause application writes during
|
|
666
|
+
execution, and account for MySQL's implicit commits when planning recovery.
|
|
667
|
+
|
|
668
|
+
## Important Limits
|
|
669
|
+
|
|
670
|
+
This is a schema-vs-live-table diff, not a migration-history engine.
|
|
671
|
+
|
|
672
|
+
It answers:
|
|
673
|
+
|
|
674
|
+
- what the table looks like now
|
|
675
|
+
- what the resource says it should look like
|
|
676
|
+
- what Knex migration changes it can propose for review
|
|
677
|
+
|
|
678
|
+
It does not answer:
|
|
679
|
+
|
|
680
|
+
- what changed since a previous migration file
|
|
681
|
+
- how to reconstruct migration history
|
|
682
|
+
- whether the proposed change is safe for all existing data and application code
|
|
683
|
+
- how to synchronize an entire database from a resource schema
|
|
684
|
+
|
|
685
|
+
Other important limits:
|
|
686
|
+
|
|
687
|
+
- destructive changes are surfaced as warnings
|
|
688
|
+
- omitted columns, indexes and foreign keys are preserved unless their respective
|
|
689
|
+
drop option is explicitly enabled
|
|
690
|
+
- partial and expression indexes cannot be represented by the simple SQLite or
|
|
691
|
+
PostgreSQL snapshot and reject introspection instead of producing a false match
|
|
692
|
+
- SQLite descending or non-binary-collation indexes, and MySQL expression,
|
|
693
|
+
prefix or descending indexes, also reject full snapshots
|
|
694
|
+
- generated columns reject full table snapshots on SQLite, MySQL and PostgreSQL;
|
|
695
|
+
the narrower reader used by direct field alterations remains separate
|
|
696
|
+
- SQLite check-constraint add/drop/alter support is warning-only
|
|
697
|
+
- changing/removing an existing named check is warning-only on all dialects;
|
|
698
|
+
PostgreSQL inline-enum replacements are handled separately
|
|
699
|
+
- changing the allowed enum values on SQLite is warning-only: its native ALTER
|
|
700
|
+
cannot replace the check, and Knex's column rebuild retains the old check.
|
|
701
|
+
Supply a reviewed table-rebuild migration for that change. The generated diff
|
|
702
|
+
leaves the existing enum column unchanged and reports why; it does not claim
|
|
703
|
+
that the new values became valid. Changing only an enum default is supported
|
|
704
|
+
- `setValues` diffing is only emitted for MySQL-compatible targets
|
|
705
|
+
- diff migrations do not auto-generate a destructive `down`
|
|
706
|
+
|
|
707
|
+
Passing the real-table suite does not verify every alteration: arbitrary SQL
|
|
708
|
+
default expressions, complex dependencies from other tables, native PostgreSQL
|
|
709
|
+
enum types and manual SQLite enum rebuilds remain outside its verified surface. Review the
|
|
710
|
+
generated SQL and warnings for the actual database. See
|
|
711
|
+
[contributing](../contributing.md#real-databases-and-redis) for native test commands.
|
|
712
|
+
|
|
713
|
+
PostgreSQL index sort direction, operator classes and collation details are not
|
|
714
|
+
fully represented by this snapshot. Inspect those definitions separately before
|
|
715
|
+
replacing an index; a successful snapshot is not a complete dependency or SQL
|
|
716
|
+
feature audit.
|
|
717
|
+
|
|
718
|
+
## When to Use Which Helper
|
|
719
|
+
|
|
720
|
+
- `createKnexTable()`
|
|
721
|
+
- bootstrap a table directly from the resource
|
|
722
|
+
|
|
723
|
+
- `addKnexFields()` / `alterKnexFields()`
|
|
724
|
+
- small column-only local changes
|
|
725
|
+
|
|
726
|
+
- `introspectKnexTableSnapshot()`
|
|
727
|
+
- inspect the real physical table
|
|
728
|
+
|
|
729
|
+
- `generateKnexMigration()`
|
|
730
|
+
- emit a full create migration from the current resource definition
|
|
731
|
+
|
|
732
|
+
- `generateKnexMigrationDiff()`
|
|
733
|
+
- emit an additive migration against the live table
|