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
package/tests/queries.test.js
DELETED
|
@@ -1,521 +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 { createBasicApi, createExtendedApi } from './fixtures/api-configs.js'
|
|
5
|
-
import {
|
|
6
|
-
validateJsonApiStructure,
|
|
7
|
-
resourceIdentifier,
|
|
8
|
-
cleanTables,
|
|
9
|
-
countRecords,
|
|
10
|
-
createJsonApiDocument,
|
|
11
|
-
createRelationship,
|
|
12
|
-
createToManyRelationship,
|
|
13
|
-
assertResourceAttributes,
|
|
14
|
-
assertResourceRelationship
|
|
15
|
-
} from './helpers/test-utils.js'
|
|
16
|
-
|
|
17
|
-
// Create Knex instance for tests
|
|
18
|
-
const knex = knexLib({
|
|
19
|
-
client: 'better-sqlite3',
|
|
20
|
-
connection: {
|
|
21
|
-
filename: ':memory:'
|
|
22
|
-
},
|
|
23
|
-
useNullAsDefault: true
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
// API instances that persist across tests
|
|
27
|
-
let basicApi
|
|
28
|
-
let extendedApi
|
|
29
|
-
|
|
30
|
-
describe('Query Operations', () => {
|
|
31
|
-
before(async () => {
|
|
32
|
-
// Initialize APIs once
|
|
33
|
-
basicApi = await createBasicApi(knex)
|
|
34
|
-
extendedApi = await createExtendedApi(knex)
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
after(async () => {
|
|
38
|
-
// Close database connection to allow tests to exit
|
|
39
|
-
await knex.destroy()
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
describe('Filtering', () => {
|
|
43
|
-
const testData = {}
|
|
44
|
-
|
|
45
|
-
beforeEach(async () => {
|
|
46
|
-
// Clean all tables
|
|
47
|
-
await cleanTables(knex, [
|
|
48
|
-
'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
|
|
49
|
-
])
|
|
50
|
-
|
|
51
|
-
// Create test data set
|
|
52
|
-
// Create 3 countries
|
|
53
|
-
const countries = []
|
|
54
|
-
for (const [name, code] of [['United States', 'US'], ['United Kingdom', 'UK'], ['France', 'FR']]) {
|
|
55
|
-
const doc = createJsonApiDocument('countries', { name, code })
|
|
56
|
-
const result = await basicApi.resources.countries.post({
|
|
57
|
-
inputRecord: doc,
|
|
58
|
-
simplified: false
|
|
59
|
-
})
|
|
60
|
-
countries.push(result.data)
|
|
61
|
-
}
|
|
62
|
-
testData.countries = countries
|
|
63
|
-
|
|
64
|
-
// Create 2 publishers per country
|
|
65
|
-
const publishers = []
|
|
66
|
-
let pubIndex = 0
|
|
67
|
-
for (const country of countries) {
|
|
68
|
-
for (let i = 0; i < 2; i++) {
|
|
69
|
-
const doc = createJsonApiDocument('publishers',
|
|
70
|
-
{ name: `Publisher ${++pubIndex}` },
|
|
71
|
-
{ country: createRelationship(resourceIdentifier('countries', country.id)) }
|
|
72
|
-
)
|
|
73
|
-
const result = await basicApi.resources.publishers.post({
|
|
74
|
-
inputRecord: doc,
|
|
75
|
-
simplified: false
|
|
76
|
-
})
|
|
77
|
-
publishers.push({ ...result.data, countryId: country.id })
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
testData.publishers = publishers
|
|
81
|
-
|
|
82
|
-
// Create 3 authors
|
|
83
|
-
const authors = []
|
|
84
|
-
for (const name of ['Author One', 'Author Two', 'Author Three']) {
|
|
85
|
-
const doc = createJsonApiDocument('authors', { name })
|
|
86
|
-
const result = await basicApi.resources.authors.post({
|
|
87
|
-
inputRecord: doc,
|
|
88
|
-
simplified: false
|
|
89
|
-
})
|
|
90
|
-
authors.push(result.data)
|
|
91
|
-
}
|
|
92
|
-
testData.authors = authors
|
|
93
|
-
|
|
94
|
-
// Create 5 books with various relationships
|
|
95
|
-
const books = [
|
|
96
|
-
{ title: 'Book A', countryId: countries[0].id, publisherId: publishers[0].id, authorIds: [authors[0].id] },
|
|
97
|
-
{ title: 'Book B', countryId: countries[0].id, publisherId: publishers[1].id, authorIds: [authors[0].id, authors[1].id] },
|
|
98
|
-
{ title: 'Book C', countryId: countries[1].id, publisherId: publishers[2].id, authorIds: [authors[1].id] },
|
|
99
|
-
{ title: 'Book D', countryId: countries[1].id, publisherId: publishers[3].id, authorIds: [authors[2].id] },
|
|
100
|
-
{ title: 'Book E', countryId: countries[2].id, publisherId: publishers[4].id, authorIds: [authors[0].id, authors[2].id] }
|
|
101
|
-
]
|
|
102
|
-
|
|
103
|
-
testData.books = []
|
|
104
|
-
for (const bookData of books) {
|
|
105
|
-
const doc = createJsonApiDocument('books',
|
|
106
|
-
{
|
|
107
|
-
title: bookData.title
|
|
108
|
-
},
|
|
109
|
-
{
|
|
110
|
-
country: createRelationship(resourceIdentifier('countries', bookData.countryId)),
|
|
111
|
-
publisher: createRelationship(resourceIdentifier('publishers', bookData.publisherId)),
|
|
112
|
-
authors: createToManyRelationship(
|
|
113
|
-
bookData.authorIds.map(id => resourceIdentifier('authors', id))
|
|
114
|
-
)
|
|
115
|
-
}
|
|
116
|
-
)
|
|
117
|
-
const result = await basicApi.resources.books.post({
|
|
118
|
-
inputRecord: doc,
|
|
119
|
-
simplified: false
|
|
120
|
-
})
|
|
121
|
-
testData.books.push(result.data)
|
|
122
|
-
}
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
it('should filter by simple field', async () => {
|
|
126
|
-
const result = await basicApi.resources.books.query({
|
|
127
|
-
queryParams: {
|
|
128
|
-
filters: { country: testData.countries[0].id }
|
|
129
|
-
},
|
|
130
|
-
simplified: false
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
validateJsonApiStructure(result, true)
|
|
134
|
-
assert.equal(result.data.length, 2, 'Should return 2 books from US')
|
|
135
|
-
|
|
136
|
-
// Verify all returned books are from the correct country
|
|
137
|
-
for (const book of result.data) {
|
|
138
|
-
const bookData = testData.books.find(b => b.id === book.id)
|
|
139
|
-
assert(bookData)
|
|
140
|
-
const originalBook = testData.books.find(b => b.id === book.id)
|
|
141
|
-
assert.equal(originalBook.attributes.title.startsWith('Book A') ||
|
|
142
|
-
originalBook.attributes.title.startsWith('Book B'), true)
|
|
143
|
-
}
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
it('should filter by multiple fields', async () => {
|
|
147
|
-
const result = await basicApi.resources.books.query({
|
|
148
|
-
queryParams: {
|
|
149
|
-
filters: {
|
|
150
|
-
country: testData.countries[0].id,
|
|
151
|
-
publisher: testData.publishers[0].id
|
|
152
|
-
}
|
|
153
|
-
},
|
|
154
|
-
simplified: false
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
validateJsonApiStructure(result, true)
|
|
158
|
-
assert.equal(result.data.length, 1, 'Should return 1 book matching both filters')
|
|
159
|
-
assert.equal(result.data[0].attributes.title, 'Book A')
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
it('should return empty array when no matches', async () => {
|
|
163
|
-
const result = await basicApi.resources.books.query({
|
|
164
|
-
queryParams: {
|
|
165
|
-
filters: { country: 99999 }
|
|
166
|
-
},
|
|
167
|
-
simplified: false
|
|
168
|
-
})
|
|
169
|
-
|
|
170
|
-
validateJsonApiStructure(result, true)
|
|
171
|
-
assert.equal(result.data.length, 0, 'Should return empty array')
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
it('should throw error when filtering on non-searchable field', async () => {
|
|
175
|
-
await assert.rejects(
|
|
176
|
-
async () => {
|
|
177
|
-
await basicApi.resources.countries.query({
|
|
178
|
-
queryParams: {
|
|
179
|
-
filters: { invalid_field: 'test' }
|
|
180
|
-
},
|
|
181
|
-
simplified: false
|
|
182
|
-
})
|
|
183
|
-
},
|
|
184
|
-
(err) => {
|
|
185
|
-
return err.code === 'REST_API_VALIDATION'
|
|
186
|
-
},
|
|
187
|
-
'Should throw validation error for invalid filter field'
|
|
188
|
-
)
|
|
189
|
-
})
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
describe('Sorting', () => {
|
|
193
|
-
beforeEach(async () => {
|
|
194
|
-
await cleanTables(knex, [
|
|
195
|
-
'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
|
|
196
|
-
])
|
|
197
|
-
|
|
198
|
-
// Create test data
|
|
199
|
-
const countryDoc = createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' })
|
|
200
|
-
const countryResult = await basicApi.resources.countries.post({
|
|
201
|
-
inputRecord: countryDoc,
|
|
202
|
-
simplified: false
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
// Create books with different titles
|
|
206
|
-
const titles = ['Zebra Book', 'Apple Book', 'Mango Book', 'Banana Book']
|
|
207
|
-
for (const title of titles) {
|
|
208
|
-
const doc = createJsonApiDocument('books', {
|
|
209
|
-
title
|
|
210
|
-
},
|
|
211
|
-
{
|
|
212
|
-
country: createRelationship(resourceIdentifier('countries', countryResult.data.id))
|
|
213
|
-
})
|
|
214
|
-
await basicApi.resources.books.post({
|
|
215
|
-
inputRecord: doc,
|
|
216
|
-
simplified: false
|
|
217
|
-
})
|
|
218
|
-
}
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
it('should sort by single field ascending', async () => {
|
|
222
|
-
const result = await basicApi.resources.books.query({
|
|
223
|
-
queryParams: {
|
|
224
|
-
sort: ['title']
|
|
225
|
-
},
|
|
226
|
-
simplified: false
|
|
227
|
-
})
|
|
228
|
-
|
|
229
|
-
validateJsonApiStructure(result, true)
|
|
230
|
-
assert.equal(result.data.length, 4)
|
|
231
|
-
|
|
232
|
-
const titles = result.data.map(b => b.attributes.title)
|
|
233
|
-
assert.deepEqual(titles, ['Apple Book', 'Banana Book', 'Mango Book', 'Zebra Book'])
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
it('should sort by single field descending', async () => {
|
|
237
|
-
const result = await basicApi.resources.books.query({
|
|
238
|
-
queryParams: {
|
|
239
|
-
sort: ['-title']
|
|
240
|
-
},
|
|
241
|
-
simplified: false
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
validateJsonApiStructure(result, true)
|
|
245
|
-
assert.equal(result.data.length, 4)
|
|
246
|
-
|
|
247
|
-
const titles = result.data.map(b => b.attributes.title)
|
|
248
|
-
assert.deepEqual(titles, ['Zebra Book', 'Mango Book', 'Banana Book', 'Apple Book'])
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
it('should sort by multiple fields', async () => {
|
|
252
|
-
// Create books with same country but different titles
|
|
253
|
-
const countryDoc1 = createJsonApiDocument('countries', { name: 'Country A', code: 'CA' })
|
|
254
|
-
const countryDoc2 = createJsonApiDocument('countries', { name: 'Country B', code: 'CB' })
|
|
255
|
-
|
|
256
|
-
const country1Result = await basicApi.resources.countries.post({
|
|
257
|
-
inputRecord: countryDoc1,
|
|
258
|
-
simplified: false
|
|
259
|
-
})
|
|
260
|
-
|
|
261
|
-
const country2Result = await basicApi.resources.countries.post({
|
|
262
|
-
inputRecord: countryDoc2,
|
|
263
|
-
simplified: false
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
// Create books
|
|
267
|
-
const booksToCreate = [
|
|
268
|
-
{ title: 'Book Z', countryId: country1Result.data.id },
|
|
269
|
-
{ title: 'Book A', countryId: country1Result.data.id },
|
|
270
|
-
{ title: 'Book Z', countryId: country2Result.data.id },
|
|
271
|
-
{ title: 'Book A', countryId: country2Result.data.id }
|
|
272
|
-
]
|
|
273
|
-
|
|
274
|
-
for (const bookData of booksToCreate) {
|
|
275
|
-
const doc = createJsonApiDocument('books',
|
|
276
|
-
{ title: bookData.title },
|
|
277
|
-
{ country: createRelationship(resourceIdentifier('countries', bookData.countryId)) }
|
|
278
|
-
)
|
|
279
|
-
await basicApi.resources.books.post({
|
|
280
|
-
inputRecord: doc,
|
|
281
|
-
simplified: false
|
|
282
|
-
})
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Sort by country_id ascending, then title descending
|
|
286
|
-
const result = await basicApi.resources.books.query({
|
|
287
|
-
queryParams: {
|
|
288
|
-
sort: ['country', '-title']
|
|
289
|
-
},
|
|
290
|
-
simplified: false
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
// We expect 8 books total (4 from beforeEach + 4 from this test)
|
|
294
|
-
assert.equal(result.data.length, 8)
|
|
295
|
-
|
|
296
|
-
// Filter to only the books we created in this test
|
|
297
|
-
const testBooks = result.data.filter(book =>
|
|
298
|
-
book.attributes.title === 'Book A' || book.attributes.title === 'Book Z'
|
|
299
|
-
)
|
|
300
|
-
|
|
301
|
-
assert.equal(testBooks.length, 4)
|
|
302
|
-
|
|
303
|
-
// Sort test books by country relationship for verification
|
|
304
|
-
const sortedTestBooks = testBooks.sort((a, b) => {
|
|
305
|
-
const countryA = a.relationships.country.data.id
|
|
306
|
-
const countryB = b.relationships.country.data.id
|
|
307
|
-
if (countryA !== countryB) {
|
|
308
|
-
return countryA.localeCompare(countryB)
|
|
309
|
-
}
|
|
310
|
-
// Within same country, sort by title descending
|
|
311
|
-
return b.attributes.title.localeCompare(a.attributes.title)
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
// Verify the books are from the correct countries and in correct order
|
|
315
|
-
assert.equal(sortedTestBooks[0].relationships.country.data.id, country1Result.data.id)
|
|
316
|
-
assert.equal(sortedTestBooks[1].relationships.country.data.id, country1Result.data.id)
|
|
317
|
-
assert.equal(sortedTestBooks[2].relationships.country.data.id, country2Result.data.id)
|
|
318
|
-
assert.equal(sortedTestBooks[3].relationships.country.data.id, country2Result.data.id)
|
|
319
|
-
|
|
320
|
-
// Within each country, should be sorted by title descending
|
|
321
|
-
assert.equal(sortedTestBooks[0].attributes.title, 'Book Z')
|
|
322
|
-
assert.equal(sortedTestBooks[1].attributes.title, 'Book A')
|
|
323
|
-
assert.equal(sortedTestBooks[2].attributes.title, 'Book Z')
|
|
324
|
-
assert.equal(sortedTestBooks[3].attributes.title, 'Book A')
|
|
325
|
-
})
|
|
326
|
-
})
|
|
327
|
-
|
|
328
|
-
describe('Pagination', () => {
|
|
329
|
-
beforeEach(async () => {
|
|
330
|
-
await cleanTables(knex, [
|
|
331
|
-
'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
|
|
332
|
-
])
|
|
333
|
-
|
|
334
|
-
// Create test data
|
|
335
|
-
const countryDoc = createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' })
|
|
336
|
-
const countryResult = await basicApi.resources.countries.post({
|
|
337
|
-
inputRecord: countryDoc,
|
|
338
|
-
simplified: false
|
|
339
|
-
})
|
|
340
|
-
|
|
341
|
-
// Create 10 books
|
|
342
|
-
for (let i = 1; i <= 10; i++) {
|
|
343
|
-
const doc = createJsonApiDocument('books',
|
|
344
|
-
{ title: `Book ${String(i).padStart(2, '0')}` },
|
|
345
|
-
{ country: createRelationship(resourceIdentifier('countries', countryResult.data.id)) }
|
|
346
|
-
)
|
|
347
|
-
await basicApi.resources.books.post({
|
|
348
|
-
inputRecord: doc,
|
|
349
|
-
simplified: false
|
|
350
|
-
})
|
|
351
|
-
}
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
it('should paginate results', async () => {
|
|
355
|
-
// Get first page
|
|
356
|
-
const page1 = await basicApi.resources.books.query({
|
|
357
|
-
queryParams: {
|
|
358
|
-
page: { number: 1, size: 3 },
|
|
359
|
-
sort: ['title']
|
|
360
|
-
},
|
|
361
|
-
simplified: false
|
|
362
|
-
})
|
|
363
|
-
|
|
364
|
-
validateJsonApiStructure(page1, true)
|
|
365
|
-
assert.equal(page1.data.length, 3, 'Should return 3 results for page 1')
|
|
366
|
-
|
|
367
|
-
const page1Titles = page1.data.map(b => b.attributes.title)
|
|
368
|
-
assert.deepEqual(page1Titles, ['Book 01', 'Book 02', 'Book 03'])
|
|
369
|
-
|
|
370
|
-
// Get second page
|
|
371
|
-
const page2 = await basicApi.resources.books.query({
|
|
372
|
-
queryParams: {
|
|
373
|
-
page: { number: 2, size: 3 },
|
|
374
|
-
sort: ['title']
|
|
375
|
-
},
|
|
376
|
-
simplified: false
|
|
377
|
-
})
|
|
378
|
-
|
|
379
|
-
validateJsonApiStructure(page2, true)
|
|
380
|
-
assert.equal(page2.data.length, 3, 'Should return 3 results for page 2')
|
|
381
|
-
|
|
382
|
-
const page2Titles = page2.data.map(b => b.attributes.title)
|
|
383
|
-
assert.deepEqual(page2Titles, ['Book 04', 'Book 05', 'Book 06'])
|
|
384
|
-
|
|
385
|
-
// Verify different results
|
|
386
|
-
const page1Ids = page1.data.map(b => b.id)
|
|
387
|
-
const page2Ids = page2.data.map(b => b.id)
|
|
388
|
-
assert.equal(page1Ids.some(id => page2Ids.includes(id)), false, 'Pages should have different results')
|
|
389
|
-
})
|
|
390
|
-
|
|
391
|
-
it('should handle last page with fewer results', async () => {
|
|
392
|
-
const lastPage = await basicApi.resources.books.query({
|
|
393
|
-
queryParams: {
|
|
394
|
-
page: { number: 4, size: 3 },
|
|
395
|
-
sort: ['title']
|
|
396
|
-
},
|
|
397
|
-
simplified: false
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
validateJsonApiStructure(lastPage, true)
|
|
401
|
-
assert.equal(lastPage.data.length, 1, 'Should return 1 result for last page')
|
|
402
|
-
assert.equal(lastPage.data[0].attributes.title, 'Book 10')
|
|
403
|
-
})
|
|
404
|
-
|
|
405
|
-
it('should handle page beyond available data', async () => {
|
|
406
|
-
const emptyPage = await basicApi.resources.books.query({
|
|
407
|
-
queryParams: {
|
|
408
|
-
page: { number: 10, size: 5 }
|
|
409
|
-
},
|
|
410
|
-
simplified: false
|
|
411
|
-
})
|
|
412
|
-
|
|
413
|
-
validateJsonApiStructure(emptyPage, true)
|
|
414
|
-
assert.equal(emptyPage.data.length, 0, 'Should return empty array for page beyond data')
|
|
415
|
-
})
|
|
416
|
-
|
|
417
|
-
it('should respect queryMaxLimit limit', async () => {
|
|
418
|
-
// Assuming queryMaxLimit is 100 (from vars)
|
|
419
|
-
const result = await basicApi.resources.books.query({
|
|
420
|
-
queryParams: {
|
|
421
|
-
page: { number: 1, size: 200 } // Request more than max
|
|
422
|
-
},
|
|
423
|
-
simplified: false
|
|
424
|
-
})
|
|
425
|
-
|
|
426
|
-
validateJsonApiStructure(result, true)
|
|
427
|
-
assert(result.data.length <= 100, 'Should not exceed queryMaxLimit')
|
|
428
|
-
})
|
|
429
|
-
})
|
|
430
|
-
|
|
431
|
-
describe('Combined Query Features', () => {
|
|
432
|
-
beforeEach(async () => {
|
|
433
|
-
await cleanTables(knex, [
|
|
434
|
-
'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors',
|
|
435
|
-
'ext_countries', 'ext_publishers', 'ext_authors', 'ext_books', 'ext_book_authors'
|
|
436
|
-
])
|
|
437
|
-
|
|
438
|
-
// Create test data for extended API with more fields
|
|
439
|
-
const countries = []
|
|
440
|
-
for (const [name, code, pop] of [['USA', 'US', 300000000], ['UK', 'GB', 60000000], ['France', 'FR', 65000000]]) {
|
|
441
|
-
const doc = createJsonApiDocument('countries', {
|
|
442
|
-
name,
|
|
443
|
-
code,
|
|
444
|
-
population: pop,
|
|
445
|
-
currency: code === 'US' ? 'USD' : code === 'GB' ? 'GBP' : 'EUR'
|
|
446
|
-
})
|
|
447
|
-
const result = await extendedApi.resources.countries.post({
|
|
448
|
-
inputRecord: doc,
|
|
449
|
-
simplified: false
|
|
450
|
-
})
|
|
451
|
-
countries.push(result.data)
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Create books with prices
|
|
455
|
-
const bookData = [
|
|
456
|
-
{ title: 'Cheap Book A', price: 9.99, countryId: countries[0].id, language: 'en' },
|
|
457
|
-
{ title: 'Expensive Book B', price: 29.99, countryId: countries[0].id, language: 'en' },
|
|
458
|
-
{ title: 'Medium Book C', price: 19.99, countryId: countries[1].id, language: 'en' },
|
|
459
|
-
{ title: 'Cheap Book D', price: 12.99, countryId: countries[1].id, language: 'fr' },
|
|
460
|
-
{ title: 'Expensive Book E', price: 39.99, countryId: countries[2].id, language: 'fr' }
|
|
461
|
-
]
|
|
462
|
-
|
|
463
|
-
for (const data of bookData) {
|
|
464
|
-
const doc = createJsonApiDocument('books',
|
|
465
|
-
{
|
|
466
|
-
title: data.title,
|
|
467
|
-
price: data.price,
|
|
468
|
-
language: data.language
|
|
469
|
-
},
|
|
470
|
-
{ country: createRelationship(resourceIdentifier('countries', data.countryId)) }
|
|
471
|
-
)
|
|
472
|
-
await extendedApi.resources.books.post({
|
|
473
|
-
inputRecord: doc,
|
|
474
|
-
simplified: false
|
|
475
|
-
})
|
|
476
|
-
}
|
|
477
|
-
})
|
|
478
|
-
|
|
479
|
-
it('should filter, sort, and paginate together', async () => {
|
|
480
|
-
const result = await extendedApi.resources.books.query({
|
|
481
|
-
queryParams: {
|
|
482
|
-
filters: { language: 'en' },
|
|
483
|
-
sort: ['-price'],
|
|
484
|
-
page: { number: 1, size: 2 }
|
|
485
|
-
},
|
|
486
|
-
simplified: false
|
|
487
|
-
})
|
|
488
|
-
|
|
489
|
-
validateJsonApiStructure(result, true)
|
|
490
|
-
assert.equal(result.data.length, 2)
|
|
491
|
-
|
|
492
|
-
// Should get the 2 most expensive English books
|
|
493
|
-
assert.equal(result.data[0].attributes.title, 'Expensive Book B')
|
|
494
|
-
assert.equal(result.data[1].attributes.title, 'Medium Book C')
|
|
495
|
-
})
|
|
496
|
-
|
|
497
|
-
it('should filter and include relationships', async () => {
|
|
498
|
-
const result = await extendedApi.resources.books.query({
|
|
499
|
-
queryParams: {
|
|
500
|
-
filters: { language: 'fr' },
|
|
501
|
-
include: ['country']
|
|
502
|
-
},
|
|
503
|
-
simplified: false
|
|
504
|
-
})
|
|
505
|
-
|
|
506
|
-
validateJsonApiStructure(result, true)
|
|
507
|
-
assert.equal(result.data.length, 2, 'Should return 2 French books')
|
|
508
|
-
|
|
509
|
-
// Verify includes
|
|
510
|
-
assert(result.included, 'Should have included data')
|
|
511
|
-
const includedCountries = result.included.filter(r => r.type === 'countries')
|
|
512
|
-
assert(includedCountries.length >= 1, 'Should include at least one country')
|
|
513
|
-
|
|
514
|
-
// Verify included countries have the expected fields
|
|
515
|
-
for (const country of includedCountries) {
|
|
516
|
-
assert(country.attributes.name)
|
|
517
|
-
assert(country.attributes.currency)
|
|
518
|
-
}
|
|
519
|
-
})
|
|
520
|
-
})
|
|
521
|
-
})
|