json-rest-api 1.0.29 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +744 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +733 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +103 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
- package/docs/GUIDE/33-migrating-to-v2.md +3838 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +37 -0
- package/index.js +3 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +70 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -177
- package/plugins/core/lib/anyapi/anyapi-registry.js +586 -418
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +120 -56
- package/plugins/core/lib/anyapi/stored-definition.js +109 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +13 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +128 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +290 -2475
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1077
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -395
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +36 -68
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +37 -81
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/anyapi.d.ts +30 -0
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +65 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -148
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -155
- package/tests/fixtures/api-configs.js +0 -2566
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-boundaries.test.js +0 -154
- package/tests/pagination-contract.test.js +0 -122
- package/tests/pagination-cursor-multifield.test.js +0 -641
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/temporal-boundaries.test.js +0 -208
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -1,7 +1,31 @@
|
|
|
1
1
|
import {
|
|
2
|
+
RestApiWriteError,
|
|
2
3
|
REST_API_FIELDSET_ERROR_CODE,
|
|
4
|
+
REST_API_INCLUDE_ERROR_CODE,
|
|
3
5
|
REST_API_TEMPORAL_DATA_ERROR_CODE
|
|
4
6
|
} from '../../../../lib/rest-api-errors.js'
|
|
7
|
+
import { isRestApiError } from '../../../../lib/error-context.js'
|
|
8
|
+
import { parse as parseContentType } from 'content-type'
|
|
9
|
+
import vary from 'vary'
|
|
10
|
+
|
|
11
|
+
export function mergeResponseHeaders (...sources) {
|
|
12
|
+
const headers = new Map()
|
|
13
|
+
for (const source of sources) {
|
|
14
|
+
for (const [name, value] of Object.entries(source || {})) {
|
|
15
|
+
if (value === undefined || value === null) continue
|
|
16
|
+
const key = name.toLowerCase()
|
|
17
|
+
headers.set(key, key === 'vary' ? vary.append(headers.get(key) || '', value) : value)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return Object.fromEntries(headers)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getHttpDiagnosticError (error) {
|
|
24
|
+
// Parser messages and stacks can quote raw input before a resource is known.
|
|
25
|
+
// Keep the original error for hooks/responses, but log only this safe preview.
|
|
26
|
+
if (error?.type !== 'entity.parse.failed') return error
|
|
27
|
+
return { name: 'SyntaxError', message: 'Invalid JSON request body', type: 'entity.parse.failed', status: 400 }
|
|
28
|
+
}
|
|
5
29
|
|
|
6
30
|
const JSON_API_WRITE_CONTENT_TYPES = [
|
|
7
31
|
'application/vnd.api+json',
|
|
@@ -12,6 +36,10 @@ export function isWriteMethod (method) {
|
|
|
12
36
|
return ['POST', 'PUT', 'PATCH', 'DELETE'].includes(String(method || '').toUpperCase())
|
|
13
37
|
}
|
|
14
38
|
|
|
39
|
+
export function isMultipartContentType (contentType) {
|
|
40
|
+
try { return parseContentType(contentType).type === 'multipart/form-data' } catch { return false }
|
|
41
|
+
}
|
|
42
|
+
|
|
15
43
|
export function getAllowedWriteContentTypes ({ allowMultipart = false } = {}) {
|
|
16
44
|
return allowMultipart
|
|
17
45
|
? [...JSON_API_WRITE_CONTENT_TYPES, 'multipart/form-data']
|
|
@@ -20,9 +48,40 @@ export function getAllowedWriteContentTypes ({ allowMultipart = false } = {}) {
|
|
|
20
48
|
|
|
21
49
|
export function isAllowedWriteContentType (contentType, { allowMultipart = false } = {}) {
|
|
22
50
|
if (!contentType) return true
|
|
51
|
+
try {
|
|
52
|
+
const media = parseContentType(contentType)
|
|
53
|
+
return getAllowedWriteContentTypes({ allowMultipart }).includes(media.type) &&
|
|
54
|
+
(media.type !== JSON_API_WRITE_CONTENT_TYPES[0] || Object.keys(media.parameters).every(key => key === 'profile'))
|
|
55
|
+
} catch { return false }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function acceptsJsonApi (accept) {
|
|
59
|
+
if (accept === undefined) return true
|
|
60
|
+
// Split only between media ranges; quoted profile URIs can contain commas.
|
|
61
|
+
const ranges = (String(accept).match(/(?:[^,"]|"(?:\\.|[^"\\])*")+/g) || []).flatMap(value => {
|
|
62
|
+
try { return [parseContentType(value.trim())] } catch { return [] }
|
|
63
|
+
})
|
|
64
|
+
const vendorType = JSON_API_WRITE_CONTENT_TYPES[0]
|
|
65
|
+
const validParameters = media => Object.keys(media.parameters).every(key => ['profile', 'q'].includes(key))
|
|
66
|
+
const vendorRanges = ranges.filter(media => media.type === vendorType)
|
|
67
|
+
if (vendorRanges.length && !vendorRanges.some(validParameters)) return false
|
|
68
|
+
const matches = ranges.flatMap(media => {
|
|
69
|
+
const specificity = [vendorType, 'application/*', '*/*'].indexOf(media.type)
|
|
70
|
+
if (specificity < 0 || !validParameters(media)) return []
|
|
71
|
+
const q = media.parameters.q ?? '1'
|
|
72
|
+
const quality = /^(?:0(?:\.\d{0,3})?|1(?:\.0{0,3})?)$/.test(q) ? Number(q) : 0
|
|
73
|
+
return [{ specificity, quality }]
|
|
74
|
+
})
|
|
75
|
+
matches.sort((a, b) => a.specificity - b.specificity || b.quality - a.quality)
|
|
76
|
+
return (matches[0]?.quality || 0) > 0
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function getNotAcceptableErrorBody () {
|
|
80
|
+
return { errors: [{ status: '406', title: 'Not Acceptable', detail: 'Accept must allow application/vnd.api+json without unsupported parameters or extensions' }] }
|
|
81
|
+
}
|
|
23
82
|
|
|
24
|
-
|
|
25
|
-
|
|
83
|
+
export function getNotFoundErrorBody (method, url) {
|
|
84
|
+
return { errors: [{ status: '404', title: 'Not Found', detail: `The requested endpoint ${method} ${url} does not exist` }] }
|
|
26
85
|
}
|
|
27
86
|
|
|
28
87
|
export function getUnsupportedMediaTypeErrorBody ({ allowMultipart = false } = {}) {
|
|
@@ -53,6 +112,9 @@ export function determineResponseStatus (method, result) {
|
|
|
53
112
|
}
|
|
54
113
|
|
|
55
114
|
export function mapRestApiErrorToHttp (error) {
|
|
115
|
+
// Read hooks can reject with null or undefined.
|
|
116
|
+
error ??= {}
|
|
117
|
+
const statusHint = error.statusCode ?? error.status
|
|
56
118
|
let status = 500
|
|
57
119
|
let errors = [{
|
|
58
120
|
status: '500',
|
|
@@ -76,6 +138,15 @@ export function mapRestApiErrorToHttp (error) {
|
|
|
76
138
|
title: 'Invalid Sparse Fieldset',
|
|
77
139
|
detail: error.message
|
|
78
140
|
}]
|
|
141
|
+
} else if (error.code === REST_API_INCLUDE_ERROR_CODE) {
|
|
142
|
+
status = 400
|
|
143
|
+
errors = [{
|
|
144
|
+
status: '400',
|
|
145
|
+
code: error.code,
|
|
146
|
+
title: 'Invalid Include Path',
|
|
147
|
+
detail: error.message,
|
|
148
|
+
source: { parameter: 'include' }
|
|
149
|
+
}]
|
|
79
150
|
} else if (error.code === REST_API_TEMPORAL_DATA_ERROR_CODE) {
|
|
80
151
|
status = 500
|
|
81
152
|
errors = [{
|
|
@@ -101,6 +172,12 @@ export function mapRestApiErrorToHttp (error) {
|
|
|
101
172
|
...(error.details ? { source: error.details } : {})
|
|
102
173
|
}]
|
|
103
174
|
}
|
|
175
|
+
} else if (error.code === 'REST_API_PRECONDITION_FAILED') {
|
|
176
|
+
status = 412
|
|
177
|
+
errors = [{ status: '412', code: error.code, title: 'Precondition Failed', detail: error.message }]
|
|
178
|
+
} else if (error.code === 'REST_API_VERSION_CONFLICT') {
|
|
179
|
+
status = 409
|
|
180
|
+
errors = [{ status: '409', code: error.code, title: 'Version Conflict', detail: error.message }]
|
|
104
181
|
} else if (error.code === 'REST_API_RESOURCE') {
|
|
105
182
|
switch (error.subtype) {
|
|
106
183
|
case 'not_found':
|
|
@@ -120,15 +197,15 @@ export function mapRestApiErrorToHttp (error) {
|
|
|
120
197
|
errors = [{ status: '400', title: 'Bad Request', detail: error.message }]
|
|
121
198
|
}
|
|
122
199
|
} else if (error.code === 'REST_API_PAYLOAD') {
|
|
123
|
-
status = 400
|
|
200
|
+
status = error.statusCode === 413 ? 413 : 400
|
|
124
201
|
errors = [{
|
|
125
|
-
status:
|
|
126
|
-
title: 'Bad Request',
|
|
202
|
+
status: String(status),
|
|
203
|
+
title: status === 413 ? 'Content Too Large' : 'Bad Request',
|
|
127
204
|
detail: error.message,
|
|
128
|
-
...(error.path ? { source: { pointer: error.path } } : {})
|
|
205
|
+
...(error.parameter ? { source: { parameter: error.parameter } } : error.path ? { source: { pointer: error.path } } : {})
|
|
129
206
|
}]
|
|
130
|
-
} else if (typeof
|
|
131
|
-
status =
|
|
207
|
+
} else if (typeof statusHint === 'number' && statusHint >= 400 && statusHint < 500) {
|
|
208
|
+
status = statusHint
|
|
132
209
|
errors = [{
|
|
133
210
|
status: String(status),
|
|
134
211
|
title: status === 415 ? 'Unsupported Media Type' : 'Bad Request',
|
|
@@ -136,6 +213,10 @@ export function mapRestApiErrorToHttp (error) {
|
|
|
136
213
|
}]
|
|
137
214
|
}
|
|
138
215
|
|
|
216
|
+
if (isRestApiError(error) && error instanceof RestApiWriteError) {
|
|
217
|
+
errors = errors.map(item => ({ ...item, meta: { ...item.meta, transactionOutcome: error.transactionOutcome } }))
|
|
218
|
+
}
|
|
219
|
+
|
|
139
220
|
return {
|
|
140
221
|
status,
|
|
141
222
|
body: { errors }
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getRequestContracts } from '../../lib/querying-writing/request-contracts.js'
|
|
2
2
|
|
|
3
|
-
export function
|
|
3
|
+
export function getTransportRouteContract ({ routeMeta, api }) {
|
|
4
4
|
if (!routeMeta) {
|
|
5
5
|
return null
|
|
6
6
|
}
|
|
@@ -38,7 +38,10 @@ export function buildTransportRouteSchema ({ routeMeta, api }) {
|
|
|
38
38
|
return null
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
return
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
return contract
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function buildTransportRouteSchema (options) {
|
|
45
|
+
const contract = getTransportRouteContract(options)
|
|
46
|
+
return contract ? { body: contract.schema.toJsonSchema({ mode: contract.mode, additionalProperties: false }) } : null
|
|
44
47
|
}
|
|
@@ -1,49 +1,17 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* File Handling Plugin for JSON REST API
|
|
3
|
-
*
|
|
4
|
-
* This plugin provides automatic file upload handling based on schema definitions.
|
|
5
|
-
* It works with any protocol connector (HTTP, Express, WebSocket, etc.) by using
|
|
6
|
-
* a detector registry pattern.
|
|
7
|
-
*
|
|
8
|
-
* Features:
|
|
9
|
-
* - Schema-driven: Detects file fields from type: 'file' in schemas
|
|
10
|
-
* - Protocol-agnostic: Works with any connector that registers a detector
|
|
11
|
-
* - Storage pluggable: Different fields can use different storage backends
|
|
12
|
-
* - Zero configuration: Just define file fields in your schema
|
|
13
|
-
*
|
|
14
|
-
* Usage:
|
|
15
|
-
* ```javascript
|
|
16
|
-
* // 1. Define schema with file fields
|
|
17
|
-
* const imageSchema = {
|
|
18
|
-
* title: { type: 'string' },
|
|
19
|
-
* uploadedImage: {
|
|
20
|
-
* type: 'file',
|
|
21
|
-
* storage: S3Storage,
|
|
22
|
-
* accepts: ['image/jpeg', 'image/png']
|
|
23
|
-
* }
|
|
24
|
-
* };
|
|
25
|
-
*
|
|
26
|
-
* // 2. Use plugins (order matters - file-handling depends on rest-api)
|
|
27
|
-
* api.use(RestApiPlugin);
|
|
28
|
-
* api.use(FileHandlingPlugin);
|
|
29
|
-
* api.use(ExpressPlugin); // Or any other connector
|
|
30
|
-
*
|
|
31
|
-
* // 3. Files are automatically handled!
|
|
32
|
-
* ```
|
|
33
|
-
*/
|
|
1
|
+
/** File detection, uploads and cleanup tied to resource writes. */
|
|
34
2
|
|
|
35
3
|
import { RestApiValidationError } from '../../lib/rest-api-errors.js'
|
|
36
|
-
import {
|
|
4
|
+
import { getOperationDiagnosticContext, wrapUnexpectedError } from '../../lib/error-context.js'
|
|
5
|
+
import { createEnhancedLogger } from '../../lib/enhanced-logger.js'
|
|
37
6
|
|
|
38
7
|
export const FileHandlingPlugin = {
|
|
39
8
|
name: 'file-handling',
|
|
40
9
|
dependencies: ['rest-api'],
|
|
41
10
|
|
|
42
|
-
install ({ addHook,
|
|
43
|
-
|
|
44
|
-
const
|
|
11
|
+
install ({ addHook, scopes, log, api }) {
|
|
12
|
+
const boundedLog = createEnhancedLogger(log)
|
|
13
|
+
const fileFieldsBySchema = new WeakMap()
|
|
45
14
|
|
|
46
|
-
// Registry of file detectors from various protocols
|
|
47
15
|
const detectorRegistry = []
|
|
48
16
|
|
|
49
17
|
/**
|
|
@@ -60,27 +28,31 @@ export const FileHandlingPlugin = {
|
|
|
60
28
|
}
|
|
61
29
|
|
|
62
30
|
detectorRegistry.push(detector)
|
|
63
|
-
|
|
31
|
+
boundedLog.debug(`Registered file detector: ${detector.name}`)
|
|
64
32
|
}
|
|
65
33
|
|
|
66
|
-
// Store detectors array for inspection
|
|
67
34
|
api.rest.fileDetectors = detectorRegistry
|
|
68
35
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
36
|
+
const recordCleanupFailure = async (context, phase, field, error) => {
|
|
37
|
+
const errors = context.cleanupErrors ||= []
|
|
38
|
+
errors.push({ phase, field, error })
|
|
39
|
+
try {
|
|
40
|
+
await createEnhancedLogger(log, { schemaInfo: context.schemaInfo }).warn('File cleanup failed', {
|
|
41
|
+
...getOperationDiagnosticContext(context, { phase }), field, error
|
|
42
|
+
})
|
|
43
|
+
} catch (error) {
|
|
44
|
+
errors.push({ phase: 'logging', during: phase, field, error })
|
|
72
45
|
}
|
|
73
|
-
context.fileHandlingUploads.push(upload)
|
|
74
46
|
}
|
|
75
47
|
|
|
76
|
-
const cleanupParsedFiles = async (files = {}) => {
|
|
77
|
-
for (const file of Object.
|
|
48
|
+
const cleanupParsedFiles = async (files = {}, context) => {
|
|
49
|
+
for (const [field, file] of Object.entries(files)) {
|
|
78
50
|
if (!file?.cleanup) continue
|
|
79
51
|
|
|
80
52
|
try {
|
|
81
53
|
await file.cleanup()
|
|
82
54
|
} catch (error) {
|
|
83
|
-
|
|
55
|
+
await recordCleanupFailure(context, 'temporaryFileCleanup', field, error)
|
|
84
56
|
}
|
|
85
57
|
}
|
|
86
58
|
}
|
|
@@ -89,130 +61,89 @@ export const FileHandlingPlugin = {
|
|
|
89
61
|
const uploads = context.fileHandlingUploads
|
|
90
62
|
if (!uploads || uploads.length === 0) return
|
|
91
63
|
|
|
92
|
-
|
|
64
|
+
const remaining = []
|
|
93
65
|
for (const upload of uploads) {
|
|
94
|
-
if (!upload
|
|
66
|
+
if (upload.transaction !== context.transaction || !upload.url || typeof upload.storage?.delete !== 'function') {
|
|
67
|
+
remaining.push(upload)
|
|
95
68
|
continue
|
|
96
69
|
}
|
|
97
70
|
|
|
98
71
|
try {
|
|
99
72
|
await upload.storage.delete(upload.url)
|
|
100
73
|
} catch (error) {
|
|
101
|
-
|
|
74
|
+
remaining.push(upload)
|
|
75
|
+
await recordCleanupFailure(context, 'uploadedFileCleanup', upload.field, error)
|
|
102
76
|
}
|
|
103
77
|
}
|
|
78
|
+
context.fileHandlingUploads = remaining
|
|
104
79
|
}
|
|
105
80
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (!schema) return
|
|
81
|
+
const getFileFields = scopeName => {
|
|
82
|
+
const schemaInfo = scopes[scopeName]?.vars?.schemaInfo
|
|
83
|
+
if (!schemaInfo) return []
|
|
84
|
+
const cached = fileFieldsBySchema.get(schemaInfo)
|
|
85
|
+
if (cached) return cached
|
|
112
86
|
|
|
113
87
|
const fileFields = []
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
for (const [fieldName, fieldConfig] of Object.entries(schema)) {
|
|
117
|
-
if (fieldConfig && fieldConfig.type === 'file') {
|
|
88
|
+
for (const [fieldName, fieldConfig] of Object.entries(schemaInfo.schemaStructure)) {
|
|
89
|
+
if (fieldConfig?.type === 'file') {
|
|
118
90
|
fileFields.push({
|
|
119
91
|
field: fieldName,
|
|
120
92
|
storage: fieldConfig.storage,
|
|
121
93
|
accepts: fieldConfig.accepts || ['*'],
|
|
122
|
-
maxSize: fieldConfig.maxSize
|
|
123
|
-
required: fieldConfig.required || false
|
|
94
|
+
maxSize: fieldConfig.maxSize
|
|
124
95
|
})
|
|
125
96
|
}
|
|
126
97
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
fileScopes.set(scopeName, fileFields)
|
|
130
|
-
log.info(`Scope '${scopeName}' has ${fileFields.length} file field(s): ${fileFields.map(f => f.field).join(', ')}`)
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Analyze existing scopes
|
|
135
|
-
for (const [scopeName, scope] of Object.entries(scopes)) {
|
|
136
|
-
if (scope._scopeOptions) {
|
|
137
|
-
analyzeScopeSchema(scopeName, scope._scopeOptions)
|
|
138
|
-
}
|
|
98
|
+
fileFieldsBySchema.set(schemaInfo, fileFields)
|
|
99
|
+
return fileFields
|
|
139
100
|
}
|
|
140
101
|
|
|
141
|
-
// Listen for new scopes being added
|
|
142
|
-
addHook('scope:added', 'analyzeFileFields', {}, ({ context }) => {
|
|
143
|
-
const { scopeName, scopeOptions } = context
|
|
144
|
-
analyzeScopeSchema(scopeName, scopeOptions)
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Process files for a scope if it has file fields
|
|
149
|
-
*/
|
|
150
102
|
const processFiles = async (scopeName, params, context) => {
|
|
151
|
-
const fileFields =
|
|
152
|
-
if (
|
|
153
|
-
return // This scope doesn't have file fields
|
|
154
|
-
}
|
|
103
|
+
const fileFields = getFileFields(scopeName)
|
|
104
|
+
if (fileFields.length === 0) return
|
|
155
105
|
|
|
156
|
-
// Try each detector to see if we have files
|
|
157
106
|
let parsed = null
|
|
158
107
|
let detectorUsed = null
|
|
159
108
|
|
|
160
109
|
for (const detector of detectorRegistry) {
|
|
110
|
+
let matched
|
|
161
111
|
try {
|
|
162
|
-
|
|
163
|
-
log.debug(`Detector '${detector.name}' matched for scope '${scopeName}'`)
|
|
164
|
-
parsed = await detector.parse(params, context)
|
|
165
|
-
detectorUsed = detector.name
|
|
166
|
-
break
|
|
167
|
-
}
|
|
112
|
+
matched = await detector.detect(params, context)
|
|
168
113
|
} catch (error) {
|
|
169
|
-
|
|
170
|
-
|
|
114
|
+
throw wrapUnexpectedError(error, {
|
|
115
|
+
message: `File detector '${detector.name}' failed`,
|
|
116
|
+
context: { scopeName, detector: detector.name, phase: 'fileDetection' }
|
|
117
|
+
})
|
|
171
118
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
fields: [fieldConfig.field],
|
|
182
|
-
violations: [{
|
|
183
|
-
field: fieldConfig.field,
|
|
184
|
-
message: 'This field is required'
|
|
185
|
-
}]
|
|
186
|
-
}
|
|
187
|
-
)
|
|
188
|
-
}
|
|
119
|
+
if (!matched) continue
|
|
120
|
+
boundedLog.debug(`Detector '${detector.name}' matched for scope '${scopeName}'`)
|
|
121
|
+
try {
|
|
122
|
+
parsed = await detector.parse(params, context)
|
|
123
|
+
} catch (error) {
|
|
124
|
+
throw wrapUnexpectedError(error, {
|
|
125
|
+
message: `File parser '${detector.name}' failed`,
|
|
126
|
+
context: { scopeName, detector: detector.name, phase: 'fileParsing' }
|
|
127
|
+
})
|
|
189
128
|
}
|
|
190
|
-
|
|
129
|
+
detectorUsed = detector.name
|
|
130
|
+
break
|
|
191
131
|
}
|
|
192
132
|
|
|
193
|
-
|
|
133
|
+
if (!parsed) return
|
|
134
|
+
|
|
194
135
|
const { fields = {}, files = {} } = parsed
|
|
195
136
|
|
|
196
137
|
try {
|
|
197
|
-
|
|
138
|
+
boundedLog.debug(`Processing files with detector '${detectorUsed}'`)
|
|
139
|
+
const fileNames = new Set(fileFields.map(config => config.field))
|
|
140
|
+
for (const name of Object.keys(files)) {
|
|
141
|
+
if (!fileNames.has(name)) throw new RestApiValidationError(`Unknown file field '${name}'`, { fields: [name] })
|
|
142
|
+
}
|
|
198
143
|
for (const fieldConfig of fileFields) {
|
|
199
144
|
const file = files[fieldConfig.field]
|
|
200
145
|
|
|
201
|
-
if (!file)
|
|
202
|
-
if (fieldConfig.required) {
|
|
203
|
-
throw new RestApiValidationError(
|
|
204
|
-
`Required file field '${fieldConfig.field}' is missing`,
|
|
205
|
-
{
|
|
206
|
-
fields: [fieldConfig.field],
|
|
207
|
-
violations: [{
|
|
208
|
-
field: fieldConfig.field,
|
|
209
|
-
message: 'This field is required'
|
|
210
|
-
}]
|
|
211
|
-
}
|
|
212
|
-
)
|
|
213
|
-
}
|
|
214
|
-
continue
|
|
215
|
-
}
|
|
146
|
+
if (!file) continue
|
|
216
147
|
|
|
217
148
|
// Validate mime type
|
|
218
149
|
if (fieldConfig.accepts[0] !== '*') {
|
|
@@ -240,7 +171,7 @@ export const FileHandlingPlugin = {
|
|
|
240
171
|
}
|
|
241
172
|
|
|
242
173
|
// Validate file size
|
|
243
|
-
if (fieldConfig.maxSize) {
|
|
174
|
+
if (fieldConfig.maxSize !== undefined) {
|
|
244
175
|
const maxBytes = parseSize(fieldConfig.maxSize)
|
|
245
176
|
if (file.size > maxBytes) {
|
|
246
177
|
throw new RestApiValidationError(
|
|
@@ -256,75 +187,53 @@ export const FileHandlingPlugin = {
|
|
|
256
187
|
}
|
|
257
188
|
}
|
|
258
189
|
|
|
259
|
-
// Upload to storage
|
|
260
190
|
if (!fieldConfig.storage) {
|
|
261
191
|
throw new Error(`No storage configured for file field '${fieldConfig.field}'`)
|
|
262
192
|
}
|
|
263
193
|
|
|
264
194
|
try {
|
|
265
195
|
const storedUrl = await fieldConfig.storage.upload(file)
|
|
266
|
-
fields
|
|
267
|
-
|
|
196
|
+
Object.defineProperty(fields, fieldConfig.field, { value: storedUrl, enumerable: true, writable: true, configurable: true })
|
|
197
|
+
const uploads = context.fileHandlingUploads ||= []
|
|
198
|
+
uploads.push({
|
|
268
199
|
field: fieldConfig.field,
|
|
269
200
|
storage: fieldConfig.storage,
|
|
270
|
-
url: storedUrl
|
|
201
|
+
url: storedUrl,
|
|
202
|
+
transaction: context.transaction
|
|
271
203
|
})
|
|
272
|
-
|
|
204
|
+
boundedLog.debug(`Uploaded file for field '${fieldConfig.field}'`)
|
|
273
205
|
} catch (error) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
fields: [fieldConfig.field],
|
|
279
|
-
violations: [{
|
|
280
|
-
field: fieldConfig.field,
|
|
281
|
-
message: error.message
|
|
282
|
-
}]
|
|
283
|
-
}
|
|
284
|
-
)
|
|
206
|
+
throw wrapUnexpectedError(error, {
|
|
207
|
+
message: `Failed to upload file for field '${fieldConfig.field}'`,
|
|
208
|
+
context: { scopeName, field: fieldConfig.field, phase: 'fileUpload' }
|
|
209
|
+
})
|
|
285
210
|
}
|
|
286
211
|
}
|
|
287
212
|
|
|
288
|
-
//
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
if (!params.inputRecord.data) {
|
|
293
|
-
params.inputRecord.data = { attributes: {} }
|
|
294
|
-
}
|
|
295
|
-
if (!params.inputRecord.data.attributes) {
|
|
296
|
-
params.inputRecord.data.attributes = {}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Merge fields into attributes
|
|
300
|
-
Object.assign(params.inputRecord.data.attributes, fields)
|
|
213
|
+
// Work on the canonical document, including calls using the plain format.
|
|
214
|
+
context.inputRecord.data ||= { attributes: {} }
|
|
215
|
+
context.inputRecord.data.attributes = { ...context.inputRecord.data.attributes, ...fields }
|
|
301
216
|
} finally {
|
|
302
|
-
await cleanupParsedFiles(files)
|
|
217
|
+
await cleanupParsedFiles(files, context)
|
|
303
218
|
}
|
|
304
219
|
}
|
|
305
220
|
|
|
306
|
-
/**
|
|
307
|
-
* Hook into REST API methods to process files
|
|
308
|
-
*/
|
|
309
221
|
addHook('beforeProcessing', 'processFiles', {}, async ({ context }) => {
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const params = context.params
|
|
313
|
-
|
|
314
|
-
// Only process for mutation methods
|
|
315
|
-
if (!['post', 'put', 'patch'].includes(method)) {
|
|
316
|
-
return
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
// Process files if this scope has file fields
|
|
320
|
-
await processFiles(scopeName, params, context)
|
|
222
|
+
if (!['post', 'put', 'patch'].includes(context.method)) return
|
|
223
|
+
await processFiles(context.scopeName, context.params, context)
|
|
321
224
|
})
|
|
322
225
|
|
|
323
226
|
addHook('afterRollback', 'cleanupUploadedFiles', {}, async ({ context }) => {
|
|
324
227
|
await cleanupUploadedFiles(context)
|
|
325
228
|
})
|
|
326
229
|
|
|
327
|
-
|
|
230
|
+
addHook('afterCommit', 'releaseUploadedFiles', {}, ({ context }) => {
|
|
231
|
+
if (context.fileHandlingUploads) {
|
|
232
|
+
context.fileHandlingUploads = context.fileHandlingUploads.filter(upload => upload.transaction !== context.transaction)
|
|
233
|
+
}
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
boundedLog.info('File handling plugin initialized successfully')
|
|
328
237
|
}
|
|
329
238
|
}
|
|
330
239
|
|
|
@@ -334,6 +243,7 @@ export const FileHandlingPlugin = {
|
|
|
334
243
|
* @returns {number} Size in bytes
|
|
335
244
|
*/
|
|
336
245
|
function parseSize (size) {
|
|
246
|
+
if (typeof size === 'number' && Number.isFinite(size) && size >= 0) return size
|
|
337
247
|
const units = {
|
|
338
248
|
b: 1,
|
|
339
249
|
kb: 1024,
|