json-rest-api 1.0.6 → 1.0.8
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/.claude/agents/library-guide-expert.md +29 -0
- package/.claude/agents/rest-api-internals-expert.md +53 -0
- package/CLAUDE.md +139 -1
- package/README.md +2 -5
- package/docTest.js +238 -0
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
- package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
- package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
- package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
- package/docs/GUIDE/index.md +58 -0
- package/docs/GUIDE.md +61 -2238
- package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
- package/docs/README.md +57 -2239
- package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
- package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
- package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
- package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
- package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
- package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
- package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
- package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
- package/docs/assets/css/style.scss +78 -5
- package/docs/index.md +2 -12
- package/helper-reorganization-plan.md +85 -0
- package/helper-usage-analysis.md +94 -0
- package/package.json +5 -4
- package/plugins/core/bulk-operations-plugin.js +2 -2
- package/plugins/core/connectors/express-plugin.js +99 -33
- package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
- package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
- package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
- package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
- package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
- package/plugins/core/lib/querying/knex-window-queries.js +328 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
- package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
- package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
- package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
- package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
- package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
- package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
- package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
- package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
- package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
- package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
- package/plugins/core/lib/writing/relationship-processor.js +252 -0
- package/plugins/core/rest-api-knex-plugin.js +183 -62
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
- package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
- package/plugins/core/rest-api-plugin-methods/common.js +605 -0
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
- package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
- package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
- package/plugins/core/rest-api-plugin-methods/get.js +187 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
- package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
- package/plugins/core/rest-api-plugin-methods/post.js +157 -0
- package/plugins/core/rest-api-plugin-methods/put.js +257 -0
- package/plugins/core/rest-api-plugin-methods/query.js +208 -0
- package/plugins/core/rest-api-plugin.js +108 -2498
- package/plugins/core/socketio-plugin.js +28 -5
- package/plugins/storage/local-storage.js +2 -2
- package/tests/computed-fields.test.js +483 -0
- package/tests/cors.test.js +4 -4
- package/tests/curl.test.js +5 -4
- package/tests/custom-idproperty-relationships.test.js +891 -0
- package/tests/field-getters.test.js +353 -0
- package/tests/field-setters.test.js +310 -0
- package/tests/fixtures/api-configs.js +800 -28
- package/tests/jwt-auth.test.js +0 -2
- package/tests/nested-includes.test.js +1 -1
- package/tests/pagination-cursor-multifield.test.js +514 -0
- package/tests/pagination-enhanced.test.js +12 -9
- package/tests/polymorphic-relationships.test.js +10 -8
- package/tests/polymorphic-simple.test.js +6 -4
- package/tests/positioning-advanced.test.js +12 -12
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +24 -24
- package/tests/positioning.test.js +3 -3
- package/tests/query-limits.test.js +24 -8
- package/tests/relationship-endpoints.test.js +4 -5
- package/tests/relationships.test.js +0 -1
- package/tests/socketio.test.js +12 -1
- package/tests/virtual-fields.test.js +13 -13
- package/PLAN_FOR_WEBSOCKETS.md +0 -398
- package/README_1_Initial_Setup.md +0 -393
- package/README_X_Hooks.md +0 -647
- package/TODO_rest-api-plugin-extras.todo +0 -23
- package/connectors_ideas/express-plugin.js +0 -120
- package/connectors_ideas/http-plugin.js +0 -138
- package/docs/examples/express-plugin.js +0 -366
- package/example.js +0 -301
- package/plugins/core/HELPER_MODULES.md +0 -285
- package/plugins/core/lib/compile-schemas.js +0 -245
- package/plugins/core/lib/database-value-normalizers.js +0 -192
- package/plugins/core/lib/default-data-helpers.js +0 -100
- package/plugins/core/lib/knex-cross-table-search.js +0 -442
- package/plugins/core/lib/knex-json-api-helpers.js +0 -84
- package/plugins/core/lib/knex-json-api-transformers.js +0 -449
- package/plugins/core/lib/knex-pagination-helpers.js +0 -193
- package/plugins/core/lib/knex-window-queries.js +0 -142
- package/plugins/core/lib/many-to-many-manipulations.js +0 -343
- package/plugins/core/lib/relationship-processor.js +0 -278
- package/plugins/core/lib/schema-helpers.js +0 -343
- package/plugins/core/lib/scope-validations.js +0 -252
- package/plugins/core/rest-api-relationships-plugin.js +0 -700
- package/plugins/core/utils/knex-query-helpers-base.js +0 -56
- /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
- /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
- /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
- /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
- /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
- /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
- /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
- /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
- /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
- /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
- /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
- /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
- /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
- /package/{readmeSnippets → docs/README_VARIOUS}/test_put_fields_without_defaults.js +0 -0
|
@@ -0,0 +1,891 @@
|
|
|
1
|
+
import { describe, it, before, beforeEach, after } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import knexLib from 'knex';
|
|
4
|
+
import { createCustomIdPropertyApi } 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 instance that persists across tests
|
|
27
|
+
let api;
|
|
28
|
+
|
|
29
|
+
describe('Custom idProperty Relationship Operations', () => {
|
|
30
|
+
before(async () => {
|
|
31
|
+
// Initialize API with custom idProperty for all resources
|
|
32
|
+
api = await createCustomIdPropertyApi(knex);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
after(async () => {
|
|
36
|
+
await knex.destroy();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('Basic CRUD with custom idProperty', () => {
|
|
40
|
+
beforeEach(async () => {
|
|
41
|
+
await cleanTables(knex, [
|
|
42
|
+
'custom_id_countries', 'custom_id_publishers', 'custom_id_authors',
|
|
43
|
+
'custom_id_books', 'custom_id_book_authors', 'custom_id_reviews'
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should create and retrieve resources with custom ID fields', async () => {
|
|
48
|
+
// Create country
|
|
49
|
+
const countryDoc = createJsonApiDocument('countries', {
|
|
50
|
+
name: 'United States',
|
|
51
|
+
code: 'US'
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const countryResult = await api.resources.countries.post({
|
|
55
|
+
inputRecord: countryDoc
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Verify response has 'id' not 'country_id'
|
|
59
|
+
assert(countryResult.data.id, 'Should have id field');
|
|
60
|
+
assert(!countryResult.data.attributes.country_id, 'Should not expose country_id in attributes');
|
|
61
|
+
|
|
62
|
+
// Verify database has correct column
|
|
63
|
+
const dbCountry = await knex('custom_id_countries').first();
|
|
64
|
+
assert(dbCountry.country_id, 'Database should have country_id column');
|
|
65
|
+
assert.equal(String(dbCountry.country_id), countryResult.data.id);
|
|
66
|
+
|
|
67
|
+
// Test GET
|
|
68
|
+
const getResult = await api.resources.countries.get({
|
|
69
|
+
id: countryResult.data.id
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
assert.equal(getResult.data.id, countryResult.data.id);
|
|
73
|
+
assert.equal(getResult.data.attributes.name, 'United States');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should handle PATCH updates returning full record', async () => {
|
|
77
|
+
const country = await api.resources.countries.post({
|
|
78
|
+
inputRecord: createJsonApiDocument('countries', {
|
|
79
|
+
name: 'United States',
|
|
80
|
+
code: 'US'
|
|
81
|
+
})
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const patchResult = await api.resources.countries.patch({
|
|
85
|
+
inputRecord: {
|
|
86
|
+
data: {
|
|
87
|
+
type: 'countries',
|
|
88
|
+
id: country.data.id,
|
|
89
|
+
attributes: {
|
|
90
|
+
name: 'USA'
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// With returnRecordApi.patch = 'full', we should get the complete record
|
|
97
|
+
validateJsonApiStructure(patchResult, false);
|
|
98
|
+
assert.equal(patchResult.data.attributes.name, 'USA');
|
|
99
|
+
assert.equal(patchResult.data.attributes.code, 'US'); // Original value preserved
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('One-to-One (belongsTo) Relationships with custom idProperty', () => {
|
|
104
|
+
beforeEach(async () => {
|
|
105
|
+
await cleanTables(knex, [
|
|
106
|
+
'custom_id_countries', 'custom_id_publishers', 'custom_id_authors',
|
|
107
|
+
'custom_id_books', 'custom_id_book_authors', 'custom_id_reviews'
|
|
108
|
+
]);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should create resource with belongsTo relationship using custom IDs', async () => {
|
|
112
|
+
// Create a country first
|
|
113
|
+
const countryDoc = createJsonApiDocument('countries', {
|
|
114
|
+
name: 'United States',
|
|
115
|
+
code: 'US'
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const countryResult = await api.resources.countries.post({
|
|
119
|
+
inputRecord: countryDoc
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Create publisher with country relationship
|
|
123
|
+
const publisherDoc = createJsonApiDocument('publishers',
|
|
124
|
+
{
|
|
125
|
+
name: 'Random House'
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
country: createRelationship(resourceIdentifier('countries', countryResult.data.id))
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const publisherResult = await api.resources.publishers.post({
|
|
133
|
+
inputRecord: publisherDoc
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Verify response
|
|
137
|
+
validateJsonApiStructure(publisherResult, false);
|
|
138
|
+
assert.equal(publisherResult.data.type, 'publishers');
|
|
139
|
+
assertResourceRelationship(publisherResult.data, 'country',
|
|
140
|
+
resourceIdentifier('countries', countryResult.data.id));
|
|
141
|
+
|
|
142
|
+
// Verify database foreign key uses country_id
|
|
143
|
+
const dbPublisher = await knex('custom_id_publishers').first();
|
|
144
|
+
assert.equal(String(dbPublisher.country_id), countryResult.data.id);
|
|
145
|
+
|
|
146
|
+
// Verify through GET with include
|
|
147
|
+
const getResult = await api.resources.publishers.get({
|
|
148
|
+
id: publisherResult.data.id,
|
|
149
|
+
queryParams: {
|
|
150
|
+
include: ['country']
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
validateJsonApiStructure(getResult, false);
|
|
155
|
+
assert(getResult.included, 'Should have included data');
|
|
156
|
+
assert.equal(getResult.included.length, 1);
|
|
157
|
+
assert.equal(getResult.included[0].type, 'countries');
|
|
158
|
+
assert.equal(getResult.included[0].attributes.name, 'United States');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should update belongsTo relationship via PATCH', async () => {
|
|
162
|
+
// Create two countries
|
|
163
|
+
const country1 = await api.resources.countries.post({
|
|
164
|
+
inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const country2 = await api.resources.countries.post({
|
|
168
|
+
inputRecord: createJsonApiDocument('countries', { name: 'UK', code: 'GB' })
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Create publisher with first country
|
|
172
|
+
const publisher = await api.resources.publishers.post({
|
|
173
|
+
inputRecord: createJsonApiDocument('publishers',
|
|
174
|
+
{ name: 'Test Publisher' },
|
|
175
|
+
{ country: createRelationship(resourceIdentifier('countries', country1.data.id)) }
|
|
176
|
+
)
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Update to second country
|
|
180
|
+
const patchDoc = {
|
|
181
|
+
data: {
|
|
182
|
+
type: 'publishers',
|
|
183
|
+
id: publisher.data.id,
|
|
184
|
+
relationships: {
|
|
185
|
+
country: createRelationship(resourceIdentifier('countries', country2.data.id))
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const patchResult = await api.resources.publishers.patch({
|
|
191
|
+
inputRecord: patchDoc
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Verify we get full record back
|
|
195
|
+
assert.equal(patchResult.data.attributes.name, 'Test Publisher');
|
|
196
|
+
assertResourceRelationship(patchResult.data, 'country',
|
|
197
|
+
resourceIdentifier('countries', country2.data.id));
|
|
198
|
+
|
|
199
|
+
// Verify update via GET with include
|
|
200
|
+
const updated = await api.resources.publishers.get({
|
|
201
|
+
id: publisher.data.id,
|
|
202
|
+
queryParams: { include: ['country'] }
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
assert.equal(updated.included[0].id, country2.data.id);
|
|
206
|
+
assert.equal(updated.included[0].attributes.name, 'UK');
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
describe('Many-to-Many Relationships with custom idProperty', () => {
|
|
211
|
+
let countryId;
|
|
212
|
+
let publisherId;
|
|
213
|
+
let author1Id;
|
|
214
|
+
let author2Id;
|
|
215
|
+
let bookId;
|
|
216
|
+
|
|
217
|
+
beforeEach(async () => {
|
|
218
|
+
await cleanTables(knex, [
|
|
219
|
+
'custom_id_countries', 'custom_id_publishers', 'custom_id_authors',
|
|
220
|
+
'custom_id_books', 'custom_id_book_authors', 'custom_id_reviews'
|
|
221
|
+
]);
|
|
222
|
+
|
|
223
|
+
// Setup test data
|
|
224
|
+
const country = await api.resources.countries.post({
|
|
225
|
+
inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
|
|
226
|
+
});
|
|
227
|
+
countryId = country.data.id;
|
|
228
|
+
|
|
229
|
+
const publisher = await api.resources.publishers.post({
|
|
230
|
+
inputRecord: createJsonApiDocument('publishers',
|
|
231
|
+
{ name: 'Test Publisher' },
|
|
232
|
+
{ country: createRelationship(resourceIdentifier('countries', countryId)) }
|
|
233
|
+
)
|
|
234
|
+
});
|
|
235
|
+
publisherId = publisher.data.id;
|
|
236
|
+
|
|
237
|
+
const author1 = await api.resources.authors.post({
|
|
238
|
+
inputRecord: createJsonApiDocument('authors', { name: 'Author One' })
|
|
239
|
+
});
|
|
240
|
+
author1Id = author1.data.id;
|
|
241
|
+
|
|
242
|
+
const author2 = await api.resources.authors.post({
|
|
243
|
+
inputRecord: createJsonApiDocument('authors', { name: 'Author Two' })
|
|
244
|
+
});
|
|
245
|
+
author2Id = author2.data.id;
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('should create many-to-many relationships with custom IDs', async () => {
|
|
249
|
+
// Create book with multiple authors
|
|
250
|
+
const bookDoc = createJsonApiDocument('books',
|
|
251
|
+
{ title: 'Test Book' },
|
|
252
|
+
{
|
|
253
|
+
country: createRelationship(resourceIdentifier('countries', countryId)),
|
|
254
|
+
publisher: createRelationship(resourceIdentifier('publishers', publisherId)),
|
|
255
|
+
authors: createToManyRelationship([
|
|
256
|
+
resourceIdentifier('authors', author1Id),
|
|
257
|
+
resourceIdentifier('authors', author2Id)
|
|
258
|
+
])
|
|
259
|
+
}
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const bookResult = await api.resources.books.post({
|
|
263
|
+
inputRecord: bookDoc
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
bookId = bookResult.data.id;
|
|
267
|
+
|
|
268
|
+
// Verify pivot table has correct foreign keys
|
|
269
|
+
const pivotRecords = await knex('custom_id_book_authors').select('*');
|
|
270
|
+
assert.equal(pivotRecords.length, 2);
|
|
271
|
+
|
|
272
|
+
// Check that book_id and author_id columns contain the right values
|
|
273
|
+
const bookIds = pivotRecords.map(r => r.book_id);
|
|
274
|
+
const authorIds = pivotRecords.map(r => r.author_id).sort();
|
|
275
|
+
|
|
276
|
+
assert(bookIds.every(id => String(id) === bookId));
|
|
277
|
+
assert.deepEqual(authorIds.map(String).sort(), [author1Id, author2Id].sort());
|
|
278
|
+
|
|
279
|
+
// Verify through GET with include
|
|
280
|
+
const getResult = await api.resources.books.get({
|
|
281
|
+
id: bookId,
|
|
282
|
+
queryParams: { include: ['authors'] }
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
assert(getResult.included);
|
|
286
|
+
assert.equal(getResult.included.length, 2);
|
|
287
|
+
const includedNames = getResult.included.map(a => a.attributes.name).sort();
|
|
288
|
+
assert.deepEqual(includedNames, ['Author One', 'Author Two']);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should handle PATCH updates to many-to-many relationships', async () => {
|
|
292
|
+
// Create book with one author
|
|
293
|
+
const book = await api.resources.books.post({
|
|
294
|
+
inputRecord: createJsonApiDocument('books',
|
|
295
|
+
{ title: 'Test Book' },
|
|
296
|
+
{
|
|
297
|
+
country: createRelationship(resourceIdentifier('countries', countryId)),
|
|
298
|
+
authors: createToManyRelationship([resourceIdentifier('authors', author1Id)])
|
|
299
|
+
}
|
|
300
|
+
)
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Update to have both authors
|
|
304
|
+
const patchDoc = {
|
|
305
|
+
data: {
|
|
306
|
+
type: 'books',
|
|
307
|
+
id: book.data.id,
|
|
308
|
+
relationships: {
|
|
309
|
+
authors: createToManyRelationship([
|
|
310
|
+
resourceIdentifier('authors', author1Id),
|
|
311
|
+
resourceIdentifier('authors', author2Id)
|
|
312
|
+
])
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const patchResult = await api.resources.books.patch({
|
|
318
|
+
inputRecord: patchDoc,
|
|
319
|
+
queryParams: {
|
|
320
|
+
include: ['authors']
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
// Verify full record returned
|
|
325
|
+
assert.equal(patchResult.data.attributes.title, 'Test Book');
|
|
326
|
+
// With include, we should get the authors relationship data
|
|
327
|
+
assertResourceRelationship(patchResult.data, 'authors',
|
|
328
|
+
[resourceIdentifier('authors', author1Id), resourceIdentifier('authors', author2Id)]);
|
|
329
|
+
|
|
330
|
+
// Verify update
|
|
331
|
+
const updated = await api.resources.books.get({
|
|
332
|
+
id: book.data.id,
|
|
333
|
+
queryParams: { include: ['authors'] }
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
assert.equal(updated.included.length, 2);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
describe('Polymorphic Relationships with custom idProperty', () => {
|
|
341
|
+
let bookId;
|
|
342
|
+
let authorId;
|
|
343
|
+
let publisherId;
|
|
344
|
+
|
|
345
|
+
beforeEach(async () => {
|
|
346
|
+
await cleanTables(knex, [
|
|
347
|
+
'custom_id_countries', 'custom_id_publishers', 'custom_id_authors',
|
|
348
|
+
'custom_id_books', 'custom_id_book_authors', 'custom_id_reviews'
|
|
349
|
+
]);
|
|
350
|
+
|
|
351
|
+
// Create test resources
|
|
352
|
+
const country = await api.resources.countries.post({
|
|
353
|
+
inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
const book = await api.resources.books.post({
|
|
357
|
+
inputRecord: createJsonApiDocument('books',
|
|
358
|
+
{ title: 'Test Book' },
|
|
359
|
+
{ country: createRelationship(resourceIdentifier('countries', country.data.id)) }
|
|
360
|
+
)
|
|
361
|
+
});
|
|
362
|
+
bookId = book.data.id;
|
|
363
|
+
|
|
364
|
+
const author = await api.resources.authors.post({
|
|
365
|
+
inputRecord: createJsonApiDocument('authors', { name: 'Test Author' })
|
|
366
|
+
});
|
|
367
|
+
authorId = author.data.id;
|
|
368
|
+
|
|
369
|
+
const publisher = await api.resources.publishers.post({
|
|
370
|
+
inputRecord: createJsonApiDocument('publishers', { name: 'Test Publisher' })
|
|
371
|
+
});
|
|
372
|
+
publisherId = publisher.data.id;
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should create polymorphic review for book with custom IDs', async () => {
|
|
376
|
+
const review = await api.resources.reviews.post({
|
|
377
|
+
inputRecord: createJsonApiDocument('reviews', {
|
|
378
|
+
rating: 5,
|
|
379
|
+
title: 'Great book!',
|
|
380
|
+
content: 'Excellent read.',
|
|
381
|
+
reviewer_name: 'John Doe',
|
|
382
|
+
reviewable_type: 'books',
|
|
383
|
+
reviewable_id: bookId
|
|
384
|
+
})
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
validateJsonApiStructure(review);
|
|
388
|
+
assert.equal(review.data.attributes.reviewable_type, 'books');
|
|
389
|
+
assert.equal(String(review.data.attributes.reviewable_id), bookId);
|
|
390
|
+
|
|
391
|
+
// Verify database
|
|
392
|
+
const dbReview = await knex('custom_id_reviews').first();
|
|
393
|
+
assert.equal(dbReview.reviewable_type, 'books');
|
|
394
|
+
assert.equal(String(dbReview.reviewable_id), bookId);
|
|
395
|
+
assert(dbReview.review_id, 'Should have custom review_id column');
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('should create polymorphic review for author with custom IDs', async () => {
|
|
399
|
+
const review = await api.resources.reviews.post({
|
|
400
|
+
inputRecord: createJsonApiDocument('reviews', {
|
|
401
|
+
rating: 4,
|
|
402
|
+
title: 'Talented author',
|
|
403
|
+
content: 'Great writing style.',
|
|
404
|
+
reviewer_name: 'Jane Doe',
|
|
405
|
+
reviewable_type: 'authors',
|
|
406
|
+
reviewable_id: authorId
|
|
407
|
+
})
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
assert.equal(review.data.attributes.reviewable_type, 'authors');
|
|
411
|
+
assert.equal(String(review.data.attributes.reviewable_id), authorId);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('should query polymorphic reviews by type', async () => {
|
|
415
|
+
// Create reviews for different types
|
|
416
|
+
await api.resources.reviews.post({
|
|
417
|
+
inputRecord: createJsonApiDocument('reviews', {
|
|
418
|
+
rating: 5,
|
|
419
|
+
title: 'Book review',
|
|
420
|
+
content: 'Great book',
|
|
421
|
+
reviewer_name: 'Reader',
|
|
422
|
+
reviewable_type: 'books',
|
|
423
|
+
reviewable_id: bookId
|
|
424
|
+
})
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
await api.resources.reviews.post({
|
|
428
|
+
inputRecord: createJsonApiDocument('reviews', {
|
|
429
|
+
rating: 4,
|
|
430
|
+
title: 'Author review',
|
|
431
|
+
content: 'Great author',
|
|
432
|
+
reviewer_name: 'Fan',
|
|
433
|
+
reviewable_type: 'authors',
|
|
434
|
+
reviewable_id: authorId
|
|
435
|
+
})
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// Query only book reviews
|
|
439
|
+
const bookReviews = await api.resources.reviews.query({
|
|
440
|
+
queryParams: {
|
|
441
|
+
filters: { reviewable_type: 'books' }
|
|
442
|
+
}
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
debugger
|
|
446
|
+
assert.equal(bookReviews.data.length, 1, `Expected 1 book review, got ${bookReviews.data.length}`);
|
|
447
|
+
assert.equal(bookReviews.data[0].attributes.reviewable_type, 'books');
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
describe('Nested Includes with custom idProperty', () => {
|
|
452
|
+
beforeEach(async () => {
|
|
453
|
+
await cleanTables(knex, [
|
|
454
|
+
'custom_id_countries', 'custom_id_publishers', 'custom_id_authors',
|
|
455
|
+
'custom_id_books', 'custom_id_book_authors', 'custom_id_reviews'
|
|
456
|
+
]);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('should handle nested includes through custom ID relationships', async () => {
|
|
460
|
+
// Create hierarchy: Country -> Publisher -> Book -> Authors
|
|
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: 'Test Publisher' },
|
|
468
|
+
{ country: createRelationship(resourceIdentifier('countries', country.data.id)) }
|
|
469
|
+
)
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const author = await api.resources.authors.post({
|
|
473
|
+
inputRecord: createJsonApiDocument('authors', { name: 'Test Author' })
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
const book = await api.resources.books.post({
|
|
477
|
+
inputRecord: createJsonApiDocument('books',
|
|
478
|
+
{ title: 'Test Book' },
|
|
479
|
+
{
|
|
480
|
+
country: createRelationship(resourceIdentifier('countries', country.data.id)),
|
|
481
|
+
publisher: createRelationship(resourceIdentifier('publishers', publisher.data.id)),
|
|
482
|
+
authors: createToManyRelationship([resourceIdentifier('authors', author.data.id)])
|
|
483
|
+
}
|
|
484
|
+
)
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// Query with nested includes
|
|
488
|
+
const result = await api.resources.books.query({
|
|
489
|
+
queryParams: {
|
|
490
|
+
include: ['publisher.country', 'authors']
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
validateJsonApiStructure(result, true);
|
|
495
|
+
assert(result.included);
|
|
496
|
+
|
|
497
|
+
// Find each type in included
|
|
498
|
+
const includedPublisher = result.included.find(r => r.type === 'publishers');
|
|
499
|
+
const includedCountry = result.included.find(r => r.type === 'countries');
|
|
500
|
+
const includedAuthor = result.included.find(r => r.type === 'authors');
|
|
501
|
+
|
|
502
|
+
assert(includedPublisher, 'Should include publisher');
|
|
503
|
+
assert(includedCountry, 'Should include country');
|
|
504
|
+
assert(includedAuthor, 'Should include author');
|
|
505
|
+
|
|
506
|
+
// Verify nested relationship
|
|
507
|
+
assert.equal(includedPublisher.relationships.country.data.id, country.data.id);
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it('should handle 3+ levels of nested includes', async () => {
|
|
511
|
+
// Create deeper hierarchy
|
|
512
|
+
const country = await api.resources.countries.post({
|
|
513
|
+
inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
const author = await api.resources.authors.post({
|
|
517
|
+
inputRecord: createJsonApiDocument('authors',
|
|
518
|
+
{ name: 'Test Author' },
|
|
519
|
+
{ country: createRelationship(resourceIdentifier('countries', country.data.id)) }
|
|
520
|
+
)
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
const publisher = await api.resources.publishers.post({
|
|
524
|
+
inputRecord: createJsonApiDocument('publishers',
|
|
525
|
+
{ name: 'Test Publisher' },
|
|
526
|
+
{ country: createRelationship(resourceIdentifier('countries', country.data.id)) }
|
|
527
|
+
)
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const book = await api.resources.books.post({
|
|
531
|
+
inputRecord: createJsonApiDocument('books',
|
|
532
|
+
{ title: 'Test Book' },
|
|
533
|
+
{
|
|
534
|
+
country: createRelationship(resourceIdentifier('countries', country.data.id)),
|
|
535
|
+
publisher: createRelationship(resourceIdentifier('publishers', publisher.data.id)),
|
|
536
|
+
authors: createToManyRelationship([resourceIdentifier('authors', author.data.id)])
|
|
537
|
+
}
|
|
538
|
+
)
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// Query with 3 levels: books -> authors -> country
|
|
542
|
+
const result = await api.resources.books.query({
|
|
543
|
+
queryParams: {
|
|
544
|
+
include: ['authors.country', 'publisher.country']
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
validateJsonApiStructure(result, true);
|
|
549
|
+
assert(result.included);
|
|
550
|
+
|
|
551
|
+
const types = result.included.map(r => r.type);
|
|
552
|
+
assert(types.includes('authors'), 'Should include authors');
|
|
553
|
+
assert(types.includes('publishers'), 'Should include publishers');
|
|
554
|
+
assert(types.includes('countries'), 'Should include countries');
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
describe('Include with Sparse Fieldsets and custom idProperty', () => {
|
|
559
|
+
let testData = {};
|
|
560
|
+
|
|
561
|
+
beforeEach(async () => {
|
|
562
|
+
await cleanTables(knex, [
|
|
563
|
+
'custom_id_countries', 'custom_id_publishers', 'custom_id_authors',
|
|
564
|
+
'custom_id_books', 'custom_id_book_authors', 'custom_id_reviews'
|
|
565
|
+
]);
|
|
566
|
+
|
|
567
|
+
// Create test data
|
|
568
|
+
const country = await api.resources.countries.post({
|
|
569
|
+
inputRecord: createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' })
|
|
570
|
+
});
|
|
571
|
+
testData.country = country.data;
|
|
572
|
+
|
|
573
|
+
const publisher = await api.resources.publishers.post({
|
|
574
|
+
inputRecord: createJsonApiDocument('publishers',
|
|
575
|
+
{ name: 'Test Publisher' },
|
|
576
|
+
{ country: createRelationship(resourceIdentifier('countries', testData.country.id)) }
|
|
577
|
+
)
|
|
578
|
+
});
|
|
579
|
+
testData.publisher = publisher.data;
|
|
580
|
+
|
|
581
|
+
const author = await api.resources.authors.post({
|
|
582
|
+
inputRecord: createJsonApiDocument('authors', { name: 'Test Author', biography: 'A great author' })
|
|
583
|
+
});
|
|
584
|
+
testData.author = author.data;
|
|
585
|
+
|
|
586
|
+
const book = await api.resources.books.post({
|
|
587
|
+
inputRecord: createJsonApiDocument('books',
|
|
588
|
+
{ title: 'Test Book', isbn: '1234567890', pages: 300 },
|
|
589
|
+
{
|
|
590
|
+
country: createRelationship(resourceIdentifier('countries', testData.country.id)),
|
|
591
|
+
publisher: createRelationship(resourceIdentifier('publishers', testData.publisher.id)),
|
|
592
|
+
authors: createToManyRelationship([resourceIdentifier('authors', testData.author.id)])
|
|
593
|
+
}
|
|
594
|
+
)
|
|
595
|
+
});
|
|
596
|
+
testData.book = book.data;
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
it('should combine sparse fieldsets with includes using custom IDs', async () => {
|
|
600
|
+
const result = await api.resources.books.get({
|
|
601
|
+
id: testData.book.id,
|
|
602
|
+
queryParams: {
|
|
603
|
+
include: ['publisher', 'authors'],
|
|
604
|
+
fields: {
|
|
605
|
+
books: 'title',
|
|
606
|
+
publishers: 'name',
|
|
607
|
+
authors: 'name' // Exclude biography
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
// Check main resource has only requested fields
|
|
613
|
+
assert(result.data.attributes.title);
|
|
614
|
+
assert(!result.data.attributes.isbn);
|
|
615
|
+
assert(!result.data.attributes.pages);
|
|
616
|
+
|
|
617
|
+
// Check included resources have only requested fields
|
|
618
|
+
const publisher = result.included.find(r => r.type === 'publishers');
|
|
619
|
+
assert(publisher.attributes.name);
|
|
620
|
+
assert(publisher.id); // ID is always included
|
|
621
|
+
|
|
622
|
+
const author = result.included.find(r => r.type === 'authors');
|
|
623
|
+
assert(author.attributes.name);
|
|
624
|
+
assert(!author.attributes.biography);
|
|
625
|
+
assert(author.id);
|
|
626
|
+
});
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
describe('Include Depth Validation with custom idProperty', () => {
|
|
630
|
+
beforeEach(async () => {
|
|
631
|
+
await cleanTables(knex, [
|
|
632
|
+
'custom_id_countries', 'custom_id_publishers', 'custom_id_authors',
|
|
633
|
+
'custom_id_books', 'custom_id_book_authors', 'custom_id_reviews'
|
|
634
|
+
]);
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
it('should enforce include depth limits', async () => {
|
|
638
|
+
// Create deep hierarchy
|
|
639
|
+
const country = await api.resources.countries.post({
|
|
640
|
+
inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
const publisher = await api.resources.publishers.post({
|
|
644
|
+
inputRecord: createJsonApiDocument('publishers',
|
|
645
|
+
{ name: 'Test Publisher' },
|
|
646
|
+
{ country: createRelationship(resourceIdentifier('countries', country.data.id)) }
|
|
647
|
+
)
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
const author = await api.resources.authors.post({
|
|
651
|
+
inputRecord: createJsonApiDocument('authors',
|
|
652
|
+
{ name: 'Test Author' },
|
|
653
|
+
{ country: createRelationship(resourceIdentifier('countries', country.data.id)) }
|
|
654
|
+
)
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
const book = await api.resources.books.post({
|
|
658
|
+
inputRecord: createJsonApiDocument('books',
|
|
659
|
+
{ title: 'Test Book' },
|
|
660
|
+
{
|
|
661
|
+
country: createRelationship(resourceIdentifier('countries', country.data.id)),
|
|
662
|
+
publisher: createRelationship(resourceIdentifier('publishers', publisher.data.id)),
|
|
663
|
+
authors: createToManyRelationship([resourceIdentifier('authors', author.data.id)])
|
|
664
|
+
}
|
|
665
|
+
)
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
// Test depth limit (default is 3)
|
|
669
|
+
// This should work: books -> authors -> country (depth 2)
|
|
670
|
+
const validDepth = await api.resources.books.query({
|
|
671
|
+
queryParams: {
|
|
672
|
+
include: ['authors.country']
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
assert(validDepth.data);
|
|
676
|
+
|
|
677
|
+
// This should fail: trying to go too deep (if we had deeper relationships)
|
|
678
|
+
// Since our test data doesn't have 4+ levels, we test that 3 levels work
|
|
679
|
+
const maxDepth = await api.resources.books.query({
|
|
680
|
+
queryParams: {
|
|
681
|
+
include: ['publisher.country', 'authors.country']
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
assert(maxDepth.data);
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
describe('Complex Scenarios with custom idProperty', () => {
|
|
689
|
+
beforeEach(async () => {
|
|
690
|
+
await cleanTables(knex, [
|
|
691
|
+
'custom_id_countries', 'custom_id_publishers', 'custom_id_authors',
|
|
692
|
+
'custom_id_books', 'custom_id_book_authors', 'custom_id_reviews'
|
|
693
|
+
]);
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
it('should handle complex queries with filters, includes, and custom IDs', async () => {
|
|
697
|
+
// Create multiple countries
|
|
698
|
+
const usa = await api.resources.countries.post({
|
|
699
|
+
inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
const uk = await api.resources.countries.post({
|
|
703
|
+
inputRecord: createJsonApiDocument('countries', { name: 'UK', code: 'GB' })
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
// Create publishers in different countries
|
|
707
|
+
const usPublisher = await api.resources.publishers.post({
|
|
708
|
+
inputRecord: createJsonApiDocument('publishers',
|
|
709
|
+
{ name: 'US Publisher' },
|
|
710
|
+
{ country: createRelationship(resourceIdentifier('countries', usa.data.id)) }
|
|
711
|
+
)
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
const ukPublisher = await api.resources.publishers.post({
|
|
715
|
+
inputRecord: createJsonApiDocument('publishers',
|
|
716
|
+
{ name: 'UK Publisher' },
|
|
717
|
+
{ country: createRelationship(resourceIdentifier('countries', uk.data.id)) }
|
|
718
|
+
)
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
// Create authors
|
|
722
|
+
const author1 = await api.resources.authors.post({
|
|
723
|
+
inputRecord: createJsonApiDocument('authors', { name: 'Author One' })
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
const author2 = await api.resources.authors.post({
|
|
727
|
+
inputRecord: createJsonApiDocument('authors', { name: 'Author Two' })
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
// Create books
|
|
731
|
+
await api.resources.books.post({
|
|
732
|
+
inputRecord: createJsonApiDocument('books',
|
|
733
|
+
{ title: 'US Book 1' },
|
|
734
|
+
{
|
|
735
|
+
country: createRelationship(resourceIdentifier('countries', usa.data.id)),
|
|
736
|
+
publisher: createRelationship(resourceIdentifier('publishers', usPublisher.data.id)),
|
|
737
|
+
authors: createToManyRelationship([resourceIdentifier('authors', author1.data.id)])
|
|
738
|
+
}
|
|
739
|
+
)
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
await api.resources.books.post({
|
|
743
|
+
inputRecord: createJsonApiDocument('books',
|
|
744
|
+
{ title: 'UK Book 1' },
|
|
745
|
+
{
|
|
746
|
+
country: createRelationship(resourceIdentifier('countries', uk.data.id)),
|
|
747
|
+
publisher: createRelationship(resourceIdentifier('publishers', ukPublisher.data.id)),
|
|
748
|
+
authors: createToManyRelationship([
|
|
749
|
+
resourceIdentifier('authors', author1.data.id),
|
|
750
|
+
resourceIdentifier('authors', author2.data.id)
|
|
751
|
+
])
|
|
752
|
+
}
|
|
753
|
+
)
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
// Complex query: US books with their publishers and authors
|
|
757
|
+
const result = await api.resources.books.query({
|
|
758
|
+
queryParams: {
|
|
759
|
+
filters: { country_id: usa.data.id },
|
|
760
|
+
include: ['publisher.country', 'authors'],
|
|
761
|
+
sort: ['title']
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
assert.equal(result.data.length, 1);
|
|
766
|
+
assert.equal(result.data[0].attributes.title, 'US Book 1');
|
|
767
|
+
|
|
768
|
+
// Verify includes
|
|
769
|
+
const includedTypes = result.included.map(r => r.type);
|
|
770
|
+
assert(includedTypes.includes('publishers'));
|
|
771
|
+
assert(includedTypes.includes('countries'));
|
|
772
|
+
assert(includedTypes.includes('authors'));
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it('should handle bulk operations with custom IDs', async () => {
|
|
776
|
+
const country = await api.resources.countries.post({
|
|
777
|
+
inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
// Create multiple authors
|
|
781
|
+
const authors = [];
|
|
782
|
+
for (let i = 1; i <= 3; i++) {
|
|
783
|
+
const author = await api.resources.authors.post({
|
|
784
|
+
inputRecord: createJsonApiDocument('authors', { name: `Author ${i}` })
|
|
785
|
+
});
|
|
786
|
+
authors.push(author.data);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Create book with all authors
|
|
790
|
+
const book = await api.resources.books.post({
|
|
791
|
+
inputRecord: createJsonApiDocument('books',
|
|
792
|
+
{ title: 'Multi-author Book' },
|
|
793
|
+
{
|
|
794
|
+
country: createRelationship(resourceIdentifier('countries', country.data.id)),
|
|
795
|
+
authors: createToManyRelationship(
|
|
796
|
+
authors.map(a => resourceIdentifier('authors', a.id))
|
|
797
|
+
)
|
|
798
|
+
}
|
|
799
|
+
)
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
// Verify all relationships were created
|
|
803
|
+
const bookWithAuthors = await api.resources.books.get({
|
|
804
|
+
id: book.data.id,
|
|
805
|
+
queryParams: { include: ['authors'] }
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
assert.equal(bookWithAuthors.included.length, 3);
|
|
809
|
+
|
|
810
|
+
// Verify pivot table
|
|
811
|
+
const pivotCount = await knex('custom_id_book_authors').count('* as count').first();
|
|
812
|
+
assert.equal(pivotCount.count, 3);
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
it('should handle DELETE operations with custom IDs', async () => {
|
|
816
|
+
const country = await api.resources.countries.post({
|
|
817
|
+
inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
const author = await api.resources.authors.post({
|
|
821
|
+
inputRecord: createJsonApiDocument('authors', { name: 'Test Author' })
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
const book = await api.resources.books.post({
|
|
825
|
+
inputRecord: createJsonApiDocument('books',
|
|
826
|
+
{ title: 'Test Book' },
|
|
827
|
+
{
|
|
828
|
+
country: createRelationship(resourceIdentifier('countries', country.data.id)),
|
|
829
|
+
authors: createToManyRelationship([resourceIdentifier('authors', author.data.id)])
|
|
830
|
+
}
|
|
831
|
+
)
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
// Delete the book
|
|
835
|
+
await api.resources.books.delete({ id: book.data.id });
|
|
836
|
+
|
|
837
|
+
// Verify book is deleted
|
|
838
|
+
try {
|
|
839
|
+
await api.resources.books.get({ id: book.data.id });
|
|
840
|
+
assert.fail('Should have thrown error');
|
|
841
|
+
} catch (error) {
|
|
842
|
+
assert(error.message.includes('not found') || error.message.includes('404'));
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// The REST API correctly does NOT cascade delete pivot records
|
|
846
|
+
// This is intentional - automatic cascade deletion would be dangerous
|
|
847
|
+
// Pivot records remain and should be cleaned up by application logic if needed
|
|
848
|
+
const pivotCount = await knex('custom_id_book_authors').count('* as count').first();
|
|
849
|
+
assert.equal(pivotCount.count, 1, 'Pivot record should still exist');
|
|
850
|
+
|
|
851
|
+
// Verify author still exists
|
|
852
|
+
const authorStillExists = await api.resources.authors.get({ id: author.data.id });
|
|
853
|
+
assert(authorStillExists.data);
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
it('should handle PUT operations with custom IDs', async () => {
|
|
857
|
+
const country = await api.resources.countries.post({
|
|
858
|
+
inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
const publisher = await api.resources.publishers.post({
|
|
862
|
+
inputRecord: createJsonApiDocument('publishers',
|
|
863
|
+
{ name: 'Original Publisher' },
|
|
864
|
+
{ country: createRelationship(resourceIdentifier('countries', country.data.id)) }
|
|
865
|
+
)
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
// PUT to completely replace the publisher
|
|
869
|
+
const putResult = await api.resources.publishers.put({
|
|
870
|
+
inputRecord: {
|
|
871
|
+
data: {
|
|
872
|
+
type: 'publishers',
|
|
873
|
+
id: publisher.data.id,
|
|
874
|
+
attributes: {
|
|
875
|
+
name: 'Replaced Publisher'
|
|
876
|
+
},
|
|
877
|
+
relationships: {
|
|
878
|
+
country: createRelationship(resourceIdentifier('countries', country.data.id))
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
// Verify full record returned
|
|
885
|
+
validateJsonApiStructure(putResult, false);
|
|
886
|
+
assert.equal(putResult.data.attributes.name, 'Replaced Publisher');
|
|
887
|
+
assertResourceRelationship(putResult.data, 'country',
|
|
888
|
+
resourceIdentifier('countries', country.data.id));
|
|
889
|
+
});
|
|
890
|
+
});
|
|
891
|
+
});
|