json-rest-api 1.0.13 → 1.0.15

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.
Files changed (118) hide show
  1. package/connectors_ideas/fastify.js +66 -68
  2. package/connectors_ideas/koa-plugin.js +50 -51
  3. package/eslint.config.js +13 -0
  4. package/examples/url-override-example.js +63 -63
  5. package/index.js +13 -13
  6. package/lib/enhanced-logger.js +51 -51
  7. package/lib/error-formatter.js +61 -61
  8. package/lib/rest-api-errors.js +28 -28
  9. package/package.json +9 -5
  10. package/plugins/core/bulk-operations-plugin.js +142 -145
  11. package/plugins/core/connectors/express-plugin.js +210 -217
  12. package/plugins/core/connectors/lib/busboy-detector.js +84 -84
  13. package/plugins/core/connectors/lib/formidable-detector.js +50 -50
  14. package/plugins/core/connectors/lib/request-helpers.js +74 -74
  15. package/plugins/core/file-handling-plugin.js +125 -125
  16. package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
  17. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
  18. package/plugins/core/lib/anyapi/schema-utils.js +101 -101
  19. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
  20. package/plugins/core/lib/dbTablesOperations.js +134 -135
  21. package/plugins/core/lib/fractional-positioning.js +74 -76
  22. package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
  23. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
  24. package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
  25. package/plugins/core/lib/querying/knex-process-includes.js +50 -50
  26. package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
  27. package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
  28. package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
  29. package/plugins/core/lib/querying/knex-window-queries.js +89 -89
  30. package/plugins/core/lib/querying/url-helpers.js +35 -35
  31. package/plugins/core/lib/querying-writing/compile-schemas.js +97 -98
  32. package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
  33. package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
  34. package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
  35. package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
  36. package/plugins/core/lib/querying-writing/field-utils.js +44 -42
  37. package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
  38. package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
  39. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
  40. package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
  41. package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
  42. package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
  43. package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
  44. package/plugins/core/lib/storage/storage-adapter.js +68 -68
  45. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
  46. package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
  47. package/plugins/core/lib/writing/relationship-processor.js +58 -58
  48. package/plugins/core/rest-api-access.js +8 -8
  49. package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
  50. package/plugins/core/rest-api-cors-plugin.js +170 -171
  51. package/plugins/core/rest-api-knex-plugin.js +482 -493
  52. package/plugins/core/rest-api-multihome-plugin.js +247 -251
  53. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
  54. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
  55. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
  56. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
  57. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
  58. package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
  59. package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
  60. package/plugins/core/rest-api-plugin-methods/common.js +295 -297
  61. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
  62. package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
  63. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
  64. package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
  65. package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
  66. package/plugins/core/rest-api-plugin-methods/get.js +86 -86
  67. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
  68. package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
  69. package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
  70. package/plugins/core/rest-api-plugin-methods/post.js +61 -62
  71. package/plugins/core/rest-api-plugin-methods/put.js +86 -87
  72. package/plugins/core/rest-api-plugin-methods/query.js +88 -88
  73. package/plugins/core/rest-api-plugin-methods/release.js +5 -6
  74. package/plugins/core/rest-api-plugin.js +76 -78
  75. package/plugins/core/rest-api-positioning-plugin.js +251 -252
  76. package/plugins/core/socketio-plugin.js +240 -236
  77. package/plugins/storage/local-storage.js +103 -103
  78. package/plugins/storage/s3-storage.js +41 -42
  79. package/quickTest.js +37 -37
  80. package/scripts/debug-anyapi-review-filter.mjs +18 -18
  81. package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
  82. package/scripts/inspect-anyapi-descriptor.mjs +10 -10
  83. package/scripts/run-docs.js +112 -113
  84. package/test-debug.js +28 -28
  85. package/tests/TEST_TEMPLATE.test.js +51 -51
  86. package/tests/anyapi-basic.test.js +42 -42
  87. package/tests/anyapi-cursor-pagination.test.js +48 -48
  88. package/tests/bulk-operations.test.js +208 -208
  89. package/tests/computed-fields.test.js +204 -204
  90. package/tests/cors.test.js +412 -413
  91. package/tests/curl.test.js +275 -276
  92. package/tests/custom-idproperty-relationships.test.js +266 -266
  93. package/tests/field-getters.test.js +132 -132
  94. package/tests/field-setters.test.js +110 -110
  95. package/tests/fixtures/api-configs.js +699 -699
  96. package/tests/helpers/storage-mode.js +32 -32
  97. package/tests/helpers/test-utils.js +76 -76
  98. package/tests/include-depth-validation.test.js +76 -76
  99. package/tests/includes.test.js +145 -145
  100. package/tests/multihome.test.js +175 -175
  101. package/tests/nested-includes.test.js +255 -255
  102. package/tests/pagination-cursor-multifield.test.js +164 -164
  103. package/tests/pagination-enhanced.test.js +225 -225
  104. package/tests/polymorphic-relationships.test.js +96 -96
  105. package/tests/polymorphic-simple.test.js +44 -44
  106. package/tests/positioning-advanced.test.js +226 -226
  107. package/tests/positioning-extended.test.js +399 -399
  108. package/tests/positioning-multifilter.test.js +204 -204
  109. package/tests/positioning.test.js +266 -266
  110. package/tests/queries.test.js +184 -184
  111. package/tests/query-limits.test.js +154 -155
  112. package/tests/relationship-endpoints.test.js +127 -127
  113. package/tests/relationships.test.js +178 -179
  114. package/tests/rest-api-access.test.js +86 -86
  115. package/tests/rest-api.test.js +103 -103
  116. package/tests/searchschema-merge.test.js +93 -93
  117. package/tests/socketio.test.js +326 -326
  118. package/tests/virtual-fields.test.js +164 -164
