json-rest-api 1.0.29 → 2.1.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 +733 -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 +103 -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 +3838 -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 +37 -0
- package/index.js +3 -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 +70 -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 +586 -418
- 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 +120 -56
- package/plugins/core/lib/anyapi/stored-definition.js +109 -0
- 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 +290 -2475
- 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/anyapi.d.ts +30 -0
- 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 +65 -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
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
import { after, before, beforeEach, describe, it } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import knexLib from 'knex'
|
|
4
|
-
import { createCamelCaseBelongsToApi } from './fixtures/api-configs.js'
|
|
5
|
-
import {
|
|
6
|
-
createJsonApiDocument,
|
|
7
|
-
createRelationship,
|
|
8
|
-
resourceIdentifier,
|
|
9
|
-
assertResourceRelationship
|
|
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('Full JSON:API belongsTo linkage', () => {
|
|
23
|
-
before(async () => {
|
|
24
|
-
api = await createCamelCaseBelongsToApi(knex)
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
after(async () => {
|
|
28
|
-
await knex.destroy()
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
beforeEach(async () => {
|
|
32
|
-
await knex('camel_fk_publishers').delete()
|
|
33
|
-
await knex('camel_fk_countries').delete()
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
const createCountry = async () => {
|
|
37
|
-
return api.resources.countries.post({
|
|
38
|
-
inputRecord: createJsonApiDocument('countries', {
|
|
39
|
-
name: 'United States'
|
|
40
|
-
}),
|
|
41
|
-
simplified: false
|
|
42
|
-
})
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const createPublisher = async (countryId = null) => {
|
|
46
|
-
return api.resources.publishers.post({
|
|
47
|
-
inputRecord: createJsonApiDocument(
|
|
48
|
-
'publishers',
|
|
49
|
-
{ name: 'Mapped Publisher' },
|
|
50
|
-
{
|
|
51
|
-
country: createRelationship(
|
|
52
|
-
countryId == null ? null : resourceIdentifier('countries', countryId)
|
|
53
|
-
)
|
|
54
|
-
}
|
|
55
|
-
),
|
|
56
|
-
simplified: false
|
|
57
|
-
})
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
it('emits belongsTo linkage on primary resources without include for single and collection responses', async () => {
|
|
61
|
-
const country = await createCountry()
|
|
62
|
-
const publisher = await createPublisher(country.data.id)
|
|
63
|
-
|
|
64
|
-
assertResourceRelationship(
|
|
65
|
-
publisher.data,
|
|
66
|
-
'country',
|
|
67
|
-
resourceIdentifier('countries', country.data.id)
|
|
68
|
-
)
|
|
69
|
-
assert.equal(Object.hasOwn(publisher.data.attributes, 'countryId'), false)
|
|
70
|
-
|
|
71
|
-
const single = await api.resources.publishers.get({
|
|
72
|
-
id: publisher.data.id,
|
|
73
|
-
simplified: false
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
assertResourceRelationship(
|
|
77
|
-
single.data,
|
|
78
|
-
'country',
|
|
79
|
-
resourceIdentifier('countries', country.data.id)
|
|
80
|
-
)
|
|
81
|
-
assert.equal(Object.hasOwn(single.data.attributes, 'countryId'), false)
|
|
82
|
-
|
|
83
|
-
const collection = await api.resources.publishers.query({
|
|
84
|
-
simplified: false
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
assert.equal(collection.data.length, 1)
|
|
88
|
-
assertResourceRelationship(
|
|
89
|
-
collection.data[0],
|
|
90
|
-
'country',
|
|
91
|
-
resourceIdentifier('countries', country.data.id)
|
|
92
|
-
)
|
|
93
|
-
assert.equal(Object.hasOwn(collection.data[0].attributes, 'countryId'), false)
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('keeps primary belongsTo linkage when include is requested and adds included resources', async () => {
|
|
97
|
-
const country = await createCountry()
|
|
98
|
-
const publisher = await createPublisher(country.data.id)
|
|
99
|
-
|
|
100
|
-
const result = await api.resources.publishers.get({
|
|
101
|
-
id: publisher.data.id,
|
|
102
|
-
queryParams: {
|
|
103
|
-
include: ['country']
|
|
104
|
-
},
|
|
105
|
-
simplified: false,
|
|
106
|
-
returnFullRecord: 'full'
|
|
107
|
-
})
|
|
108
|
-
|
|
109
|
-
assertResourceRelationship(
|
|
110
|
-
result.data,
|
|
111
|
-
'country',
|
|
112
|
-
resourceIdentifier('countries', country.data.id)
|
|
113
|
-
)
|
|
114
|
-
assert.equal(Object.hasOwn(result.data.attributes, 'countryId'), false)
|
|
115
|
-
assert.equal(result.included?.length, 1)
|
|
116
|
-
assert.equal(result.included?.[0]?.type, 'countries')
|
|
117
|
-
assert.equal(result.included?.[0]?.id, country.data.id)
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
it('emits explicit null belongsTo linkage without exposing the foreign key attribute', async () => {
|
|
121
|
-
const country = await createCountry()
|
|
122
|
-
const publisher = await createPublisher(country.data.id)
|
|
123
|
-
|
|
124
|
-
const patched = await api.resources.publishers.patch({
|
|
125
|
-
id: publisher.data.id,
|
|
126
|
-
inputRecord: {
|
|
127
|
-
data: {
|
|
128
|
-
type: 'publishers',
|
|
129
|
-
id: publisher.data.id,
|
|
130
|
-
relationships: {
|
|
131
|
-
country: { data: null }
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
simplified: false,
|
|
136
|
-
returnFullRecord: 'full'
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
assert.equal(patched.data.relationships?.country?.data, null)
|
|
140
|
-
assert.equal(Object.hasOwn(patched.data.attributes, 'countryId'), false)
|
|
141
|
-
|
|
142
|
-
const single = await api.resources.publishers.get({
|
|
143
|
-
id: publisher.data.id,
|
|
144
|
-
simplified: false
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
assert.equal(single.data.relationships?.country?.data, null)
|
|
148
|
-
assert.equal(Object.hasOwn(single.data.attributes, 'countryId'), false)
|
|
149
|
-
})
|
|
150
|
-
})
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
export class FakeFastifyApp {
|
|
2
|
-
constructor () {
|
|
3
|
-
this.routes = []
|
|
4
|
-
this.parsers = []
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
route (definition) {
|
|
8
|
-
this.routes.push(definition)
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
addContentTypeParser (contentType, options, parser) {
|
|
12
|
-
this.parsers.push({ contentType, options, parser })
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
hasContentTypeParser (contentType) {
|
|
16
|
-
return this.parsers.some((entry) => entry.contentType === contentType)
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export class FakeReply {
|
|
21
|
-
constructor () {
|
|
22
|
-
this.statusCode = null
|
|
23
|
-
this.contentType = null
|
|
24
|
-
this.headers = {}
|
|
25
|
-
this.payload = undefined
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
code (statusCode) {
|
|
29
|
-
this.statusCode = statusCode
|
|
30
|
-
return this
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
type (contentType) {
|
|
34
|
-
this.contentType = contentType
|
|
35
|
-
return this
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
header (name, value) {
|
|
39
|
-
this.headers[name] = value
|
|
40
|
-
return this
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
send (payload) {
|
|
44
|
-
this.payload = payload
|
|
45
|
-
return this
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function findFastifyRoute (app, method, url) {
|
|
50
|
-
return app.routes.find((route) => route.method === method && route.url === url)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function invokeFastifyRoute (
|
|
54
|
-
app,
|
|
55
|
-
{
|
|
56
|
-
method,
|
|
57
|
-
routeUrl,
|
|
58
|
-
requestUrl = routeUrl,
|
|
59
|
-
headers = {},
|
|
60
|
-
body,
|
|
61
|
-
params = {},
|
|
62
|
-
query = {}
|
|
63
|
-
}
|
|
64
|
-
) {
|
|
65
|
-
const route = findFastifyRoute(app, method, routeUrl)
|
|
66
|
-
if (!route) {
|
|
67
|
-
throw new Error(`Fastify route not found: ${method} ${routeUrl}`)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const reply = new FakeReply()
|
|
71
|
-
const request = {
|
|
72
|
-
method,
|
|
73
|
-
url: requestUrl,
|
|
74
|
-
raw: { url: requestUrl },
|
|
75
|
-
headers,
|
|
76
|
-
body,
|
|
77
|
-
params,
|
|
78
|
-
query
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (typeof route.preValidation === 'function') {
|
|
82
|
-
await route.preValidation(request, reply)
|
|
83
|
-
if (reply.statusCode !== null || reply.payload !== undefined) {
|
|
84
|
-
return { route, request, reply }
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
try {
|
|
89
|
-
await route.handler(request, reply)
|
|
90
|
-
} catch (error) {
|
|
91
|
-
if (typeof route.errorHandler === 'function') {
|
|
92
|
-
await route.errorHandler(error, request, reply)
|
|
93
|
-
} else {
|
|
94
|
-
throw error
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return { route, request, reply }
|
|
99
|
-
}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
const STORAGE_MODE = process.env.JSON_REST_API_STORAGE === 'anyapi' ? 'anyapi' : 'knex'
|
|
2
|
-
const DEFAULT_TENANT = 'default'
|
|
3
|
-
|
|
4
|
-
const tableMetadata = new Map()
|
|
5
|
-
let resetMetadataFn = null
|
|
6
|
-
const linkMappings = new Map()
|
|
7
|
-
let currentTenant = DEFAULT_TENANT
|
|
8
|
-
|
|
9
|
-
export const storageMode = {
|
|
10
|
-
mode: STORAGE_MODE,
|
|
11
|
-
isAnyApi () {
|
|
12
|
-
return STORAGE_MODE === 'anyapi'
|
|
13
|
-
},
|
|
14
|
-
defaultTenant: DEFAULT_TENANT,
|
|
15
|
-
get currentTenant () {
|
|
16
|
-
return currentTenant
|
|
17
|
-
},
|
|
18
|
-
registerReset (fn) {
|
|
19
|
-
resetMetadataFn = fn
|
|
20
|
-
},
|
|
21
|
-
setCurrentTenant (tenantId) {
|
|
22
|
-
currentTenant = tenantId || DEFAULT_TENANT
|
|
23
|
-
},
|
|
24
|
-
registerTable (tableName, resourceName, tenantId = currentTenant || DEFAULT_TENANT) {
|
|
25
|
-
if (!tableName || !resourceName) return
|
|
26
|
-
tableMetadata.set(tableName, {
|
|
27
|
-
resource: resourceName,
|
|
28
|
-
tenantId: tenantId || DEFAULT_TENANT,
|
|
29
|
-
})
|
|
30
|
-
},
|
|
31
|
-
registerLink (
|
|
32
|
-
tableName,
|
|
33
|
-
ownerResource,
|
|
34
|
-
relationshipName,
|
|
35
|
-
relationshipKey,
|
|
36
|
-
inverseRelationshipKey,
|
|
37
|
-
tenantId = currentTenant || DEFAULT_TENANT
|
|
38
|
-
) {
|
|
39
|
-
if (!tableName || !ownerResource || !relationshipName) return
|
|
40
|
-
linkMappings.set(tableName, {
|
|
41
|
-
ownerResource,
|
|
42
|
-
relationshipName,
|
|
43
|
-
relationshipKey,
|
|
44
|
-
inverseRelationshipKey,
|
|
45
|
-
tenantId: tenantId || DEFAULT_TENANT,
|
|
46
|
-
})
|
|
47
|
-
},
|
|
48
|
-
getResourceForTable (tableName) {
|
|
49
|
-
return tableMetadata.get(tableName)?.resource
|
|
50
|
-
},
|
|
51
|
-
getTenantForTable (tableName) {
|
|
52
|
-
return tableMetadata.get(tableName)?.tenantId || DEFAULT_TENANT
|
|
53
|
-
},
|
|
54
|
-
getLinkInfo (tableName) {
|
|
55
|
-
return linkMappings.get(tableName)
|
|
56
|
-
},
|
|
57
|
-
clearRegistry () {
|
|
58
|
-
tableMetadata.clear()
|
|
59
|
-
linkMappings.clear()
|
|
60
|
-
currentTenant = DEFAULT_TENANT
|
|
61
|
-
if (resetMetadataFn) {
|
|
62
|
-
resetMetadataFn()
|
|
63
|
-
}
|
|
64
|
-
},
|
|
65
|
-
}
|
|
@@ -1,203 +0,0 @@
|
|
|
1
|
-
import assert from 'node:assert/strict'
|
|
2
|
-
import { storageMode } from './storage-mode.js'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Validates that a response follows JSON:API structure
|
|
6
|
-
*/
|
|
7
|
-
export function validateJsonApiStructure (response, isCollection = false) {
|
|
8
|
-
assert(response, 'Response should exist')
|
|
9
|
-
|
|
10
|
-
if (isCollection) {
|
|
11
|
-
assert(Array.isArray(response.data), 'Response data should be an array for collections')
|
|
12
|
-
} else {
|
|
13
|
-
assert(response.data && typeof response.data === 'object', 'Response data should be an object')
|
|
14
|
-
assert(!Array.isArray(response.data), 'Response data should not be an array for single resources')
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// Validate each resource object
|
|
18
|
-
const resources = isCollection ? response.data : [response.data]
|
|
19
|
-
resources.forEach(resource => {
|
|
20
|
-
assert(resource.type, 'Resource should have a type')
|
|
21
|
-
assert(resource.id, 'Resource should have an id')
|
|
22
|
-
assert(resource.attributes && typeof resource.attributes === 'object', 'Resource should have attributes object')
|
|
23
|
-
|
|
24
|
-
if (resource.relationships) {
|
|
25
|
-
assert(typeof resource.relationships === 'object', 'Relationships should be an object')
|
|
26
|
-
Object.values(resource.relationships).forEach(rel => {
|
|
27
|
-
assert(rel.data !== undefined || rel.links !== undefined || rel.meta !== undefined,
|
|
28
|
-
'Relationship should have data, links, or meta')
|
|
29
|
-
})
|
|
30
|
-
}
|
|
31
|
-
})
|
|
32
|
-
|
|
33
|
-
if (response.included) {
|
|
34
|
-
assert(Array.isArray(response.included), 'Included should be an array')
|
|
35
|
-
response.included.forEach(resource => {
|
|
36
|
-
assert(resource.type && resource.id, 'Included resources should have type and id')
|
|
37
|
-
})
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Creates a resource identifier for JSON:API relationships
|
|
43
|
-
*/
|
|
44
|
-
export function resourceIdentifier (type, id) {
|
|
45
|
-
return { type, id: String(id) }
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Cleans all records from the given tables
|
|
50
|
-
*/
|
|
51
|
-
export async function cleanTables (knex, tableNames) {
|
|
52
|
-
if (storageMode.isAnyApi()) {
|
|
53
|
-
const resourceGroups = new Map()
|
|
54
|
-
|
|
55
|
-
for (const table of tableNames) {
|
|
56
|
-
const resource = storageMode.getResourceForTable(table)
|
|
57
|
-
if (!resource) continue
|
|
58
|
-
const tenantId = storageMode.getTenantForTable(table)
|
|
59
|
-
const key = `${tenantId}::${resource}`
|
|
60
|
-
if (!resourceGroups.has(key)) {
|
|
61
|
-
resourceGroups.set(key, { resource, tenantId })
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
for (const { resource, tenantId } of resourceGroups.values()) {
|
|
66
|
-
await knex('any_records')
|
|
67
|
-
.where({ tenant_id: tenantId, resource })
|
|
68
|
-
.delete()
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
for (const table of tableNames) {
|
|
72
|
-
const linkInfo = storageMode.getLinkInfo(table)
|
|
73
|
-
if (linkInfo) {
|
|
74
|
-
const tenantId = linkInfo.tenantId || storageMode.defaultTenant
|
|
75
|
-
const relationshipKey = linkInfo.relationshipKey ||
|
|
76
|
-
`${tenantId}:${linkInfo.ownerResource}:${linkInfo.relationshipName}`
|
|
77
|
-
await knex('any_links')
|
|
78
|
-
.where({ tenant_id: tenantId })
|
|
79
|
-
.andWhere((builder) => {
|
|
80
|
-
builder.where('relationship', relationshipKey)
|
|
81
|
-
.orWhere('inverse_relationship', relationshipKey)
|
|
82
|
-
if (linkInfo.inverseRelationshipKey) {
|
|
83
|
-
builder.orWhere('relationship', linkInfo.inverseRelationshipKey)
|
|
84
|
-
.orWhere('inverse_relationship', linkInfo.inverseRelationshipKey)
|
|
85
|
-
}
|
|
86
|
-
})
|
|
87
|
-
.delete()
|
|
88
|
-
.catch(() => {})
|
|
89
|
-
}
|
|
90
|
-
try {
|
|
91
|
-
await knex(table).delete()
|
|
92
|
-
} catch (error) {
|
|
93
|
-
// Ignore missing tables in anyapi mode
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
} else {
|
|
97
|
-
for (const table of tableNames) {
|
|
98
|
-
await knex(table).delete()
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Counts records in a table
|
|
105
|
-
*/
|
|
106
|
-
export async function countRecords (knex, tableName) {
|
|
107
|
-
if (storageMode.isAnyApi()) {
|
|
108
|
-
const resource = storageMode.getResourceForTable(tableName)
|
|
109
|
-
const linkInfo = storageMode.getLinkInfo(tableName)
|
|
110
|
-
const tenantId = storageMode.getTenantForTable(tableName)
|
|
111
|
-
if (linkInfo) {
|
|
112
|
-
const relationshipKey = linkInfo.relationshipKey ||
|
|
113
|
-
`${tenantId}:${linkInfo.ownerResource}:${linkInfo.relationshipName}`
|
|
114
|
-
const result = await knex('any_links')
|
|
115
|
-
.where({ tenant_id: tenantId })
|
|
116
|
-
.andWhere((builder) => {
|
|
117
|
-
builder.where('relationship', relationshipKey)
|
|
118
|
-
.orWhere('inverse_relationship', relationshipKey)
|
|
119
|
-
})
|
|
120
|
-
.count('* as count')
|
|
121
|
-
.first()
|
|
122
|
-
return parseInt(result?.count ?? 0, 10)
|
|
123
|
-
}
|
|
124
|
-
if (resource) {
|
|
125
|
-
const result = await knex('any_records')
|
|
126
|
-
.where({
|
|
127
|
-
tenant_id: tenantId,
|
|
128
|
-
resource,
|
|
129
|
-
})
|
|
130
|
-
.count('* as count')
|
|
131
|
-
.first()
|
|
132
|
-
return parseInt(result.count)
|
|
133
|
-
}
|
|
134
|
-
try {
|
|
135
|
-
const result = await knex(tableName).count('* as count').first()
|
|
136
|
-
return parseInt(result?.count || 0)
|
|
137
|
-
} catch (error) {
|
|
138
|
-
return 0
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const result = await knex(tableName).count('* as count').first()
|
|
143
|
-
return parseInt(result.count)
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Helper to create a JSON:API document for POST/PUT/PATCH
|
|
148
|
-
*/
|
|
149
|
-
export function createJsonApiDocument (type, attributes, relationships = {}) {
|
|
150
|
-
const doc = {
|
|
151
|
-
data: {
|
|
152
|
-
type,
|
|
153
|
-
attributes
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if (Object.keys(relationships).length > 0) {
|
|
158
|
-
doc.data.relationships = relationships
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return doc
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Helper to create a relationship object
|
|
166
|
-
*/
|
|
167
|
-
export function createRelationship (resourceIdentifier) {
|
|
168
|
-
return {
|
|
169
|
-
data: resourceIdentifier
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Helper to create a to-many relationship object
|
|
175
|
-
*/
|
|
176
|
-
export function createToManyRelationship (resourceIdentifiers) {
|
|
177
|
-
return {
|
|
178
|
-
data: resourceIdentifiers
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Asserts that a resource has specific attributes
|
|
184
|
-
*/
|
|
185
|
-
export function assertResourceAttributes (resource, expectedAttributes) {
|
|
186
|
-
Object.entries(expectedAttributes).forEach(([key, value]) => {
|
|
187
|
-
assert.deepEqual(resource.attributes[key], value,
|
|
188
|
-
`Attribute ${key} should equal ${JSON.stringify(value)}`)
|
|
189
|
-
})
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Asserts that a resource has a specific relationship
|
|
194
|
-
*/
|
|
195
|
-
export function assertResourceRelationship (resource, relationshipName, expectedIdentifier) {
|
|
196
|
-
assert(resource.relationships, 'Resource should have relationships')
|
|
197
|
-
assert(resource.relationships[relationshipName], `Resource should have ${relationshipName} relationship`)
|
|
198
|
-
assert.deepEqual(
|
|
199
|
-
resource.relationships[relationshipName].data,
|
|
200
|
-
expectedIdentifier,
|
|
201
|
-
`Relationship ${relationshipName} should match expected identifier`
|
|
202
|
-
)
|
|
203
|
-
}
|