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,617 +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
|
-
createJsonApiDocument,
|
|
10
|
-
createRelationship,
|
|
11
|
-
validateJsonApiStructure
|
|
12
|
-
} from './helpers/test-utils.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
|
-
describe('Nested Include Operations', () => {
|
|
24
|
-
let api
|
|
25
|
-
|
|
26
|
-
before(async () => {
|
|
27
|
-
// Create API instance
|
|
28
|
-
api = new Api({
|
|
29
|
-
name: 'test-nested-includes'
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
await api.use(RestApiPlugin, {
|
|
33
|
-
queryDefaultLimit: 10,
|
|
34
|
-
queryMaxLimit: 50,
|
|
35
|
-
simplified: false
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
await api.use(RestApiKnexPlugin, {
|
|
39
|
-
knex
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
// Countries
|
|
43
|
-
await api.addResource('countries', {
|
|
44
|
-
schema: {
|
|
45
|
-
id: { type: 'id' },
|
|
46
|
-
name: { type: 'string', required: true },
|
|
47
|
-
code: { type: 'string', max: 2 }
|
|
48
|
-
}
|
|
49
|
-
})
|
|
50
|
-
await api.resources.countries.createKnexTable()
|
|
51
|
-
|
|
52
|
-
// Publishers
|
|
53
|
-
await api.addResource('publishers', {
|
|
54
|
-
schema: {
|
|
55
|
-
id: { type: 'id' },
|
|
56
|
-
name: { type: 'string', required: true },
|
|
57
|
-
country_id: { type: 'number', belongsTo: 'countries', as: 'country' }
|
|
58
|
-
},
|
|
59
|
-
relationships: {
|
|
60
|
-
books: {
|
|
61
|
-
type: 'hasMany',
|
|
62
|
-
target: 'books',
|
|
63
|
-
foreignKey: 'publisher_id'
|
|
64
|
-
// Relationships are always includable via ?include=
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
})
|
|
68
|
-
await api.resources.publishers.createKnexTable()
|
|
69
|
-
|
|
70
|
-
// Authors
|
|
71
|
-
await api.addResource('authors', {
|
|
72
|
-
schema: {
|
|
73
|
-
id: { type: 'id' },
|
|
74
|
-
name: { type: 'string', required: true, search: true },
|
|
75
|
-
country_id: { type: 'number', belongsTo: 'countries', as: 'country' }
|
|
76
|
-
},
|
|
77
|
-
relationships: {
|
|
78
|
-
books: {
|
|
79
|
-
type: 'manyToMany',
|
|
80
|
-
through: 'book_authors',
|
|
81
|
-
foreignKey: 'author_id',
|
|
82
|
-
otherKey: 'book_id',
|
|
83
|
-
// Relationships are always includable via ?include=
|
|
84
|
-
include: {
|
|
85
|
-
limit: 5,
|
|
86
|
-
strategy: 'window'
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
})
|
|
91
|
-
await api.resources.authors.createKnexTable()
|
|
92
|
-
|
|
93
|
-
// Books
|
|
94
|
-
await api.addResource('books', {
|
|
95
|
-
schema: {
|
|
96
|
-
id: { type: 'id' },
|
|
97
|
-
title: { type: 'string', required: true },
|
|
98
|
-
publisher_id: { type: 'number', belongsTo: 'publishers', as: 'publisher' }
|
|
99
|
-
},
|
|
100
|
-
relationships: {
|
|
101
|
-
authors: {
|
|
102
|
-
type: 'manyToMany',
|
|
103
|
-
through: 'book_authors',
|
|
104
|
-
foreignKey: 'book_id',
|
|
105
|
-
otherKey: 'author_id'
|
|
106
|
-
// Relationships are always includable via ?include=
|
|
107
|
-
},
|
|
108
|
-
reviews: {
|
|
109
|
-
type: 'hasMany',
|
|
110
|
-
target: 'reviews',
|
|
111
|
-
foreignKey: 'book_id'
|
|
112
|
-
// Relationships are always includable via ?include=
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
})
|
|
116
|
-
await api.resources.books.createKnexTable()
|
|
117
|
-
|
|
118
|
-
// Book-Authors pivot table
|
|
119
|
-
await api.addResource('book_authors', {
|
|
120
|
-
schema: {
|
|
121
|
-
id: { type: 'id' },
|
|
122
|
-
book_id: { type: 'number', belongsTo: 'books', as: 'book' },
|
|
123
|
-
author_id: { type: 'number', belongsTo: 'authors', as: 'author' }
|
|
124
|
-
}
|
|
125
|
-
})
|
|
126
|
-
await api.resources.book_authors.createKnexTable()
|
|
127
|
-
|
|
128
|
-
// Reviews
|
|
129
|
-
await api.addResource('reviews', {
|
|
130
|
-
schema: {
|
|
131
|
-
id: { type: 'id' },
|
|
132
|
-
content: { type: 'string', required: true },
|
|
133
|
-
rating: { type: 'number', required: true },
|
|
134
|
-
book_id: { type: 'number', belongsTo: 'books', as: 'book' }
|
|
135
|
-
}
|
|
136
|
-
})
|
|
137
|
-
await api.resources.reviews.createKnexTable()
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
after(async () => {
|
|
141
|
-
await knex.destroy()
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
beforeEach(async () => {
|
|
145
|
-
await cleanTables(knex, [
|
|
146
|
-
'countries', 'publishers', 'authors', 'books', 'book_authors', 'reviews'
|
|
147
|
-
])
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
describe('Bidirectional Many-to-Many Relationships', () => {
|
|
151
|
-
beforeEach(async () => {
|
|
152
|
-
// Create test data
|
|
153
|
-
const country = await api.resources.countries.post({
|
|
154
|
-
inputRecord: createJsonApiDocument('countries', {
|
|
155
|
-
name: 'USA',
|
|
156
|
-
code: 'US'
|
|
157
|
-
})
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
const publisher = await api.resources.publishers.post({
|
|
161
|
-
inputRecord: createJsonApiDocument('publishers', {
|
|
162
|
-
name: 'Test Publisher'
|
|
163
|
-
}, {
|
|
164
|
-
country: createRelationship({ type: 'countries', id: String(country.id) })
|
|
165
|
-
})
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
// Create authors
|
|
169
|
-
const author1 = await api.resources.authors.post({
|
|
170
|
-
inputRecord: createJsonApiDocument('authors', {
|
|
171
|
-
name: 'Author 1'
|
|
172
|
-
}, {
|
|
173
|
-
country: createRelationship({ type: 'countries', id: String(country.id) })
|
|
174
|
-
})
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
const author2 = await api.resources.authors.post({
|
|
178
|
-
inputRecord: createJsonApiDocument('authors', {
|
|
179
|
-
name: 'Author 2'
|
|
180
|
-
}, {
|
|
181
|
-
country: createRelationship({ type: 'countries', id: String(country.id) })
|
|
182
|
-
})
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
// Create books
|
|
186
|
-
const book1 = await api.resources.books.post({
|
|
187
|
-
inputRecord: createJsonApiDocument('books', {
|
|
188
|
-
title: 'Book 1'
|
|
189
|
-
}, {
|
|
190
|
-
publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
|
|
191
|
-
})
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
const book2 = await api.resources.books.post({
|
|
195
|
-
inputRecord: createJsonApiDocument('books', {
|
|
196
|
-
title: 'Book 2'
|
|
197
|
-
}, {
|
|
198
|
-
publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
|
|
199
|
-
})
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
const book3 = await api.resources.books.post({
|
|
203
|
-
inputRecord: createJsonApiDocument('books', {
|
|
204
|
-
title: 'Book 3'
|
|
205
|
-
}, {
|
|
206
|
-
publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
|
|
207
|
-
})
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
// Create relationships
|
|
211
|
-
// Book 1 - Author 1 only
|
|
212
|
-
await api.resources.book_authors.post({
|
|
213
|
-
inputRecord: createJsonApiDocument('book_authors', {}, {
|
|
214
|
-
book: createRelationship({ type: 'books', id: String(book1.id) }),
|
|
215
|
-
author: createRelationship({ type: 'authors', id: String(author1.id) })
|
|
216
|
-
})
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
// Book 2 - Both authors
|
|
220
|
-
await api.resources.book_authors.post({
|
|
221
|
-
inputRecord: createJsonApiDocument('book_authors', {}, {
|
|
222
|
-
book: createRelationship({ type: 'books', id: String(book2.id) }),
|
|
223
|
-
author: createRelationship({ type: 'authors', id: String(author1.id) })
|
|
224
|
-
})
|
|
225
|
-
})
|
|
226
|
-
await api.resources.book_authors.post({
|
|
227
|
-
inputRecord: createJsonApiDocument('book_authors', {}, {
|
|
228
|
-
book: createRelationship({ type: 'books', id: String(book2.id) }),
|
|
229
|
-
author: createRelationship({ type: 'authors', id: String(author2.id) })
|
|
230
|
-
})
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
// Book 3 - Author 2 only
|
|
234
|
-
await api.resources.book_authors.post({
|
|
235
|
-
inputRecord: createJsonApiDocument('book_authors', {}, {
|
|
236
|
-
book: createRelationship({ type: 'books', id: String(book3.id) }),
|
|
237
|
-
author: createRelationship({ type: 'authors', id: String(author2.id) })
|
|
238
|
-
})
|
|
239
|
-
})
|
|
240
|
-
})
|
|
241
|
-
|
|
242
|
-
it('should populate reverse relationships when using nested includes', async () => {
|
|
243
|
-
const result = await api.resources.authors.query({
|
|
244
|
-
queryParams: {
|
|
245
|
-
include: ['books', 'books.authors']
|
|
246
|
-
},
|
|
247
|
-
simplified: false
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
validateJsonApiStructure(result, { isCollection: true })
|
|
251
|
-
assert.equal(result.data.length, 2, 'Should have 2 authors')
|
|
252
|
-
|
|
253
|
-
// Check that books are included
|
|
254
|
-
const books = result.included.filter(r => r.type === 'books')
|
|
255
|
-
assert.equal(books.length, 3, 'Should include 3 books')
|
|
256
|
-
|
|
257
|
-
// Check that authors are included (for the books.authors include)
|
|
258
|
-
const includedAuthors = result.included.filter(r => r.type === 'authors')
|
|
259
|
-
assert.equal(includedAuthors.length, 2, 'Should include 2 authors')
|
|
260
|
-
|
|
261
|
-
// Verify each book has its authors relationship populated
|
|
262
|
-
books.forEach(book => {
|
|
263
|
-
assert(book.relationships?.authors?.data, `Book ${book.id} should have authors relationship`)
|
|
264
|
-
|
|
265
|
-
if (book.attributes.title === 'Book 1') {
|
|
266
|
-
assert.equal(book.relationships.authors.data.length, 1, 'Book 1 should have 1 author')
|
|
267
|
-
assert.equal(book.relationships.authors.data[0].id, '1', 'Book 1 should have Author 1')
|
|
268
|
-
} else if (book.attributes.title === 'Book 2') {
|
|
269
|
-
assert.equal(book.relationships.authors.data.length, 2, 'Book 2 should have 2 authors')
|
|
270
|
-
const authorIds = book.relationships.authors.data.map(a => a.id).sort()
|
|
271
|
-
assert.deepEqual(authorIds, ['1', '2'], 'Book 2 should have both authors')
|
|
272
|
-
} else if (book.attributes.title === 'Book 3') {
|
|
273
|
-
assert.equal(book.relationships.authors.data.length, 1, 'Book 3 should have 1 author')
|
|
274
|
-
assert.equal(book.relationships.authors.data[0].id, '2', 'Book 3 should have Author 2')
|
|
275
|
-
}
|
|
276
|
-
})
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
it('should work with window strategy limits', async () => {
|
|
280
|
-
// Create more books to test window limits
|
|
281
|
-
const publisher = await api.resources.publishers.query({ simplified: false })
|
|
282
|
-
const publisherId = publisher.data[0].id
|
|
283
|
-
|
|
284
|
-
// Get the first author
|
|
285
|
-
const authorsResult = await api.resources.authors.query({ simplified: false })
|
|
286
|
-
const authorId = authorsResult.data[0].id
|
|
287
|
-
|
|
288
|
-
for (let i = 4; i <= 10; i++) {
|
|
289
|
-
const book = await api.resources.books.post({
|
|
290
|
-
inputRecord: createJsonApiDocument('books', {
|
|
291
|
-
title: `Book ${i}`
|
|
292
|
-
}, {
|
|
293
|
-
publisher: createRelationship({ type: 'publishers', id: publisherId })
|
|
294
|
-
})
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
// Associate all new books with Author 1
|
|
298
|
-
await api.resources.book_authors.post({
|
|
299
|
-
inputRecord: createJsonApiDocument('book_authors', {}, {
|
|
300
|
-
book: createRelationship({ type: 'books', id: String(book.id) }),
|
|
301
|
-
author: createRelationship({ type: 'authors', id: String(authorId) })
|
|
302
|
-
})
|
|
303
|
-
})
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
const result = await api.resources.authors.query({
|
|
307
|
-
queryParams: {
|
|
308
|
-
include: ['books', 'books.authors'],
|
|
309
|
-
filters: { name: 'Author 1' }
|
|
310
|
-
},
|
|
311
|
-
simplified: false
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
const author1 = result.data[0]
|
|
315
|
-
// Author 1 has books 1,2 from setup + books 4-10 from test = 9 total books
|
|
316
|
-
// But window limit is 5, so should only get 5
|
|
317
|
-
// Verify we got the right author and they have books
|
|
318
|
-
assert(author1, 'Should have found Author 1')
|
|
319
|
-
assert(author1.relationships?.books?.data, 'Author 1 should have books relationship')
|
|
320
|
-
|
|
321
|
-
// The window limit should apply
|
|
322
|
-
const bookCount = author1.relationships.books.data.length
|
|
323
|
-
assert(bookCount >= 2, `Author 1 should have at least 2 books, got ${bookCount}`)
|
|
324
|
-
assert(bookCount <= 5, `Author 1 should have at most 5 books (window limit), got ${bookCount}`)
|
|
325
|
-
|
|
326
|
-
// All included books should have their authors relationship populated
|
|
327
|
-
const books = result.included.filter(r => r.type === 'books')
|
|
328
|
-
assert(books.length >= 2, 'Should include at least 2 books')
|
|
329
|
-
|
|
330
|
-
// At least some books should have their authors relationship populated
|
|
331
|
-
const booksWithAuthors = books.filter(book =>
|
|
332
|
-
book.relationships?.authors?.data && book.relationships.authors.data.length > 0
|
|
333
|
-
)
|
|
334
|
-
assert(booksWithAuthors.length > 0, 'At least some books should have authors relationship populated')
|
|
335
|
-
})
|
|
336
|
-
})
|
|
337
|
-
|
|
338
|
-
describe('Deep Nested Includes', () => {
|
|
339
|
-
beforeEach(async () => {
|
|
340
|
-
// Create hierarchical test data
|
|
341
|
-
const usa = await api.resources.countries.post({
|
|
342
|
-
inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
|
|
343
|
-
})
|
|
344
|
-
|
|
345
|
-
const uk = await api.resources.countries.post({
|
|
346
|
-
inputRecord: createJsonApiDocument('countries', { name: 'UK', code: 'GB' })
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
const publisher1 = await api.resources.publishers.post({
|
|
350
|
-
inputRecord: createJsonApiDocument('publishers', {
|
|
351
|
-
name: 'US Publisher'
|
|
352
|
-
}, {
|
|
353
|
-
country: createRelationship({ type: 'countries', id: String(usa.id) })
|
|
354
|
-
})
|
|
355
|
-
})
|
|
356
|
-
|
|
357
|
-
const publisher2 = await api.resources.publishers.post({
|
|
358
|
-
inputRecord: createJsonApiDocument('publishers', {
|
|
359
|
-
name: 'UK Publisher'
|
|
360
|
-
}, {
|
|
361
|
-
country: createRelationship({ type: 'countries', id: String(uk.id) })
|
|
362
|
-
})
|
|
363
|
-
})
|
|
364
|
-
|
|
365
|
-
const author = await api.resources.authors.post({
|
|
366
|
-
inputRecord: createJsonApiDocument('authors', {
|
|
367
|
-
name: 'International Author'
|
|
368
|
-
}, {
|
|
369
|
-
country: createRelationship({ type: 'countries', id: String(usa.id) })
|
|
370
|
-
})
|
|
371
|
-
})
|
|
372
|
-
|
|
373
|
-
const book1 = await api.resources.books.post({
|
|
374
|
-
inputRecord: createJsonApiDocument('books', {
|
|
375
|
-
title: 'US Book'
|
|
376
|
-
}, {
|
|
377
|
-
publisher: createRelationship({ type: 'publishers', id: String(publisher1.id) })
|
|
378
|
-
})
|
|
379
|
-
})
|
|
380
|
-
|
|
381
|
-
const book2 = await api.resources.books.post({
|
|
382
|
-
inputRecord: createJsonApiDocument('books', {
|
|
383
|
-
title: 'UK Book'
|
|
384
|
-
}, {
|
|
385
|
-
publisher: createRelationship({ type: 'publishers', id: String(publisher2.id) })
|
|
386
|
-
})
|
|
387
|
-
})
|
|
388
|
-
|
|
389
|
-
// Associate both books with the author
|
|
390
|
-
await api.resources.book_authors.post({
|
|
391
|
-
inputRecord: createJsonApiDocument('book_authors', {}, {
|
|
392
|
-
book: createRelationship({ type: 'books', id: String(book1.id) }),
|
|
393
|
-
author: createRelationship({ type: 'authors', id: String(author.id) })
|
|
394
|
-
})
|
|
395
|
-
})
|
|
396
|
-
|
|
397
|
-
await api.resources.book_authors.post({
|
|
398
|
-
inputRecord: createJsonApiDocument('book_authors', {}, {
|
|
399
|
-
book: createRelationship({ type: 'books', id: String(book2.id) }),
|
|
400
|
-
author: createRelationship({ type: 'authors', id: String(author.id) })
|
|
401
|
-
})
|
|
402
|
-
})
|
|
403
|
-
})
|
|
404
|
-
|
|
405
|
-
it('should handle 3+ levels of nested includes', async () => {
|
|
406
|
-
const result = await api.resources.authors.query({
|
|
407
|
-
queryParams: {
|
|
408
|
-
include: ['books', 'books.publisher', 'books.publisher.country']
|
|
409
|
-
},
|
|
410
|
-
simplified: false
|
|
411
|
-
})
|
|
412
|
-
|
|
413
|
-
validateJsonApiStructure(result, { isCollection: true })
|
|
414
|
-
|
|
415
|
-
// Check all resource types are included
|
|
416
|
-
const books = result.included.filter(r => r.type === 'books')
|
|
417
|
-
const publishers = result.included.filter(r => r.type === 'publishers')
|
|
418
|
-
const countries = result.included.filter(r => r.type === 'countries')
|
|
419
|
-
|
|
420
|
-
assert.equal(books.length, 2, 'Should include 2 books')
|
|
421
|
-
assert.equal(publishers.length, 2, 'Should include 2 publishers')
|
|
422
|
-
// All defined relationships will be included when requested via ?include=
|
|
423
|
-
|
|
424
|
-
// Verify nested relationships are populated
|
|
425
|
-
books.forEach(book => {
|
|
426
|
-
assert(book.relationships?.publisher?.data, 'Book should have publisher relationship')
|
|
427
|
-
})
|
|
428
|
-
|
|
429
|
-
publishers.forEach(publisher => {
|
|
430
|
-
assert(publisher.relationships?.country?.data, 'Publisher should have country relationship')
|
|
431
|
-
})
|
|
432
|
-
})
|
|
433
|
-
|
|
434
|
-
it('should handle multiple paths to the same resource', async () => {
|
|
435
|
-
const result = await api.resources.publishers.query({
|
|
436
|
-
queryParams: {
|
|
437
|
-
include: ['books', 'books.authors', 'books.authors.country', 'country']
|
|
438
|
-
},
|
|
439
|
-
simplified: false
|
|
440
|
-
})
|
|
441
|
-
|
|
442
|
-
validateJsonApiStructure(result, { isCollection: true })
|
|
443
|
-
|
|
444
|
-
// Countries should be deduplicated
|
|
445
|
-
const countries = result.included.filter(r => r.type === 'countries')
|
|
446
|
-
const uniqueCountryIds = [...new Set(countries.map(c => c.id))]
|
|
447
|
-
assert.equal(countries.length, uniqueCountryIds.length, 'Countries should be deduplicated')
|
|
448
|
-
|
|
449
|
-
// Authors should have their relationships populated
|
|
450
|
-
const authors = result.included.filter(r => r.type === 'authors')
|
|
451
|
-
authors.forEach(author => {
|
|
452
|
-
assert(author.relationships?.country?.data, 'Author should have country relationship')
|
|
453
|
-
assert(author.relationships?.books?.data, 'Author should have books relationship')
|
|
454
|
-
})
|
|
455
|
-
})
|
|
456
|
-
})
|
|
457
|
-
|
|
458
|
-
describe('Nested Includes with Sparse Fieldsets', () => {
|
|
459
|
-
beforeEach(async () => {
|
|
460
|
-
// Create test data with reviews
|
|
461
|
-
const country = await api.resources.countries.post({
|
|
462
|
-
inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
|
|
463
|
-
})
|
|
464
|
-
|
|
465
|
-
const publisher = await api.resources.publishers.post({
|
|
466
|
-
inputRecord: createJsonApiDocument('publishers', {
|
|
467
|
-
name: 'Publisher'
|
|
468
|
-
}, {
|
|
469
|
-
country: createRelationship({ type: 'countries', id: String(country.id) })
|
|
470
|
-
})
|
|
471
|
-
})
|
|
472
|
-
|
|
473
|
-
const author = await api.resources.authors.post({
|
|
474
|
-
inputRecord: createJsonApiDocument('authors', {
|
|
475
|
-
name: 'Author'
|
|
476
|
-
}, {
|
|
477
|
-
country: createRelationship({ type: 'countries', id: String(country.id) })
|
|
478
|
-
})
|
|
479
|
-
})
|
|
480
|
-
|
|
481
|
-
const book = await api.resources.books.post({
|
|
482
|
-
inputRecord: createJsonApiDocument('books', {
|
|
483
|
-
title: 'Book with Reviews'
|
|
484
|
-
}, {
|
|
485
|
-
publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
|
|
486
|
-
})
|
|
487
|
-
})
|
|
488
|
-
|
|
489
|
-
await api.resources.book_authors.post({
|
|
490
|
-
inputRecord: createJsonApiDocument('book_authors', {}, {
|
|
491
|
-
book: createRelationship({ type: 'books', id: String(book.id) }),
|
|
492
|
-
author: createRelationship({ type: 'authors', id: String(author.id) })
|
|
493
|
-
})
|
|
494
|
-
})
|
|
495
|
-
|
|
496
|
-
// Create reviews
|
|
497
|
-
await api.resources.reviews.post({
|
|
498
|
-
inputRecord: createJsonApiDocument('reviews', {
|
|
499
|
-
content: 'Great book!',
|
|
500
|
-
rating: 5
|
|
501
|
-
}, {
|
|
502
|
-
book: createRelationship({ type: 'books', id: String(book.id) })
|
|
503
|
-
})
|
|
504
|
-
})
|
|
505
|
-
|
|
506
|
-
await api.resources.reviews.post({
|
|
507
|
-
inputRecord: createJsonApiDocument('reviews', {
|
|
508
|
-
content: 'Good read',
|
|
509
|
-
rating: 4
|
|
510
|
-
}, {
|
|
511
|
-
book: createRelationship({ type: 'books', id: String(book.id) })
|
|
512
|
-
})
|
|
513
|
-
})
|
|
514
|
-
})
|
|
515
|
-
|
|
516
|
-
it('should respect sparse fieldsets with nested includes', async () => {
|
|
517
|
-
const result = await api.resources.authors.query({
|
|
518
|
-
queryParams: {
|
|
519
|
-
include: ['books', 'books.reviews'],
|
|
520
|
-
fields: {
|
|
521
|
-
authors: 'name',
|
|
522
|
-
books: 'title',
|
|
523
|
-
reviews: 'rating'
|
|
524
|
-
}
|
|
525
|
-
},
|
|
526
|
-
simplified: false
|
|
527
|
-
})
|
|
528
|
-
|
|
529
|
-
validateJsonApiStructure(result, { isCollection: true })
|
|
530
|
-
|
|
531
|
-
// Check sparse fieldsets are applied
|
|
532
|
-
const author = result.data[0]
|
|
533
|
-
assert.deepEqual(Object.keys(author.attributes), ['name'], 'Author should only have name field')
|
|
534
|
-
|
|
535
|
-
const book = result.included.find(r => r.type === 'books')
|
|
536
|
-
assert(book, 'Should include book')
|
|
537
|
-
// Note: Some internal fields like pages and _relationshipMetadata might still be present
|
|
538
|
-
assert(book.attributes.title, 'Book should have title field')
|
|
539
|
-
assert(!book.attributes.isbn, 'Book should not have isbn field')
|
|
540
|
-
|
|
541
|
-
const review = result.included.find(r => r.type === 'reviews')
|
|
542
|
-
assert(review, 'Should include review')
|
|
543
|
-
assert(review.attributes.rating !== undefined, 'Review should have rating field')
|
|
544
|
-
assert(review.attributes.content === undefined, 'Review should not have content field')
|
|
545
|
-
|
|
546
|
-
// Relationships should still be populated
|
|
547
|
-
assert(book.relationships?.reviews?.data, 'Book should have reviews relationship')
|
|
548
|
-
})
|
|
549
|
-
})
|
|
550
|
-
|
|
551
|
-
describe('Circular Include Handling', () => {
|
|
552
|
-
beforeEach(async () => {
|
|
553
|
-
// Create simple circular relationship data
|
|
554
|
-
const country = await api.resources.countries.post({
|
|
555
|
-
inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
|
|
556
|
-
})
|
|
557
|
-
|
|
558
|
-
const publisher = await api.resources.publishers.post({
|
|
559
|
-
inputRecord: createJsonApiDocument('publishers', {
|
|
560
|
-
name: 'Publisher'
|
|
561
|
-
}, {
|
|
562
|
-
country: createRelationship({ type: 'countries', id: String(country.id) })
|
|
563
|
-
})
|
|
564
|
-
})
|
|
565
|
-
|
|
566
|
-
const author = await api.resources.authors.post({
|
|
567
|
-
inputRecord: createJsonApiDocument('authors', {
|
|
568
|
-
name: 'Author'
|
|
569
|
-
}, {
|
|
570
|
-
country: createRelationship({ type: 'countries', id: String(country.id) })
|
|
571
|
-
})
|
|
572
|
-
})
|
|
573
|
-
|
|
574
|
-
const book = await api.resources.books.post({
|
|
575
|
-
inputRecord: createJsonApiDocument('books', {
|
|
576
|
-
title: 'Book'
|
|
577
|
-
}, {
|
|
578
|
-
publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
|
|
579
|
-
})
|
|
580
|
-
})
|
|
581
|
-
|
|
582
|
-
await api.resources.book_authors.post({
|
|
583
|
-
inputRecord: createJsonApiDocument('book_authors', {}, {
|
|
584
|
-
book: createRelationship({ type: 'books', id: String(book.id) }),
|
|
585
|
-
author: createRelationship({ type: 'authors', id: String(author.id) })
|
|
586
|
-
})
|
|
587
|
-
})
|
|
588
|
-
})
|
|
589
|
-
|
|
590
|
-
it('should handle circular includes gracefully', async () => {
|
|
591
|
-
const result = await api.resources.books.query({
|
|
592
|
-
queryParams: {
|
|
593
|
-
include: ['authors', 'authors.books', 'authors.books.authors']
|
|
594
|
-
},
|
|
595
|
-
simplified: false
|
|
596
|
-
})
|
|
597
|
-
|
|
598
|
-
validateJsonApiStructure(result, { isCollection: true })
|
|
599
|
-
|
|
600
|
-
// Should not cause infinite loops or errors
|
|
601
|
-
const books = result.data.concat(result.included.filter(r => r.type === 'books'))
|
|
602
|
-
const authors = result.included.filter(r => r.type === 'authors')
|
|
603
|
-
|
|
604
|
-
assert(books.length > 0, 'Should have books')
|
|
605
|
-
assert(authors.length > 0, 'Should have authors')
|
|
606
|
-
|
|
607
|
-
// All relationships should be properly populated
|
|
608
|
-
books.forEach(book => {
|
|
609
|
-
assert(book.relationships?.authors?.data, 'Book should have authors relationship')
|
|
610
|
-
})
|
|
611
|
-
|
|
612
|
-
authors.forEach(author => {
|
|
613
|
-
assert(author.relationships?.books?.data, 'Author should have books relationship')
|
|
614
|
-
})
|
|
615
|
-
})
|
|
616
|
-
})
|
|
617
|
-
})
|