@@ -1,15 +1,15 @@
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';
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
7
  import {
8
8
  cleanTables,
9
9
  createJsonApiDocument,
10
10
  createRelationship,
11
11
  validateJsonApiStructure
12
- } from './helpers/test-utils.js';
12
+ } from './helpers/test-utils.js'
13
13
 
14
14
  // Create Knex instance for tests
15
15
  const knex = knexLib({
@@ -18,26 +18,26 @@ const knex = knexLib({
18
18
  filename: ':memory:'
19
19
  },
20
20
  useNullAsDefault: true
21
- });
21
+ })
22
22
 
23
23
  describe('Nested Include Operations', () => {
24
- let api;
24
+ let api
25
25
 
26
26
  before(async () => {
27
27
  // Create API instance
28
28
  api = new Api({
29
29
  name: 'test-nested-includes'
30
- });
30
+ })
31
31
 
32
32
  await api.use(RestApiPlugin, {
33
33
  queryDefaultLimit: 10,
34
34
  queryMaxLimit: 50,
35
35
  simplified: false
36
- });
36
+ })
37
37
 
38
38
  await api.use(RestApiKnexPlugin, {
39
- knex: knex
40
- });
39
+ knex
40
+ })
41
41
 
42
42
  // Countries
43
43
  await api.addResource('countries', {
@@ -46,8 +46,8 @@ describe('Nested Include Operations', () => {
46
46
  name: { type: 'string', required: true },
47
47
  code: { type: 'string', max: 2 }
48
48
  }
49
- });
50
- await api.resources.countries.createKnexTable();
49
+ })
50
+ await api.resources.countries.createKnexTable()
51
51
 
52
52
  // Publishers
53
53
  await api.addResource('publishers', {
@@ -57,15 +57,15 @@ describe('Nested Include Operations', () => {
57
57
  country_id: { type: 'number', belongsTo: 'countries', as: 'country' }
58
58
  },
59
59
  relationships: {
60
- books: {
60
+ books: {
61
61
  type: 'hasMany',
62
- target: 'books',
62
+ target: 'books',
63
63
  foreignKey: 'publisher_id'
64
64
  // Relationships are always includable via ?include=
65
65
  }
66
66
  }
67
- });
68
- await api.resources.publishers.createKnexTable();
67
+ })
68
+ await api.resources.publishers.createKnexTable()
69
69
 
70
70
  // Authors
