json-rest-api 1.0.13 → 1.0.15

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 (118) hide show
  1. package/connectors_ideas/fastify.js +66 -68
  2. package/connectors_ideas/koa-plugin.js +50 -51
  3. package/eslint.config.js +13 -0
  4. package/examples/url-override-example.js +63 -63
  5. package/index.js +13 -13
  6. package/lib/enhanced-logger.js +51 -51
  7. package/lib/error-formatter.js +61 -61
  8. package/lib/rest-api-errors.js +28 -28
  9. package/package.json +9 -5
  10. package/plugins/core/bulk-operations-plugin.js +142 -145
  11. package/plugins/core/connectors/express-plugin.js +210 -217
  12. package/plugins/core/connectors/lib/busboy-detector.js +84 -84
  13. package/plugins/core/connectors/lib/formidable-detector.js +50 -50
  14. package/plugins/core/connectors/lib/request-helpers.js +74 -74
  15. package/plugins/core/file-handling-plugin.js +125 -125
  16. package/plugins/core/lib/anyapi/anyapi-registry.js +254 -254
  17. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +287 -287
  18. package/plugins/core/lib/anyapi/schema-utils.js +101 -101
  19. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +41 -41
  20. package/plugins/core/lib/dbTablesOperations.js +134 -135
  21. package/plugins/core/lib/fractional-positioning.js +74 -76
  22. package/plugins/core/lib/querying/knex-cross-table-search.js +320 -320
  23. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +144 -144
  24. package/plugins/core/lib/querying/knex-pagination-helpers.js +164 -164
  25. package/plugins/core/lib/querying/knex-process-includes.js +50 -50
  26. package/plugins/core/lib/querying/knex-query-helpers-base.js +31 -31
  27. package/plugins/core/lib/querying/knex-query-helpers.js +451 -452
  28. package/plugins/core/lib/querying/knex-relationship-includes.js +1160 -1170
  29. package/plugins/core/lib/querying/knex-window-queries.js +89 -89
  30. package/plugins/core/lib/querying/url-helpers.js +35 -35
  31. package/plugins/core/lib/querying-writing/compile-schemas.js +97 -98
  32. package/plugins/core/lib/querying-writing/connectors-query-parser.js +31 -31
  33. package/plugins/core/lib/querying-writing/database-capabilities.js +94 -94
  34. package/plugins/core/lib/querying-writing/database-value-normalizers.js +76 -76
  35. package/plugins/core/lib/querying-writing/default-data-helpers.js +27 -27
  36. package/plugins/core/lib/querying-writing/field-utils.js +44 -42
  37. package/plugins/core/lib/querying-writing/knex-constants.js +15 -16
  38. package/plugins/core/lib/querying-writing/knex-field-helpers.js +117 -115
  39. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +41 -41
  40. package/plugins/core/lib/querying-writing/payload-validators.js +275 -275
  41. package/plugins/core/lib/querying-writing/schema-helpers.js +129 -129
  42. package/plugins/core/lib/querying-writing/scope-validations.js +143 -146
  43. package/plugins/core/lib/querying-writing/simplified-helpers.js +133 -129
  44. package/plugins/core/lib/storage/storage-adapter.js +68 -68
  45. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +31 -31
  46. package/plugins/core/lib/writing/many-to-many-manipulations.js +81 -81
  47. package/plugins/core/lib/writing/relationship-processor.js +58 -58
  48. package/plugins/core/rest-api-access.js +8 -8
  49. package/plugins/core/rest-api-anyapi-knex-plugin.js +920 -920
  50. package/plugins/core/rest-api-cors-plugin.js +170 -171
  51. package/plugins/core/rest-api-knex-plugin.js +482 -493
  52. package/plugins/core/rest-api-multihome-plugin.js +247 -251
  53. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +5 -5
  54. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +20 -21
  55. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +44 -44
  56. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +23 -23
  57. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +15 -15
  58. package/plugins/core/rest-api-plugin-methods/add-route.js +9 -9
  59. package/plugins/core/rest-api-plugin-methods/check-permissions.js +5 -6
  60. package/plugins/core/rest-api-plugin-methods/common.js +295 -297
  61. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +35 -37
  62. package/plugins/core/rest-api-plugin-methods/delete.js +57 -59
  63. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +172 -171
  64. package/plugins/core/rest-api-plugin-methods/get-related.js +96 -96
  65. package/plugins/core/rest-api-plugin-methods/get-relationship.js +16 -18
  66. package/plugins/core/rest-api-plugin-methods/get.js +86 -86
  67. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +16 -17
  68. package/plugins/core/rest-api-plugin-methods/patch.js +63 -64
  69. package/plugins/core/rest-api-plugin-methods/post-relationship.js +54 -55
  70. package/plugins/core/rest-api-plugin-methods/post.js +61 -62
  71. package/plugins/core/rest-api-plugin-methods/put.js +86 -87
  72. package/plugins/core/rest-api-plugin-methods/query.js +88 -88
  73. package/plugins/core/rest-api-plugin-methods/release.js +5 -6
  74. package/plugins/core/rest-api-plugin.js +76 -78
  75. package/plugins/core/rest-api-positioning-plugin.js +251 -252
  76. package/plugins/core/socketio-plugin.js +240 -236
  77. package/plugins/storage/local-storage.js +103 -103
  78. package/plugins/storage/s3-storage.js +41 -42
  79. package/quickTest.js +37 -37
  80. package/scripts/debug-anyapi-review-filter.mjs +18 -18
  81. package/scripts/inspect-anyapi-descriptor-country.mjs +10 -10
  82. package/scripts/inspect-anyapi-descriptor.mjs +10 -10
  83. package/scripts/run-docs.js +112 -113
  84. package/test-debug.js +28 -28
  85. package/tests/TEST_TEMPLATE.test.js +51 -51
  86. package/tests/anyapi-basic.test.js +42 -42
  87. package/tests/anyapi-cursor-pagination.test.js +48 -48
  88. package/tests/bulk-operations.test.js +208 -208
  89. package/tests/computed-fields.test.js +204 -204
  90. package/tests/cors.test.js +412 -413
  91. package/tests/curl.test.js +275 -276
  92. package/tests/custom-idproperty-relationships.test.js +266 -266
  93. package/tests/field-getters.test.js +132 -132
  94. package/tests/field-setters.test.js +110 -110
  95. package/tests/fixtures/api-configs.js +699 -699
  96. package/tests/helpers/storage-mode.js +32 -32
  97. package/tests/helpers/test-utils.js +76 -76
  98. package/tests/include-depth-validation.test.js +76 -76
  99. package/tests/includes.test.js +145 -145
  100. package/tests/multihome.test.js +175 -175
  101. package/tests/nested-includes.test.js +255 -255
  102. package/tests/pagination-cursor-multifield.test.js +164 -164
  103. package/tests/pagination-enhanced.test.js +225 -225
  104. package/tests/polymorphic-relationships.test.js +96 -96
  105. package/tests/polymorphic-simple.test.js +44 -44
  106. package/tests/positioning-advanced.test.js +226 -226
  107. package/tests/positioning-extended.test.js +399 -399
  108. package/tests/positioning-multifilter.test.js +204 -204
  109. package/tests/positioning.test.js +266 -266
  110. package/tests/queries.test.js +184 -184
  111. package/tests/query-limits.test.js +154 -155
  112. package/tests/relationship-endpoints.test.js +127 -127
  113. package/tests/relationships.test.js +178 -179
  114. package/tests/rest-api-access.test.js +86 -86
  115. package/tests/rest-api.test.js +103 -103
  116. package/tests/searchschema-merge.test.js +93 -93
  117. package/tests/socketio.test.js +326 -326
  118. package/tests/virtual-fields.test.js +164 -164
