json-rest-api 1.0.10 → 1.0.13

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 (117) hide show
  1. package/.claude/settings.local.json +9 -1
  2. package/AGENTS.md +34 -0
  3. package/CLAUDE.md +26 -1
  4. package/CRUSH.md +55 -0
  5. package/README.md +3 -3
  6. package/TODO.todo +8 -0
  7. package/docs/API.md +17 -18
  8. package/docs/COMPARISON.md +18 -13
  9. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
  10. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
  11. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
  12. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
  13. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
  14. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
  15. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
  16. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
  17. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
  18. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
  19. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
  20. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
  21. package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
  22. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
  23. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
  24. package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
  25. package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
  26. package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
  27. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
  28. package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
  29. package/docs/GUIDE/index.md +3 -0
  30. package/docs/QUICKSTART.md +12 -12
  31. package/docs/index.md +2 -2
  32. package/docs/mini-guides/file-uploads-complete.md +0 -1
  33. package/examples/url-override-example.js +137 -0
  34. package/index.js +7 -1
  35. package/package.json +7 -11
  36. package/plugins/core/bulk-operations-plugin.js +2 -0
  37. package/plugins/core/connectors/express-plugin.js +43 -24
  38. package/plugins/core/connectors/lib/request-helpers.js +2 -2
  39. package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
  40. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
  41. package/plugins/core/lib/anyapi/schema-utils.js +186 -0
  42. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
  43. package/plugins/core/lib/dbTablesOperations.js +305 -0
  44. package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
  45. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
  46. package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
  47. package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
  48. package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
  49. package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
  50. package/plugins/core/lib/querying/url-helpers.js +119 -0
  51. package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
  52. package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
  53. package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
  54. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
  55. package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
  56. package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
  57. package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
  58. package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
  59. package/plugins/core/lib/storage/storage-adapter.js +160 -0
  60. package/plugins/core/lib/writing/relationship-processor.js +4 -11
  61. package/plugins/core/rest-api-access.js +456 -0
  62. package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
  63. package/plugins/core/rest-api-knex-plugin.js +226 -87
  64. package/plugins/core/rest-api-multihome-plugin.js +89 -27
  65. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
  66. package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
  67. package/plugins/core/rest-api-plugin-methods/common.js +46 -10
  68. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
  69. package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
  70. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
  71. package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
  72. package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
  73. package/plugins/core/rest-api-plugin-methods/get.js +2 -5
  74. package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
  75. package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
  76. package/plugins/core/rest-api-plugin-methods/post.js +23 -11
  77. package/plugins/core/rest-api-plugin-methods/put.js +18 -21
  78. package/plugins/core/rest-api-plugin-methods/query.js +3 -4
  79. package/plugins/core/rest-api-plugin-methods/release.js +13 -0
  80. package/plugins/core/rest-api-plugin.js +3 -4
  81. package/plugins/core/rest-api-positioning-plugin.js +253 -76
  82. package/plugins/core/socketio-plugin.js +436 -502
  83. package/quickTest.js +2 -2
  84. package/scripts/debug-anyapi-review-filter.mjs +63 -0
  85. package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
  86. package/scripts/inspect-anyapi-descriptor.mjs +19 -0
  87. package/test-debug.js +59 -0
  88. package/tests/README.md +7 -1
  89. package/tests/anyapi-basic.test.js +102 -0
  90. package/tests/anyapi-cursor-pagination.test.js +144 -0
  91. package/tests/computed-fields.test.js +27 -12
  92. package/tests/cors.test.js +5 -6
  93. package/tests/curl.test.js +1 -1
  94. package/tests/custom-idproperty-relationships.test.js +37 -29
  95. package/tests/field-getters.test.js +25 -7
  96. package/tests/fixtures/api-configs.js +576 -150
  97. package/tests/helpers/storage-mode.js +65 -0
  98. package/tests/helpers/test-utils.js +85 -3
  99. package/tests/include-depth-validation.test.js +35 -1
  100. package/tests/nested-includes.test.js +130 -103
  101. package/tests/pagination-cursor-multifield.test.js +17 -13
  102. package/tests/pagination-enhanced.test.js +19 -24
  103. package/tests/polymorphic-simple.test.js +9 -9
  104. package/tests/positioning-advanced.test.js +19 -19
  105. package/tests/positioning-extended.test.js +25 -25
  106. package/tests/positioning-multifilter.test.js +62 -62
  107. package/tests/positioning.test.js +7 -7
  108. package/tests/queries.test.js +5 -5
  109. package/tests/query-limits.test.js +77 -92
  110. package/tests/rest-api-access.test.js +206 -0
  111. package/tests/searchschema-merge.test.js +182 -0
  112. package/tests/socketio.test.js +33 -14
  113. package/tests/virtual-fields.test.js +0 -1
  114. package/docs/DOCS_ALL.md +0 -21894
  115. package/plugins/core/jwt-auth-plugin.js +0 -897
  116. package/plugins/core/lib/jwt-auth-helpers.js +0 -130
  117. package/tests/jwt-auth.test.js +0 -692