71
71
  await api.addResource('authors', {
@@ -75,10 +75,10 @@ describe('Nested Include Operations', () => {
75
75
  country_id: { type: 'number', belongsTo: 'countries', as: 'country' }
76
76
  },
77
77
  relationships: {
78
- books: {
78
+ books: {
79
79
  type: 'manyToMany',
80
- through: 'book_authors',
81
- foreignKey: 'author_id',
80
+ through: 'book_authors',
81
+ foreignKey: 'author_id',
82
82
  otherKey: 'book_id',
83
83
  // Relationships are always includable via ?include=
84
84
  include: {
@@ -87,8 +87,8 @@ describe('Nested Include Operations', () => {
87
87
  }
88
88
  }
89
89
  }
90
- });
91
- await api.resources.authors.createKnexTable();
90
+ })
91
+ await api.resources.authors.createKnexTable()
92
92
 
93
93
  // Books
94
94
  await api.addResource('books', {
@@ -98,22 +98,22 @@ describe('Nested Include Operations', () => {
98
98
  publisher_id: { type: 'number', belongsTo: 'publishers', as: 'publisher' }
99
99
  },
100
100
  relationships: {
101
- authors: {
101
+ authors: {
102
102
  type: 'manyToMany',
103
- through: 'book_authors',
104
- foreignKey: 'book_id',
103
+ through: 'book_authors',
104
+ foreignKey: 'book_id',
105
105
  otherKey: 'author_id'
106
106
  // Relationships are always includable via ?include=
107
107
  },
108
- reviews: {
108
+ reviews: {
109
109
  type: 'hasMany',
110
- target: 'reviews',
110
+ target: 'reviews',
111
111
  foreignKey: 'book_id'
112
112
  // Relationships are always includable via ?include=
113
113
  }
114
114
  }
115
- });
116
- await api.resources.books.createKnexTable();
115
+ })
116
+ await api.resources.books.createKnexTable()
117
117
 
118
118
  // Book-Authors pivot table
119
119
  await api.addResource('book_authors', {
@@ -122,8 +122,8 @@ describe('Nested Include Operations', () => {
122
122
  book_id: { type: 'number', belongsTo: 'books', as: 'book' },
123
123
  author_id: { type: 'number', belongsTo: 'authors', as: 'author' }
124
124
  }
125
- });
126
- await api.resources.book_authors.createKnexTable();
125
+ })
126
+ await api.resources.book_authors.createKnexTable()
127
127
 
128
128
  // Reviews
129
129
  await api.addResource('reviews', {
@@ -133,19 +133,19 @@ describe('Nested Include Operations', () => {
133
133
  rating: { type: 'number', required: true },
134
134
  book_id: { type: 'number', belongsTo: 'books', as: 'book' }
135
135
  }
136
- });
137
- await api.resources.reviews.createKnexTable();
138
- });
136
+ })
137
+ await api.resources.reviews.createKnexTable()
138
+ })
139
139
 
140
140
  after(async () => {
141
- await knex.destroy();
142
- });
141
+ await knex.destroy()
142
+ })
143
143
 
144
144
  beforeEach(async () => {
145
145
  await cleanTables(knex, [
146
146
  'countries', 'publishers', 'authors', 'books', 'book_authors', 'reviews'
147
- ]);
148
- });
147
+ ])
148
+ })
149
149
 
150
150
  describe('Bidirectional Many-to-Many Relationships', () => {
151
151
  beforeEach(async () => {
@@ -155,89 +155,89 @@ describe('Nested Include Operations', () => {
155
155
  name: 'USA',
156
156
  code: 'US'
157
157
  })
158
- });
158
+ })
159
159
 
160
160
  const publisher = await api.resources.publishers.post({
161
161
  inputRecord: createJsonApiDocument('publishers', {
162
162
  name: 'Test Publisher'
163
163
  }, {
164
- country: createRelationship({ type: 'countries', id: String(country.id) })
165
- })
166
- });
164
+ country: createRelationship({ type: 'countries', id: String(country.id) })
165
+ })
166
+ })
167
167
 
168
168
  // Create authors
169
169
  const author1 = await api.resources.authors.post({
170
170
  inputRecord: createJsonApiDocument('authors', {
171
171
  name: 'Author 1'
172
172
  }, {
173
- country: createRelationship({ type: 'countries', id: String(country.id) })
174
- })
175
- });
173
+ country: createRelationship({ type: 'countries', id: String(country.id) })
174
+ })
175
+ })
176
176
 
177
177
  const author2 = await api.resources.authors.post({
178
178
  inputRecord: createJsonApiDocument('authors', {
179
179
  name: 'Author 2'
180
180
  }, {
181
- country: createRelationship({ type: 'countries', id: String(country.id) })
182
- })
183
- });
181
+ country: createRelationship({ type: 'countries', id: String(country.id) })
182
+ })
183
+ })
184
184
 
