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,584 +0,0 @@
|
|
|
1
|
-
import { describe, it, before, beforeEach, after } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import knexLib from 'knex'
|
|
4
|
-
import { Api } from 'hooked-api'
|
|
5
|
-
import { RestApiPlugin } from '../plugins/core/rest-api-plugin.js'
|
|
6
|
-
import { RestApiKnexPlugin } from '../plugins/core/rest-api-knex-plugin.js'
|
|
7
|
-
import {
|
|
8
|
-
cleanTables,
|
|
9
|
-
countRecords,
|
|
10
|
-
createJsonApiDocument,
|
|
11
|
-
createRelationship,
|
|
12
|
-
validateJsonApiStructure
|
|
13
|
-
} from './helpers/test-utils.js'
|
|
14
|
-
|
|
15
|
-
// Create Knex instance for tests
|
|
16
|
-
const knex = knexLib({
|
|
17
|
-
client: 'better-sqlite3',
|
|
18
|
-
connection: {
|
|
19
|
-
filename: ':memory:'
|
|
20
|
-
},
|
|
21
|
-
useNullAsDefault: true
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
describe('Query Limits and Include Limits', () => {
|
|
25
|
-
let api
|
|
26
|
-
|
|
27
|
-
before(async () => {
|
|
28
|
-
// Create API with specific limits for testing
|
|
29
|
-
api = new Api({
|
|
30
|
-
name: 'test-api-limits'
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
await api.use(RestApiPlugin, {
|
|
34
|
-
queryDefaultLimit: 10,
|
|
35
|
-
queryMaxLimit: 50,
|
|
36
|
-
simplified: false,
|
|
37
|
-
returnFullRecord: {
|
|
38
|
-
post: true
|
|
39
|
-
}
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
await api.use(RestApiKnexPlugin, {
|
|
43
|
-
knex
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
// Countries
|
|
47
|
-
await api.addResource('countries', {
|
|
48
|
-
schema: {
|
|
49
|
-
id: { type: 'id' },
|
|
50
|
-
name: { type: 'string', required: true },
|
|
51
|
-
code: { type: 'string', required: true }
|
|
52
|
-
}
|
|
53
|
-
})
|
|
54
|
-
await api.resources.countries.createKnexTable()
|
|
55
|
-
|
|
56
|
-
// Publishers
|
|
57
|
-
await api.addResource('publishers', {
|
|
58
|
-
schema: {
|
|
59
|
-
id: { type: 'id' },
|
|
60
|
-
name: { type: 'string', required: true },
|
|
61
|
-
country_id: { type: 'number', belongsTo: 'countries', as: 'country' }
|
|
62
|
-
},
|
|
63
|
-
relationships: {
|
|
64
|
-
books: {
|
|
65
|
-
type: 'hasMany',
|
|
66
|
-
target: 'books',
|
|
67
|
-
foreignKey: 'publisher_id',
|
|
68
|
-
// Relationships are always includable via ?include=
|
|
69
|
-
include: {
|
|
70
|
-
strategy: 'window' // Use window strategy for per-publisher limits
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
})
|
|
75
|
-
await api.resources.publishers.createKnexTable()
|
|
76
|
-
|
|
77
|
-
// Authors
|
|
78
|
-
await api.addResource('authors', {
|
|
79
|
-
schema: {
|
|
80
|
-
id: { type: 'id' },
|
|
81
|
-
name: { type: 'string', required: true },
|
|
82
|
-
country_id: { type: 'number', belongsTo: 'countries', as: 'country' }
|
|
83
|
-
},
|
|
84
|
-
relationships: {
|
|
85
|
-
books: {
|
|
86
|
-
type: 'manyToMany',
|
|
87
|
-
through: 'book_authors',
|
|
88
|
-
foreignKey: 'author_id',
|
|
89
|
-
otherKey: 'book_id',
|
|
90
|
-
// Relationships are always includable via ?include=
|
|
91
|
-
include: {
|
|
92
|
-
limit: 3, // Explicit limit for testing
|
|
93
|
-
strategy: 'window'
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
})
|
|
98
|
-
await api.resources.authors.createKnexTable()
|
|
99
|
-
|
|
100
|
-
// Books
|
|
101
|
-
await api.addResource('books', {
|
|
102
|
-
schema: {
|
|
103
|
-
id: { type: 'id' },
|
|
104
|
-
title: { type: 'string', required: true },
|
|
105
|
-
publisher_id: { type: 'number', belongsTo: 'publishers', as: 'publisher' }
|
|
106
|
-
},
|
|
107
|
-
relationships: {
|
|
108
|
-
authors: {
|
|
109
|
-
type: 'manyToMany',
|
|
110
|
-
through: 'book_authors',
|
|
111
|
-
foreignKey: 'book_id',
|
|
112
|
-
otherKey: 'author_id'
|
|
113
|
-
// Relationships are always includable via ?include=
|
|
114
|
-
},
|
|
115
|
-
reviews: { type: 'hasMany', target: 'reviews', via: 'reviewable' }
|
|
116
|
-
}
|
|
117
|
-
})
|
|
118
|
-
await api.resources.books.createKnexTable()
|
|
119
|
-
|
|
120
|
-
// Book Authors (pivot table)
|
|
121
|
-
await api.addResource('book_authors', {
|
|
122
|
-
schema: {
|
|
123
|
-
id: { type: 'id' },
|
|
124
|
-
book_id: { type: 'number', belongsTo: 'books', as: 'book' },
|
|
125
|
-
author_id: { type: 'number', belongsTo: 'authors', as: 'author' }
|
|
126
|
-
}
|
|
127
|
-
})
|
|
128
|
-
await api.resources.book_authors.createKnexTable()
|
|
129
|
-
|
|
130
|
-
// Reviews (polymorphic)
|
|
131
|
-
await api.addResource('reviews', {
|
|
132
|
-
schema: {
|
|
133
|
-
id: { type: 'id' },
|
|
134
|
-
content: { type: 'string', required: true },
|
|
135
|
-
rating: { type: 'number', required: true },
|
|
136
|
-
reviewer_name: { type: 'string', required: true },
|
|
137
|
-
reviewable_type: { type: 'string', required: true },
|
|
138
|
-
reviewable_id: { type: 'number', required: true }
|
|
139
|
-
},
|
|
140
|
-
relationships: {
|
|
141
|
-
reviewable: {
|
|
142
|
-
belongsToPolymorphic: {
|
|
143
|
-
types: ['books', 'authors', 'publishers'],
|
|
144
|
-
typeField: 'reviewable_type',
|
|
145
|
-
idField: 'reviewable_id'
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
})
|
|
150
|
-
await api.resources.reviews.createKnexTable()
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
after(async () => {
|
|
154
|
-
await knex.destroy()
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
beforeEach(async () => {
|
|
158
|
-
await cleanTables(knex, [
|
|
159
|
-
'countries', 'publishers', 'authors', 'books', 'book_authors', 'reviews'
|
|
160
|
-
])
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
describe('Query Default Limits', () => {
|
|
164
|
-
it('should apply queryDefaultLimit when no page size specified', async () => {
|
|
165
|
-
// Create 30 countries
|
|
166
|
-
for (let i = 1; i <= 30; i++) {
|
|
167
|
-
await api.resources.countries.post({
|
|
168
|
-
inputRecord: createJsonApiDocument('countries', {
|
|
169
|
-
name: `Country ${i}`,
|
|
170
|
-
code: `C${i}`
|
|
171
|
-
})
|
|
172
|
-
})
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Query without page size
|
|
176
|
-
const result = await api.resources.countries.query({
|
|
177
|
-
queryParams: {},
|
|
178
|
-
simplified: false
|
|
179
|
-
})
|
|
180
|
-
|
|
181
|
-
// Should use queryDefaultLimit of 10
|
|
182
|
-
assert.equal(result.data.length, 10, 'Should apply default limit of 10')
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
it('should respect queryMaxLimit when page size exceeds it', async () => {
|
|
186
|
-
// Create 100 countries
|
|
187
|
-
for (let i = 1; i <= 100; i++) {
|
|
188
|
-
await api.resources.countries.post({
|
|
189
|
-
inputRecord: createJsonApiDocument('countries', {
|
|
190
|
-
name: `Country ${i}`,
|
|
191
|
-
code: `C${i}`
|
|
192
|
-
})
|
|
193
|
-
})
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Query with page size exceeding max (use offset-based pagination)
|
|
197
|
-
const result = await api.resources.countries.query({
|
|
198
|
-
queryParams: {
|
|
199
|
-
page: { size: 100, number: 1 } // Exceeds queryMaxLimit of 50
|
|
200
|
-
},
|
|
201
|
-
simplified: false
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
// Should be capped at queryMaxLimit of 50
|
|
205
|
-
assert.equal(result.data.length, 50, 'Should cap at queryMaxLimit of 50')
|
|
206
|
-
})
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
describe('Include Limits - HasMany Relationships', () => {
|
|
210
|
-
beforeEach(async () => {
|
|
211
|
-
// Create test data
|
|
212
|
-
const country = await api.resources.countries.post({
|
|
213
|
-
inputRecord: createJsonApiDocument('countries', {
|
|
214
|
-
name: 'USA',
|
|
215
|
-
code: 'US'
|
|
216
|
-
})
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
const publisher = await api.resources.publishers.post({
|
|
220
|
-
inputRecord: createJsonApiDocument('publishers', {
|
|
221
|
-
name: 'Test Publisher'
|
|
222
|
-
}, {
|
|
223
|
-
country: createRelationship({ type: 'countries', id: String(country.id) })
|
|
224
|
-
})
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
// Create 25 books for the publisher
|
|
228
|
-
for (let i = 1; i <= 25; i++) {
|
|
229
|
-
await api.resources.books.post({
|
|
230
|
-
inputRecord: createJsonApiDocument('books', {
|
|
231
|
-
title: `Book ${i}`
|
|
232
|
-
}, {
|
|
233
|
-
publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
|
|
234
|
-
})
|
|
235
|
-
})
|
|
236
|
-
}
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
it('should apply default limit to hasMany includes when not specified', async () => {
|
|
240
|
-
// Query publishers with books (no explicit limit in include config)
|
|
241
|
-
const result = await api.resources.publishers.query({
|
|
242
|
-
queryParams: {
|
|
243
|
-
include: ['books']
|
|
244
|
-
},
|
|
245
|
-
simplified: false
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
assert(result.data, 'Result should have data array')
|
|
249
|
-
assert(result.data.length > 0, 'Should have at least one publisher')
|
|
250
|
-
|
|
251
|
-
const publisher = result.data[0]
|
|
252
|
-
const publisherBooks = (result.included || []).filter(r => r.type === 'books')
|
|
253
|
-
|
|
254
|
-
// Should apply queryDefaultLimit of 10
|
|
255
|
-
assert.equal(publisherBooks.length, 10, 'Should include only 10 books by default')
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
it('should respect explicit limit in relationship config', async () => {
|
|
259
|
-
// Create a country first
|
|
260
|
-
const country = await api.resources.countries.post({
|
|
261
|
-
inputRecord: createJsonApiDocument('countries', {
|
|
262
|
-
name: 'Test Country',
|
|
263
|
-
code: 'TC'
|
|
264
|
-
})
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
// Create a publisher
|
|
268
|
-
const publisher = await api.resources.publishers.post({
|
|
269
|
-
inputRecord: createJsonApiDocument('publishers', {
|
|
270
|
-
name: 'Test Publisher'
|
|
271
|
-
}, {
|
|
272
|
-
country: createRelationship({ type: 'countries', id: String(country.id) })
|
|
273
|
-
})
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
// Create authors with many books
|
|
277
|
-
const author = await api.resources.authors.post({
|
|
278
|
-
inputRecord: createJsonApiDocument('authors', {
|
|
279
|
-
name: 'Test Author'
|
|
280
|
-
}, {
|
|
281
|
-
country: createRelationship({ type: 'countries', id: String(country.id) })
|
|
282
|
-
})
|
|
283
|
-
})
|
|
284
|
-
|
|
285
|
-
// Create 10 books and associate with author
|
|
286
|
-
for (let i = 1; i <= 10; i++) {
|
|
287
|
-
const book = await api.resources.books.post({
|
|
288
|
-
inputRecord: createJsonApiDocument('books', {
|
|
289
|
-
title: `Book ${i}`
|
|
290
|
-
}, {
|
|
291
|
-
publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
|
|
292
|
-
})
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
await api.resources.book_authors.post({
|
|
296
|
-
inputRecord: createJsonApiDocument('book_authors', {}, {
|
|
297
|
-
book: createRelationship({ type: 'books', id: String(book.id) }),
|
|
298
|
-
author: createRelationship({ type: 'authors', id: String(author.id) })
|
|
299
|
-
})
|
|
300
|
-
})
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Query authors with books (has explicit limit of 3)
|
|
304
|
-
const result = await api.resources.authors.query({
|
|
305
|
-
queryParams: {
|
|
306
|
-
include: ['books']
|
|
307
|
-
},
|
|
308
|
-
simplified: false
|
|
309
|
-
})
|
|
310
|
-
|
|
311
|
-
const authorBooks = result.included.filter(r => r.type === 'books')
|
|
312
|
-
assert.equal(authorBooks.length, 3, 'Should respect explicit limit of 3')
|
|
313
|
-
})
|
|
314
|
-
})
|
|
315
|
-
|
|
316
|
-
describe('Include Limits - Polymorphic Relationships', () => {
|
|
317
|
-
beforeEach(async () => {
|
|
318
|
-
// Create a country first
|
|
319
|
-
const country = await api.resources.countries.post({
|
|
320
|
-
inputRecord: createJsonApiDocument('countries', {
|
|
321
|
-
name: 'Test Country',
|
|
322
|
-
code: 'TC'
|
|
323
|
-
})
|
|
324
|
-
})
|
|
325
|
-
|
|
326
|
-
// Create a publisher with country
|
|
327
|
-
const publisher = await api.resources.publishers.post({
|
|
328
|
-
inputRecord: createJsonApiDocument('publishers', {
|
|
329
|
-
name: 'Test Publisher'
|
|
330
|
-
}, {
|
|
331
|
-
country: createRelationship({ type: 'countries', id: String(country.id) })
|
|
332
|
-
})
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
// Create books with reviews
|
|
336
|
-
for (let bookNum = 1; bookNum <= 3; bookNum++) {
|
|
337
|
-
const book = await api.resources.books.post({
|
|
338
|
-
inputRecord: {
|
|
339
|
-
data: {
|
|
340
|
-
type: 'books',
|
|
341
|
-
attributes: {
|
|
342
|
-
title: `Book ${bookNum}`
|
|
343
|
-
},
|
|
344
|
-
relationships: {
|
|
345
|
-
publisher: { data: { type: 'publishers', id: publisher.id } }
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
})
|
|
350
|
-
|
|
351
|
-
// Create 15 reviews for each book
|
|
352
|
-
for (let i = 1; i <= 15; i++) {
|
|
353
|
-
await api.resources.reviews.post({
|
|
354
|
-
inputRecord: {
|
|
355
|
-
data: {
|
|
356
|
-
type: 'reviews',
|
|
357
|
-
attributes: {
|
|
358
|
-
content: `Review ${i} for Book ${bookNum}`,
|
|
359
|
-
rating: Math.floor(Math.random() * 5) + 1,
|
|
360
|
-
reviewer_name: `Reviewer ${i}`
|
|
361
|
-
},
|
|
362
|
-
relationships: {
|
|
363
|
-
reviewable: {
|
|
364
|
-
data: { type: 'books', id: book.id }
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
},
|
|
369
|
-
simplified: false
|
|
370
|
-
})
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
})
|
|
374
|
-
|
|
375
|
-
it('should apply default limit to polymorphic hasMany includes', async () => {
|
|
376
|
-
// Query books with reviews
|
|
377
|
-
const result = await api.resources.books.query({
|
|
378
|
-
queryParams: {
|
|
379
|
-
include: ['reviews']
|
|
380
|
-
},
|
|
381
|
-
simplified: false
|
|
382
|
-
})
|
|
383
|
-
|
|
384
|
-
// Without window strategy, the default strategy will limit total reviews
|
|
385
|
-
const totalReviews = result.included.filter(r => r.type === 'reviews')
|
|
386
|
-
|
|
387
|
-
// Should apply queryDefaultLimit of 10 to total reviews
|
|
388
|
-
assert.equal(totalReviews.length, 10, 'Should limit total reviews to default of 10')
|
|
389
|
-
})
|
|
390
|
-
})
|
|
391
|
-
|
|
392
|
-
describe('Include Limit Validation', () => {
|
|
393
|
-
it('should throw error when include limit exceeds queryMaxLimit at resource definition', async () => {
|
|
394
|
-
// Create a new API instance with stricter limits
|
|
395
|
-
const strictApi = new Api({ name: 'strict-api' })
|
|
396
|
-
|
|
397
|
-
await strictApi.use(RestApiPlugin, {
|
|
398
|
-
queryDefaultLimit: 5,
|
|
399
|
-
queryMaxLimit: 20,
|
|
400
|
-
simplified: false,
|
|
401
|
-
returnFullRecord: {
|
|
402
|
-
post: true
|
|
403
|
-
}
|
|
404
|
-
})
|
|
405
|
-
|
|
406
|
-
await strictApi.use(RestApiKnexPlugin, { knex })
|
|
407
|
-
|
|
408
|
-
// Try to create a resource with include limit exceeding max
|
|
409
|
-
await assert.rejects(
|
|
410
|
-
async () => {
|
|
411
|
-
await strictApi.addResource('test_items', {
|
|
412
|
-
schema: {
|
|
413
|
-
id: { type: 'id' },
|
|
414
|
-
name: { type: 'string' }
|
|
415
|
-
},
|
|
416
|
-
relationships: {
|
|
417
|
-
subitems: {
|
|
418
|
-
type: 'hasMany',
|
|
419
|
-
target: 'subitems',
|
|
420
|
-
foreignKey: 'test_item_id',
|
|
421
|
-
include: {
|
|
422
|
-
limit: 25 // Exceeds queryMaxLimit of 20
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
})
|
|
427
|
-
},
|
|
428
|
-
(err) => {
|
|
429
|
-
return err.message && err.message.includes('limit') && err.message.includes('exceeds queryMaxLimit')
|
|
430
|
-
},
|
|
431
|
-
'Should throw error for excessive include limit'
|
|
432
|
-
)
|
|
433
|
-
})
|
|
434
|
-
})
|
|
435
|
-
|
|
436
|
-
describe('Window Strategy with Defaults', () => {
|
|
437
|
-
beforeEach(async () => {
|
|
438
|
-
// Create a country first
|
|
439
|
-
const country = await api.resources.countries.post({
|
|
440
|
-
inputRecord: createJsonApiDocument('countries', {
|
|
441
|
-
name: 'USA',
|
|
442
|
-
code: 'US'
|
|
443
|
-
})
|
|
444
|
-
})
|
|
445
|
-
|
|
446
|
-
// Create multiple publishers with different numbers of books
|
|
447
|
-
for (let pubNum = 1; pubNum <= 3; pubNum++) {
|
|
448
|
-
const publisher = await api.resources.publishers.post({
|
|
449
|
-
inputRecord: {
|
|
450
|
-
data: createJsonApiDocument('publishers', {
|
|
451
|
-
name: `Publisher ${pubNum}`
|
|
452
|
-
}, {
|
|
453
|
-
country: createRelationship({ type: 'countries', id: String(country.id) })
|
|
454
|
-
}).data
|
|
455
|
-
}
|
|
456
|
-
})
|
|
457
|
-
|
|
458
|
-
// Create different number of books for each publisher
|
|
459
|
-
const bookCount = pubNum * 8 // 8, 16, 24 books
|
|
460
|
-
for (let i = 1; i <= bookCount; i++) {
|
|
461
|
-
await api.resources.books.post({
|
|
462
|
-
inputRecord: {
|
|
463
|
-
data: {
|
|
464
|
-
type: 'books',
|
|
465
|
-
attributes: {
|
|
466
|
-
title: `Publisher ${pubNum} Book ${i}`
|
|
467
|
-
},
|
|
468
|
-
relationships: {
|
|
469
|
-
publisher: { data: { type: 'publishers', id: publisher.id } }
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
})
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
it('should apply per-parent limits with window strategy', async () => {
|
|
479
|
-
// Query all publishers with books (window strategy configured)
|
|
480
|
-
const result = await api.resources.publishers.query({
|
|
481
|
-
queryParams: {
|
|
482
|
-
include: ['books']
|
|
483
|
-
},
|
|
484
|
-
simplified: false
|
|
485
|
-
})
|
|
486
|
-
|
|
487
|
-
// Each publisher should have its own limit applied
|
|
488
|
-
for (const publisher of result.data) {
|
|
489
|
-
const publisherBooks = result.included.filter(r =>
|
|
490
|
-
r.type === 'books' &&
|
|
491
|
-
r.relationships.publisher.data.id === publisher.id
|
|
492
|
-
)
|
|
493
|
-
|
|
494
|
-
// Each publisher gets up to queryDefaultLimit (10) books
|
|
495
|
-
assert(publisherBooks.length <= 10,
|
|
496
|
-
`Publisher ${publisher.id} should have at most 10 books with window strategy`)
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// Total books can exceed queryDefaultLimit since each parent gets its own limit
|
|
500
|
-
const totalBooks = result.included.filter(r => r.type === 'books').length
|
|
501
|
-
assert(totalBooks > 10, 'Total books should exceed 10 with window strategy')
|
|
502
|
-
// Publisher 1 has 8 books, Publishers 2 and 3 are limited to 10 each
|
|
503
|
-
assert.equal(totalBooks, 28, 'Should have 28 total books (8 + 10 + 10)')
|
|
504
|
-
})
|
|
505
|
-
})
|
|
506
|
-
|
|
507
|
-
describe('Many-to-Many with Window Strategy', () => {
|
|
508
|
-
it('should apply window limits to many-to-many relationships', async () => {
|
|
509
|
-
// Create test data
|
|
510
|
-
const country = await api.resources.countries.post({
|
|
511
|
-
inputRecord: createJsonApiDocument('countries', {
|
|
512
|
-
name: 'USA',
|
|
513
|
-
code: 'US'
|
|
514
|
-
})
|
|
515
|
-
})
|
|
516
|
-
|
|
517
|
-
const publisher = await api.resources.publishers.post({
|
|
518
|
-
inputRecord: createJsonApiDocument('publishers', {
|
|
519
|
-
name: 'Test Publisher'
|
|
520
|
-
}, {
|
|
521
|
-
country: createRelationship({ type: 'countries', id: String(country.id) })
|
|
522
|
-
})
|
|
523
|
-
})
|
|
524
|
-
|
|
525
|
-
// Create 2 authors
|
|
526
|
-
const authors = []
|
|
527
|
-
for (let i = 1; i <= 2; i++) {
|
|
528
|
-
const author = await api.resources.authors.post({
|
|
529
|
-
inputRecord: createJsonApiDocument('authors', {
|
|
530
|
-
name: `Author ${i}`
|
|
531
|
-
}, {
|
|
532
|
-
country: createRelationship({ type: 'countries', id: String(country.id) })
|
|
533
|
-
})
|
|
534
|
-
})
|
|
535
|
-
authors.push(author)
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// Create 10 books and associate ALL with BOTH authors
|
|
539
|
-
for (let i = 1; i <= 10; i++) {
|
|
540
|
-
const book = await api.resources.books.post({
|
|
541
|
-
inputRecord: createJsonApiDocument('books', {
|
|
542
|
-
title: `Book ${i}`
|
|
543
|
-
}, {
|
|
544
|
-
publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
|
|
545
|
-
})
|
|
546
|
-
})
|
|
547
|
-
|
|
548
|
-
// Associate with both authors
|
|
549
|
-
for (const author of authors) {
|
|
550
|
-
await api.resources.book_authors.post({
|
|
551
|
-
inputRecord: createJsonApiDocument('book_authors', {}, {
|
|
552
|
-
book: createRelationship({ type: 'books', id: String(book.id) }),
|
|
553
|
-
author: createRelationship({ type: 'authors', id: String(author.id) })
|
|
554
|
-
})
|
|
555
|
-
})
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// Query authors with books (configured with limit: 3, strategy: window)
|
|
560
|
-
const result = await api.resources.authors.query({
|
|
561
|
-
queryParams: {
|
|
562
|
-
include: ['books']
|
|
563
|
-
},
|
|
564
|
-
simplified: false
|
|
565
|
-
})
|
|
566
|
-
|
|
567
|
-
// Each author should get exactly 3 books (configured limit)
|
|
568
|
-
for (const author of result.data) {
|
|
569
|
-
// Check the author's relationships to see which books they have
|
|
570
|
-
const authorBookIds = author.relationships?.books?.data?.map(b => b.id) || []
|
|
571
|
-
|
|
572
|
-
// Verify the books exist in the included array
|
|
573
|
-
const authorBooks = result.included.filter(r =>
|
|
574
|
-
r.type === 'books' && authorBookIds.includes(r.id)
|
|
575
|
-
)
|
|
576
|
-
|
|
577
|
-
assert.equal(authorBookIds.length, 3,
|
|
578
|
-
`Author ${author.id} should have exactly 3 book relationships (configured limit)`)
|
|
579
|
-
assert.equal(authorBooks.length, 3,
|
|
580
|
-
`Author ${author.id} should have exactly 3 books in included array`)
|
|
581
|
-
}
|
|
582
|
-
})
|
|
583
|
-
})
|
|
584
|
-
})
|