@@ -0,0 +1,65 @@
1
+ const STORAGE_MODE = process.env.JSON_REST_API_STORAGE === 'anyapi' ? 'anyapi' : 'knex';
2
+ const DEFAULT_TENANT = 'default';
3
+
4
+ const tableMetadata = new Map();
5
+ let resetMetadataFn = null;
6
+ const linkMappings = new Map();
7
+ let currentTenant = DEFAULT_TENANT;
8
+
9
+ export const storageMode = {
10
+ mode: STORAGE_MODE,
11
+ isAnyApi() {
12
+ return STORAGE_MODE === 'anyapi';
13
+ },
14
+ defaultTenant: DEFAULT_TENANT,
15
+ get currentTenant() {
16
+ return currentTenant;
17
+ },
18
+ registerReset(fn) {
19
+ resetMetadataFn = fn;
20
+ },
21
+ setCurrentTenant(tenantId) {
22
+ currentTenant = tenantId || DEFAULT_TENANT;
23
+ },
24
+ registerTable(tableName, resourceName, tenantId = currentTenant || DEFAULT_TENANT) {
25
+ if (!tableName || !resourceName) return;
26
+ tableMetadata.set(tableName, {
27
+ resource: resourceName,
28
+ tenantId: tenantId || DEFAULT_TENANT,
29
+ });
30
+ },
31
+ registerLink(
32
+ tableName,
33
+ ownerResource,
34
+ relationshipName,
35
+ relationshipKey,
36
+ inverseRelationshipKey,
37
+ tenantId = currentTenant || DEFAULT_TENANT
38
+ ) {
39
+ if (!tableName || !ownerResource || !relationshipName) return;
40
+ linkMappings.set(tableName, {
41
+ ownerResource,
42
+ relationshipName,
43
+ relationshipKey,
44
+ inverseRelationshipKey,
45
+ tenantId: tenantId || DEFAULT_TENANT,
46
+ });
47
+ },
48
+ getResourceForTable(tableName) {
49
+ return tableMetadata.get(tableName)?.resource;
50
+ },
51
+ getTenantForTable(tableName) {
52
+ return tableMetadata.get(tableName)?.tenantId || DEFAULT_TENANT;
53
+ },
54
+ getLinkInfo(tableName) {
55
+ return linkMappings.get(tableName);
56
+ },
57
+ clearRegistry() {
58
+ tableMetadata.clear();
59
+ linkMappings.clear();
60
+ currentTenant = DEFAULT_TENANT;
61
+ if (resetMetadataFn) {
62
+ resetMetadataFn();
63
+ }
64
+ },
65
+ };
@@ -1,4 +1,5 @@
1
1
  import assert from 'node:assert/strict';
2
+ import { storageMode } from './storage-mode.js';
2
3
 
3
4
  /**
4
5
  * Validates that a response follows JSON:API structure
@@ -48,8 +49,54 @@ export function resourceIdentifier(type, id) {
48
49
  * Cleans all records from the given tables
49
50
  */