185
185
  // Create books
186
186
  const book1 = await api.resources.books.post({
187
187
  inputRecord: createJsonApiDocument('books', {
188
188
  title: 'Book 1'
189
189
  }, {
190
- publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
191
- })
192
- });
190
+ publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
191
+ })
192
+ })
193
193
 
194
194
  const book2 = await api.resources.books.post({
195
195
  inputRecord: createJsonApiDocument('books', {
196
196
  title: 'Book 2'
197
197
  }, {
198
- publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
199
- })
200
- });
198
+ publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
199
+ })
200
+ })
201
201
 
202
202
  const book3 = await api.resources.books.post({
203
203
  inputRecord: createJsonApiDocument('books', {
204
204
  title: 'Book 3'
205
205
  }, {
206
- publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
207
- })
208
- });
206
+ publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
207
+ })
208
+ })
209
209
 
210
210
  // Create relationships
211
211
  // Book 1 - Author 1 only
212
212
  await api.resources.book_authors.post({
213
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
- });
214
+ book: createRelationship({ type: 'books', id: String(book1.id) }),
215
+ author: createRelationship({ type: 'authors', id: String(author1.id) })
216
+ })
217
+ })
218
218
 
219
219
  // Book 2 - Both authors
220
220
  await api.resources.book_authors.post({
221
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
- });
222
+ book: createRelationship({ type: 'books', id: String(book2.id) }),
223
+ author: createRelationship({ type: 'authors', id: String(author1.id) })
224
+ })
225
+ })
226
226
  await api.resources.book_authors.post({
227
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
- });
228
+ book: createRelationship({ type: 'books', id: String(book2.id) }),
229
+ author: createRelationship({ type: 'authors', id: String(author2.id) })
230
+ })
231
+ })
232
232
 
233
233
  // Book 3 - Author 2 only
234
234
  await api.resources.book_authors.post({
235
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
- });
236
+ book: createRelationship({ type: 'books', id: String(book3.id) }),
237
+ author: createRelationship({ type: 'authors', id: String(author2.id) })
238
+ })
239
+ })
240
+ })
241
241
 
242
242
  it('should populate reverse relationships when using nested includes', async () => {
243
243
  const result = await api.resources.authors.query({
@@ -245,62 +245,62 @@ describe('Nested Include Operations', () => {
245
245
  include: ['books', 'books.authors']
246
246
  },
247
247
  simplified: false
248
- });
248
+ })
249
+
250
+ validateJsonApiStructure(result, { isCollection: true })
251
+ assert.equal(result.data.length, 2, 'Should have 2 authors')
249
252
 
250
- validateJsonApiStructure(result, { isCollection: true });
251
- assert.equal(result.data.length, 2, 'Should have 2 authors');
252
-
253
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
-
254
+ const books = result.included.filter(r => r.type === 'books')
255
+ assert.equal(books.length, 3, 'Should include 3 books')
256
+
257
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
-
258
+ const includedAuthors = result.included.filter(r => r.type === 'authors')
259
+ assert.equal(includedAuthors.length, 2, 'Should include 2 authors')
260
+
261
261
  // Verify each book has its authors relationship populated
262
262
  books.forEach(book => {
263
- assert(book.relationships?.authors?.data, `Book ${book.id} should have authors relationship`);
264
-
263
+ assert(book.relationships?.authors?.data, `Book ${book.id} should have authors relationship`)
264
+
265
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');
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
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');
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
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');
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
275
  }
276
- });
277
- });
276
+ })
277
+ })
278
278
 
279
279
  it('should work with window strategy limits', async () => {
280
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
-
281
+ const publisher = await api.resources.publishers.query({ simplified: false })
282
+ const publisherId = publisher.data[0].id
283
+
284
284
  // Get the first author
285
- const authorsResult = await api.resources.authors.query({ simplified: false });
286
- const authorId = authorsResult.data[0].id;
287
-
285
+ const authorsResult = await api.resources.authors.query({ simplified: false })
286
+ const authorId = authorsResult.data[0].id
287
+
288
288
  for (let i = 4; i <= 10; i++) {
289
289
  const book = await api.resources.books.post({
290
290
  inputRecord: createJsonApiDocument('books', {
291
291
  title: `Book ${i}`
292
292
  }, {
293
- publisher: createRelationship({ type: 'publishers', id: publisherId })
294
- })
295
- });
296
-
293
+ publisher: createRelationship({ type: 'publishers', id: publisherId })
294
+ })
295
+ })
296
+
297
297
  // Associate all new books with Author 1
