json-rest-api 1.0.28 → 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 +746 -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} +90 -31
- 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 -174
- 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 +67 -330
- 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 +205 -30
- package/plugins/core/lib/querying/relationship-identifiers.js +208 -0
- package/plugins/core/lib/querying/sort-helpers.js +20 -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 +238 -187
- 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 +111 -287
- 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 +109 -44
- 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 +142 -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 -2509
- 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 -1069
- 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 -385
- 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 -145
- 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 +38 -69
- 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 +39 -82
- 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 -1238
- 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 -150
- 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 -118
- package/tests/fixtures/api-configs.js +0 -2509
- 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-cursor-multifield.test.js +0 -643
- 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/test-development-guide.md +0 -370
- package/tests/virtual-fields.test.js +0 -404
|
@@ -1,404 +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 {
|
|
5
|
-
validateJsonApiStructure,
|
|
6
|
-
cleanTables,
|
|
7
|
-
createJsonApiDocument,
|
|
8
|
-
assertResourceAttributes
|
|
9
|
-
} from './helpers/test-utils.js'
|
|
10
|
-
import { Api } from 'hooked-api'
|
|
11
|
-
import { RestApiPlugin } from '../plugins/core/rest-api-plugin.js'
|
|
12
|
-
import { RestApiKnexPlugin } from '../plugins/core/rest-api-knex-plugin.js'
|
|
13
|
-
|
|
14
|
-
// Create Knex instance for tests - always use SQLite in-memory
|
|
15
|
-
const knex = knexLib({
|
|
16
|
-
client: 'better-sqlite3',
|
|
17
|
-
connection: {
|
|
18
|
-
filename: ':memory:'
|
|
19
|
-
},
|
|
20
|
-
useNullAsDefault: true
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
// API instance that persists across ALL tests
|
|
24
|
-
let api
|
|
25
|
-
|
|
26
|
-
describe('Virtual Fields Tests', () => {
|
|
27
|
-
// IMPORTANT: before() runs ONCE for the entire test suite
|
|
28
|
-
before(async () => {
|
|
29
|
-
// Create a custom API with virtual fields
|
|
30
|
-
api = new Api({
|
|
31
|
-
name: 'virtual-fields-test',
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
// Install plugins
|
|
35
|
-
await api.use(RestApiPlugin, {
|
|
36
|
-
simplifiedApi: false,
|
|
37
|
-
simplifiedTransport: false,
|
|
38
|
-
returnRecordApi: {
|
|
39
|
-
post: 'full',
|
|
40
|
-
put: 'full',
|
|
41
|
-
patch: 'full'
|
|
42
|
-
}
|
|
43
|
-
})
|
|
44
|
-
await api.use(RestApiKnexPlugin, { knex })
|
|
45
|
-
|
|
46
|
-
// Add a resource with virtual fields
|
|
47
|
-
await api.addResource('users', {
|
|
48
|
-
schema: {
|
|
49
|
-
id: { type: 'id' },
|
|
50
|
-
username: { type: 'string', required: true },
|
|
51
|
-
email: { type: 'string', required: true },
|
|
52
|
-
password: { type: 'string', required: true, hidden: true },
|
|
53
|
-
passwordConfirmation: { type: 'string', virtual: true },
|
|
54
|
-
termsAccepted: { type: 'boolean', virtual: true }
|
|
55
|
-
}
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
// Create table
|
|
59
|
-
await api.resources.users.createKnexTable()
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
// IMPORTANT: after() cleans up resources
|
|
63
|
-
after(async () => {
|
|
64
|
-
// Always destroy knex connection to allow tests to exit
|
|
65
|
-
await knex.destroy()
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
// IMPORTANT: beforeEach() cleans data but does NOT recreate API
|
|
69
|
-
beforeEach(async () => {
|
|
70
|
-
// Clean all tables - list ALL tables your tests use
|
|
71
|
-
await cleanTables(knex, ['users'])
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
describe('Virtual field validation', () => {
|
|
75
|
-
it('should accept virtual fields in POST request', async () => {
|
|
76
|
-
const doc = createJsonApiDocument('users', {
|
|
77
|
-
username: 'testuser',
|
|
78
|
-
email: 'test@example.com',
|
|
79
|
-
password: 'secret123',
|
|
80
|
-
passwordConfirmation: 'secret123',
|
|
81
|
-
termsAccepted: true
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
const result = await api.resources.users.post({
|
|
85
|
-
inputRecord: doc,
|
|
86
|
-
simplified: false
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
// Validate response structure - POST returns single resource, not array
|
|
90
|
-
validateJsonApiStructure(result)
|
|
91
|
-
assert.equal(result.data.type, 'users')
|
|
92
|
-
assert.ok(result.data.id)
|
|
93
|
-
|
|
94
|
-
// Virtual fields should be returned in response
|
|
95
|
-
assertResourceAttributes(result.data, {
|
|
96
|
-
username: 'testuser',
|
|
97
|
-
email: 'test@example.com',
|
|
98
|
-
passwordConfirmation: 'secret123',
|
|
99
|
-
termsAccepted: true
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
// Password should NOT be in response (security)
|
|
103
|
-
assert.ok(!('password' in result.data.attributes))
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
it('should not store virtual fields in database', async () => {
|
|
107
|
-
const doc = createJsonApiDocument('users', {
|
|
108
|
-
username: 'testuser2',
|
|
109
|
-
email: 'test2@example.com',
|
|
110
|
-
password: 'secret456',
|
|
111
|
-
passwordConfirmation: 'secret456',
|
|
112
|
-
termsAccepted: false
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
const createResult = await api.resources.users.post({
|
|
116
|
-
inputRecord: doc,
|
|
117
|
-
simplified: false
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
// Verify data was created
|
|
121
|
-
const userId = createResult.data.id
|
|
122
|
-
|
|
123
|
-
// Check database directly - virtual fields should not be stored
|
|
124
|
-
const dbRecord = await knex('users').where('id', userId).first()
|
|
125
|
-
assert.ok(dbRecord)
|
|
126
|
-
assert.equal(dbRecord.username, 'testuser2')
|
|
127
|
-
assert.equal(dbRecord.email, 'test2@example.com')
|
|
128
|
-
assert.ok(dbRecord.password) // Password is stored
|
|
129
|
-
|
|
130
|
-
// Virtual fields should NOT exist in database
|
|
131
|
-
assert.ok(!('passwordConfirmation' in dbRecord))
|
|
132
|
-
assert.ok(!('termsAccepted' in dbRecord))
|
|
133
|
-
})
|
|
134
|
-
|
|
135
|
-
it('should handle virtual fields in PATCH request', async () => {
|
|
136
|
-
// Create a user first
|
|
137
|
-
const createDoc = createJsonApiDocument('users', {
|
|
138
|
-
username: 'patchuser',
|
|
139
|
-
email: 'patch@example.com',
|
|
140
|
-
password: 'oldpassword'
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
const createResult = await api.resources.users.post({
|
|
144
|
-
inputRecord: createDoc,
|
|
145
|
-
simplified: false
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
const userId = createResult.data.id
|
|
149
|
-
|
|
150
|
-
// Update with virtual field
|
|
151
|
-
const patchDoc = {
|
|
152
|
-
data: {
|
|
153
|
-
type: 'users',
|
|
154
|
-
id: userId,
|
|
155
|
-
attributes: {
|
|
156
|
-
password: 'newpassword',
|
|
157
|
-
passwordConfirmation: 'newpassword'
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const patchResult = await api.resources.users.patch({
|
|
163
|
-
id: userId,
|
|
164
|
-
inputRecord: patchDoc,
|
|
165
|
-
simplified: false
|
|
166
|
-
})
|
|
167
|
-
|
|
168
|
-
// Virtual field should be in response
|
|
169
|
-
assert.equal(patchResult.data.attributes.passwordConfirmation, 'newpassword')
|
|
170
|
-
|
|
171
|
-
// Check database - passwordConfirmation should not be stored
|
|
172
|
-
const dbRecord = await knex('users').where('id', userId).first()
|
|
173
|
-
assert.ok(!('passwordConfirmation' in dbRecord))
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
it('should respect sparse fieldsets with virtual fields on PATCH', async () => {
|
|
177
|
-
// Create a user first
|
|
178
|
-
const createDoc = createJsonApiDocument('users', {
|
|
179
|
-
username: 'sparseuser',
|
|
180
|
-
email: 'sparse@example.com',
|
|
181
|
-
password: 'password123'
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
const createResult = await api.resources.users.post({
|
|
185
|
-
inputRecord: createDoc,
|
|
186
|
-
simplified: false
|
|
187
|
-
})
|
|
188
|
-
|
|
189
|
-
const userId = createResult.data.id
|
|
190
|
-
|
|
191
|
-
// Update with virtual field and request specific fields in response
|
|
192
|
-
const patchDoc = {
|
|
193
|
-
data: {
|
|
194
|
-
type: 'users',
|
|
195
|
-
id: userId,
|
|
196
|
-
attributes: {
|
|
197
|
-
email: 'newemail@example.com',
|
|
198
|
-
passwordConfirmation: 'password123'
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const patchResult = await api.resources.users.patch({
|
|
204
|
-
id: userId,
|
|
205
|
-
inputRecord: patchDoc,
|
|
206
|
-
queryParams: {
|
|
207
|
-
fields: {
|
|
208
|
-
users: 'email,passwordConfirmation'
|
|
209
|
-
}
|
|
210
|
-
},
|
|
211
|
-
simplified: false
|
|
212
|
-
})
|
|
213
|
-
|
|
214
|
-
// Should only have requested fields
|
|
215
|
-
assert.equal(Object.keys(patchResult.data.attributes).length, 2)
|
|
216
|
-
assert.equal(patchResult.data.attributes.email, 'newemail@example.com')
|
|
217
|
-
assert.equal(patchResult.data.attributes.passwordConfirmation, 'password123')
|
|
218
|
-
|
|
219
|
-
// Other fields should not be present
|
|
220
|
-
assert.ok(!('username' in patchResult.data.attributes))
|
|
221
|
-
})
|
|
222
|
-
|
|
223
|
-
it('should exclude virtual fields when not requested in sparse fieldsets', async () => {
|
|
224
|
-
// Create a user
|
|
225
|
-
const doc = createJsonApiDocument('users', {
|
|
226
|
-
username: 'excludeuser',
|
|
227
|
-
email: 'exclude@example.com',
|
|
228
|
-
password: 'password123',
|
|
229
|
-
passwordConfirmation: 'password123',
|
|
230
|
-
termsAccepted: true
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
const createResult = await api.resources.users.post({
|
|
234
|
-
inputRecord: doc,
|
|
235
|
-
simplified: false
|
|
236
|
-
})
|
|
237
|
-
|
|
238
|
-
const userId = createResult.data.id
|
|
239
|
-
|
|
240
|
-
// Fetch with sparse fieldsets NOT including virtual fields
|
|
241
|
-
const getResult = await api.resources.users.get({
|
|
242
|
-
id: userId,
|
|
243
|
-
queryParams: {
|
|
244
|
-
fields: {
|
|
245
|
-
users: 'username,email'
|
|
246
|
-
}
|
|
247
|
-
},
|
|
248
|
-
simplified: false
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
// Should only have requested fields
|
|
252
|
-
assert.equal(Object.keys(getResult.data.attributes).length, 2)
|
|
253
|
-
assert.equal(getResult.data.attributes.username, 'excludeuser')
|
|
254
|
-
assert.equal(getResult.data.attributes.email, 'exclude@example.com')
|
|
255
|
-
|
|
256
|
-
// Virtual fields should not be present
|
|
257
|
-
assert.ok(!('passwordConfirmation' in getResult.data.attributes))
|
|
258
|
-
assert.ok(!('termsAccepted' in getResult.data.attributes))
|
|
259
|
-
})
|
|
260
|
-
|
|
261
|
-
it('should handle PUT requests with virtual fields', async () => {
|
|
262
|
-
// Create initial record
|
|
263
|
-
const createDoc = createJsonApiDocument('users', {
|
|
264
|
-
username: 'putuser',
|
|
265
|
-
email: 'put@example.com',
|
|
266
|
-
password: 'initialpass'
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
const createResult = await api.resources.users.post({
|
|
270
|
-
inputRecord: createDoc,
|
|
271
|
-
simplified: false
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
const userId = createResult.data.id
|
|
275
|
-
|
|
276
|
-
// Replace entire record with PUT, including virtual fields
|
|
277
|
-
const putDoc = {
|
|
278
|
-
data: {
|
|
279
|
-
type: 'users',
|
|
280
|
-
id: userId,
|
|
281
|
-
attributes: {
|
|
282
|
-
username: 'updateduser',
|
|
283
|
-
email: 'updated@example.com',
|
|
284
|
-
password: 'newpass',
|
|
285
|
-
passwordConfirmation: 'newpass',
|
|
286
|
-
termsAccepted: false
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const putResult = await api.resources.users.put({
|
|
292
|
-
id: userId,
|
|
293
|
-
inputRecord: putDoc,
|
|
294
|
-
simplified: false
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
// Virtual fields should be in response
|
|
298
|
-
assertResourceAttributes(putResult.data, {
|
|
299
|
-
username: 'updateduser',
|
|
300
|
-
email: 'updated@example.com',
|
|
301
|
-
passwordConfirmation: 'newpass',
|
|
302
|
-
termsAccepted: false
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
// Check database - virtual fields should not be stored
|
|
306
|
-
const dbRecord = await knex('users').where('id', userId).first()
|
|
307
|
-
assert.equal(dbRecord.username, 'updateduser')
|
|
308
|
-
assert.equal(dbRecord.email, 'updated@example.com')
|
|
309
|
-
assert.ok(!('passwordConfirmation' in dbRecord))
|
|
310
|
-
assert.ok(!('termsAccepted' in dbRecord))
|
|
311
|
-
})
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
describe('Virtual fields vs computed fields', () => {
|
|
315
|
-
before(async () => {
|
|
316
|
-
// Add a resource with both virtual and computed fields
|
|
317
|
-
await api.addResource('products', {
|
|
318
|
-
schema: {
|
|
319
|
-
id: { type: 'id' },
|
|
320
|
-
name: { type: 'string', required: true },
|
|
321
|
-
price: { type: 'number', required: true },
|
|
322
|
-
cost: { type: 'number', required: true, normallyHidden: true },
|
|
323
|
-
priceInCents: { type: 'number', virtual: true },
|
|
324
|
-
discountCode: { type: 'string', virtual: true },
|
|
325
|
-
profitMargin: {
|
|
326
|
-
type: 'number',
|
|
327
|
-
computed: true,
|
|
328
|
-
dependencies: ['price', 'cost'],
|
|
329
|
-
compute: ({ attributes }) => {
|
|
330
|
-
if (!attributes.price || !attributes.cost) return null
|
|
331
|
-
return ((attributes.price - attributes.cost) / attributes.price * 100).toFixed(2)
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
-
await api.resources.products.createKnexTable()
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
beforeEach(async () => {
|
|
341
|
-
await cleanTables(knex, ['products'])
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
it('should handle both virtual and computed fields correctly', async () => {
|
|
345
|
-
// Create product with virtual fields
|
|
346
|
-
const doc = createJsonApiDocument('products', {
|
|
347
|
-
name: 'Test Product',
|
|
348
|
-
price: 100,
|
|
349
|
-
cost: 60,
|
|
350
|
-
priceInCents: 10000,
|
|
351
|
-
discountCode: 'SAVE20'
|
|
352
|
-
})
|
|
353
|
-
|
|
354
|
-
const result = await api.resources.products.post({
|
|
355
|
-
inputRecord: doc,
|
|
356
|
-
simplified: false
|
|
357
|
-
})
|
|
358
|
-
|
|
359
|
-
// Check response
|
|
360
|
-
assertResourceAttributes(result.data, {
|
|
361
|
-
name: 'Test Product',
|
|
362
|
-
price: 100,
|
|
363
|
-
profitMargin: '40.00', // Computed field
|
|
364
|
-
priceInCents: 10000, // Virtual field
|
|
365
|
-
discountCode: 'SAVE20' // Virtual field
|
|
366
|
-
})
|
|
367
|
-
|
|
368
|
-
// Cost should not be in response (normallyHidden)
|
|
369
|
-
assert.ok(!('cost' in result.data.attributes))
|
|
370
|
-
|
|
371
|
-
// Check database
|
|
372
|
-
const dbRecord = await knex('products').where('id', result.data.id).first()
|
|
373
|
-
assert.equal(dbRecord.name, 'Test Product')
|
|
374
|
-
assert.equal(dbRecord.price, 100)
|
|
375
|
-
assert.equal(dbRecord.cost, 60) // Cost is stored in DB
|
|
376
|
-
|
|
377
|
-
// Neither virtual nor computed fields should be in DB
|
|
378
|
-
assert.ok(!('profitMargin' in dbRecord))
|
|
379
|
-
assert.ok(!('priceInCents' in dbRecord))
|
|
380
|
-
assert.ok(!('discountCode' in dbRecord))
|
|
381
|
-
})
|
|
382
|
-
|
|
383
|
-
it('should calculate computed fields regardless of input', async () => {
|
|
384
|
-
// Create product without trying to set computed field
|
|
385
|
-
const doc = createJsonApiDocument('products', {
|
|
386
|
-
name: 'Test Product 2',
|
|
387
|
-
price: 200,
|
|
388
|
-
cost: 50
|
|
389
|
-
})
|
|
390
|
-
|
|
391
|
-
const result = await api.resources.products.post({
|
|
392
|
-
inputRecord: doc,
|
|
393
|
-
simplified: false
|
|
394
|
-
})
|
|
395
|
-
|
|
396
|
-
// Computed field should have calculated value
|
|
397
|
-
assert.equal(result.data.attributes.profitMargin, '75.00')
|
|
398
|
-
|
|
399
|
-
// Virtual fields were not provided, so they shouldn't be in response
|
|
400
|
-
assert.ok(!('priceInCents' in result.data.attributes))
|
|
401
|
-
assert.ok(!('discountCode' in result.data.attributes))
|
|
402
|
-
})
|
|
403
|
-
})
|
|
404
|
-
})
|