50
51
  export async function cleanTables(knex, tableNames) {
51
- for (const table of tableNames) {
52
- await knex(table).delete();
52
+ if (storageMode.isAnyApi()) {
53
+ const resourceGroups = new Map();
54
+
55
+ for (const table of tableNames) {
56
+ const resource = storageMode.getResourceForTable(table);
57
+ if (!resource) continue;
58
+ const tenantId = storageMode.getTenantForTable(table);
59
+ const key = `${tenantId}::${resource}`;
60
+ if (!resourceGroups.has(key)) {
61
+ resourceGroups.set(key, { resource, tenantId });
62
+ }
63
+ }
64
+
65
+ for (const { resource, tenantId } of resourceGroups.values()) {
66
+ await knex('any_records')
67
+ .where({ tenant_id: tenantId, resource })
68
+ .delete();
69
+ }
70
+
71
+ for (const table of tableNames) {
72
+ const linkInfo = storageMode.getLinkInfo(table);
73
+ if (linkInfo) {
74
+ const tenantId = linkInfo.tenantId || storageMode.defaultTenant;
75
+ const relationshipKey = linkInfo.relationshipKey
76
+ || `${tenantId}:${linkInfo.ownerResource}:${linkInfo.relationshipName}`;
77
+ await knex('any_links')
78
+ .where({ tenant_id: tenantId })
79
+ .andWhere((builder) => {
80
+ builder.where('relationship', relationshipKey)
81
+ .orWhere('inverse_relationship', relationshipKey);
82
+ if (linkInfo.inverseRelationshipKey) {
83
+ builder.orWhere('relationship', linkInfo.inverseRelationshipKey)
84
+ .orWhere('inverse_relationship', linkInfo.inverseRelationshipKey);
85
+ }
86
+ })
87
+ .delete()
88
+ .catch(() => {});
89
+ }
90
+ try {
91
+ await knex(table).delete();
92
+ } catch (error) {
93
+ // Ignore missing tables in anyapi mode
94
+ }
95
+ }
96
+ } else {
97
+ for (const table of tableNames) {
98
+ await knex(table).delete();
99
+ }
53
100
  }
54
101
  }
55
102
 
@@ -57,6 +104,41 @@ export async function cleanTables(knex, tableNames) {
57
104
  * Counts records in a table
58
105
  */
59
106
  export async function countRecords(knex, tableName) {
107
+ if (storageMode.isAnyApi()) {
108
+ const resource = storageMode.getResourceForTable(tableName);
109
+ const linkInfo = storageMode.getLinkInfo(tableName);
110
+ const tenantId = storageMode.getTenantForTable(tableName);
111
+ if (linkInfo) {
112
+ const relationshipKey = linkInfo.relationshipKey
113
+ || `${tenantId}:${linkInfo.ownerResource}:${linkInfo.relationshipName}`;
114
+ const result = await knex('any_links')
115
+ .where({ tenant_id: tenantId })
116
+ .andWhere((builder) => {
117
+ builder.where('relationship', relationshipKey)
118
+ .orWhere('inverse_relationship', relationshipKey);
119
+ })
120
+ .count('* as count')
121
+ .first();
122
+ return parseInt(result?.count ?? 0, 10);
123
+ }
124
+ if (resource) {
125
+ const result = await knex('any_records')
126
+ .where({
127
+ tenant_id: tenantId,
128
+ resource,
129
+ })
130
+ .count('* as count')
131
+ .first();
132
+ return parseInt(result.count);
133
+ }
134
+ try {
135
+ const result = await knex(tableName).count('* as count').first();
136
+ return parseInt(result?.count || 0);
137
+ } catch (error) {
138
+ return 0;
139
+ }
140
+ }
141
+
60
142
  const result = await knex(tableName).count('* as count').first();
61
143
  return parseInt(result.count);
62
144
  }
@@ -118,4 +200,4 @@ export function assertResourceRelationship(resource, relationshipName, expectedI
118
200
  expectedIdentifier,
119
201
  `Relationship ${relationshipName} should match expected identifier`
120
202
  );
121
- }
203
+ }
@@ -271,6 +271,40 @@ describe('Include Depth Validation', () => {
271
271
  });
272
272
 
