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,563 +0,0 @@
|
|
|
1
|
-
import { describe, it, before, after, beforeEach } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import knexLib from 'knex'
|
|
4
|
-
import {
|
|
5
|
-
validateJsonApiStructure,
|
|
6
|
-
cleanTables,
|
|
7
|
-
createJsonApiDocument,
|
|
8
|
-
createRelationship,
|
|
9
|
-
resourceIdentifier
|
|
10
|
-
} from './helpers/test-utils.js'
|
|
11
|
-
import { createPaginationApi } from './fixtures/api-configs.js'
|
|
12
|
-
import { parseJsonApiQuery } from '../plugins/core/lib/querying-writing/connectors-query-parser.js'
|
|
13
|
-
|
|
14
|
-
// Create Knex instance for tests
|
|
15
|
-
const knex = knexLib({
|
|
16
|
-
client: 'better-sqlite3',
|
|
17
|
-
connection: {
|
|
18
|
-
filename: ':memory:'
|
|
19
|
-
},
|
|
20
|
-
useNullAsDefault: true
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
// API instance that persists across tests
|
|
24
|
-
let api
|
|
25
|
-
|
|
26
|
-
const parseLinkQuery = (link) => {
|
|
27
|
-
const url = new URL(link, 'https://example.test')
|
|
28
|
-
return parseJsonApiQuery(url.search.slice(1))
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
describe('Enhanced Pagination Features', () => {
|
|
32
|
-
before(async () => {
|
|
33
|
-
// Initialize API once with pagination features
|
|
34
|
-
api = await createPaginationApi(knex)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
after(async () => {
|
|
38
|
-
// Close database connection
|
|
39
|
-
await knex.destroy()
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
describe('Self Links', () => {
|
|
43
|
-
let countryId
|
|
44
|
-
|
|
45
|
-
beforeEach(async () => {
|
|
46
|
-
await cleanTables(knex, [
|
|
47
|
-
'pagination_countries', 'pagination_publishers', 'pagination_books'
|
|
48
|
-
])
|
|
49
|
-
|
|
50
|
-
// Create test data
|
|
51
|
-
const countryDoc = createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' })
|
|
52
|
-
const countryResult = await api.resources.countries.post({
|
|
53
|
-
inputRecord: countryDoc,
|
|
54
|
-
simplified: false
|
|
55
|
-
})
|
|
56
|
-
countryId = countryResult.data.id
|
|
57
|
-
|
|
58
|
-
// Create some books
|
|
59
|
-
for (let i = 1; i <= 3; i++) {
|
|
60
|
-
const doc = createJsonApiDocument('books',
|
|
61
|
-
{ title: `Book ${i}` },
|
|
62
|
-
{ country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
|
|
63
|
-
)
|
|
64
|
-
await api.resources.books.post({
|
|
65
|
-
inputRecord: doc,
|
|
66
|
-
simplified: false
|
|
67
|
-
})
|
|
68
|
-
}
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
it('should add self links to individual resources', async () => {
|
|
72
|
-
const list = await api.resources.books.query({ simplified: false })
|
|
73
|
-
const firstId = list.data[0]?.id
|
|
74
|
-
|
|
75
|
-
const result = await api.resources.books.get({
|
|
76
|
-
id: firstId,
|
|
77
|
-
simplified: false
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
validateJsonApiStructure(result, false)
|
|
81
|
-
assert(result.data.links, 'Resource should have links')
|
|
82
|
-
assert.equal(result.data.links.self, `/books/${firstId}`)
|
|
83
|
-
assert(result.links, 'Response should have top-level links')
|
|
84
|
-
assert.equal(result.links.self, `/books/${firstId}`)
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('should add self links to collection resources', async () => {
|
|
88
|
-
const result = await api.resources.books.query({
|
|
89
|
-
queryParams: {},
|
|
90
|
-
simplified: false
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
validateJsonApiStructure(result, true)
|
|
94
|
-
|
|
95
|
-
// Check individual resource links
|
|
96
|
-
result.data.forEach(book => {
|
|
97
|
-
assert(book.links, 'Each resource should have links')
|
|
98
|
-
assert(book.links.self, 'Each resource should have self link')
|
|
99
|
-
assert(book.links.self.startsWith('/books/'))
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
// Check top-level links
|
|
103
|
-
assert(result.links, 'Response should have top-level links')
|
|
104
|
-
assert.equal(result.links.self, '/books')
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('should add self links to included resources', async () => {
|
|
108
|
-
const result = await api.resources.books.query({
|
|
109
|
-
queryParams: {
|
|
110
|
-
include: ['country']
|
|
111
|
-
},
|
|
112
|
-
simplified: false
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
validateJsonApiStructure(result, true)
|
|
116
|
-
assert(result.included, 'Should have included resources')
|
|
117
|
-
|
|
118
|
-
result.included.forEach(resource => {
|
|
119
|
-
assert(resource.links, 'Included resource should have links')
|
|
120
|
-
assert(resource.links.self, 'Included resource should have self link')
|
|
121
|
-
assert(resource.links.self.startsWith('/'))
|
|
122
|
-
})
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
it('should add relative self links when no URL prefix configured', async () => {
|
|
126
|
-
// URLs are now always relative unless overridden via context
|
|
127
|
-
const result = await api.resources.books.query({
|
|
128
|
-
queryParams: {},
|
|
129
|
-
simplified: false
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
validateJsonApiStructure(result, true)
|
|
133
|
-
|
|
134
|
-
// Links should be present and relative
|
|
135
|
-
result.data.forEach(book => {
|
|
136
|
-
assert(book.links, 'Resources should have links')
|
|
137
|
-
assert(book.links.self, 'Each resource should have self link')
|
|
138
|
-
assert(book.links.self.startsWith('/books/'), 'Self link should be relative')
|
|
139
|
-
})
|
|
140
|
-
assert(result.links, 'Response should have top-level links')
|
|
141
|
-
assert(result.links.self.startsWith('/books'), 'Top-level self link should be relative')
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
it('should serialize collection self-link query parameters in JSON:API form', async () => {
|
|
145
|
-
const result = await api.resources.books.query({
|
|
146
|
-
queryParams: {
|
|
147
|
-
filters: {
|
|
148
|
-
country: countryId
|
|
149
|
-
},
|
|
150
|
-
fields: {
|
|
151
|
-
books: 'title,country_id',
|
|
152
|
-
countries: 'name,code'
|
|
153
|
-
},
|
|
154
|
-
include: ['country'],
|
|
155
|
-
sort: ['title']
|
|
156
|
-
},
|
|
157
|
-
simplified: false
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
validateJsonApiStructure(result, true)
|
|
161
|
-
|
|
162
|
-
const selfUrl = new URL(result.links.self, 'https://example.test')
|
|
163
|
-
const selfQuery = parseLinkQuery(result.links.self)
|
|
164
|
-
|
|
165
|
-
assert.equal(selfUrl.searchParams.get('filter[country]'), countryId)
|
|
166
|
-
assert.equal(selfUrl.searchParams.has('filters[country]'), false)
|
|
167
|
-
assert.deepEqual(selfQuery.filters, { country: countryId })
|
|
168
|
-
assert.deepEqual(selfQuery.include, ['country'])
|
|
169
|
-
assert.deepEqual(selfQuery.sort, ['title'])
|
|
170
|
-
assert.deepEqual(selfQuery.fields, {
|
|
171
|
-
books: 'title,country_id',
|
|
172
|
-
countries: 'name,code'
|
|
173
|
-
})
|
|
174
|
-
})
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
describe('Pagination Metadata', () => {
|
|
178
|
-
beforeEach(async () => {
|
|
179
|
-
await cleanTables(knex, [
|
|
180
|
-
'pagination_countries', 'pagination_publishers', 'pagination_books'
|
|
181
|
-
])
|
|
182
|
-
|
|
183
|
-
// Create test data
|
|
184
|
-
const countryDoc = createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' })
|
|
185
|
-
const countryResult = await api.resources.countries.post({
|
|
186
|
-
inputRecord: countryDoc,
|
|
187
|
-
simplified: false
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
// Create 10 books for pagination testing
|
|
191
|
-
for (let i = 1; i <= 10; i++) {
|
|
192
|
-
const doc = createJsonApiDocument('books',
|
|
193
|
-
{ title: `Book ${String(i).padStart(2, '0')}` },
|
|
194
|
-
{ country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
|
|
195
|
-
)
|
|
196
|
-
await api.resources.books.post({
|
|
197
|
-
inputRecord: doc,
|
|
198
|
-
simplified: false
|
|
199
|
-
})
|
|
200
|
-
}
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
it('should include pagination metadata in response', async () => {
|
|
204
|
-
const result = await api.resources.books.query({
|
|
205
|
-
queryParams: {
|
|
206
|
-
page: { number: 2, size: 3 }
|
|
207
|
-
},
|
|
208
|
-
simplified: false
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
validateJsonApiStructure(result, true)
|
|
212
|
-
assert(result.meta, 'Response should have meta')
|
|
213
|
-
assert(result.meta.pagination, 'Meta should have pagination')
|
|
214
|
-
|
|
215
|
-
const pagination = result.meta.pagination
|
|
216
|
-
assert.equal(pagination.page, 2)
|
|
217
|
-
assert.equal(pagination.pageSize, 3)
|
|
218
|
-
assert.equal(pagination.total, 10)
|
|
219
|
-
assert.equal(pagination.pageCount, 4)
|
|
220
|
-
assert.equal(pagination.hasMore, true)
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
it('should calculate hasMore correctly for last page', async () => {
|
|
224
|
-
const result = await api.resources.books.query({
|
|
225
|
-
queryParams: {
|
|
226
|
-
page: { number: 4, size: 3 }
|
|
227
|
-
},
|
|
228
|
-
simplified: false
|
|
229
|
-
})
|
|
230
|
-
|
|
231
|
-
assert.equal(result.meta.pagination.hasMore, false)
|
|
232
|
-
assert.equal(result.meta.pagination.page, 4)
|
|
233
|
-
assert.equal(result.meta.pagination.pageCount, 4)
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
it('should include pagination links', async () => {
|
|
237
|
-
const result = await api.resources.books.query({
|
|
238
|
-
queryParams: {
|
|
239
|
-
page: { number: 2, size: 3 }
|
|
240
|
-
},
|
|
241
|
-
simplified: false
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
assert(result.links, 'Response should have links')
|
|
245
|
-
assert(result.links.self, 'Should have self link')
|
|
246
|
-
assert(result.links.first, 'Should have first link')
|
|
247
|
-
assert(result.links.last, 'Should have last link')
|
|
248
|
-
assert(result.links.prev, 'Should have prev link')
|
|
249
|
-
assert(result.links.next, 'Should have next link')
|
|
250
|
-
|
|
251
|
-
assert.deepEqual(parseLinkQuery(result.links.self).page, { number: 2, size: 3 })
|
|
252
|
-
assert.deepEqual(parseLinkQuery(result.links.first).page, { number: 1, size: 3 })
|
|
253
|
-
assert.deepEqual(parseLinkQuery(result.links.last).page, { number: 4, size: 3 })
|
|
254
|
-
assert.deepEqual(parseLinkQuery(result.links.prev).page, { number: 1, size: 3 })
|
|
255
|
-
assert.deepEqual(parseLinkQuery(result.links.next).page, { number: 3, size: 3 })
|
|
256
|
-
})
|
|
257
|
-
|
|
258
|
-
it('should not include prev link on first page', async () => {
|
|
259
|
-
const result = await api.resources.books.query({
|
|
260
|
-
queryParams: {
|
|
261
|
-
page: { number: 1, size: 3 }
|
|
262
|
-
},
|
|
263
|
-
simplified: false
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
assert(result.links.first)
|
|
267
|
-
assert(result.links.next)
|
|
268
|
-
assert(!result.links.prev, 'First page should not have prev link')
|
|
269
|
-
})
|
|
270
|
-
|
|
271
|
-
it('should not include next link on last page', async () => {
|
|
272
|
-
const result = await api.resources.books.query({
|
|
273
|
-
queryParams: {
|
|
274
|
-
page: { number: 4, size: 3 }
|
|
275
|
-
},
|
|
276
|
-
simplified: false
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
assert(result.links.first)
|
|
280
|
-
assert(result.links.prev)
|
|
281
|
-
assert(!result.links.next, 'Last page should not have next link')
|
|
282
|
-
})
|
|
283
|
-
|
|
284
|
-
it('should preserve other query parameters in pagination links', async () => {
|
|
285
|
-
const result = await api.resources.books.query({
|
|
286
|
-
queryParams: {
|
|
287
|
-
page: { number: 2, size: 3 },
|
|
288
|
-
sort: ['-title']
|
|
289
|
-
},
|
|
290
|
-
simplified: false
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
assert(result.links, 'Should have links')
|
|
294
|
-
assert(result.links.next, 'Should have next link since page 2 of 4')
|
|
295
|
-
const nextQuery = parseLinkQuery(result.links.next)
|
|
296
|
-
assert.deepEqual(nextQuery.sort, ['-title'])
|
|
297
|
-
assert.deepEqual(nextQuery.page, { number: 3, size: 3 })
|
|
298
|
-
})
|
|
299
|
-
})
|
|
300
|
-
|
|
301
|
-
describe('Cursor-based Pagination', () => {
|
|
302
|
-
beforeEach(async () => {
|
|
303
|
-
await cleanTables(knex, [
|
|
304
|
-
'pagination_countries', 'pagination_publishers', 'pagination_books'
|
|
305
|
-
])
|
|
306
|
-
|
|
307
|
-
// Create test data
|
|
308
|
-
const countryDoc = createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' })
|
|
309
|
-
const countryResult = await api.resources.countries.post({
|
|
310
|
-
inputRecord: countryDoc,
|
|
311
|
-
simplified: false
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
// Create books
|
|
315
|
-
for (let i = 1; i <= 15; i++) {
|
|
316
|
-
const doc = createJsonApiDocument('books',
|
|
317
|
-
{
|
|
318
|
-
title: `Book ${String(i).padStart(2, '0')}`
|
|
319
|
-
},
|
|
320
|
-
{ country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
|
|
321
|
-
)
|
|
322
|
-
await api.resources.books.post({
|
|
323
|
-
inputRecord: doc,
|
|
324
|
-
simplified: false
|
|
325
|
-
})
|
|
326
|
-
}
|
|
327
|
-
})
|
|
328
|
-
|
|
329
|
-
it('should support cursor-based pagination with after parameter', async () => {
|
|
330
|
-
// Get first page
|
|
331
|
-
const firstPage = await api.resources.books.query({
|
|
332
|
-
queryParams: {
|
|
333
|
-
page: { size: 5 },
|
|
334
|
-
sort: ['-id']
|
|
335
|
-
},
|
|
336
|
-
simplified: false
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
validateJsonApiStructure(firstPage, true)
|
|
340
|
-
assert.equal(firstPage.data.length, 5)
|
|
341
|
-
assert(firstPage.meta.pagination.hasMore)
|
|
342
|
-
assert(firstPage.meta.pagination.cursor?.next)
|
|
343
|
-
|
|
344
|
-
// Get next page using cursor
|
|
345
|
-
const secondPage = await api.resources.books.query({
|
|
346
|
-
queryParams: {
|
|
347
|
-
page: {
|
|
348
|
-
size: 5,
|
|
349
|
-
after: firstPage.meta.pagination.cursor.next
|
|
350
|
-
},
|
|
351
|
-
sort: ['-id']
|
|
352
|
-
},
|
|
353
|
-
simplified: false
|
|
354
|
-
})
|
|
355
|
-
|
|
356
|
-
validateJsonApiStructure(secondPage, true)
|
|
357
|
-
assert.equal(secondPage.data.length, 5)
|
|
358
|
-
|
|
359
|
-
// Verify different records
|
|
360
|
-
const firstPageIds = firstPage.data.map(b => b.id)
|
|
361
|
-
const secondPageIds = secondPage.data.map(b => b.id)
|
|
362
|
-
assert(!firstPageIds.some(id => secondPageIds.includes(id)), 'Pages should have different records')
|
|
363
|
-
})
|
|
364
|
-
|
|
365
|
-
it('should include cursor pagination links', async () => {
|
|
366
|
-
const result = await api.resources.books.query({
|
|
367
|
-
queryParams: {
|
|
368
|
-
page: { size: 5 }
|
|
369
|
-
},
|
|
370
|
-
simplified: false
|
|
371
|
-
})
|
|
372
|
-
|
|
373
|
-
assert(result.links)
|
|
374
|
-
assert(result.links.self)
|
|
375
|
-
assert(result.links.first)
|
|
376
|
-
assert(result.links.next)
|
|
377
|
-
assert(parseLinkQuery(result.links.next).page.after, 'Next link should include cursor parameter')
|
|
378
|
-
})
|
|
379
|
-
|
|
380
|
-
it('should handle last page with cursor pagination', async () => {
|
|
381
|
-
// Get to last page
|
|
382
|
-
let currentCursor = null
|
|
383
|
-
let result
|
|
384
|
-
|
|
385
|
-
// Navigate through pages
|
|
386
|
-
for (let i = 0; i < 3; i++) {
|
|
387
|
-
result = await api.resources.books.query({
|
|
388
|
-
queryParams: {
|
|
389
|
-
page: {
|
|
390
|
-
size: 5,
|
|
391
|
-
...(currentCursor && { after: currentCursor })
|
|
392
|
-
}
|
|
393
|
-
},
|
|
394
|
-
simplified: false
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
if (result.meta.pagination.cursor?.next) {
|
|
398
|
-
currentCursor = result.meta.pagination.cursor.next
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
// Last page should have no next cursor
|
|
403
|
-
assert.equal(result.meta.pagination.hasMore, false)
|
|
404
|
-
assert(!result.meta.pagination.cursor?.next)
|
|
405
|
-
assert(!result.links.next)
|
|
406
|
-
})
|
|
407
|
-
})
|
|
408
|
-
|
|
409
|
-
describe('Count Query Configuration', () => {
|
|
410
|
-
beforeEach(async () => {
|
|
411
|
-
await cleanTables(knex, [
|
|
412
|
-
'pagination_countries', 'pagination_publishers', 'pagination_books'
|
|
413
|
-
])
|
|
414
|
-
|
|
415
|
-
// Create test data
|
|
416
|
-
const countryDoc = createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' })
|
|
417
|
-
const countryResult = await api.resources.countries.post({
|
|
418
|
-
inputRecord: countryDoc,
|
|
419
|
-
simplified: false
|
|
420
|
-
})
|
|
421
|
-
|
|
422
|
-
// Create books
|
|
423
|
-
for (let i = 1; i <= 20; i++) {
|
|
424
|
-
const doc = createJsonApiDocument('books',
|
|
425
|
-
{ title: `Book ${i}` },
|
|
426
|
-
{ country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
|
|
427
|
-
)
|
|
428
|
-
await api.resources.books.post({
|
|
429
|
-
inputRecord: doc,
|
|
430
|
-
simplified: false
|
|
431
|
-
})
|
|
432
|
-
}
|
|
433
|
-
})
|
|
434
|
-
|
|
435
|
-
it('should include count by default', async () => {
|
|
436
|
-
const result = await api.resources.books.query({
|
|
437
|
-
queryParams: {
|
|
438
|
-
page: { number: 1, size: 5 }
|
|
439
|
-
},
|
|
440
|
-
simplified: false
|
|
441
|
-
})
|
|
442
|
-
|
|
443
|
-
assert(result.meta.pagination.total !== undefined)
|
|
444
|
-
assert.equal(result.meta.pagination.total, 20)
|
|
445
|
-
assert.equal(result.meta.pagination.pageCount, 4)
|
|
446
|
-
})
|
|
447
|
-
|
|
448
|
-
it('should respect enablePaginationCounts when set to false', async () => {
|
|
449
|
-
// Override the enablePaginationCounts setting for this scope
|
|
450
|
-
const booksScope = api.resources.books
|
|
451
|
-
const originalSetting = booksScope.vars.enablePaginationCounts
|
|
452
|
-
booksScope.vars.enablePaginationCounts = false
|
|
453
|
-
|
|
454
|
-
const result = await api.resources.books.query({
|
|
455
|
-
queryParams: {
|
|
456
|
-
page: { number: 1, size: 5 }
|
|
457
|
-
},
|
|
458
|
-
simplified: false
|
|
459
|
-
})
|
|
460
|
-
|
|
461
|
-
assert(result.meta.pagination)
|
|
462
|
-
assert.equal(result.meta.pagination.page, 1)
|
|
463
|
-
assert.equal(result.meta.pagination.pageSize, 5)
|
|
464
|
-
assert(result.meta.pagination.total === undefined, 'Should not include total when counts disabled')
|
|
465
|
-
assert(result.meta.pagination.pageCount === undefined, 'Should not include pageCount when counts disabled')
|
|
466
|
-
assert(result.meta.pagination.hasMore === undefined, 'Should not include hasMore when counts disabled')
|
|
467
|
-
|
|
468
|
-
// Restore the setting
|
|
469
|
-
booksScope.vars.enablePaginationCounts = originalSetting
|
|
470
|
-
})
|
|
471
|
-
})
|
|
472
|
-
|
|
473
|
-
describe('Combined Features', () => {
|
|
474
|
-
let usCountryId
|
|
475
|
-
|
|
476
|
-
beforeEach(async () => {
|
|
477
|
-
await cleanTables(knex, [
|
|
478
|
-
'pagination_countries', 'pagination_publishers', 'pagination_books'
|
|
479
|
-
])
|
|
480
|
-
|
|
481
|
-
// Create multiple countries
|
|
482
|
-
const countries = []
|
|
483
|
-
for (const code of ['US', 'UK', 'FR']) {
|
|
484
|
-
const doc = createJsonApiDocument('countries', { name: `Country ${code}`, code })
|
|
485
|
-
const result = await api.resources.countries.post({
|
|
486
|
-
inputRecord: doc,
|
|
487
|
-
simplified: false
|
|
488
|
-
})
|
|
489
|
-
countries.push(result.data)
|
|
490
|
-
if (code === 'US') {
|
|
491
|
-
usCountryId = result.data.id
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
// Create books distributed across countries
|
|
496
|
-
let bookIndex = 1
|
|
497
|
-
for (const country of countries) {
|
|
498
|
-
for (let i = 0; i < 8; i++) {
|
|
499
|
-
const doc = createJsonApiDocument('books',
|
|
500
|
-
{ title: `Book ${String(bookIndex++).padStart(2, '0')}` },
|
|
501
|
-
{ country: createRelationship(resourceIdentifier('countries', country.id)) }
|
|
502
|
-
)
|
|
503
|
-
await api.resources.books.post({
|
|
504
|
-
inputRecord: doc,
|
|
505
|
-
simplified: false
|
|
506
|
-
})
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
})
|
|
510
|
-
|
|
511
|
-
it('should combine pagination with filtering and includes', async () => {
|
|
512
|
-
const result = await api.resources.books.query({
|
|
513
|
-
queryParams: {
|
|
514
|
-
page: { number: 1, size: 5 },
|
|
515
|
-
filters: {
|
|
516
|
-
country: parseInt(usCountryId)
|
|
517
|
-
},
|
|
518
|
-
fields: {
|
|
519
|
-
books: 'title,country_id',
|
|
520
|
-
countries: 'name,code'
|
|
521
|
-
},
|
|
522
|
-
include: ['country'],
|
|
523
|
-
sort: ['title']
|
|
524
|
-
},
|
|
525
|
-
simplified: false
|
|
526
|
-
})
|
|
527
|
-
|
|
528
|
-
validateJsonApiStructure(result, true)
|
|
529
|
-
|
|
530
|
-
// Check pagination worked
|
|
531
|
-
assert.equal(result.data.length, 5)
|
|
532
|
-
assert.equal(result.meta.pagination.total, 8)
|
|
533
|
-
assert.equal(result.meta.pagination.pageCount, 2)
|
|
534
|
-
|
|
535
|
-
// Check filtering worked
|
|
536
|
-
const countryIds = result.data.map(b => b.relationships.country.data.id)
|
|
537
|
-
assert(countryIds.every(id => id === countryIds[0]), 'All books should be from same country')
|
|
538
|
-
|
|
539
|
-
// Check includes worked
|
|
540
|
-
assert(result.included)
|
|
541
|
-
const usCountry = result.included.find(r => r.type === 'countries' && r.attributes.code === 'US')
|
|
542
|
-
assert(usCountry)
|
|
543
|
-
|
|
544
|
-
// Check self links present
|
|
545
|
-
result.data.forEach(book => {
|
|
546
|
-
assert(book.links.self)
|
|
547
|
-
})
|
|
548
|
-
|
|
549
|
-
const nextUrl = new URL(result.links.next, 'https://example.test')
|
|
550
|
-
const nextQuery = parseJsonApiQuery(nextUrl.search.slice(1))
|
|
551
|
-
assert.equal(nextUrl.searchParams.get('filter[country]'), usCountryId)
|
|
552
|
-
assert.equal(nextUrl.searchParams.has('filters[country]'), false)
|
|
553
|
-
assert.deepEqual(nextQuery.filters, { country: usCountryId })
|
|
554
|
-
assert.deepEqual(nextQuery.include, ['country'])
|
|
555
|
-
assert.deepEqual(nextQuery.sort, ['title'])
|
|
556
|
-
assert.deepEqual(nextQuery.fields, {
|
|
557
|
-
books: 'title,country_id',
|
|
558
|
-
countries: 'name,code'
|
|
559
|
-
})
|
|
560
|
-
assert.deepEqual(nextQuery.page, { number: 2, size: 5 })
|
|
561
|
-
})
|
|
562
|
-
})
|
|
563
|
-
})
|