json-rest-api 1.0.29 → 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 +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 +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} +74 -30
- 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 -177
- 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 +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 +116 -2485
- 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/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 -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,5 +1,6 @@
|
|
|
1
1
|
import { addType, addValidator, createSchema } from 'json-rest-schema'
|
|
2
2
|
import { RestApiValidationError } from '../../../../lib/rest-api-errors.js'
|
|
3
|
+
import { requireDocumentResourceId, requireExistingResourceId } from './resource-id-normalization.js'
|
|
3
4
|
import { getRelationshipCardinality } from './relationship-contracts.js'
|
|
4
5
|
|
|
5
6
|
function isPlainObject (value) {
|
|
@@ -345,18 +346,9 @@ function installRequestContractSupport () {
|
|
|
345
346
|
|
|
346
347
|
function buildWritableAttributesStructure (schemaInfo = {}) {
|
|
347
348
|
const schemaStructure = schemaInfo.schemaStructure || {}
|
|
348
|
-
const schemaRelationships = schemaInfo.schemaRelationships || {}
|
|
349
349
|
const idProperty = schemaInfo.idProperty || 'id'
|
|
350
350
|
const attributesStructure = {}
|
|
351
|
-
const excludedFieldNames =
|
|
352
|
-
|
|
353
|
-
for (const relDef of Object.values(schemaRelationships)) {
|
|
354
|
-
if (!relDef?.belongsToPolymorphic) continue
|
|
355
|
-
|
|
356
|
-
const { typeField, idField } = relDef.belongsToPolymorphic
|
|
357
|
-
if (typeField) excludedFieldNames.add(typeField)
|
|
358
|
-
if (idField) excludedFieldNames.add(idField)
|
|
359
|
-
}
|
|
351
|
+
const excludedFieldNames = schemaInfo.foreignKeyFields
|
|
360
352
|
|
|
361
353
|
for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
|
|
362
354
|
if (!fieldDef) continue
|
|
@@ -364,7 +356,6 @@ function buildWritableAttributesStructure (schemaInfo = {}) {
|
|
|
364
356
|
if (excludedFieldNames.has(fieldName)) continue
|
|
365
357
|
if (fieldDef.computed === true) continue
|
|
366
358
|
if (fieldDef.type === undefined) continue
|
|
367
|
-
if (fieldDef.belongsTo && fieldDef.as) continue
|
|
368
359
|
attributesStructure[fieldName] = { ...fieldDef }
|
|
369
360
|
}
|
|
370
361
|
|
|
@@ -386,14 +377,14 @@ function resolveRelationshipAllowedTypes (relName, relDef) {
|
|
|
386
377
|
return relDef.belongsToPolymorphic.types
|
|
387
378
|
}
|
|
388
379
|
|
|
389
|
-
if (relDef?.target) {
|
|
390
|
-
return [relDef.target]
|
|
391
|
-
}
|
|
392
|
-
|
|
393
380
|
if (relDef?.belongsTo) {
|
|
394
381
|
return [relDef.belongsTo]
|
|
395
382
|
}
|
|
396
383
|
|
|
384
|
+
if (relDef?.target) {
|
|
385
|
+
return [relDef.target]
|
|
386
|
+
}
|
|
387
|
+
|
|
397
388
|
if (relDef?.type === 'manyToMany' || relDef?.type === 'hasMany' || relDef?.type === 'hasOne') {
|
|
398
389
|
return [relDef.target || relName]
|
|
399
390
|
}
|
|
@@ -402,28 +393,9 @@ function resolveRelationshipAllowedTypes (relName, relDef) {
|
|
|
402
393
|
}
|
|
403
394
|
|
|
404
395
|
function buildRelationshipStructure (schemaInfo = {}) {
|
|
405
|
-
const schemaStructure = schemaInfo.schemaStructure || {}
|
|
406
|
-
const schemaRelationships = schemaInfo.schemaRelationships || {}
|
|
407
396
|
const relationshipStructure = {}
|
|
408
397
|
|
|
409
|
-
for (const [,
|
|
410
|
-
if (!fieldDef?.belongsTo || !fieldDef?.as) continue
|
|
411
|
-
|
|
412
|
-
relationshipStructure[fieldDef.as] = {
|
|
413
|
-
type: 'object',
|
|
414
|
-
schema: createSchema({
|
|
415
|
-
data: {
|
|
416
|
-
type: 'jsonApiRelationshipData',
|
|
417
|
-
required: true,
|
|
418
|
-
nullable: true,
|
|
419
|
-
cardinality: 'one',
|
|
420
|
-
allowedTypes: [fieldDef.belongsTo]
|
|
421
|
-
}
|
|
422
|
-
})
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
for (const [relName, relDef] of Object.entries(schemaRelationships)) {
|
|
398
|
+
for (const [relName, relDef] of Object.entries(schemaInfo.outputRelationships || {})) {
|
|
427
399
|
const cardinality = getRelationshipCardinality(relDef)
|
|
428
400
|
relationshipStructure[relName] = {
|
|
429
401
|
type: 'object',
|
|
@@ -483,7 +455,10 @@ function buildWriteDocumentContract (scopeName, schemaInfo, mode) {
|
|
|
483
455
|
}
|
|
484
456
|
|
|
485
457
|
const schema = createSchema({
|
|
486
|
-
data: dataFieldDefinition
|
|
458
|
+
data: dataFieldDefinition,
|
|
459
|
+
meta: { type: 'object', additionalProperties: true },
|
|
460
|
+
links: { type: 'object', additionalProperties: true },
|
|
461
|
+
jsonapi: { type: 'object', additionalProperties: true }
|
|
487
462
|
})
|
|
488
463
|
|
|
489
464
|
return {
|
|
@@ -593,17 +568,19 @@ function buildRequestContracts ({ scopeName, schemaInfo, includeDepthLimit, sort
|
|
|
593
568
|
}
|
|
594
569
|
}
|
|
595
570
|
|
|
596
|
-
function buildContractCacheKey ({ includeDepthLimit, sortableFields }) {
|
|
571
|
+
function buildContractCacheKey ({ scopeName, includeDepthLimit, sortableFields }) {
|
|
597
572
|
return JSON.stringify({
|
|
573
|
+
scopeName,
|
|
598
574
|
includeDepthLimit,
|
|
599
575
|
sortableFields: Array.isArray(sortableFields) ? [...sortableFields].sort() : []
|
|
600
576
|
})
|
|
601
577
|
}
|
|
602
578
|
|
|
579
|
+
/** @param {{ scopeName: string, schemaInfo: object, includeDepthLimit?: number, sortableFields?: string[] }} options */
|
|
603
580
|
export function getRequestContracts ({ scopeName, schemaInfo, includeDepthLimit = 3, sortableFields = [] }) {
|
|
604
581
|
installRequestContractSupport()
|
|
605
582
|
|
|
606
|
-
const cacheKey = buildContractCacheKey({ includeDepthLimit, sortableFields })
|
|
583
|
+
const cacheKey = buildContractCacheKey({ scopeName, includeDepthLimit, sortableFields })
|
|
607
584
|
if (schemaInfo.requestContracts && schemaInfo.requestContractsCacheKey === cacheKey) {
|
|
608
585
|
return schemaInfo.requestContracts
|
|
609
586
|
}
|
|
@@ -629,6 +606,10 @@ export function validateRequestContractOrThrow (contract, payload, message = 'Re
|
|
|
629
606
|
throw new Error('Invalid request contract.')
|
|
630
607
|
}
|
|
631
608
|
|
|
609
|
+
if (!isPlainObject(payload)) {
|
|
610
|
+
throw new RestApiValidationError('document must be an object', { fields: ['document'] })
|
|
611
|
+
}
|
|
612
|
+
|
|
632
613
|
const { validatedObject, errors } = schema[mode](payload)
|
|
633
614
|
if (Object.keys(errors).length === 0) {
|
|
634
615
|
return validatedObject
|
|
@@ -647,3 +628,77 @@ export function validateRequestContractOrThrow (contract, payload, message = 'Re
|
|
|
647
628
|
violations
|
|
648
629
|
})
|
|
649
630
|
}
|
|
631
|
+
|
|
632
|
+
/** Select input by argument name, independently of the response format. */
|
|
633
|
+
export function selectWriteInput (params) {
|
|
634
|
+
const hasData = Object.hasOwn(params, 'data')
|
|
635
|
+
const hasDocument = Object.hasOwn(params, 'document')
|
|
636
|
+
if (hasData === hasDocument) {
|
|
637
|
+
throw new RestApiValidationError('Supply exactly one of data or document', { fields: ['data', 'document'] })
|
|
638
|
+
}
|
|
639
|
+
if (hasDocument) {
|
|
640
|
+
if (!isPlainObject(params.document)) {
|
|
641
|
+
throw new RestApiValidationError('document must be an object', { fields: ['document'] })
|
|
642
|
+
}
|
|
643
|
+
const unsupported = ['errors', 'included'].filter(key => Object.hasOwn(params.document, key))
|
|
644
|
+
if (unsupported.length) {
|
|
645
|
+
throw new RestApiValidationError('Write documents do not support errors or included resources', {
|
|
646
|
+
fields: unsupported.map(key => `document.${key}`)
|
|
647
|
+
})
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return hasDocument ? 'document' : 'data'
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/** Validate PUT/PATCH identity and body, then store the normalized document and ID on context. */
|
|
654
|
+
export function validateUpdateRequest ({ method, params, context, vars, scopeOptions, scopeName }) {
|
|
655
|
+
const requestContracts = getRequestContracts({
|
|
656
|
+
scopeName,
|
|
657
|
+
schemaInfo: context.schemaInfo,
|
|
658
|
+
includeDepthLimit: vars.includeDepthLimit,
|
|
659
|
+
sortableFields: vars.sortableFields
|
|
660
|
+
})
|
|
661
|
+
const normalizedPathId = params.id === undefined
|
|
662
|
+
? null
|
|
663
|
+
: requireExistingResourceId(params.id, {
|
|
664
|
+
scopeOptions,
|
|
665
|
+
vars,
|
|
666
|
+
scopeName
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
if (normalizedPathId && context.inputRecord?.data?.id === undefined) {
|
|
670
|
+
context.inputRecord = {
|
|
671
|
+
...context.inputRecord,
|
|
672
|
+
data: {
|
|
673
|
+
...(context.inputRecord?.data || {}),
|
|
674
|
+
id: normalizedPathId
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
context.inputRecord = validateRequestContractOrThrow(
|
|
680
|
+
requestContracts[method],
|
|
681
|
+
context.inputRecord,
|
|
682
|
+
`${method.toUpperCase()} request body is invalid`
|
|
683
|
+
)
|
|
684
|
+
const normalizedBodyId = requireDocumentResourceId(context.inputRecord.data.id, {
|
|
685
|
+
scopeOptions,
|
|
686
|
+
vars
|
|
687
|
+
})
|
|
688
|
+
|
|
689
|
+
if (normalizedPathId && normalizedPathId !== normalizedBodyId) {
|
|
690
|
+
throw new RestApiValidationError(
|
|
691
|
+
`ID mismatch. URL path ID '${normalizedPathId}' does not match request body ID '${normalizedBodyId}'`,
|
|
692
|
+
{
|
|
693
|
+
fields: ['data.id'],
|
|
694
|
+
violations: [{
|
|
695
|
+
field: 'data.id',
|
|
696
|
+
rule: 'id_consistency',
|
|
697
|
+
message: 'Request body ID must match URL path ID when both are provided'
|
|
698
|
+
}]
|
|
699
|
+
}
|
|
700
|
+
)
|
|
701
|
+
}
|
|
702
|
+
context.inputRecord.data.id = normalizedBodyId
|
|
703
|
+
context.id = normalizedPathId || normalizedBodyId
|
|
704
|
+
}
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** @import { RestApiPluginOptions } from '../../../../types/plugin-options.js' */
|
|
4
|
+
/** @typedef {Pick<RestApiPluginOptions, 'normalizeId'>} NormalizerConfig */
|
|
5
|
+
/** @typedef {{ scopeOptions?: NormalizerConfig | null, vars?: NormalizerConfig | null, scopeName?: string }} NormalizationOptions */
|
|
6
|
+
/** @typedef {{ resources?: Record<string, { scopeOptions?: NormalizerConfig | null, _scopeOptions?: NormalizerConfig | null, vars?: NormalizerConfig | null }> }} NormalizationApi */
|
|
7
|
+
/** @typedef {{ api?: NormalizationApi }} ReferenceOptions */
|
|
8
|
+
|
|
1
9
|
import {
|
|
2
10
|
RestApiResourceError,
|
|
3
11
|
RestApiValidationError
|
|
4
12
|
} from '../../../../lib/rest-api-errors.js'
|
|
5
13
|
|
|
14
|
+
/** @param {unknown} value @returns {string | null} */
|
|
6
15
|
export function defaultNormalizeResourceId (value) {
|
|
7
16
|
if (value === null || value === undefined) {
|
|
8
17
|
return null
|
|
@@ -21,10 +30,10 @@ export function defaultNormalizeResourceId (value) {
|
|
|
21
30
|
return String(value)
|
|
22
31
|
}
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
return normalized || null
|
|
33
|
+
return null
|
|
26
34
|
}
|
|
27
35
|
|
|
36
|
+
/** @param {NormalizationOptions} [options] @returns {NonNullable<RestApiPluginOptions["normalizeId"]>} */
|
|
28
37
|
export function resolveResourceIdNormalizer ({ scopeOptions = null, vars = null } = {}) {
|
|
29
38
|
if (typeof scopeOptions?.normalizeId === 'function') {
|
|
30
39
|
return scopeOptions.normalizeId
|
|
@@ -37,11 +46,13 @@ export function resolveResourceIdNormalizer ({ scopeOptions = null, vars = null
|
|
|
37
46
|
return defaultNormalizeResourceId
|
|
38
47
|
}
|
|
39
48
|
|
|
49
|
+
/** @param {unknown} value @param {NormalizationOptions} [options] @returns {string | null} */
|
|
40
50
|
export function normalizeResourceId (value, options = {}) {
|
|
41
51
|
const normalizer = resolveResourceIdNormalizer(options)
|
|
42
52
|
return defaultNormalizeResourceId(normalizer(value))
|
|
43
53
|
}
|
|
44
54
|
|
|
55
|
+
/** @param {unknown} value @param {NormalizationOptions} [options] @returns {string} */
|
|
45
56
|
export function requireExistingResourceId (value, {
|
|
46
57
|
scopeOptions = null,
|
|
47
58
|
vars = null,
|
|
@@ -57,11 +68,12 @@ export function requireExistingResourceId (value, {
|
|
|
57
68
|
{
|
|
58
69
|
subtype: 'not_found',
|
|
59
70
|
resourceType: scopeName,
|
|
60
|
-
resourceId: value
|
|
71
|
+
resourceId: typeof value === 'string' ? value : defaultNormalizeResourceId(value)
|
|
61
72
|
}
|
|
62
73
|
)
|
|
63
74
|
}
|
|
64
75
|
|
|
76
|
+
/** @param {unknown} value @param {NormalizationOptions} [options] @returns {string} */
|
|
65
77
|
export function requireDocumentResourceId (value, {
|
|
66
78
|
scopeOptions = null,
|
|
67
79
|
vars = null
|
|
@@ -84,6 +96,7 @@ export function requireDocumentResourceId (value, {
|
|
|
84
96
|
)
|
|
85
97
|
}
|
|
86
98
|
|
|
99
|
+
/** @param {string} resourceType @param {ReferenceOptions} [options] @returns {NormalizationOptions} */
|
|
87
100
|
function resolveResourceNormalizationOptions (resourceType, { api } = {}) {
|
|
88
101
|
const scope = api?.resources?.[resourceType]
|
|
89
102
|
return {
|
|
@@ -93,6 +106,7 @@ function resolveResourceNormalizationOptions (resourceType, { api } = {}) {
|
|
|
93
106
|
}
|
|
94
107
|
}
|
|
95
108
|
|
|
109
|
+
/** @param {string} resourceType @param {unknown} value @param {ReferenceOptions} [options] @returns {string} */
|
|
96
110
|
export function requireReferencedResourceId (resourceType, value, { api } = {}) {
|
|
97
111
|
return requireExistingResourceId(
|
|
98
112
|
value,
|
|
@@ -100,12 +114,15 @@ export function requireReferencedResourceId (resourceType, value, { api } = {})
|
|
|
100
114
|
)
|
|
101
115
|
}
|
|
102
116
|
|
|
117
|
+
/** @param {unknown} identifier @param {ReferenceOptions} [options] @returns {unknown} */
|
|
103
118
|
function normalizeRelationshipIdentifier (identifier, { api } = {}) {
|
|
104
119
|
if (!identifier || typeof identifier !== 'object' || Array.isArray(identifier)) {
|
|
105
120
|
return identifier
|
|
106
121
|
}
|
|
107
122
|
|
|
108
|
-
|
|
123
|
+
// Object shape is known here; its property values remain unvalidated.
|
|
124
|
+
const record = /** @type {Record<string, unknown>} */ (identifier)
|
|
125
|
+
const type = typeof record.type === 'string' ? record.type : ''
|
|
109
126
|
if (!type || !Object.hasOwn(identifier, 'id')) {
|
|
110
127
|
return {
|
|
111
128
|
...identifier
|
|
@@ -114,10 +131,11 @@ function normalizeRelationshipIdentifier (identifier, { api } = {}) {
|
|
|
114
131
|
|
|
115
132
|
return {
|
|
116
133
|
...identifier,
|
|
117
|
-
id: requireReferencedResourceId(type,
|
|
134
|
+
id: requireReferencedResourceId(type, record.id, { api })
|
|
118
135
|
}
|
|
119
136
|
}
|
|
120
137
|
|
|
138
|
+
/** @param {unknown} relationshipData @param {ReferenceOptions} [options] @returns {unknown} */
|
|
121
139
|
export function normalizeRelationshipIdentifiers (relationshipData, { api } = {}) {
|
|
122
140
|
if (Array.isArray(relationshipData)) {
|
|
123
141
|
return relationshipData.map((identifier) => normalizeRelationshipIdentifier(identifier, { api }))
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { RestApiValidationError } from '../../../../lib/rest-api-errors.js'
|
|
3
|
+
|
|
4
|
+
/** @import { ResourceFormat, WriteReturning } from '../../../../types/representations.js' */
|
|
5
|
+
|
|
6
|
+
/** @param {unknown} [value] @param {unknown} [fallback] @returns {ResourceFormat} */
|
|
7
|
+
export function resolveFormat (value, fallback = 'plain') {
|
|
8
|
+
const format = value === undefined ? fallback : value
|
|
9
|
+
if (format !== 'plain' && format !== 'jsonapi') {
|
|
10
|
+
throw new RestApiValidationError("format must be 'plain' or 'jsonapi'", { fields: ['format'] })
|
|
11
|
+
}
|
|
12
|
+
return format
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** @param {unknown} [value] @param {unknown} [fallback] @returns {WriteReturning} */
|
|
16
|
+
export function resolveReturning (value, fallback = 'full') {
|
|
17
|
+
const returning = value === undefined ? fallback : value
|
|
18
|
+
if (returning !== 'none' && returning !== 'minimal' && returning !== 'full') {
|
|
19
|
+
throw new RestApiValidationError("returning must be 'none', 'minimal', or 'full'", { fields: ['returning'] })
|
|
20
|
+
}
|
|
21
|
+
return returning
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** @param {object} options @returns {void} */
|
|
25
|
+
export function rejectRemovedOptions (options) {
|
|
26
|
+
const removedInput = ['inputRecord', 'inputRecords'].filter(name => Object.hasOwn(options, name))
|
|
27
|
+
if (removedInput.length) {
|
|
28
|
+
throw new RestApiValidationError(`Removed input options: ${removedInput.join(', ')}. Use data or document.`, { fields: removedInput })
|
|
29
|
+
}
|
|
30
|
+
const fields = ['simplified', 'simplifiedApi', 'simplifiedTransport', 'returnFullRecord', 'returnRecordApi', 'returnRecordTransport', 'isTransport']
|
|
31
|
+
.filter(name => Object.hasOwn(options, name))
|
|
32
|
+
if (fields.length) {
|
|
33
|
+
throw new RestApiValidationError(`Removed options: ${fields.join(', ')}. Use format and returning.`, { fields })
|
|
34
|
+
}
|
|
35
|
+
}
|