273
273
  it('should handle single-level paths correctly', async () => {
274
+ const country = await api.resources.countries.post({
275
+ inputRecord: {
276
+ data: {
277
+ type: 'countries',
278
+ attributes: { name: 'Single Level Country', code: `S${Date.now().toString().slice(-2)}` }
279
+ }
280
+ }
281
+ });
282
+
283
+ const publisher = await api.resources.publishers.post({
284
+ inputRecord: {
285
+ data: {
286
+ type: 'publishers',
287
+ attributes: { name: 'Single Level Publisher' },
288
+ relationships: {
289
+ country: { data: { type: 'countries', id: country.data.id } }
290
+ }
291
+ }
292
+ }
293
+ });
294
+
295
+ await api.resources.books.post({
296
+ inputRecord: {
297
+ data: {
298
+ type: 'books',
299
+ attributes: { title: 'Single Level Book' },
300
+ relationships: {
301
+ country: { data: { type: 'countries', id: country.data.id } },
302
+ publisher: { data: { type: 'publishers', id: publisher.data.id } }
303
+ }
304
+ }
305
+ }
306
+ });
307
+
274
308
  const result = await api.resources.books.query({
275
309
  queryParams: { include: ['publisher'] }
276
310
  });
@@ -294,4 +328,4 @@ describe('Include Depth Validation', () => {
294
328
  );
295
329
  });
296
330
  });
297
- });
331
+ });
@@ -7,6 +7,7 @@ import { RestApiKnexPlugin } from '../plugins/core/rest-api-knex-plugin.js';
7
7
  import {
8
8
  cleanTables,
9
9
  createJsonApiDocument,
10
+ createRelationship,
10
11
  validateJsonApiStructure
11
12
  } from './helpers/test-utils.js';
12
13
 
@@ -57,8 +58,9 @@ describe('Nested Include Operations', () => {
57
58
  },
58
59
  relationships: {
59
60
  books: {
60
- hasMany: 'books',
61
- foreignKey: 'publisher_id',
61
+ type: 'hasMany',
62
+ target: 'books',
63
+ foreignKey: 'publisher_id'
62
64
  // Relationships are always includable via ?include=
63
65
  }
64
66
  }
@@ -74,7 +76,7 @@ describe('Nested Include Operations', () => {
74
76
  },
75
77
  relationships: {
76
78
  books: {
77
- hasMany: 'books',
79
+ type: 'manyToMany',
78
80
  through: 'book_authors',
79
81
  foreignKey: 'author_id',
80
82
  otherKey: 'book_id',
@@ -97,15 +99,16 @@ describe('Nested Include Operations', () => {
97
99
  },
98
100
  relationships: {
99
101
  authors: {
100
- hasMany: 'authors',
102
+ type: 'manyToMany',
101
103
  through: 'book_authors',
102
104
  foreignKey: 'book_id',
103
- otherKey: 'author_id',
105
+ otherKey: 'author_id'
104
106
  // Relationships are always includable via ?include=
105
107
  },
106
108
  reviews: {
107
- hasMany: 'reviews',
108
- foreignKey: 'book_id',
109
+ type: 'hasMany',
110
+ target: 'reviews',
111
+ foreignKey: 'book_id'
109
112
  // Relationships are always includable via ?include=
110
113
  }
111
114
  }
@@ -156,77 +159,83 @@ describe('Nested Include Operations', () => {
156
159
 
157
160
  const publisher = await api.resources.publishers.post({
158
161
  inputRecord: createJsonApiDocument('publishers', {
159
- name: 'Test Publisher',
160
- country_id: parseInt(country.id)
161
- })
162
+ name: 'Test Publisher'
163
+ }, {
164
+ country: createRelationship({ type: 'countries', id: String(country.id) })
165
+ })
162
166
  });
163
167
 
164
168
  // Create authors
165
169
  const author1 = await api.resources.authors.post({
166
170
  inputRecord: createJsonApiDocument('authors', {
167
- name: 'Author 1',
168
- country_id: parseInt(country.id)
169
- })
171
+ name: 'Author 1'
172
+ }, {
173
+ country: createRelationship({ type: 'countries', id: String(country.id) })
174
+ })
170
175
  });
171
176
 
172
177
  const author2 = await api.resources.authors.post({
173
178
  inputRecord: createJsonApiDocument('authors', {
174
- name: 'Author 2',
175
- country_id: parseInt(country.id)
176
- })
179
+ name: 'Author 2'
180
+ }, {
181
+ country: createRelationship({ type: 'countries', id: String(country.id) })
182
+ })
177
183
  });
178
184
 
179
185
  // Create books
180
186
  const book1 = await api.resources.books.post({
181
187
  inputRecord: createJsonApiDocument('books', {
182
- title: 'Book 1',
183
- publisher_id: parseInt(publisher.id)
184
- })
188
+ title: 'Book 1'
189
+ }, {
190
+ publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
191
+ })
185
192
  });
