json-rest-api 1.0.28 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +746 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +634 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +100 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +90 -31
- package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +36 -0
- package/index.js +1 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +64 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -174
- package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +25 -16
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +67 -330
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +205 -30
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +20 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +238 -187
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +111 -287
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +109 -44
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +142 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2509
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1069
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -385
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -145
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +38 -69
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +39 -82
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +53 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1238
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -150
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -118
- package/tests/fixtures/api-configs.js +0 -2509
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-cursor-multifield.test.js +0 -643
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
package/lib/error-context.js
CHANGED
|
@@ -1,21 +1,461 @@
|
|
|
1
|
-
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @import { OwnedTransaction, TransactionOutcome } from './transaction-types.js' */
|
|
3
|
+
/** @import { TransactionContext, TransactionState, TransactionFactory, TransactionCallback, CompletionHook, CleanupDiagnostic, WriteRequest } from './transaction-types.js' */
|
|
4
|
+
import { finishKnexTransactionConnection } from './knex-transaction.js'
|
|
5
|
+
import { RestApiError, RestApiValidationError, RestApiWriteError } from './rest-api-errors.js'
|
|
6
|
+
import { types } from 'node:util'
|
|
2
7
|
|
|
8
|
+
/** @type {WeakSet<OwnedTransaction>} */
|
|
9
|
+
const commitRollbacks = new WeakSet()
|
|
10
|
+
/** @type {WeakMap<OwnedTransaction, TransactionState>} */
|
|
11
|
+
const managedTransactions = new WeakMap()
|
|
12
|
+
/** @type {WeakMap<TransactionContext, TransactionState>} */
|
|
13
|
+
const activeContexts = new WeakMap()
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {unknown} value
|
|
17
|
+
* @param {object} prototype
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
function hasPrototype (value, prototype) {
|
|
21
|
+
if (value === null || (typeof value !== 'object' && typeof value !== 'function')) return false
|
|
22
|
+
/** @type {object | null} */
|
|
23
|
+
let current = value
|
|
24
|
+
try {
|
|
25
|
+
while (current !== null) {
|
|
26
|
+
// Ordinary prototype chains cannot cycle; proxies can invent their next link.
|
|
27
|
+
if (types.isProxy(current)) return false
|
|
28
|
+
current = Object.getPrototypeOf(current)
|
|
29
|
+
if (current === prototype) return true
|
|
30
|
+
}
|
|
31
|
+
} catch { /* Unreadable prototypes do not establish identity. */ }
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** @param {unknown} error @returns {error is Error} */
|
|
36
|
+
export function isErrorObject (error) {
|
|
37
|
+
return types.isNativeError(error) || hasPrototype(error, Error.prototype)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @param {unknown} error
|
|
42
|
+
* @returns {error is RestApiError}
|
|
43
|
+
*/
|
|
3
44
|
export function isRestApiError (error) {
|
|
4
|
-
return error
|
|
45
|
+
return hasPrototype(error, RestApiError.prototype)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @template {WriteRequest} Request
|
|
50
|
+
* @template Result
|
|
51
|
+
* @param {(request: Request) => Result | Promise<Result>} handler
|
|
52
|
+
* @returns {(request: Request) => Promise<Result>}
|
|
53
|
+
*/
|
|
54
|
+
export function withWriteOutcome (handler) {
|
|
55
|
+
return async request => {
|
|
56
|
+
try {
|
|
57
|
+
assertContextAvailable(request.context)
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw wrapWriteError(error, { transaction: activeContexts.get(request.context)?.transaction })
|
|
60
|
+
}
|
|
61
|
+
Object.assign(request.context, {
|
|
62
|
+
error: undefined,
|
|
63
|
+
cleanupErrors: undefined,
|
|
64
|
+
transaction: undefined,
|
|
65
|
+
transactionOutcome: undefined,
|
|
66
|
+
transactionCommitted: false,
|
|
67
|
+
shouldCommit: false
|
|
68
|
+
})
|
|
69
|
+
try {
|
|
70
|
+
return await handler(request)
|
|
71
|
+
} catch (error) {
|
|
72
|
+
const context = request.context
|
|
73
|
+
const transaction = context.transaction || request.params?.transaction
|
|
74
|
+
const state = transaction ? managedTransactions.get(transaction) : undefined
|
|
75
|
+
const participant = state?.participants.find(operation => operation.context === context)
|
|
76
|
+
if (participant) participant.failed = true
|
|
77
|
+
const ownsTransaction = state ? state.owner === context : context.shouldCommit
|
|
78
|
+
if (ownsTransaction && state?.phase !== 'finished') {
|
|
79
|
+
await rollbackAfterError(error, context, context.transaction)
|
|
80
|
+
}
|
|
81
|
+
throw wrapWriteError(error, context, context.transaction || request.params?.transaction)
|
|
82
|
+
} finally {
|
|
83
|
+
endWriteOperation(request.context)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @template {{ context: TransactionContext }} Request
|
|
90
|
+
* @template Result
|
|
91
|
+
* @param {(request: Request) => Result} handler
|
|
92
|
+
* @returns {(request: Request) => Result}
|
|
93
|
+
*/
|
|
94
|
+
export function withAvailableContext (handler) {
|
|
95
|
+
return request => {
|
|
96
|
+
assertContextAvailable(request.context)
|
|
97
|
+
return handler(request)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {TransactionContext} context
|
|
103
|
+
*/
|
|
104
|
+
function assertContextAvailable (context) {
|
|
105
|
+
const state = activeContexts.get(context)
|
|
106
|
+
if (!state) return
|
|
107
|
+
const error = new RestApiValidationError('Context is still in use by a transaction; use a separate context for each operation', { fields: ['context'] })
|
|
108
|
+
failTransaction(state, error)
|
|
109
|
+
throw error
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @param {TransactionState | undefined} state
|
|
114
|
+
* @param {unknown} error
|
|
115
|
+
*/
|
|
116
|
+
function failTransaction (state, error) {
|
|
117
|
+
if (!state || state.phase !== 'pending' || state.failed) return
|
|
118
|
+
state.failed = true
|
|
119
|
+
state.error = error
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @param {OwnedTransaction | undefined} transaction
|
|
124
|
+
*/
|
|
125
|
+
function transactionState (transaction) {
|
|
126
|
+
for (let current = transaction; current; current = current.parentTransaction) {
|
|
127
|
+
const state = managedTransactions.get(current)
|
|
128
|
+
if (state) return state
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* @param {TransactionContext} context
|
|
134
|
+
* @param {OwnedTransaction | null | undefined} transaction
|
|
135
|
+
* @param {TransactionFactory | undefined} createTransaction
|
|
136
|
+
* @param {CompletionHook} [runHooks]
|
|
137
|
+
*/
|
|
138
|
+
export async function beginWriteTransaction (context, transaction, createTransaction, runHooks) {
|
|
139
|
+
/** @type {TransactionState | undefined} */
|
|
140
|
+
let state
|
|
141
|
+
if (transaction) {
|
|
142
|
+
state = managedTransactions.get(transaction)
|
|
143
|
+
if (!state || state.phase !== 'pending' || transaction.isCompleted()) {
|
|
144
|
+
throw new RestApiValidationError('transaction must be an active transaction from api.transaction; raw transactions and savepoints cannot own library writes', { fields: ['transaction'] })
|
|
145
|
+
}
|
|
146
|
+
if (state.failed) throw state.error
|
|
147
|
+
context.transaction = transaction
|
|
148
|
+
context.shouldCommit = false
|
|
149
|
+
} else {
|
|
150
|
+
if (typeof createTransaction !== 'function') throw new RestApiValidationError('No transaction factory is installed')
|
|
151
|
+
transaction = context.transaction = await createTransaction(context)
|
|
152
|
+
if (managedTransactions.has(transaction)) {
|
|
153
|
+
throw new RestApiValidationError('A transaction factory must create a new transaction, not return an existing owner', { fields: ['transaction'] })
|
|
154
|
+
}
|
|
155
|
+
context.shouldCommit = !!transaction
|
|
156
|
+
if (!transaction || transaction.parentTransaction) {
|
|
157
|
+
throw new RestApiValidationError('Owned transactions must be top-level; use api.transaction to compose library writes', { fields: ['transaction'] })
|
|
158
|
+
}
|
|
159
|
+
state = {
|
|
160
|
+
transaction,
|
|
161
|
+
owner: context,
|
|
162
|
+
phase: 'pending',
|
|
163
|
+
failed: false,
|
|
164
|
+
participants: [],
|
|
165
|
+
contexts: new Set(),
|
|
166
|
+
finalizers: [],
|
|
167
|
+
inFlight: new Map()
|
|
168
|
+
}
|
|
169
|
+
managedTransactions.set(transaction, state)
|
|
170
|
+
state.onQueryError = error => failTransaction(state, error)
|
|
171
|
+
transaction.on('query-error', state.onQueryError)
|
|
172
|
+
}
|
|
173
|
+
state.contexts.add(context)
|
|
174
|
+
state.inFlight.set(context, Promise.withResolvers())
|
|
175
|
+
activeContexts.set(context, state)
|
|
176
|
+
if (runHooks) state.participants.push({ context, runHooks, operationIndex: state.participants.length })
|
|
177
|
+
return transaction
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {TransactionContext} context
|
|
182
|
+
*/
|
|
183
|
+
export function endWriteOperation (context) {
|
|
184
|
+
const state = context.transaction ? managedTransactions.get(context.transaction) : undefined
|
|
185
|
+
state?.inFlight.get(context)?.resolve()
|
|
186
|
+
state?.inFlight.delete(context)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {TransactionContext} context
|
|
191
|
+
*/
|
|
192
|
+
export function isTransactionOwner (context) {
|
|
193
|
+
return activeContexts.get(context)?.owner === context
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @param {TransactionState | undefined} state
|
|
198
|
+
* @param {TransactionContext} context
|
|
199
|
+
*/
|
|
200
|
+
async function awaitOtherOperations (state, context) {
|
|
201
|
+
if (!state) return
|
|
202
|
+
const pending = []
|
|
203
|
+
for (const [operation, completion] of state.inFlight) {
|
|
204
|
+
if (operation !== context) pending.push(completion.promise)
|
|
205
|
+
}
|
|
206
|
+
await Promise.all(pending)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @template Result
|
|
211
|
+
* @param {{ params: TransactionCallback<Result>, context: TransactionContext, helpers: { newTransaction?: TransactionFactory } }} request
|
|
212
|
+
*/
|
|
213
|
+
export async function transactionMethod ({ params: callback, context, helpers }) {
|
|
214
|
+
if (typeof callback !== 'function') throw new RestApiValidationError('transaction requires a callback', { fields: ['callback'] })
|
|
215
|
+
const transaction = await beginWriteTransaction(context, null, helpers.newTransaction)
|
|
216
|
+
const value = await callback(transaction)
|
|
217
|
+
await commitTransaction(transaction, context)
|
|
218
|
+
return value
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @param {OwnedTransaction} transaction
|
|
223
|
+
* @param {() => unknown | Promise<unknown>} callback
|
|
224
|
+
*/
|
|
225
|
+
export function onTransactionFinished (transaction, callback) {
|
|
226
|
+
const state = managedTransactions.get(transaction)
|
|
227
|
+
if (state && state.phase !== 'finished') state.finalizers.push(callback)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* @param {TransactionState | undefined} state
|
|
232
|
+
* @param {TransactionContext} context
|
|
233
|
+
*/
|
|
234
|
+
async function finishTransaction (state, context) {
|
|
235
|
+
if (state?.phase === 'finished') return
|
|
236
|
+
if (!state) {
|
|
237
|
+
try {
|
|
238
|
+
await finishKnexTransactionConnection(context.transaction, context.transactionOutcome, context.error)
|
|
239
|
+
} catch (error) {
|
|
240
|
+
(context.cleanupErrors ||= []).push({ phase: 'connectionRelease', error })
|
|
241
|
+
}
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
state.phase = 'finishing'
|
|
245
|
+
const outcome = context.transactionOutcome
|
|
246
|
+
/** @type {'afterCommit' | 'afterRollback' | null} */
|
|
247
|
+
let hook = null
|
|
248
|
+
if (outcome === 'committed') {
|
|
249
|
+
hook = 'afterCommit'
|
|
250
|
+
} else if (outcome === 'rolledBack') {
|
|
251
|
+
hook = 'afterRollback'
|
|
252
|
+
}
|
|
253
|
+
let failed = false
|
|
254
|
+
let primary
|
|
255
|
+
/** @param {unknown} error
|
|
256
|
+
* @param {Omit<CleanupDiagnostic, "error">} details
|
|
257
|
+
*/
|
|
258
|
+
const retainFailure = (error, details) => {
|
|
259
|
+
if (outcome === 'committed' && !failed) {
|
|
260
|
+
failed = true
|
|
261
|
+
primary = error
|
|
262
|
+
} else {
|
|
263
|
+
(context.cleanupErrors ||= []).push({ ...details, error })
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
try {
|
|
267
|
+
try {
|
|
268
|
+
await finishKnexTransactionConnection(state.transaction, outcome, context.error)
|
|
269
|
+
} catch (error) {
|
|
270
|
+
retainFailure(error, { phase: 'connectionRelease' })
|
|
271
|
+
}
|
|
272
|
+
for (const operation of state.contexts) {
|
|
273
|
+
operation.transactionOutcome = outcome || 'unknown'
|
|
274
|
+
operation.transactionCommitted = outcome === 'committed'
|
|
275
|
+
}
|
|
276
|
+
const participants = outcome === 'rolledBack' ? [...state.participants].reverse() : state.participants
|
|
277
|
+
for (const { context: operation, runHooks, operationIndex, failed: operationFailed } of participants) {
|
|
278
|
+
if (!hook) continue
|
|
279
|
+
if (hook === 'afterRollback' && !operationFailed) operation.error = context.error
|
|
280
|
+
try {
|
|
281
|
+
await runHooks(hook)
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (hook === 'afterCommit') operation.error = error
|
|
284
|
+
retainFailure(error, { phase: hook, operationIndex, scopeName: operation.scopeName, method: operation.method })
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
for (const [finalizerIndex, finalize] of state.finalizers.entries()) {
|
|
288
|
+
try {
|
|
289
|
+
await finalize()
|
|
290
|
+
} catch (error) {
|
|
291
|
+
retainFailure(error, { phase: 'finalization', finalizerIndex })
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
} finally {
|
|
295
|
+
state.phase = 'finished'
|
|
296
|
+
if (state.onQueryError) state.transaction.off('query-error', state.onQueryError)
|
|
297
|
+
for (const operation of state.contexts) activeContexts.delete(operation)
|
|
298
|
+
state.contexts.clear()
|
|
299
|
+
state.participants.length = 0
|
|
300
|
+
state.finalizers.length = 0
|
|
301
|
+
state.inFlight.clear()
|
|
302
|
+
}
|
|
303
|
+
if (failed) throw primary
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* @param {unknown} error
|
|
308
|
+
* @param {TransactionContext} context
|
|
309
|
+
* @param {OwnedTransaction} [transaction]
|
|
310
|
+
*/
|
|
311
|
+
export function wrapWriteError (error, context, transaction = context.transaction) {
|
|
312
|
+
context.error = error
|
|
313
|
+
const wrapped = new RestApiWriteError(errorMessage(error), { cause: error, transactionOutcome: getWriteOutcome(context) })
|
|
314
|
+
failTransaction(transactionState(transaction), wrapped)
|
|
315
|
+
return wrapped
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* @param {TransactionContext} context
|
|
320
|
+
* @returns {TransactionOutcome}
|
|
321
|
+
*/
|
|
322
|
+
export function getWriteOutcome (context) {
|
|
323
|
+
if (context.transactionOutcome) return context.transactionOutcome
|
|
324
|
+
if (!context.transaction) return 'none'
|
|
325
|
+
if (context.transaction.isCompleted?.()) return 'unknown'
|
|
326
|
+
return 'pending'
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* @param {TransactionContext & { db?: { client?: { config?: { client?: unknown } } } }} context
|
|
331
|
+
* @param {{ phase: string, method?: string, scopeName?: string, backend?: unknown }} options
|
|
332
|
+
*/
|
|
333
|
+
export function getOperationDiagnosticContext (context, {
|
|
334
|
+
phase,
|
|
335
|
+
method = context.method,
|
|
336
|
+
scopeName = context.scopeName,
|
|
337
|
+
backend = context.db?.client?.config?.client
|
|
338
|
+
}) {
|
|
339
|
+
return {
|
|
340
|
+
method: method ?? null,
|
|
341
|
+
scopeName: scopeName ?? null,
|
|
342
|
+
phase,
|
|
343
|
+
backend: typeof backend === 'string' ? backend : null,
|
|
344
|
+
transactionOutcome: getWriteOutcome(context)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* @param {OwnedTransaction} transaction
|
|
350
|
+
* @param {TransactionContext} context
|
|
351
|
+
*/
|
|
352
|
+
export async function commitTransaction (transaction, context) {
|
|
353
|
+
if (transaction.parentTransaction) {
|
|
354
|
+
throw new RestApiValidationError('Owned transactions must be top-level; use api.transaction to compose library writes', { fields: ['transaction'] })
|
|
355
|
+
}
|
|
356
|
+
const state = managedTransactions.get(transaction)
|
|
357
|
+
if (state && state.owner !== context) throw new RestApiValidationError('Only the transaction owner can complete it', { fields: ['transaction'] })
|
|
358
|
+
if (state?.failed) throw state.error
|
|
359
|
+
if (state && [...state.inFlight.keys()].some(operation => operation !== context)) {
|
|
360
|
+
state.phase = 'completing'
|
|
361
|
+
await awaitOtherOperations(state, context)
|
|
362
|
+
throw new RestApiValidationError('Await every library operation before returning from the transaction callback', { fields: ['callback'] })
|
|
363
|
+
}
|
|
364
|
+
if (transaction.isCompleted()) throw new RestApiValidationError('Transaction was completed outside its owner', { fields: ['transaction'] })
|
|
365
|
+
if (state) state.phase = 'completing'
|
|
366
|
+
context.transactionOutcome = 'unknown'
|
|
367
|
+
const result = /** @type {unknown} */ (await transaction.commit())
|
|
368
|
+
const response = result && typeof result === 'object' && 'response' in result ? result.response : undefined
|
|
369
|
+
const rolledBack = !!response && typeof response === 'object' && 'command' in response && response.command === 'ROLLBACK'
|
|
370
|
+
if (rolledBack) {
|
|
371
|
+
commitRollbacks.add(transaction)
|
|
372
|
+
context.transactionOutcome = 'rolledBack'
|
|
373
|
+
}
|
|
374
|
+
await transaction.executionPromise
|
|
375
|
+
if (rolledBack) throw new Error('Transaction was rolled back instead of committed')
|
|
376
|
+
context.transactionOutcome = 'committed'
|
|
377
|
+
context.transactionCommitted = true
|
|
378
|
+
await finishTransaction(state, context)
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* @param {unknown} error
|
|
383
|
+
* @param {TransactionContext} context
|
|
384
|
+
* @param {OwnedTransaction | undefined} transaction
|
|
385
|
+
*/
|
|
386
|
+
export async function rollbackAfterError (error, context, transaction) {
|
|
387
|
+
context.error = error
|
|
388
|
+
context.cleanupErrors ||= []
|
|
389
|
+
if (!transaction) return false
|
|
390
|
+
const state = managedTransactions.get(transaction)
|
|
391
|
+
if (state && state.owner !== context) return false
|
|
392
|
+
if (state?.phase === 'finished') return false
|
|
393
|
+
const participant = state?.participants.find(operation => operation.context === context)
|
|
394
|
+
if (participant) participant.failed = true
|
|
395
|
+
if (state) state.phase = 'completing'
|
|
396
|
+
await awaitOtherOperations(state, context)
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
// PostgreSQL can acknowledge COMMIT with ROLLBACK after a caught SQL error.
|
|
400
|
+
if (commitRollbacks.delete(transaction)) {
|
|
401
|
+
context.transactionOutcome = 'rolledBack'
|
|
402
|
+
await finishTransaction(state, context)
|
|
403
|
+
return true
|
|
404
|
+
}
|
|
405
|
+
if (transaction.isCompleted?.()) {
|
|
406
|
+
await finishTransaction(state, context)
|
|
407
|
+
return false
|
|
408
|
+
}
|
|
409
|
+
context.transactionOutcome = 'unknown'
|
|
410
|
+
await transaction.rollback()
|
|
411
|
+
await transaction.executionPromise
|
|
412
|
+
context.transactionOutcome = 'rolledBack'
|
|
413
|
+
await finishTransaction(state, context)
|
|
414
|
+
return true
|
|
415
|
+
} catch (error) {
|
|
416
|
+
context.cleanupErrors.push({ phase: 'rollback', error })
|
|
417
|
+
await finishTransaction(state, context)
|
|
418
|
+
return false
|
|
419
|
+
}
|
|
5
420
|
}
|
|
6
421
|
|
|
7
422
|
/**
|
|
8
423
|
* Add operational context to unexpected failures without erasing typed API
|
|
9
424
|
* errors that callers and transports need to classify.
|
|
425
|
+
*
|
|
426
|
+
* @param {unknown} error
|
|
427
|
+
* @param {{ message?: string, context?: unknown }} [options]
|
|
10
428
|
*/
|
|
11
429
|
export function wrapUnexpectedError (error, { message, context } = {}) {
|
|
12
430
|
if (isRestApiError(error)) {
|
|
13
431
|
return error
|
|
14
432
|
}
|
|
15
433
|
|
|
16
|
-
const wrappedError = new Error(`${message}: ${error
|
|
434
|
+
const wrappedError = new Error(`${message}: ${errorMessage(error)}`, {
|
|
17
435
|
cause: error
|
|
18
436
|
})
|
|
19
|
-
wrappedError
|
|
20
|
-
|
|
437
|
+
return Object.assign(wrappedError, { context })
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* @param {unknown} error
|
|
442
|
+
*/
|
|
443
|
+
export function errorMessage (error) {
|
|
444
|
+
try {
|
|
445
|
+
if (getBinaryDiagnostic(error)) return 'Binary value thrown'
|
|
446
|
+
const message = /** @type {unknown} */ (Reflect.get(Object(error), 'message'))
|
|
447
|
+
return typeof message === 'string' ? message : String(error)
|
|
448
|
+
} catch {
|
|
449
|
+
return 'Non-Error value thrown'
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** @param {unknown} value */
|
|
454
|
+
export function getBinaryDiagnostic (value) {
|
|
455
|
+
if (ArrayBuffer.isView(value)) {
|
|
456
|
+
const type = hasPrototype(value, Buffer.prototype) ? 'Buffer' : 'ArrayBufferView'
|
|
457
|
+
return { type, byteLength: value.byteLength }
|
|
458
|
+
}
|
|
459
|
+
if (types.isSharedArrayBuffer(value)) return { type: 'SharedArrayBuffer', byteLength: value.byteLength }
|
|
460
|
+
if (types.isAnyArrayBuffer(value)) return { type: 'ArrayBuffer', byteLength: value.byteLength }
|
|
21
461
|
}
|