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,4 +1,5 @@
|
|
|
1
1
|
import { RestApiValidationError } from '../../lib/rest-api-errors.js'
|
|
2
|
+
import { snapshotResourceConfiguration } from './lib/querying-writing/schema-helpers.js'
|
|
2
3
|
import { createStorageAdapterUtilities } from './lib/querying/storage-adapter-utils.js'
|
|
3
4
|
|
|
4
5
|
const PUBLIC_PRESET_NAME = 'public'
|
|
@@ -37,7 +38,7 @@ function buildAutofilterConsistencyError ({ filter, resolvedValue }) {
|
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
function ensureResolverMap (resolverMap = {}) {
|
|
40
|
-
const normalized =
|
|
41
|
+
const normalized = Object.create(null)
|
|
41
42
|
|
|
42
43
|
for (const [name, resolver] of Object.entries(resolverMap)) {
|
|
43
44
|
if (typeof resolver !== 'function') {
|
|
@@ -50,7 +51,7 @@ function ensureResolverMap (resolverMap = {}) {
|
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
function getPresetFilters (presetName, presets) {
|
|
53
|
-
const preset = presets[presetName]
|
|
54
|
+
const preset = Object.hasOwn(presets, presetName) ? presets[presetName] : undefined
|
|
54
55
|
|
|
55
56
|
if (!preset) {
|
|
56
57
|
throw new Error(`Unknown autofilter preset '${presetName}'.`)
|
|
@@ -77,7 +78,7 @@ function normalizeFilterDefinition ({ filterDef, resolvers, schemaStructure, sco
|
|
|
77
78
|
throw new Error(`Autofilter definitions on resource '${scopeName}' must include a field.`)
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
const fieldDef = schemaStructure[field]
|
|
81
|
+
const fieldDef = Object.hasOwn(schemaStructure, field) ? schemaStructure[field] : undefined
|
|
81
82
|
if (!fieldDef) {
|
|
82
83
|
throw new Error(`Autofilter field '${field}' does not exist on resource '${scopeName}'.`)
|
|
83
84
|
}
|
|
@@ -91,7 +92,7 @@ function normalizeFilterDefinition ({ filterDef, resolvers, schemaStructure, sco
|
|
|
91
92
|
let resolverName
|
|
92
93
|
|
|
93
94
|
if (typeof resolverDef === 'string') {
|
|
94
|
-
resolve = resolvers[resolverDef]
|
|
95
|
+
resolve = Object.hasOwn(resolvers, resolverDef) ? resolvers[resolverDef] : undefined
|
|
95
96
|
resolverName = resolverDef
|
|
96
97
|
if (!resolve) {
|
|
97
98
|
throw new Error(`Unknown autofilter resolver '${resolverDef}' on resource '${scopeName}'.`)
|
|
@@ -274,7 +275,7 @@ async function enforceScopedInput ({ compiledConfig, context, scopeName, injectM
|
|
|
274
275
|
|
|
275
276
|
export const AutoFilterPlugin = {
|
|
276
277
|
name: 'autofilter',
|
|
277
|
-
dependencies: ['rest-api', 'rest-api-knex
|
|
278
|
+
dependencies: ['rest-api', ['rest-api-knex', 'rest-api-anyapi-knex']],
|
|
278
279
|
|
|
279
280
|
install ({ api, addHook, vars, helpers, log, scopes, pluginOptions = {} }) {
|
|
280
281
|
if (!api.knex?.instance) {
|
|
@@ -283,10 +284,10 @@ export const AutoFilterPlugin = {
|
|
|
283
284
|
|
|
284
285
|
const state = {
|
|
285
286
|
resolvers: ensureResolverMap(pluginOptions.resolvers || {}),
|
|
286
|
-
presets: {
|
|
287
|
+
presets: snapshotResourceConfiguration({
|
|
287
288
|
[PUBLIC_PRESET_NAME]: { filters: [] },
|
|
288
289
|
...(pluginOptions.presets || {})
|
|
289
|
-
}
|
|
290
|
+
})
|
|
290
291
|
}
|
|
291
292
|
|
|
292
293
|
vars.autofilter = {
|
|
@@ -294,15 +295,12 @@ export const AutoFilterPlugin = {
|
|
|
294
295
|
resolvers: Object.keys(state.resolvers)
|
|
295
296
|
}
|
|
296
297
|
|
|
297
|
-
addHook('
|
|
298
|
-
const { scopeName, scopeOptions
|
|
299
|
-
|
|
300
|
-
const schemaStructure = scope?.vars?.schemaInfo?.schemaStructure || scopeOptions.schema || {}
|
|
301
|
-
|
|
302
|
-
scope.vars.autofilter = compileAutoFilterDefinition({
|
|
298
|
+
addHook('schema:compiled', 'compile-autofilter', {}, ({ context }) => {
|
|
299
|
+
const { scopeName, scopeOptions, schemaInfo } = context
|
|
300
|
+
schemaInfo.autofilter = compileAutoFilterDefinition({
|
|
303
301
|
scopeName,
|
|
304
302
|
scopeOptions,
|
|
305
|
-
schemaStructure,
|
|
303
|
+
schemaStructure: schemaInfo.schemaStructure,
|
|
306
304
|
resolvers: state.resolvers,
|
|
307
305
|
presets: state.presets
|
|
308
306
|
})
|
|
@@ -312,7 +310,7 @@ export const AutoFilterPlugin = {
|
|
|
312
310
|
const { query, tableName, scopeName } = context.knexQuery || {}
|
|
313
311
|
if (!query || !scopeName) return
|
|
314
312
|
|
|
315
|
-
const compiledConfig = scopes[scopeName]?.vars?.autofilter
|
|
313
|
+
const compiledConfig = scopes[scopeName]?.vars?.schemaInfo?.autofilter
|
|
316
314
|
if (!compiledConfig || compiledConfig.filters.length === 0) return
|
|
317
315
|
const adapterUtils = createStorageAdapterUtilities({ context }, {
|
|
318
316
|
getStorageAdapter: helpers.getStorageAdapter
|
|
@@ -345,7 +343,7 @@ export const AutoFilterPlugin = {
|
|
|
345
343
|
|
|
346
344
|
addHook('beforeProcessingPost', 'autofilter-stamp-post', {}, async ({ context, scopeName }) => {
|
|
347
345
|
await enforceScopedInput({
|
|
348
|
-
compiledConfig: scopes[scopeName]?.vars?.autofilter,
|
|
346
|
+
compiledConfig: scopes[scopeName]?.vars?.schemaInfo?.autofilter,
|
|
349
347
|
context,
|
|
350
348
|
scopeName,
|
|
351
349
|
injectMissing: true,
|
|
@@ -359,7 +357,7 @@ export const AutoFilterPlugin = {
|
|
|
359
357
|
|
|
360
358
|
addHook('beforeProcessingPut', 'autofilter-stamp-put', {}, async ({ context, scopeName }) => {
|
|
361
359
|
await enforceScopedInput({
|
|
362
|
-
compiledConfig: scopes[scopeName]?.vars?.autofilter,
|
|
360
|
+
compiledConfig: scopes[scopeName]?.vars?.schemaInfo?.autofilter,
|
|
363
361
|
context,
|
|
364
362
|
scopeName,
|
|
365
363
|
injectMissing: true,
|
|
@@ -373,7 +371,7 @@ export const AutoFilterPlugin = {
|
|
|
373
371
|
|
|
374
372
|
addHook('beforeProcessingPatch', 'autofilter-validate-patch', {}, async ({ context, scopeName }) => {
|
|
375
373
|
await enforceScopedInput({
|
|
376
|
-
compiledConfig: scopes[scopeName]?.vars?.autofilter,
|
|
374
|
+
compiledConfig: scopes[scopeName]?.vars?.schemaInfo?.autofilter,
|
|
377
375
|
context,
|
|
378
376
|
scopeName,
|
|
379
377
|
injectMissing: false,
|
|
@@ -392,7 +390,7 @@ export const AutoFilterPlugin = {
|
|
|
392
390
|
}),
|
|
393
391
|
|
|
394
392
|
getScopeConfig: (scopeName) => {
|
|
395
|
-
const compiledConfig = scopes[scopeName]?.vars?.autofilter
|
|
393
|
+
const compiledConfig = scopes[scopeName]?.vars?.schemaInfo?.autofilter
|
|
396
394
|
if (!compiledConfig) return null
|
|
397
395
|
|
|
398
396
|
return {
|
|
@@ -1,237 +1,84 @@
|
|
|
1
|
+
import { mergeResponseHeaders } from './connectors/lib/transport-http-helpers.js'
|
|
2
|
+
|
|
1
3
|
export const CorsPlugin = {
|
|
2
4
|
name: 'rest-api-cors',
|
|
3
5
|
dependencies: ['rest-api'],
|
|
4
6
|
|
|
5
|
-
async install ({ api, addHook, vars,
|
|
6
|
-
|
|
7
|
-
const corsOptions = pluginOptions || {}
|
|
7
|
+
async install ({ api, addHook, vars, log, pluginOptions = {} }) {
|
|
8
|
+
if (!vars.transport) throw new Error('CorsPlugin requires an HTTP connector to be installed first')
|
|
8
9
|
|
|
9
|
-
// Store configuration - using a plain object for runtime updates
|
|
10
10
|
const corsConfig = {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
methods: corsOptions.methods || ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
17
|
-
|
|
18
|
-
// Allowed headers
|
|
19
|
-
allowedHeaders: corsOptions.allowedHeaders || [
|
|
20
|
-
'Content-Type',
|
|
21
|
-
'Authorization',
|
|
22
|
-
'X-Requested-With',
|
|
23
|
-
'X-HTTP-Method-Override',
|
|
24
|
-
'Accept',
|
|
25
|
-
'Origin'
|
|
26
|
-
],
|
|
27
|
-
|
|
28
|
-
// Exposed headers (for client to read)
|
|
29
|
-
exposedHeaders: corsOptions.exposedHeaders || [
|
|
30
|
-
'X-Total-Count',
|
|
31
|
-
'X-Page-Count',
|
|
32
|
-
'Link',
|
|
33
|
-
'Location'
|
|
11
|
+
origin: pluginOptions.origin ?? '*',
|
|
12
|
+
credentials: pluginOptions.credentials ?? true,
|
|
13
|
+
methods: pluginOptions.methods ?? ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
|
14
|
+
allowedHeaders: pluginOptions.allowedHeaders ?? [
|
|
15
|
+
'Content-Type', 'Authorization', 'X-Requested-With', 'X-HTTP-Method-Override', 'Accept', 'Origin'
|
|
34
16
|
],
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// Options success status
|
|
40
|
-
optionsSuccessStatus: corsOptions.optionsSuccessStatus || 204
|
|
17
|
+
exposedHeaders: pluginOptions.exposedHeaders ?? ['X-Total-Count', 'X-Page-Count', 'Link', 'Location'],
|
|
18
|
+
maxAge: pluginOptions.maxAge ?? 86400,
|
|
19
|
+
optionsSuccessStatus: pluginOptions.optionsSuccessStatus ?? 204
|
|
41
20
|
}
|
|
42
|
-
|
|
43
|
-
// Store reference in vars for access in hooks
|
|
44
21
|
vars.cors = corsConfig
|
|
22
|
+
const originDecision = Symbol('corsOriginDecision')
|
|
45
23
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
// String match
|
|
51
|
-
if (typeof allowedOrigin === 'string') {
|
|
52
|
-
return allowedOrigin === '*' || allowedOrigin === origin
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Regex match
|
|
56
|
-
if (allowedOrigin instanceof RegExp) {
|
|
57
|
-
return allowedOrigin.test(origin)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Array of allowed origins
|
|
24
|
+
async function isOriginAllowed (origin, allowedOrigin) {
|
|
25
|
+
if (!origin) return allowedOrigin === '*' && !corsConfig.credentials
|
|
26
|
+
if (typeof allowedOrigin === 'string') return allowedOrigin === '*' || allowedOrigin === origin
|
|
27
|
+
if (allowedOrigin instanceof RegExp) return new RegExp(allowedOrigin.source, allowedOrigin.flags).test(origin)
|
|
61
28
|
if (Array.isArray(allowedOrigin)) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// Function check
|
|
66
|
-
if (typeof allowedOrigin === 'function') {
|
|
67
|
-
return allowedOrigin(origin)
|
|
29
|
+
for (const candidate of allowedOrigin) if (await isOriginAllowed(origin, candidate)) return true
|
|
30
|
+
return false
|
|
68
31
|
}
|
|
69
|
-
|
|
32
|
+
if (typeof allowedOrigin === 'function') return !!(await allowedOrigin(origin))
|
|
70
33
|
return false
|
|
71
34
|
}
|
|
72
35
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
36
|
+
async function allowRequestOrigin (context) {
|
|
37
|
+
if (!(originDecision in context)) {
|
|
38
|
+
// If the predicate throws, the error response must not retry or grant access.
|
|
39
|
+
context[originDecision] = false
|
|
40
|
+
context[originDecision] = await isOriginAllowed(context.transport.request.headers?.origin, corsConfig.origin)
|
|
41
|
+
}
|
|
42
|
+
return context[originDecision]
|
|
77
43
|
}
|
|
78
44
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
'Access-Control-Allow-Methods': corsConfig.methods.join(', '),
|
|
89
|
-
'Access-Control-Allow-Headers': corsConfig.allowedHeaders.join(', '),
|
|
90
|
-
'Access-Control-Max-Age': String(corsConfig.maxAge)
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Set origin header
|
|
94
|
-
if (corsConfig.origin === '*' && !corsConfig.credentials) {
|
|
95
|
-
responseHeaders['Access-Control-Allow-Origin'] = '*'
|
|
96
|
-
} else if (origin) {
|
|
97
|
-
responseHeaders['Access-Control-Allow-Origin'] = origin
|
|
98
|
-
responseHeaders['Vary'] = 'Origin'
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Set credentials if enabled
|
|
102
|
-
if (corsConfig.credentials) {
|
|
103
|
-
responseHeaders['Access-Control-Allow-Credentials'] = 'true'
|
|
45
|
+
await api.addRoute({
|
|
46
|
+
method: 'OPTIONS',
|
|
47
|
+
path: vars.transport.matchAll,
|
|
48
|
+
handler: async ({ context }) => {
|
|
49
|
+
if (!await allowRequestOrigin(context)) {
|
|
50
|
+
return {
|
|
51
|
+
statusCode: 403,
|
|
52
|
+
body: { errors: [{ status: '403', title: 'Forbidden', detail: 'CORS origin not allowed' }] }
|
|
53
|
+
}
|
|
104
54
|
}
|
|
105
|
-
|
|
106
55
|
return {
|
|
107
56
|
statusCode: corsConfig.optionsSuccessStatus,
|
|
108
|
-
headers:
|
|
57
|
+
headers: {
|
|
58
|
+
'Access-Control-Allow-Methods': corsConfig.methods.join(', '),
|
|
59
|
+
'Access-Control-Allow-Headers': corsConfig.allowedHeaders.join(', '),
|
|
60
|
+
'Access-Control-Max-Age': String(corsConfig.maxAge)
|
|
61
|
+
},
|
|
109
62
|
body: null
|
|
110
63
|
}
|
|
111
|
-
} else {
|
|
112
|
-
// Origin not allowed
|
|
113
|
-
log.warn('CORS origin not allowed', { origin })
|
|
114
|
-
return {
|
|
115
|
-
statusCode: 403,
|
|
116
|
-
body: { error: 'CORS origin not allowed' }
|
|
117
|
-
}
|
|
118
64
|
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Register OPTIONS route for all paths
|
|
122
|
-
await api.addRoute({
|
|
123
|
-
method: 'OPTIONS',
|
|
124
|
-
path: vars.transport.matchAll,
|
|
125
|
-
handler: optionsHandler
|
|
126
65
|
})
|
|
127
66
|
|
|
128
|
-
|
|
129
|
-
addHook('transport:response', 'cors-headers', { order: -1000 }, async ({ context }) => {
|
|
130
|
-
// Transport data is now nested in context
|
|
67
|
+
addHook('transport:response', 'cors-headers', {}, async ({ context }) => {
|
|
131
68
|
const { request, response } = context.transport || {}
|
|
69
|
+
if (!request || !response) return
|
|
132
70
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const origin = request.headers?.origin
|
|
139
|
-
const method = request.method?.toUpperCase()
|
|
71
|
+
const wildcard = corsConfig.origin === '*' && !corsConfig.credentials
|
|
72
|
+
// Responses without a matching Origin must also have the correct cache key.
|
|
73
|
+
if (!wildcard) response.headers = mergeResponseHeaders(response.headers, { vary: 'Origin' })
|
|
74
|
+
if (!await allowRequestOrigin(context)) return
|
|
140
75
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
// Skip if this was an OPTIONS request (already handled by route)
|
|
148
|
-
if (method === 'OPTIONS') {
|
|
149
|
-
return
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Check if origin is allowed
|
|
153
|
-
if (isOriginAllowed(origin, corsConfig.origin)) {
|
|
154
|
-
// Set origin header
|
|
155
|
-
if (corsConfig.origin === '*' && !corsConfig.credentials) {
|
|
156
|
-
response.headers['Access-Control-Allow-Origin'] = '*'
|
|
157
|
-
} else if (origin) {
|
|
158
|
-
response.headers['Access-Control-Allow-Origin'] = origin
|
|
159
|
-
response.headers['Vary'] = 'Origin'
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Set credentials if enabled
|
|
163
|
-
if (corsConfig.credentials) {
|
|
164
|
-
response.headers['Access-Control-Allow-Credentials'] = 'true'
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Set exposed headers
|
|
168
|
-
if (corsConfig.exposedHeaders.length > 0) {
|
|
169
|
-
response.headers['Access-Control-Expose-Headers'] = corsConfig.exposedHeaders.join(', ')
|
|
170
|
-
}
|
|
171
|
-
} else if (origin) {
|
|
172
|
-
// Origin not allowed - don't set any CORS headers
|
|
173
|
-
log.warn('CORS origin not allowed for response', {
|
|
174
|
-
origin,
|
|
175
|
-
allowedOrigins: corsConfig.origin
|
|
176
|
-
})
|
|
177
|
-
}
|
|
76
|
+
const headers = { 'Access-Control-Allow-Origin': wildcard ? '*' : request.headers.origin }
|
|
77
|
+
if (corsConfig.credentials) headers['Access-Control-Allow-Credentials'] = 'true'
|
|
78
|
+
if (corsConfig.exposedHeaders.length) headers['Access-Control-Expose-Headers'] = corsConfig.exposedHeaders.join(', ')
|
|
79
|
+
response.headers = mergeResponseHeaders(response.headers, headers)
|
|
178
80
|
})
|
|
179
81
|
|
|
180
|
-
log.info('CORS plugin installed', {
|
|
181
|
-
origin: corsConfig.origin,
|
|
182
|
-
credentials: corsConfig.credentials,
|
|
183
|
-
methods: corsConfig.methods
|
|
184
|
-
})
|
|
82
|
+
log.info('CORS plugin installed', { origin: corsConfig.origin, credentials: corsConfig.credentials, methods: corsConfig.methods })
|
|
185
83
|
}
|
|
186
84
|
}
|
|
187
|
-
|
|
188
|
-
/*
|
|
189
|
-
Usage examples:
|
|
190
|
-
|
|
191
|
-
// Basic usage - allow all origins
|
|
192
|
-
await api.use(CorsPlugin);
|
|
193
|
-
|
|
194
|
-
// Specific origin
|
|
195
|
-
await api.use(CorsPlugin, {
|
|
196
|
-
'rest-api-cors': {
|
|
197
|
-
origin: 'https://app.example.com'
|
|
198
|
-
}
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
// Multiple origins
|
|
202
|
-
await api.use(CorsPlugin, {
|
|
203
|
-
'rest-api-cors': {
|
|
204
|
-
origin: ['https://app.example.com', 'https://admin.example.com']
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
// Dynamic origin with regex
|
|
209
|
-
await api.use(CorsPlugin, {
|
|
210
|
-
'rest-api-cors': {
|
|
211
|
-
origin: /^https:\/\/.*\.example\.com$/
|
|
212
|
-
}
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
// Function-based origin check
|
|
216
|
-
await api.use(CorsPlugin, {
|
|
217
|
-
'rest-api-cors': {
|
|
218
|
-
origin: (origin) => {
|
|
219
|
-
// Custom logic to determine if origin is allowed
|
|
220
|
-
return myAllowedOrigins.includes(origin);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// Full configuration
|
|
226
|
-
await api.use(CorsPlugin, {
|
|
227
|
-
'rest-api-cors': {
|
|
228
|
-
origin: 'https://app.example.com',
|
|
229
|
-
credentials: true,
|
|
230
|
-
methods: ['GET', 'POST', 'PATCH', 'DELETE'],
|
|
231
|
-
allowedHeaders: ['Content-Type', 'Authorization', 'X-Custom-Header'],
|
|
232
|
-
exposedHeaders: ['X-Total-Count', 'X-RateLimit-Remaining'],
|
|
233
|
-
maxAge: 3600 // 1 hour
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
*/
|