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
|
@@ -1,586 +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 { cleanTables } from './helpers/test-utils.js'
|
|
5
|
-
import { storageMode } from './helpers/storage-mode.js'
|
|
6
|
-
import { createComputedFieldsApi } from './fixtures/api-configs.js'
|
|
7
|
-
import {
|
|
8
|
-
REST_API_FIELDSET_ERROR_CODE,
|
|
9
|
-
RestApiFieldsetError
|
|
10
|
-
} from '../index.js'
|
|
11
|
-
import { mapRestApiErrorToHttp } from '../plugins/core/connectors/lib/transport-http-helpers.js'
|
|
12
|
-
|
|
13
|
-
function assertFieldsetError (error, { field, resourceType }) {
|
|
14
|
-
assert(error instanceof RestApiFieldsetError)
|
|
15
|
-
assert.equal(error.code, REST_API_FIELDSET_ERROR_CODE)
|
|
16
|
-
assert.equal(error.statusCode, 400)
|
|
17
|
-
assert.deepEqual(error.details, { field, resourceType })
|
|
18
|
-
assert.equal(
|
|
19
|
-
error.message,
|
|
20
|
-
`Unknown sparse field '${field}' requested for '${resourceType}'`
|
|
21
|
-
)
|
|
22
|
-
return true
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Create Knex instance for tests
|
|
26
|
-
const knex = knexLib({
|
|
27
|
-
client: 'better-sqlite3',
|
|
28
|
-
connection: {
|
|
29
|
-
filename: ':memory:'
|
|
30
|
-
},
|
|
31
|
-
useNullAsDefault: true
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
describe('Computed Fields and Sparse Fieldsets', () => {
|
|
35
|
-
let api
|
|
36
|
-
const testData = {}
|
|
37
|
-
|
|
38
|
-
before(async () => {
|
|
39
|
-
api = await createComputedFieldsApi(knex)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
after(async () => {
|
|
43
|
-
await knex.destroy()
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
beforeEach(async () => {
|
|
47
|
-
await cleanTables(knex, ['test_products', 'test_reviews'])
|
|
48
|
-
|
|
49
|
-
// Create test data
|
|
50
|
-
const product = await api.resources.products.post({
|
|
51
|
-
name: 'Premium Widget',
|
|
52
|
-
price: 99.99,
|
|
53
|
-
cost: 45.00,
|
|
54
|
-
internal_notes: 'Supplier: ABC Corp'
|
|
55
|
-
})
|
|
56
|
-
testData.product = product
|
|
57
|
-
|
|
58
|
-
const review1 = await api.resources.reviews.post({
|
|
59
|
-
product: product.id,
|
|
60
|
-
reviewer_name: 'Alice',
|
|
61
|
-
rating: 5,
|
|
62
|
-
comment: 'Excellent product!',
|
|
63
|
-
helpful_votes: 45,
|
|
64
|
-
total_votes: 50,
|
|
65
|
-
spam_score: 0.1
|
|
66
|
-
})
|
|
67
|
-
testData.review1 = review1
|
|
68
|
-
|
|
69
|
-
const review2 = await api.resources.reviews.post({
|
|
70
|
-
product: product.id,
|
|
71
|
-
reviewer_name: 'Bob',
|
|
72
|
-
rating: 4,
|
|
73
|
-
comment: 'Good value',
|
|
74
|
-
helpful_votes: 8,
|
|
75
|
-
total_votes: 20,
|
|
76
|
-
spam_score: 0.2
|
|
77
|
-
})
|
|
78
|
-
testData.review2 = review2
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
describe('Basic Computed Fields', () => {
|
|
82
|
-
it('should compute fields automatically when fetching', async () => {
|
|
83
|
-
const product = await api.resources.products.get({ id: testData.product.id })
|
|
84
|
-
|
|
85
|
-
assert.equal(product.name, 'Premium Widget')
|
|
86
|
-
assert.equal(product.price, 99.99)
|
|
87
|
-
assert.equal(product.profit_margin, 55.00)
|
|
88
|
-
assert.equal(product.profit_amount, 54.99)
|
|
89
|
-
assert.equal(product.calculated_at, '2026-08-25T23:45:01Z')
|
|
90
|
-
assert.equal(product.calculated_time, '23:45:01.987')
|
|
91
|
-
|
|
92
|
-
// normallyHidden fields should not be included
|
|
93
|
-
assert.equal(product.cost, undefined)
|
|
94
|
-
assert.equal(product.internal_notes, undefined)
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
it('should normalize computed temporal values at the final return boundary', async () => {
|
|
98
|
-
const product = await api.resources.products.get({
|
|
99
|
-
id: testData.product.id,
|
|
100
|
-
queryParams: { include: ['reviews'] }
|
|
101
|
-
})
|
|
102
|
-
assert.equal(product.calculated_at, '2026-08-25T23:45:01Z')
|
|
103
|
-
assert.equal(product.calculated_time, '23:45:01.987')
|
|
104
|
-
assert.equal(product.reviews.length, 2)
|
|
105
|
-
for (const review of product.reviews) {
|
|
106
|
-
assert.equal(review.calculated_at, '2026-08-25T23:46:02Z')
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
const products = await api.resources.products.query()
|
|
110
|
-
assert.equal(products.data[0].calculated_at, '2026-08-25T23:45:01Z')
|
|
111
|
-
assert.equal(products.data[0].calculated_time, '23:45:01.987')
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it('should handle division by zero in computed fields', async () => {
|
|
115
|
-
const freeProduct = await api.resources.products.post({
|
|
116
|
-
name: 'Free Sample',
|
|
117
|
-
price: 0,
|
|
118
|
-
cost: 0
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
const fetched = await api.resources.products.get({ id: freeProduct.id })
|
|
122
|
-
assert.equal(fetched.profit_margin, 0)
|
|
123
|
-
assert.equal(fetched.profit_amount, 0)
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
it('should handle null values in computed fields', async () => {
|
|
127
|
-
const review = await api.resources.reviews.post({
|
|
128
|
-
product: testData.product.id,
|
|
129
|
-
reviewer_name: 'Charlie',
|
|
130
|
-
rating: 3,
|
|
131
|
-
comment: 'Average',
|
|
132
|
-
helpful_votes: 0,
|
|
133
|
-
total_votes: 0
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
const fetched = await api.resources.reviews.get({ id: review.id })
|
|
137
|
-
assert.equal(fetched.helpfulness_score, null)
|
|
138
|
-
assert.equal(fetched.is_helpful, null)
|
|
139
|
-
})
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
describe('Sparse Fieldsets with Computed Fields', () => {
|
|
143
|
-
it('should return only requested fields including computed', async () => {
|
|
144
|
-
const product = await api.resources.products.get({
|
|
145
|
-
id: testData.product.id,
|
|
146
|
-
queryParams: {
|
|
147
|
-
fields: { products: 'name,profit_margin' }
|
|
148
|
-
}
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
assert.equal(product.id, testData.product.id)
|
|
152
|
-
assert.equal(product.name, 'Premium Widget')
|
|
153
|
-
assert.equal(product.profit_margin, 55.00)
|
|
154
|
-
assert.equal(product.calculated_at, undefined)
|
|
155
|
-
assert.equal(product.calculated_time, undefined)
|
|
156
|
-
|
|
157
|
-
// Other fields should not be included
|
|
158
|
-
assert.equal(product.price, undefined)
|
|
159
|
-
assert.equal(product.profit_amount, undefined)
|
|
160
|
-
assert.equal(product.cost, undefined)
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
it('should fetch dependencies but not include them in response', async () => {
|
|
164
|
-
const product = await api.resources.products.get({
|
|
165
|
-
id: testData.product.id,
|
|
166
|
-
queryParams: {
|
|
167
|
-
fields: { products: 'profit_margin' }
|
|
168
|
-
}
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
assert.equal(product.id, testData.product.id)
|
|
172
|
-
assert.equal(product.profit_margin, 55.00)
|
|
173
|
-
|
|
174
|
-
// Dependencies should not be included
|
|
175
|
-
assert.equal(product.price, undefined)
|
|
176
|
-
assert.equal(product.cost, undefined)
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
it('should allow explicitly requesting normallyHidden dependencies', async () => {
|
|
180
|
-
const product = await api.resources.products.get({
|
|
181
|
-
id: testData.product.id,
|
|
182
|
-
queryParams: {
|
|
183
|
-
fields: { products: 'name,cost,profit_margin' }
|
|
184
|
-
}
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
assert.equal(product.name, 'Premium Widget')
|
|
188
|
-
assert.equal(product.cost, 45) // Explicitly requested
|
|
189
|
-
assert.equal(product.profit_margin, 55.00)
|
|
190
|
-
|
|
191
|
-
// Price was not requested
|
|
192
|
-
assert.equal(product.price, undefined)
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
it('should handle multiple computed fields with overlapping dependencies', async () => {
|
|
196
|
-
const product = await api.resources.products.get({
|
|
197
|
-
id: testData.product.id,
|
|
198
|
-
queryParams: {
|
|
199
|
-
fields: { products: 'profit_margin,profit_amount' }
|
|
200
|
-
}
|
|
201
|
-
})
|
|
202
|
-
|
|
203
|
-
assert.equal(product.profit_margin, 55.00)
|
|
204
|
-
assert.equal(product.profit_amount, 54.99)
|
|
205
|
-
|
|
206
|
-
// Shared dependencies should not be included
|
|
207
|
-
assert.equal(product.price, undefined)
|
|
208
|
-
assert.equal(product.cost, undefined)
|
|
209
|
-
})
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
describe('Computed Fields in Included Resources', () => {
|
|
213
|
-
it('should compute fields in hasMany included resources', async () => {
|
|
214
|
-
const product = await api.resources.products.get({
|
|
215
|
-
id: testData.product.id,
|
|
216
|
-
queryParams: {
|
|
217
|
-
include: ['reviews']
|
|
218
|
-
}
|
|
219
|
-
})
|
|
220
|
-
|
|
221
|
-
assert.equal(product.reviews.length, 2)
|
|
222
|
-
|
|
223
|
-
const review1 = product.reviews.find(r => r.reviewer_name === 'Alice')
|
|
224
|
-
assert.equal(review1.helpfulness_score, 90)
|
|
225
|
-
assert.equal(review1.is_helpful, true)
|
|
226
|
-
assert.equal(review1.spam_score, undefined) // normallyHidden
|
|
227
|
-
|
|
228
|
-
const review2 = product.reviews.find(r => r.reviewer_name === 'Bob')
|
|
229
|
-
assert.equal(review2.helpfulness_score, 40)
|
|
230
|
-
assert.equal(review2.is_helpful, false) // Has enough votes but not helpful enough
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
it('should apply sparse fieldsets to included resources with computed fields', async () => {
|
|
234
|
-
const product = await api.resources.products.get({
|
|
235
|
-
id: testData.product.id,
|
|
236
|
-
queryParams: {
|
|
237
|
-
include: ['reviews'],
|
|
238
|
-
fields: {
|
|
239
|
-
products: 'name',
|
|
240
|
-
reviews: 'reviewer_name,rating,helpfulness_score'
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
})
|
|
244
|
-
|
|
245
|
-
assert.equal(product.name, 'Premium Widget')
|
|
246
|
-
assert.equal(product.price, undefined)
|
|
247
|
-
assert.equal(product.profit_margin, undefined)
|
|
248
|
-
|
|
249
|
-
const review = product.reviews[0]
|
|
250
|
-
assert.ok(review.reviewer_name)
|
|
251
|
-
assert.ok(review.rating)
|
|
252
|
-
assert.ok(review.helpfulness_score !== undefined)
|
|
253
|
-
|
|
254
|
-
// Other fields should not be included
|
|
255
|
-
assert.equal(review.comment, undefined)
|
|
256
|
-
assert.equal(review.helpful_votes, undefined)
|
|
257
|
-
assert.equal(review.total_votes, undefined)
|
|
258
|
-
assert.equal(review.is_helpful, undefined) // Not requested
|
|
259
|
-
})
|
|
260
|
-
|
|
261
|
-
it('should handle computed dependencies in included resources', async () => {
|
|
262
|
-
const product = await api.resources.products.get({
|
|
263
|
-
id: testData.product.id,
|
|
264
|
-
queryParams: {
|
|
265
|
-
include: ['reviews'],
|
|
266
|
-
fields: {
|
|
267
|
-
products: 'name',
|
|
268
|
-
reviews: 'is_helpful' // Depends on helpful_votes, total_votes, spam_score
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
const review = product.reviews.find(r => r.id === testData.review1.id)
|
|
274
|
-
assert.equal(review.is_helpful, true)
|
|
275
|
-
|
|
276
|
-
// Dependencies should not be included
|
|
277
|
-
assert.equal(review.helpful_votes, undefined)
|
|
278
|
-
assert.equal(review.total_votes, undefined)
|
|
279
|
-
assert.equal(review.spam_score, undefined)
|
|
280
|
-
})
|
|
281
|
-
})
|
|
282
|
-
|
|
283
|
-
describe('Collection Queries with Computed Fields', () => {
|
|
284
|
-
it('should compute fields for all records in collection', async () => {
|
|
285
|
-
const products = await api.resources.products.query()
|
|
286
|
-
|
|
287
|
-
assert.equal(products.data.length, 1)
|
|
288
|
-
const product = products.data[0]
|
|
289
|
-
assert.equal(product.profit_margin, 55.00)
|
|
290
|
-
assert.equal(product.profit_amount, 54.99)
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
it('should apply sparse fieldsets to collections', async () => {
|
|
294
|
-
const products = await api.resources.products.query({
|
|
295
|
-
queryParams: {
|
|
296
|
-
fields: { products: 'name,profit_margin' }
|
|
297
|
-
}
|
|
298
|
-
})
|
|
299
|
-
|
|
300
|
-
const product = products.data[0]
|
|
301
|
-
assert.equal(product.name, 'Premium Widget')
|
|
302
|
-
assert.equal(product.profit_margin, 55.00)
|
|
303
|
-
assert.equal(product.price, undefined)
|
|
304
|
-
assert.equal(product.cost, undefined)
|
|
305
|
-
})
|
|
306
|
-
|
|
307
|
-
it('should handle includes with computed fields in collections', async () => {
|
|
308
|
-
const products = await api.resources.products.query({
|
|
309
|
-
queryParams: {
|
|
310
|
-
include: ['reviews'],
|
|
311
|
-
fields: {
|
|
312
|
-
products: 'name',
|
|
313
|
-
reviews: 'rating,helpfulness_score'
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
})
|
|
317
|
-
|
|
318
|
-
const product = products.data[0]
|
|
319
|
-
assert.equal(product.reviews.length, 2)
|
|
320
|
-
|
|
321
|
-
product.reviews.forEach(review => {
|
|
322
|
-
assert.ok(review.rating)
|
|
323
|
-
assert.ok(review.helpfulness_score !== undefined)
|
|
324
|
-
assert.equal(review.reviewer_name, undefined)
|
|
325
|
-
assert.equal(review.comment, undefined)
|
|
326
|
-
})
|
|
327
|
-
})
|
|
328
|
-
})
|
|
329
|
-
|
|
330
|
-
describe('Relationship Fields', () => {
|
|
331
|
-
it('should always include minimal relationship objects regardless of sparse fieldsets', async () => {
|
|
332
|
-
const product = await api.resources.products.get({
|
|
333
|
-
id: testData.product.id,
|
|
334
|
-
queryParams: {
|
|
335
|
-
fields: { products: 'name' }
|
|
336
|
-
}
|
|
337
|
-
})
|
|
338
|
-
|
|
339
|
-
// Minimal relationship objects should still be included
|
|
340
|
-
assert.ok(Array.isArray(product.reviews))
|
|
341
|
-
assert.equal(product.reviews.length, 2)
|
|
342
|
-
assert.equal(product.reviews[0].id, testData.review1.id)
|
|
343
|
-
assert.equal(product.reviews[1].id, testData.review2.id)
|
|
344
|
-
// Should only have id property (minimal object)
|
|
345
|
-
assert.equal(Object.keys(product.reviews[0]).length, 1)
|
|
346
|
-
assert.equal(Object.keys(product.reviews[1]).length, 1)
|
|
347
|
-
})
|
|
348
|
-
|
|
349
|
-
it('should include empty arrays for empty relationships', async () => {
|
|
350
|
-
const newProduct = await api.resources.products.post({
|
|
351
|
-
name: 'New Product',
|
|
352
|
-
price: 50,
|
|
353
|
-
cost: 25
|
|
354
|
-
})
|
|
355
|
-
|
|
356
|
-
const fetched = await api.resources.products.get({
|
|
357
|
-
id: newProduct.id,
|
|
358
|
-
queryParams: {
|
|
359
|
-
fields: { products: 'name' }
|
|
360
|
-
}
|
|
361
|
-
})
|
|
362
|
-
|
|
363
|
-
assert.ok(Array.isArray(fetched.reviews))
|
|
364
|
-
assert.equal(fetched.reviews.length, 0)
|
|
365
|
-
})
|
|
366
|
-
})
|
|
367
|
-
|
|
368
|
-
describe('Error Handling', () => {
|
|
369
|
-
it('should handle errors in compute functions gracefully', async () => {
|
|
370
|
-
// Add a product resource with a faulty computed field
|
|
371
|
-
await api.addResource('faulty_products', {
|
|
372
|
-
schema: {
|
|
373
|
-
id: { type: 'id' },
|
|
374
|
-
value: { type: 'number' },
|
|
375
|
-
bad_compute: {
|
|
376
|
-
type: 'string',
|
|
377
|
-
computed: true,
|
|
378
|
-
dependencies: ['value'],
|
|
379
|
-
compute: ({ attributes }) => {
|
|
380
|
-
throw new Error('Computation failed')
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
},
|
|
384
|
-
tableName: 'test_faulty_products'
|
|
385
|
-
})
|
|
386
|
-
await api.resources.faulty_products.createKnexTable()
|
|
387
|
-
if (storageMode.isAnyApi()) {
|
|
388
|
-
storageMode.registerTable('test_faulty_products', 'faulty_products')
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
const faulty = await api.resources.faulty_products.post({ value: 42 })
|
|
392
|
-
const fetched = await api.resources.faulty_products.get({ id: faulty.id })
|
|
393
|
-
|
|
394
|
-
assert.equal(fetched.value, 42)
|
|
395
|
-
assert.equal(fetched.bad_compute, null) // Error results in null
|
|
396
|
-
})
|
|
397
|
-
|
|
398
|
-
it('should reject unknown fields in sparse fieldsets', async () => {
|
|
399
|
-
await assert.rejects(
|
|
400
|
-
api.resources.products.get({
|
|
401
|
-
id: testData.product.id,
|
|
402
|
-
queryParams: {
|
|
403
|
-
fields: { products: 'name,unknown_field' }
|
|
404
|
-
}
|
|
405
|
-
}),
|
|
406
|
-
(error) => assertFieldsetError(error, {
|
|
407
|
-
field: 'unknown_field',
|
|
408
|
-
resourceType: 'products'
|
|
409
|
-
})
|
|
410
|
-
)
|
|
411
|
-
})
|
|
412
|
-
|
|
413
|
-
it('should reject requests for _ids fields in sparse fieldsets', async () => {
|
|
414
|
-
await assert.rejects(
|
|
415
|
-
api.resources.products.get({
|
|
416
|
-
id: testData.product.id,
|
|
417
|
-
queryParams: {
|
|
418
|
-
fields: { products: 'name,reviews_ids' }
|
|
419
|
-
}
|
|
420
|
-
}),
|
|
421
|
-
(error) => assertFieldsetError(error, {
|
|
422
|
-
field: 'reviews_ids',
|
|
423
|
-
resourceType: 'products'
|
|
424
|
-
})
|
|
425
|
-
)
|
|
426
|
-
})
|
|
427
|
-
|
|
428
|
-
it('should preserve fieldset errors from empty included relationships', async () => {
|
|
429
|
-
const productWithoutReviews = await api.resources.products.post({
|
|
430
|
-
name: 'No Reviews',
|
|
431
|
-
price: 25,
|
|
432
|
-
cost: 10
|
|
433
|
-
})
|
|
434
|
-
|
|
435
|
-
await assert.rejects(
|
|
436
|
-
api.resources.products.get({
|
|
437
|
-
id: productWithoutReviews.id,
|
|
438
|
-
queryParams: {
|
|
439
|
-
include: ['reviews'],
|
|
440
|
-
fields: {
|
|
441
|
-
products: 'name',
|
|
442
|
-
reviews: 'definitelyHidden'
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}),
|
|
446
|
-
(error) => assertFieldsetError(error, {
|
|
447
|
-
field: 'definitelyHidden',
|
|
448
|
-
resourceType: 'reviews'
|
|
449
|
-
})
|
|
450
|
-
)
|
|
451
|
-
})
|
|
452
|
-
|
|
453
|
-
it('should map fieldset errors to an HTTP 400 response', () => {
|
|
454
|
-
const error = new RestApiFieldsetError({
|
|
455
|
-
field: 'definitelyHidden',
|
|
456
|
-
resourceType: 'reviews'
|
|
457
|
-
})
|
|
458
|
-
|
|
459
|
-
assert.deepEqual(mapRestApiErrorToHttp(error), {
|
|
460
|
-
status: 400,
|
|
461
|
-
body: {
|
|
462
|
-
errors: [{
|
|
463
|
-
status: '400',
|
|
464
|
-
code: REST_API_FIELDSET_ERROR_CODE,
|
|
465
|
-
title: 'Invalid Sparse Fieldset',
|
|
466
|
-
detail: "Unknown sparse field 'definitelyHidden' requested for 'reviews'"
|
|
467
|
-
}]
|
|
468
|
-
}
|
|
469
|
-
})
|
|
470
|
-
})
|
|
471
|
-
})
|
|
472
|
-
|
|
473
|
-
describe('Async Computed Fields', () => {
|
|
474
|
-
it('should support async compute functions', async () => {
|
|
475
|
-
// Add a resource with async computed field
|
|
476
|
-
await api.addResource('async_products', {
|
|
477
|
-
schema: {
|
|
478
|
-
id: { type: 'id' },
|
|
479
|
-
name: { type: 'string', required: true },
|
|
480
|
-
external_id: { type: 'string' },
|
|
481
|
-
external_data: {
|
|
482
|
-
type: 'string',
|
|
483
|
-
computed: true,
|
|
484
|
-
dependencies: ['external_id'],
|
|
485
|
-
compute: async ({ attributes }) => {
|
|
486
|
-
// Simulate async operation (e.g., external API call)
|
|
487
|
-
await new Promise(resolve => setTimeout(resolve, 10))
|
|
488
|
-
return `fetched-${attributes.external_id}`
|
|
489
|
-
}
|
|
490
|
-
},
|
|
491
|
-
computed_name: {
|
|
492
|
-
type: 'string',
|
|
493
|
-
computed: true,
|
|
494
|
-
dependencies: ['name'],
|
|
495
|
-
compute: async ({ attributes }) => {
|
|
496
|
-
// Another async operation
|
|
497
|
-
await new Promise(resolve => setTimeout(resolve, 5))
|
|
498
|
-
return attributes.name.toUpperCase()
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
},
|
|
502
|
-
tableName: 'test_async_products'
|
|
503
|
-
})
|
|
504
|
-
await api.resources.async_products.createKnexTable()
|
|
505
|
-
if (storageMode.isAnyApi()) {
|
|
506
|
-
storageMode.registerTable('test_async_products', 'async_products')
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
const product = await api.resources.async_products.post({
|
|
510
|
-
name: 'Async Product',
|
|
511
|
-
external_id: 'ext-123'
|
|
512
|
-
})
|
|
513
|
-
|
|
514
|
-
const fetched = await api.resources.async_products.get({ id: product.id })
|
|
515
|
-
assert.equal(fetched.external_data, 'fetched-ext-123')
|
|
516
|
-
assert.equal(fetched.computed_name, 'ASYNC PRODUCT')
|
|
517
|
-
})
|
|
518
|
-
|
|
519
|
-
it('should handle errors in async compute functions', async () => {
|
|
520
|
-
// Add a resource with failing async computed field
|
|
521
|
-
await api.addResource('failing_async_products', {
|
|
522
|
-
schema: {
|
|
523
|
-
id: { type: 'id' },
|
|
524
|
-
value: { type: 'number' },
|
|
525
|
-
failing_async: {
|
|
526
|
-
type: 'string',
|
|
527
|
-
computed: true,
|
|
528
|
-
dependencies: ['value'],
|
|
529
|
-
compute: async ({ attributes }) => {
|
|
530
|
-
await new Promise(resolve => setTimeout(resolve, 5))
|
|
531
|
-
throw new Error('Async computation failed')
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
},
|
|
535
|
-
tableName: 'test_failing_async_products'
|
|
536
|
-
})
|
|
537
|
-
await api.resources.failing_async_products.createKnexTable()
|
|
538
|
-
if (storageMode.isAnyApi()) {
|
|
539
|
-
storageMode.registerTable('test_failing_async_products', 'failing_async_products')
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const product = await api.resources.failing_async_products.post({ value: 42 })
|
|
543
|
-
const fetched = await api.resources.failing_async_products.get({ id: product.id })
|
|
544
|
-
|
|
545
|
-
assert.equal(fetched.value, 42)
|
|
546
|
-
assert.equal(fetched.failing_async, null) // Error results in null
|
|
547
|
-
})
|
|
548
|
-
})
|
|
549
|
-
|
|
550
|
-
describe('Performance Considerations', () => {
|
|
551
|
-
it('should only compute requested computed fields', async () => {
|
|
552
|
-
// Track compute calls
|
|
553
|
-
let profitMarginCalls = 0
|
|
554
|
-
let profitAmountCalls = 0
|
|
555
|
-
|
|
556
|
-
// Override compute functions to track calls
|
|
557
|
-
const originalProfitMargin = api.resources.products.vars.schemaInfo.computed.profit_margin.compute
|
|
558
|
-
const originalProfitAmount = api.resources.products.vars.schemaInfo.computed.profit_amount.compute
|
|
559
|
-
|
|
560
|
-
api.resources.products.vars.schemaInfo.computed.profit_margin.compute = (ctx) => {
|
|
561
|
-
profitMarginCalls++
|
|
562
|
-
return originalProfitMargin(ctx)
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
api.resources.products.vars.schemaInfo.computed.profit_amount.compute = (ctx) => {
|
|
566
|
-
profitAmountCalls++
|
|
567
|
-
return originalProfitAmount(ctx)
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
// Request only profit_margin
|
|
571
|
-
await api.resources.products.get({
|
|
572
|
-
id: testData.product.id,
|
|
573
|
-
queryParams: {
|
|
574
|
-
fields: { products: 'name,profit_margin' }
|
|
575
|
-
}
|
|
576
|
-
})
|
|
577
|
-
|
|
578
|
-
assert.equal(profitMarginCalls, 1)
|
|
579
|
-
assert.equal(profitAmountCalls, 0) // Should not be computed
|
|
580
|
-
|
|
581
|
-
// Restore original functions
|
|
582
|
-
api.resources.products.vars.schemaInfo.computed.profit_margin.compute = originalProfitMargin
|
|
583
|
-
api.resources.products.vars.schemaInfo.computed.profit_amount.compute = originalProfitAmount
|
|
584
|
-
})
|
|
585
|
-
})
|
|
586
|
-
})
|