186
193
 
187
194
  const book2 = await api.resources.books.post({
188
195
  inputRecord: createJsonApiDocument('books', {
189
- title: 'Book 2',
190
- publisher_id: parseInt(publisher.id)
191
- })
196
+ title: 'Book 2'
197
+ }, {
198
+ publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
199
+ })
192
200
  });
193
201
 
194
202
  const book3 = await api.resources.books.post({
195
203
  inputRecord: createJsonApiDocument('books', {
196
- title: 'Book 3',
197
- publisher_id: parseInt(publisher.id)
198
- })
204
+ title: 'Book 3'
205
+ }, {
206
+ publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
207
+ })
199
208
  });
200
209
 
201
210
  // Create relationships
202
211
  // Book 1 - Author 1 only
203
212
  await api.resources.book_authors.post({
204
- inputRecord: createJsonApiDocument('book_authors', {
205
- book_id: parseInt(book1.id),
206
- author_id: parseInt(author1.id)
207
- })
213
+ inputRecord: createJsonApiDocument('book_authors', {}, {
214
+ book: createRelationship({ type: 'books', id: String(book1.id) }),
215
+ author: createRelationship({ type: 'authors', id: String(author1.id) })
216
+ })
208
217
  });
209
218
 
210
219
  // Book 2 - Both authors
211
220
  await api.resources.book_authors.post({
212
- inputRecord: createJsonApiDocument('book_authors', {
213
- book_id: parseInt(book2.id),
214
- author_id: parseInt(author1.id)
215
- })
221
+ inputRecord: createJsonApiDocument('book_authors', {}, {
222
+ book: createRelationship({ type: 'books', id: String(book2.id) }),
223
+ author: createRelationship({ type: 'authors', id: String(author1.id) })
224
+ })
216
225
  });
217
226
  await api.resources.book_authors.post({
218
- inputRecord: createJsonApiDocument('book_authors', {
219
- book_id: parseInt(book2.id),
220
- author_id: parseInt(author2.id)
221
- })
227
+ inputRecord: createJsonApiDocument('book_authors', {}, {
228
+ book: createRelationship({ type: 'books', id: String(book2.id) }),
229
+ author: createRelationship({ type: 'authors', id: String(author2.id) })
230
+ })
222
231
  });
223
232
 
224
233
  // Book 3 - Author 2 only
225
234
  await api.resources.book_authors.post({
226
- inputRecord: createJsonApiDocument('book_authors', {
227
- book_id: parseInt(book3.id),
228
- author_id: parseInt(author2.id)
229
- })
235
+ inputRecord: createJsonApiDocument('book_authors', {}, {
236
+ book: createRelationship({ type: 'books', id: String(book3.id) }),
237
+ author: createRelationship({ type: 'authors', id: String(author2.id) })
238
+ })
230
239
  });
231
240
  });
232
241
 