298
298
  await api.resources.book_authors.post({
299
299
  inputRecord: createJsonApiDocument('book_authors', {}, {
300
- book: createRelationship({ type: 'books', id: String(book.id) }),
301
- author: createRelationship({ type: 'authors', id: String(authorId) })
302
- })
303
- });
300
+ book: createRelationship({ type: 'books', id: String(book.id) }),
301
+ author: createRelationship({ type: 'authors', id: String(authorId) })
302
+ })
303
+ })
304
304
  }
305
305
 
306
306
  const result = await api.resources.authors.query({
@@ -309,98 +309,98 @@ describe('Nested Include Operations', () => {
309
309
  filters: { name: 'Author 1' }
310
310
  },
311
311
  simplified: false
312
- });
312
+ })
313
313
 
314
- const author1 = result.data[0];
314
+ const author1 = result.data[0]
315
315
  // Author 1 has books 1,2 from setup + books 4-10 from test = 9 total books
316
316
  // But window limit is 5, so should only get 5
317
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
-
318
+ assert(author1, 'Should have found Author 1')
319
+ assert(author1.relationships?.books?.data, 'Author 1 should have books relationship')
320
+
321
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
-
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
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
-
327
+ const books = result.included.filter(r => r.type === 'books')
328
+ assert(books.length >= 2, 'Should include at least 2 books')
329
+
330
330
  // At least some books should have their authors relationship populated
331
- const booksWithAuthors = books.filter(book =>
331
+ const booksWithAuthors = books.filter(book =>
332
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
- });
333
+ )
334
+ assert(booksWithAuthors.length > 0, 'At least some books should have authors relationship populated')
335
+ })
336
+ })
337
337
 
338
338
  describe('Deep Nested Includes', () => {
339
339
  beforeEach(async () => {
340
340
  // Create hierarchical test data
341
341
  const usa = await api.resources.countries.post({
342
342
  inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
343
- });
343
+ })
344
344
 
345
345
  const uk = await api.resources.countries.post({
346
346
  inputRecord: createJsonApiDocument('countries', { name: 'UK', code: 'GB' })
347
- });
347
+ })
348
348
 
349
349
  const publisher1 = await api.resources.publishers.post({
350
350
  inputRecord: createJsonApiDocument('publishers', {
351
351
  name: 'US Publisher'
352
352
  }, {
353
- country: createRelationship({ type: 'countries', id: String(usa.id) })
354
- })
355
- });
353
+ country: createRelationship({ type: 'countries', id: String(usa.id) })
354
+ })
355
+ })
356
356
 
357
357
  const publisher2 = await api.resources.publishers.post({
358
358
  inputRecord: createJsonApiDocument('publishers', {
359
359
  name: 'UK Publisher'
360
360
  }, {
361
- country: createRelationship({ type: 'countries', id: String(uk.id) })
362
- })
363
- });
361
+ country: createRelationship({ type: 'countries', id: String(uk.id) })
362
+ })
363
+ })
364
364
 
365
365
  const author = await api.resources.authors.post({
366
366
  inputRecord: createJsonApiDocument('authors', {
367
367
  name: 'International Author'
368
368
  }, {
369
- country: createRelationship({ type: 'countries', id: String(usa.id) })
370
- })
371
- });
369
+ country: createRelationship({ type: 'countries', id: String(usa.id) })
370
+ })
371
+ })
372
372
 
373
373
  const book1 = await api.resources.books.post({
374
374
  inputRecord: createJsonApiDocument('books', {
375
375
  title: 'US Book'
376
376
  }, {
377
- publisher: createRelationship({ type: 'publishers', id: String(publisher1.id) })
378
- })
379
- });
377
+ publisher: createRelationship({ type: 'publishers', id: String(publisher1.id) })
378
+ })
379
+ })
380
380
 
381
381
  const book2 = await api.resources.books.post({
382
382
  inputRecord: createJsonApiDocument('books', {
383
383
  title: 'UK Book'
384
384
  }, {
385
- publisher: createRelationship({ type: 'publishers', id: String(publisher2.id) })
386
- })
387
- });
385
+ publisher: createRelationship({ type: 'publishers', id: String(publisher2.id) })
386
+ })
387
+ })
388
388
 
