json-rest-api 1.0.14 → 1.0.16
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/connectors_ideas/fastify.js +66 -68
- package/connectors_ideas/koa-plugin.js +50 -51
- package/eslint.config.js +11 -2
- package/examples/url-override-example.js +63 -63
- package/index.js +13 -13
- package/lib/enhanced-logger.js +51 -51
- package/lib/error-formatter.js +61 -61
- package/lib/rest-api-errors.js +28 -28
- package/package.json +1 -1
- package/plugins/core/bulk-operations-plugin.js +142 -145
- package/plugins/core/connectors/express-plugin.js +210 -217
- package/plugins/core/connectors/lib/busboy-detector.js +84 -84
- package/plugins/core/connectors/lib/formidable-detector.js +50 -50
- package/plugins/core/connectors/lib/request-helpers.js +74 -74
- package/plugins/core/file-handling-plugin.js +125 -125
- package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
- package/plugins/core/lib/anyapi/schema-utils.js +101 -101
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
- package/plugins/core/lib/dbTablesOperations.js +134 -135
- package/plugins/core/lib/fractional-positioning.js +74 -76
- package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
- package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
- package/plugins/core/lib/querying/knex-process-includes.js +50 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
- package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
- package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
- package/plugins/core/lib/querying/knex-window-queries.js +89 -89
- package/plugins/core/lib/querying/url-helpers.js +35 -35
- package/plugins/core/lib/querying-writing/compile-schemas.js +99 -99
- package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
- package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
- package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
- package/plugins/core/lib/querying-writing/field-utils.js +44 -42
- package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
- package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
- package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
- package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
- package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
- package/plugins/core/lib/storage/storage-adapter.js +68 -68
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
- package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
- package/plugins/core/lib/writing/relationship-processor.js +58 -58
- package/plugins/core/rest-api-access.js +11 -15
- package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
- package/plugins/core/rest-api-cors-plugin.js +170 -171
- package/plugins/core/rest-api-knex-plugin.js +482 -493
- package/plugins/core/rest-api-multihome-plugin.js +247 -251
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
- package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
- package/plugins/core/rest-api-plugin-methods/common.js +295 -297
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
- package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
- package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
- package/plugins/core/rest-api-plugin-methods/get.js +86 -86
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
- package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
- package/plugins/core/rest-api-plugin-methods/post.js +61 -62
- package/plugins/core/rest-api-plugin-methods/put.js +86 -87
- package/plugins/core/rest-api-plugin-methods/query.js +88 -88
- package/plugins/core/rest-api-plugin-methods/release.js +5 -6
- package/plugins/core/rest-api-plugin.js +76 -78
- package/plugins/core/rest-api-positioning-plugin.js +251 -252
- package/plugins/core/socketio-plugin.js +240 -236
- package/plugins/storage/local-storage.js +103 -103
- package/plugins/storage/s3-storage.js +41 -42
- package/quickTest.js +37 -37
- package/scripts/debug-anyapi-review-filter.mjs +18 -18
- package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
- package/scripts/inspect-anyapi-descriptor.mjs +10 -10
- package/scripts/run-docs.js +112 -113
- package/test-debug.js +28 -28
- package/tests/TEST_TEMPLATE.test.js +51 -51
- package/tests/anyapi-basic.test.js +42 -42
- package/tests/anyapi-cursor-pagination.test.js +48 -48
- package/tests/bulk-operations.test.js +208 -208
- package/tests/computed-fields.test.js +204 -204
- package/tests/cors.test.js +412 -413
- package/tests/curl.test.js +275 -276
- package/tests/custom-idproperty-relationships.test.js +266 -266
- package/tests/field-getters.test.js +132 -132
- package/tests/field-setters.test.js +110 -110
- package/tests/fixtures/api-configs.js +699 -699
- package/tests/helpers/storage-mode.js +32 -32
- package/tests/helpers/test-utils.js +76 -76
- package/tests/include-depth-validation.test.js +76 -76
- package/tests/includes.test.js +145 -145
- package/tests/multihome.test.js +175 -175
- package/tests/nested-includes.test.js +255 -255
- package/tests/pagination-cursor-multifield.test.js +164 -164
- package/tests/pagination-enhanced.test.js +225 -225
- package/tests/polymorphic-relationships.test.js +96 -96
- package/tests/polymorphic-simple.test.js +44 -44
- package/tests/positioning-advanced.test.js +226 -226
- package/tests/positioning-extended.test.js +399 -399
- package/tests/positioning-multifilter.test.js +204 -204
- package/tests/positioning.test.js +266 -266
- package/tests/queries.test.js +184 -184
- package/tests/query-limits.test.js +154 -155
- package/tests/relationship-endpoints.test.js +127 -127
- package/tests/relationships.test.js +178 -179
- package/tests/rest-api-access.test.js +86 -86
- package/tests/rest-api.test.js +103 -103
- package/tests/searchschema-merge.test.js +93 -93
- package/tests/socketio.test.js +326 -326
- package/tests/virtual-fields.test.js +164 -164
|
@@ -1,9 +1,9 @@
|
|
|
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'
|
|
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
7
|
|
|
8
8
|
// Create Knex instance for tests
|
|
9
9
|
const knex = knexLib({
|
|
@@ -12,31 +12,31 @@ const knex = knexLib({
|
|
|
12
12
|
filename: ':memory:'
|
|
13
13
|
},
|
|
14
14
|
useNullAsDefault: true
|
|
15
|
-
})
|
|
15
|
+
})
|
|
16
16
|
|
|
17
17
|
describe('Computed Fields and Sparse Fieldsets', () => {
|
|
18
|
-
let api
|
|
19
|
-
|
|
18
|
+
let api
|
|
19
|
+
const testData = {}
|
|
20
20
|
|
|
21
21
|
before(async () => {
|
|
22
|
-
api = await createComputedFieldsApi(knex)
|
|
23
|
-
})
|
|
22
|
+
api = await createComputedFieldsApi(knex)
|
|
23
|
+
})
|
|
24
24
|
|
|
25
25
|
after(async () => {
|
|
26
|
-
await knex.destroy()
|
|
27
|
-
})
|
|
26
|
+
await knex.destroy()
|
|
27
|
+
})
|
|
28
28
|
|
|
29
29
|
beforeEach(async () => {
|
|
30
|
-
await cleanTables(knex, ['test_products', 'test_reviews'])
|
|
31
|
-
|
|
30
|
+
await cleanTables(knex, ['test_products', 'test_reviews'])
|
|
31
|
+
|
|
32
32
|
// Create test data
|
|
33
33
|
const product = await api.resources.products.post({
|
|
34
34
|
name: 'Premium Widget',
|
|
35
35
|
price: 99.99,
|
|
36
36
|
cost: 45.00,
|
|
37
37
|
internal_notes: 'Supplier: ABC Corp'
|
|
38
|
-
})
|
|
39
|
-
testData.product = product
|
|
38
|
+
})
|
|
39
|
+
testData.product = product
|
|
40
40
|
|
|
41
41
|
const review1 = await api.resources.reviews.post({
|
|
42
42
|
product: product.id,
|
|
@@ -46,8 +46,8 @@ describe('Computed Fields and Sparse Fieldsets', () => {
|
|
|
46
46
|
helpful_votes: 45,
|
|
47
47
|
total_votes: 50,
|
|
48
48
|
spam_score: 0.1
|
|
49
|
-
})
|
|
50
|
-
testData.review1 = review1
|
|
49
|
+
})
|
|
50
|
+
testData.review1 = review1
|
|
51
51
|
|
|
52
52
|
const review2 = await api.resources.reviews.post({
|
|
53
53
|
product: product.id,
|
|
@@ -57,35 +57,35 @@ describe('Computed Fields and Sparse Fieldsets', () => {
|
|
|
57
57
|
helpful_votes: 8,
|
|
58
58
|
total_votes: 20,
|
|
59
59
|
spam_score: 0.2
|
|
60
|
-
})
|
|
61
|
-
testData.review2 = review2
|
|
62
|
-
})
|
|
60
|
+
})
|
|
61
|
+
testData.review2 = review2
|
|
62
|
+
})
|
|
63
63
|
|
|
64
64
|
describe('Basic Computed Fields', () => {
|
|
65
65
|
it('should compute fields automatically when fetching', async () => {
|
|
66
|
-
const product = await api.resources.products.get({ id: testData.product.id })
|
|
67
|
-
|
|
68
|
-
assert.equal(product.name, 'Premium Widget')
|
|
69
|
-
assert.equal(product.price, 99.99)
|
|
70
|
-
assert.equal(product.profit_margin, 55.00)
|
|
71
|
-
assert.equal(product.profit_amount, 54.99)
|
|
72
|
-
|
|
66
|
+
const product = await api.resources.products.get({ id: testData.product.id })
|
|
67
|
+
|
|
68
|
+
assert.equal(product.name, 'Premium Widget')
|
|
69
|
+
assert.equal(product.price, 99.99)
|
|
70
|
+
assert.equal(product.profit_margin, 55.00)
|
|
71
|
+
assert.equal(product.profit_amount, 54.99)
|
|
72
|
+
|
|
73
73
|
// normallyHidden fields should not be included
|
|
74
|
-
assert.equal(product.cost, undefined)
|
|
75
|
-
assert.equal(product.internal_notes, undefined)
|
|
76
|
-
})
|
|
74
|
+
assert.equal(product.cost, undefined)
|
|
75
|
+
assert.equal(product.internal_notes, undefined)
|
|
76
|
+
})
|
|
77
77
|
|
|
78
78
|
it('should handle division by zero in computed fields', async () => {
|
|
79
79
|
const freeProduct = await api.resources.products.post({
|
|
80
80
|
name: 'Free Sample',
|
|
81
81
|
price: 0,
|
|
82
82
|
cost: 0
|
|
83
|
-
})
|
|
83
|
+
})
|
|
84
84
|
|
|
85
|
-
const fetched = await api.resources.products.get({ id: freeProduct.id })
|
|
86
|
-
assert.equal(fetched.profit_margin, 0)
|
|
87
|
-
assert.equal(fetched.profit_amount, 0)
|
|
88
|
-
})
|
|
85
|
+
const fetched = await api.resources.products.get({ id: freeProduct.id })
|
|
86
|
+
assert.equal(fetched.profit_margin, 0)
|
|
87
|
+
assert.equal(fetched.profit_amount, 0)
|
|
88
|
+
})
|
|
89
89
|
|
|
90
90
|
it('should handle null values in computed fields', async () => {
|
|
91
91
|
const review = await api.resources.reviews.post({
|
|
@@ -95,13 +95,13 @@ describe('Computed Fields and Sparse Fieldsets', () => {
|
|
|
95
95
|
comment: 'Average',
|
|
96
96
|
helpful_votes: 0,
|
|
97
97
|
total_votes: 0
|
|
98
|
-
})
|
|
98
|
+
})
|
|
99
99
|
|
|
100
|
-
const fetched = await api.resources.reviews.get({ id: review.id })
|
|
101
|
-
assert.equal(fetched.helpfulness_score, null)
|
|
102
|
-
assert.equal(fetched.is_helpful, null)
|
|
103
|
-
})
|
|
104
|
-
})
|
|
100
|
+
const fetched = await api.resources.reviews.get({ id: review.id })
|
|
101
|
+
assert.equal(fetched.helpfulness_score, null)
|
|
102
|
+
assert.equal(fetched.is_helpful, null)
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
105
|
|
|
106
106
|
describe('Sparse Fieldsets with Computed Fields', () => {
|
|
107
107
|
it('should return only requested fields including computed', async () => {
|
|
@@ -110,17 +110,17 @@ describe('Computed Fields and Sparse Fieldsets', () => {
|
|
|
110
110
|
queryParams: {
|
|
111
111
|
fields: { products: 'name,profit_margin' }
|
|
112
112
|
}
|
|
113
|
-
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
assert.equal(product.id, testData.product.id)
|
|
116
|
+
assert.equal(product.name, 'Premium Widget')
|
|
117
|
+
assert.equal(product.profit_margin, 55.00)
|
|
114
118
|
|
|
115
|
-
assert.equal(product.id, testData.product.id);
|
|
116
|
-
assert.equal(product.name, 'Premium Widget');
|
|
117
|
-
assert.equal(product.profit_margin, 55.00);
|
|
118
|
-
|
|
119
119
|
// Other fields should not be included
|
|
120
|
-
assert.equal(product.price, undefined)
|
|
121
|
-
assert.equal(product.profit_amount, undefined)
|
|
122
|
-
assert.equal(product.cost, undefined)
|
|
123
|
-
})
|
|
120
|
+
assert.equal(product.price, undefined)
|
|
121
|
+
assert.equal(product.profit_amount, undefined)
|
|
122
|
+
assert.equal(product.cost, undefined)
|
|
123
|
+
})
|
|
124
124
|
|
|
125
125
|
it('should fetch dependencies but not include them in response', async () => {
|
|
126
126
|
const product = await api.resources.products.get({
|
|
@@ -128,15 +128,15 @@ describe('Computed Fields and Sparse Fieldsets', () => {
|
|
|
128
128
|
queryParams: {
|
|
129
129
|
fields: { products: 'profit_margin' }
|
|
130
130
|
}
|
|
131
|
-
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
assert.equal(product.id, testData.product.id)
|
|
134
|
+
assert.equal(product.profit_margin, 55.00)
|
|
132
135
|
|
|
133
|
-
assert.equal(product.id, testData.product.id);
|
|
134
|
-
assert.equal(product.profit_margin, 55.00);
|
|
135
|
-
|
|
136
136
|
// Dependencies should not be included
|
|
137
|
-
assert.equal(product.price, undefined)
|
|
138
|
-
assert.equal(product.cost, undefined)
|
|
139
|
-
})
|
|
137
|
+
assert.equal(product.price, undefined)
|
|
138
|
+
assert.equal(product.cost, undefined)
|
|
139
|
+
})
|
|
140
140
|
|
|
141
141
|
it('should allow explicitly requesting normallyHidden dependencies', async () => {
|
|
142
142
|
const product = await api.resources.products.get({
|
|
@@ -144,15 +144,15 @@ describe('Computed Fields and Sparse Fieldsets', () => {
|
|
|
144
144
|
queryParams: {
|
|
145
145
|
fields: { products: 'name,cost,profit_margin' }
|
|
146
146
|
}
|
|
147
|
-
})
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
assert.equal(product.name, 'Premium Widget')
|
|
150
|
+
assert.equal(product.cost, 45) // Explicitly requested
|
|
151
|
+
assert.equal(product.profit_margin, 55.00)
|
|
148
152
|
|
|
149
|
-
assert.equal(product.name, 'Premium Widget');
|
|
150
|
-
assert.equal(product.cost, 45); // Explicitly requested
|
|
151
|
-
assert.equal(product.profit_margin, 55.00);
|
|
152
|
-
|
|
153
153
|
// Price was not requested
|
|
154
|
-
assert.equal(product.price, undefined)
|
|
155
|
-
})
|
|
154
|
+
assert.equal(product.price, undefined)
|
|
155
|
+
})
|
|
156
156
|
|
|
157
157
|
it('should handle multiple computed fields with overlapping dependencies', async () => {
|
|
158
158
|
const product = await api.resources.products.get({
|
|
@@ -160,16 +160,16 @@ describe('Computed Fields and Sparse Fieldsets', () => {
|
|
|
160
160
|
queryParams: {
|
|
161
161
|
fields: { products: 'profit_margin,profit_amount' }
|
|
162
162
|
}
|
|
163
|
-
})
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
assert.equal(product.profit_margin, 55.00)
|
|
166
|
+
assert.equal(product.profit_amount, 54.99)
|
|
164
167
|
|
|
165
|
-
assert.equal(product.profit_margin, 55.00);
|
|
166
|
-
assert.equal(product.profit_amount, 54.99);
|
|
167
|
-
|
|
168
168
|
// Shared dependencies should not be included
|
|
169
|
-
assert.equal(product.price, undefined)
|
|
170
|
-
assert.equal(product.cost, undefined)
|
|
171
|
-
})
|
|
172
|
-
})
|
|
169
|
+
assert.equal(product.price, undefined)
|
|
170
|
+
assert.equal(product.cost, undefined)
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
173
|
|
|
174
174
|
describe('Computed Fields in Included Resources', () => {
|
|
175
175
|
it('should compute fields in hasMany included resources', async () => {
|
|
@@ -178,19 +178,19 @@ describe('Computed Fields and Sparse Fieldsets', () => {
|
|
|
178
178
|
queryParams: {
|
|
179
179
|
include: ['reviews']
|
|
180
180
|
}
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
assert.equal(product.reviews.length, 2)
|
|
184
|
-
|
|
185
|
-
const review1 = product.reviews.find(r => r.reviewer_name === 'Alice')
|
|
186
|
-
assert.equal(review1.helpfulness_score, 90)
|
|
187
|
-
assert.equal(review1.is_helpful, true)
|
|
188
|
-
assert.equal(review1.spam_score, undefined)
|
|
189
|
-
|
|
190
|
-
const review2 = product.reviews.find(r => r.reviewer_name === 'Bob')
|
|
191
|
-
assert.equal(review2.helpfulness_score, 40)
|
|
192
|
-
assert.equal(review2.is_helpful, false)
|
|
193
|
-
})
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
assert.equal(product.reviews.length, 2)
|
|
184
|
+
|
|
185
|
+
const review1 = product.reviews.find(r => r.reviewer_name === 'Alice')
|
|
186
|
+
assert.equal(review1.helpfulness_score, 90)
|
|
187
|
+
assert.equal(review1.is_helpful, true)
|
|
188
|
+
assert.equal(review1.spam_score, undefined) // normallyHidden
|
|
189
|
+
|
|
190
|
+
const review2 = product.reviews.find(r => r.reviewer_name === 'Bob')
|
|
191
|
+
assert.equal(review2.helpfulness_score, 40)
|
|
192
|
+
assert.equal(review2.is_helpful, false) // Has enough votes but not helpful enough
|
|
193
|
+
})
|
|
194
194
|
|
|
195
195
|
it('should apply sparse fieldsets to included resources with computed fields', async () => {
|
|
196
196
|
const product = await api.resources.products.get({
|
|
@@ -202,23 +202,23 @@ describe('Computed Fields and Sparse Fieldsets', () => {
|
|
|
202
202
|
reviews: 'reviewer_name,rating,helpfulness_score'
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
|
-
})
|
|
205
|
+
})
|
|
206
206
|
|
|
207
|
-
assert.equal(product.name, 'Premium Widget')
|
|
208
|
-
assert.equal(product.price, undefined)
|
|
209
|
-
assert.equal(product.profit_margin, undefined)
|
|
207
|
+
assert.equal(product.name, 'Premium Widget')
|
|
208
|
+
assert.equal(product.price, undefined)
|
|
209
|
+
assert.equal(product.profit_margin, undefined)
|
|
210
|
+
|
|
211
|
+
const review = product.reviews[0]
|
|
212
|
+
assert.ok(review.reviewer_name)
|
|
213
|
+
assert.ok(review.rating)
|
|
214
|
+
assert.ok(review.helpfulness_score !== undefined)
|
|
210
215
|
|
|
211
|
-
const review = product.reviews[0];
|
|
212
|
-
assert.ok(review.reviewer_name);
|
|
213
|
-
assert.ok(review.rating);
|
|
214
|
-
assert.ok(review.helpfulness_score !== undefined);
|
|
215
|
-
|
|
216
216
|
// Other fields should not be included
|
|
217
|
-
assert.equal(review.comment, undefined)
|
|
218
|
-
assert.equal(review.helpful_votes, undefined)
|
|
219
|
-
assert.equal(review.total_votes, undefined)
|
|
220
|
-
assert.equal(review.is_helpful, undefined)
|
|
221
|
-
})
|
|
217
|
+
assert.equal(review.comment, undefined)
|
|
218
|
+
assert.equal(review.helpful_votes, undefined)
|
|
219
|
+
assert.equal(review.total_votes, undefined)
|
|
220
|
+
assert.equal(review.is_helpful, undefined) // Not requested
|
|
221
|
+
})
|
|
222
222
|
|
|
223
223
|
it('should handle computed dependencies in included resources', async () => {
|
|
224
224
|
const product = await api.resources.products.get({
|
|
@@ -230,41 +230,41 @@ describe('Computed Fields and Sparse Fieldsets', () => {
|
|
|
230
230
|
reviews: 'is_helpful' // Depends on helpful_votes, total_votes, spam_score
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
|
-
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
const review = product.reviews.find(r => r.id === testData.review1.id)
|
|
236
|
+
assert.equal(review.is_helpful, true)
|
|
234
237
|
|
|
235
|
-
const review = product.reviews.find(r => r.id === testData.review1.id);
|
|
236
|
-
assert.equal(review.is_helpful, true);
|
|
237
|
-
|
|
238
238
|
// Dependencies should not be included
|
|
239
|
-
assert.equal(review.helpful_votes, undefined)
|
|
240
|
-
assert.equal(review.total_votes, undefined)
|
|
241
|
-
assert.equal(review.spam_score, undefined)
|
|
242
|
-
})
|
|
243
|
-
})
|
|
239
|
+
assert.equal(review.helpful_votes, undefined)
|
|
240
|
+
assert.equal(review.total_votes, undefined)
|
|
241
|
+
assert.equal(review.spam_score, undefined)
|
|
242
|
+
})
|
|
243
|
+
})
|
|
244
244
|
|
|
245
245
|
describe('Collection Queries with Computed Fields', () => {
|
|
246
246
|
it('should compute fields for all records in collection', async () => {
|
|
247
|
-
const products = await api.resources.products.query()
|
|
248
|
-
|
|
249
|
-
assert.equal(products.data.length, 1)
|
|
250
|
-
const product = products.data[0]
|
|
251
|
-
assert.equal(product.profit_margin, 55.00)
|
|
252
|
-
assert.equal(product.profit_amount, 54.99)
|
|
253
|
-
})
|
|
247
|
+
const products = await api.resources.products.query()
|
|
248
|
+
|
|
249
|
+
assert.equal(products.data.length, 1)
|
|
250
|
+
const product = products.data[0]
|
|
251
|
+
assert.equal(product.profit_margin, 55.00)
|
|
252
|
+
assert.equal(product.profit_amount, 54.99)
|
|
253
|
+
})
|
|
254
254
|
|
|
255
255
|
it('should apply sparse fieldsets to collections', async () => {
|
|
256
256
|
const products = await api.resources.products.query({
|
|
257
257
|
queryParams: {
|
|
258
258
|
fields: { products: 'name,profit_margin' }
|
|
259
259
|
}
|
|
260
|
-
})
|
|
260
|
+
})
|
|
261
261
|
|
|
262
|
-
const product = products.data[0]
|
|
263
|
-
assert.equal(product.name, 'Premium Widget')
|
|
264
|
-
assert.equal(product.profit_margin, 55.00)
|
|
265
|
-
assert.equal(product.price, undefined)
|
|
266
|
-
assert.equal(product.cost, undefined)
|
|
267
|
-
})
|
|
262
|
+
const product = products.data[0]
|
|
263
|
+
assert.equal(product.name, 'Premium Widget')
|
|
264
|
+
assert.equal(product.profit_margin, 55.00)
|
|
265
|
+
assert.equal(product.price, undefined)
|
|
266
|
+
assert.equal(product.cost, undefined)
|
|
267
|
+
})
|
|
268
268
|
|
|
269
269
|
it('should handle includes with computed fields in collections', async () => {
|
|
270
270
|
const products = await api.resources.products.query({
|
|
@@ -275,19 +275,19 @@ describe('Computed Fields and Sparse Fieldsets', () => {
|
|
|
275
275
|
reviews: 'rating,helpfulness_score'
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
|
-
})
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
const product = products.data[0]
|
|
281
|
+
assert.equal(product.reviews.length, 2)
|
|
279
282
|
|
|
280
|
-
const product = products.data[0];
|
|
281
|
-
assert.equal(product.reviews.length, 2);
|
|
282
|
-
|
|
283
283
|
product.reviews.forEach(review => {
|
|
284
|
-
assert.ok(review.rating)
|
|
285
|
-
assert.ok(review.helpfulness_score !== undefined)
|
|
286
|
-
assert.equal(review.reviewer_name, undefined)
|
|
287
|
-
assert.equal(review.comment, undefined)
|
|
288
|
-
})
|
|
289
|
-
})
|
|
290
|
-
})
|
|
284
|
+
assert.ok(review.rating)
|
|
285
|
+
assert.ok(review.helpfulness_score !== undefined)
|
|
286
|
+
assert.equal(review.reviewer_name, undefined)
|
|
287
|
+
assert.equal(review.comment, undefined)
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
})
|
|
291
291
|
|
|
292
292
|
describe('Relationship Fields', () => {
|
|
293
293
|
it('should always include minimal relationship objects regardless of sparse fieldsets', async () => {
|
|
@@ -296,36 +296,36 @@ describe('Computed Fields and Sparse Fieldsets', () => {
|
|
|
296
296
|
queryParams: {
|
|
297
297
|
fields: { products: 'name' }
|
|
298
298
|
}
|
|
299
|
-
})
|
|
299
|
+
})
|
|
300
300
|
|
|
301
301
|
// Minimal relationship objects should still be included
|
|
302
|
-
assert.ok(Array.isArray(product.reviews))
|
|
303
|
-
assert.equal(product.reviews.length, 2)
|
|
304
|
-
assert.equal(product.reviews[0].id, testData.review1.id)
|
|
305
|
-
assert.equal(product.reviews[1].id, testData.review2.id)
|
|
302
|
+
assert.ok(Array.isArray(product.reviews))
|
|
303
|
+
assert.equal(product.reviews.length, 2)
|
|
304
|
+
assert.equal(product.reviews[0].id, testData.review1.id)
|
|
305
|
+
assert.equal(product.reviews[1].id, testData.review2.id)
|
|
306
306
|
// Should only have id property (minimal object)
|
|
307
|
-
assert.equal(Object.keys(product.reviews[0]).length, 1)
|
|
308
|
-
assert.equal(Object.keys(product.reviews[1]).length, 1)
|
|
309
|
-
})
|
|
307
|
+
assert.equal(Object.keys(product.reviews[0]).length, 1)
|
|
308
|
+
assert.equal(Object.keys(product.reviews[1]).length, 1)
|
|
309
|
+
})
|
|
310
310
|
|
|
311
311
|
it('should include empty arrays for empty relationships', async () => {
|
|
312
312
|
const newProduct = await api.resources.products.post({
|
|
313
313
|
name: 'New Product',
|
|
314
314
|
price: 50,
|
|
315
315
|
cost: 25
|
|
316
|
-
})
|
|
316
|
+
})
|
|
317
317
|
|
|
318
318
|
const fetched = await api.resources.products.get({
|
|
319
319
|
id: newProduct.id,
|
|
320
320
|
queryParams: {
|
|
321
321
|
fields: { products: 'name' }
|
|
322
322
|
}
|
|
323
|
-
})
|
|
323
|
+
})
|
|
324
324
|
|
|
325
|
-
assert.ok(Array.isArray(fetched.reviews))
|
|
326
|
-
assert.equal(fetched.reviews.length, 0)
|
|
327
|
-
})
|
|
328
|
-
})
|
|
325
|
+
assert.ok(Array.isArray(fetched.reviews))
|
|
326
|
+
assert.equal(fetched.reviews.length, 0)
|
|
327
|
+
})
|
|
328
|
+
})
|
|
329
329
|
|
|
330
330
|
describe('Error Handling', () => {
|
|
331
331
|
it('should handle errors in compute functions gracefully', async () => {
|
|
@@ -339,23 +339,23 @@ describe('Computed Fields and Sparse Fieldsets', () => {
|
|
|
339
339
|
computed: true,
|
|
340
340
|
dependencies: ['value'],
|
|
341
341
|
compute: ({ attributes }) => {
|
|
342
|
-
throw new Error('Computation failed')
|
|
342
|
+
throw new Error('Computation failed')
|
|
343
343
|
}
|
|
344
344
|
}
|
|
345
345
|
},
|
|
346
346
|
tableName: 'test_faulty_products'
|
|
347
|
-
})
|
|
348
|
-
await api.resources.faulty_products.createKnexTable()
|
|
347
|
+
})
|
|
348
|
+
await api.resources.faulty_products.createKnexTable()
|
|
349
349
|
if (storageMode.isAnyApi()) {
|
|
350
|
-
storageMode.registerTable('test_faulty_products', 'faulty_products')
|
|
350
|
+
storageMode.registerTable('test_faulty_products', 'faulty_products')
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
-
const faulty = await api.resources.faulty_products.post({ value: 42 })
|
|
354
|
-
const fetched = await api.resources.faulty_products.get({ id: faulty.id })
|
|
353
|
+
const faulty = await api.resources.faulty_products.post({ value: 42 })
|
|
354
|
+
const fetched = await api.resources.faulty_products.get({ id: faulty.id })
|
|
355
355
|
|
|
356
|
-
assert.equal(fetched.value, 42)
|
|
357
|
-
assert.equal(fetched.bad_compute, null)
|
|
358
|
-
})
|
|
356
|
+
assert.equal(fetched.value, 42)
|
|
357
|
+
assert.equal(fetched.bad_compute, null) // Error results in null
|
|
358
|
+
})
|
|
359
359
|
|
|
360
360
|
it('should reject unknown fields in sparse fieldsets', async () => {
|
|
361
361
|
await assert.rejects(
|
|
@@ -366,8 +366,8 @@ describe('Computed Fields and Sparse Fieldsets', () => {
|
|
|
366
366
|
}
|
|
367
367
|
}),
|
|
368
368
|
/Unknown sparse field 'unknown_field'/
|
|
369
|
-
)
|
|
370
|
-
})
|
|
369
|
+
)
|
|
370
|
+
})
|
|
371
371
|
|
|
372
372
|
it('should reject requests for _ids fields in sparse fieldsets', async () => {
|
|
373
373
|
await assert.rejects(
|
|
@@ -378,9 +378,9 @@ describe('Computed Fields and Sparse Fieldsets', () => {
|
|
|
378
378
|
}
|
|
379
379
|
}),
|
|
380
380
|
/Unknown sparse field 'reviews_ids'/
|
|
381
|
-
)
|
|
382
|
-
})
|
|
383
|
-
})
|
|
381
|
+
)
|
|
382
|
+
})
|
|
383
|
+
})
|
|
384
384
|
|
|
385
385
|
describe('Async Computed Fields', () => {
|
|
386
386
|
it('should support async compute functions', async () => {
|
|
@@ -396,8 +396,8 @@ describe('Computed Fields and Sparse Fieldsets', () => {
|
|
|
396
396
|
dependencies: ['external_id'],
|
|
397
397
|
compute: async ({ attributes }) => {
|
|
398
398
|
// Simulate async operation (e.g., external API call)
|
|
399
|
-
await new Promise(resolve => setTimeout(resolve, 10))
|
|
400
|
-
return `fetched-${attributes.external_id}
|
|
399
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
400
|
+
return `fetched-${attributes.external_id}`
|
|
401
401
|
}
|
|
402
402
|
},
|
|
403
403
|
computed_name: {
|
|
@@ -406,27 +406,27 @@ describe('Computed Fields and Sparse Fieldsets', () => {
|
|
|
406
406
|
dependencies: ['name'],
|
|
407
407
|
compute: async ({ attributes }) => {
|
|
408
408
|
// Another async operation
|
|
409
|
-
await new Promise(resolve => setTimeout(resolve, 5))
|
|
410
|
-
return attributes.name.toUpperCase()
|
|
409
|
+
await new Promise(resolve => setTimeout(resolve, 5))
|
|
410
|
+
return attributes.name.toUpperCase()
|
|
411
411
|
}
|
|
412
412
|
}
|
|
413
413
|
},
|
|
414
414
|
tableName: 'test_async_products'
|
|
415
|
-
})
|
|
416
|
-
await api.resources.async_products.createKnexTable()
|
|
415
|
+
})
|
|
416
|
+
await api.resources.async_products.createKnexTable()
|
|
417
417
|
if (storageMode.isAnyApi()) {
|
|
418
|
-
storageMode.registerTable('test_async_products', 'async_products')
|
|
418
|
+
storageMode.registerTable('test_async_products', 'async_products')
|
|
419
419
|
}
|
|
420
420
|
|
|
421
421
|
const product = await api.resources.async_products.post({
|
|
422
422
|
name: 'Async Product',
|
|
423
423
|
external_id: 'ext-123'
|
|
424
|
-
})
|
|
424
|
+
})
|
|
425
425
|
|
|
426
|
-
const fetched = await api.resources.async_products.get({ id: product.id })
|
|
427
|
-
assert.equal(fetched.external_data, 'fetched-ext-123')
|
|
428
|
-
assert.equal(fetched.computed_name, 'ASYNC PRODUCT')
|
|
429
|
-
})
|
|
426
|
+
const fetched = await api.resources.async_products.get({ id: product.id })
|
|
427
|
+
assert.equal(fetched.external_data, 'fetched-ext-123')
|
|
428
|
+
assert.equal(fetched.computed_name, 'ASYNC PRODUCT')
|
|
429
|
+
})
|
|
430
430
|
|
|
431
431
|
it('should handle errors in async compute functions', async () => {
|
|
432
432
|
// Add a resource with failing async computed field
|
|
@@ -439,45 +439,45 @@ describe('Computed Fields and Sparse Fieldsets', () => {
|
|
|
439
439
|
computed: true,
|
|
440
440
|
dependencies: ['value'],
|
|
441
441
|
compute: async ({ attributes }) => {
|
|
442
|
-
await new Promise(resolve => setTimeout(resolve, 5))
|
|
443
|
-
throw new Error('Async computation failed')
|
|
442
|
+
await new Promise(resolve => setTimeout(resolve, 5))
|
|
443
|
+
throw new Error('Async computation failed')
|
|
444
444
|
}
|
|
445
445
|
}
|
|
446
446
|
},
|
|
447
447
|
tableName: 'test_failing_async_products'
|
|
448
|
-
})
|
|
449
|
-
await api.resources.failing_async_products.createKnexTable()
|
|
448
|
+
})
|
|
449
|
+
await api.resources.failing_async_products.createKnexTable()
|
|
450
450
|
if (storageMode.isAnyApi()) {
|
|
451
|
-
storageMode.registerTable('test_failing_async_products', 'failing_async_products')
|
|
451
|
+
storageMode.registerTable('test_failing_async_products', 'failing_async_products')
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
-
const product = await api.resources.failing_async_products.post({ value: 42 })
|
|
455
|
-
const fetched = await api.resources.failing_async_products.get({ id: product.id })
|
|
454
|
+
const product = await api.resources.failing_async_products.post({ value: 42 })
|
|
455
|
+
const fetched = await api.resources.failing_async_products.get({ id: product.id })
|
|
456
456
|
|
|
457
|
-
assert.equal(fetched.value, 42)
|
|
458
|
-
assert.equal(fetched.failing_async, null)
|
|
459
|
-
})
|
|
460
|
-
})
|
|
457
|
+
assert.equal(fetched.value, 42)
|
|
458
|
+
assert.equal(fetched.failing_async, null) // Error results in null
|
|
459
|
+
})
|
|
460
|
+
})
|
|
461
461
|
|
|
462
462
|
describe('Performance Considerations', () => {
|
|
463
463
|
it('should only compute requested computed fields', async () => {
|
|
464
464
|
// Track compute calls
|
|
465
|
-
let profitMarginCalls = 0
|
|
466
|
-
let profitAmountCalls = 0
|
|
465
|
+
let profitMarginCalls = 0
|
|
466
|
+
let profitAmountCalls = 0
|
|
467
467
|
|
|
468
468
|
// Override compute functions to track calls
|
|
469
|
-
const originalProfitMargin = api.resources.products.vars.schemaInfo.computed.profit_margin.compute
|
|
470
|
-
const originalProfitAmount = api.resources.products.vars.schemaInfo.computed.profit_amount.compute
|
|
469
|
+
const originalProfitMargin = api.resources.products.vars.schemaInfo.computed.profit_margin.compute
|
|
470
|
+
const originalProfitAmount = api.resources.products.vars.schemaInfo.computed.profit_amount.compute
|
|
471
471
|
|
|
472
472
|
api.resources.products.vars.schemaInfo.computed.profit_margin.compute = (ctx) => {
|
|
473
|
-
profitMarginCalls
|
|
474
|
-
return originalProfitMargin(ctx)
|
|
475
|
-
}
|
|
473
|
+
profitMarginCalls++
|
|
474
|
+
return originalProfitMargin(ctx)
|
|
475
|
+
}
|
|
476
476
|
|
|
477
477
|
api.resources.products.vars.schemaInfo.computed.profit_amount.compute = (ctx) => {
|
|
478
|
-
profitAmountCalls
|
|
479
|
-
return originalProfitAmount(ctx)
|
|
480
|
-
}
|
|
478
|
+
profitAmountCalls++
|
|
479
|
+
return originalProfitAmount(ctx)
|
|
480
|
+
}
|
|
481
481
|
|
|
482
482
|
// Request only profit_margin
|
|
483
483
|
await api.resources.products.get({
|
|
@@ -485,14 +485,14 @@ describe('Computed Fields and Sparse Fieldsets', () => {
|
|
|
485
485
|
queryParams: {
|
|
486
486
|
fields: { products: 'name,profit_margin' }
|
|
487
487
|
}
|
|
488
|
-
})
|
|
488
|
+
})
|
|
489
489
|
|
|
490
|
-
assert.equal(profitMarginCalls, 1)
|
|
491
|
-
assert.equal(profitAmountCalls, 0)
|
|
490
|
+
assert.equal(profitMarginCalls, 1)
|
|
491
|
+
assert.equal(profitAmountCalls, 0) // Should not be computed
|
|
492
492
|
|
|
493
493
|
// Restore original functions
|
|
494
|
-
api.resources.products.vars.schemaInfo.computed.profit_margin.compute = originalProfitMargin
|
|
495
|
-
api.resources.products.vars.schemaInfo.computed.profit_amount.compute = originalProfitAmount
|
|
496
|
-
})
|
|
497
|
-
})
|
|
498
|
-
})
|
|
494
|
+
api.resources.products.vars.schemaInfo.computed.profit_margin.compute = originalProfitMargin
|
|
495
|
+
api.resources.products.vars.schemaInfo.computed.profit_amount.compute = originalProfitAmount
|
|
496
|
+
})
|
|
497
|
+
})
|
|
498
|
+
})
|