@@ -272,20 +281,25 @@ describe('Nested Include Operations', () => {
272
281
  const publisher = await api.resources.publishers.query({ simplified: false });
273
282
  const publisherId = publisher.data[0].id;
274
283
 
284
+ // Get the first author
285
+ const authorsResult = await api.resources.authors.query({ simplified: false });
286
+ const authorId = authorsResult.data[0].id;
287
+
275
288
  for (let i = 4; i <= 10; i++) {
276
289
  const book = await api.resources.books.post({
277
290
  inputRecord: createJsonApiDocument('books', {
278
- title: `Book ${i}`,
279
- publisher_id: parseInt(publisherId)
280
- })
291
+ title: `Book ${i}`
292
+ }, {
293
+ publisher: createRelationship({ type: 'publishers', id: publisherId })
294
+ })
281
295
  });
282
296
 
283
297
  // Associate all new books with Author 1
284
298
  await api.resources.book_authors.post({
285
- inputRecord: createJsonApiDocument('book_authors', {
286
- book_id: parseInt(book.id),
287
- author_id: 1
288
- })
299
+ inputRecord: createJsonApiDocument('book_authors', {}, {
300
+ book: createRelationship({ type: 'books', id: String(book.id) }),
301
+ author: createRelationship({ type: 'authors', id: String(authorId) })
302
+ })
289
303
  });
290
304
  }
291
305
 
@@ -334,52 +348,57 @@ describe('Nested Include Operations', () => {
334
348
 
335
349
  const publisher1 = await api.resources.publishers.post({
336
350
  inputRecord: createJsonApiDocument('publishers', {
337
- name: 'US Publisher',
338
- country_id: parseInt(usa.id)
339
- })
351
+ name: 'US Publisher'
352
+ }, {
353
+ country: createRelationship({ type: 'countries', id: String(usa.id) })
354
+ })
340
355
  });
341
356
 
342
357
  const publisher2 = await api.resources.publishers.post({
343
358
  inputRecord: createJsonApiDocument('publishers', {
344
- name: 'UK Publisher',
345
- country_id: parseInt(uk.id)
346
- })
359
+ name: 'UK Publisher'
360
+ }, {
361
+ country: createRelationship({ type: 'countries', id: String(uk.id) })
362
+ })
347
363
  });
348
364
 
349
365
  const author = await api.resources.authors.post({
350
366
  inputRecord: createJsonApiDocument('authors', {
351
- name: 'International Author',
352
- country_id: parseInt(usa.id)
353
- })
367
+ name: 'International Author'
368
+ }, {
369
+ country: createRelationship({ type: 'countries', id: String(usa.id) })
370
+ })
354
371
  });
355
372
 
356
373
  const book1 = await api.resources.books.post({
357
374
  inputRecord: createJsonApiDocument('books', {
358
- title: 'US Book',
359
- publisher_id: parseInt(publisher1.id)
360
- })
375
+ title: 'US Book'
376
+ }, {
377
+ publisher: createRelationship({ type: 'publishers', id: String(publisher1.id) })
378
+ })
361
379
  });
362
380
 
363
381
  const book2 = await api.resources.books.post({
364
382
  inputRecord: createJsonApiDocument('books', {
365
- title: 'UK Book',
366
- publisher_id: parseInt(publisher2.id)
367
- })
383
+ title: 'UK Book'
384
+ }, {
385
+ publisher: createRelationship({ type: 'publishers', id: String(publisher2.id) })
386
+ })
368
387
  });
369
388
 
370
389
  // Associate both books with the author
371
390
  await api.resources.book_authors.post({
372
- inputRecord: createJsonApiDocument('book_authors', {
373
- book_id: parseInt(book1.id),
374
- author_id: parseInt(author.id)
375
- })
391
+ inputRecord: createJsonApiDocument('book_authors', {}, {
392
+ book: createRelationship({ type: 'books', id: String(book1.id) }),
393
+ author: createRelationship({ type: 'authors', id: String(author.id) })
394
+ })
376
395
  });
377
396
 
378
397
  await api.resources.book_authors.post({
379
- inputRecord: createJsonApiDocument('book_authors', {
380
- book_id: parseInt(book2.id),
381
- author_id: parseInt(author.id)
382
- })
398
+ inputRecord: createJsonApiDocument('book_authors', {}, {
399
+ book: createRelationship({ type: 'books', id: String(book2.id) }),
400
+ author: createRelationship({ type: 'authors', id: String(author.id) })
401
+ })
383
402
  });
384
403
  });
385
404
 