@@ -1,75 +1,75 @@
1
- import { Api } from 'hooked-api';
2
- import { RestApiPlugin, RestApiKnexPlugin, RestApiAnyapiKnexPlugin } from '../../index.js';
3
- import { AccessPlugin } from '../../plugins/core/rest-api-access.js';
4
- import { ExpressPlugin } from '../../plugins/core/connectors/express-plugin.js';
5
- import express from 'express';
6
- import { createServer } from 'http';
7
- import { ensureAnyApiSchema } from '../../plugins/core/lib/anyapi/schema-utils.js';
8
- import { storageMode } from '../helpers/storage-mode.js';
9
-
10
- async function resetAnyApiTables(knex) {
11
- if (!storageMode.isAnyApi()) return;
12
- await knex('any_links').delete().catch(() => {});
13
- await knex('any_records').delete().catch(() => {});
14
- await knex('any_relationship_configs').delete().catch(() => {});
15
- await knex('any_field_configs').delete().catch(() => {});
16
- await knex('any_resource_configs').delete().catch(() => {});
1
+ import { Api } from 'hooked-api'
2
+ import { RestApiPlugin, RestApiKnexPlugin, RestApiAnyapiKnexPlugin } from '../../index.js'
3
+ import { AccessPlugin } from '../../plugins/core/rest-api-access.js'
4
+ import { ExpressPlugin } from '../../plugins/core/connectors/express-plugin.js'
5
+ import express from 'express'
6
+ import { createServer } from 'http'
7
+ import { ensureAnyApiSchema } from '../../plugins/core/lib/anyapi/schema-utils.js'
8
+ import { storageMode } from '../helpers/storage-mode.js'
9
+
10
+ async function resetAnyApiTables (knex) {
11
+ if (!storageMode.isAnyApi()) return
12
+ await knex('any_links').delete().catch(() => {})
13
+ await knex('any_records').delete().catch(() => {})
14
+ await knex('any_relationship_configs').delete().catch(() => {})
15
+ await knex('any_field_configs').delete().catch(() => {})
16
+ await knex('any_resource_configs').delete().catch(() => {})
17
17
  }
18
18
 
19
- async function useStoragePlugin(api, knex, { tenantId } = {}) {
19
+ async function useStoragePlugin (api, knex, { tenantId } = {}) {
20
20
  if (storageMode.isAnyApi()) {
21
- await ensureAnyApiSchema(knex);
22
- const sanitizedName = api?.name ? api.name.replace(/[^A-Za-z0-9]+/g, '_').toLowerCase() : 'tenant';
23
- const effectiveTenantId = tenantId || api?.anyapi?.tenantId || `${sanitizedName}_tenant`;
24
- api.anyapi = api.anyapi || {};
25
- api.anyapi.tenantId = effectiveTenantId;
26
- await api.use(RestApiAnyapiKnexPlugin, { knex, tenantId: effectiveTenantId });
27
- storageMode.setCurrentTenant(effectiveTenantId);
21
+ await ensureAnyApiSchema(knex)
22
+ const sanitizedName = api?.name ? api.name.replace(/[^A-Za-z0-9]+/g, '_').toLowerCase() : 'tenant'
23
+ const effectiveTenantId = tenantId || api?.anyapi?.tenantId || `${sanitizedName}_tenant`
24
+ api.anyapi = api.anyapi || {}
25
+ api.anyapi.tenantId = effectiveTenantId
26
+ await api.use(RestApiAnyapiKnexPlugin, { knex, tenantId: effectiveTenantId })
27
+ storageMode.setCurrentTenant(effectiveTenantId)
28
28
  } else {
29
- await api.use(RestApiKnexPlugin, { knex });
29
+ await api.use(RestApiKnexPlugin, { knex })
30
30
  }
31
31
  }
32
32
 
33
- function mapTable(tableName, resourceName) {
33
+ function mapTable (tableName, resourceName) {
34
34
  if (storageMode.isAnyApi()) {
35
- storageMode.registerTable(tableName, resourceName);
35
+ storageMode.registerTable(tableName, resourceName)
36
36
  }
37
37
  }
38
38
 
39
- async function withTenantContext(tenantId, fn) {
39
+ async function withTenantContext (tenantId, fn) {
40
40
  if (!storageMode.isAnyApi()) {
41
- return await fn();
41
+ return await fn()
42
42
  }
43
43
 
44
- const previousTenant = storageMode.currentTenant;
45
- storageMode.setCurrentTenant(tenantId || storageMode.defaultTenant);
44
+ const previousTenant = storageMode.currentTenant
45
+ storageMode.setCurrentTenant(tenantId || storageMode.defaultTenant)
46
46
  try {
47
- return await fn();
47
+ return await fn()
48
48
  } finally {
49
- storageMode.setCurrentTenant(previousTenant);
49
+ storageMode.setCurrentTenant(previousTenant)
50
50
  }
51
51
  }
52
52
 
53
53
  /**
54
54
  * Creates a basic API configuration with Countries, Publishers, Authors, Books
55
55
  */
56
- export async function createBasicApi(knex, pluginOptions = {}) {
57
- const apiName = pluginOptions.apiName || `basic-test-api`;
58
- const tablePrefix = pluginOptions.tablePrefix || 'basic';
56
+ export async function createBasicApi (knex, pluginOptions = {}) {
57
+ const apiName = pluginOptions.apiName || 'basic-test-api'
58
+ const tablePrefix = pluginOptions.tablePrefix || 'basic'
59
59
  if (storageMode.isAnyApi()) {
60
- storageMode.clearRegistry();
60
+ storageMode.clearRegistry()
61
61
  }
62
62
  const api = new Api({
63
63
  name: apiName,
64
64
  log: { level: process.env.LOG_LEVEL || 'info' }
65
- });
65
+ })
66
66
 
67
- const previousTenant = storageMode.currentTenant;
67
+ const previousTenant = storageMode.currentTenant
68
68
  const tenantId = storageMode.isAnyApi()
69
69
  ? (pluginOptions.tenantId || `${tablePrefix}_tenant`)
70
- : storageMode.defaultTenant;
70
+ : storageMode.defaultTenant
71
71
  if (storageMode.isAnyApi()) {
72
- storageMode.setCurrentTenant(tenantId);
72
+ storageMode.setCurrentTenant(tenantId)
73
73
  }
74
74
 
75
75
  const restApiOptions = {
@@ -87,134 +87,134 @@ export async function createBasicApi(knex, pluginOptions = {}) {
87
87
  },
88
88
  sortableFields: ['id', 'title', 'country_id', 'publisher_id', 'name', 'code'],
89
89
  ...pluginOptions['rest-api'] // Merge any custom options for rest-api plugin
90
- };
90
+ }
91
91
 
92
- await api.use(RestApiPlugin, restApiOptions);
93
- await useStoragePlugin(api, knex, { tenantId });
94
- await resetAnyApiTables(knex);
92
+ await api.use(RestApiPlugin, restApiOptions)
93
+ await useStoragePlugin(api, knex, { tenantId })
94
+ await resetAnyApiTables(knex)
95
95
 
96
96
  // Add Express plugin if requested
97
97
  if (pluginOptions.includeExpress) {
98
98
  await api.use(ExpressPlugin, {
99
99
  mountPath: '/api', // Default mount path for tests
100
100
  ...(pluginOptions.express || {})
101
- });
101
+ })
102
102
  }
103
103
 
104
104
  try {
105
105
  // Countries table
106
- await api.addResource('countries', {
107
- schema: {
108
- id: { type: 'id' },
109
- name: { type: 'string', required: true, max: 100, search: true },
110
- code: { type: 'string', max: 2, unique: true }
111
- },
112
- relationships: {
113
- publishers: { type: 'hasMany', target: 'publishers', foreignKey: 'country_id' },
114
- books: { type: 'hasMany', target: 'books', foreignKey: 'country_id' }
115
- },
116
- tableName: `${tablePrefix}_countries`
117
- });
118
- await api.resources.countries.createKnexTable();
119
- mapTable(`${tablePrefix}_countries`, 'countries');
106
+ await api.addResource('countries', {
107
+ schema: {
108
+ id: { type: 'id' },
109
+ name: { type: 'string', required: true, max: 100, search: true },
110
+ code: { type: 'string', max: 2, unique: true }
111
+ },
112
+ relationships: {
113
+ publishers: { type: 'hasMany', target: 'publishers', foreignKey: 'country_id' },
114
+ books: { type: 'hasMany', target: 'books', foreignKey: 'country_id' }
115
+ },
116
+ tableName: `${tablePrefix}_countries`
117
+ })
118
+ await api.resources.countries.createKnexTable()
119
+ mapTable(`${tablePrefix}_countries`, 'countries')
120
120
 
121
- // Publishers table
122
- await api.addResource('publishers', {
123
- schema: {
124
- id: { type: 'id' },
125
- name: { type: 'string', required: true, max: 200 },
126
- country_id: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' }
127
- },
128
- relationships: {
129
- books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' }
130
- },
131
- tableName: `${tablePrefix}_publishers`
132
- });
133
- await api.resources.publishers.createKnexTable();
134
- mapTable(`${tablePrefix}_publishers`, 'publishers');
121
+ // Publishers table
122
+ await api.addResource('publishers', {
123
+ schema: {
124
+ id: { type: 'id' },
125
+ name: { type: 'string', required: true, max: 200 },
126
+ country_id: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' }
127
+ },
128
+ relationships: {
129
+ books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' }
130
+ },
131
+ tableName: `${tablePrefix}_publishers`
132
+ })
133
+ await api.resources.publishers.createKnexTable()
134
+ mapTable(`${tablePrefix}_publishers`, 'publishers')
135
135
 
136
- // Authors table
137
- await api.addResource('authors', {
138
- schema: {
139
- id: { type: 'id' },
140
- name: { type: 'string', required: true, max: 200 }
141
- },
142
- relationships: {
143
- books: { type: 'manyToMany', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' }
144
- },
145
- tableName: `${tablePrefix}_authors`
146
- });
147
- await api.resources.authors.createKnexTable();
148
- mapTable(`${tablePrefix}_authors`, 'authors');
136
+ // Authors table
137
+ await api.addResource('authors', {
138
+ schema: {
139
+ id: { type: 'id' },
140
+ name: { type: 'string', required: true, max: 200 }
141
+ },
142
+ relationships: {
143
+ books: { type: 'manyToMany', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' }
144
+ },
145
+ tableName: `${tablePrefix}_authors`
146
+ })
147
+ await api.resources.authors.createKnexTable()
148
+ mapTable(`${tablePrefix}_authors`, 'authors')
149
149
 
150
- // Books table
151
- await api.addResource('books', {
152
- schema: {
153
- id: { type: 'id' },
154
- title: { type: 'string', required: true, max: 300, search: true },
155
- country_id: { type: 'number', required: true, belongsTo: 'countries', as: 'country', search: true },
156
- publisher_id: { type: 'number', belongsTo: 'publishers', as: 'publisher', search: true }
157
- },
158
- relationships: {
159
- authors: { type: 'manyToMany', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' }
160
- },
161
- tableName: `${tablePrefix}_books`
162
- });
163
- await api.resources.books.createKnexTable();
164
- mapTable(`${tablePrefix}_books`, 'books');
150
+ // Books table
151
+ await api.addResource('books', {
152
+ schema: {
153
+ id: { type: 'id' },
154
+ title: { type: 'string', required: true, max: 300, search: true },
155
+ country_id: { type: 'number', required: true, belongsTo: 'countries', as: 'country', search: true },
156
+ publisher_id: { type: 'number', belongsTo: 'publishers', as: 'publisher', search: true }
157
+ },
158
+ relationships: {
159
+ authors: { type: 'manyToMany', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' }
160
+ },
161
+ tableName: `${tablePrefix}_books`
162
+ })
163
+ await api.resources.books.createKnexTable()
164
+ mapTable(`${tablePrefix}_books`, 'books')
165
165
 
166
- // Book-Authors pivot table
167
- await api.addResource('book_authors', {
168
- schema: {
169
- id: { type: 'id' },
170
- book_id: { type: 'number', required: true, belongsTo: 'books', as: 'book' },
171
- author_id: { type: 'number', required: true, belongsTo: 'authors', as: 'author' }
172
- },
173
- tableName: `${tablePrefix}_book_authors`
174
- });
175
- await api.resources.book_authors.createKnexTable();
176
- mapTable(`${tablePrefix}_book_authors`, 'book_authors');
177
- if (storageMode.isAnyApi()) {
178
- const descriptorTenant = api.anyapi?.tenantId || tenantId;
179
- const booksDescriptor = await api.anyapi.registry.getDescriptor(descriptorTenant, 'books');
180
- const authorsDescriptor = await api.anyapi.registry.getDescriptor(descriptorTenant, 'authors');
181
- const relationshipKey = booksDescriptor?.manyToMany?.authors?.relationship;
182
- const inverseRelationshipKey = authorsDescriptor?.manyToMany?.books?.relationship;
183
- storageMode.registerLink(
166
+ // Book-Authors pivot table
167
+ await api.addResource('book_authors', {
168
+ schema: {
169
+ id: { type: 'id' },
170
+ book_id: { type: 'number', required: true, belongsTo: 'books', as: 'book' },
171
+ author_id: { type: 'number', required: true, belongsTo: 'authors', as: 'author' }
172
+ },
173
+ tableName: `${tablePrefix}_book_authors`
174
+ })
175
+ await api.resources.book_authors.createKnexTable()
176
+ mapTable(`${tablePrefix}_book_authors`, 'book_authors')
177
+ if (storageMode.isAnyApi()) {
178
+ const descriptorTenant = api.anyapi?.tenantId || tenantId
179
+ const booksDescriptor = await api.anyapi.registry.getDescriptor(descriptorTenant, 'books')
180
+ const authorsDescriptor = await api.anyapi.registry.getDescriptor(descriptorTenant, 'authors')
181
+ const relationshipKey = booksDescriptor?.manyToMany?.authors?.relationship
182
+ const inverseRelationshipKey = authorsDescriptor?.manyToMany?.books?.relationship
183
+ storageMode.registerLink(
184
184
  `${tablePrefix}_book_authors`,
185
185
  'books',
186
186
  'authors',
187
187
  relationshipKey,
188
- inverseRelationshipKey,
189
- );
190
- }
188
+ inverseRelationshipKey
189
+ )
190
+ }
191
191
 
192
- return api;
192
+ return api
193
193
  } finally {
194
194
  if (storageMode.isAnyApi()) {
195
- storageMode.setCurrentTenant(previousTenant);
195
+ storageMode.setCurrentTenant(previousTenant)
196
196
  }
197
197
  }
198
198
  }
199
199
 
200
- export async function createAccessControlApi(knex, pluginOptions = {}) {
201
- const apiName = pluginOptions.apiName || 'access-test-api';
202
- const tablePrefix = pluginOptions.tablePrefix || 'access';
200
+ export async function createAccessControlApi (knex, pluginOptions = {}) {
201
+ const apiName = pluginOptions.apiName || 'access-test-api'
202
+ const tablePrefix = pluginOptions.tablePrefix || 'access'
203
203
  if (storageMode.isAnyApi()) {
204
- storageMode.clearRegistry();
204
+ storageMode.clearRegistry()
205
205
  }
206
206
 
207
207
  const api = new Api({
208
208
  name: apiName,
209
209
  log: { level: process.env.LOG_LEVEL || 'info' }
210
- });
210
+ })
211
211
 
212
- const previousTenant = storageMode.currentTenant;
212
+ const previousTenant = storageMode.currentTenant
213
213
  const tenantId = storageMode.isAnyApi()
214
214
  ? (pluginOptions.tenantId || `${tablePrefix}_tenant`)
215
- : storageMode.defaultTenant;
215
+ : storageMode.defaultTenant
216
216
  if (storageMode.isAnyApi()) {
217
- storageMode.setCurrentTenant(tenantId);
217
+ storageMode.setCurrentTenant(tenantId)
218
218
  }
219
219
 
220
220
  await api.use(RestApiPlugin, {
@@ -231,12 +231,12 @@ export async function createAccessControlApi(knex, pluginOptions = {}) {
231
231
  patch: 'minimal'
232
232
  },
233
233
  sortableFields: ['id', 'name', 'user_id']
234
- });
235
- await useStoragePlugin(api, knex, { tenantId });
236
- await resetAnyApiTables(knex);
234
+ })
235
+ await useStoragePlugin(api, knex, { tenantId })
236
+ await resetAnyApiTables(knex)
237
237
 
238
238
  try {
239
- await api.use(AccessPlugin, pluginOptions.access || {});
239
+ await api.use(AccessPlugin, pluginOptions.access || {})
240
240
 
241
241
  await api.addResource('users', {
242
242
  schema: {
@@ -245,9 +245,9 @@ export async function createAccessControlApi(knex, pluginOptions = {}) {
245
245
  display_name: { type: 'string', nullable: true }
246
246
  },
247
247
  tableName: `${tablePrefix}_users`
248
- });
249
- await api.resources.users.createKnexTable();
250
- mapTable(`${tablePrefix}_users`, 'users');
248
+ })
249
+ await api.resources.users.createKnexTable()
250
+ mapTable(`${tablePrefix}_users`, 'users')
251
251
 
252
252
  await api.addResource('projects', {
253
253
  schema: {
@@ -268,9 +268,9 @@ export async function createAccessControlApi(knex, pluginOptions = {}) {
268
268
  },
269
269
  ownership: true,
270
270
  tableName: `${tablePrefix}_projects`
271
- });
272
- await api.resources.projects.createKnexTable();
273
- mapTable(`${tablePrefix}_projects`, 'projects');
271
+ })
272
+ await api.resources.projects.createKnexTable()
273
+ mapTable(`${tablePrefix}_projects`, 'projects')
274
274
 
275
275
  await api.addResource('notes', {
276
276
  schema: {
@@ -292,14 +292,14 @@ export async function createAccessControlApi(knex, pluginOptions = {}) {
292
292
  },
293
293
  ownership: true,
294
294
  tableName: `${tablePrefix}_notes`
295
- });
296
- await api.resources.notes.createKnexTable();
297
- mapTable(`${tablePrefix}_notes`, 'notes');
295
+ })
296
+ await api.resources.notes.createKnexTable()
297
+ mapTable(`${tablePrefix}_notes`, 'notes')
298
298
 
299
- return api;
299
+ return api
300
300
  } finally {
301
301
  if (storageMode.isAnyApi()) {
302
- storageMode.setCurrentTenant(previousTenant);
302
+ storageMode.setCurrentTenant(previousTenant)
303
303
  }
304
304
  }
305
305
  }