389
389
  // Associate both books with the author
390
390
  await api.resources.book_authors.post({
391
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
- });
392
+ book: createRelationship({ type: 'books', id: String(book1.id) }),
393
+ author: createRelationship({ type: 'authors', id: String(author.id) })
394
+ })
395
+ })
396
396
 
397
397
  await api.resources.book_authors.post({
398
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
- });
399
+ book: createRelationship({ type: 'books', id: String(book2.id) }),
400
+ author: createRelationship({ type: 'authors', id: String(author.id) })
401
+ })
402
+ })
403
+ })
404
404
 
405
405
  it('should handle 3+ levels of nested includes', async () => {
406
406
  const result = await api.resources.authors.query({
@@ -408,28 +408,28 @@ describe('Nested Include Operations', () => {
408
408
  include: ['books', 'books.publisher', 'books.publisher.country']
409
409
  },
410
410
  simplified: false
411
- });
411
+ })
412
+
413
+ validateJsonApiStructure(result, { isCollection: true })
412
414
 
413
- validateJsonApiStructure(result, { isCollection: true });
414
-
415
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');
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
422
  // All defined relationships will be included when requested via ?include=
423
-
423
+
424
424
  // Verify nested relationships are populated
425
425
  books.forEach(book => {
426
- assert(book.relationships?.publisher?.data, 'Book should have publisher relationship');
427
- });
428
-
426
+ assert(book.relationships?.publisher?.data, 'Book should have publisher relationship')
427
+ })
428
+
429
429
  publishers.forEach(publisher => {
430
- assert(publisher.relationships?.country?.data, 'Publisher should have country relationship');
431
- });
432
- });
430
+ assert(publisher.relationships?.country?.data, 'Publisher should have country relationship')
431
+ })
432
+ })
433
433
 
434
434
  it('should handle multiple paths to the same resource', async () => {
435
435
  const result = await api.resources.publishers.query({
@@ -437,61 +437,61 @@ describe('Nested Include Operations', () => {
437
437
  include: ['books', 'books.authors', 'books.authors.country', 'country']
438
438
  },
439
439
  simplified: false
440
- });
440
+ })
441
+
442
+ validateJsonApiStructure(result, { isCollection: true })
441
443
 
442
- validateJsonApiStructure(result, { isCollection: true });
443
-
444
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
-
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
449
  // Authors should have their relationships populated
450
- const authors = result.included.filter(r => r.type === 'authors');
450
+ const authors = result.included.filter(r => r.type === 'authors')
451
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
- });
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
457
 
458
458
  describe('Nested Includes with Sparse Fieldsets', () => {
459
459
  beforeEach(async () => {
460
460
  // Create test data with reviews
461
461
  const country = await api.resources.countries.post({
462
462
  inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
463
- });
463
+ })
464
464
 
465
465
  const publisher = await api.resources.publishers.post({
466
466
  inputRecord: createJsonApiDocument('publishers', {
467
467
  name: 'Publisher'
468
468
  }, {
469
- country: createRelationship({ type: 'countries', id: String(country.id) })
470
- })
471
- });
469
+ country: createRelationship({ type: 'countries', id: String(country.id) })
470
+ })
471
+ })
472
472
 
473
473
  const author = await api.resources.authors.post({
474
474
  inputRecord: createJsonApiDocument('authors', {
475
475
  name: 'Author'
476
476
  }, {
477
- country: createRelationship({ type: 'countries', id: String(country.id) })
478
- })
479
- });
477
+ country: createRelationship({ type: 'countries', id: String(country.id) })
478
+ })
479
+ })
480
480
 
481
481
  const book = await api.resources.books.post({
482
482
  inputRecord: createJsonApiDocument('books', {
483
483
  title: 'Book with Reviews'
484
484
  }, {
485
- publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
486
- })
487
- });
485
+ publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
486
+ })
487
+ })
488
488
 
489
489
  await api.resources.book_authors.post({
490
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
- });
491
+ book: createRelationship({ type: 'books', id: String(book.id) }),
492
+ author: createRelationship({ type: 'authors', id: String(author.id) })
493
+ })
494
+ })
495
495
 
496
496
  // Create reviews
