json-rest-api 1.0.10 → 1.0.11

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 +5 -9
  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
package/quickTest.js CHANGED
@@ -20,10 +20,10 @@ const knex = knexLib({
20
20
  });
21
21
 
22
22
  // Create API instance
23
- const api = new Api({ name: 'book-catalog-api', version: '1.0.0', logLevel: 'trace' });
23
+ const api = new Api({ name: 'book-catalog-api', logLevel: 'trace' });
24
24
 
25
25
  // Install plugins
26
- await api.use(RestApiPlugin, { publicBaseUrl: '/api' });
26
+ await api.use(RestApiPlugin, {returnBasePath: '/api' });
27
27
  await api.use(RestApiKnexPlugin, { knex });
28
28
  await api.use(ExpressPlugin, { mountPath: '/api' }); // Added: Express Plugin
29
29
 
@@ -0,0 +1,63 @@
1
+ import knexLib from 'knex';
2
+ import { createCustomIdPropertyApi } from '../tests/fixtures/api-configs.js';
3
+ import { createJsonApiDocument } from '../tests/helpers/test-utils.js';
4
+
5
+ async function run() {
6
+ const knex = knexLib({ client: 'better-sqlite3', connection: { filename: ':memory:' }, useNullAsDefault: true });
7
+ const api = await createCustomIdPropertyApi(knex);
8
+
9
+ const country = await api.resources.countries.post({
10
+ inputRecord: createJsonApiDocument('countries', { name: 'USA', code: 'US' })
11
+ });
12
+
13
+ const book = await api.resources.books.post({
14
+ inputRecord: createJsonApiDocument('books',
15
+ { title: 'Test Book' },
16
+ { country: { data: { type: 'countries', id: country.data.id } } }
17
+ )
18
+ });
19
+
20
+ const author = await api.resources.authors.post({
21
+ inputRecord: createJsonApiDocument('authors', { name: 'Author' })
22
+ });
23
+
24
+ await api.resources.reviews.post({
25
+ inputRecord: createJsonApiDocument('reviews', {
26
+ rating: 5,
27
+ title: 'Book review',
28
+ content: 'Great book',
29
+ reviewer_name: 'Reader',
30
+ reviewable_type: 'books',
31
+ reviewable_id: book.data.id
32
+ })
33
+ });
34
+
35
+ await api.resources.reviews.post({
36
+ inputRecord: createJsonApiDocument('reviews', {
37
+ rating: 4,
38
+ title: 'Author review',
39
+ content: 'Great author',
40
+ reviewer_name: 'Fan',
41
+ reviewable_type: 'authors',
42
+ reviewable_id: author.data.id
43
+ })
44
+ });
45
+
46
+ try {
47
+ const res = await api.resources.reviews.query({
48
+ queryParams: { filters: { reviewable_type: 'books' } }
49
+ });
50
+ console.log('Query succeeded with rows:', res.data.length);
51
+ } catch (error) {
52
+ console.error('Query failed:', error);
53
+ if (error?.sql) {
54
+ console.error('SQL:', error.sql);
55
+ }
56
+ }
57
+
58
+ await knex.destroy();
59
+ }
60
+
61
+ run().catch((err) => {
62
+ console.error('Unexpected failure:', err);
63
+ });
@@ -0,0 +1,19 @@
1
+ import knexLib from 'knex';
2
+ import { createCustomIdPropertyApi } from '../tests/fixtures/api-configs.js';
3
+
4
+ async function run() {
5
+ const knex = knexLib({ client: 'better-sqlite3', connection: { filename: ':memory:' }, useNullAsDefault: true });
6
+ const api = await createCustomIdPropertyApi(knex);
7
+ const descriptor = await api.anyapi.registry.getDescriptor('default', 'countries');
8
+ console.log(JSON.stringify({
9
+ idProperty: descriptor.idProperty,
10
+ fields: descriptor.fields,
11
+ canonicalFieldMap: descriptor.canonicalFieldMap,
12
+ reverseAttributes: descriptor.reverseAttributes,
13
+ }, null, 2));
14
+ await knex.destroy();
15
+ }
16
+
17
+ run().catch((err) => {
18
+ console.error('Failed to inspect descriptor', err);
19
+ });
@@ -0,0 +1,19 @@
1
+ import knexLib from 'knex';
2
+ import { createCustomIdPropertyApi } from '../tests/fixtures/api-configs.js';
3
+
4
+ async function run() {
5
+ const knex = knexLib({ client: 'better-sqlite3', connection: { filename: ':memory:' }, useNullAsDefault: true });
6
+ const api = await createCustomIdPropertyApi(knex);
7
+ const descriptor = await api.anyapi.registry.getDescriptor('default', 'reviews');
8
+ console.log(JSON.stringify({
9
+ idProperty: descriptor.idProperty,
10
+ fields: descriptor.fields,
11
+ canonicalFieldMap: descriptor.canonicalFieldMap,
12
+ reverseAttributes: descriptor.reverseAttributes,
13
+ }, null, 2));
14
+ await knex.destroy();
15
+ }
16
+
17
+ run().catch((err) => {
18
+ console.error('Failed to inspect descriptor', err);
19
+ });
package/test-debug.js ADDED
@@ -0,0 +1,59 @@
1
+ import { createBasicApi } from './tests/fixtures/api-configs.js';
2
+ import { cleanTables, createJsonApiDocument, createRelationship } from './tests/helpers/test-utils.js';
3
+ import knex from 'knex';
4
+
5
+ const dbConfig = {
6
+ client: 'better-sqlite3',
7
+ connection: { filename: ':memory:' },
8
+ useNullAsDefault: true
9
+ };
10
+
11
+ const db = knex(dbConfig);
12
+
13
+ async function test() {
14
+ const api = await createBasicApi(db);
15
+
16
+ // Create test data
17
+ await cleanTables(db, ['basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors']);
18
+
19
+ // Create a country first
20
+ const countryDoc = createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' });
21
+ const country = await api.resources.countries.post({ inputRecord: countryDoc });
22
+
23
+ // Create an author
24
+ const authorDoc = createJsonApiDocument('authors', { name: 'Test Author' });
25
+ const author = await api.resources.authors.post({ inputRecord: authorDoc });
26
+
27
+ // Create a book with proper relationships
28
+ const bookDoc = {
29
+ data: {
30
+ type: 'books',
31
+ attributes: { title: 'Test Book' },
32
+ relationships: {
33
+ country: { data: { type: 'countries', id: String(country.data.id) } }
34
+ }
35
+ }
36
+ };
37
+ const book = await api.resources.books.post({ inputRecord: bookDoc });
38
+
39
+ // Create the many-to-many relationship
40
+ await db('basic_book_authors').insert({
41
+ author_id: author.data.id,
42
+ book_id: book.data.id
43
+ });
44
+
45
+ // Now try to query authors with books included
46
+ console.log('Querying authors with books included...');
47
+ try {
48
+ const result = await api.resources.authors.query({
49
+ queryParams: { include: ['books'] }
50
+ });
51
+ console.log('Success!', JSON.stringify(result, null, 2));
52
+ } catch (error) {
53
+ console.error('Error:', error.message);
54
+ }
55
+
56
+ await db.destroy();
57
+ }
58
+
59
+ test().catch(console.error);
package/tests/README.md CHANGED
@@ -163,6 +163,12 @@ const result = await api.resources.books.query({
163
163
  # Run all tests
164
164
  npm test
165
165
 
166
+ # Run all tests against the AnyAPI storage backend
167
+ npm run test:anyapi
168
+
169
+ # Windows PowerShell (AnyAPI run)
170
+ $Env:JSON_REST_API_STORAGE = 'anyapi'; npm test
171
+
166
172
  # Run specific test file
167
173
  npm test -- tests/relationships.test.js
168
174
 
@@ -186,4 +192,4 @@ DEBUG=* npm test
186
192
  - Tests intentionally generate ERROR logs for validation testing
187
193
  - All tests should pass with exit code 0
188
194
  - Database connections are properly closed in `after` hooks
189
- - Pivot table operations use `simplified: true` internally for foreign keys
195
+ - Pivot table operations use `simplified: true` internally for foreign keys
@@ -0,0 +1,102 @@
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
+
6
+ import { RestApiPlugin, RestApiAnyapiKnexPlugin } from '../index.js';
7
+ import { ensureAnyApiSchema } from '../plugins/core/lib/anyapi/schema-utils.js';
8
+
9
+ const knex = knexLib({
10
+ client: 'better-sqlite3',
11
+ connection: { filename: ':memory:' },
12
+ useNullAsDefault: true,
13
+ });
14
+
15
+ let api;
16
+
17
+ describe('AnyAPI Knex Plugin - Basic Attributes', () => {
18
+ before(async () => {
19
+ try {
20
+ await ensureAnyApiSchema(knex);
21
+
22
+ api = new Api({ name: 'anyapi-test', log: { level: 'warn' } });
23
+ await api.use(RestApiPlugin, { simplifiedApi: false, simplifiedTransport: false });
24
+ await api.use(RestApiAnyapiKnexPlugin, { knex });
25
+
26
+ await api.addResource('countries', {
27
+ schema: {
28
+ id: { type: 'id' },
29
+ name: { type: 'string', required: true },
30
+ code: { type: 'string', required: true },
31
+ },
32
+ });
33
+
34
+ if (api.resources.countries?.createKnexTable) {
35
+ await api.resources.countries.createKnexTable();
36
+ }
37
+ } catch (error) {
38
+ console.error('Setup failed', error);
39
+ throw error;
40
+ }
41
+ });
42
+
43
+ after(async () => {
44
+ await api.release();
45
+ await knex.destroy();
46
+ });
47
+
48
+ beforeEach(async () => {
49
+ await knex('any_records').truncate();
50
+ });
51
+
52
+ it('creates and retrieves records', async () => {
53
+ const result = await api.resources.countries.post({
54
+ inputRecord: {
55
+ data: {
56
+ type: 'countries',
57
+ attributes: {
58
+ name: 'Australia',
59
+ code: 'AU',
60
+ },
61
+ },
62
+ },
63
+ simplified: false,
64
+ }).catch((error) => {
65
+ console.error('POST failed', error);
66
+ throw error;
67
+ });
68
+
69
+ assert.ok(result.data.id, 'should return new id');
70
+ assert.equal(result.data.attributes.name, 'Australia');
71
+
72
+ const stored = await knex('any_records').first();
73
+ assert.equal(stored.string_1, 'Australia');
74
+ assert.equal(stored.string_2, 'AU');
75
+
76
+ const fetched = await api.resources.countries.get({ id: result.data.id, simplified: false })
77
+ .catch((error) => {
78
+ console.error('GET failed', error);
79
+ throw error;
80
+ });
81
+ assert.equal(fetched.data.attributes.name, 'Australia');
82
+ });
83
+
84
+ it('lists records', async () => {
85
+ await knex('any_records').insert([{
86
+ tenant_id: 'default',
87
+ resource: 'countries',
88
+ string_1: 'Canada',
89
+ string_2: 'CA',
90
+ }, {
91
+ tenant_id: 'default',
92
+ resource: 'countries',
93
+ string_1: 'Brazil',
94
+ string_2: 'BR',
95
+ }]);
96
+
97
+ const result = await api.resources.countries.query({ simplified: false });
98
+ assert.equal(result.data.length, 2);
99
+ const names = result.data.map((entry) => entry.attributes.name).sort();
100
+ assert.deepEqual(names, ['Brazil', 'Canada']);
101
+ });
102
+ });
@@ -0,0 +1,144 @@
1
+ import { describe, it, before, beforeEach, after } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import knexLib from 'knex';
4
+ import { createPaginationApi } from './fixtures/api-configs.js';
5
+ import {
6
+ cleanTables,
7
+ createJsonApiDocument,
8
+ createRelationship,
9
+ resourceIdentifier,
10
+ } from './helpers/test-utils.js';
11
+ import { storageMode } from './helpers/storage-mode.js';
12
+
13
+ const isAnyApi = storageMode.isAnyApi();
14
+
15
+ const maybeDescribe = isAnyApi ? describe : describe.skip;
16
+
17
+ maybeDescribe('AnyAPI Cursor Pagination', () => {
18
+ const knex = knexLib({
19
+ client: 'better-sqlite3',
20
+ connection: { filename: ':memory:' },
21
+ useNullAsDefault: true,
22
+ });
23
+
24
+ let api;
25
+
26
+ before(async () => {
27
+ api = await createPaginationApi(knex);
28
+ });
29
+
30
+ after(async () => {
31
+ await knex.destroy();
32
+ });
33
+
34
+ beforeEach(async () => {
35
+ await cleanTables(knex, [
36
+ 'pagination_countries',
37
+ 'pagination_publishers',
38
+ 'pagination_books',
39
+ ]);
40
+
41
+ const country = await api.resources.countries.post({
42
+ inputRecord: createJsonApiDocument('countries', { name: 'Cursorland', code: 'CL' }),
43
+ simplified: false,
44
+ });
45
+
46
+ const publisher = await api.resources.publishers.post({
47
+ inputRecord: createJsonApiDocument(
48
+ 'publishers',
49
+ { name: 'Cursor Press' },
50
+ { country: createRelationship(resourceIdentifier('countries', country.data.id)) },
51
+ ),
52
+ simplified: false,
53
+ });
54
+
55
+ const titles = ['Book A', 'Book B', 'Book C', 'Book D', 'Book E', 'Book F'];
56
+ for (const title of titles) {
57
+ await api.resources.books.post({
58
+ inputRecord: createJsonApiDocument(
59
+ 'books',
60
+ { title },
61
+ {
62
+ country: createRelationship(resourceIdentifier('countries', country.data.id)),
63
+ publisher: createRelationship(resourceIdentifier('publishers', publisher.data.id)),
64
+ },
65
+ ),
66
+ simplified: false,
67
+ });
68
+ }
69
+ });
70
+
71
+ it('returns cursor metadata and links when requesting the first page', async () => {
72
+ const result = await api.resources.books.query({
73
+ queryParams: {
74
+ page: { size: 2 },
75
+ },
76
+ simplified: false,
77
+ });
78
+
79
+ assert.equal(result.data.length, 2);
80
+ assert(result.meta?.pagination?.cursor?.next, 'Should include next cursor');
81
+ assert(result.links?.next?.includes('page[after]='), 'Next link should include cursor parameter');
82
+ });
83
+
84
+ it('paginates forward using page[after]', async () => {
85
+ const firstPage = await api.resources.books.query({
86
+ queryParams: {
87
+ page: { size: 2 },
88
+ },
89
+ simplified: false,
90
+ });
91
+
92
+ const cursor = firstPage.meta.pagination.cursor.next;
93
+ const secondPage = await api.resources.books.query({
94
+ queryParams: {
95
+ page: { size: 2, after: cursor },
96
+ },
97
+ simplified: false,
98
+ });
99
+
100
+ assert.equal(secondPage.data.length, 2);
101
+ const firstIds = firstPage.data.map((book) => book.id);
102
+ const secondIds = secondPage.data.map((book) => book.id);
103
+ assert(!secondIds.some((id) => firstIds.includes(id)), 'Second page should not repeat first page records');
104
+ });
105
+
106
+ it('paginates backward using page[before]', async () => {
107
+ const firstPage = await api.resources.books.query({
108
+ queryParams: {
109
+ page: { size: 2 },
110
+ },
111
+ simplified: false,
112
+ });
113
+
114
+ const secondPage = await api.resources.books.query({
115
+ queryParams: {
116
+ page: {
117
+ size: 2,
118
+ after: firstPage.meta.pagination.cursor.next,
119
+ },
120
+ },
121
+ simplified: false,
122
+ });
123
+
124
+ const beforeCursor =
125
+ secondPage.meta.pagination.cursor?.next
126
+ || `id:${encodeURIComponent(secondPage.data[0].id)}`;
127
+
128
+ const previousPage = await api.resources.books.query({
129
+ queryParams: {
130
+ page: {
131
+ size: 2,
132
+ before: beforeCursor,
133
+ },
134
+ },
135
+ simplified: false,
136
+ });
137
+
138
+ const secondIds = secondPage.data.map((book) => book.id);
139
+ const previousIds = previousPage.data.map((book) => book.id);
140
+ assert(!previousIds.some((id) => secondIds.includes(id)), 'Page[before] should return different records');
141
+ const firstIds = firstPage.data.map((book) => book.id);
142
+ assert(previousIds.every((id) => firstIds.includes(id)), 'Page[before] should return earlier records');
143
+ });
144
+ });
@@ -2,6 +2,7 @@ import { describe, it, before, beforeEach, after } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
3
  import knexLib from 'knex';
4
4
  import { cleanTables } from './helpers/test-utils.js';
5
+ import { storageMode } from './helpers/storage-mode.js';
5
6
  import { createComputedFieldsApi } from './fixtures/api-configs.js';
6
7
 
7
8
  // Create Knex instance for tests
@@ -38,7 +39,7 @@ describe('Computed Fields and Sparse Fieldsets', () => {
38
39
  testData.product = product;
39
40
 
40
41
  const review1 = await api.resources.reviews.post({
41
- product_id: product.id,
42
+ product: product.id,
42
43
  reviewer_name: 'Alice',
43
44
  rating: 5,
44
45
  comment: 'Excellent product!',
@@ -49,7 +50,7 @@ describe('Computed Fields and Sparse Fieldsets', () => {
49
50
  testData.review1 = review1;
50
51
 
51
52
  const review2 = await api.resources.reviews.post({
52
- product_id: product.id,
53
+ product: product.id,
53
54
  reviewer_name: 'Bob',
54
55
  rating: 4,
55
56
  comment: 'Good value',
@@ -88,7 +89,7 @@ describe('Computed Fields and Sparse Fieldsets', () => {
88
89
 
89
90
  it('should handle null values in computed fields', async () => {
90
91
  const review = await api.resources.reviews.post({
91
- product_id: testData.product.id,
92
+ product: testData.product.id,
92
93
  reviewer_name: 'Charlie',
93
94
  rating: 3,
94
95
  comment: 'Average',
@@ -288,8 +289,8 @@ describe('Computed Fields and Sparse Fieldsets', () => {
288
289
  });
289
290
  });
290
291
 
291
- describe('Relationship _ids Fields', () => {
292
- it('should always include _ids fields regardless of sparse fieldsets', async () => {
292
+ describe('Relationship Fields', () => {
293
+ it('should always include minimal relationship objects regardless of sparse fieldsets', async () => {
293
294
  const product = await api.resources.products.get({
294
295
  id: testData.product.id,
295
296
  queryParams: {
@@ -297,12 +298,17 @@ describe('Computed Fields and Sparse Fieldsets', () => {
297
298
  }
298
299
  });
299
300
 
300
- // Relationship _ids should still be included
301
- assert.ok(Array.isArray(product.reviews_ids));
302
- assert.equal(product.reviews_ids.length, 2);
301
+ // Minimal relationship objects should still be included
302
+ assert.ok(Array.isArray(product.reviews));
303
+ assert.equal(product.reviews.length, 2);
304
+ assert.equal(product.reviews[0].id, testData.review1.id);
305
+ assert.equal(product.reviews[1].id, testData.review2.id);
306
+ // Should only have id property (minimal object)
307
+ assert.equal(Object.keys(product.reviews[0]).length, 1);
308
+ assert.equal(Object.keys(product.reviews[1]).length, 1);
303
309
  });
304
310
 
305
- it('should include _ids for empty relationships', async () => {
311
+ it('should include empty arrays for empty relationships', async () => {
306
312
  const newProduct = await api.resources.products.post({
307
313
  name: 'New Product',
308
314
  price: 50,
@@ -316,8 +322,8 @@ describe('Computed Fields and Sparse Fieldsets', () => {
316
322
  }
317
323
  });
318
324
 
319
- assert.ok(Array.isArray(fetched.reviews_ids));
320
- assert.equal(fetched.reviews_ids.length, 0);
325
+ assert.ok(Array.isArray(fetched.reviews));
326
+ assert.equal(fetched.reviews.length, 0);
321
327
  });
322
328
  });
323
329
 
@@ -340,6 +346,9 @@ describe('Computed Fields and Sparse Fieldsets', () => {
340
346
  tableName: 'test_faulty_products'
341
347
  });
342
348
  await api.resources.faulty_products.createKnexTable();
349
+ if (storageMode.isAnyApi()) {
350
+ storageMode.registerTable('test_faulty_products', 'faulty_products');
351
+ }
343
352
 
344
353
  const faulty = await api.resources.faulty_products.post({ value: 42 });
345
354
  const fetched = await api.resources.faulty_products.get({ id: faulty.id });
@@ -405,6 +414,9 @@ describe('Computed Fields and Sparse Fieldsets', () => {
405
414
  tableName: 'test_async_products'
406
415
  });
407
416
  await api.resources.async_products.createKnexTable();
417
+ if (storageMode.isAnyApi()) {
418
+ storageMode.registerTable('test_async_products', 'async_products');
419
+ }
408
420
 
409
421
  const product = await api.resources.async_products.post({
410
422
  name: 'Async Product',
@@ -435,6 +447,9 @@ describe('Computed Fields and Sparse Fieldsets', () => {
435
447
  tableName: 'test_failing_async_products'
436
448
  });
437
449
  await api.resources.failing_async_products.createKnexTable();
450
+ if (storageMode.isAnyApi()) {
451
+ storageMode.registerTable('test_failing_async_products', 'failing_async_products');
452
+ }
438
453
 
439
454
  const product = await api.resources.failing_async_products.post({ value: 42 });
440
455
  const fetched = await api.resources.failing_async_products.get({ id: product.id });
@@ -480,4 +495,4 @@ describe('Computed Fields and Sparse Fieldsets', () => {
480
495
  api.resources.products.vars.schemaInfo.computed.profit_amount.compute = originalProfitAmount;
481
496
  });
482
497
  });
483
- });
498
+ });
@@ -37,7 +37,6 @@ describe('CORS Plugin Tests', { timeout: 30000 }, () => {
37
37
  const Api = (await import('hooked-api')).Api;
38
38
  baseUrlApi = new Api({
39
39
  name: 'cors-baseurl-test-api',
40
- version: '1.0.0',
41
40
  log: { level: 'info' }
42
41
  });
43
42
 
@@ -48,7 +47,7 @@ describe('CORS Plugin Tests', { timeout: 30000 }, () => {
48
47
  await baseUrlApi.use((await import('../plugins/core/rest-api-knex-plugin.js')).RestApiKnexPlugin, { knex });
49
48
  await baseUrlApi.use((await import('../plugins/core/connectors/express-plugin.js')).ExpressPlugin, {
50
49
  app: baseUrlApp,
51
- mountPath: '/v1/api'
50
+ mountPath: '/api'
52
51
  });
53
52
 
54
53
  // Add basic scope
@@ -75,7 +74,7 @@ describe('CORS Plugin Tests', { timeout: 30000 }, () => {
75
74
  console.log('[TEST] CORS installation error:', error.message);
76
75
  console.log('[TEST] vars.transport.matchAll:', baseUrlApi.vars.transport?.matchAll);
77
76
  console.log('[TEST] mountPath from transport:', baseUrlApi.vars.transport?.mountPath);
78
- console.log('[TEST] Full wildcard path would be:', '/v1' + (baseUrlApi.vars.transport?.matchAll || '*'));
77
+ console.log('[TEST] Full wildcard path would be:', '' + (baseUrlApi.vars.transport?.matchAll || '*'));
79
78
  throw error;
80
79
  }
81
80
 
@@ -85,7 +84,7 @@ describe('CORS Plugin Tests', { timeout: 30000 }, () => {
85
84
 
86
85
  it('should handle OPTIONS preflight with baseUrl', async () => {
87
86
  const response = await request(baseUrlApp)
88
- .options('/v1/api/countries')
87
+ .options('/api/countries')
89
88
  .set('Origin', 'https://example.com')
90
89
  .set('Access-Control-Request-Method', 'POST')
91
90
  .set('Access-Control-Request-Headers', 'Content-Type');
@@ -105,7 +104,7 @@ describe('CORS Plugin Tests', { timeout: 30000 }, () => {
105
104
  // Just test the GET endpoint without creating data first
106
105
  // This tests CORS headers on the response regardless of data
107
106
  const response = await request(baseUrlApp)
108
- .get('/v1/api/countries')
107
+ .get('/api/countries')
109
108
  .set('Origin', 'https://example.com')
110
109
  .set('Accept', 'application/vnd.api+json');
111
110
 
@@ -124,7 +123,7 @@ describe('CORS Plugin Tests', { timeout: 30000 }, () => {
124
123
  it('should handle wildcard OPTIONS route with baseUrl', async () => {
125
124
  // Test a non-existent endpoint to check if wildcard OPTIONS still works
126
125
  const response = await request(baseUrlApp)
127
- .options('/v1/api/non-existent-endpoint')
126
+ .options('/api/non-existent-endpoint')
128
127
  .set('Origin', 'https://example.com')
129
128
  .set('Access-Control-Request-Method', 'POST');
130
129
 
@@ -430,7 +430,7 @@ describe('CURL HTTP Abstraction Layer Tests', () => {
430
430
  validateJsonApiStructure(response, true);
431
431
 
432
432
  assert.equal(response.data.length, 3, 'Should return 3 books per page');
433
- // Note: Links are now always included, using relative URLs when publicBaseUrl isn't configured
433
+ // Note: Links are now always included, using relative URLs whenreturnBasePath isn't configured
434
434
  });
435
435
 
436
436
  it('should handle sorting using CURL', async () => {