@@ -307,13 +307,13 @@ export async function createAccessControlApi(knex, pluginOptions = {}) {
307
307
  /**
308
308
  * Creates a basic API with bulk operations enabled
309
309
  */
310
- export async function createBulkOperationsApi(knex, pluginOptions = {}) {
311
- const { BulkOperationsPlugin } = await import('../../plugins/core/bulk-operations-plugin.js');
312
-
310
+ export async function createBulkOperationsApi (knex, pluginOptions = {}) {
311
+ const { BulkOperationsPlugin } = await import('../../plugins/core/bulk-operations-plugin.js')
312
+
313
313
  const api = await createBasicApi(knex, {
314
314
  ...pluginOptions,
315
315
  tenantId: pluginOptions.tenantId || 'bulk_ops_tenant'
316
- });
316
+ })
317
317
 
318
318
  // Add bulk operations plugin
319
319
  await api.use(BulkOperationsPlugin, {
@@ -324,20 +324,20 @@ export async function createBulkOperationsApi(knex, pluginOptions = {}) {
324
324
  enableOptimizations: true,
325
325
  ...pluginOptions['bulk-operations']
326
326
  }
327
- });
328
-
329
- return api;
327
+ })
328
+
329
+ return api
330
330
  }
331
331
 
332
332
  /**
333
333
  * Creates an extended API with additional fields for more complex testing
334
334
  */
335
- export async function createExtendedApi(knex) {
335
+ export async function createExtendedApi (knex) {
336
336
  const api = new Api({
337
337
  name: 'extended-test-api',
338
- });
338
+ })
339
339
 
340
- const tenantId = storageMode.isAnyApi() ? 'extended_tenant' : storageMode.defaultTenant;
340
+ const tenantId = storageMode.isAnyApi() ? 'extended_tenant' : storageMode.defaultTenant
341
341
 
342
342
  await api.use(RestApiPlugin, {
343
343
  simplifiedApi: false,
@@ -353,164 +353,164 @@ export async function createExtendedApi(knex) {
353
353
  patch: 'minimal' // Tests expect minimal response with ID
354
354
  },
355
355
  sortableFields: ['id', 'title', 'country_id', 'publisher_id', 'price', 'language', 'population', 'name', 'code']
356
- });
356
+ })
357
357
  await withTenantContext(tenantId, async () => {
358
- await useStoragePlugin(api, knex, { tenantId });
359
- await resetAnyApiTables(knex);
358
+ await useStoragePlugin(api, knex, { tenantId })
359
+ await resetAnyApiTables(knex)
360
360
 
361
361
  // Countries with extended fields
362
362
  await api.addResource('countries', {
363
- schema: {
364
- id: { type: 'id' },
365
- name: { type: 'string', required: true, max: 100 },
366
- code: { type: 'string', max: 2, unique: true },
367
- capital: { type: 'string', max: 100 },
368
- population: { type: 'number' },
369
- currency: { type: 'string', max: 3 }
370
- },
371
- relationships: {
372
- publishers: { type: 'hasMany', target: 'publishers', foreignKey: 'country_id' },
373
- books: { type: 'hasMany', target: 'books', foreignKey: 'country_id' },
374
- authors: { type: 'hasMany', target: 'authors', foreignKey: 'nationality_id' }
375
- },
376
- tableName: 'ext_countries'
377
- });
378
- await api.resources.countries.createKnexTable();
379
- mapTable('ext_countries', 'countries');
363
+ schema: {
364
+ id: { type: 'id' },
365
+ name: { type: 'string', required: true, max: 100 },
366
+ code: { type: 'string', max: 2, unique: true },
367
+ capital: { type: 'string', max: 100 },
368
+ population: { type: 'number' },
369
+ currency: { type: 'string', max: 3 }
370
+ },
371
+ relationships: {
372
+ publishers: { type: 'hasMany', target: 'publishers', foreignKey: 'country_id' },
373
+ books: { type: 'hasMany', target: 'books', foreignKey: 'country_id' },
374
+ authors: { type: 'hasMany', target: 'authors', foreignKey: 'nationality_id' }
375
+ },
376
+ tableName: 'ext_countries'
377
+ })
378
+ await api.resources.countries.createKnexTable()
379
+ mapTable('ext_countries', 'countries')
380
380
 
381
- // Publishers with extended fields
382
- await api.addResource('publishers', {
383
- schema: {
384
- id: { type: 'id' },
385
- name: { type: 'string', required: true, max: 200 },
386
- country_id: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' },
387
- founded_year: { type: 'number' },
388
- website: { type: 'string', max: 255 },
389
- active: { type: 'boolean', default: true }
390
- },
391
- relationships: {
392
- books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' },
393
- reviews: {
394
- type: 'hasMany',
395
- target: 'reviews',
396
- via: 'reviewable'
397
- }
398
- },
399
- tableName: 'ext_publishers'
400
- });
401
- await api.resources.publishers.createKnexTable();
402
- mapTable('ext_publishers', 'publishers');
381
+ // Publishers with extended fields
382
+ await api.addResource('publishers', {
383
+ schema: {
384
+ id: { type: 'id' },
385
+ name: { type: 'string', required: true, max: 200 },
386
+ country_id: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' },
387
+ founded_year: { type: 'number' },
388
+ website: { type: 'string', max: 255 },
389
+ active: { type: 'boolean', default: true }
390
+ },
391
+ relationships: {
392
+ books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' },
393
+ reviews: {
394
+ type: 'hasMany',
395
+ target: 'reviews',
396
+ via: 'reviewable'
397
+ }
398
+ },
399
+ tableName: 'ext_publishers'
400
+ })
401
+ await api.resources.publishers.createKnexTable()
402
+ mapTable('ext_publishers', 'publishers')
403
403
 
404
- // Authors with extended fields
405
- await api.addResource('authors', {
406
- schema: {
407
- id: { type: 'id' },
408
- name: { type: 'string', required: true, max: 200 },
409
- birth_date: { type: 'date' },
410
- biography: { type: 'string', max: 5000 },
411
- nationality_id: { type: 'number', belongsTo: 'countries', as: 'nationality' }
412
- },
413
- relationships: {
414
- books: { type: 'manyToMany', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' },
415
- reviews: {
416
- type: 'hasMany',
417
- target: 'reviews',
418
- via: 'reviewable'
419
- }
420
- },
421
- tableName: 'ext_authors'
422
- });
423
- await api.resources.authors.createKnexTable();
424
- mapTable('ext_authors', 'authors');
404
+ // Authors with extended fields
405
+ await api.addResource('authors', {
406
+ schema: {
407
+ id: { type: 'id' },
408
+ name: { type: 'string', required: true, max: 200 },
409
+ birth_date: { type: 'date' },
410
+ biography: { type: 'string', max: 5000 },
411
+ nationality_id: { type: 'number', belongsTo: 'countries', as: 'nationality' }
412
+ },
413
+ relationships: {
414
+ books: { type: 'manyToMany', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' },
415
+ reviews: {
416
+ type: 'hasMany',
417
+ target: 'reviews',
418
+ via: 'reviewable'
419
+ }
420
+ },
421
+ tableName: 'ext_authors'
422
+ })
423
+ await api.resources.authors.createKnexTable()
424
+ mapTable('ext_authors', 'authors')
425
425
 
426
- // Books with extended fields
427
- await api.addResource('books', {
428
- schema: {
429
- id: { type: 'id' },
430
- title: { type: 'string', required: true, max: 300, search: true },
431
- isbn: { type: 'string', max: 13 },
432
- pages: { type: 'number' },
433
- price: { type: 'number', search: true }, // Store price as string for decimal precision
434
- published_date: { type: 'date' },
435
- language: { type: 'string', max: 2, default: 'en', search: true },
436
- country_id: { type: 'number', required: true, belongsTo: 'countries', as: 'country', search: true },
437
- publisher_id: { type: 'number', belongsTo: 'publishers', as: 'publisher', search: true }
438
- },
439
- relationships: {
440
- authors: { type: 'manyToMany', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' },
441
- reviews: {
442
- type: 'hasMany',
443
- target: 'reviews',
444
- via: 'reviewable'
445
- }
446
- },
447
- tableName: 'ext_books'
448
- });
449
- await api.resources.books.createKnexTable();
450
- mapTable('ext_books', 'books');
426
+ // Books with extended fields
427
+ await api.addResource('books', {
428
+ schema: {
429
+ id: { type: 'id' },
430
+ title: { type: 'string', required: true, max: 300, search: true },
431
+ isbn: { type: 'string', max: 13 },
432
+ pages: { type: 'number' },
433
+ price: { type: 'number', search: true }, // Store price as string for decimal precision
434
+ published_date: { type: 'date' },
435
+ language: { type: 'string', max: 2, default: 'en', search: true },
436
+ country_id: { type: 'number', required: true, belongsTo: 'countries', as: 'country', search: true },
437
+ publisher_id: { type: 'number', belongsTo: 'publishers', as: 'publisher', search: true }
438
+ },
439
+ relationships: {
440
+ authors: { type: 'manyToMany', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' },
441
+ reviews: {
442
+ type: 'hasMany',
443
+ target: 'reviews',
444
+ via: 'reviewable'
445
+ }
446
+ },
447
+ tableName: 'ext_books'
448
+ })
449
+ await api.resources.books.createKnexTable()
450
+ mapTable('ext_books', 'books')
451
451
 
452
- // Book-Authors pivot with extended fields
453
- await api.addResource('book_authors', {
454
- schema: {
455
- id: { type: 'id' },
456
- book_id: { type: 'number', required: true, belongsTo: 'books', as: 'book' },
457
- author_id: { type: 'number', required: true, belongsTo: 'authors', as: 'author' },
458
- contribution_type: { type: 'string', max: 50 },
459
- order: { type: 'number' }
460
- },
461
- tableName: 'ext_book_authors'
462
- });
463
- await api.resources.book_authors.createKnexTable();
464
- mapTable('ext_book_authors', 'book_authors');
465
- if (storageMode.isAnyApi()) {
466
- const booksDescriptor = await api.anyapi.registry.getDescriptor(tenantId, 'books');
467
- const authorsDescriptor = await api.anyapi.registry.getDescriptor(tenantId, 'authors');
468
- const relationshipKey = booksDescriptor?.manyToMany?.authors?.relationship;
469
- const inverseRelationshipKey = authorsDescriptor?.manyToMany?.books?.relationship;
470
- storageMode.registerLink('ext_book_authors', 'books', 'authors', relationshipKey, inverseRelationshipKey);
471
- }
452
+ // Book-Authors pivot with extended fields
453
+ await api.addResource('book_authors', {
454
+ schema: {
455
+ id: { type: 'id' },
456
+ book_id: { type: 'number', required: true, belongsTo: 'books', as: 'book' },
457
+ author_id: { type: 'number', required: true, belongsTo: 'authors', as: 'author' },
458
+ contribution_type: { type: 'string', max: 50 },
459
+ order: { type: 'number' }
460
+ },
461
+ tableName: 'ext_book_authors'
462
+ })
463
+ await api.resources.book_authors.createKnexTable()
464
+ mapTable('ext_book_authors', 'book_authors')
465
+ if (storageMode.isAnyApi()) {
466
+ const booksDescriptor = await api.anyapi.registry.getDescriptor(tenantId, 'books')
467
+ const authorsDescriptor = await api.anyapi.registry.getDescriptor(tenantId, 'authors')
468
+ const relationshipKey = booksDescriptor?.manyToMany?.authors?.relationship
469
+ const inverseRelationshipKey = authorsDescriptor?.manyToMany?.books?.relationship
470
+ storageMode.registerLink('ext_book_authors', 'books', 'authors', relationshipKey, inverseRelationshipKey)
471
+ }
472
472
 
473
- // Polymorphic reviews (can go on authors, books and publishers)
474
- await api.addResource('reviews', {
475
- schema: {
476
- id: { type: 'id' },
477
- rating: { type: 'number', required: true, min: 1, max: 5 },
478
- title: { type: 'string', max: 200 },
479
- content: { type: 'string', required: true, max: 5000 },
480
- reviewer_name: { type: 'string', required: true, max: 100 },
481
- review_date: { type: 'dateTime', default: 'now()' },
482
- helpful_count: { type: 'number', default: 0 },
483
- reviewable_type: { type: 'string', required: true },
484
- reviewable_id: { type: 'number', required: true }
485
- },
486
- relationships: {
487
- reviewable: {
488
- belongsToPolymorphic: {
489
- types: ['books', 'authors', 'publishers'],
490
- typeField: 'reviewable_type',
491
- idField: 'reviewable_id'
473
+ // Polymorphic reviews (can go on authors, books and publishers)
474
+ await api.addResource('reviews', {
475
+ schema: {
476
+ id: { type: 'id' },
477
+ rating: { type: 'number', required: true, min: 1, max: 5 },
478
+ title: { type: 'string', max: 200 },
479
+ content: { type: 'string', required: true, max: 5000 },
480
+ reviewer_name: { type: 'string', required: true, max: 100 },
481
+ review_date: { type: 'dateTime', default: 'now()' },
482
+ helpful_count: { type: 'number', default: 0 },
483
+ reviewable_type: { type: 'string', required: true },
484
+ reviewable_id: { type: 'number', required: true }
485
+ },
486
+ relationships: {
487
+ reviewable: {
488
+ belongsToPolymorphic: {
489
+ types: ['books', 'authors', 'publishers'],
490
+ typeField: 'reviewable_type',
491
+ idField: 'reviewable_id'
492
+ }
492
493
  }
493
- }
494
- },
495
- tableName: 'ext_reviews'
496
- });
497
- await api.resources.reviews.createKnexTable();
498
- mapTable('ext_reviews', 'reviews');
499
- });
494
+ },
495
+ tableName: 'ext_reviews'
496
+ })
497
+ await api.resources.reviews.createKnexTable()
498
+ mapTable('ext_reviews', 'reviews')
499
+ })
500
500
 
501
- return api;
501
+ return api
502
502
  }
