json-rest-api 1.0.7 → 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.
Files changed (146) hide show
  1. package/.claude/agents/library-guide-expert.md +29 -0
  2. package/.claude/agents/rest-api-internals-expert.md +53 -0
  3. package/CLAUDE.md +139 -1
  4. package/README.md +2 -5
  5. package/docTest.js +238 -0
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
  7. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
  8. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
  9. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
  10. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
  11. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
  12. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
  13. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
  14. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
  15. package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
  16. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
  17. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
  18. package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
  19. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
  20. package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
  21. package/docs/GUIDE/index.md +58 -0
  22. package/docs/GUIDE.md +61 -2238
  23. package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
  24. package/docs/README.md +57 -2239
  25. package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
  26. package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
  27. package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
  28. package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
  29. package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
  30. package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
  31. package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
  32. package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
  33. package/docs/assets/css/style.scss +78 -5
  34. package/docs/index.md +2 -12
  35. package/helper-reorganization-plan.md +85 -0
  36. package/helper-usage-analysis.md +94 -0
  37. package/package.json +5 -4
  38. package/plugins/core/bulk-operations-plugin.js +2 -2
  39. package/plugins/core/connectors/express-plugin.js +90 -32
  40. package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
  41. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
  42. package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
  43. package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
  44. package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
  45. package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
  46. package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
  47. package/plugins/core/lib/querying/knex-window-queries.js +328 -0
  48. package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
  49. package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
  50. package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
  51. package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
  52. package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
  53. package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
  54. package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
  55. package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
  56. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
  57. package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
  58. package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
  59. package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
  60. package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
  61. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
  62. package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
  63. package/plugins/core/lib/writing/relationship-processor.js +252 -0
  64. package/plugins/core/rest-api-knex-plugin.js +183 -62
  65. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
  66. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
  67. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
  68. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
  69. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
  70. package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
  71. package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
  72. package/plugins/core/rest-api-plugin-methods/common.js +605 -0
  73. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
  74. package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
  75. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
  76. package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
  77. package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
  78. package/plugins/core/rest-api-plugin-methods/get.js +187 -0
  79. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
  80. package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
  81. package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
  82. package/plugins/core/rest-api-plugin-methods/post.js +157 -0
  83. package/plugins/core/rest-api-plugin-methods/put.js +257 -0
  84. package/plugins/core/rest-api-plugin-methods/query.js +208 -0
  85. package/plugins/core/rest-api-plugin.js +108 -2498
  86. package/plugins/core/socketio-plugin.js +28 -5
  87. package/plugins/storage/local-storage.js +2 -2
  88. package/tests/computed-fields.test.js +483 -0
  89. package/tests/cors.test.js +4 -4
  90. package/tests/curl.test.js +5 -4
  91. package/tests/custom-idproperty-relationships.test.js +891 -0
  92. package/tests/field-getters.test.js +353 -0
  93. package/tests/field-setters.test.js +310 -0
  94. package/tests/fixtures/api-configs.js +800 -28
  95. package/tests/jwt-auth.test.js +0 -2
  96. package/tests/nested-includes.test.js +1 -1
  97. package/tests/pagination-cursor-multifield.test.js +514 -0
  98. package/tests/pagination-enhanced.test.js +12 -9
  99. package/tests/polymorphic-relationships.test.js +10 -8
  100. package/tests/polymorphic-simple.test.js +6 -4
  101. package/tests/positioning-advanced.test.js +12 -12
  102. package/tests/positioning-extended.test.js +25 -25
  103. package/tests/positioning-multifilter.test.js +24 -24
  104. package/tests/positioning.test.js +3 -3
  105. package/tests/query-limits.test.js +24 -8
  106. package/tests/relationship-endpoints.test.js +4 -5
  107. package/tests/relationships.test.js +0 -1
  108. package/tests/socketio.test.js +12 -1
  109. package/tests/virtual-fields.test.js +13 -13
  110. package/PLAN_FOR_WEBSOCKETS.md +0 -398
  111. package/README_1_Initial_Setup.md +0 -393
  112. package/README_X_Hooks.md +0 -647
  113. package/TODO_rest-api-plugin-extras.todo +0 -23
  114. package/connectors_ideas/express-plugin.js +0 -120
  115. package/connectors_ideas/http-plugin.js +0 -138
  116. package/docs/examples/express-plugin.js +0 -366
  117. package/example.js +0 -301
  118. package/plugins/core/HELPER_MODULES.md +0 -285
  119. package/plugins/core/lib/compile-schemas.js +0 -245
  120. package/plugins/core/lib/database-value-normalizers.js +0 -192
  121. package/plugins/core/lib/default-data-helpers.js +0 -100
  122. package/plugins/core/lib/knex-cross-table-search.js +0 -442
  123. package/plugins/core/lib/knex-json-api-helpers.js +0 -84
  124. package/plugins/core/lib/knex-json-api-transformers.js +0 -449
  125. package/plugins/core/lib/knex-pagination-helpers.js +0 -193
  126. package/plugins/core/lib/knex-window-queries.js +0 -142
  127. package/plugins/core/lib/many-to-many-manipulations.js +0 -343
  128. package/plugins/core/lib/relationship-processor.js +0 -278
  129. package/plugins/core/lib/schema-helpers.js +0 -343
  130. package/plugins/core/lib/scope-validations.js +0 -252
  131. package/plugins/core/rest-api-relationships-plugin.js +0 -700
  132. package/plugins/core/utils/knex-query-helpers-base.js +0 -56
  133. /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
  134. /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
  135. /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
  136. /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
  137. /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
  138. /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
  139. /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
  140. /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
  141. /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
  142. /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
  143. /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
  144. /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
  145. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
  146. /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
- returnFullRecord: {
22
+ returnRecordApi: {
22
23
  post: true, // Need to return record to get ID for tests
23
24
  put: false,
24
- patch: false,
25
- allowRemoteOverride: false
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, pluginOptions.express || {});
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
- returnFullRecord: {
153
+ returnRecordApi: {
146
154
  post: true, // Need to return record to get ID for tests
147
155
  put: false,
148
- patch: false,
149
- allowRemoteOverride: false
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
- // Define the polymorphic field in schema
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
- returnFullRecord: {
308
+ returnRecordApi: {
298
309
  post: true,
299
310
  put: false,
300
- patch: false,
301
- allowRemoteOverride: false
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
- returnFullRecord: {
405
+ returnRecordApi: {
391
406
  post: true,
392
407
  put: false,
393
- patch: false,
394
- allowRemoteOverride: false
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 resourceUrlPrefix, enablePaginationCounts
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 MultiHome (multi-tenancy) support for testing
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
- returnFullRecord: {
1010
+ returnRecordApi: {
510
1011
  post: true,
511
1012
  put: false,
512
- patch: false,
513
- allowRemoteOverride: false
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
- returnFullRecord: {
1110
+ returnRecordApi: {
606
1111
  post: true,
607
1112
  put: true,
608
- patch: true,
609
- allowRemoteOverride: false
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
  }