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-formatter.js
CHANGED
|
@@ -1,105 +1,250 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @import { DiagnosticPrimitive, DiagnosticValue, DiagnosticObject, DiagnosticFormattingOptions } from './logger-types.js' */
|
|
1
3
|
/**
|
|
2
4
|
* Error formatting utility for comprehensive error logging
|
|
3
5
|
* Serializes errors with all their properties including custom fields like violations
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* @param {Error} error - The error to format
|
|
9
|
-
* @param {Object} options - Formatting options
|
|
10
|
-
* @param {boolean} options.includeStack - Whether to include stack trace (default: true)
|
|
11
|
-
* @param {number} options.maxDepth - Maximum depth for nested objects (default: 5)
|
|
12
|
-
* @returns {Object} Formatted error object safe for JSON serialization
|
|
13
|
-
*/
|
|
14
|
-
export function formatError (error, options = {}) {
|
|
15
|
-
const { includeStack = true, maxDepth = 5 } = options
|
|
8
|
+
import { types } from 'node:util'
|
|
9
|
+
import { errorMessage, getBinaryDiagnostic, isErrorObject } from './error-context.js'
|
|
16
10
|
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
/** @param {unknown} object @param {string | number} key @returns {unknown} */
|
|
12
|
+
export function readDiagnosticProperty (object, key) {
|
|
13
|
+
try { return object == null ? undefined : Reflect.get(Object(object), key) } catch (error) {
|
|
14
|
+
return `[Error reading ${key}: ${errorMessage(error)}]`
|
|
19
15
|
}
|
|
16
|
+
}
|
|
20
17
|
|
|
18
|
+
/** @param {DiagnosticFormattingOptions} [options] */
|
|
19
|
+
function createDiagnosticFormatter ({ includeStack = true, maxDepth = 5, redactFields = [] } = {}) {
|
|
20
|
+
const redactedFields = new Set(redactFields)
|
|
21
|
+
/** @param {string} key */
|
|
22
|
+
const isRedacted = key => redactedFields.has(key)
|
|
23
|
+
const includeErrorStack = includeStack && !isRedacted('name') && !isRedacted('message')
|
|
24
|
+
const useCustomJson = includeErrorStack && redactedFields.size === 0
|
|
25
|
+
const redactedPaths = new Set([...redactedFields].map(field => `data.attributes.${field}`))
|
|
21
26
|
// Handle circular references
|
|
22
27
|
const seen = new WeakSet()
|
|
28
|
+
let remainingNodes = 256
|
|
29
|
+
let remainingText = 8192
|
|
30
|
+
const maxEntries = 50
|
|
31
|
+
const hasBudget = () => remainingNodes > 0 && remainingText > 0
|
|
32
|
+
|
|
33
|
+
/** @overload @param {string} value @returns {string} */
|
|
34
|
+
/** @overload @param {unknown} value @returns {DiagnosticPrimitive} */
|
|
35
|
+
/** @param {unknown} value @returns {DiagnosticPrimitive} */
|
|
36
|
+
function boundedText (value) {
|
|
37
|
+
if (typeof value === 'string') {
|
|
38
|
+
const limit = Math.min(2048, remainingText)
|
|
39
|
+
remainingText -= Math.min(value.length, limit)
|
|
40
|
+
return value.length > limit ? `${value.slice(0, limit)}[Truncated]` : value
|
|
41
|
+
}
|
|
42
|
+
if (value == null || typeof value === 'number' || typeof value === 'boolean') return value
|
|
43
|
+
if (typeof value === 'bigint' || typeof value === 'symbol') return boundedText(String(value))
|
|
44
|
+
if (typeof value === 'function') return '[Function]'
|
|
45
|
+
return '[Non-string diagnostic text]'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** @param {DiagnosticObject} target @param {string} key @param {DiagnosticValue} value */
|
|
49
|
+
function setValue (target, key, value) {
|
|
50
|
+
Object.defineProperty(target, boundedText(key), { value: isRedacted(key) ? '[Redacted]' : value, enumerable: true, writable: true, configurable: true })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @param {unknown} object @param {string} key */
|
|
54
|
+
function readProperty (object, key) {
|
|
55
|
+
if (isRedacted(key)) return '[Redacted]'
|
|
56
|
+
return readDiagnosticProperty(object, key)
|
|
57
|
+
}
|
|
23
58
|
|
|
59
|
+
/** @param {unknown} error @returns {DiagnosticObject} */
|
|
60
|
+
function errorMetadata (error) {
|
|
61
|
+
// Native stack formatting can invoke name/message getters and repeat their values.
|
|
62
|
+
const stack = includeErrorStack ? readProperty(error, 'stack') : undefined
|
|
63
|
+
return {
|
|
64
|
+
name: boundedText(readProperty(error, 'name') || 'Error'),
|
|
65
|
+
message: boundedText(readProperty(error, 'message') || ''),
|
|
66
|
+
...(stack ? { stack: boundedText(stack) } : {})
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** @param {unknown} obj @param {number} [depth] @returns {DiagnosticValue} */
|
|
24
71
|
function serialize (obj, depth = 0) {
|
|
72
|
+
try {
|
|
73
|
+
return serializeValue(obj, depth)
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return boundedText(`[Error serializing: ${errorMessage(error)}]`)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** @param {unknown} obj @param {number} depth @returns {DiagnosticValue} */
|
|
80
|
+
function serializeValue (obj, depth) {
|
|
81
|
+
if (remainingNodes-- <= 0) return '[Truncated]'
|
|
25
82
|
if (depth > maxDepth) {
|
|
26
83
|
return '[Max depth exceeded]'
|
|
27
84
|
}
|
|
28
85
|
|
|
29
|
-
if (obj === null || obj
|
|
30
|
-
return obj
|
|
31
|
-
}
|
|
86
|
+
if (obj === null || typeof obj !== 'object') return boundedText(obj)
|
|
32
87
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
88
|
+
const binary = getBinaryDiagnostic(obj)
|
|
89
|
+
if (binary) return binary
|
|
36
90
|
|
|
37
91
|
if (seen.has(obj)) {
|
|
38
92
|
return '[Circular reference]'
|
|
39
93
|
}
|
|
40
94
|
|
|
41
|
-
if (obj
|
|
42
|
-
return obj.toISOString()
|
|
95
|
+
if (types.isDate(obj)) {
|
|
96
|
+
return boundedText(obj.toISOString())
|
|
43
97
|
}
|
|
44
98
|
|
|
45
|
-
if (obj
|
|
46
|
-
return obj.toString()
|
|
99
|
+
if (types.isRegExp(obj)) {
|
|
100
|
+
return boundedText(obj.toString())
|
|
47
101
|
}
|
|
48
102
|
|
|
49
103
|
seen.add(obj)
|
|
50
104
|
|
|
51
105
|
if (Array.isArray(obj)) {
|
|
52
|
-
|
|
106
|
+
const result = []
|
|
107
|
+
for (let i = 0; i < obj.length; i++) {
|
|
108
|
+
if (i >= maxEntries || !hasBudget()) {
|
|
109
|
+
result.push('[Truncated]')
|
|
110
|
+
break
|
|
111
|
+
}
|
|
112
|
+
result.push(serialize(readDiagnosticProperty(obj, i), depth + 1))
|
|
113
|
+
}
|
|
114
|
+
return result
|
|
53
115
|
}
|
|
54
116
|
|
|
55
|
-
const
|
|
56
|
-
|
|
117
|
+
const field = readProperty(obj, 'field')
|
|
118
|
+
if (typeof field === 'string' && (isRedacted(field) || redactedPaths.has(field))) {
|
|
119
|
+
return { field: boundedText(field), rule: boundedText(Object.getOwnPropertyDescriptor(obj, 'rule')?.value), message: '[Redacted]' }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const isError = isErrorObject(obj)
|
|
123
|
+
/** @type {DiagnosticObject} */
|
|
124
|
+
const result = isError ? errorMetadata(obj) : {}
|
|
125
|
+
const keys = new Set(Object.keys(obj))
|
|
126
|
+
if (isError) {
|
|
127
|
+
// Native causes and AggregateError members are not enumerable.
|
|
128
|
+
if (Object.hasOwn(obj, 'cause')) keys.add('cause')
|
|
129
|
+
if (Object.hasOwn(obj, 'errors')) keys.add('errors')
|
|
130
|
+
}
|
|
131
|
+
let entries = 0
|
|
132
|
+
for (const key of keys) {
|
|
133
|
+
if (entries++ >= maxEntries || !hasBudget()) {
|
|
134
|
+
result['[Truncated]'] = true
|
|
135
|
+
break
|
|
136
|
+
}
|
|
137
|
+
if (key === 'stack' && !includeErrorStack) continue
|
|
138
|
+
if (isRedacted(key)) {
|
|
139
|
+
setValue(result, key, '[Redacted]')
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
if (key === 'toJSON' && !useCustomJson) continue
|
|
57
143
|
try {
|
|
58
|
-
|
|
144
|
+
const value = key === 'field' ? field : Reflect.get(obj, key)
|
|
145
|
+
setValue(result, key, serialize(value, depth + 1))
|
|
59
146
|
} catch (e) {
|
|
60
|
-
result
|
|
147
|
+
setValue(result, key, boundedText(`[Error serializing: ${errorMessage(e)}]`))
|
|
61
148
|
}
|
|
62
149
|
}
|
|
63
150
|
|
|
64
151
|
return result
|
|
65
152
|
}
|
|
66
153
|
|
|
154
|
+
return { serialize, boundedText, setValue, readProperty, errorMetadata, includeErrorStack, useCustomJson, isRedacted, maxEntries, hasBudget }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Formats arbitrary diagnostic arguments with one shared preview budget.
|
|
159
|
+
* @param {unknown} value
|
|
160
|
+
* @param {DiagnosticFormattingOptions} [options]
|
|
161
|
+
* @returns {DiagnosticValue}
|
|
162
|
+
*/
|
|
163
|
+
export function formatDiagnosticValue (value, options = {}) {
|
|
164
|
+
return createDiagnosticFormatter(options).serialize(value)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** @overload @param {object} error @param {DiagnosticFormattingOptions} [options] @returns {DiagnosticObject} */
|
|
168
|
+
/** @overload @param {unknown} error @param {DiagnosticFormattingOptions} [options] @returns {DiagnosticObject | null} */
|
|
169
|
+
/**
|
|
170
|
+
* Formats an error object for logging, including all enumerable properties
|
|
171
|
+
* @param {unknown} error - Error or non-Error thrown value.
|
|
172
|
+
* @param {import('./logger-types.js').DiagnosticFormattingOptions} [options]
|
|
173
|
+
* @returns {DiagnosticObject | null} Bounded diagnostic properties, or null for an absent/falsy error.
|
|
174
|
+
*/
|
|
175
|
+
export function formatError (error, options = {}) {
|
|
176
|
+
if (!error) return null
|
|
177
|
+
let binary
|
|
178
|
+
try { binary = getBinaryDiagnostic(error) } catch { /* Continue with guarded property reads. */ }
|
|
179
|
+
if (binary) return { name: 'Error', message: 'Binary value thrown', data: binary }
|
|
180
|
+
const { serialize, boundedText, setValue, errorMetadata, includeErrorStack, useCustomJson, isRedacted, maxEntries, hasBudget } = createDiagnosticFormatter(options)
|
|
181
|
+
|
|
67
182
|
// Start with basic error properties
|
|
68
|
-
const formatted =
|
|
69
|
-
name: error.name || 'Error',
|
|
70
|
-
message: error.message || '',
|
|
71
|
-
...(includeStack && error.stack ? { stack: error.stack } : {})
|
|
72
|
-
}
|
|
183
|
+
const formatted = errorMetadata(error)
|
|
73
184
|
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
185
|
+
// Custom conversion can read or relabel values before field/stack filtering.
|
|
186
|
+
try {
|
|
187
|
+
/** @type {unknown} */
|
|
188
|
+
const toJSON = useCustomJson ? Reflect.get(Object(error), 'toJSON') : undefined
|
|
189
|
+
if (typeof toJSON === 'function') {
|
|
190
|
+
const jsonError = toJSON.call(error)
|
|
191
|
+
const serialized = serialize(jsonError)
|
|
192
|
+
if (serialized && typeof serialized === 'object') {
|
|
193
|
+
for (const [key, value] of Object.entries(serialized)) setValue(formatted, key, value)
|
|
194
|
+
} else if (serialized !== undefined) {
|
|
195
|
+
formatted.toJSONResult = serialized
|
|
196
|
+
}
|
|
81
197
|
}
|
|
198
|
+
} catch (e) {
|
|
199
|
+
formatted.toJSONError = boundedText(`Failed to call toJSON: ${errorMessage(e)}`)
|
|
82
200
|
}
|
|
83
201
|
|
|
84
202
|
// Add all enumerable properties
|
|
85
|
-
|
|
203
|
+
/** @type {string[]} */
|
|
204
|
+
let keys = []
|
|
205
|
+
try {
|
|
206
|
+
keys = Object.keys(error)
|
|
207
|
+
} catch (e) {
|
|
208
|
+
formatted.propertiesError = boundedText(`[Error listing properties: ${errorMessage(e)}]`)
|
|
209
|
+
}
|
|
210
|
+
let entries = 0
|
|
211
|
+
for (const key of keys) {
|
|
212
|
+
if (entries++ >= maxEntries || !hasBudget()) {
|
|
213
|
+
formatted['[Truncated]'] = true
|
|
214
|
+
break
|
|
215
|
+
}
|
|
216
|
+
if (key === 'stack' && !includeErrorStack) continue
|
|
217
|
+
if (isRedacted(key)) {
|
|
218
|
+
setValue(formatted, key, '[Redacted]')
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
if (key === 'toJSON' && !useCustomJson) continue
|
|
86
222
|
if (!(key in formatted)) {
|
|
87
223
|
try {
|
|
88
|
-
formatted
|
|
224
|
+
setValue(formatted, key, serialize(Reflect.get(Object(error), key)))
|
|
89
225
|
} catch (e) {
|
|
90
|
-
formatted
|
|
226
|
+
setValue(formatted, key, boundedText(`[Error serializing: ${errorMessage(e)}]`))
|
|
91
227
|
}
|
|
92
228
|
}
|
|
93
229
|
}
|
|
94
230
|
|
|
95
231
|
// Ensure specific error properties are included
|
|
96
|
-
const importantProps = ['code', 'type', 'details', 'violations', 'subtype', 'path', 'fields']
|
|
232
|
+
const importantProps = ['code', 'type', 'details', 'violations', 'subtype', 'path', 'fields', 'cause', 'errors']
|
|
97
233
|
for (const prop of importantProps) {
|
|
98
|
-
if (
|
|
234
|
+
if (!hasBudget()) {
|
|
235
|
+
formatted['[Truncated]'] = true
|
|
236
|
+
break
|
|
237
|
+
}
|
|
238
|
+
if (!(prop in formatted)) {
|
|
99
239
|
try {
|
|
100
|
-
|
|
240
|
+
if (isRedacted(prop)) {
|
|
241
|
+
if (prop in Object(error)) formatted[prop] = '[Redacted]'
|
|
242
|
+
} else {
|
|
243
|
+
const value = Reflect.get(Object(error), prop)
|
|
244
|
+
if (value !== undefined) formatted[prop] = serialize(value)
|
|
245
|
+
}
|
|
101
246
|
} catch (e) {
|
|
102
|
-
formatted[prop] = `[Error serializing: ${e
|
|
247
|
+
formatted[prop] = isRedacted(prop) ? '[Redacted]' : boundedText(`[Error serializing: ${errorMessage(e)}]`)
|
|
103
248
|
}
|
|
104
249
|
}
|
|
105
250
|
}
|
|
@@ -109,8 +254,8 @@ export function formatError (error, options = {}) {
|
|
|
109
254
|
|
|
110
255
|
/**
|
|
111
256
|
* Formats an error as a human-readable string with indentation
|
|
112
|
-
* @param {
|
|
113
|
-
* @param {
|
|
257
|
+
* @param {unknown} error - The error to format
|
|
258
|
+
* @param {DiagnosticFormattingOptions} [options]
|
|
114
259
|
* @returns {string} Formatted error string
|
|
115
260
|
*/
|
|
116
261
|
export function formatErrorString (error, options = {}) {
|
|
@@ -120,28 +265,43 @@ export function formatErrorString (error, options = {}) {
|
|
|
120
265
|
|
|
121
266
|
/**
|
|
122
267
|
* Creates a one-line summary of an error including key details
|
|
123
|
-
* @param {
|
|
268
|
+
* @param {unknown} error - The error to summarize
|
|
269
|
+
* @param {import('./logger-types.js').DiagnosticFormattingOptions} [options]
|
|
124
270
|
* @returns {string} One-line error summary
|
|
125
271
|
*/
|
|
126
|
-
export function formatErrorSummary (error) {
|
|
272
|
+
export function formatErrorSummary (error, options = {}) {
|
|
127
273
|
if (!error) return 'Unknown error'
|
|
128
274
|
|
|
129
|
-
const
|
|
275
|
+
const { readProperty } = createDiagnosticFormatter(options)
|
|
276
|
+
const details = readProperty(error, 'details')
|
|
277
|
+
const formatted = formatError({
|
|
278
|
+
message: readProperty(error, 'message'),
|
|
279
|
+
code: readProperty(error, 'code'),
|
|
280
|
+
details: { violations: readProperty(details, 'violations'), fields: readProperty(details, 'fields') }
|
|
281
|
+
}, { ...options, includeStack: false })
|
|
282
|
+
/** @param {unknown} value */
|
|
283
|
+
const text = value => typeof value === 'string' ? value : JSON.stringify(value)
|
|
284
|
+
const parts = [text(formatted.message || 'No message')]
|
|
130
285
|
|
|
131
|
-
if (
|
|
132
|
-
parts.push(`code: ${
|
|
286
|
+
if (formatted.code) {
|
|
287
|
+
parts.push(`code: ${text(formatted.code)}`)
|
|
133
288
|
}
|
|
134
289
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
290
|
+
const formattedDetails = readDiagnosticProperty(formatted, 'details')
|
|
291
|
+
const violations = readDiagnosticProperty(formattedDetails, 'violations')
|
|
292
|
+
const fields = readDiagnosticProperty(formattedDetails, 'fields')
|
|
293
|
+
if (Array.isArray(violations) && violations.length > 0) {
|
|
294
|
+
const violationSummary = violations
|
|
295
|
+
.map(v => v && typeof v === 'object' ? `${text(readDiagnosticProperty(v, 'field'))}: ${text(readDiagnosticProperty(v, 'message'))}` : text(v))
|
|
138
296
|
.join(', ')
|
|
139
297
|
parts.push(`violations: [${violationSummary}]`)
|
|
140
298
|
}
|
|
141
299
|
|
|
142
|
-
if (
|
|
143
|
-
parts.push(`fields: ${
|
|
300
|
+
if (Array.isArray(fields) && fields.length > 0) {
|
|
301
|
+
parts.push(`fields: ${fields.map(text).join(', ')}`)
|
|
144
302
|
}
|
|
303
|
+
if (formatted['[Truncated]']) parts.push('[Truncated]')
|
|
145
304
|
|
|
146
|
-
|
|
305
|
+
const summary = parts.join(' | ')
|
|
306
|
+
return summary.length > 2048 ? `${summary.slice(0, 2037)}[Truncated]` : summary
|
|
147
307
|
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/** @import { OwnedTransaction, TransactionOutcome } from './transaction-types.js' */
|
|
3
|
+
/** @import { StorageDatabase } from '../plugins/core/lib/storage/storage-types.js' */
|
|
4
|
+
/** @typedef {{ cleanupErrors?: Array<{ phase: string, error: unknown }>, [serializableTransaction]?: boolean }} CleanupContext */
|
|
5
|
+
/** @type {WeakMap<OwnedTransaction, (error?: unknown) => Promise<void>>} */
|
|
6
|
+
const transactionConnections = new WeakMap()
|
|
7
|
+
|
|
8
|
+
// Internal connector request; HTTP input cannot select transaction isolation.
|
|
9
|
+
export const serializableTransaction = Symbol('serializableTransaction')
|
|
10
|
+
|
|
11
|
+
// Own the lease without changing the driver's SQL-completion promise.
|
|
12
|
+
/**
|
|
13
|
+
* @param {StorageDatabase} knex
|
|
14
|
+
* @param {CleanupContext} [context]
|
|
15
|
+
* @returns {Promise<OwnedTransaction>}
|
|
16
|
+
*/
|
|
17
|
+
export async function createKnexTransaction (knex, context = {}) {
|
|
18
|
+
/** @type {{ __knex__disposed?: unknown }} */
|
|
19
|
+
const connection = await knex.client.acquireConnection()
|
|
20
|
+
/** @type {OwnedTransaction | undefined} */
|
|
21
|
+
let transaction
|
|
22
|
+
/** @type {Promise<void> | undefined} */
|
|
23
|
+
let releasePromise
|
|
24
|
+
/** @param {unknown} [error] */
|
|
25
|
+
const release = error => {
|
|
26
|
+
releasePromise ||= (async () => {
|
|
27
|
+
if (error !== undefined) connection.__knex__disposed = error || true
|
|
28
|
+
await knex.client.releaseConnection(connection)
|
|
29
|
+
})()
|
|
30
|
+
return releasePromise
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const serializable = context[serializableTransaction] === true
|
|
34
|
+
const sqlite = ['sqlite3', 'better-sqlite3'].includes(knex.client.config.client)
|
|
35
|
+
transaction = await knex.transaction({ connection, ...(serializable && !sqlite ? { isolationLevel: 'serializable' } : {}) })
|
|
36
|
+
transactionConnections.set(transaction, release)
|
|
37
|
+
transaction.executionPromise.then(
|
|
38
|
+
() => release(),
|
|
39
|
+
error => release(error || true)
|
|
40
|
+
).catch(() => {})
|
|
41
|
+
if (transaction.isCompleted()) await transaction.executionPromise
|
|
42
|
+
return transaction
|
|
43
|
+
} catch (error) {
|
|
44
|
+
try { await release(error || true) } catch (releaseError) {
|
|
45
|
+
(context.cleanupErrors ||= []).push({ phase: 'connectionRelease', error: releaseError })
|
|
46
|
+
}
|
|
47
|
+
if (transaction) transactionConnections.delete(transaction)
|
|
48
|
+
throw error
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {OwnedTransaction | null | undefined} transaction
|
|
54
|
+
* @param {TransactionOutcome | undefined} outcome
|
|
55
|
+
* @param {unknown} [error]
|
|
56
|
+
*/
|
|
57
|
+
export async function finishKnexTransactionConnection (transaction, outcome, error) {
|
|
58
|
+
if (!transaction) return
|
|
59
|
+
const release = transactionConnections.get(transaction)
|
|
60
|
+
if (!release) return
|
|
61
|
+
try {
|
|
62
|
+
await release(outcome === 'committed' || outcome === 'rolledBack' ? undefined : error || true)
|
|
63
|
+
} finally { transactionConnections.delete(transaction) }
|
|
64
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { StorageFieldDefinition } from '../plugins/core/lib/storage/storage-types.js'
|
|
2
|
+
|
|
3
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'trace' | 'fatal' | 'log'
|
|
4
|
+
export type DiagnosticWriter = (...args: unknown[]) => unknown
|
|
5
|
+
export type DiagnosticPrimitive = string | number | boolean | null | undefined
|
|
6
|
+
export type DiagnosticValue = DiagnosticPrimitive | DiagnosticValue[] | DiagnosticObject
|
|
7
|
+
export interface DiagnosticObject { [property: string]: DiagnosticValue }
|
|
8
|
+
export type DiagnosticLogger = Partial<Record<LogLevel, DiagnosticWriter>>
|
|
9
|
+
export interface DiagnosticFormattingOptions {
|
|
10
|
+
includeStack?: boolean
|
|
11
|
+
maxDepth?: number
|
|
12
|
+
redactFields?: readonly string[]
|
|
13
|
+
}
|
|
14
|
+
export interface DiagnosticOptions extends Omit<DiagnosticFormattingOptions, 'maxDepth'> {
|
|
15
|
+
logFullErrors?: boolean
|
|
16
|
+
schemaInfo?: { outputFields?: Record<string, StorageFieldDefinition> }
|
|
17
|
+
}
|
|
18
|
+
export interface EnhancedDiagnosticLogger extends DiagnosticLogger {
|
|
19
|
+
[property: string]: unknown
|
|
20
|
+
logError: (this: EnhancedDiagnosticLogger & { error: DiagnosticWriter }, message: string, error: unknown, additionalData?: Record<string, unknown>) => unknown
|
|
21
|
+
logValidationError: (this: EnhancedDiagnosticLogger & { error: DiagnosticWriter }, message: string, error: unknown, additionalData?: Record<string, unknown>) => unknown
|
|
22
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** @param {string} packageName @param {string} pluginName @param {string} [description] */
|
|
2
|
+
export function throwMissingPackage (packageName, pluginName, description = '') {
|
|
3
|
+
throw new Error(`Plugin '${pluginName}' requires '${packageName}'. Install it with npm install ${packageName}. ${description}`)
|
|
4
|
+
}
|
package/lib/rest-api-errors.js
CHANGED
|
@@ -1,8 +1,55 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { types } from 'node:util'
|
|
2
|
+
|
|
3
|
+
export class RestApiError extends Error {
|
|
4
|
+
/** @param {string} message @param {unknown} [code] */
|
|
5
|
+
constructor (message, code = 'REST_API_ERROR') {
|
|
6
|
+
super(message)
|
|
7
|
+
this.name = this.constructor.name
|
|
8
|
+
this.code = code
|
|
9
|
+
Error.captureStackTrace(this, this.constructor)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
2
12
|
|
|
3
13
|
export const REST_API_FIELDSET_ERROR_CODE = 'REST_API_FIELDSET_INVALID'
|
|
14
|
+
export const REST_API_INCLUDE_ERROR_CODE = 'REST_API_INCLUDE_INVALID'
|
|
4
15
|
export const REST_API_TEMPORAL_DATA_ERROR_CODE = 'REST_API_TEMPORAL_DATA_INVALID'
|
|
5
16
|
|
|
17
|
+
const transactionOutcomes = new Set(['none', 'pending', 'committed', 'rolledBack', 'unknown'])
|
|
18
|
+
const writeErrorFields = ['code', 'type', 'subtype', 'details', 'statusCode', 'status', 'path', 'parameter', 'validation']
|
|
19
|
+
|
|
20
|
+
export class RestApiWriteError extends RestApiError {
|
|
21
|
+
constructor (message, { cause, transactionOutcome } = {}) {
|
|
22
|
+
super(message, 'REST_API_WRITE')
|
|
23
|
+
if (!transactionOutcomes.has(transactionOutcome)) throw new TypeError('Invalid transaction outcome')
|
|
24
|
+
Object.defineProperty(this, 'cause', { value: cause, configurable: true, writable: true })
|
|
25
|
+
Object.defineProperty(this, 'transactionOutcome', { value: transactionOutcome, enumerable: true })
|
|
26
|
+
if (cause && (typeof cause === 'object' || typeof cause === 'function')) {
|
|
27
|
+
for (const field of writeErrorFields) {
|
|
28
|
+
let descriptor
|
|
29
|
+
let owner = cause
|
|
30
|
+
try {
|
|
31
|
+
while (owner) {
|
|
32
|
+
descriptor = Object.getOwnPropertyDescriptor(owner, field)
|
|
33
|
+
// Proxy prototype links may cycle or invent an unlimited chain.
|
|
34
|
+
if (descriptor || types.isProxy(owner)) break
|
|
35
|
+
owner = Object.getPrototypeOf(owner)
|
|
36
|
+
}
|
|
37
|
+
} catch { /* Unreadable metadata must not replace the original cause or outcome. */ }
|
|
38
|
+
if (descriptor && Object.hasOwn(descriptor, 'value')) this[field] = descriptor.value
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
toJSON () {
|
|
44
|
+
return {
|
|
45
|
+
name: this.name,
|
|
46
|
+
message: this.message,
|
|
47
|
+
...Object.fromEntries(writeErrorFields.filter(field => Object.hasOwn(this, field)).map(field => [field, this[field]])),
|
|
48
|
+
transactionOutcome: this.transactionOutcome
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
6
53
|
/**
|
|
7
54
|
* REST API specific error classes that can be mapped to HTTP status codes
|
|
8
55
|
* by the HTTP plugin or other protocol plugins
|
|
@@ -12,7 +59,11 @@ export const REST_API_TEMPORAL_DATA_ERROR_CODE = 'REST_API_TEMPORAL_DATA_INVALID
|
|
|
12
59
|
* Validation error for invalid payloads, schema violations, etc.
|
|
13
60
|
* HTTP plugin should map this to 422 Unprocessable Entity
|
|
14
61
|
*/
|
|
15
|
-
export class RestApiValidationError extends
|
|
62
|
+
export class RestApiValidationError extends RestApiError {
|
|
63
|
+
/**
|
|
64
|
+
* @param {string} message
|
|
65
|
+
* @param {{ fields?: string[], violations?: Array<{ field: string, rule?: string, message: string }> }} [options]
|
|
66
|
+
*/
|
|
16
67
|
constructor (message, { fields = [], violations = [] } = {}) {
|
|
17
68
|
super(message, 'REST_API_VALIDATION_ERROR')
|
|
18
69
|
this.code = 'REST_API_VALIDATION'
|
|
@@ -39,7 +90,7 @@ export class RestApiValidationError extends HookedApiError {
|
|
|
39
90
|
* Resource-related errors (not found, conflicts, forbidden access)
|
|
40
91
|
* HTTP plugin should map based on subtype
|
|
41
92
|
*/
|
|
42
|
-
export class RestApiResourceError extends
|
|
93
|
+
export class RestApiResourceError extends RestApiError {
|
|
43
94
|
constructor (message, { subtype, resourceType, resourceId } = {}) {
|
|
44
95
|
super(message, 'REST_API_RESOURCE_ERROR')
|
|
45
96
|
this.code = 'REST_API_RESOURCE'
|
|
@@ -64,16 +115,35 @@ export class RestApiResourceError extends HookedApiError {
|
|
|
64
115
|
}
|
|
65
116
|
}
|
|
66
117
|
|
|
118
|
+
export class RestApiVersionConflictError extends RestApiResourceError {
|
|
119
|
+
constructor ({ resourceType, resourceId } = {}) {
|
|
120
|
+
super('Resource version condition failed', { subtype: 'conflict', resourceType, resourceId })
|
|
121
|
+
this.code = 'REST_API_VERSION_CONFLICT'
|
|
122
|
+
this.type = 'rest_api_version_conflict'
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export class RestApiPreconditionFailedError extends RestApiResourceError {
|
|
127
|
+
constructor ({ resourceType, resourceId } = {}) {
|
|
128
|
+
super('HTTP representation precondition failed', { subtype: 'precondition_failed', resourceType, resourceId })
|
|
129
|
+
this.code = 'REST_API_PRECONDITION_FAILED'
|
|
130
|
+
this.type = 'rest_api_precondition_failed'
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
67
134
|
/**
|
|
68
135
|
* Payload structure errors (malformed JSON:API documents)
|
|
69
136
|
* HTTP plugin should map this to 400 Bad Request
|
|
70
137
|
*/
|
|
71
|
-
export class RestApiPayloadError extends
|
|
72
|
-
constructor (message, { path, expected, received } = {}) {
|
|
138
|
+
export class RestApiPayloadError extends RestApiError {
|
|
139
|
+
constructor (message, { path, parameter, expected, received, statusCode = 400, cause } = {}) {
|
|
73
140
|
super(message, 'REST_API_PAYLOAD_ERROR')
|
|
74
141
|
this.code = 'REST_API_PAYLOAD'
|
|
75
142
|
this.type = 'rest_api_payload'
|
|
143
|
+
this.statusCode = statusCode
|
|
144
|
+
if (cause !== undefined) this.cause = cause
|
|
76
145
|
this.path = path
|
|
146
|
+
this.parameter = parameter
|
|
77
147
|
this.details = {
|
|
78
148
|
path,
|
|
79
149
|
expected,
|
|
@@ -88,6 +158,8 @@ export class RestApiPayloadError extends HookedApiError {
|
|
|
88
158
|
code: this.code,
|
|
89
159
|
type: this.type,
|
|
90
160
|
path: this.path,
|
|
161
|
+
parameter: this.parameter,
|
|
162
|
+
statusCode: this.statusCode,
|
|
91
163
|
details: this.details,
|
|
92
164
|
stack: this.stack
|
|
93
165
|
}
|
|
@@ -100,10 +172,12 @@ export class RestApiPayloadError extends HookedApiError {
|
|
|
100
172
|
* The structured code lets transports distinguish invalid client input from
|
|
101
173
|
* query or include failures without parsing the human-readable message.
|
|
102
174
|
*/
|
|
103
|
-
export class RestApiFieldsetError extends
|
|
175
|
+
export class RestApiFieldsetError extends RestApiError {
|
|
104
176
|
constructor ({ field, resourceType } = {}) {
|
|
105
177
|
super(
|
|
106
|
-
|
|
178
|
+
field === undefined
|
|
179
|
+
? `Unknown resource type '${resourceType}' requested in sparse fields`
|
|
180
|
+
: `Unknown sparse field '${field}' requested for '${resourceType}'`,
|
|
107
181
|
REST_API_FIELDSET_ERROR_CODE
|
|
108
182
|
)
|
|
109
183
|
this.type = 'rest_api_fieldset'
|
|
@@ -127,11 +201,32 @@ export class RestApiFieldsetError extends HookedApiError {
|
|
|
127
201
|
}
|
|
128
202
|
}
|
|
129
203
|
|
|
204
|
+
export class RestApiIncludeError extends RestApiError {
|
|
205
|
+
constructor ({ path, resourceType } = {}) {
|
|
206
|
+
super(`Unsupported include path '${path}' requested for '${resourceType}'`, REST_API_INCLUDE_ERROR_CODE)
|
|
207
|
+
this.type = 'rest_api_include'
|
|
208
|
+
this.statusCode = 400
|
|
209
|
+
this.details = { path, resourceType }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
toJSON () {
|
|
213
|
+
return {
|
|
214
|
+
name: this.name,
|
|
215
|
+
message: this.message,
|
|
216
|
+
code: this.code,
|
|
217
|
+
type: this.type,
|
|
218
|
+
statusCode: this.statusCode,
|
|
219
|
+
details: this.details,
|
|
220
|
+
stack: this.stack
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
130
225
|
/**
|
|
131
226
|
* A non-null temporal value produced by storage or response enrichment does
|
|
132
227
|
* not satisfy the resource's public temporal contract.
|
|
133
228
|
*/
|
|
134
|
-
export class RestApiTemporalDataError extends
|
|
229
|
+
export class RestApiTemporalDataError extends RestApiError {
|
|
135
230
|
constructor ({ field, resourceType, fieldType, source = 'database' } = {}) {
|
|
136
231
|
const fieldPath = [resourceType, field].filter(Boolean).join('.') || 'unknown field'
|
|
137
232
|
super(
|