503
503
 
504
504
  /**
505
505
  * Creates an API with limited include depth for testing depth validation
506
506
  * Uses 'limited_' prefix for all tables to avoid conflicts
507
507
  */
508
- export async function createLimitedDepthApi(knex) {
508
+ export async function createLimitedDepthApi (knex) {
509
509
  const api = new Api({
510
510
  name: 'limited-depth-api',
511
- });
511
+ })
512
512
 
513
- const tenantId = storageMode.isAnyApi() ? 'limited_depth_tenant' : storageMode.defaultTenant;
513
+ const tenantId = storageMode.isAnyApi() ? 'limited_depth_tenant' : storageMode.defaultTenant
514
514
 
515
515
  await api.use(RestApiPlugin, {
516
516
  simplifiedApi: false,
@@ -527,99 +527,99 @@ export async function createLimitedDepthApi(knex) {
527
527
  },
528
528
  sortableFields: ['id', 'title', 'country_id', 'publisher_id', 'name', 'code'],
529
529
  includeDepthLimit: 2 // Key difference: limit is 2 instead of default 3
530
- });
530
+ })
531
531
  await withTenantContext(tenantId, async () => {
532
- await useStoragePlugin(api, knex, { tenantId });
532
+ await useStoragePlugin(api, knex, { tenantId })
533
533
 
534
534
  // Use different table names with 'limited_' prefix to avoid conflicts
535
535
  await api.addResource('countries', {
536
- schema: {
537
- id: { type: 'id' },
538
- name: { type: 'string', required: true, max: 100 },
539
- code: { type: 'string', max: 2, unique: true }
540
- },
541
- relationships: {
542
- publishers: { type: 'hasMany', target: 'publishers', foreignKey: 'country_id' },
543
- books: { type: 'hasMany', target: 'books', foreignKey: 'country_id' }
544
- },
545
- tableName: 'limited_countries',
546
- });
547
- await api.resources.countries.createKnexTable();
548
- mapTable('limited_countries', 'countries');
536
+ schema: {
537
+ id: { type: 'id' },
538
+ name: { type: 'string', required: true, max: 100 },
539
+ code: { type: 'string', max: 2, unique: true }
540
+ },
541
+ relationships: {
542
+ publishers: { type: 'hasMany', target: 'publishers', foreignKey: 'country_id' },
543
+ books: { type: 'hasMany', target: 'books', foreignKey: 'country_id' }
544
+ },
545
+ tableName: 'limited_countries',
546
+ })
547
+ await api.resources.countries.createKnexTable()
548
+ mapTable('limited_countries', 'countries')
549
549
 
550
- await api.addResource('publishers', {
551
- schema: {
552
- id: { type: 'id' },
553
- name: { type: 'string', required: true, max: 200 },
554
- country_id: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' }
555
- },
556
- relationships: {
557
- books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' },
558
- authors: { type: 'hasMany', target: 'authors', foreignKey: 'publisher_id' }
559
- },
560
- tableName: 'limited_publishers'
561
- });
562
- await api.resources.publishers.createKnexTable();
563
- mapTable('limited_publishers', 'publishers');
550
+ await api.addResource('publishers', {
551
+ schema: {
552
+ id: { type: 'id' },
553
+ name: { type: 'string', required: true, max: 200 },
554
+ country_id: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' }
555
+ },
556
+ relationships: {
557
+ books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' },
558
+ authors: { type: 'hasMany', target: 'authors', foreignKey: 'publisher_id' }
559
+ },
560
+ tableName: 'limited_publishers'
561
+ })
562
+ await api.resources.publishers.createKnexTable()
563
+ mapTable('limited_publishers', 'publishers')
564
564
 
565
- await api.addResource('authors', {
566
- schema: {
567
- id: { type: 'id' },
568
- name: { type: 'string', required: true, max: 200 },
569
- publisher_id: { type: 'number', nullable: true, belongsTo: 'publishers', as: 'publisher' }
570
- },
571
- relationships: {
572
- books: { type: 'manyToMany', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' }
573
- },
574
- tableName: 'limited_authors'
575
- });
576
- await api.resources.authors.createKnexTable();
565
+ await api.addResource('authors', {
566
+ schema: {
567
+ id: { type: 'id' },
568
+ name: { type: 'string', required: true, max: 200 },
569
+ publisher_id: { type: 'number', nullable: true, belongsTo: 'publishers', as: 'publisher' }
570
+ },
571
+ relationships: {
572
+ books: { type: 'manyToMany', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' }
573
+ },
574
+ tableName: 'limited_authors'
575
+ })
576
+ await api.resources.authors.createKnexTable()
577
577
 
578
- await api.addResource('books', {
579
- schema: {
580
- id: { type: 'id' },
581
- title: { type: 'string', required: true, max: 300, search: true },
582
- country_id: { type: 'number', required: true, belongsTo: 'countries', as: 'country', search: true },
583
- publisher_id: { type: 'number', belongsTo: 'publishers', as: 'publisher', search: true }
584
- },
585
- relationships: {
586
- authors: { type: 'manyToMany', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' }
587
- },
588
- tableName: 'limited_books'
589
- });
590
- await api.resources.books.createKnexTable();
578
+ await api.addResource('books', {
579
+ schema: {
580
+ id: { type: 'id' },
581
+ title: { type: 'string', required: true, max: 300, search: true },
582
+ country_id: { type: 'number', required: true, belongsTo: 'countries', as: 'country', search: true },
583
+ publisher_id: { type: 'number', belongsTo: 'publishers', as: 'publisher', search: true }
584
+ },
585
+ relationships: {
586
+ authors: { type: 'manyToMany', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' }
587
+ },
588
+ tableName: 'limited_books'
589
+ })
590
+ await api.resources.books.createKnexTable()
591
591
 
592
- await api.addResource('book_authors', {
593
- schema: {
594
- id: { type: 'id' },
595
- book_id: { type: 'number', required: true, belongsTo: 'books', as: 'book' },
596
- author_id: { type: 'number', required: true, belongsTo: 'authors', as: 'author' }
597
- },
598
- tableName: 'limited_book_authors'
599
- });
600
- await api.resources.book_authors.createKnexTable();
601
- mapTable('limited_book_authors', 'book_authors');
602
- if (storageMode.isAnyApi()) {
603
- const booksDescriptor = await api.anyapi.registry.getDescriptor(tenantId, 'books');
604
- const authorsDescriptor = await api.anyapi.registry.getDescriptor(tenantId, 'authors');
605
- const relationshipKey = booksDescriptor?.manyToMany?.authors?.relationship;
606
- const inverseRelationshipKey = authorsDescriptor?.manyToMany?.books?.relationship;
607
- storageMode.registerLink('limited_book_authors', 'books', 'authors', relationshipKey, inverseRelationshipKey);
608
- }
609
- });
592
+ await api.addResource('book_authors', {
593
+ schema: {
594
+ id: { type: 'id' },
595
+ book_id: { type: 'number', required: true, belongsTo: 'books', as: 'book' },
596
+ author_id: { type: 'number', required: true, belongsTo: 'authors', as: 'author' }
597
+ },
598
+ tableName: 'limited_book_authors'
599
+ })
600
+ await api.resources.book_authors.createKnexTable()
601
+ mapTable('limited_book_authors', 'book_authors')
602
+ if (storageMode.isAnyApi()) {
603
+ const booksDescriptor = await api.anyapi.registry.getDescriptor(tenantId, 'books')
604
+ const authorsDescriptor = await api.anyapi.registry.getDescriptor(tenantId, 'authors')
605
+ const relationshipKey = booksDescriptor?.manyToMany?.authors?.relationship
606
+ const inverseRelationshipKey = authorsDescriptor?.manyToMany?.books?.relationship
607
+ storageMode.registerLink('limited_book_authors', 'books', 'authors', relationshipKey, inverseRelationshipKey)
608
+ }
609
+ })
610
610
 
611
- return api;
611
+ return api
612
612
  }
613
613
 
614
614
  /**
615
615
  * Creates an API configuration for pagination testing
616
616
  */
617
- export async function createPaginationApi(knex, options = {}) {
617
+ export async function createPaginationApi (knex, options = {}) {
618
618
  const api = new Api({
619
619
  name: 'pagination-test-api',
620
- });
620
+ })
621
621
 
622
- const tenantId = storageMode.isAnyApi() ? 'pagination_tenant' : storageMode.defaultTenant;
622
+ const tenantId = storageMode.isAnyApi() ? 'pagination_tenant' : storageMode.defaultTenant
623
623
 
624
624
  const restApiOptions = {
625
625
  simplifiedApi: false,
@@ -636,66 +636,66 @@ export async function createPaginationApi(knex, options = {}) {
636
636
  },
637
637
  sortableFields: ['id', 'title', 'country_id', 'publisher_id', 'name', 'code'],
638
638
  ...options // Allow overriding options likereturnBasePath, enablePaginationCounts
639
- };
639
+ }
640
640
 
641
- await api.use(RestApiPlugin, restApiOptions);
641
+ await api.use(RestApiPlugin, restApiOptions)
642
642
  await withTenantContext(tenantId, async () => {
643
- await useStoragePlugin(api, knex, { tenantId });
643
+ await useStoragePlugin(api, knex, { tenantId })
644
644
 
645
645
  // Countries table
646
646
  await api.addResource('countries', {
647
- schema: {
648
- id: { type: 'id' },
649
- name: { type: 'string', required: true, max: 100 },
650
- code: { type: 'string', max: 2, unique: true }
651
- },
652
- relationships: {
653
- publishers: { type: 'hasMany', target: 'publishers', foreignKey: 'country_id' },
654
- books: { type: 'hasMany', target: 'books', foreignKey: 'country_id' }
655
- },
656
- tableName: 'pagination_countries'
657
- });
658
- await api.resources.countries.createKnexTable();
659
- mapTable('pagination_countries', 'countries');
647
+ schema: {
648
+ id: { type: 'id' },
649
+ name: { type: 'string', required: true, max: 100 },
650
+ code: { type: 'string', max: 2, unique: true }
651
+ },
652
+ relationships: {
653
+ publishers: { type: 'hasMany', target: 'publishers', foreignKey: 'country_id' },
654
+ books: { type: 'hasMany', target: 'books', foreignKey: 'country_id' }
655
+ },
656
+ tableName: 'pagination_countries'
657
+ })
658
+ await api.resources.countries.createKnexTable()
659
+ mapTable('pagination_countries', 'countries')
660
660
 
661
- // Publishers table
662
- await api.addResource('publishers', {
663
- schema: {
664
- id: { type: 'id' },
665
- name: { type: 'string', required: true, max: 200 },
666
- country_id: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' }
667
- },
668
- relationships: {
669
- books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' }
670
- },
671
- tableName: 'pagination_publishers'
672
- });
673
- await api.resources.publishers.createKnexTable();
674
- mapTable('pagination_publishers', 'publishers');
661
+ // Publishers table
662
+ await api.addResource('publishers', {
663
+ schema: {
664
+ id: { type: 'id' },
665
+ name: { type: 'string', required: true, max: 200 },
666
+ country_id: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' }
667
+ },
668
+ relationships: {
669
+ books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' }
670
+ },
671
+ tableName: 'pagination_publishers'
672
+ })
673
+ await api.resources.publishers.createKnexTable()
674
+ mapTable('pagination_publishers', 'publishers')
675
675
 
676
- // Books table
677
- await api.addResource('books', {
678
- schema: {
679
- id: { type: 'id' },
680
- title: { type: 'string', required: true, max: 300, search: true },
681
- country_id: { type: 'number', required: true, belongsTo: 'countries', as: 'country', search: true },
682
- publisher_id: { type: 'number', belongsTo: 'publishers', as: 'publisher', search: true }
683
- },
684
- tableName: 'pagination_books'
685
- });
686
- await api.resources.books.createKnexTable();
687
- mapTable('pagination_books', 'books');
688
- });
676
+ // Books table
677
+ await api.addResource('books', {
678
+ schema: {
679
+ id: { type: 'id' },
680
+ title: { type: 'string', required: true, max: 300, search: true },
681
+ country_id: { type: 'number', required: true, belongsTo: 'countries', as: 'country', search: true },
682
+ publisher_id: { type: 'number', belongsTo: 'publishers', as: 'publisher', search: true }
683
+ },
684
+ tableName: 'pagination_books'
685
+ })
686
+ await api.resources.books.createKnexTable()
687
+ mapTable('pagination_books', 'books')
688
+ })
689
689
 
690
- return api;
690
+ return api
691
691
  }
692
692
 
