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
|
@@ -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) {
|
|
@@ -76,12 +77,12 @@ function buildFieldMapJsonSchemaFragment () {
|
|
|
76
77
|
|
|
77
78
|
function buildKnownPageSchema () {
|
|
78
79
|
return createSchema({
|
|
79
|
-
number: { type: '
|
|
80
|
-
size: { type: '
|
|
81
|
-
limit: { type: '
|
|
82
|
-
offset: { type: '
|
|
83
|
-
after: { type: 'string' },
|
|
84
|
-
before: { type: 'string' },
|
|
80
|
+
number: { type: 'integer', min: 1 },
|
|
81
|
+
size: { type: 'integer', min: 1 },
|
|
82
|
+
limit: { type: 'integer', min: 1 },
|
|
83
|
+
offset: { type: 'integer', min: 0 },
|
|
84
|
+
after: { type: 'string', min: 1 },
|
|
85
|
+
before: { type: 'string', min: 1 },
|
|
85
86
|
cursor: { type: 'string' }
|
|
86
87
|
})
|
|
87
88
|
}
|
|
@@ -313,6 +314,16 @@ function installRequestContractSupport () {
|
|
|
313
314
|
const firstError = pickFirstError(errors)
|
|
314
315
|
context.throwParamError(firstError?.code || 'INVALID_PAGE', firstError?.message || 'Invalid page parameters.')
|
|
315
316
|
}
|
|
317
|
+
for (const direction of ['after', 'before']) {
|
|
318
|
+
if (Object.hasOwn(knownPageParams, direction) && !validatedObject[direction]?.trim()) {
|
|
319
|
+
context.throwParamError('INVALID_PAGE', `page[${direction}] must contain a cursor.`)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if ((validatedObject.after !== undefined && validatedObject.before !== undefined) ||
|
|
324
|
+
(validatedObject.number !== undefined && (validatedObject.after !== undefined || validatedObject.before !== undefined))) {
|
|
325
|
+
context.throwParamError('INVALID_PAGE', 'Use either page[number], page[after], or page[before].')
|
|
326
|
+
}
|
|
316
327
|
|
|
317
328
|
return {
|
|
318
329
|
...context.value,
|
|
@@ -335,18 +346,9 @@ function installRequestContractSupport () {
|
|
|
335
346
|
|
|
336
347
|
function buildWritableAttributesStructure (schemaInfo = {}) {
|
|
337
348
|
const schemaStructure = schemaInfo.schemaStructure || {}
|
|
338
|
-
const schemaRelationships = schemaInfo.schemaRelationships || {}
|
|
339
349
|
const idProperty = schemaInfo.idProperty || 'id'
|
|
340
350
|
const attributesStructure = {}
|
|
341
|
-
const excludedFieldNames =
|
|
342
|
-
|
|
343
|
-
for (const relDef of Object.values(schemaRelationships)) {
|
|
344
|
-
if (!relDef?.belongsToPolymorphic) continue
|
|
345
|
-
|
|
346
|
-
const { typeField, idField } = relDef.belongsToPolymorphic
|
|
347
|
-
if (typeField) excludedFieldNames.add(typeField)
|
|
348
|
-
if (idField) excludedFieldNames.add(idField)
|
|
349
|
-
}
|
|
351
|
+
const excludedFieldNames = schemaInfo.foreignKeyFields
|
|
350
352
|
|
|
351
353
|
for (const [fieldName, fieldDef] of Object.entries(schemaStructure)) {
|
|
352
354
|
if (!fieldDef) continue
|
|
@@ -354,7 +356,6 @@ function buildWritableAttributesStructure (schemaInfo = {}) {
|
|
|
354
356
|
if (excludedFieldNames.has(fieldName)) continue
|
|
355
357
|
if (fieldDef.computed === true) continue
|
|
356
358
|
if (fieldDef.type === undefined) continue
|
|
357
|
-
if (fieldDef.belongsTo && fieldDef.as) continue
|
|
358
359
|
attributesStructure[fieldName] = { ...fieldDef }
|
|
359
360
|
}
|
|
360
361
|
|
|
@@ -376,14 +377,14 @@ function resolveRelationshipAllowedTypes (relName, relDef) {
|
|
|
376
377
|
return relDef.belongsToPolymorphic.types
|
|
377
378
|
}
|
|
378
379
|
|
|
379
|
-
if (relDef?.target) {
|
|
380
|
-
return [relDef.target]
|
|
381
|
-
}
|
|
382
|
-
|
|
383
380
|
if (relDef?.belongsTo) {
|
|
384
381
|
return [relDef.belongsTo]
|
|
385
382
|
}
|
|
386
383
|
|
|
384
|
+
if (relDef?.target) {
|
|
385
|
+
return [relDef.target]
|
|
386
|
+
}
|
|
387
|
+
|
|
387
388
|
if (relDef?.type === 'manyToMany' || relDef?.type === 'hasMany' || relDef?.type === 'hasOne') {
|
|
388
389
|
return [relDef.target || relName]
|
|
389
390
|
}
|
|
@@ -392,28 +393,9 @@ function resolveRelationshipAllowedTypes (relName, relDef) {
|
|
|
392
393
|
}
|
|
393
394
|
|
|
394
395
|
function buildRelationshipStructure (schemaInfo = {}) {
|
|
395
|
-
const schemaStructure = schemaInfo.schemaStructure || {}
|
|
396
|
-
const schemaRelationships = schemaInfo.schemaRelationships || {}
|
|
397
396
|
const relationshipStructure = {}
|
|
398
397
|
|
|
399
|
-
for (const [,
|
|
400
|
-
if (!fieldDef?.belongsTo || !fieldDef?.as) continue
|
|
401
|
-
|
|
402
|
-
relationshipStructure[fieldDef.as] = {
|
|
403
|
-
type: 'object',
|
|
404
|
-
schema: createSchema({
|
|
405
|
-
data: {
|
|
406
|
-
type: 'jsonApiRelationshipData',
|
|
407
|
-
required: true,
|
|
408
|
-
nullable: true,
|
|
409
|
-
cardinality: 'one',
|
|
410
|
-
allowedTypes: [fieldDef.belongsTo]
|
|
411
|
-
}
|
|
412
|
-
})
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
for (const [relName, relDef] of Object.entries(schemaRelationships)) {
|
|
398
|
+
for (const [relName, relDef] of Object.entries(schemaInfo.outputRelationships || {})) {
|
|
417
399
|
const cardinality = getRelationshipCardinality(relDef)
|
|
418
400
|
relationshipStructure[relName] = {
|
|
419
401
|
type: 'object',
|
|
@@ -473,7 +455,10 @@ function buildWriteDocumentContract (scopeName, schemaInfo, mode) {
|
|
|
473
455
|
}
|
|
474
456
|
|
|
475
457
|
const schema = createSchema({
|
|
476
|
-
data: dataFieldDefinition
|
|
458
|
+
data: dataFieldDefinition,
|
|
459
|
+
meta: { type: 'object', additionalProperties: true },
|
|
460
|
+
links: { type: 'object', additionalProperties: true },
|
|
461
|
+
jsonapi: { type: 'object', additionalProperties: true }
|
|
477
462
|
})
|
|
478
463
|
|
|
479
464
|
return {
|
|
@@ -583,17 +568,19 @@ function buildRequestContracts ({ scopeName, schemaInfo, includeDepthLimit, sort
|
|
|
583
568
|
}
|
|
584
569
|
}
|
|
585
570
|
|
|
586
|
-
function buildContractCacheKey ({ includeDepthLimit, sortableFields }) {
|
|
571
|
+
function buildContractCacheKey ({ scopeName, includeDepthLimit, sortableFields }) {
|
|
587
572
|
return JSON.stringify({
|
|
573
|
+
scopeName,
|
|
588
574
|
includeDepthLimit,
|
|
589
575
|
sortableFields: Array.isArray(sortableFields) ? [...sortableFields].sort() : []
|
|
590
576
|
})
|
|
591
577
|
}
|
|
592
578
|
|
|
579
|
+
/** @param {{ scopeName: string, schemaInfo: object, includeDepthLimit?: number, sortableFields?: string[] }} options */
|
|
593
580
|
export function getRequestContracts ({ scopeName, schemaInfo, includeDepthLimit = 3, sortableFields = [] }) {
|
|
594
581
|
installRequestContractSupport()
|
|
595
582
|
|
|
596
|
-
const cacheKey = buildContractCacheKey({ includeDepthLimit, sortableFields })
|
|
583
|
+
const cacheKey = buildContractCacheKey({ scopeName, includeDepthLimit, sortableFields })
|
|
597
584
|
if (schemaInfo.requestContracts && schemaInfo.requestContractsCacheKey === cacheKey) {
|
|
598
585
|
return schemaInfo.requestContracts
|
|
599
586
|
}
|
|
@@ -619,6 +606,10 @@ export function validateRequestContractOrThrow (contract, payload, message = 'Re
|
|
|
619
606
|
throw new Error('Invalid request contract.')
|
|
620
607
|
}
|
|
621
608
|
|
|
609
|
+
if (!isPlainObject(payload)) {
|
|
610
|
+
throw new RestApiValidationError('document must be an object', { fields: ['document'] })
|
|
611
|
+
}
|
|
612
|
+
|
|
622
613
|
const { validatedObject, errors } = schema[mode](payload)
|
|
623
614
|
if (Object.keys(errors).length === 0) {
|
|
624
615
|
return validatedObject
|
|
@@ -637,3 +628,77 @@ export function validateRequestContractOrThrow (contract, payload, message = 'Re
|
|
|
637
628
|
violations
|
|
638
629
|
})
|
|
639
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
|
+
}
|