json-rest-api 1.0.29 → 2.0.0
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/LICENSE +10 -670
- package/LICENSE-GPL-3.0 +674 -0
- package/LICENSE-MIT +21 -0
- package/README.md +39 -30
- package/docs/API.md +744 -1273
- package/docs/GUIDE/01-getting-started.md +338 -0
- package/docs/GUIDE/02-resources-and-relationships.md +33 -0
- package/docs/GUIDE/03-running-example.md +87 -0
- package/docs/GUIDE/04-creating-and-querying.md +181 -0
- package/docs/GUIDE/05-belongs-to.md +177 -0
- package/docs/GUIDE/06-has-many.md +184 -0
- package/docs/GUIDE/07-polymorphic-relationships.md +191 -0
- package/docs/GUIDE/08-many-to-many.md +184 -0
- package/docs/GUIDE/09-pagination-and-sorting.md +149 -0
- package/docs/GUIDE/10-put-and-patch.md +174 -0
- package/docs/GUIDE/11-relationship-endpoints.md +174 -0
- package/docs/GUIDE/12-field-transformations.md +287 -0
- package/docs/GUIDE/13-hooks-and-lifecycle.md +479 -0
- package/docs/GUIDE/14-custom-resource-methods.md +125 -0
- package/docs/GUIDE/15-query-projections.md +266 -0
- package/docs/GUIDE/{GUIDE_X_Autofiltering.md → 16-autofiltering.md} +77 -15
- package/docs/GUIDE/{GUIDE_X_Row_Policies.md → 17-row-policies.md} +137 -44
- package/docs/GUIDE/18-labels.md +86 -0
- package/docs/GUIDE/19-managed-transactions.md +179 -0
- package/docs/GUIDE/20-transaction-outcomes.md +207 -0
- package/docs/GUIDE/21-schema-and-migrations.md +634 -0
- package/docs/GUIDE/22-version-field-migrations.md +208 -0
- package/docs/GUIDE/23-fastify.md +179 -0
- package/docs/GUIDE/24-cors.md +172 -0
- package/docs/GUIDE/{GUIDE_X_URL_Management.md → 25-url-management.md} +7 -1
- package/docs/GUIDE/26-file-uploads.md +278 -0
- package/docs/GUIDE/27-bulk-operations.md +277 -0
- package/docs/GUIDE/28-socketio.md +325 -0
- package/docs/GUIDE/29-writing-plugins.md +278 -0
- package/docs/GUIDE/30-backend-capabilities.md +100 -0
- package/docs/GUIDE/31-positioning.md +181 -0
- package/docs/GUIDE/{GUIDE_Y_Appendices.md → 32-date-and-time.md} +74 -30
- package/docs/GUIDE/33-migrating-to-v2.md +3827 -0
- package/docs/GUIDE/index.md +51 -59
- package/docs/QUICKSTART.md +120 -287
- package/docs/architecture.md +75 -0
- package/docs/contributing.md +178 -0
- package/index.d.ts +36 -0
- package/index.js +1 -22
- package/lib/enhanced-logger.js +91 -85
- package/lib/error-context.js +445 -5
- package/lib/error-formatter.js +219 -59
- package/lib/knex-transaction.js +64 -0
- package/lib/logger-types.d.ts +22 -0
- package/lib/missing-package.js +4 -0
- package/lib/rest-api-errors.js +103 -8
- package/lib/runtime/json-rest-api.js +172 -0
- package/lib/transaction-types.d.ts +59 -0
- package/package.json +97 -27
- package/plugins/core/bulk-operations-plugin.js +191 -184
- package/plugins/core/connectors/express-plugin.js +148 -219
- package/plugins/core/connectors/fastify-plugin.js +125 -82
- package/plugins/core/connectors/lib/busboy-detector.js +74 -156
- package/plugins/core/connectors/lib/connector-core.js +144 -20
- package/plugins/core/connectors/lib/formidable-detector.js +63 -120
- package/plugins/core/connectors/lib/http-validators.js +77 -0
- package/plugins/core/connectors/lib/multipart-helpers.js +18 -0
- package/plugins/core/connectors/lib/request-helpers.js +10 -1
- package/plugins/core/connectors/lib/transport-http-helpers.js +89 -8
- package/plugins/core/connectors/lib/transport-route-schemas.js +7 -4
- package/plugins/core/file-handling-plugin.js +87 -177
- package/plugins/core/lib/anyapi/anyapi-registry.js +213 -164
- package/plugins/core/lib/anyapi/canonical-link-store.js +507 -0
- package/plugins/core/lib/anyapi/canonical-relationship-reader.js +598 -0
- package/plugins/core/lib/anyapi/schema-utils.js +25 -16
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +0 -64
- package/plugins/core/lib/dbIntrospection.js +228 -21
- package/plugins/core/lib/dbTablesOperations.js +409 -92
- package/plugins/core/lib/fractional-positioning.js +12 -48
- package/plugins/core/lib/querying/identifier-query.js +19 -0
- package/plugins/core/lib/querying/include-query-helpers.js +364 -0
- package/plugins/core/lib/querying/include-to-many.js +428 -0
- package/plugins/core/lib/querying/include-to-one.js +579 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +31 -307
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +99 -401
- package/plugins/core/lib/querying/knex-pagination-helpers.js +48 -317
- package/plugins/core/lib/querying/knex-process-includes.js +5 -110
- package/plugins/core/lib/querying/knex-query-helpers-base.js +21 -56
- package/plugins/core/lib/querying/knex-query-helpers.js +298 -448
- package/plugins/core/lib/querying/knex-relationship-includes.js +120 -2138
- package/plugins/core/lib/querying/knex-window-queries.js +62 -253
- package/plugins/core/lib/querying/query-builder-utils.js +44 -4
- package/plugins/core/lib/querying/query-constraint.d.ts +12 -0
- package/plugins/core/lib/querying/query-constraint.js +27 -0
- package/plugins/core/lib/querying/query-field-sort-helpers.js +134 -37
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +13 -1
- package/plugins/core/lib/querying/storage-adapter-utils.js +13 -5
- package/plugins/core/lib/querying/url-helpers.js +11 -4
- package/plugins/core/lib/querying-writing/compile-schemas.js +148 -249
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +33 -118
- package/plugins/core/lib/querying-writing/database-capabilities.js +149 -223
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +179 -194
- package/plugins/core/lib/querying-writing/default-data-helpers.js +15 -45
- package/plugins/core/lib/querying-writing/field-selection-types.d.ts +43 -0
- package/plugins/core/lib/querying-writing/field-utils.js +113 -92
- package/plugins/core/lib/querying-writing/knex-constants.d.ts +18 -0
- package/plugins/core/lib/querying-writing/knex-constants.js +2 -26
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +100 -290
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +31 -120
- package/plugins/core/lib/querying-writing/query-field-helpers.js +108 -32
- package/plugins/core/lib/querying-writing/query-field-types.d.ts +60 -0
- package/plugins/core/lib/querying-writing/relationship-contracts.js +33 -13
- package/plugins/core/lib/querying-writing/request-contracts.js +93 -38
- package/plugins/core/lib/querying-writing/resource-id-normalization.js +23 -5
- package/plugins/core/lib/querying-writing/response-options.js +35 -0
- package/plugins/core/lib/querying-writing/schema-helpers.js +203 -324
- package/plugins/core/lib/querying-writing/scope-validations.js +92 -290
- package/plugins/core/lib/querying-writing/simplified-helpers.js +113 -712
- package/plugins/core/lib/storage/canonical-data-helpers.js +683 -0
- package/plugins/core/lib/storage/canonical-storage-mapping.js +51 -20
- package/plugins/core/lib/storage/ordinary-data-helpers.js +710 -0
- package/plugins/core/lib/storage/storage-adapter.js +128 -62
- package/plugins/core/lib/storage/storage-mapping.js +78 -28
- package/plugins/core/lib/storage/storage-types.d.ts +502 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +114 -284
- package/plugins/core/lib/writing/relationship-processor.js +94 -158
- package/plugins/core/lib/writing/resource-version.js +172 -0
- package/plugins/core/lib/writing/reverse-relationship-manipulations.js +110 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +116 -2485
- package/plugins/core/rest-api-autofilter-plugin.js +17 -19
- package/plugins/core/rest-api-cors-plugin.js +52 -205
- package/plugins/core/rest-api-knex-plugin.js +45 -1077
- package/plugins/core/rest-api-label-plugin.js +25 -38
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +9 -6
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +7 -26
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +20 -26
- package/plugins/core/rest-api-plugin-methods/add-route.js +4 -6
- package/plugins/core/rest-api-plugin-methods/apply-query-filters.js +42 -13
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +11 -5
- package/plugins/core/rest-api-plugin-methods/common.js +291 -395
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +49 -47
- package/plugins/core/rest-api-plugin-methods/delete.js +31 -25
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +108 -148
- package/plugins/core/rest-api-plugin-methods/get-related.js +113 -305
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +25 -18
- package/plugins/core/rest-api-plugin-methods/get.js +36 -68
- package/plugins/core/rest-api-plugin-methods/lifecycle-types.d.ts +225 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +28 -25
- package/plugins/core/rest-api-plugin-methods/patch.js +46 -99
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +49 -53
- package/plugins/core/rest-api-plugin-methods/post.js +51 -51
- package/plugins/core/rest-api-plugin-methods/put.js +69 -148
- package/plugins/core/rest-api-plugin-methods/query.js +37 -81
- package/plugins/core/rest-api-plugin-methods/release.js +2 -1
- package/plugins/core/rest-api-plugin.js +32 -46
- package/plugins/core/rest-api-positioning-plugin.js +183 -628
- package/plugins/core/rest-api-query-projections-plugin.js +19 -102
- package/plugins/core/rest-api-row-policy-plugin.js +3 -3
- package/plugins/core/socketio-plugin.js +364 -344
- package/plugins/storage/local-storage.js +40 -36
- package/types/autofilter.d.ts +57 -0
- package/types/bulk-methods.d.ts +63 -0
- package/types/errors.d.ts +118 -0
- package/types/fastify-options.d.ts +19 -0
- package/types/file-detectors.d.ts +33 -0
- package/types/file-storage.d.ts +48 -0
- package/types/hook-context.d.ts +180 -0
- package/types/plugin-options.d.ts +53 -0
- package/types/relationship-methods.d.ts +74 -0
- package/types/representations.d.ts +50 -0
- package/types/resource-methods.d.ts +95 -0
- package/types/resource-schema.d.ts +157 -0
- package/types/row-policy.d.ts +31 -0
- package/types/runtime.d.ts +132 -0
- package/types/transactions.d.ts +21 -0
- package/.claude/agents/library-guide-expert.md +0 -30
- package/.claude/agents/rest-api-internals-expert.md +0 -54
- package/.claude/settings.local.json +0 -70
- package/AGENTS.md +0 -34
- package/CLAUDE.md +0 -160
- package/CRUSH.md +0 -55
- package/JSKIT_BACKEND_MIGRATION_TRACKER.txt +0 -180
- package/SINGLE_VALIDATION_CONTRACT_TODO.txt +0 -172
- package/TODO.todo +0 -13
- package/connectors_ideas/fastify.js +0 -217
- package/connectors_ideas/koa-plugin.js +0 -112
- package/docs/.bundle/config +0 -2
- package/docs/COMPARISON.md +0 -433
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +0 -1057
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +0 -88
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +0 -808
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +0 -491
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +0 -398
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +0 -898
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +0 -712
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +0 -1247
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +0 -748
- package/docs/GUIDE/GUIDE_2_9_Relationships_Urls.md +0 -517
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +0 -66
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +0 -1252
- package/docs/GUIDE/GUIDE_5_Non-Db_Resources.md +0 -1107
- package/docs/GUIDE/GUIDE_6_Positioning.md +0 -692
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +0 -1491
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +0 -732
- package/docs/GUIDE/GUIDE_X_Cors.md +0 -1957
- package/docs/GUIDE/GUIDE_X_Fastify.md +0 -78
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +0 -729
- package/docs/GUIDE/GUIDE_X_Knex_Schema_And_Migrations.md +0 -370
- package/docs/GUIDE/GUIDE_X_Query_Projections.md +0 -159
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -1040
- package/docs/GUIDE/GUIDE_X_Writing_Plugins.md +0 -201
- package/docs/Gemfile +0 -6
- package/docs/Gemfile.lock +0 -308
- package/docs/ONBOARDING.md +0 -315
- package/docs/_config.yml +0 -36
- package/docs/_layouts/default.html +0 -123
- package/docs/apple-touch-icon.svg +0 -4
- package/docs/assets/css/style.scss +0 -392
- package/docs/favicon.svg +0 -4
- package/docs/index.md +0 -164
- package/docs/mini-guides/file-uploads-complete.md +0 -161
- package/eslint.config.js +0 -13
- package/examples/url-override-example.js +0 -137
- package/fixing_plan.md +0 -112
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +0 -657
- package/plugins/core/lib/querying-writing/return-record-settings.js +0 -47
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +0 -110
- package/plugins/storage/s3-storage.js +0 -93
- package/quickTest.js +0 -136
- package/scripts/debug-anyapi-review-filter.mjs +0 -63
- package/scripts/inspect-anyapi-descriptor-country.mjs +0 -19
- package/scripts/inspect-anyapi-descriptor.mjs +0 -19
- package/scripts/run-docs.js +0 -133
- package/test-debug.js +0 -59
- package/tests/README.md +0 -202
- package/tests/TEST_TEMPLATE.test.js +0 -178
- package/tests/anyapi-basic.test.js +0 -105
- package/tests/anyapi-cursor-pagination.test.js +0 -148
- package/tests/anyapi-custom-idproperty.test.js +0 -163
- package/tests/autofilter.test.js +0 -463
- package/tests/bulk-operations.test.js +0 -657
- package/tests/computed-fields.test.js +0 -586
- package/tests/cors.test.js +0 -834
- package/tests/curl.test.js +0 -467
- package/tests/custom-id-mysql-returning-fallback.test.js +0 -128
- package/tests/custom-id-simplified-post.test.js +0 -75
- package/tests/custom-idproperty-relationships.test.js +0 -932
- package/tests/db-introspection.test.js +0 -521
- package/tests/db-table-operations.test.js +0 -798
- package/tests/fastify-plugin.test.js +0 -184
- package/tests/field-getters.test.js +0 -370
- package/tests/field-setters.test.js +0 -306
- package/tests/file-handling.test.js +0 -155
- package/tests/fixtures/api-configs.js +0 -2566
- package/tests/full-jsonapi-belongsto-linkage.test.js +0 -150
- package/tests/helpers/fake-fastify.js +0 -99
- package/tests/helpers/storage-mode.js +0 -65
- package/tests/helpers/test-utils.js +0 -203
- package/tests/http-connectors-parity.test.js +0 -395
- package/tests/id-normalization.test.js +0 -444
- package/tests/include-depth-validation.test.js +0 -331
- package/tests/includes.test.js +0 -427
- package/tests/local-storage.test.js +0 -96
- package/tests/logical-id-write-validation.test.js +0 -151
- package/tests/nested-includes.test.js +0 -617
- package/tests/pagination-boundaries.test.js +0 -154
- package/tests/pagination-contract.test.js +0 -122
- package/tests/pagination-cursor-multifield.test.js +0 -641
- package/tests/pagination-enhanced.test.js +0 -563
- package/tests/polymorphic-relationships.test.js +0 -347
- package/tests/polymorphic-simple.test.js +0 -148
- package/tests/positioning-advanced.test.js +0 -692
- package/tests/positioning-extended.test.js +0 -915
- package/tests/positioning-multifilter.test.js +0 -699
- package/tests/positioning.test.js +0 -560
- package/tests/post-commit-hooks.test.js +0 -120
- package/tests/projected-fields.test.js +0 -319
- package/tests/public-exports.test.js +0 -15
- package/tests/put-create-parity.test.js +0 -216
- package/tests/queries.test.js +0 -521
- package/tests/query-limits.test.js +0 -584
- package/tests/relationship-endpoints.test.js +0 -620
- package/tests/relationship-include-storage-adapter.test.js +0 -56
- package/tests/relationships.test.js +0 -681
- package/tests/rest-api.test.js +0 -337
- package/tests/return-record-settings.test.js +0 -184
- package/tests/row-policy.test.js +0 -489
- package/tests/s3-storage.test.js +0 -38
- package/tests/searchschema-knex-behavior.test.js +0 -187
- package/tests/searchschema-merge.test.js +0 -151
- package/tests/socketio.test.js +0 -860
- package/tests/storage-mapping.test.js +0 -362
- package/tests/temporal-boundaries.test.js +0 -208
- package/tests/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
package/tests/includes.test.js
DELETED
|
@@ -1,427 +0,0 @@
|
|
|
1
|
-
import { describe, it, before, beforeEach, after } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import knexLib from 'knex'
|
|
4
|
-
import { createBasicApi, createExtendedApi } from './fixtures/api-configs.js'
|
|
5
|
-
import {
|
|
6
|
-
REST_API_FIELDSET_ERROR_CODE,
|
|
7
|
-
RestApiFieldsetError
|
|
8
|
-
} from '../index.js'
|
|
9
|
-
import {
|
|
10
|
-
validateJsonApiStructure,
|
|
11
|
-
resourceIdentifier,
|
|
12
|
-
cleanTables,
|
|
13
|
-
countRecords,
|
|
14
|
-
createJsonApiDocument,
|
|
15
|
-
createRelationship,
|
|
16
|
-
createToManyRelationship,
|
|
17
|
-
assertResourceAttributes,
|
|
18
|
-
assertResourceRelationship
|
|
19
|
-
} from './helpers/test-utils.js'
|
|
20
|
-
|
|
21
|
-
// Create Knex instance for tests
|
|
22
|
-
const knex = knexLib({
|
|
23
|
-
client: 'better-sqlite3',
|
|
24
|
-
connection: {
|
|
25
|
-
filename: ':memory:'
|
|
26
|
-
},
|
|
27
|
-
useNullAsDefault: true
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
// API instances that persist across tests
|
|
31
|
-
let basicApi
|
|
32
|
-
let extendedApi
|
|
33
|
-
|
|
34
|
-
describe('Include/Sideloading Operations', () => {
|
|
35
|
-
before(async () => {
|
|
36
|
-
// Initialize APIs once
|
|
37
|
-
basicApi = await createBasicApi(knex)
|
|
38
|
-
extendedApi = await createExtendedApi(knex)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
after(async () => {
|
|
42
|
-
// Close database connection to allow tests to exit
|
|
43
|
-
await knex.destroy()
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
describe('Single-Level Includes', () => {
|
|
47
|
-
const testData = {}
|
|
48
|
-
|
|
49
|
-
beforeEach(async () => {
|
|
50
|
-
// Clean all tables
|
|
51
|
-
await cleanTables(knex, [
|
|
52
|
-
'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
|
|
53
|
-
])
|
|
54
|
-
|
|
55
|
-
// Create test data
|
|
56
|
-
const countryDoc = createJsonApiDocument('countries', { name: 'Test Country', code: 'TC' })
|
|
57
|
-
const countryResult = await basicApi.resources.countries.post({
|
|
58
|
-
inputRecord: countryDoc,
|
|
59
|
-
simplified: false
|
|
60
|
-
})
|
|
61
|
-
testData.country = countryResult.data
|
|
62
|
-
|
|
63
|
-
const publisherDoc = createJsonApiDocument('publishers',
|
|
64
|
-
{ name: 'Test Publisher' },
|
|
65
|
-
{ country: createRelationship(resourceIdentifier('countries', testData.country.id)) }
|
|
66
|
-
)
|
|
67
|
-
const publisherResult = await basicApi.resources.publishers.post({
|
|
68
|
-
inputRecord: publisherDoc,
|
|
69
|
-
simplified: false
|
|
70
|
-
})
|
|
71
|
-
testData.publisher = publisherResult.data
|
|
72
|
-
|
|
73
|
-
const author1Doc = createJsonApiDocument('authors', { name: 'Author One' })
|
|
74
|
-
const author2Doc = createJsonApiDocument('authors', { name: 'Author Two' })
|
|
75
|
-
const author1Result = await basicApi.resources.authors.post({
|
|
76
|
-
inputRecord: author1Doc,
|
|
77
|
-
simplified: false
|
|
78
|
-
})
|
|
79
|
-
const author2Result = await basicApi.resources.authors.post({
|
|
80
|
-
inputRecord: author2Doc,
|
|
81
|
-
simplified: false
|
|
82
|
-
})
|
|
83
|
-
testData.authors = [author1Result.data, author2Result.data]
|
|
84
|
-
|
|
85
|
-
const bookDoc = createJsonApiDocument('books',
|
|
86
|
-
{ title: 'Test Book' },
|
|
87
|
-
{
|
|
88
|
-
country: createRelationship(resourceIdentifier('countries', testData.country.id)),
|
|
89
|
-
publisher: createRelationship(resourceIdentifier('publishers', testData.publisher.id)),
|
|
90
|
-
authors: createToManyRelationship([
|
|
91
|
-
resourceIdentifier('authors', testData.authors[0].id),
|
|
92
|
-
resourceIdentifier('authors', testData.authors[1].id)
|
|
93
|
-
])
|
|
94
|
-
}
|
|
95
|
-
)
|
|
96
|
-
const bookResult = await basicApi.resources.books.post({
|
|
97
|
-
inputRecord: bookDoc,
|
|
98
|
-
simplified: false
|
|
99
|
-
})
|
|
100
|
-
testData.book = bookResult.data
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('should include single belongsTo relationship', async () => {
|
|
104
|
-
const result = await basicApi.resources.books.get({
|
|
105
|
-
id: testData.book.id,
|
|
106
|
-
queryParams: {
|
|
107
|
-
include: ['publisher']
|
|
108
|
-
},
|
|
109
|
-
simplified: false
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
validateJsonApiStructure(result, false)
|
|
113
|
-
|
|
114
|
-
// Verify included array exists
|
|
115
|
-
assert(result.included, 'Should have included data')
|
|
116
|
-
assert.equal(result.included.length, 1, 'Should include one resource')
|
|
117
|
-
|
|
118
|
-
// Verify included publisher
|
|
119
|
-
const includedPublisher = result.included[0]
|
|
120
|
-
assert.equal(includedPublisher.type, 'publishers')
|
|
121
|
-
assert.equal(includedPublisher.id, testData.publisher.id)
|
|
122
|
-
assert.equal(includedPublisher.attributes.name, 'Test Publisher')
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
it('should include multiple relationships', async () => {
|
|
126
|
-
const result = await basicApi.resources.books.get({
|
|
127
|
-
id: testData.book.id,
|
|
128
|
-
queryParams: {
|
|
129
|
-
include: ['publisher', 'authors', 'country']
|
|
130
|
-
},
|
|
131
|
-
simplified: false
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
validateJsonApiStructure(result, false)
|
|
135
|
-
|
|
136
|
-
// Verify included array
|
|
137
|
-
assert(result.included)
|
|
138
|
-
assert.equal(result.included.length, 4, 'Should include 4 resources (1 publisher + 2 authors + 1 country)')
|
|
139
|
-
|
|
140
|
-
// Verify each type is included
|
|
141
|
-
const includedTypes = result.included.map(r => r.type)
|
|
142
|
-
assert(includedTypes.includes('publishers'))
|
|
143
|
-
assert(includedTypes.includes('authors'))
|
|
144
|
-
assert(includedTypes.includes('countries'))
|
|
145
|
-
|
|
146
|
-
// Verify specific resources
|
|
147
|
-
const authors = result.included.filter(r => r.type === 'authors')
|
|
148
|
-
assert.equal(authors.length, 2)
|
|
149
|
-
const authorNames = authors.map(a => a.attributes.name).sort()
|
|
150
|
-
assert.deepEqual(authorNames, ['Author One', 'Author Two'])
|
|
151
|
-
})
|
|
152
|
-
|
|
153
|
-
it('should include relationships in collection queries', async () => {
|
|
154
|
-
const result = await basicApi.resources.books.query({
|
|
155
|
-
queryParams: {
|
|
156
|
-
include: ['publisher', 'country']
|
|
157
|
-
},
|
|
158
|
-
simplified: false
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
validateJsonApiStructure(result, true)
|
|
162
|
-
|
|
163
|
-
// Should have one book
|
|
164
|
-
assert.equal(result.data.length, 1)
|
|
165
|
-
|
|
166
|
-
// Should have included resources
|
|
167
|
-
assert(result.included)
|
|
168
|
-
const includedTypes = result.included.map(r => r.type)
|
|
169
|
-
assert(includedTypes.includes('publishers'))
|
|
170
|
-
assert(includedTypes.includes('countries'))
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
it('should preserve typed fieldset errors across included relationships', async () => {
|
|
174
|
-
const requests = [
|
|
175
|
-
{
|
|
176
|
-
resource: basicApi.resources.countries,
|
|
177
|
-
id: testData.country.id,
|
|
178
|
-
include: 'books',
|
|
179
|
-
primaryType: 'countries',
|
|
180
|
-
includedType: 'books'
|
|
181
|
-
},
|
|
182
|
-
{
|
|
183
|
-
resource: basicApi.resources.books,
|
|
184
|
-
id: testData.book.id,
|
|
185
|
-
include: 'publisher',
|
|
186
|
-
primaryType: 'books',
|
|
187
|
-
includedType: 'publishers'
|
|
188
|
-
}
|
|
189
|
-
]
|
|
190
|
-
|
|
191
|
-
for (const request of requests) {
|
|
192
|
-
await assert.rejects(
|
|
193
|
-
request.resource.get({
|
|
194
|
-
id: request.id,
|
|
195
|
-
queryParams: {
|
|
196
|
-
include: [request.include],
|
|
197
|
-
fields: {
|
|
198
|
-
[request.primaryType]: 'id',
|
|
199
|
-
[request.includedType]: 'definitelyHidden'
|
|
200
|
-
}
|
|
201
|
-
},
|
|
202
|
-
simplified: false
|
|
203
|
-
}),
|
|
204
|
-
(error) => {
|
|
205
|
-
assert(error instanceof RestApiFieldsetError)
|
|
206
|
-
assert.equal(error.code, REST_API_FIELDSET_ERROR_CODE)
|
|
207
|
-
assert.equal(error.statusCode, 400)
|
|
208
|
-
assert.deepEqual(error.details, {
|
|
209
|
-
field: 'definitelyHidden',
|
|
210
|
-
resourceType: request.includedType
|
|
211
|
-
})
|
|
212
|
-
return true
|
|
213
|
-
}
|
|
214
|
-
)
|
|
215
|
-
}
|
|
216
|
-
})
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
describe('Nested Includes', () => {
|
|
220
|
-
const testData = {}
|
|
221
|
-
|
|
222
|
-
beforeEach(async () => {
|
|
223
|
-
await cleanTables(knex, [
|
|
224
|
-
'basic_countries', 'basic_publishers', 'basic_authors', 'basic_books', 'basic_book_authors'
|
|
225
|
-
])
|
|
226
|
-
|
|
227
|
-
// Create hierarchy: Country -> Publisher -> Book
|
|
228
|
-
const countryDoc = createJsonApiDocument('countries', { name: 'Nested Country', code: 'NC' })
|
|
229
|
-
const countryResult = await basicApi.resources.countries.post({
|
|
230
|
-
inputRecord: countryDoc,
|
|
231
|
-
simplified: false
|
|
232
|
-
})
|
|
233
|
-
testData.country = countryResult.data
|
|
234
|
-
|
|
235
|
-
const publisherDoc = createJsonApiDocument('publishers',
|
|
236
|
-
{ name: 'Nested Publisher' },
|
|
237
|
-
{ country: createRelationship(resourceIdentifier('countries', testData.country.id)) }
|
|
238
|
-
)
|
|
239
|
-
const publisherResult = await basicApi.resources.publishers.post({
|
|
240
|
-
inputRecord: publisherDoc,
|
|
241
|
-
simplified: false
|
|
242
|
-
})
|
|
243
|
-
testData.publisher = publisherResult.data
|
|
244
|
-
|
|
245
|
-
const bookDoc = createJsonApiDocument('books',
|
|
246
|
-
{ title: 'Nested Book' },
|
|
247
|
-
{
|
|
248
|
-
country: createRelationship(resourceIdentifier('countries', testData.country.id)),
|
|
249
|
-
publisher: createRelationship(resourceIdentifier('publishers', testData.publisher.id))
|
|
250
|
-
}
|
|
251
|
-
)
|
|
252
|
-
const bookResult = await basicApi.resources.books.post({
|
|
253
|
-
inputRecord: bookDoc,
|
|
254
|
-
simplified: false
|
|
255
|
-
})
|
|
256
|
-
testData.book = bookResult.data
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
it('should include nested relationships', async () => {
|
|
260
|
-
const result = await basicApi.resources.books.get({
|
|
261
|
-
id: testData.book.id,
|
|
262
|
-
queryParams: {
|
|
263
|
-
include: ['publisher.country']
|
|
264
|
-
},
|
|
265
|
-
simplified: false
|
|
266
|
-
})
|
|
267
|
-
|
|
268
|
-
validateJsonApiStructure(result, false)
|
|
269
|
-
|
|
270
|
-
// Should include both publisher and its country
|
|
271
|
-
assert(result.included)
|
|
272
|
-
assert.equal(result.included.length, 2)
|
|
273
|
-
|
|
274
|
-
// Find the resources
|
|
275
|
-
const publisher = result.included.find(r => r.type === 'publishers')
|
|
276
|
-
const country = result.included.find(r => r.type === 'countries')
|
|
277
|
-
|
|
278
|
-
assert(publisher, 'Should include publisher')
|
|
279
|
-
assert(country, 'Should include country')
|
|
280
|
-
|
|
281
|
-
// Verify the publisher has country relationship
|
|
282
|
-
assert(publisher.relationships?.country, 'Publisher should have country relationship')
|
|
283
|
-
assert.equal(publisher.relationships.country.data.id, country.id)
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
it('should handle multiple nested includes', async () => {
|
|
287
|
-
// Create an author to have more relationships
|
|
288
|
-
const authorDoc = createJsonApiDocument('authors', { name: 'Nested Author' })
|
|
289
|
-
const authorResult = await basicApi.resources.authors.post({
|
|
290
|
-
inputRecord: authorDoc,
|
|
291
|
-
simplified: false
|
|
292
|
-
})
|
|
293
|
-
|
|
294
|
-
// Update book to have author
|
|
295
|
-
const patchDoc = {
|
|
296
|
-
data: {
|
|
297
|
-
type: 'books',
|
|
298
|
-
id: String(testData.book.id),
|
|
299
|
-
relationships: {
|
|
300
|
-
authors: createToManyRelationship([
|
|
301
|
-
resourceIdentifier('authors', authorResult.data.id)
|
|
302
|
-
])
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
await basicApi.resources.books.patch({
|
|
307
|
-
id: testData.book.id,
|
|
308
|
-
inputRecord: patchDoc,
|
|
309
|
-
simplified: false
|
|
310
|
-
})
|
|
311
|
-
|
|
312
|
-
// Query with multiple nested includes
|
|
313
|
-
const result = await basicApi.resources.books.query({
|
|
314
|
-
queryParams: {
|
|
315
|
-
include: ['publisher.country', 'authors', 'country']
|
|
316
|
-
},
|
|
317
|
-
simplified: false
|
|
318
|
-
})
|
|
319
|
-
|
|
320
|
-
validateJsonApiStructure(result, true)
|
|
321
|
-
|
|
322
|
-
// Should have all included resources
|
|
323
|
-
assert(result.included)
|
|
324
|
-
const includedTypes = result.included.map(r => r.type)
|
|
325
|
-
assert(includedTypes.includes('publishers'))
|
|
326
|
-
assert(includedTypes.includes('countries'))
|
|
327
|
-
assert(includedTypes.includes('authors'))
|
|
328
|
-
|
|
329
|
-
// Verify no duplicates
|
|
330
|
-
const countryResources = result.included.filter(r => r.type === 'countries')
|
|
331
|
-
assert.equal(countryResources.length, 1, 'Should not duplicate country resource')
|
|
332
|
-
})
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
describe('Include with Sparse Fieldsets', () => {
|
|
336
|
-
const testData = {}
|
|
337
|
-
|
|
338
|
-
beforeEach(async () => {
|
|
339
|
-
await cleanTables(knex, [
|
|
340
|
-
'ext_countries', 'ext_publishers', 'ext_authors', 'ext_books', 'ext_book_authors'
|
|
341
|
-
])
|
|
342
|
-
|
|
343
|
-
// Use extended API for more fields
|
|
344
|
-
const countryDoc = createJsonApiDocument('countries', {
|
|
345
|
-
name: 'Field Test Country',
|
|
346
|
-
code: 'FT',
|
|
347
|
-
capital: 'Capital City',
|
|
348
|
-
population: 1000000,
|
|
349
|
-
currency: 'FTC'
|
|
350
|
-
})
|
|
351
|
-
const countryResult = await extendedApi.resources.countries.post({
|
|
352
|
-
inputRecord: countryDoc,
|
|
353
|
-
simplified: false
|
|
354
|
-
})
|
|
355
|
-
testData.country = countryResult.data
|
|
356
|
-
|
|
357
|
-
const publisherDoc = createJsonApiDocument('publishers',
|
|
358
|
-
{
|
|
359
|
-
name: 'Field Test Publisher',
|
|
360
|
-
founded_year: 2000,
|
|
361
|
-
website: 'https://example.com',
|
|
362
|
-
active: true
|
|
363
|
-
},
|
|
364
|
-
{ country: createRelationship(resourceIdentifier('countries', testData.country.id)) }
|
|
365
|
-
)
|
|
366
|
-
const publisherResult = await extendedApi.resources.publishers.post({
|
|
367
|
-
inputRecord: publisherDoc,
|
|
368
|
-
simplified: false
|
|
369
|
-
})
|
|
370
|
-
testData.publisher = publisherResult.data
|
|
371
|
-
|
|
372
|
-
const bookDoc = createJsonApiDocument('books',
|
|
373
|
-
{
|
|
374
|
-
title: 'Field Test Book',
|
|
375
|
-
isbn: '1234567890123',
|
|
376
|
-
pages: 300,
|
|
377
|
-
price: '29.99',
|
|
378
|
-
language: 'en'
|
|
379
|
-
},
|
|
380
|
-
{
|
|
381
|
-
country: createRelationship(resourceIdentifier('countries', testData.country.id)),
|
|
382
|
-
publisher: createRelationship(resourceIdentifier('publishers', testData.publisher.id))
|
|
383
|
-
}
|
|
384
|
-
)
|
|
385
|
-
const bookResult = await extendedApi.resources.books.post({
|
|
386
|
-
inputRecord: bookDoc,
|
|
387
|
-
simplified: false
|
|
388
|
-
})
|
|
389
|
-
testData.book = bookResult.data
|
|
390
|
-
})
|
|
391
|
-
|
|
392
|
-
it('should include relationships with sparse fieldsets', async () => {
|
|
393
|
-
const result = await extendedApi.resources.books.get({
|
|
394
|
-
id: testData.book.id,
|
|
395
|
-
queryParams: {
|
|
396
|
-
include: ['publisher', 'country'],
|
|
397
|
-
fields: {
|
|
398
|
-
books: 'title,isbn',
|
|
399
|
-
publishers: 'name',
|
|
400
|
-
countries: 'name,code'
|
|
401
|
-
}
|
|
402
|
-
},
|
|
403
|
-
simplified: false
|
|
404
|
-
})
|
|
405
|
-
|
|
406
|
-
validateJsonApiStructure(result, false)
|
|
407
|
-
|
|
408
|
-
// Verify main resource has only requested fields
|
|
409
|
-
assert(result.data.attributes.title)
|
|
410
|
-
assert(result.data.attributes.isbn)
|
|
411
|
-
assert(!result.data.attributes.pages, 'Should not include pages')
|
|
412
|
-
assert(!result.data.attributes.price, 'Should not include price')
|
|
413
|
-
|
|
414
|
-
// Verify included resources have only requested fields
|
|
415
|
-
const publisher = result.included.find(r => r.type === 'publishers')
|
|
416
|
-
assert(publisher.attributes.name)
|
|
417
|
-
assert(!publisher.attributes.founded_year, 'Should not include founded_year')
|
|
418
|
-
assert(!publisher.attributes.website, 'Should not include website')
|
|
419
|
-
|
|
420
|
-
const country = result.included.find(r => r.type === 'countries')
|
|
421
|
-
assert(country.attributes.name)
|
|
422
|
-
assert(country.attributes.code)
|
|
423
|
-
assert(!country.attributes.capital, 'Should not include capital')
|
|
424
|
-
assert(!country.attributes.population, 'Should not include population')
|
|
425
|
-
})
|
|
426
|
-
})
|
|
427
|
-
})
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
import { describe, it, beforeEach, afterEach } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { promises as fs } from 'node:fs'
|
|
4
|
-
import os from 'node:os'
|
|
5
|
-
import path from 'node:path'
|
|
6
|
-
import { LocalStorage } from '../plugins/storage/local-storage.js'
|
|
7
|
-
|
|
8
|
-
let tempRoot
|
|
9
|
-
|
|
10
|
-
async function createTempRoot () {
|
|
11
|
-
tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'json-rest-api-local-storage-'))
|
|
12
|
-
return tempRoot
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function createFile (filename = 'photo.png') {
|
|
16
|
-
return {
|
|
17
|
-
filename,
|
|
18
|
-
mimetype: 'image/png',
|
|
19
|
-
data: Buffer.from('x')
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
describe('LocalStorage path containment', () => {
|
|
24
|
-
beforeEach(async () => {
|
|
25
|
-
await createTempRoot()
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
afterEach(async () => {
|
|
29
|
-
await fs.rm(tempRoot, { recursive: true, force: true })
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
it('rejects custom generated paths that escape to sibling directories', async () => {
|
|
33
|
-
const directory = path.join(tempRoot, 'uploads')
|
|
34
|
-
const evilDirectory = path.join(tempRoot, 'uploads_evil')
|
|
35
|
-
await fs.mkdir(evilDirectory, { recursive: true })
|
|
36
|
-
|
|
37
|
-
const storage = new LocalStorage({
|
|
38
|
-
directory,
|
|
39
|
-
fileBaseUrl: '/uploads',
|
|
40
|
-
nameStrategy: 'custom',
|
|
41
|
-
nameGenerator: async () => '../uploads_evil/pwn'
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
await assert.rejects(
|
|
45
|
-
storage.upload(createFile()),
|
|
46
|
-
/Custom nameGenerator must return a basename, not a path/
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
await assert.rejects(
|
|
50
|
-
fs.access(path.join(evilDirectory, 'pwn.png')),
|
|
51
|
-
{ code: 'ENOENT' }
|
|
52
|
-
)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('rejects empty and drive-prefixed custom generated names', async () => {
|
|
56
|
-
const directory = path.join(tempRoot, 'uploads')
|
|
57
|
-
|
|
58
|
-
const emptyNameStorage = new LocalStorage({
|
|
59
|
-
directory,
|
|
60
|
-
nameStrategy: 'custom',
|
|
61
|
-
nameGenerator: async () => ' '
|
|
62
|
-
})
|
|
63
|
-
await assert.rejects(
|
|
64
|
-
emptyNameStorage.upload(createFile()),
|
|
65
|
-
/Custom nameGenerator must return a non-empty basename/
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
const driveNameStorage = new LocalStorage({
|
|
69
|
-
directory,
|
|
70
|
-
nameStrategy: 'custom',
|
|
71
|
-
nameGenerator: async () => 'C:evil'
|
|
72
|
-
})
|
|
73
|
-
await assert.rejects(
|
|
74
|
-
driveNameStorage.upload(createFile()),
|
|
75
|
-
/Custom nameGenerator must return a basename, not a path/
|
|
76
|
-
)
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('keeps safe custom basenames inside the configured directory', async () => {
|
|
80
|
-
const directory = path.join(tempRoot, 'uploads')
|
|
81
|
-
const storage = new LocalStorage({
|
|
82
|
-
directory,
|
|
83
|
-
fileBaseUrl: '/uploads',
|
|
84
|
-
nameStrategy: 'custom',
|
|
85
|
-
nameGenerator: async () => 'avatar'
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
const firstUrl = await storage.upload(createFile())
|
|
89
|
-
const secondUrl = await storage.upload(createFile())
|
|
90
|
-
|
|
91
|
-
assert.equal(firstUrl, '/uploads/avatar.png')
|
|
92
|
-
assert.equal(secondUrl, '/uploads/avatar_1.png')
|
|
93
|
-
assert.equal(await fs.readFile(path.join(directory, 'avatar.png'), 'utf8'), 'x')
|
|
94
|
-
assert.equal(await fs.readFile(path.join(directory, 'avatar_1.png'), 'utf8'), 'x')
|
|
95
|
-
})
|
|
96
|
-
})
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { describe, it, before, after, beforeEach } 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
|
-
import {
|
|
8
|
-
cleanTables,
|
|
9
|
-
validateJsonApiStructure,
|
|
10
|
-
} from './helpers/test-utils.js'
|
|
11
|
-
|
|
12
|
-
const knex = knexLib({
|
|
13
|
-
client: 'better-sqlite3',
|
|
14
|
-
connection: {
|
|
15
|
-
filename: ':memory:'
|
|
16
|
-
},
|
|
17
|
-
useNullAsDefault: true
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
let api
|
|
21
|
-
|
|
22
|
-
describe('Logical resource ids in write validation', () => {
|
|
23
|
-
before(async () => {
|
|
24
|
-
api = new Api({
|
|
25
|
-
name: 'logical-id-write-validation-test'
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
await api.use(RestApiPlugin, {
|
|
29
|
-
simplifiedApi: false,
|
|
30
|
-
simplifiedTransport: false,
|
|
31
|
-
returnRecordApi: {
|
|
32
|
-
post: 'full',
|
|
33
|
-
put: 'full',
|
|
34
|
-
patch: 'full'
|
|
35
|
-
}
|
|
36
|
-
})
|
|
37
|
-
await api.use(RestApiKnexPlugin, { knex })
|
|
38
|
-
|
|
39
|
-
await api.addResource('profiles', {
|
|
40
|
-
tableName: 'logical_id_profiles',
|
|
41
|
-
idProperty: 'user_id',
|
|
42
|
-
schema: {
|
|
43
|
-
id: { type: 'id', required: true, storage: { column: 'user_id' } },
|
|
44
|
-
name: { type: 'string', required: true, max: 64 }
|
|
45
|
-
}
|
|
46
|
-
})
|
|
47
|
-
await api.resources.profiles.createKnexTable()
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
after(async () => {
|
|
51
|
-
await knex.destroy()
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
beforeEach(async () => {
|
|
55
|
-
await cleanTables(knex, ['logical_id_profiles'])
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('accepts explicit resource ids in non-simplified POST without exposing id as an attribute', async () => {
|
|
59
|
-
const created = await api.resources.profiles.post({
|
|
60
|
-
inputRecord: {
|
|
61
|
-
data: {
|
|
62
|
-
type: 'profiles',
|
|
63
|
-
id: '101',
|
|
64
|
-
attributes: {
|
|
65
|
-
name: 'Alice'
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
validateJsonApiStructure(created)
|
|
72
|
-
assert.equal(created.data.id, '101')
|
|
73
|
-
assert.equal(created.data.attributes.name, 'Alice')
|
|
74
|
-
assert.ok(!('id' in created.data.attributes))
|
|
75
|
-
|
|
76
|
-
const dbRow = await knex('logical_id_profiles').where('user_id', 101).first()
|
|
77
|
-
assert.ok(dbRow)
|
|
78
|
-
assert.equal(String(dbRow.user_id), '101')
|
|
79
|
-
assert.equal(dbRow.name, 'Alice')
|
|
80
|
-
})
|
|
81
|
-
|
|
82
|
-
it('reports missing logical ids against data.id during POST validation', async () => {
|
|
83
|
-
await assert.rejects(
|
|
84
|
-
() => api.resources.profiles.post({
|
|
85
|
-
inputRecord: {
|
|
86
|
-
data: {
|
|
87
|
-
type: 'profiles',
|
|
88
|
-
attributes: {
|
|
89
|
-
name: 'Missing Id'
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}),
|
|
94
|
-
(error) => {
|
|
95
|
-
assert.equal(error.code, 'REST_API_VALIDATION')
|
|
96
|
-
assert.deepEqual(error.details?.fields, ['data.id'])
|
|
97
|
-
assert.equal(error.details?.violations?.[0]?.field, 'data.id')
|
|
98
|
-
return true
|
|
99
|
-
}
|
|
100
|
-
)
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
it('accepts explicit resource ids in simplified POST and still keeps id out of attributes', async () => {
|
|
104
|
-
const created = await api.resources.profiles.post({
|
|
105
|
-
id: '202',
|
|
106
|
-
name: 'Bob'
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
assert.equal(created.id, '202')
|
|
110
|
-
assert.equal(created.name, 'Bob')
|
|
111
|
-
|
|
112
|
-
const fetched = await api.resources.profiles.get({
|
|
113
|
-
id: '202',
|
|
114
|
-
simplified: false
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
validateJsonApiStructure(fetched)
|
|
118
|
-
assert.equal(fetched.data.id, '202')
|
|
119
|
-
assert.equal(fetched.data.attributes.name, 'Bob')
|
|
120
|
-
assert.ok(!('id' in fetched.data.attributes))
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
it('accepts logical id validation through PUT full replacement', async () => {
|
|
124
|
-
await knex('logical_id_profiles').insert({
|
|
125
|
-
user_id: 303,
|
|
126
|
-
name: 'Before'
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
const updated = await api.resources.profiles.put({
|
|
130
|
-
id: '303',
|
|
131
|
-
inputRecord: {
|
|
132
|
-
data: {
|
|
133
|
-
type: 'profiles',
|
|
134
|
-
id: '303',
|
|
135
|
-
attributes: {
|
|
136
|
-
name: 'After'
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
validateJsonApiStructure(updated)
|
|
143
|
-
assert.equal(updated.data.id, '303')
|
|
144
|
-
assert.equal(updated.data.attributes.name, 'After')
|
|
145
|
-
assert.ok(!('id' in updated.data.attributes))
|
|
146
|
-
|
|
147
|
-
const dbRow = await knex('logical_id_profiles').where('user_id', 303).first()
|
|
148
|
-
assert.ok(dbRow)
|
|
149
|
-
assert.equal(dbRow.name, 'After')
|
|
150
|
-
})
|
|
151
|
-
})
|