693
693
  /**
694
694
  * Creates an API with WebSocket/Socket.IO support for testing
695
695
  */
696
- export async function createWebSocketApi(knex, pluginOptions = {}) {
697
- const { SocketIOPlugin } = await import('../../plugins/core/socketio-plugin.js');
698
- const { jwtVerify } = await import('jose');
696
+ export async function createWebSocketApi (knex, pluginOptions = {}) {
697
+ const { SocketIOPlugin } = await import('../../plugins/core/socketio-plugin.js')
698
+ const { jwtVerify } = await import('jose')
699
699
 
700
700
  const api = await createBasicApi(knex, {
701
701
  ...pluginOptions,
@@ -704,91 +704,91 @@ export async function createWebSocketApi(knex, pluginOptions = {}) {
704
704
  express: {
705
705
  port: 0 // Let OS assign a port
706
706
  }
707
- });
707
+ })
708
708
 
709
- const socketioOptions = { ...(pluginOptions['socketio'] || {}) };
710
- const authOptions = { ...(socketioOptions.auth || {}) };
711
- const sharedSecret = authOptions.sharedSecret || socketioOptions.sharedSecret || 'test-secret-key';
709
+ const socketioOptions = { ...(pluginOptions['socketio'] || {}) }
710
+ const authOptions = { ...(socketioOptions.auth || {}) }
711
+ const sharedSecret = authOptions.sharedSecret || socketioOptions.sharedSecret || 'test-secret-key'
712
712
 
713
713
  if (!authOptions.authenticate) {
714
714
  authOptions.authenticate = async ({ socket }) => {
715
- const token = socket.handshake.auth?.token;
715
+ const token = socket.handshake.auth?.token
716
716
  if (!token) {
717
- throw new Error('Authentication required');
717
+ throw new Error('Authentication required')
718
718
  }
719
719
 
720
- const encoder = new TextEncoder();
721
- const key = encoder.encode(sharedSecret);
722
- const { payload } = await jwtVerify(token, key);
720
+ const encoder = new TextEncoder()
721
+ const key = encoder.encode(sharedSecret)
722
+ const { payload } = await jwtVerify(token, key)
723
723
 
724
724
  const roles = Array.isArray(payload.roles)
725
725
  ? payload.roles
726
- : payload.role ? [payload.role] : [];
726
+ : payload.role ? [payload.role] : []
727
727
 
728
728
  const authContext = {
729
729
  userId: payload.userId || payload.sub,
730
730
  roles,
731
731
  token: payload
732
- };
732
+ }
733
733
 
734
734
  if (!authContext.userId) {
735
- throw new Error('Token is missing required user identifier');
735
+ throw new Error('Token is missing required user identifier')
736
736
  }
737
737
 
738
- return authContext;
739
- };
738
+ return authContext
739
+ }
740
740
  }
741
741
 
742
742
  if (authOptions.requireAuth === undefined) {
743
- authOptions.requireAuth = true;
743
+ authOptions.requireAuth = true
744
744
  }
745
745
 
746
- socketioOptions.auth = authOptions;
746
+ socketioOptions.auth = authOptions
747
+
748
+ await api.use(SocketIOPlugin, socketioOptions)
747
749
 
748
- await api.use(SocketIOPlugin, socketioOptions);
749
-
750
750
  // Create and start Express server
751
- const app = express();
752
- api.http.express.app = app;
753
-
751
+ const app = express()
752
+ api.http.express.app = app
753
+
754
754
  // Mount the API routes
755
- app.use('/api', api.http.express.router);
756
-
755
+ app.use('/api', api.http.express.router)
756
+
757
757
  // Create HTTP server
758
- const server = createServer(app);
759
-
758
+ const server = createServer(app)
759
+
760
760
  // Start Socket.IO server
761
- await api.startSocketServer(server);
762
-
761
+ await api.startSocketServer(server)
762
+
763
763
  // Start listening
764
764
  await new Promise((resolve) => {
765
765
  server.listen(0, () => {
766
- resolve();
767
- });
768
- });
769
-
770
- return { api, server };
766
+ resolve()
767
+ })
768
+ })
769
+
770
+ return { api, server }
771
771
  }
772
772
 
773
773
  /**
774
774
  * Creates an API with computed fields for testing
775
775
  */
776
- export async function createComputedFieldsApi(knex, pluginOptions = {}) {
776
+ export async function createComputedFieldsApi (knex, pluginOptions = {}) {
777
777
  const api = new Api({
778
778
  name: 'computed-fields-test-api',
779
779
  log: { level: process.env.LOG_LEVEL || 'info' }
780
- });
780
+ })
781
781
 
782
- const tenantId = storageMode.isAnyApi() ? 'computed_fields_tenant' : storageMode.defaultTenant;
782
+ const tenantId = storageMode.isAnyApi() ? 'computed_fields_tenant' : storageMode.defaultTenant
783
783
 
784
784
  await api.use(RestApiPlugin, {
785
785
  simplifiedApi: true,
786
786
  simplifiedTransport: true
787
- });
787
+ })
788
788
 
789
789
  await withTenantContext(tenantId, async () => {
790
- await useStoragePlugin(api, knex, { tenantId });
791
- await resetAnyApiTables(knex);
790
+ await useStoragePlugin(api, knex, { tenantId })
791
+ await resetAnyApiTables(knex)
792
792
 
793
793
  // Products resource with computed fields
794
794
  await api.addResource('products', {
@@ -803,8 +803,8 @@ export async function createComputedFieldsApi(knex, pluginOptions = {}) {
803
803
  computed: true,
804
804
  dependencies: ['price', 'cost'],
805
805
  compute: ({ attributes }) => {
806
- if (!attributes.price || attributes.price === 0) return 0;
807
- return Number(((attributes.price - attributes.cost) / attributes.price * 100).toFixed(2));
806
+ if (!attributes.price || attributes.price === 0) return 0
807
+ return Number(((attributes.price - attributes.cost) / attributes.price * 100).toFixed(2))
808
808
  }
809
809
  },
810
810
  profit_amount: {
@@ -812,7 +812,7 @@ export async function createComputedFieldsApi(knex, pluginOptions = {}) {
812
812
  computed: true,
813
813
  dependencies: ['price', 'cost'],
814
814
  compute: ({ attributes }) => {
815
- return Number((attributes.price - attributes.cost).toFixed(2));
815
+ return Number((attributes.price - attributes.cost).toFixed(2))
816
816
  }
817
817
  }
818
818
  },
@@ -820,10 +820,10 @@ export async function createComputedFieldsApi(knex, pluginOptions = {}) {
820
820
  reviews: { type: 'hasMany', target: 'reviews', foreignKey: 'product_id' }
821
821
  },
822
822
  tableName: 'test_products'
823
- });
824
- await api.resources.products.createKnexTable();
825
- mapTable('searchmerge_products', 'products');
826
- mapTable('test_products', 'products');
823
+ })
824
+ await api.resources.products.createKnexTable()
825
+ mapTable('searchmerge_products', 'products')
826
+ mapTable('test_products', 'products')
827
827
 
828
828
  // Reviews resource with computed fields
829
829
  await api.addResource('reviews', {
@@ -841,8 +841,8 @@ export async function createComputedFieldsApi(knex, pluginOptions = {}) {
841
841
  computed: true,
842
842
  dependencies: ['helpful_votes', 'total_votes'],
843
843
  compute: ({ attributes }) => {
844
- if (attributes.total_votes === 0) return null;
845
- return Number(((attributes.helpful_votes / attributes.total_votes) * 100).toFixed(0));
844
+ if (attributes.total_votes === 0) return null
845
+ return Number(((attributes.helpful_votes / attributes.total_votes) * 100).toFixed(0))
846
846
  }
847
847
  },
848
848
  is_helpful: {
@@ -850,49 +850,49 @@ export async function createComputedFieldsApi(knex, pluginOptions = {}) {
850
850
  computed: true,
851
851
  dependencies: ['helpful_votes', 'total_votes', 'spam_score'],
852
852
  compute: ({ attributes }) => {
853
- if (attributes.total_votes < 10) return null;
854
- const helpfulnessScore = (attributes.helpful_votes / attributes.total_votes) * 100;
855
- return helpfulnessScore > 70 && attributes.spam_score < 0.5;
853
+ if (attributes.total_votes < 10) return null
854
+ const helpfulnessScore = (attributes.helpful_votes / attributes.total_votes) * 100
855
+ return helpfulnessScore > 70 && attributes.spam_score < 0.5
856
856
  }
857
857
  }
858
858
  },
859
859
  tableName: 'test_reviews'
860
- });
861
- await api.resources.reviews.createKnexTable();
862
- mapTable('test_reviews', 'reviews');
863
- });
860
+ })
861
+ await api.resources.reviews.createKnexTable()
862
+ mapTable('test_reviews', 'reviews')
863
+ })
864
864
 
865
- return api;
865
+ return api
866
866
  }
867
867
 
868
868
  /**
869
869
  * Creates an API with field getters for testing
870
870
  */
871
- export async function createFieldGettersApi(knex, pluginOptions = {}) {
871
+ export async function createFieldGettersApi (knex, pluginOptions = {}) {
872
872
  const api = new Api({
873
873
  name: 'field-getters-test-api',
874
874
  log: { level: process.env.LOG_LEVEL || 'info' }
875
- });
875
+ })
876
876
 
877
- const tenantId = storageMode.isAnyApi() ? 'field_getters_tenant' : storageMode.defaultTenant;
878
- const previousTenant = storageMode.currentTenant;
877
+ const tenantId = storageMode.isAnyApi() ? 'field_getters_tenant' : storageMode.defaultTenant
878
+ const previousTenant = storageMode.currentTenant
879
879
  if (storageMode.isAnyApi()) {
880
- storageMode.setCurrentTenant(tenantId);
880
+ storageMode.setCurrentTenant(tenantId)
881
881
  }
882
882
 
883
883
  await api.use(RestApiPlugin, {
884
884
  simplifiedApi: true,
885
885
  simplifiedTransport: true
886
- });
887
-
888
- await useStoragePlugin(api, knex, { tenantId });
889
- await resetAnyApiTables(knex);
886
+ })
887
+
888
+ await useStoragePlugin(api, knex, { tenantId })
889
+ await resetAnyApiTables(knex)
890
890
 
891
891
  // Users resource with basic getters
