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.
- package/.claude/settings.local.json +9 -1
- package/AGENTS.md +34 -0
- package/CLAUDE.md +26 -1
- package/CRUSH.md +55 -0
- package/README.md +3 -3
- package/TODO.todo +8 -0
- package/docs/API.md +17 -18
- package/docs/COMPARISON.md +18 -13
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
- package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
- package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
- package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
- package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
- package/docs/GUIDE/index.md +3 -0
- package/docs/QUICKSTART.md +12 -12
- package/docs/index.md +2 -2
- package/docs/mini-guides/file-uploads-complete.md +0 -1
- package/examples/url-override-example.js +137 -0
- package/index.js +7 -1
- package/package.json +7 -11
- package/plugins/core/bulk-operations-plugin.js +2 -0
- package/plugins/core/connectors/express-plugin.js +43 -24
- package/plugins/core/connectors/lib/request-helpers.js +2 -2
- package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
- package/plugins/core/lib/anyapi/schema-utils.js +186 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
- package/plugins/core/lib/dbTablesOperations.js +305 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
- package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
- package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
- package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
- package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
- package/plugins/core/lib/querying/url-helpers.js +119 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
- package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
- package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
- package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
- package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
- package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
- package/plugins/core/lib/storage/storage-adapter.js +160 -0
- package/plugins/core/lib/writing/relationship-processor.js +4 -11
- package/plugins/core/rest-api-access.js +456 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
- package/plugins/core/rest-api-knex-plugin.js +226 -87
- package/plugins/core/rest-api-multihome-plugin.js +89 -27
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
- package/plugins/core/rest-api-plugin-methods/common.js +46 -10
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
- package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
- package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
- package/plugins/core/rest-api-plugin-methods/get.js +2 -5
- package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
- package/plugins/core/rest-api-plugin-methods/post.js +23 -11
- package/plugins/core/rest-api-plugin-methods/put.js +18 -21
- package/plugins/core/rest-api-plugin-methods/query.js +3 -4
- package/plugins/core/rest-api-plugin-methods/release.js +13 -0
- package/plugins/core/rest-api-plugin.js +3 -4
- package/plugins/core/rest-api-positioning-plugin.js +253 -76
- package/plugins/core/socketio-plugin.js +436 -502
- package/quickTest.js +2 -2
- package/scripts/debug-anyapi-review-filter.mjs +63 -0
- package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
- package/scripts/inspect-anyapi-descriptor.mjs +19 -0
- package/test-debug.js +59 -0
- package/tests/README.md +7 -1
- package/tests/anyapi-basic.test.js +102 -0
- package/tests/anyapi-cursor-pagination.test.js +144 -0
- package/tests/computed-fields.test.js +27 -12
- package/tests/cors.test.js +5 -6
- package/tests/curl.test.js +1 -1
- package/tests/custom-idproperty-relationships.test.js +37 -29
- package/tests/field-getters.test.js +25 -7
- package/tests/fixtures/api-configs.js +576 -150
- package/tests/helpers/storage-mode.js +65 -0
- package/tests/helpers/test-utils.js +85 -3
- package/tests/include-depth-validation.test.js +35 -1
- package/tests/nested-includes.test.js +130 -103
- package/tests/pagination-cursor-multifield.test.js +17 -13
- package/tests/pagination-enhanced.test.js +19 -24
- package/tests/polymorphic-simple.test.js +9 -9
- package/tests/positioning-advanced.test.js +19 -19
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +62 -62
- package/tests/positioning.test.js +7 -7
- package/tests/queries.test.js +5 -5
- package/tests/query-limits.test.js +77 -92
- package/tests/rest-api-access.test.js +206 -0
- package/tests/searchschema-merge.test.js +182 -0
- package/tests/socketio.test.js +33 -14
- package/tests/virtual-fields.test.js +0 -1
- package/docs/DOCS_ALL.md +0 -21894
- package/plugins/core/jwt-auth-plugin.js +0 -897
- package/plugins/core/lib/jwt-auth-helpers.js +0 -130
- 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',
|
|
23
|
+
const api = new Api({ name: 'book-catalog-api', logLevel: 'trace' });
|
|
24
24
|
|
|
25
25
|
// Install plugins
|
|
26
|
-
await api.use(RestApiPlugin, {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
292
|
-
it('should always include
|
|
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
|
-
//
|
|
301
|
-
assert.ok(Array.isArray(product.
|
|
302
|
-
assert.equal(product.
|
|
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
|
|
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.
|
|
320
|
-
assert.equal(fetched.
|
|
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
|
+
});
|
package/tests/cors.test.js
CHANGED
|
@@ -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: '/
|
|
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:', '
|
|
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('/
|
|
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('/
|
|
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('/
|
|
126
|
+
.options('/api/non-existent-endpoint')
|
|
128
127
|
.set('Origin', 'https://example.com')
|
|
129
128
|
.set('Access-Control-Request-Method', 'POST');
|
|
130
129
|
|
package/tests/curl.test.js
CHANGED
|
@@ -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
|
|
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 () => {
|