json-rest-api 1.0.6 → 1.0.8
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/.claude/agents/library-guide-expert.md +29 -0
- package/.claude/agents/rest-api-internals-expert.md +53 -0
- package/CLAUDE.md +139 -1
- package/README.md +2 -5
- package/docTest.js +238 -0
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
- package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
- package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
- package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
- package/docs/GUIDE/index.md +58 -0
- package/docs/GUIDE.md +61 -2238
- package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
- package/docs/README.md +57 -2239
- package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
- package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
- package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
- package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
- package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
- package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
- package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
- package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
- package/docs/assets/css/style.scss +78 -5
- package/docs/index.md +2 -12
- package/helper-reorganization-plan.md +85 -0
- package/helper-usage-analysis.md +94 -0
- package/package.json +5 -4
- package/plugins/core/bulk-operations-plugin.js +2 -2
- package/plugins/core/connectors/express-plugin.js +99 -33
- package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
- package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
- package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
- package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
- package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
- package/plugins/core/lib/querying/knex-window-queries.js +328 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
- package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
- package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
- package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
- package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
- package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
- package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
- package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
- package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
- package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
- package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
- package/plugins/core/lib/writing/relationship-processor.js +252 -0
- package/plugins/core/rest-api-knex-plugin.js +183 -62
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
- package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
- package/plugins/core/rest-api-plugin-methods/common.js +605 -0
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
- package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
- package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
- package/plugins/core/rest-api-plugin-methods/get.js +187 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
- package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
- package/plugins/core/rest-api-plugin-methods/post.js +157 -0
- package/plugins/core/rest-api-plugin-methods/put.js +257 -0
- package/plugins/core/rest-api-plugin-methods/query.js +208 -0
- package/plugins/core/rest-api-plugin.js +108 -2498
- package/plugins/core/socketio-plugin.js +28 -5
- package/plugins/storage/local-storage.js +2 -2
- package/tests/computed-fields.test.js +483 -0
- package/tests/cors.test.js +4 -4
- package/tests/curl.test.js +5 -4
- package/tests/custom-idproperty-relationships.test.js +891 -0
- package/tests/field-getters.test.js +353 -0
- package/tests/field-setters.test.js +310 -0
- package/tests/fixtures/api-configs.js +800 -28
- package/tests/jwt-auth.test.js +0 -2
- package/tests/nested-includes.test.js +1 -1
- package/tests/pagination-cursor-multifield.test.js +514 -0
- package/tests/pagination-enhanced.test.js +12 -9
- package/tests/polymorphic-relationships.test.js +10 -8
- package/tests/polymorphic-simple.test.js +6 -4
- package/tests/positioning-advanced.test.js +12 -12
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +24 -24
- package/tests/positioning.test.js +3 -3
- package/tests/query-limits.test.js +24 -8
- package/tests/relationship-endpoints.test.js +4 -5
- package/tests/relationships.test.js +0 -1
- package/tests/socketio.test.js +12 -1
- package/tests/virtual-fields.test.js +13 -13
- package/PLAN_FOR_WEBSOCKETS.md +0 -398
- package/README_1_Initial_Setup.md +0 -393
- package/README_X_Hooks.md +0 -647
- package/TODO_rest-api-plugin-extras.todo +0 -23
- package/connectors_ideas/express-plugin.js +0 -120
- package/connectors_ideas/http-plugin.js +0 -138
- package/docs/examples/express-plugin.js +0 -366
- package/example.js +0 -301
- package/plugins/core/HELPER_MODULES.md +0 -285
- package/plugins/core/lib/compile-schemas.js +0 -245
- package/plugins/core/lib/database-value-normalizers.js +0 -192
- package/plugins/core/lib/default-data-helpers.js +0 -100
- package/plugins/core/lib/knex-cross-table-search.js +0 -442
- package/plugins/core/lib/knex-json-api-helpers.js +0 -84
- package/plugins/core/lib/knex-json-api-transformers.js +0 -449
- package/plugins/core/lib/knex-pagination-helpers.js +0 -193
- package/plugins/core/lib/knex-window-queries.js +0 -142
- package/plugins/core/lib/many-to-many-manipulations.js +0 -343
- package/plugins/core/lib/relationship-processor.js +0 -278
- package/plugins/core/lib/schema-helpers.js +0 -343
- package/plugins/core/lib/scope-validations.js +0 -252
- package/plugins/core/rest-api-relationships-plugin.js +0 -700
- package/plugins/core/utils/knex-query-helpers-base.js +0 -56
- /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
- /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
- /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
- /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
- /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
- /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
- /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
- /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
- /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
- /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
- /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
- /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
- /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
- /package/{readmeSnippets → docs/README_VARIOUS}/test_put_fields_without_defaults.js +0 -0
|
@@ -12,17 +12,22 @@ export async function createBasicApi(knex, pluginOptions = {}) {
|
|
|
12
12
|
const tablePrefix = pluginOptions.tablePrefix || 'basic';
|
|
13
13
|
const api = new Api({
|
|
14
14
|
name: apiName,
|
|
15
|
-
version: '1.0.0'
|
|
15
|
+
version: '1.0.0',
|
|
16
|
+
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
16
17
|
});
|
|
17
18
|
|
|
18
19
|
const restApiOptions = {
|
|
19
20
|
simplifiedApi: false,
|
|
20
21
|
simplifiedTransport: false,
|
|
21
|
-
|
|
22
|
+
returnRecordApi: {
|
|
22
23
|
post: true, // Need to return record to get ID for tests
|
|
23
24
|
put: false,
|
|
24
|
-
patch: false
|
|
25
|
-
|
|
25
|
+
patch: false
|
|
26
|
+
},
|
|
27
|
+
returnRecordTransport: {
|
|
28
|
+
post: 'full', // Tests expect full JSON:API response
|
|
29
|
+
put: 'no',
|
|
30
|
+
patch: 'minimal' // Tests expect minimal response with ID
|
|
26
31
|
},
|
|
27
32
|
sortableFields: ['id', 'title', 'country_id', 'publisher_id', 'name', 'code'],
|
|
28
33
|
...pluginOptions['rest-api'] // Merge any custom options for rest-api plugin
|
|
@@ -34,7 +39,10 @@ export async function createBasicApi(knex, pluginOptions = {}) {
|
|
|
34
39
|
|
|
35
40
|
// Add Express plugin if requested
|
|
36
41
|
if (pluginOptions.includeExpress) {
|
|
37
|
-
await api.use(ExpressPlugin,
|
|
42
|
+
await api.use(ExpressPlugin, {
|
|
43
|
+
mountPath: '/api', // Default mount path for tests
|
|
44
|
+
...(pluginOptions.express || {})
|
|
45
|
+
});
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
// Countries table
|
|
@@ -142,11 +150,15 @@ export async function createExtendedApi(knex) {
|
|
|
142
150
|
await api.use(RestApiPlugin, {
|
|
143
151
|
simplifiedApi: false,
|
|
144
152
|
simplifiedTransport: false,
|
|
145
|
-
|
|
153
|
+
returnRecordApi: {
|
|
146
154
|
post: true, // Need to return record to get ID for tests
|
|
147
155
|
put: false,
|
|
148
|
-
patch: false
|
|
149
|
-
|
|
156
|
+
patch: false
|
|
157
|
+
},
|
|
158
|
+
returnRecordTransport: {
|
|
159
|
+
post: 'full', // Tests expect full JSON:API response
|
|
160
|
+
put: 'no',
|
|
161
|
+
patch: 'minimal' // Tests expect minimal response with ID
|
|
150
162
|
},
|
|
151
163
|
sortableFields: ['id', 'title', 'country_id', 'publisher_id', 'price', 'language', 'population', 'name', 'code']
|
|
152
164
|
});
|
|
@@ -261,18 +273,17 @@ export async function createExtendedApi(knex) {
|
|
|
261
273
|
review_date: { type: 'dateTime', default: 'now()' },
|
|
262
274
|
helpful_count: { type: 'number', default: 0 },
|
|
263
275
|
reviewable_type: { type: 'string', required: true },
|
|
264
|
-
reviewable_id: { type: 'number', required: true }
|
|
265
|
-
|
|
276
|
+
reviewable_id: { type: 'number', required: true }
|
|
277
|
+
},
|
|
278
|
+
relationships: {
|
|
266
279
|
reviewable: {
|
|
267
280
|
belongsToPolymorphic: {
|
|
268
281
|
types: ['books', 'authors', 'publishers'],
|
|
269
282
|
typeField: 'reviewable_type',
|
|
270
283
|
idField: 'reviewable_id'
|
|
271
|
-
}
|
|
272
|
-
as: 'reviewable'
|
|
284
|
+
}
|
|
273
285
|
}
|
|
274
286
|
},
|
|
275
|
-
relationships: {},
|
|
276
287
|
tableName: 'ext_reviews'
|
|
277
288
|
});
|
|
278
289
|
await api.resources.reviews.createKnexTable();
|
|
@@ -294,11 +305,15 @@ export async function createLimitedDepthApi(knex) {
|
|
|
294
305
|
await api.use(RestApiPlugin, {
|
|
295
306
|
simplifiedApi: false,
|
|
296
307
|
simplifiedTransport: false,
|
|
297
|
-
|
|
308
|
+
returnRecordApi: {
|
|
298
309
|
post: true,
|
|
299
310
|
put: false,
|
|
300
|
-
patch: false
|
|
301
|
-
|
|
311
|
+
patch: false
|
|
312
|
+
},
|
|
313
|
+
returnRecordTransport: {
|
|
314
|
+
post: 'full',
|
|
315
|
+
put: 'no',
|
|
316
|
+
patch: 'minimal'
|
|
302
317
|
},
|
|
303
318
|
sortableFields: ['id', 'title', 'country_id', 'publisher_id', 'name', 'code'],
|
|
304
319
|
includeDepthLimit: 2 // Key difference: limit is 2 instead of default 3
|
|
@@ -317,7 +332,7 @@ export async function createLimitedDepthApi(knex) {
|
|
|
317
332
|
publishers: { hasMany: 'publishers', foreignKey: 'country_id' },
|
|
318
333
|
books: { hasMany: 'books', foreignKey: 'country_id' }
|
|
319
334
|
},
|
|
320
|
-
tableName: 'limited_countries'
|
|
335
|
+
tableName: 'limited_countries',
|
|
321
336
|
});
|
|
322
337
|
await api.resources.countries.createKnexTable();
|
|
323
338
|
|
|
@@ -387,14 +402,18 @@ export async function createPaginationApi(knex, options = {}) {
|
|
|
387
402
|
const restApiOptions = {
|
|
388
403
|
simplifiedApi: false,
|
|
389
404
|
simplifiedTransport: false,
|
|
390
|
-
|
|
405
|
+
returnRecordApi: {
|
|
391
406
|
post: true,
|
|
392
407
|
put: false,
|
|
393
|
-
patch: false
|
|
394
|
-
|
|
408
|
+
patch: false
|
|
409
|
+
},
|
|
410
|
+
returnRecordTransport: {
|
|
411
|
+
post: 'full',
|
|
412
|
+
put: 'no',
|
|
413
|
+
patch: 'minimal'
|
|
395
414
|
},
|
|
396
415
|
sortableFields: ['id', 'title', 'country_id', 'publisher_id', 'name', 'code'],
|
|
397
|
-
...options // Allow overriding options like
|
|
416
|
+
...options // Allow overriding options like publicBaseUrl, enablePaginationCounts
|
|
398
417
|
};
|
|
399
418
|
|
|
400
419
|
await api.use(RestApiPlugin, restApiOptions);
|
|
@@ -493,8 +512,490 @@ export async function createWebSocketApi(knex, pluginOptions = {}) {
|
|
|
493
512
|
}
|
|
494
513
|
|
|
495
514
|
/**
|
|
496
|
-
* Creates an API with
|
|
515
|
+
* Creates an API with computed fields for testing
|
|
497
516
|
*/
|
|
517
|
+
export async function createComputedFieldsApi(knex, pluginOptions = {}) {
|
|
518
|
+
const api = new Api({
|
|
519
|
+
name: 'computed-fields-test-api',
|
|
520
|
+
version: '1.0.0',
|
|
521
|
+
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
await api.use(RestApiPlugin, {
|
|
525
|
+
simplifiedApi: true,
|
|
526
|
+
simplifiedTransport: true
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
await api.use(RestApiKnexPlugin, { knex });
|
|
530
|
+
|
|
531
|
+
// Products resource with computed fields
|
|
532
|
+
await api.addResource('products', {
|
|
533
|
+
schema: {
|
|
534
|
+
id: { type: 'id' },
|
|
535
|
+
name: { type: 'string', required: true, max: 255 },
|
|
536
|
+
price: { type: 'number', required: true, min: 0 },
|
|
537
|
+
cost: { type: 'number', required: true, min: 0, normallyHidden: true },
|
|
538
|
+
internal_notes: { type: 'string', normallyHidden: true },
|
|
539
|
+
profit_margin: {
|
|
540
|
+
type: 'number',
|
|
541
|
+
computed: true,
|
|
542
|
+
dependencies: ['price', 'cost'],
|
|
543
|
+
compute: ({ attributes }) => {
|
|
544
|
+
if (!attributes.price || attributes.price === 0) return 0;
|
|
545
|
+
return Number(((attributes.price - attributes.cost) / attributes.price * 100).toFixed(2));
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
profit_amount: {
|
|
549
|
+
type: 'number',
|
|
550
|
+
computed: true,
|
|
551
|
+
dependencies: ['price', 'cost'],
|
|
552
|
+
compute: ({ attributes }) => {
|
|
553
|
+
return Number((attributes.price - attributes.cost).toFixed(2));
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
relationships: {
|
|
558
|
+
reviews: { hasMany: 'reviews', foreignKey: 'product_id' }
|
|
559
|
+
},
|
|
560
|
+
tableName: 'test_products'
|
|
561
|
+
});
|
|
562
|
+
await api.resources.products.createKnexTable();
|
|
563
|
+
|
|
564
|
+
// Reviews resource with computed fields
|
|
565
|
+
await api.addResource('reviews', {
|
|
566
|
+
schema: {
|
|
567
|
+
id: { type: 'id' },
|
|
568
|
+
product_id: { type: 'number', required: true, belongsTo: 'products', as: 'product' },
|
|
569
|
+
reviewer_name: { type: 'string', required: true },
|
|
570
|
+
rating: { type: 'number', required: true, min: 1, max: 5 },
|
|
571
|
+
comment: { type: 'string', max: 1000 },
|
|
572
|
+
helpful_votes: { type: 'number', default: 0 },
|
|
573
|
+
total_votes: { type: 'number', default: 0 },
|
|
574
|
+
spam_score: { type: 'number', default: 0, normallyHidden: true },
|
|
575
|
+
helpfulness_score: {
|
|
576
|
+
type: 'number',
|
|
577
|
+
computed: true,
|
|
578
|
+
dependencies: ['helpful_votes', 'total_votes'],
|
|
579
|
+
compute: ({ attributes }) => {
|
|
580
|
+
if (attributes.total_votes === 0) return null;
|
|
581
|
+
return Number(((attributes.helpful_votes / attributes.total_votes) * 100).toFixed(0));
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
is_helpful: {
|
|
585
|
+
type: 'boolean',
|
|
586
|
+
computed: true,
|
|
587
|
+
dependencies: ['helpful_votes', 'total_votes', 'spam_score'],
|
|
588
|
+
compute: ({ attributes }) => {
|
|
589
|
+
if (attributes.total_votes < 10) return null;
|
|
590
|
+
const helpfulnessScore = (attributes.helpful_votes / attributes.total_votes) * 100;
|
|
591
|
+
return helpfulnessScore > 70 && attributes.spam_score < 0.5;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
},
|
|
595
|
+
tableName: 'test_reviews'
|
|
596
|
+
});
|
|
597
|
+
await api.resources.reviews.createKnexTable();
|
|
598
|
+
|
|
599
|
+
return api;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Creates an API with field getters for testing
|
|
604
|
+
*/
|
|
605
|
+
export async function createFieldGettersApi(knex, pluginOptions = {}) {
|
|
606
|
+
const api = new Api({
|
|
607
|
+
name: 'field-getters-test-api',
|
|
608
|
+
version: '1.0.0',
|
|
609
|
+
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
await api.use(RestApiPlugin, {
|
|
613
|
+
simplifiedApi: true,
|
|
614
|
+
simplifiedTransport: true
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
await api.use(RestApiKnexPlugin, { knex });
|
|
618
|
+
|
|
619
|
+
// Users resource with basic getters
|
|
620
|
+
await api.addResource('users', {
|
|
621
|
+
schema: {
|
|
622
|
+
id: { type: 'id' },
|
|
623
|
+
email: {
|
|
624
|
+
type: 'string',
|
|
625
|
+
nullable: true,
|
|
626
|
+
getter: (value) => value?.toLowerCase().trim()
|
|
627
|
+
},
|
|
628
|
+
name: {
|
|
629
|
+
type: 'string',
|
|
630
|
+
nullable: true,
|
|
631
|
+
getter: (value) => value?.trim()
|
|
632
|
+
},
|
|
633
|
+
phone: {
|
|
634
|
+
type: 'string',
|
|
635
|
+
nullable: true,
|
|
636
|
+
getter: (value) => {
|
|
637
|
+
if (!value) return null;
|
|
638
|
+
// Remove all non-digits and format
|
|
639
|
+
const digits = value.replace(/\D/g, '');
|
|
640
|
+
if (digits.length === 10) {
|
|
641
|
+
return `(${digits.slice(0,3)}) ${digits.slice(3,6)}-${digits.slice(6)}`;
|
|
642
|
+
}
|
|
643
|
+
return value;
|
|
644
|
+
}
|
|
645
|
+
},
|
|
646
|
+
metadata_json: {
|
|
647
|
+
type: 'string',
|
|
648
|
+
nullable: true,
|
|
649
|
+
getter: (value) => {
|
|
650
|
+
if (!value) return {};
|
|
651
|
+
try {
|
|
652
|
+
return JSON.parse(value);
|
|
653
|
+
} catch {
|
|
654
|
+
return {};
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
},
|
|
658
|
+
tags_csv: {
|
|
659
|
+
type: 'string',
|
|
660
|
+
nullable: true,
|
|
661
|
+
getter: (value) => {
|
|
662
|
+
if (!value) return [];
|
|
663
|
+
return value.split(',').map(t => t.trim()).filter(t => t);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
},
|
|
667
|
+
tableName: 'getter_users'
|
|
668
|
+
});
|
|
669
|
+
await api.resources.users.createKnexTable();
|
|
670
|
+
|
|
671
|
+
// Products resource with getters and computed fields
|
|
672
|
+
await api.addResource('products', {
|
|
673
|
+
schema: {
|
|
674
|
+
id: { type: 'id' },
|
|
675
|
+
name: {
|
|
676
|
+
type: 'string',
|
|
677
|
+
getter: (value) => value?.toUpperCase()
|
|
678
|
+
},
|
|
679
|
+
description: {
|
|
680
|
+
type: 'string',
|
|
681
|
+
getter: (value) => {
|
|
682
|
+
// Truncate long descriptions
|
|
683
|
+
if (!value) return '';
|
|
684
|
+
return value.length > 50 ? value.substring(0, 47) + '...' : value;
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
price_str: {
|
|
688
|
+
type: 'string',
|
|
689
|
+
getter: (value) => parseFloat(value) || 0
|
|
690
|
+
},
|
|
691
|
+
tax_rate_str: {
|
|
692
|
+
type: 'string',
|
|
693
|
+
getter: (value) => parseFloat(value) || 0
|
|
694
|
+
},
|
|
695
|
+
total_price: {
|
|
696
|
+
type: 'number',
|
|
697
|
+
computed: true,
|
|
698
|
+
dependencies: ['price_str', 'tax_rate_str'],
|
|
699
|
+
compute: ({ attributes }) => {
|
|
700
|
+
// Should see numbers from getters, not strings
|
|
701
|
+
return attributes.price_str * (1 + attributes.tax_rate_str);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
},
|
|
705
|
+
relationships: {
|
|
706
|
+
reviews: { hasMany: 'reviews', foreignKey: 'product_id' }
|
|
707
|
+
},
|
|
708
|
+
tableName: 'getter_products'
|
|
709
|
+
});
|
|
710
|
+
await api.resources.products.createKnexTable();
|
|
711
|
+
|
|
712
|
+
// Reviews resource with getters
|
|
713
|
+
await api.addResource('reviews', {
|
|
714
|
+
schema: {
|
|
715
|
+
id: { type: 'id' },
|
|
716
|
+
product_id: { type: 'id', belongsTo: 'products', as: 'product' },
|
|
717
|
+
content: {
|
|
718
|
+
type: 'string',
|
|
719
|
+
getter: (value) => `[REVIEW] ${value}`
|
|
720
|
+
},
|
|
721
|
+
rating: { type: 'number' }
|
|
722
|
+
},
|
|
723
|
+
tableName: 'getter_reviews'
|
|
724
|
+
});
|
|
725
|
+
await api.resources.reviews.createKnexTable();
|
|
726
|
+
|
|
727
|
+
// Resource with getter dependencies
|
|
728
|
+
await api.addResource('formatted_data', {
|
|
729
|
+
schema: {
|
|
730
|
+
id: { type: 'id' },
|
|
731
|
+
step1: {
|
|
732
|
+
type: 'string',
|
|
733
|
+
nullable: true,
|
|
734
|
+
getter: (value) => value?.trim()
|
|
735
|
+
},
|
|
736
|
+
step2: {
|
|
737
|
+
type: 'string',
|
|
738
|
+
nullable: true,
|
|
739
|
+
getter: (value, { attributes }) => {
|
|
740
|
+
if (!value) return value; // Return null/undefined as-is
|
|
741
|
+
// Depends on step1 being trimmed first
|
|
742
|
+
return `${value} [step1: ${attributes.step1}]`;
|
|
743
|
+
},
|
|
744
|
+
runGetterAfter: ['step1']
|
|
745
|
+
},
|
|
746
|
+
step3: {
|
|
747
|
+
type: 'string',
|
|
748
|
+
getter: (value, { attributes }) => {
|
|
749
|
+
if (!value) return value; // Return null/undefined as-is
|
|
750
|
+
// Depends on step2
|
|
751
|
+
return `${value} [step2: ${attributes.step2}]`;
|
|
752
|
+
},
|
|
753
|
+
runGetterAfter: ['step2']
|
|
754
|
+
}
|
|
755
|
+
},
|
|
756
|
+
tableName: 'getter_formatted'
|
|
757
|
+
});
|
|
758
|
+
await api.resources.formatted_data.createKnexTable();
|
|
759
|
+
|
|
760
|
+
// Async getters example
|
|
761
|
+
await api.addResource('encrypted_data', {
|
|
762
|
+
schema: {
|
|
763
|
+
id: { type: 'id' },
|
|
764
|
+
secret: {
|
|
765
|
+
type: 'string',
|
|
766
|
+
nullable: true,
|
|
767
|
+
getter: async (value) => {
|
|
768
|
+
if (!value) return null;
|
|
769
|
+
// Simulate async decryption
|
|
770
|
+
await new Promise(resolve => setTimeout(resolve, 5));
|
|
771
|
+
return Buffer.from(value, 'base64').toString();
|
|
772
|
+
}
|
|
773
|
+
},
|
|
774
|
+
data: {
|
|
775
|
+
type: 'string',
|
|
776
|
+
nullable: true,
|
|
777
|
+
getter: async (value, context) => {
|
|
778
|
+
if (!value) return null;
|
|
779
|
+
await new Promise(resolve => setTimeout(resolve, 5));
|
|
780
|
+
const decrypted = Buffer.from(value, 'base64').toString();
|
|
781
|
+
return `[${context.scopeName}] ${decrypted}`;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
},
|
|
785
|
+
tableName: 'getter_encrypted'
|
|
786
|
+
});
|
|
787
|
+
await api.resources.encrypted_data.createKnexTable();
|
|
788
|
+
|
|
789
|
+
return api;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Creates an API with field setters for testing
|
|
794
|
+
*/
|
|
795
|
+
export async function createFieldSettersApi(knex, pluginOptions = {}) {
|
|
796
|
+
const api = new Api({
|
|
797
|
+
name: 'field-setters-test-api',
|
|
798
|
+
version: '1.0.0',
|
|
799
|
+
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
await api.use(RestApiPlugin, {
|
|
803
|
+
simplifiedApi: true,
|
|
804
|
+
simplifiedTransport: true,
|
|
805
|
+
returnRecordApi: {
|
|
806
|
+
post: true,
|
|
807
|
+
put: true,
|
|
808
|
+
patch: true
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
await api.use(RestApiKnexPlugin, { knex });
|
|
813
|
+
|
|
814
|
+
// Users resource with basic setters
|
|
815
|
+
await api.addResource('users', {
|
|
816
|
+
schema: {
|
|
817
|
+
id: { type: 'id' },
|
|
818
|
+
email: {
|
|
819
|
+
type: 'string',
|
|
820
|
+
required: true,
|
|
821
|
+
setter: (value) => value?.toLowerCase().trim()
|
|
822
|
+
},
|
|
823
|
+
username: {
|
|
824
|
+
type: 'string',
|
|
825
|
+
required: true,
|
|
826
|
+
setter: (value) => value?.toLowerCase().replace(/\s+/g, '')
|
|
827
|
+
},
|
|
828
|
+
tags: {
|
|
829
|
+
type: 'string', // Accepts string, stored as string
|
|
830
|
+
setter: (value) => value // Pass through
|
|
831
|
+
},
|
|
832
|
+
preferences: {
|
|
833
|
+
type: 'string', // Accepts string, stored as string
|
|
834
|
+
setter: (value) => value // Pass through
|
|
835
|
+
}
|
|
836
|
+
},
|
|
837
|
+
tableName: 'setter_users'
|
|
838
|
+
});
|
|
839
|
+
await api.resources.users.createKnexTable();
|
|
840
|
+
|
|
841
|
+
// Products with type conversion setters
|
|
842
|
+
await api.addResource('products', {
|
|
843
|
+
schema: {
|
|
844
|
+
id: { type: 'id' },
|
|
845
|
+
name: { type: 'string', required: true },
|
|
846
|
+
price: {
|
|
847
|
+
type: 'number',
|
|
848
|
+
required: true,
|
|
849
|
+
min: 0,
|
|
850
|
+
setter: (value) => {
|
|
851
|
+
// Convert to cents and round
|
|
852
|
+
return Math.round(value * 100);
|
|
853
|
+
}
|
|
854
|
+
},
|
|
855
|
+
discount_percent: {
|
|
856
|
+
type: 'number',
|
|
857
|
+
min: 0,
|
|
858
|
+
max: 100,
|
|
859
|
+
setter: (value) => Math.round(value)
|
|
860
|
+
},
|
|
861
|
+
metadata: {
|
|
862
|
+
type: 'object',
|
|
863
|
+
setter: (value) => JSON.stringify(value || {})
|
|
864
|
+
}
|
|
865
|
+
},
|
|
866
|
+
tableName: 'setter_products'
|
|
867
|
+
});
|
|
868
|
+
await api.resources.products.createKnexTable();
|
|
869
|
+
|
|
870
|
+
// Secure data with async setters
|
|
871
|
+
await api.addResource('secure_data', {
|
|
872
|
+
schema: {
|
|
873
|
+
id: { type: 'id' },
|
|
874
|
+
password: {
|
|
875
|
+
type: 'string',
|
|
876
|
+
required: true,
|
|
877
|
+
min: 8,
|
|
878
|
+
setter: async (value) => {
|
|
879
|
+
// Simulate async password hashing
|
|
880
|
+
await new Promise(resolve => setTimeout(resolve, 5));
|
|
881
|
+
return `hashed:${value}`;
|
|
882
|
+
}
|
|
883
|
+
},
|
|
884
|
+
api_key: {
|
|
885
|
+
type: 'string',
|
|
886
|
+
setter: async (value) => {
|
|
887
|
+
if (!value) return null;
|
|
888
|
+
// Simulate encryption
|
|
889
|
+
await new Promise(resolve => setTimeout(resolve, 5));
|
|
890
|
+
return Buffer.from(value).toString('base64');
|
|
891
|
+
}
|
|
892
|
+
},
|
|
893
|
+
data: {
|
|
894
|
+
type: 'string',
|
|
895
|
+
setter: async (value) => {
|
|
896
|
+
if (!value) return null;
|
|
897
|
+
return Buffer.from(value).toString('base64');
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
},
|
|
901
|
+
tableName: 'setter_secure'
|
|
902
|
+
});
|
|
903
|
+
await api.resources.secure_data.createKnexTable();
|
|
904
|
+
|
|
905
|
+
// Resource with setter dependencies
|
|
906
|
+
await api.addResource('computed_data', {
|
|
907
|
+
schema: {
|
|
908
|
+
id: { type: 'id' },
|
|
909
|
+
base_value: {
|
|
910
|
+
type: 'number',
|
|
911
|
+
setter: (value) => value || 0
|
|
912
|
+
},
|
|
913
|
+
multiplier: {
|
|
914
|
+
type: 'number',
|
|
915
|
+
setter: (value) => value || 1
|
|
916
|
+
},
|
|
917
|
+
adjustment: {
|
|
918
|
+
type: 'number',
|
|
919
|
+
setter: (value) => value || 0
|
|
920
|
+
},
|
|
921
|
+
calculated_value: {
|
|
922
|
+
type: 'number',
|
|
923
|
+
setter: (value, { attributes }) => {
|
|
924
|
+
// Calculate based on other fields
|
|
925
|
+
return (attributes.base_value || 0) * (attributes.multiplier || 1);
|
|
926
|
+
},
|
|
927
|
+
runSetterAfter: ['base_value', 'multiplier']
|
|
928
|
+
},
|
|
929
|
+
final_value: {
|
|
930
|
+
type: 'number',
|
|
931
|
+
setter: (value, { attributes }) => {
|
|
932
|
+
// Depends on calculated_value
|
|
933
|
+
return (attributes.calculated_value || 0) + (attributes.adjustment || 0);
|
|
934
|
+
},
|
|
935
|
+
runSetterAfter: ['calculated_value', 'adjustment']
|
|
936
|
+
}
|
|
937
|
+
},
|
|
938
|
+
tableName: 'setter_computed'
|
|
939
|
+
});
|
|
940
|
+
await api.resources.computed_data.createKnexTable();
|
|
941
|
+
|
|
942
|
+
// Nullable fields handling
|
|
943
|
+
await api.addResource('nullable_data', {
|
|
944
|
+
schema: {
|
|
945
|
+
id: { type: 'id' },
|
|
946
|
+
field1: {
|
|
947
|
+
type: 'string',
|
|
948
|
+
nullable: true,
|
|
949
|
+
setter: (value) => value === undefined ? null : value
|
|
950
|
+
},
|
|
951
|
+
field2: {
|
|
952
|
+
type: 'string',
|
|
953
|
+
nullable: true,
|
|
954
|
+
setter: (value) => value === undefined ? null : value
|
|
955
|
+
},
|
|
956
|
+
field3: {
|
|
957
|
+
type: 'string',
|
|
958
|
+
setter: (value) => value === '' ? 'empty' : value
|
|
959
|
+
},
|
|
960
|
+
field4: {
|
|
961
|
+
type: 'number',
|
|
962
|
+
setter: (value) => value === 0 ? -1 : value
|
|
963
|
+
}
|
|
964
|
+
},
|
|
965
|
+
tableName: 'setter_nullable'
|
|
966
|
+
});
|
|
967
|
+
await api.resources.nullable_data.createKnexTable();
|
|
968
|
+
|
|
969
|
+
// Validated data
|
|
970
|
+
await api.addResource('validated_data', {
|
|
971
|
+
schema: {
|
|
972
|
+
id: { type: 'id' },
|
|
973
|
+
email: {
|
|
974
|
+
type: 'string',
|
|
975
|
+
required: true,
|
|
976
|
+
format: 'email',
|
|
977
|
+
setter: (value) => value.toLowerCase().trim()
|
|
978
|
+
},
|
|
979
|
+
age: {
|
|
980
|
+
type: 'number',
|
|
981
|
+
min: 0,
|
|
982
|
+
max: 120,
|
|
983
|
+
setter: (value) => value // No transformation, just to verify it's a number
|
|
984
|
+
},
|
|
985
|
+
score: {
|
|
986
|
+
type: 'number',
|
|
987
|
+
min: 0,
|
|
988
|
+
max: 100,
|
|
989
|
+
setter: (value) => Math.ceil(value) // Round up
|
|
990
|
+
}
|
|
991
|
+
},
|
|
992
|
+
tableName: 'setter_validated'
|
|
993
|
+
});
|
|
994
|
+
await api.resources.validated_data.createKnexTable();
|
|
995
|
+
|
|
996
|
+
return api;
|
|
997
|
+
}
|
|
998
|
+
|
|
498
999
|
export async function createMultiHomeApi(knex, pluginOptions = {}) {
|
|
499
1000
|
const { MultiHomePlugin } = await import('../../plugins/core/rest-api-multihome-plugin.js');
|
|
500
1001
|
|
|
@@ -506,11 +1007,15 @@ export async function createMultiHomeApi(knex, pluginOptions = {}) {
|
|
|
506
1007
|
await api.use(RestApiPlugin, {
|
|
507
1008
|
simplifiedApi: false,
|
|
508
1009
|
simplifiedTransport: false,
|
|
509
|
-
|
|
1010
|
+
returnRecordApi: {
|
|
510
1011
|
post: true,
|
|
511
1012
|
put: false,
|
|
512
|
-
patch: false
|
|
513
|
-
|
|
1013
|
+
patch: false
|
|
1014
|
+
},
|
|
1015
|
+
returnRecordTransport: {
|
|
1016
|
+
post: 'full',
|
|
1017
|
+
put: 'no',
|
|
1018
|
+
patch: 'minimal'
|
|
514
1019
|
},
|
|
515
1020
|
sortableFields: ['id', 'title', 'name', 'tenant_id']
|
|
516
1021
|
});
|
|
@@ -602,11 +1107,15 @@ export async function createPositioningApi(knex, pluginOptions = {}) {
|
|
|
602
1107
|
const restApiOptions = {
|
|
603
1108
|
simplifiedApi: true, // Changed to true to allow simplified API calls in tests
|
|
604
1109
|
simplifiedTransport: false,
|
|
605
|
-
|
|
1110
|
+
returnRecordApi: {
|
|
606
1111
|
post: true,
|
|
607
1112
|
put: true,
|
|
608
|
-
patch: true
|
|
609
|
-
|
|
1113
|
+
patch: true
|
|
1114
|
+
},
|
|
1115
|
+
returnRecordTransport: {
|
|
1116
|
+
post: 'full',
|
|
1117
|
+
put: 'full',
|
|
1118
|
+
patch: 'full'
|
|
610
1119
|
},
|
|
611
1120
|
sortableFields: ['id', 'title', 'name', 'position', 'sort_order', 'category_id', 'project_id', 'status'],
|
|
612
1121
|
...pluginOptions['rest-api']
|
|
@@ -672,5 +1181,268 @@ export async function createPositioningApi(knex, pluginOptions = {}) {
|
|
|
672
1181
|
});
|
|
673
1182
|
await api.resources.items.createKnexTable();
|
|
674
1183
|
|
|
1184
|
+
return api;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* Creates an API with custom idProperty for all resources to test idProperty functionality
|
|
1189
|
+
* Uses 'custom_id_' prefix for all tables to avoid conflicts
|
|
1190
|
+
*/
|
|
1191
|
+
export async function createCustomIdPropertyApi(knex, pluginOptions = {}) {
|
|
1192
|
+
const apiName = pluginOptions.apiName || `custom-id-test-api`;
|
|
1193
|
+
const tablePrefix = pluginOptions.tablePrefix || 'custom_id';
|
|
1194
|
+
const api = new Api({
|
|
1195
|
+
name: apiName,
|
|
1196
|
+
version: '1.0.0',
|
|
1197
|
+
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
const restApiOptions = {
|
|
1201
|
+
simplifiedApi: false,
|
|
1202
|
+
simplifiedTransport: false,
|
|
1203
|
+
returnRecordApi: {
|
|
1204
|
+
post: 'full',
|
|
1205
|
+
put: 'full',
|
|
1206
|
+
patch: 'full'
|
|
1207
|
+
},
|
|
1208
|
+
returnRecordTransport: {
|
|
1209
|
+
post: 'full',
|
|
1210
|
+
put: 'full',
|
|
1211
|
+
patch: 'full'
|
|
1212
|
+
},
|
|
1213
|
+
sortableFields: ['country_id', 'publisher_id', 'author_id', 'book_id', 'title', 'name', 'code'],
|
|
1214
|
+
...pluginOptions['rest-api'] // Merge any custom options for rest-api plugin
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
await api.use(RestApiPlugin, restApiOptions);
|
|
1218
|
+
await api.use(RestApiKnexPlugin, { knex });
|
|
1219
|
+
|
|
1220
|
+
// Countries table with custom idProperty
|
|
1221
|
+
await api.addResource('countries', {
|
|
1222
|
+
schema: {
|
|
1223
|
+
// NO id: { type: 'id' } - this is key!
|
|
1224
|
+
name: { type: 'string', required: true, max: 100, search: true },
|
|
1225
|
+
code: { type: 'string', max: 2, unique: true }
|
|
1226
|
+
},
|
|
1227
|
+
relationships: {
|
|
1228
|
+
publishers: { hasMany: 'publishers', foreignKey: 'country_id' },
|
|
1229
|
+
books: { hasMany: 'books', foreignKey: 'country_id' }
|
|
1230
|
+
},
|
|
1231
|
+
tableName: `${tablePrefix}_countries`,
|
|
1232
|
+
idProperty: 'country_id' // Custom ID property
|
|
1233
|
+
});
|
|
1234
|
+
await api.resources.countries.createKnexTable();
|
|
1235
|
+
|
|
1236
|
+
// Publishers table with custom idProperty
|
|
1237
|
+
await api.addResource('publishers', {
|
|
1238
|
+
schema: {
|
|
1239
|
+
name: { type: 'string', required: true, max: 200 },
|
|
1240
|
+
country_id: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' }
|
|
1241
|
+
},
|
|
1242
|
+
relationships: {
|
|
1243
|
+
books: { hasMany: 'books', foreignKey: 'publisher_id' },
|
|
1244
|
+
reviews: {
|
|
1245
|
+
hasMany: 'reviews',
|
|
1246
|
+
via: 'reviewable'
|
|
1247
|
+
}
|
|
1248
|
+
},
|
|
1249
|
+
tableName: `${tablePrefix}_publishers`,
|
|
1250
|
+
idProperty: 'publisher_id'
|
|
1251
|
+
});
|
|
1252
|
+
await api.resources.publishers.createKnexTable();
|
|
1253
|
+
|
|
1254
|
+
// Authors table with custom idProperty
|
|
1255
|
+
await api.addResource('authors', {
|
|
1256
|
+
schema: {
|
|
1257
|
+
name: { type: 'string', required: true, max: 200 },
|
|
1258
|
+
biography: { type: 'string', max: 5000 },
|
|
1259
|
+
country_id: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' }
|
|
1260
|
+
},
|
|
1261
|
+
relationships: {
|
|
1262
|
+
books: { hasMany: 'books', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' },
|
|
1263
|
+
reviews: {
|
|
1264
|
+
hasMany: 'reviews',
|
|
1265
|
+
via: 'reviewable'
|
|
1266
|
+
}
|
|
1267
|
+
},
|
|
1268
|
+
tableName: `${tablePrefix}_authors`,
|
|
1269
|
+
idProperty: 'author_id'
|
|
1270
|
+
});
|
|
1271
|
+
await api.resources.authors.createKnexTable();
|
|
1272
|
+
|
|
1273
|
+
// Books table with custom idProperty
|
|
1274
|
+
await api.addResource('books', {
|
|
1275
|
+
schema: {
|
|
1276
|
+
title: { type: 'string', required: true, max: 300, search: true },
|
|
1277
|
+
isbn: { type: 'string', max: 13 },
|
|
1278
|
+
pages: { type: 'number' },
|
|
1279
|
+
published_date: { type: 'date' },
|
|
1280
|
+
country_id: { type: 'number', required: true, belongsTo: 'countries', as: 'country', search: true },
|
|
1281
|
+
publisher_id: { type: 'number', nullable: true, belongsTo: 'publishers', as: 'publisher', search: true }
|
|
1282
|
+
},
|
|
1283
|
+
relationships: {
|
|
1284
|
+
authors: { hasMany: 'authors', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' },
|
|
1285
|
+
reviews: {
|
|
1286
|
+
hasMany: 'reviews',
|
|
1287
|
+
via: 'reviewable'
|
|
1288
|
+
}
|
|
1289
|
+
},
|
|
1290
|
+
tableName: `${tablePrefix}_books`,
|
|
1291
|
+
idProperty: 'book_id'
|
|
1292
|
+
});
|
|
1293
|
+
await api.resources.books.createKnexTable();
|
|
1294
|
+
|
|
1295
|
+
// Book-Authors pivot table with custom idProperty
|
|
1296
|
+
await api.addResource('book_authors', {
|
|
1297
|
+
schema: {
|
|
1298
|
+
book_id: { type: 'number', required: true, belongsTo: 'books', as: 'book' },
|
|
1299
|
+
author_id: { type: 'number', required: true, belongsTo: 'authors', as: 'author' },
|
|
1300
|
+
contribution_type: { type: 'string', max: 50 },
|
|
1301
|
+
order: { type: 'number' }
|
|
1302
|
+
},
|
|
1303
|
+
tableName: `${tablePrefix}_book_authors`,
|
|
1304
|
+
idProperty: 'book_author_id'
|
|
1305
|
+
});
|
|
1306
|
+
await api.resources.book_authors.createKnexTable();
|
|
1307
|
+
|
|
1308
|
+
// Polymorphic reviews with custom idProperty
|
|
1309
|
+
await api.addResource('reviews', {
|
|
1310
|
+
schema: {
|
|
1311
|
+
rating: { type: 'number', required: true, min: 1, max: 5 },
|
|
1312
|
+
title: { type: 'string', max: 200 },
|
|
1313
|
+
content: { type: 'string', required: true, max: 5000 },
|
|
1314
|
+
reviewer_name: { type: 'string', required: true, max: 100 },
|
|
1315
|
+
review_date: { type: 'dateTime', default: 'now()' },
|
|
1316
|
+
reviewable_type: { type: 'string', required: true, search: true },
|
|
1317
|
+
reviewable_id: { type: 'number', required: true },
|
|
1318
|
+
// Define the polymorphic field in schema
|
|
1319
|
+
reviewable: {
|
|
1320
|
+
belongsToPolymorphic: {
|
|
1321
|
+
types: ['books', 'authors', 'publishers'],
|
|
1322
|
+
typeField: 'reviewable_type',
|
|
1323
|
+
idField: 'reviewable_id'
|
|
1324
|
+
},
|
|
1325
|
+
as: 'reviewable'
|
|
1326
|
+
}
|
|
1327
|
+
},
|
|
1328
|
+
relationships: {},
|
|
1329
|
+
tableName: `${tablePrefix}_reviews`,
|
|
1330
|
+
idProperty: 'review_id'
|
|
1331
|
+
});
|
|
1332
|
+
await api.resources.reviews.createKnexTable();
|
|
1333
|
+
|
|
1334
|
+
return api;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Creates an API configuration for multi-field cursor pagination testing
|
|
1339
|
+
* Uses 'cursor_' prefix for all tables to avoid conflicts
|
|
1340
|
+
*/
|
|
1341
|
+
export async function createCursorPaginationApi(knex) {
|
|
1342
|
+
const api = new Api({
|
|
1343
|
+
name: 'cursor-pagination-test-api',
|
|
1344
|
+
version: '1.0.0'
|
|
1345
|
+
});
|
|
1346
|
+
|
|
1347
|
+
const restApiOptions = {
|
|
1348
|
+
simplified: false,
|
|
1349
|
+
responseFormat: {
|
|
1350
|
+
post: 'full',
|
|
1351
|
+
put: 'full',
|
|
1352
|
+
patch: 'minimal'
|
|
1353
|
+
},
|
|
1354
|
+
sortableFields: ['id', 'name', 'category', 'brand', 'price', 'sku', 'created_at', 'code', 'status', 'type'],
|
|
1355
|
+
queryDefaultLimit: 20,
|
|
1356
|
+
queryMaxLimit: 100,
|
|
1357
|
+
publicBaseUrl: 'https://api.example.com/v1'
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
await api.use(RestApiPlugin, restApiOptions);
|
|
1361
|
+
await api.use(RestApiKnexPlugin, { knex });
|
|
1362
|
+
|
|
1363
|
+
// Create products table with fields suitable for multi-field sorting
|
|
1364
|
+
await api.addResource('products', {
|
|
1365
|
+
schema: {
|
|
1366
|
+
id: { type: 'id' },
|
|
1367
|
+
name: { type: 'string', required: true, max: 200, search: true },
|
|
1368
|
+
category: { type: 'string', required: true, max: 100, search: true, indexed: true },
|
|
1369
|
+
brand: { type: 'string', required: true, max: 100, search: true, indexed: true },
|
|
1370
|
+
price: { type: 'number', required: true },
|
|
1371
|
+
sku: { type: 'string', required: true, max: 50, unique: true },
|
|
1372
|
+
status: { type: 'string', max: 50, default: 'active' },
|
|
1373
|
+
created_at: { type: 'dateTime', default: 'now()' }
|
|
1374
|
+
},
|
|
1375
|
+
tableName: 'cursor_products'
|
|
1376
|
+
});
|
|
1377
|
+
await api.resources.products.createKnexTable();
|
|
1378
|
+
|
|
1379
|
+
// Create items table with custom ID property
|
|
1380
|
+
await api.addResource('items', {
|
|
1381
|
+
schema: {
|
|
1382
|
+
item_id: { type: 'id' },
|
|
1383
|
+
code: { type: 'string', required: true, max: 10, unique: true },
|
|
1384
|
+
name: { type: 'string', required: true, max: 200 },
|
|
1385
|
+
category: { type: 'string', required: true, max: 100 },
|
|
1386
|
+
type: { type: 'string', required: true, max: 50 }
|
|
1387
|
+
},
|
|
1388
|
+
idProperty: 'item_id',
|
|
1389
|
+
tableName: 'cursor_items'
|
|
1390
|
+
});
|
|
1391
|
+
await api.resources.items.createKnexTable();
|
|
1392
|
+
|
|
1393
|
+
return api;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* Creates an API configuration for testing virtual fields validation
|
|
1398
|
+
*/
|
|
1399
|
+
export async function createVirtualFieldsApi(knex, pluginOptions = {}) {
|
|
1400
|
+
const apiName = pluginOptions.apiName || 'virtual-fields-test-api';
|
|
1401
|
+
const api = new Api({
|
|
1402
|
+
name: apiName,
|
|
1403
|
+
version: '1.0.0',
|
|
1404
|
+
log: { level: process.env.LOG_LEVEL || 'info' }
|
|
1405
|
+
});
|
|
1406
|
+
|
|
1407
|
+
const restApiOptions = {
|
|
1408
|
+
simplifiedApi: false,
|
|
1409
|
+
simplifiedTransport: false,
|
|
1410
|
+
returnRecordApi: {
|
|
1411
|
+
post: true,
|
|
1412
|
+
put: false,
|
|
1413
|
+
patch: false
|
|
1414
|
+
},
|
|
1415
|
+
returnRecordTransport: {
|
|
1416
|
+
post: 'full',
|
|
1417
|
+
put: 'no',
|
|
1418
|
+
patch: 'minimal'
|
|
1419
|
+
},
|
|
1420
|
+
...pluginOptions['rest-api']
|
|
1421
|
+
};
|
|
1422
|
+
|
|
1423
|
+
await api.use(RestApiPlugin, restApiOptions);
|
|
1424
|
+
await api.use(RestApiKnexPlugin, { knex });
|
|
1425
|
+
|
|
1426
|
+
// Add a test resource with virtual fields
|
|
1427
|
+
await api.addResource('users', {
|
|
1428
|
+
schema: {
|
|
1429
|
+
// Regular fields
|
|
1430
|
+
username: { type: 'string', required: true },
|
|
1431
|
+
email: { type: 'string', required: true },
|
|
1432
|
+
age: { type: 'number' },
|
|
1433
|
+
|
|
1434
|
+
// Virtual fields with validation rules
|
|
1435
|
+
password: { type: 'string', virtual: true, minLength: 8 },
|
|
1436
|
+
passwordConfirmation: { type: 'string', virtual: true, minLength: 8 },
|
|
1437
|
+
termsAccepted: { type: 'boolean', virtual: true, required: true },
|
|
1438
|
+
captchaScore: { type: 'number', virtual: true, min: 0, max: 1 },
|
|
1439
|
+
tags: { type: 'array', virtual: true },
|
|
1440
|
+
metadata: { type: 'object', virtual: true }
|
|
1441
|
+
},
|
|
1442
|
+
tableName: 'virtual_users'
|
|
1443
|
+
});
|
|
1444
|
+
|
|
1445
|
+
await api.resources.users.createKnexTable();
|
|
1446
|
+
|
|
675
1447
|
return api;
|
|
676
1448
|
}
|