892
892
  await api.addResource('users', {
893
893
  schema: {
894
894
  id: { type: 'id' },
895
- email: {
895
+ email: {
896
896
  type: 'string',
897
897
  nullable: true,
898
898
  getter: (value) => value?.toLowerCase().trim()
@@ -906,24 +906,24 @@ export async function createFieldGettersApi(knex, pluginOptions = {}) {
906
906
  type: 'string',
907
907
  nullable: true,
908
908
  getter: (value) => {
909
- if (!value) return null;
909
+ if (!value) return null
910
910
  // Remove all non-digits and format
911
- const digits = value.replace(/\D/g, '');
911
+ const digits = value.replace(/\D/g, '')
912
912
  if (digits.length === 10) {
913
- return `(${digits.slice(0,3)}) ${digits.slice(3,6)}-${digits.slice(6)}`;
913
+ return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`
914
914
  }
915
- return value;
915
+ return value
916
916
  }
917
917
  },
918
918
  metadata_json: {
919
919
  type: 'string',
920
920
  nullable: true,
921
921
  getter: (value) => {
922
- if (!value) return {};
922
+ if (!value) return {}
923
923
  try {
924
- return JSON.parse(value);
924
+ return JSON.parse(value)
925
925
  } catch {
926
- return {};
926
+ return {}
927
927
  }
928
928
  }
929
929
  },
@@ -931,16 +931,16 @@ export async function createFieldGettersApi(knex, pluginOptions = {}) {
931
931
  type: 'string',
932
932
  nullable: true,
933
933
  getter: (value) => {
934
- if (!value) return [];
935
- return value.split(',').map(t => t.trim()).filter(t => t);
934
+ if (!value) return []
935
+ return value.split(',').map(t => t.trim()).filter(t => t)
936
936
  }
937
937
  }
938
938
  },
939
939
  tableName: 'getter_users'
940
- });
941
- await api.resources.users.createKnexTable();
942
- mapTable('searchmerge_users', 'users');
943
- mapTable('getter_users', 'users');
940
+ })
941
+ await api.resources.users.createKnexTable()
942
+ mapTable('searchmerge_users', 'users')
943
+ mapTable('getter_users', 'users')
944
944
 
945
945
  // Products resource with getters and computed fields
946
946
  await api.addResource('products', {
@@ -954,8 +954,8 @@ export async function createFieldGettersApi(knex, pluginOptions = {}) {
954
954
  type: 'string',
955
955
  getter: (value) => {
956
956
  // Truncate long descriptions
957
- if (!value) return '';
958
- return value.length > 50 ? value.substring(0, 47) + '...' : value;
957
+ if (!value) return ''
958
+ return value.length > 50 ? value.substring(0, 47) + '...' : value
959
959
  }
960
960
  },
961
961
  price_str: {
@@ -972,7 +972,7 @@ export async function createFieldGettersApi(knex, pluginOptions = {}) {
972
972
  dependencies: ['price_str', 'tax_rate_str'],
973
973
  compute: ({ attributes }) => {
974
974
  // Should see numbers from getters, not strings
975
- return attributes.price_str * (1 + attributes.tax_rate_str);
975
+ return attributes.price_str * (1 + attributes.tax_rate_str)
976
976
  }
977
977
  }
978
978
  },
@@ -980,9 +980,9 @@ export async function createFieldGettersApi(knex, pluginOptions = {}) {
980
980
  reviews: { type: 'hasMany', target: 'reviews', foreignKey: 'product_id' }
981
981
  },
982
982
  tableName: 'getter_products'
983
- });
984
- await api.resources.products.createKnexTable();
985
- mapTable('getter_products', 'products');
983
+ })
984
+ await api.resources.products.createKnexTable()
985
+ mapTable('getter_products', 'products')
986
986
 
987
987
  // Reviews resource with getters
988
988
  await api.addResource('reviews', {
@@ -996,9 +996,9 @@ export async function createFieldGettersApi(knex, pluginOptions = {}) {
996
996
  rating: { type: 'number' }
997
997
  },
998
998
  tableName: 'getter_reviews'
999
- });
1000
- await api.resources.reviews.createKnexTable();
1001
- mapTable('getter_reviews', 'reviews');
999
+ })
1000
+ await api.resources.reviews.createKnexTable()
1001
+ mapTable('getter_reviews', 'reviews')
1002
1002
 
1003
1003
  // Resource with getter dependencies
1004
1004
  await api.addResource('formatted_data', {
@@ -1011,28 +1011,28 @@ export async function createFieldGettersApi(knex, pluginOptions = {}) {
1011
1011
  },
1012
1012
  step2: {
1013
1013
  type: 'string',
1014
- nullable: true,
1014
+ nullable: true,
1015
1015
  getter: (value, { attributes }) => {
1016
- if (!value) return value; // Return null/undefined as-is
1016
+ if (!value) return value // Return null/undefined as-is
1017
1017
  // Depends on step1 being trimmed first
1018
- return `${value} [step1: ${attributes.step1}]`;
1018
+ return `${value} [step1: ${attributes.step1}]`
1019
1019
  },
1020
1020
  runGetterAfter: ['step1']
1021
1021
  },
1022
1022
  step3: {
1023
1023
  type: 'string',
1024
1024
  getter: (value, { attributes }) => {
1025
- if (!value) return value; // Return null/undefined as-is
1025
+ if (!value) return value // Return null/undefined as-is
1026
1026
  // Depends on step2
1027
- return `${value} [step2: ${attributes.step2}]`;
1027
+ return `${value} [step2: ${attributes.step2}]`
1028
1028
  },
1029
1029
  runGetterAfter: ['step2']
1030
1030
  }
1031
1031
  },
1032
1032
  tableName: 'getter_formatted'
1033
- });
1034
- await api.resources.formatted_data.createKnexTable();
1035
- mapTable('getter_formatted', 'formatted_data');
1033
+ })
1034
+ await api.resources.formatted_data.createKnexTable()
1035
+ mapTable('getter_formatted', 'formatted_data')
1036
1036
 
1037
1037
  // Async getters example
1038
1038
  await api.addResource('encrypted_data', {
@@ -1042,48 +1042,48 @@ export async function createFieldGettersApi(knex, pluginOptions = {}) {
1042
1042
  type: 'string',
1043
1043
  nullable: true,
1044
1044
  getter: async (value) => {
1045
- if (!value) return null;
1045
+ if (!value) return null
1046
1046
  // Simulate async decryption
1047
- await new Promise(resolve => setTimeout(resolve, 5));
1048
- return Buffer.from(value, 'base64').toString();
1047
+ await new Promise(resolve => setTimeout(resolve, 5))
1048
+ return Buffer.from(value, 'base64').toString()
1049
1049
  }
1050
1050
  },
1051
1051
  data: {
1052
1052
  type: 'string',
1053
1053
  nullable: true,
1054
1054
  getter: async (value, context) => {
1055
- if (!value) return null;
1056
- await new Promise(resolve => setTimeout(resolve, 5));
1057
- const decrypted = Buffer.from(value, 'base64').toString();
1058
- return `[${context.scopeName}] ${decrypted}`;
1055
+ if (!value) return null
1056
+ await new Promise(resolve => setTimeout(resolve, 5))
1057
+ const decrypted = Buffer.from(value, 'base64').toString()
1058
+ return `[${context.scopeName}] ${decrypted}`
1059
1059
  }
1060
1060
  }
1061
1061
  },
1062
1062
  tableName: 'getter_encrypted'
1063
- });
1064
- await api.resources.encrypted_data.createKnexTable();
1065
- mapTable('getter_encrypted', 'encrypted_data');
1063
+ })
1064
+ await api.resources.encrypted_data.createKnexTable()
1065
+ mapTable('getter_encrypted', 'encrypted_data')
1066
1066
 
1067
1067
  if (storageMode.isAnyApi()) {
1068
- storageMode.setCurrentTenant(previousTenant);
1068
+ storageMode.setCurrentTenant(previousTenant)
1069
1069
  }
1070
1070
 
1071
- return api;
1071
+ return api
1072
1072
  }
1073
1073
 
1074
1074
  /**
1075
1075
  * Creates an API with field setters for testing
1076
1076
  */
1077
- export async function createFieldSettersApi(knex, pluginOptions = {}) {
1077
+ export async function createFieldSettersApi (knex, pluginOptions = {}) {
1078
1078
  const api = new Api({
1079
1079
  name: 'field-setters-test-api',
1080
1080
  log: { level: process.env.LOG_LEVEL || 'info' }
1081
- });
1081
+ })
1082
1082
 
1083
- const tenantId = storageMode.isAnyApi() ? 'field_setters_tenant' : storageMode.defaultTenant;
1084
- const previousTenant = storageMode.currentTenant;
1083
+ const tenantId = storageMode.isAnyApi() ? 'field_setters_tenant' : storageMode.defaultTenant
1084
+ const previousTenant = storageMode.currentTenant
1085
1085
  if (storageMode.isAnyApi()) {
1086
- storageMode.setCurrentTenant(tenantId);
1086
+ storageMode.setCurrentTenant(tenantId)
1087
1087
  }
1088
1088
 
1089
1089
  await api.use(RestApiPlugin, {
@@ -1094,16 +1094,16 @@ export async function createFieldSettersApi(knex, pluginOptions = {}) {
1094
1094
  put: true,
1095
1095
  patch: true
1096
1096
  }
1097
- });
1098
-
1099
- await useStoragePlugin(api, knex, { tenantId });
1100
- await resetAnyApiTables(knex);
1097
+ })
1098
+
1099
+ await useStoragePlugin(api, knex, { tenantId })
1100
+ await resetAnyApiTables(knex)
1101
1101
 
1102
1102
  // Users resource with basic setters
1103
1103
  await api.addResource('users', {
1104
1104
  schema: {
1105
1105
  id: { type: 'id' },
1106
- email: {
1106
+ email: {
1107
1107
  type: 'string',
1108
1108
  required: true,
1109
1109
  setter: (value) => value?.toLowerCase().trim()
@@ -1118,13 +1118,13 @@ export async function createFieldSettersApi(knex, pluginOptions = {}) {
1118
1118
  setter: (value) => value // Pass through
1119
1119
  },
1120
1120
  preferences: {
1121
- type: 'string', // Accepts string, stored as string
1121
+ type: 'string', // Accepts string, stored as string
1122
1122
  setter: (value) => value // Pass through
1123
1123
  }
1124
1124
  },
1125
1125
  tableName: 'setter_users'
1126
- });
1127
- await api.resources.users.createKnexTable();
1126
+ })
1127
+ await api.resources.users.createKnexTable()
1128
1128
 
1129
1129
  // Products with type conversion setters
1130
1130
  await api.addResource('products', {
@@ -1137,7 +1137,7 @@ export async function createFieldSettersApi(knex, pluginOptions = {}) {
1137
1137
  min: 0,
1138
1138
  setter: (value) => {
1139
1139
  // Convert to cents and round
1140
- return Math.round(value * 100);
1140
+ return Math.round(value * 100)
1141
1141
  }
1142
1142
  },
1143
1143
  discount_percent: {
@@ -1152,8 +1152,8 @@ export async function createFieldSettersApi(knex, pluginOptions = {}) {
1152
1152
  }
1153
1153
  },
1154
1154
  tableName: 'setter_products'
1155
- });
1156
- await api.resources.products.createKnexTable();
1155
+ })
1156
+ await api.resources.products.createKnexTable()
1157
1157
 
1158
1158
  // Secure data with async setters
1159
1159
  await api.addResource('secure_data', {
@@ -1165,44 +1165,44 @@ export async function createFieldSettersApi(knex, pluginOptions = {}) {
1165
1165
  min: 8,
1166
1166
  setter: async (value) => {
1167
1167
  // Simulate async password hashing
1168
- await new Promise(resolve => setTimeout(resolve, 5));
1169
- return `hashed:${value}`;
1168
+ await new Promise(resolve => setTimeout(resolve, 5))
1169
+ return `hashed:${value}`
1170
1170
  }
1171
1171
  },
1172
1172
  api_key: {
1173
1173
  type: 'string',
1174
1174
  setter: async (value) => {
1175
- if (!value) return null;
1175
+ if (!value) return null
1176
1176
  // Simulate encryption
1177
- await new Promise(resolve => setTimeout(resolve, 5));
1178
- return Buffer.from(value).toString('base64');
1177
+ await new Promise(resolve => setTimeout(resolve, 5))
1178
+ return Buffer.from(value).toString('base64')
1179
1179
  }
1180
1180
  },
1181
1181
  data: {
1182
1182
  type: 'string',
1183
1183
  setter: async (value) => {
1184
- if (!value) return null;
1185
- return Buffer.from(value).toString('base64');
1184
+ if (!value) return null
1185
+ return Buffer.from(value).toString('base64')
1186
1186
  }
1187
1187
  }
1188
1188
  },
1189
1189
  tableName: 'setter_secure'
1190
- });
1191
- await api.resources.secure_data.createKnexTable();
1190
+ })
1191
+ await api.resources.secure_data.createKnexTable()
1192
1192
 
1193
1193
  // Resource with setter dependencies
1194
1194
  await api.addResource('computed_data', {
1195
1195
  schema: {
1196
1196
  id: { type: 'id' },
1197
- base_value: {
1197
+ base_value: {
1198
1198
  type: 'number',
1199
1199
  setter: (value) => value || 0
1200
1200
  },
1201
- multiplier: {
1201
+ multiplier: {
1202
1202
  type: 'number',
1203
1203
  setter: (value) => value || 1
1204
1204
  },
1205
- adjustment: {
1205
+ adjustment: {
1206
1206
  type: 'number',
1207
1207
  setter: (value) => value || 0
1208
1208
  },
@@ -1210,7 +1210,7 @@ export async function createFieldSettersApi(knex, pluginOptions = {}) {
1210
1210
  type: 'number',
1211
1211
  setter: (value, { attributes }) => {
1212
1212
  // Calculate based on other fields
1213
- return (attributes.base_value || 0) * (attributes.multiplier || 1);
1213
+ return (attributes.base_value || 0) * (attributes.multiplier || 1)
1214
1214
  },
1215
1215
  runSetterAfter: ['base_value', 'multiplier']
1216
1216
  },
@@ -1218,14 +1218,14 @@ export async function createFieldSettersApi(knex, pluginOptions = {}) {
1218
1218
  type: 'number',
1219
1219
  setter: (value, { attributes }) => {
1220
1220
  // Depends on calculated_value
1221
- return (attributes.calculated_value || 0) + (attributes.adjustment || 0);
1221
+ return (attributes.calculated_value || 0) + (attributes.adjustment || 0)
1222
1222
  },
1223
1223
  runSetterAfter: ['calculated_value', 'adjustment']
1224
1224
  }
1225
1225
  },
1226
1226
  tableName: 'setter_computed'
1227
- });
1228
- await api.resources.computed_data.createKnexTable();
1227
+ })
1228
+ await api.resources.computed_data.createKnexTable()
1229
1229
 
1230
1230
  // Nullable fields handling
1231
1231
  await api.addResource('nullable_data', {
@@ -1251,8 +1251,8 @@ export async function createFieldSettersApi(knex, pluginOptions = {}) {
1251
1251
  }
1252
1252
  },
1253
1253
  tableName: 'setter_nullable'
1254
- });
1255
- await api.resources.nullable_data.createKnexTable();
1254
+ })
1255
+ await api.resources.nullable_data.createKnexTable()
1256
1256
 
1257
1257
  // Validated data
1258
1258
  await api.addResource('validated_data', {
@@ -1278,27 +1278,27 @@ export async function createFieldSettersApi(knex, pluginOptions = {}) {
1278
1278
  }
1279
1279
  },
1280
1280
  tableName: 'setter_validated'
1281
- });
1282
- await api.resources.validated_data.createKnexTable();
1281
+ })
1282
+ await api.resources.validated_data.createKnexTable()
1283
1283
 
1284
1284
  if (storageMode.isAnyApi()) {
1285
- storageMode.setCurrentTenant(previousTenant);
1285
+ storageMode.setCurrentTenant(previousTenant)
1286
1286
  }
1287
1287
 
1288
- return api;
1288
+ return api
1289
1289
  }
1290
1290
 
1291
- export async function createMultiHomeApi(knex, pluginOptions = {}) {
1292
- const { MultiHomePlugin } = await import('../../plugins/core/rest-api-multihome-plugin.js');
1293
-
1291
+ export async function createMultiHomeApi (knex, pluginOptions = {}) {
1292
+ const { MultiHomePlugin } = await import('../../plugins/core/rest-api-multihome-plugin.js')
1293
+
1294
1294
  const api = new Api({
1295
1295
  name: 'multihome-test-api',
1296
- });
1296
+ })
1297
1297
 
1298
- const tenantId = storageMode.isAnyApi() ? 'multihome_tenant' : storageMode.defaultTenant;
1299
- const previousTenant = storageMode.currentTenant;
1298
+ const tenantId = storageMode.isAnyApi() ? 'multihome_tenant' : storageMode.defaultTenant
1299
+ const previousTenant = storageMode.currentTenant
1300
1300
  if (storageMode.isAnyApi()) {
1301
- storageMode.setCurrentTenant(tenantId);
1301
+ storageMode.setCurrentTenant(tenantId)
1302
1302
  }
1303
1303
 
1304
1304
  await api.use(RestApiPlugin, {
@@ -1315,16 +1315,16 @@ export async function createMultiHomeApi(knex, pluginOptions = {}) {
1315
1315
  patch: 'minimal'
1316
1316
  },
1317
1317
  sortableFields: ['id', 'title', 'name', 'tenant_id']
1318
- });
1319
-
1320
- await useStoragePlugin(api, knex, { tenantId });
1321
- await resetAnyApiTables(knex);
1322
-
1318
+ })
1319
+
1320
+ await useStoragePlugin(api, knex, { tenantId })
1321
+ await resetAnyApiTables(knex)
1322
+
1323
1323
  // Add Express plugin if requested for transport testing
1324
1324
  if (pluginOptions.includeExpress) {
1325
- await api.use(ExpressPlugin, pluginOptions.express || {});
1325
+ await api.use(ExpressPlugin, pluginOptions.express || {})
1326
1326
  }
1327
-
1327
+
1328
1328
  // Add MultiHome plugin with configuration
1329
1329
  await api.use(MultiHomePlugin, {
1330
1330
  field: pluginOptions.field || 'tenant_id',
@@ -1333,9 +1333,9 @@ export async function createMultiHomeApi(knex, pluginOptions = {}) {
1333
1333
  allowMissing: pluginOptions.allowMissing || false,
1334
1334
  extractor: pluginOptions.extractor || ((request) => {
1335
1335
  // Default to header extraction for tests
1336
- return request.headers?.['x-tenant-id'] || null;
1336
+ return request.headers?.['x-tenant-id'] || null
1337
1337
  })
1338
- });
1338
+ })
1339
1339
 
1340
1340
  // Tenant-specific resources
1341
1341
  await api.addResource('projects', {
@@ -1350,9 +1350,9 @@ export async function createMultiHomeApi(knex, pluginOptions = {}) {
1350
1350
  tasks: { type: 'hasMany', target: 'tasks', foreignKey: 'project_id' }
1351
1351
  },
1352
1352
  tableName: 'multihome_projects'
1353
- });
1354
- await api.resources.projects.createKnexTable();
1355
- mapTable('multihome_projects', 'projects');
1353
+ })
1354
+ await api.resources.projects.createKnexTable()
1355
+ mapTable('multihome_projects', 'projects')
1356
1356
 
1357
1357
  await api.addResource('tasks', {
1358
1358
  schema: {
@@ -1363,9 +1363,9 @@ export async function createMultiHomeApi(knex, pluginOptions = {}) {
1363
1363
  tenant_id: { type: 'string', required: true }
1364
1364
  },
1365
1365
  tableName: 'multihome_tasks'
1366
- });
1367
- await api.resources.tasks.createKnexTable();
1368
- mapTable('multihome_tasks', 'tasks');
1366
+ })
1367
+ await api.resources.tasks.createKnexTable()
1368
+ mapTable('multihome_tasks', 'tasks')
1369
1369
 
1370
1370
  await api.addResource('users', {
1371
1371
  schema: {
@@ -1376,9 +1376,9 @@ export async function createMultiHomeApi(knex, pluginOptions = {}) {
1376
1376
  tenant_id: { type: 'string', required: true }
1377
1377
  },
1378
1378
  tableName: 'multihome_users'
1379
- });
1380
- await api.resources.users.createKnexTable();
1381
- mapTable('multihome_users', 'users');
1379
+ })
1380
+ await api.resources.users.createKnexTable()
1381
+ mapTable('multihome_users', 'users')
1382
1382
 
1383
1383
  // Global resource (excluded from multihome)
1384
1384
  await api.addResource('system_settings', {
@@ -1388,26 +1388,26 @@ export async function createMultiHomeApi(knex, pluginOptions = {}) {
1388
1388
  value: { type: 'string', required: true }
1389
1389
  },
1390
1390
  tableName: 'multihome_system_settings'
1391
- });
1392
- await api.resources.system_settings.createKnexTable();
1393
- mapTable('multihome_system_settings', 'system_settings');
1391
+ })
1392
+ await api.resources.system_settings.createKnexTable()
1393
+ mapTable('multihome_system_settings', 'system_settings')
1394
1394
 
1395
1395
  if (storageMode.isAnyApi()) {
1396
- storageMode.setCurrentTenant(previousTenant);
1396
+ storageMode.setCurrentTenant(previousTenant)
1397
1397
  }
1398
1398
 
1399
- return api;
1399
+ return api
1400
1400
  }
1401
1401
 
1402
1402
  /**
1403
1403
  * Creates an API with positioning support for testing
1404
1404
  */
1405
- export async function createPositioningApi(knex, pluginOptions = {}) {
1406
- const apiName = pluginOptions.apiName || 'positioning-test-api';
1407
- const tablePrefix = pluginOptions.tablePrefix || 'positioning';
1405
+ export async function createPositioningApi (knex, pluginOptions = {}) {
1406
+ const apiName = pluginOptions.apiName || 'positioning-test-api'
1407
+ const tablePrefix = pluginOptions.tablePrefix || 'positioning'
1408
1408
  const api = new Api({
1409
1409
  name: apiName,
1410
- });
1410
+ })
1411
1411
 
1412
1412
  const restApiOptions = {
1413
1413
  simplifiedApi: true, // Changed to true to allow simplified API calls in tests
@@ -1424,11 +1424,11 @@ export async function createPositioningApi(knex, pluginOptions = {}) {
1424
1424
  },
1425
1425
  sortableFields: ['id', 'title', 'name', 'position', 'sort_order', 'category_id', 'project_id', 'status'],
1426
1426
  ...pluginOptions['rest-api']
1427
- };
1427
+ }
1428
1428
 
1429
- await api.use(RestApiPlugin, restApiOptions);
1430
- await useStoragePlugin(api, knex);
1431
- await resetAnyApiTables(knex);
1429
+ await api.use(RestApiPlugin, restApiOptions)
1430
+ await useStoragePlugin(api, knex)
1431
+ await resetAnyApiTables(knex)
1432
1432
 
1433
1433
  // Categories (for grouping tasks)
1434
1434
  await api.addResource('categories', {
@@ -1437,9 +1437,9 @@ export async function createPositioningApi(knex, pluginOptions = {}) {
1437
1437
  name: { type: 'string', required: true, max: 100 }
1438
1438
  },
1439
1439
  tableName: `${tablePrefix}_categories`
1440
- });
1441
- await api.resources.categories.createKnexTable();
1442
- mapTable(`${tablePrefix}_categories`, 'categories');
1440
+ })
1441
+ await api.resources.categories.createKnexTable()
1442
+ mapTable(`${tablePrefix}_categories`, 'categories')
1443
1443
 
1444
1444
  // Tasks (main positioning test resource)
1445
1445
  await api.addResource('tasks', {
@@ -1456,9 +1456,9 @@ export async function createPositioningApi(knex, pluginOptions = {}) {
1456
1456
  category: { belongsTo: 'categories' }
1457
1457
  },
1458
1458
  tableName: `${tablePrefix}_tasks`
1459
- });
1460
- await api.resources.tasks.createKnexTable();
1461
- mapTable(`${tablePrefix}_tasks`, 'tasks');
1459
+ })
1460
+ await api.resources.tasks.createKnexTable()
1461
+ mapTable(`${tablePrefix}_tasks`, 'tasks')
1462
1462
 
1463
1463
  // Projects (for multi-filter testing)
1464
1464
  await api.addResource('projects', {
@@ -1467,9 +1467,9 @@ export async function createPositioningApi(knex, pluginOptions = {}) {
1467
1467
  name: { type: 'string', required: true, max: 100 }
1468
1468
  },
1469
1469
  tableName: `${tablePrefix}_projects`
1470
- });
1471
- await api.resources.projects.createKnexTable();
1472
- mapTable(`${tablePrefix}_projects`, 'projects');
1470
+ })
1471
+ await api.resources.projects.createKnexTable()
1472
+ mapTable(`${tablePrefix}_projects`, 'projects')
1473
1473
 
1474
1474
  // Items (flexible resource for various positioning tests)
1475
1475
  await api.addResource('items', {
@@ -1487,27 +1487,27 @@ export async function createPositioningApi(knex, pluginOptions = {}) {
1487
1487
  project: { belongsTo: 'projects' }
1488
1488
  },
1489
1489
  tableName: `${tablePrefix}_items`
1490
- });
1491
- await api.resources.items.createKnexTable();
1492
- mapTable(`${tablePrefix}_items`, 'items');
1490
+ })
1491
+ await api.resources.items.createKnexTable()
1492
+ mapTable(`${tablePrefix}_items`, 'items')
1493
1493
 
1494
- return api;
1494
+ return api
1495
1495
  }
1496
1496
 
1497
1497
  /**
1498
1498
  * Creates an API with custom idProperty for all resources to test idProperty functionality
1499
1499
  * Uses 'custom_id_' prefix for all tables to avoid conflicts
1500
1500
  */
1501
- export async function createCustomIdPropertyApi(knex, pluginOptions = {}) {
1502
- const apiName = pluginOptions.apiName || `custom-id-test-api`;
1503
- const tablePrefix = pluginOptions.tablePrefix || 'custom_id';
1501
+ export async function createCustomIdPropertyApi (knex, pluginOptions = {}) {
1502
+ const apiName = pluginOptions.apiName || 'custom-id-test-api'
1503
+ const tablePrefix = pluginOptions.tablePrefix || 'custom_id'
1504
1504
  const api = new Api({
1505
1505
  name: apiName,
1506
1506
  log: { level: process.env.LOG_LEVEL || 'info' }
1507
- });
1507
+ })
1508
1508
 
1509
1509
  if (storageMode.isAnyApi()) {
1510
- storageMode.clearRegistry();
1510
+ storageMode.clearRegistry()
1511
1511
  }
1512
1512
 
1513
1513
  const restApiOptions = {
@@ -1525,11 +1525,11 @@ export async function createCustomIdPropertyApi(knex, pluginOptions = {}) {
1525
1525
  },
1526
1526
  sortableFields: ['country_id', 'publisher_id', 'author_id', 'book_id', 'title', 'name', 'code'],
1527
1527
  ...pluginOptions['rest-api'] // Merge any custom options for rest-api plugin
1528
- };
1528
+ }
1529
1529
 
1530
- await api.use(RestApiPlugin, restApiOptions);
1531
- await useStoragePlugin(api, knex);
1532
- await resetAnyApiTables(knex);
1530
+ await api.use(RestApiPlugin, restApiOptions)
1531
+ await useStoragePlugin(api, knex)
1532
+ await resetAnyApiTables(knex)
1533
1533
 
1534
1534
  // Countries table with custom idProperty
1535
1535
  await api.addResource('countries', {
@@ -1544,9 +1544,9 @@ export async function createCustomIdPropertyApi(knex, pluginOptions = {}) {
1544
1544
  },
1545
1545
  tableName: `${tablePrefix}_countries`,
1546
1546
  idProperty: 'country_id' // Custom ID property
1547
- });
1548
- await api.resources.countries.createKnexTable();
1549
- mapTable(`${tablePrefix}_countries`, 'countries');
1547
+ })
1548
+ await api.resources.countries.createKnexTable()
1549
+ mapTable(`${tablePrefix}_countries`, 'countries')
1550
1550
 
1551
1551
  // Publishers table with custom idProperty
1552
1552
  await api.addResource('publishers', {
@@ -1556,17 +1556,17 @@ export async function createCustomIdPropertyApi(knex, pluginOptions = {}) {
1556
1556
  },
1557
1557
  relationships: {
1558
1558
  books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' },
1559
- reviews: {
1560
- type: 'hasMany',
1561
- target: 'reviews',
1559
+ reviews: {
1560
+ type: 'hasMany',
1561
+ target: 'reviews',
1562
1562
  via: 'reviewable'
1563
1563
  }
1564
1564
  },
1565
1565
  tableName: `${tablePrefix}_publishers`,
1566
1566
  idProperty: 'publisher_id'
1567
- });
1568
- await api.resources.publishers.createKnexTable();
1569
- mapTable(`${tablePrefix}_publishers`, 'publishers');
1567
+ })
1568
+ await api.resources.publishers.createKnexTable()
1569
+ mapTable(`${tablePrefix}_publishers`, 'publishers')
1570
1570
 
1571
1571
  // Authors table with custom idProperty
1572
1572
  await api.addResource('authors', {
@@ -1577,17 +1577,17 @@ export async function createCustomIdPropertyApi(knex, pluginOptions = {}) {
1577
1577
  },
1578
1578
  relationships: {
1579
1579
  books: { type: 'manyToMany', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' },
1580
- reviews: {
1581
- type: 'hasMany',
1582
- target: 'reviews',
1580
+ reviews: {
1581
+ type: 'hasMany',
1582
+ target: 'reviews',
1583
1583
  via: 'reviewable'
1584
1584
  }
1585
1585
  },
1586
1586
  tableName: `${tablePrefix}_authors`,
1587
1587
  idProperty: 'author_id'
1588
- });
1589
- await api.resources.authors.createKnexTable();
1590
- mapTable(`${tablePrefix}_authors`, 'authors');
1588
+ })
1589
+ await api.resources.authors.createKnexTable()
1590
+ mapTable(`${tablePrefix}_authors`, 'authors')
1591
1591
 
1592
1592
  // Books table with custom idProperty
1593
1593
  await api.addResource('books', {
@@ -1601,17 +1601,17 @@ export async function createCustomIdPropertyApi(knex, pluginOptions = {}) {
1601
1601
  },
1602
1602
  relationships: {
1603
1603
  authors: { type: 'manyToMany', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' },
1604
- reviews: {
1605
- type: 'hasMany',
1606
- target: 'reviews',
1604
+ reviews: {
1605
+ type: 'hasMany',
1606
+ target: 'reviews',
1607
1607
  via: 'reviewable'
1608
1608
  }
1609
1609
  },
1610
1610
  tableName: `${tablePrefix}_books`,
1611
1611
  idProperty: 'book_id'
1612
- });
1613
- await api.resources.books.createKnexTable();
1614
- mapTable(`${tablePrefix}_books`, 'books');
1612
+ })
1613
+ await api.resources.books.createKnexTable()
1614
+ mapTable(`${tablePrefix}_books`, 'books')
1615
1615
 
1616
1616
  // Book-Authors pivot table with custom idProperty
1617
1617
  await api.addResource('book_authors', {
@@ -1623,9 +1623,9 @@ export async function createCustomIdPropertyApi(knex, pluginOptions = {}) {
1623
1623
  },
1624
1624
  tableName: `${tablePrefix}_book_authors`,
1625
1625
  idProperty: 'book_author_id'
1626
- });
1627
- await api.resources.book_authors.createKnexTable();
1628
- mapTable(`${tablePrefix}_book_authors`, 'book_authors');
1626
+ })
1627
+ await api.resources.book_authors.createKnexTable()
1628
+ mapTable(`${tablePrefix}_book_authors`, 'book_authors')
1629
1629
 
1630
1630
  // Polymorphic reviews with custom idProperty
1631
1631
  await api.addResource('reviews', {
@@ -1650,36 +1650,36 @@ export async function createCustomIdPropertyApi(knex, pluginOptions = {}) {
1650
1650
  relationships: {},
1651
1651
  tableName: `${tablePrefix}_reviews`,
1652
1652
  idProperty: 'review_id'
1653
- });
1654
- await api.resources.reviews.createKnexTable();
1655
- mapTable(`${tablePrefix}_reviews`, 'reviews');
1653
+ })
1654
+ await api.resources.reviews.createKnexTable()
1655
+ mapTable(`${tablePrefix}_reviews`, 'reviews')
1656
1656
 
1657
1657
  if (storageMode.isAnyApi()) {
1658
- const tenant = storageMode.defaultTenant || 'default';
1659
- const booksDescriptor = await api.anyapi.registry.getDescriptor(tenant, 'books');
1660
- const authorsDescriptor = await api.anyapi.registry.getDescriptor(tenant, 'authors');
1661
- const relationshipKey = booksDescriptor?.manyToMany?.authors?.relationship;
1662
- const inverseRelationshipKey = authorsDescriptor?.manyToMany?.books?.relationship;
1658
+ const tenant = storageMode.defaultTenant || 'default'
1659
+ const booksDescriptor = await api.anyapi.registry.getDescriptor(tenant, 'books')
1660
+ const authorsDescriptor = await api.anyapi.registry.getDescriptor(tenant, 'authors')
1661
+ const relationshipKey = booksDescriptor?.manyToMany?.authors?.relationship
1662
+ const inverseRelationshipKey = authorsDescriptor?.manyToMany?.books?.relationship
1663
1663
  storageMode.registerLink(
1664
1664
  `${tablePrefix}_book_authors`,
1665
1665
  'books',
1666
1666
  'authors',
1667
1667
  relationshipKey,
1668
- inverseRelationshipKey,
1669
- );
1668
+ inverseRelationshipKey
1669
+ )
1670
1670
  }
1671
1671
 
1672
- return api;
1672
+ return api
1673
1673
  }
1674
1674
 
1675
1675
  /**
1676
1676
  * Creates an API configuration for multi-field cursor pagination testing
1677
1677
  * Uses 'cursor_' prefix for all tables to avoid conflicts
1678
1678
  */
1679
- export async function createCursorPaginationApi(knex) {
1679
+ export async function createCursorPaginationApi (knex) {
1680
1680
  const api = new Api({
1681
1681
  name: 'cursor-pagination-test-api',
1682
- });
1682
+ })
1683
1683
 
1684
1684
  const restApiOptions = {
1685
1685
  simplified: false,
@@ -1691,12 +1691,12 @@ export async function createCursorPaginationApi(knex) {
1691
1691
  sortableFields: ['id', 'name', 'category', 'brand', 'price', 'sku', 'created_at', 'code', 'status', 'type'],
1692
1692
  queryDefaultLimit: 20,
1693
1693
  queryMaxLimit: 100,
1694
- returnBasePath: 'https://api.example.com'
1695
- };
1696
-
1697
- await api.use(RestApiPlugin, restApiOptions);
1698
- await useStoragePlugin(api, knex);
1699
- await resetAnyApiTables(knex);
1694
+ returnBasePath: 'https://api.example.com'
1695
+ }
1696
+
1697
+ await api.use(RestApiPlugin, restApiOptions)
1698
+ await useStoragePlugin(api, knex)
1699
+ await resetAnyApiTables(knex)
1700
1700
 
1701
1701
  // Create products table with fields suitable for multi-field sorting
1702
1702
  await api.addResource('products', {
@@ -1711,8 +1711,8 @@ export async function createCursorPaginationApi(knex) {
1711
1711
  created_at: { type: 'dateTime', default: 'now()' }
1712
1712
  },
1713
1713
  tableName: 'cursor_products'
1714
- });
1715
- await api.resources.products.createKnexTable();
1714
+ })
1715
+ await api.resources.products.createKnexTable()
1716
1716
 
1717
1717
  // Create items table with custom ID property
1718
1718
  await api.addResource('items', {
@@ -1725,21 +1725,21 @@ export async function createCursorPaginationApi(knex) {
1725
1725
  },
1726
1726
  idProperty: 'item_id',
1727
1727
  tableName: 'cursor_items'
1728
- });
1729
- await api.resources.items.createKnexTable();
1728
+ })
1729
+ await api.resources.items.createKnexTable()
1730
1730
 
1731
- return api;
1731
+ return api
1732
1732
  }
1733
1733
 
1734
1734
  /**
1735
1735
  * Creates an API configuration for testing virtual fields validation
1736
1736
  */
1737
- export async function createVirtualFieldsApi(knex, pluginOptions = {}) {
1738
- const apiName = pluginOptions.apiName || 'virtual-fields-test-api';
1737
+ export async function createVirtualFieldsApi (knex, pluginOptions = {}) {
1738
+ const apiName = pluginOptions.apiName || 'virtual-fields-test-api'
1739
1739
  const api = new Api({
1740
1740
  name: apiName,
1741
1741
  log: { level: process.env.LOG_LEVEL || 'info' }
1742
- });
1742
+ })
1743
1743
 
1744
1744
  const restApiOptions = {
1745
1745
  simplifiedApi: false,
@@ -1755,11 +1755,11 @@ export async function createVirtualFieldsApi(knex, pluginOptions = {}) {
1755
1755
  patch: 'minimal'
1756
1756
  },
1757
1757
  ...pluginOptions['rest-api']
1758
- };
1758
+ }
1759
1759
 
1760
- await api.use(RestApiPlugin, restApiOptions);
1761
- await useStoragePlugin(api, knex);
1762
- await resetAnyApiTables(knex);
1760
+ await api.use(RestApiPlugin, restApiOptions)
1761
+ await useStoragePlugin(api, knex)
1762
+ await resetAnyApiTables(knex)
1763
1763
 
1764
1764
  // Add a test resource with virtual fields
1765
1765
  await api.addResource('users', {
@@ -1768,7 +1768,7 @@ export async function createVirtualFieldsApi(knex, pluginOptions = {}) {
1768
1768
  username: { type: 'string', required: true },
1769
1769
  email: { type: 'string', required: true },
1770
1770
  age: { type: 'number' },
1771
-
1771
+
1772
1772
  // Virtual fields with validation rules
1773
1773
  password: { type: 'string', virtual: true, minLength: 8 },
1774
1774
  passwordConfirmation: { type: 'string', virtual: true, minLength: 8 },
@@ -1778,21 +1778,21 @@ export async function createVirtualFieldsApi(knex, pluginOptions = {}) {
1778
1778
  metadata: { type: 'object', virtual: true }
1779
1779
  },
1780
1780
  tableName: 'virtual_users'
1781
- });
1782
-
1783
- await api.resources.users.createKnexTable();
1781
+ })
1782
+
1783
+ await api.resources.users.createKnexTable()
1784
1784
 
1785
- return api;
1785
+ return api
1786
1786
  }
1787
1787
 
1788
1788
  /**
1789
1789
  * Creates an API configuration for testing searchSchema merge behavior
1790
1790
  */
1791
- export async function createSearchSchemaMergeApi(knex, pluginOptions = {}) {
1791
+ export async function createSearchSchemaMergeApi (knex, pluginOptions = {}) {
1792
1792
  const api = new Api({
1793
1793
  name: 'searchschema-merge-test-api',
1794
1794
  log: { level: process.env.LOG_LEVEL || 'info' }
1795
- });
1795
+ })
1796
1796
 
1797
1797
  await api.use(RestApiPlugin, {
1798
1798
  simplifiedApi: false,
@@ -1803,10 +1803,10 @@ export async function createSearchSchemaMergeApi(knex, pluginOptions = {}) {
1803
1803
  patch: false
1804
1804
  },
1805
1805
  ...pluginOptions['rest-api']
1806
- });
1807
-
1808
- await useStoragePlugin(api, knex);
1809
- await resetAnyApiTables(knex);
1806
+ })
1807
+
1808
+ await useStoragePlugin(api, knex)
1809
+ await resetAnyApiTables(knex)
1810
1810
 
1811
1811
  // Test resource with both search:true and searchSchema
1812
1812
  await api.addResource('products', {
@@ -1821,7 +1821,7 @@ export async function createSearchSchemaMergeApi(knex, pluginOptions = {}) {
1821
1821
  searchSchema: {
1822
1822
  // This should override the price field from search:true
1823
1823
  // 'between' operator requires array type for validation [min, max]
1824
- price: {
1824
+ price: {
1825
1825
  type: 'array',
1826
1826
  filterOperator: 'between'
1827
1827
  },
@@ -1839,8 +1839,8 @@ export async function createSearchSchemaMergeApi(knex, pluginOptions = {}) {
1839
1839
  }
1840
1840
  },
1841
1841
  tableName: 'searchmerge_products'
1842
- });
1843
- await api.resources.products.createKnexTable();
1842
+ })
1843
+ await api.resources.products.createKnexTable()
1844
1844
 
1845
1845
  // Test resource with only search:true fields
1846
1846
  await api.addResource('users', {
@@ -1851,8 +1851,8 @@ export async function createSearchSchemaMergeApi(knex, pluginOptions = {}) {
1851
1851
  bio: { type: 'string', search: { filterOperator: 'like' } }
1852
1852
  },
1853
1853
  tableName: 'searchmerge_users'
1854
- });
1855
- await api.resources.users.createKnexTable();
1854
+ })
1855
+ await api.resources.users.createKnexTable()
1856
1856
 
1857
1857
  // Test resource with only explicit searchSchema
1858
1858
  await api.addResource('orders', {
@@ -1866,9 +1866,9 @@ export async function createSearchSchemaMergeApi(knex, pluginOptions = {}) {
1866
1866
  status: { type: 'array', filterOperator: 'in' } // 'in' operator requires array type
1867
1867
  },
1868
1868
  tableName: 'searchmerge_orders'
1869
- });
1870
- await api.resources.orders.createKnexTable();
1871
- mapTable('searchmerge_orders', 'orders');
1869
+ })
1870
+ await api.resources.orders.createKnexTable()
1871
+ mapTable('searchmerge_orders', 'orders')
1872
1872
 
1873
- return api;
1873
+ return api
1874
1874
  }