@@ -445,47 +464,52 @@ describe('Nested Include Operations', () => {
445
464
 
446
465
  const publisher = await api.resources.publishers.post({
447
466
  inputRecord: createJsonApiDocument('publishers', {
448
- name: 'Publisher',
449
- country_id: parseInt(country.id)
450
- })
467
+ name: 'Publisher'
468
+ }, {
469
+ country: createRelationship({ type: 'countries', id: String(country.id) })
470
+ })
451
471
  });
452
472
 
453
473
  const author = await api.resources.authors.post({
454
474
  inputRecord: createJsonApiDocument('authors', {
455
- name: 'Author',
456
- country_id: parseInt(country.id)
457
- })
475
+ name: 'Author'
476
+ }, {
477
+ country: createRelationship({ type: 'countries', id: String(country.id) })
478
+ })
458
479
  });
459
480
 
460
481
  const book = await api.resources.books.post({
461
482
  inputRecord: createJsonApiDocument('books', {
462
- title: 'Book with Reviews',
463
- publisher_id: parseInt(publisher.id)
464
- })
483
+ title: 'Book with Reviews'
484
+ }, {
485
+ publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
486
+ })
465
487
  });
466
488
 
467
489
  await api.resources.book_authors.post({
468
- inputRecord: createJsonApiDocument('book_authors', {
469
- book_id: parseInt(book.id),
470
- author_id: parseInt(author.id)
471
- })
490
+ inputRecord: createJsonApiDocument('book_authors', {}, {
491
+ book: createRelationship({ type: 'books', id: String(book.id) }),
492
+ author: createRelationship({ type: 'authors', id: String(author.id) })
493
+ })
472
494
  });
473
495
 
474
496
  // Create reviews
475
497
  await api.resources.reviews.post({
476
498
  inputRecord: createJsonApiDocument('reviews', {
477
499
  content: 'Great book!',
478
- rating: 5,
479
- book_id: parseInt(book.id)
480
- })
500
+ rating: 5
501
+ }, {
502
+ book: createRelationship({ type: 'books', id: String(book.id) })
503
+ })
481
504
  });
482
505
 
483
506
  await api.resources.reviews.post({
484
507
  inputRecord: createJsonApiDocument('reviews', {
485
508
  content: 'Good read',
486
- rating: 4,
487
- book_id: parseInt(book.id)
488
- })
509
+ rating: 4
510
+ }, {
511
+ book: createRelationship({ type: 'books', id: String(book.id) })
512
+ })
489
513
  });
490
514
  });
491
515
 
@@ -533,30 +557,33 @@ describe('Nested Include Operations', () => {
533
557
 
534
558
  const publisher = await api.resources.publishers.post({
535
559
  inputRecord: createJsonApiDocument('publishers', {
536
- name: 'Publisher',
537
- country_id: parseInt(country.id)
538
- })
560
+ name: 'Publisher'
561
+ }, {
562
+ country: createRelationship({ type: 'countries', id: String(country.id) })
563
+ })
539
564
  });
540
565
 
541
566
  const author = await api.resources.authors.post({
542
567
  inputRecord: createJsonApiDocument('authors', {
543
- name: 'Author',
544
- country_id: parseInt(country.id)
545
- })
568
+ name: 'Author'
569
+ }, {
570
+ country: createRelationship({ type: 'countries', id: String(country.id) })
571
+ })
546
572
  });
547
573
 
548
574
  const book = await api.resources.books.post({
549
575
  inputRecord: createJsonApiDocument('books', {
550
- title: 'Book',
551
- publisher_id: parseInt(publisher.id)
552
- })
576
+ title: 'Book'
577
+ }, {
578
+ publisher: createRelationship({ type: 'publishers', id: String(publisher.id) })
579
+ })
553
580
  });
554
581
 
555
582
  await api.resources.book_authors.post({
556
- inputRecord: createJsonApiDocument('book_authors', {
557
- book_id: parseInt(book.id),
558
- author_id: parseInt(author.id)
559
- })
583
+ inputRecord: createJsonApiDocument('book_authors', {}, {
584
+ book: createRelationship({ type: 'books', id: String(book.id) }),
585
+ author: createRelationship({ type: 'authors', id: String(author.id) })
586
+ })
560
587
  });
561
588
  });
562
589