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,153 +1,53 @@
|
|
|
1
1
|
import { createSchema } from 'json-rest-schema'
|
|
2
|
-
import { ensureSearchFieldsAreIndexed, generateSearchSchemaFromSchema,
|
|
2
|
+
import { ensureSearchFieldsAreIndexed, generateSearchSchemaFromSchema, compileFieldDependencies, snapshotResourceConfiguration } from './schema-helpers.js'
|
|
3
3
|
import { buildStorageInfo, normalizeStorageConfig } from '../storage/storage-mapping.js'
|
|
4
|
+
import { compileQueryFields } from './query-field-helpers.js'
|
|
5
|
+
import { buildEffectiveSortList } from '../querying/query-field-sort-helpers.js'
|
|
6
|
+
import { assertScalarQueryField, assertFieldNameMap } from './field-utils.js'
|
|
7
|
+
import { validateRelationshipFieldNames, validateSchemaFieldNames } from './scope-validations.js'
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* @example
|
|
13
|
-
* // Input: Raw schema with computed field
|
|
14
|
-
* const rawFields = {
|
|
15
|
-
* title: { type: 'string', required: true },
|
|
16
|
-
* content: { type: 'string' },
|
|
17
|
-
* author_id: { belongsTo: 'users', as: 'author' }, // Missing type
|
|
18
|
-
* word_count: {
|
|
19
|
-
* type: 'number',
|
|
20
|
-
* computed: true,
|
|
21
|
-
* compute: (record) => record.content.split(' ').length
|
|
22
|
-
* }
|
|
23
|
-
* };
|
|
24
|
-
*
|
|
25
|
-
* await compileSchemas(scope, deps);
|
|
26
|
-
*
|
|
27
|
-
* // Output in scope.vars.schemaInfo:
|
|
28
|
-
* // {
|
|
29
|
-
* // schema: Schema {}, // json-rest-schema instance
|
|
30
|
-
* // schemaStructure: {
|
|
31
|
-
* // title: { type: 'string', required: true },
|
|
32
|
-
* // content: { type: 'string' },
|
|
33
|
-
* // author_id: { type: 'id', belongsTo: 'users', as: 'author' } // Type added!
|
|
34
|
-
* // // Note: word_count removed (it's computed)
|
|
35
|
-
* // },
|
|
36
|
-
* // computed: {
|
|
37
|
-
* // word_count: { type: 'number', computed: true, compute: [Function] }
|
|
38
|
-
* // }
|
|
39
|
-
* // }
|
|
10
|
+
* Compile one resource and publish its completed metadata in vars.schemaInfo.
|
|
11
|
+
* Raw configuration is snapshotted; computed fields are kept out of the
|
|
12
|
+
* writable schema. Plugins enrich attribute, search and computed definitions
|
|
13
|
+
* before callback dependencies, output membership and storage mappings compile.
|
|
14
|
+
* schema:compiled runs before the completed metadata snapshot is published.
|
|
40
15
|
*
|
|
16
|
+
* @param {Object} scope
|
|
17
|
+
* @param {Object} scope.scopeOptions - Resource configuration
|
|
18
|
+
* @param {Object} scope.vars - Receives the compiled schemaInfo
|
|
19
|
+
* @param {Object} deps
|
|
20
|
+
* @param {Object} deps.context
|
|
21
|
+
* @param {string} deps.context.scopeName - Resource name
|
|
22
|
+
* @param {Function} deps.runHooks - Awaited schema enrichment/publication hooks
|
|
23
|
+
* @returns {Promise<void>}
|
|
41
24
|
* @example
|
|
42
|
-
* //
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* author_id: { belongsTo: 'users', as: 'author' }
|
|
47
|
-
* };
|
|
48
|
-
*
|
|
49
|
-
* // Output: Auto-generated searchSchema
|
|
50
|
-
* // searchSchemaObject contains:
|
|
51
|
-
* // {
|
|
52
|
-
* // title: {
|
|
53
|
-
* // type: 'string',
|
|
54
|
-
* // indexed: true, // Added for DB optimization
|
|
55
|
-
* // filterOperator: '=' // Default operator
|
|
56
|
-
* // }
|
|
57
|
-
* // }
|
|
58
|
-
*
|
|
59
|
-
* @example
|
|
60
|
-
* // Input: Pivot table auto-detection
|
|
61
|
-
* const pivotSchema = {
|
|
62
|
-
* article_id: { belongsTo: 'articles', as: 'article' },
|
|
63
|
-
* tag_id: { belongsTo: 'tags', as: 'tag' },
|
|
64
|
-
* display_order: { type: 'number' }
|
|
65
|
-
* };
|
|
66
|
-
*
|
|
67
|
-
* // Output: Both foreign keys become searchable
|
|
68
|
-
* // schemaStructure will have:
|
|
69
|
-
* // {
|
|
70
|
-
* // article_id: { type: 'id', belongsTo: 'articles', as: 'article', search: true },
|
|
71
|
-
* // tag_id: { type: 'id', belongsTo: 'tags', as: 'tag', search: true },
|
|
72
|
-
* // display_order: { type: 'number' }
|
|
73
|
-
* // }
|
|
74
|
-
* // This enables efficient many-to-many filtering!
|
|
75
|
-
*
|
|
76
|
-
* @example
|
|
77
|
-
* // Input: Schema with dependent getters
|
|
78
|
-
* const schema = {
|
|
79
|
-
* first_name: { type: 'string' },
|
|
80
|
-
* last_name: { type: 'string' },
|
|
81
|
-
* full_name: {
|
|
82
|
-
* type: 'string',
|
|
83
|
-
* getter: (record) => `${record.first_name} ${record.last_name}`,
|
|
84
|
-
* runGetterAfter: ['first_name', 'last_name'] // Dependencies
|
|
85
|
-
* },
|
|
86
|
-
* email: {
|
|
87
|
-
* type: 'string',
|
|
88
|
-
* setter: (value) => value.toLowerCase().trim()
|
|
89
|
-
* }
|
|
90
|
-
* };
|
|
91
|
-
*
|
|
92
|
-
* // Output includes dependency-sorted fields:
|
|
93
|
-
* // {
|
|
94
|
-
* // fieldGetters: { full_name: { getter: [Function], runGetterAfter: [...] } },
|
|
95
|
-
* // sortedGetterFields: ['full_name'], // Topologically sorted
|
|
96
|
-
* // fieldSetters: { email: { setter: [Function], runSetterAfter: [] } },
|
|
97
|
-
* // sortedSetterFields: ['email']
|
|
98
|
-
* // }
|
|
99
|
-
*
|
|
100
|
-
* @description
|
|
101
|
-
* Used by:
|
|
102
|
-
* - Every REST method calls this before any operations
|
|
103
|
-
* - Compilation happens once per scope (cached)
|
|
104
|
-
*
|
|
105
|
-
* Purpose:
|
|
106
|
-
* - Separates computed fields from database fields
|
|
107
|
-
* - Adds missing types to belongsTo relationships
|
|
108
|
-
* - Auto-detects pivot tables for many-to-many
|
|
109
|
-
* - Generates search schemas from field markers
|
|
110
|
-
* - Validates getter/setter dependencies
|
|
111
|
-
* - Provides hooks for schema enrichment
|
|
112
|
-
* - Caches everything for performance
|
|
113
|
-
*
|
|
114
|
-
* Data flow:
|
|
115
|
-
* 1. Extracts computed fields to separate object
|
|
116
|
-
* 2. Adds type:'id' to belongsTo fields
|
|
117
|
-
* 3. Detects pivot tables (2+ foreign keys, 40%+ of fields)
|
|
118
|
-
* 4. Runs schema:enrich hook for plugins
|
|
119
|
-
* 5. Creates json-rest-schema validation instance
|
|
120
|
-
* 6. Generates searchSchema from search:true fields
|
|
121
|
-
* 7. Runs searchSchema:enrich hook
|
|
122
|
-
* 8. Extracts and topologically sorts getters/setters
|
|
123
|
-
* 9. Caches all results in scope.vars.schemaInfo
|
|
25
|
+
* // A title field remains in schemaInfo.schemaStructure and is validated by
|
|
26
|
+
* // schemaInfo.schemaInstance. A computed wordCount instead lives in
|
|
27
|
+
* // schemaInfo.computed; schemaInfo.outputFields contains both definitions.
|
|
28
|
+
* // The search schema, dependency orders and storageInfo share this publication.
|
|
124
29
|
*/
|
|
125
30
|
export async function compileSchemas (scope, deps) {
|
|
126
31
|
// Extract scopeName from context
|
|
127
32
|
const { context, runHooks } = deps
|
|
128
33
|
const scopeName = context.scopeName
|
|
129
34
|
|
|
35
|
+
const scopeOptions = snapshotResourceConfiguration(scope.scopeOptions || {})
|
|
36
|
+
|
|
130
37
|
// Get raw schema
|
|
131
|
-
const rawFields =
|
|
38
|
+
const rawFields = scopeOptions.schema || {}
|
|
39
|
+
validateSchemaFieldNames(rawFields, scopeName)
|
|
40
|
+
validateRelationshipFieldNames(scopeOptions.relationships, scopeName)
|
|
132
41
|
|
|
133
42
|
// Extract computed fields from schema and build enriched schema
|
|
134
43
|
const computedFields = {}
|
|
135
44
|
const enrichedFields = {}
|
|
136
45
|
|
|
137
|
-
for (const [fieldName, fieldDef] of Object.entries(rawFields)) {
|
|
46
|
+
for (const [fieldName, fieldDef] of Object.entries(snapshotResourceConfiguration(rawFields))) {
|
|
138
47
|
if (fieldDef.computed === true) {
|
|
139
48
|
// Extract computed field - copy entire definition
|
|
140
49
|
computedFields[fieldName] = { ...fieldDef }
|
|
141
50
|
|
|
142
|
-
// Validate computed field
|
|
143
|
-
if (!fieldDef.type) {
|
|
144
|
-
throw new Error(`Computed field '${fieldName}' in scope '${scopeName}' must have a type`)
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
if (fieldDef.compute && typeof fieldDef.compute !== 'function') {
|
|
148
|
-
throw new Error(`Computed field '${fieldName}' in scope '${scopeName}' has invalid compute function`)
|
|
149
|
-
}
|
|
150
|
-
|
|
151
51
|
// Don't include computed fields in the validation schema
|
|
152
52
|
} else {
|
|
153
53
|
enrichedFields[fieldName] = { ...fieldDef }
|
|
@@ -161,34 +61,17 @@ export async function compileSchemas (scope, deps) {
|
|
|
161
61
|
}
|
|
162
62
|
}
|
|
163
63
|
|
|
164
|
-
// ADD LOGIC TO MAKE FIELDS SEARCHABLE HERE
|
|
165
|
-
// Auto-detect pivot tables and make their foreign key fields searchable
|
|
166
|
-
// This is critical for many-to-many relationship operations which need to filter pivot records
|
|
167
|
-
const fields = Object.entries(enrichedFields)
|
|
168
|
-
const belongsToFields = fields.filter(([_, def]) => def.belongsTo)
|
|
169
|
-
|
|
170
|
-
// If 2+ belongsTo fields and they make up 40%+ of non-id fields, likely a pivot table
|
|
171
|
-
const nonIdFields = fields.filter(([name, _]) => name !== 'id')
|
|
172
|
-
const isProbablyPivot = belongsToFields.length >= 2 &&
|
|
173
|
-
(nonIdFields.length === 0 || belongsToFields.length / nonIdFields.length >= 0.4)
|
|
174
|
-
|
|
175
|
-
if (isProbablyPivot) {
|
|
176
|
-
// Make all belongsTo fields searchable for pivot operations
|
|
177
|
-
for (const [, fieldDef] of belongsToFields) {
|
|
178
|
-
if (!fieldDef.search) {
|
|
179
|
-
fieldDef.search = true
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
64
|
// Hook: schema:enrich
|
|
185
65
|
const schemaContext = {
|
|
186
66
|
fields: enrichedFields, // Mutable
|
|
187
67
|
originalFields: rawFields, // Read-only
|
|
68
|
+
queryFields: {},
|
|
188
69
|
scopeName,
|
|
189
|
-
scopeOptions
|
|
70
|
+
scopeOptions
|
|
190
71
|
}
|
|
191
72
|
await runHooks('schema:enrich', schemaContext)
|
|
73
|
+
validateSchemaFieldNames(schemaContext.fields, scopeName)
|
|
74
|
+
assertFieldNameMap(schemaContext.queryFields, `query fields in '${scopeName}'`)
|
|
192
75
|
|
|
193
76
|
// Create schema object
|
|
194
77
|
const schemaObject = createSchema(schemaContext.fields)
|
|
@@ -203,38 +86,62 @@ export async function compileSchemas (scope, deps) {
|
|
|
203
86
|
// while searchSchema can specify filterOperator: 'contains' or complex join configurations.
|
|
204
87
|
const rawSearchFields = generateSearchSchemaFromSchema(
|
|
205
88
|
schemaContext.fields,
|
|
206
|
-
|
|
207
|
-
)
|
|
89
|
+
scopeOptions.searchSchema
|
|
90
|
+
) || {}
|
|
208
91
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
92
|
+
const searchSchemaContext = {
|
|
93
|
+
fields: snapshotResourceConfiguration(Object.fromEntries(Object.entries(rawSearchFields).map(([name, definition]) => [name, { ...definition }]))),
|
|
94
|
+
originalFields: rawSearchFields,
|
|
95
|
+
scopeName
|
|
96
|
+
}
|
|
97
|
+
await runHooks('searchSchema:enrich', searchSchemaContext)
|
|
98
|
+
assertFieldNameMap(searchSchemaContext.fields, `search schema in '${scopeName}'`)
|
|
99
|
+
ensureSearchFieldsAreIndexed(searchSchemaContext.fields)
|
|
100
|
+
const searchSchemaObject = createSchema(searchSchemaContext.fields)
|
|
101
|
+
|
|
102
|
+
// Derived fields must be rebuilt from the current candidate before publication.
|
|
103
|
+
const idProperty = scopeOptions.idProperty || scope.vars.idProperty || 'id'
|
|
104
|
+
const schemaRelationships = { ...(scopeOptions.relationships || {}) }
|
|
105
|
+
const computedSchemaContext = {
|
|
106
|
+
fields: snapshotResourceConfiguration(Object.fromEntries(Object.entries(computedFields).map(([name, definition]) => [name, { ...definition }]))),
|
|
107
|
+
originalFields: computedFields,
|
|
108
|
+
schemaStructure: schemaObject.structure,
|
|
109
|
+
searchSchemaStructure: searchSchemaObject.structure,
|
|
110
|
+
schemaRelationships,
|
|
111
|
+
idProperty,
|
|
112
|
+
scopeName,
|
|
113
|
+
scopeOptions
|
|
114
|
+
}
|
|
115
|
+
await runHooks('computedSchema:enrich', computedSchemaContext)
|
|
116
|
+
assertFieldNameMap(computedSchemaContext.fields, `computed fields in '${scopeName}'`)
|
|
117
|
+
validateRelationshipFieldNames(schemaRelationships, scopeName)
|
|
118
|
+
for (const [fieldName, fieldDef] of Object.entries(computedSchemaContext.fields)) {
|
|
119
|
+
if (fieldDef?.computed !== true) {
|
|
120
|
+
throw new Error(`Computed field '${fieldName}' in scope '${scopeName}' must set computed: true`)
|
|
121
|
+
}
|
|
122
|
+
if (!fieldDef.type) {
|
|
123
|
+
throw new Error(`Computed field '${fieldName}' in scope '${scopeName}' must have a type`)
|
|
124
|
+
}
|
|
125
|
+
if (fieldDef.compute !== undefined && typeof fieldDef.compute !== 'function') {
|
|
126
|
+
throw new Error(`Computed field '${fieldName}' in scope '${scopeName}' has invalid compute function`)
|
|
127
|
+
}
|
|
128
|
+
if (Object.hasOwn(schemaObject.structure, fieldName)) {
|
|
129
|
+
throw new Error(`Computed field '${fieldName}' in scope '${scopeName}' conflicts with an attribute field`)
|
|
224
130
|
}
|
|
225
|
-
await runHooks('searchSchema:enrich', searchSchemaContext)
|
|
226
|
-
|
|
227
|
-
// Create searchSchema object
|
|
228
|
-
searchSchemaObject = createSchema(rawSearchFields)
|
|
229
|
-
} else {
|
|
230
|
-
searchSchemaObject = createSchema({})
|
|
231
131
|
}
|
|
232
132
|
|
|
233
|
-
//
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
// Validate belongsTo fields
|
|
133
|
+
// Each public relationship name has one declaration.
|
|
134
|
+
const relationshipNames = new Set(Object.keys(schemaRelationships))
|
|
237
135
|
for (const [fieldName, fieldDef] of Object.entries(schemaContext.fields)) {
|
|
136
|
+
if (fieldDef.belongsToPolymorphic !== undefined) {
|
|
137
|
+
throw new Error(`Field '${fieldName}' in resource '${scopeName}' cannot declare belongsToPolymorphic; declare it in relationships`)
|
|
138
|
+
}
|
|
139
|
+
if (fieldDef.belongsTo && fieldDef.as) {
|
|
140
|
+
if (relationshipNames.has(fieldDef.as)) {
|
|
141
|
+
throw new Error(`Relationship name '${fieldDef.as}' in resource '${scopeName}' is declared more than once`)
|
|
142
|
+
}
|
|
143
|
+
relationshipNames.add(fieldDef.as)
|
|
144
|
+
}
|
|
238
145
|
// Validate that belongsTo fields have 'as' property
|
|
239
146
|
if (fieldDef.belongsTo && !fieldDef.as) {
|
|
240
147
|
throw new Error(
|
|
@@ -245,88 +152,77 @@ export async function compileSchemas (scope, deps) {
|
|
|
245
152
|
}
|
|
246
153
|
}
|
|
247
154
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
getterFields.push(fieldName)
|
|
260
|
-
|
|
261
|
-
// Validate dependencies exist
|
|
262
|
-
if (fieldDef.runGetterAfter && Array.isArray(fieldDef.runGetterAfter)) {
|
|
263
|
-
for (const dep of fieldDef.runGetterAfter) {
|
|
264
|
-
if (!schemaContext.fields[dep]) {
|
|
265
|
-
throw new Error(
|
|
266
|
-
`Field '${fieldName}' in resource '${scopeName}' has getter dependency '${dep}' that does not exist in schema`
|
|
267
|
-
)
|
|
268
|
-
}
|
|
269
|
-
}
|
|
155
|
+
const queryFields = compileQueryFields(schemaContext.queryFields, {
|
|
156
|
+
scopeName,
|
|
157
|
+
schemaStructure: schemaObject.structure,
|
|
158
|
+
computed: computedSchemaContext.fields,
|
|
159
|
+
schemaRelationships,
|
|
160
|
+
idProperty
|
|
161
|
+
})
|
|
162
|
+
for (const [kind, definitions] of [['Computed', computedSchemaContext.fields], ['Query', queryFields]]) {
|
|
163
|
+
for (const [name, definition] of Object.entries(definitions)) {
|
|
164
|
+
if (definition.belongsTo !== undefined || definition.belongsToPolymorphic !== undefined) {
|
|
165
|
+
throw new Error(`${kind} field '${name}' in scope '${scopeName}' cannot declare relationships; use a stored backing field or the resource relationships map`)
|
|
270
166
|
}
|
|
271
167
|
}
|
|
272
168
|
}
|
|
169
|
+
const fieldDependencies = compileFieldDependencies({
|
|
170
|
+
schemaStructure: schemaObject.structure,
|
|
171
|
+
computedFields: computedSchemaContext.fields,
|
|
172
|
+
queryFields,
|
|
173
|
+
schemaRelationships,
|
|
174
|
+
idProperty,
|
|
175
|
+
scopeName
|
|
176
|
+
})
|
|
273
177
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
sortedGetterFields = sortFieldsByDependencies(fieldGetters, 'runGetterAfter')
|
|
279
|
-
} catch (error) {
|
|
280
|
-
throw new Error(`Invalid getter dependencies in ${scopeName}: ${error.message}`)
|
|
178
|
+
const identityFields = new Set([...fieldDependencies.foreignKeyFields, 'id', idProperty])
|
|
179
|
+
for (const fieldName of identityFields) {
|
|
180
|
+
if (schemaObject.structure[fieldName]?.storage?.serialize !== undefined) {
|
|
181
|
+
throw new Error(`Field '${fieldName}' in scope '${scopeName}' cannot use storage.serialize: identity fields must preserve resource IDs and relationship types. Use normalizeId for resource ID canonicalization.`)
|
|
281
182
|
}
|
|
282
183
|
}
|
|
283
184
|
|
|
284
|
-
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
if (fieldDef.setter && typeof fieldDef.setter === 'function') {
|
|
290
|
-
fieldSetters[fieldName] = {
|
|
291
|
-
setter: fieldDef.setter,
|
|
292
|
-
runSetterAfter: fieldDef.runSetterAfter || [],
|
|
293
|
-
fieldDef
|
|
294
|
-
}
|
|
295
|
-
setterFields.push(fieldName)
|
|
296
|
-
|
|
297
|
-
// Validate dependencies exist
|
|
298
|
-
if (fieldDef.runSetterAfter && Array.isArray(fieldDef.runSetterAfter)) {
|
|
299
|
-
for (const dep of fieldDef.runSetterAfter) {
|
|
300
|
-
if (!schemaContext.fields[dep]) {
|
|
301
|
-
throw new Error(
|
|
302
|
-
`Field '${fieldName}' in resource '${scopeName}' has setter dependency '${dep}' that does not exist in schema`
|
|
303
|
-
)
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
}
|
|
185
|
+
const querySchema = { schemaStructure: schemaObject.structure, searchSchemaStructure: searchSchemaObject.structure, queryFields }
|
|
186
|
+
for (const [filterName, definition] of Object.entries(searchSchemaObject.structure)) {
|
|
187
|
+
if (typeof definition.applyFilter === 'function' && !definition.oneOf) continue
|
|
188
|
+
for (const target of definition.oneOf || [definition.actualField || filterName]) {
|
|
189
|
+
assertScalarQueryField(schemaObject.structure[target] || queryFields[target], target, 'filter')
|
|
307
190
|
}
|
|
308
191
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
try {
|
|
314
|
-
sortedSetterFields = sortFieldsByDependencies(fieldSetters, 'runSetterAfter')
|
|
315
|
-
} catch (error) {
|
|
316
|
-
throw new Error(`Invalid setter dependencies in ${scopeName}: ${error.message}`)
|
|
317
|
-
}
|
|
192
|
+
buildEffectiveSortList(scopeOptions.sortableFields, { schemaInfo: querySchema })
|
|
193
|
+
buildEffectiveSortList([], { defaultSort: scopeOptions.defaultSort, schemaInfo: querySchema })
|
|
194
|
+
for (const [fieldName, definition] of Object.entries(queryFields)) {
|
|
195
|
+
if (definition.sortable) buildEffectiveSortList([fieldName], { schemaInfo: querySchema })
|
|
318
196
|
}
|
|
319
197
|
|
|
320
|
-
const
|
|
321
|
-
const storage = normalizeStorageConfig(scope.scopeOptions.storage)
|
|
198
|
+
const storage = normalizeStorageConfig(scopeOptions.storage)
|
|
322
199
|
const storageInfo = buildStorageInfo({
|
|
323
200
|
schemaStructure: schemaObject.structure,
|
|
324
201
|
idProperty,
|
|
325
202
|
storage
|
|
326
203
|
})
|
|
327
204
|
|
|
205
|
+
const outputFields = { ...schemaObject.structure, ...computedSchemaContext.fields, ...queryFields }
|
|
206
|
+
const outputRelationships = { ...schemaRelationships }
|
|
207
|
+
for (const definition of Object.values(schemaObject.structure)) {
|
|
208
|
+
if (definition.as && definition.belongsTo) {
|
|
209
|
+
outputRelationships[definition.as] = definition
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const versionField = scopeOptions.versionField
|
|
214
|
+
if (versionField !== undefined) {
|
|
215
|
+
const definition = typeof versionField === 'string' && Object.hasOwn(schemaObject.structure, versionField)
|
|
216
|
+
? schemaObject.structure[versionField]
|
|
217
|
+
: undefined
|
|
218
|
+
if (!versionField || versionField === idProperty || !definition || definition.type !== 'string' ||
|
|
219
|
+
definition.computed || definition.virtual || definition.belongsTo || definition.getter || definition.setter || definition.storage?.serialize) {
|
|
220
|
+
throw new Error(`versionField in '${scopeName}' must name a stored string attribute without getters, setters, serializers or relationships, distinct from the primary ID`)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
328
224
|
// Cache everything
|
|
329
|
-
|
|
225
|
+
const schemaInfo = {
|
|
330
226
|
|
|
331
227
|
schemaInstance: schemaObject,
|
|
332
228
|
schemaStructure: schemaObject.structure,
|
|
@@ -334,18 +230,21 @@ export async function compileSchemas (scope, deps) {
|
|
|
334
230
|
searchSchemaInstance: searchSchemaObject,
|
|
335
231
|
searchSchemaStructure: searchSchemaObject.structure,
|
|
336
232
|
|
|
337
|
-
computed:
|
|
233
|
+
computed: computedSchemaContext.fields,
|
|
234
|
+
queryFields,
|
|
338
235
|
schemaRelationships,
|
|
339
|
-
|
|
236
|
+
outputFields,
|
|
237
|
+
outputRelationships,
|
|
238
|
+
...(versionField === undefined ? {} : { versionField }),
|
|
239
|
+
tableName: scopeOptions.tableName || scopeName,
|
|
340
240
|
idProperty,
|
|
341
241
|
storage,
|
|
342
242
|
storageInfo,
|
|
343
|
-
indexes: Array.isArray(
|
|
344
|
-
foreignKeys: Array.isArray(
|
|
345
|
-
checkConstraints: Array.isArray(
|
|
346
|
-
|
|
347
|
-
sortedGetterFields,
|
|
348
|
-
fieldSetters,
|
|
349
|
-
sortedSetterFields
|
|
243
|
+
indexes: Array.isArray(scopeOptions.indexes) ? [...scopeOptions.indexes] : [],
|
|
244
|
+
foreignKeys: Array.isArray(scopeOptions.foreignKeys) ? [...scopeOptions.foreignKeys] : [],
|
|
245
|
+
checkConstraints: Array.isArray(scopeOptions.checkConstraints) ? [...scopeOptions.checkConstraints] : [],
|
|
246
|
+
...fieldDependencies
|
|
350
247
|
}
|
|
248
|
+
await runHooks('schema:compiled', { scopeName, scopeOptions, schemaInfo })
|
|
249
|
+
scope.vars.schemaInfo = snapshotResourceConfiguration(schemaInfo)
|
|
351
250
|
}
|