497
497
  await api.resources.reviews.post({
@@ -499,19 +499,19 @@ describe('Nested Include Operations', () => {
499
499
  content: 'Great book!',
500
500
  rating: 5
501
501
  }, {
502
- book: createRelationship({ type: 'books', id: String(book.id) })
503
- })
504
- });
502
+ book: createRelationship({ type: 'books', id: String(book.id) })
503
+ })
504
+ })
505
505
 
506
506
  await api.resources.reviews.post({
507
507
  inputRecord: createJsonApiDocument('reviews', {
508
508
  content: 'Good read',
509
509
  rating: 4
510
510
  }, {
511
- book: createRelationship({ type: 'books', id: String(book.id) })
512
- })
513
- });
514
- });
511
+ book: createRelationship({ type: 'books', id: String(book.id) })
512
+ })
513
+ })
514
+ })
515
515
 
516
516
  it('should respect sparse fieldsets with nested includes', async () => {
517
517
  const result = await api.resources.authors.query({
@@ -524,68 +524,68 @@ describe('Nested Include Operations', () => {
524
524
  }
525
525
  },
526
526
  simplified: false
527
- });
527
+ })
528
+
529
+ validateJsonApiStructure(result, { isCollection: true })
528
530
 
529
- validateJsonApiStructure(result, { isCollection: true });
530
-
531
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');
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
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
-
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
546
  // Relationships should still be populated
547
- assert(book.relationships?.reviews?.data, 'Book should have reviews relationship');
548
- });
549
- });
547
+ assert(book.relationships?.reviews?.data, 'Book should have reviews relationship')
548
+ })
549
+ })
550
550
 
551
551
  describe('Circular Include Handling', () => {
552
552
  beforeEach(async () => {
553
553
  // Create simple circular relationship data
554
554
  const country = await api.resources.countries.post({
555
555
  inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
556
- });
556
+ })
557
557
 
558
558
  const publisher = await api.resources.publishers.post({
559
559
  inputRecord: createJsonApiDocument('publishers', {
560
560
  name: 'Publisher'
561
561
  }, {
562
- country: createRelationship({ type: 'countries', id: String(country.id) })
563
- })
564
- });
562
+ country: createRelationship({ type: 'countries', id: String(country.id) })
563
+ })
564
+ })
565
565
 
566
566
  const author = await api.resources.authors.post({
567
567
  inputRecord: createJsonApiDocument('authors', {
568
568
  name: 'Author'
569
569
  }, {
570
- country: createRelationship({ type: 'countries', id: String(country.id) })
571
- })
572
- });
570
+ country: createRelationship({ type: 'countries', id: String(country.id) })
571
+ })
572
+ })
573
573
 
574
574
  const book = await api.resources.books.post({
575
575
  inputRecord: createJsonApiDocument('books', {
576
576
  title: 'Book'
577
577
  }, {
578
- publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
579
- })
580
- });
578
+ publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
579
+ })
580
+ })
581
581
 
582
582
  await api.resources.book_authors.post({
583
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
- });
584
+ book: createRelationship({ type: 'books', id: String(book.id) }),
585
+ author: createRelationship({ type: 'authors', id: String(author.id) })
586
+ })
587
+ })
588
+ })
589
589
 
590
590
  it('should handle circular includes gracefully', async () => {
591
591
  const result = await api.resources.books.query({
@@ -593,25 +593,25 @@ describe('Nested Include Operations', () => {
593
593
  include: ['authors', 'authors.books', 'authors.books.authors']
594
594
  },
595
595
  simplified: false
596
- });
596
+ })
597
+
598
+ validateJsonApiStructure(result, { isCollection: true })
597
599
 
598
- validateJsonApiStructure(result, { isCollection: true });
599
-
600
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
-
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
607
  // All relationships should be properly populated
608
608
  books.forEach(book => {
609
- assert(book.relationships?.authors?.data, 'Book should have authors relationship');
610
- });
611
-
609
+ assert(book.relationships?.authors?.data, 'Book should have authors relationship')
610
+ })
611
+
612
612
  authors.forEach(author => {
613
- assert(author.relationships?.books?.data, 'Author should have books relationship');
614
- });
615
- });
616
- });
617
- });
613
+ assert(author.relationships?.books?.data, 'Author should have books relationship')
614
+ })
615
+ })
616
+ })
617
+ })