json-rest-api 1.0.10 → 1.0.13

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 (117) hide show
  1. package/.claude/settings.local.json +9 -1
  2. package/AGENTS.md +34 -0
  3. package/CLAUDE.md +26 -1
  4. package/CRUSH.md +55 -0
  5. package/README.md +3 -3
  6. package/TODO.todo +8 -0
  7. package/docs/API.md +17 -18
  8. package/docs/COMPARISON.md +18 -13
  9. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
  10. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
  11. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
  12. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
  13. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
  14. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
  15. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
  16. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
  17. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
  18. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
  19. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
  20. package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
  21. package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
  22. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
  23. package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
  24. package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
  25. package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
  26. package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
  27. package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
  28. package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
  29. package/docs/GUIDE/index.md +3 -0
  30. package/docs/QUICKSTART.md +12 -12
  31. package/docs/index.md +2 -2
  32. package/docs/mini-guides/file-uploads-complete.md +0 -1
  33. package/examples/url-override-example.js +137 -0
  34. package/index.js +7 -1
  35. package/package.json +7 -11
  36. package/plugins/core/bulk-operations-plugin.js +2 -0
  37. package/plugins/core/connectors/express-plugin.js +43 -24
  38. package/plugins/core/connectors/lib/request-helpers.js +2 -2
  39. package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
  40. package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
  41. package/plugins/core/lib/anyapi/schema-utils.js +186 -0
  42. package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
  43. package/plugins/core/lib/dbTablesOperations.js +305 -0
  44. package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
  45. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
  46. package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
  47. package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
  48. package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
  49. package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
  50. package/plugins/core/lib/querying/url-helpers.js +119 -0
  51. package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
  52. package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
  53. package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
  54. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
  55. package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
  56. package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
  57. package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
  58. package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
  59. package/plugins/core/lib/storage/storage-adapter.js +160 -0
  60. package/plugins/core/lib/writing/relationship-processor.js +4 -11
  61. package/plugins/core/rest-api-access.js +456 -0
  62. package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
  63. package/plugins/core/rest-api-knex-plugin.js +226 -87
  64. package/plugins/core/rest-api-multihome-plugin.js +89 -27
  65. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
  66. package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
  67. package/plugins/core/rest-api-plugin-methods/common.js +46 -10
  68. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
  69. package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
  70. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
  71. package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
  72. package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
  73. package/plugins/core/rest-api-plugin-methods/get.js +2 -5
  74. package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
  75. package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
  76. package/plugins/core/rest-api-plugin-methods/post.js +23 -11
  77. package/plugins/core/rest-api-plugin-methods/put.js +18 -21
  78. package/plugins/core/rest-api-plugin-methods/query.js +3 -4
  79. package/plugins/core/rest-api-plugin-methods/release.js +13 -0
  80. package/plugins/core/rest-api-plugin.js +3 -4
  81. package/plugins/core/rest-api-positioning-plugin.js +253 -76
  82. package/plugins/core/socketio-plugin.js +436 -502
  83. package/quickTest.js +2 -2
  84. package/scripts/debug-anyapi-review-filter.mjs +63 -0
  85. package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
  86. package/scripts/inspect-anyapi-descriptor.mjs +19 -0
  87. package/test-debug.js +59 -0
  88. package/tests/README.md +7 -1
  89. package/tests/anyapi-basic.test.js +102 -0
  90. package/tests/anyapi-cursor-pagination.test.js +144 -0
  91. package/tests/computed-fields.test.js +27 -12
  92. package/tests/cors.test.js +5 -6
  93. package/tests/curl.test.js +1 -1
  94. package/tests/custom-idproperty-relationships.test.js +37 -29
  95. package/tests/field-getters.test.js +25 -7
  96. package/tests/fixtures/api-configs.js +576 -150
  97. package/tests/helpers/storage-mode.js +65 -0
  98. package/tests/helpers/test-utils.js +85 -3
  99. package/tests/include-depth-validation.test.js +35 -1
  100. package/tests/nested-includes.test.js +130 -103
  101. package/tests/pagination-cursor-multifield.test.js +17 -13
  102. package/tests/pagination-enhanced.test.js +19 -24
  103. package/tests/polymorphic-simple.test.js +9 -9
  104. package/tests/positioning-advanced.test.js +19 -19
  105. package/tests/positioning-extended.test.js +25 -25
  106. package/tests/positioning-multifilter.test.js +62 -62
  107. package/tests/positioning.test.js +7 -7
  108. package/tests/queries.test.js +5 -5
  109. package/tests/query-limits.test.js +77 -92
  110. package/tests/rest-api-access.test.js +206 -0
  111. package/tests/searchschema-merge.test.js +182 -0
  112. package/tests/socketio.test.js +33 -14
  113. package/tests/virtual-fields.test.js +0 -1
  114. package/docs/DOCS_ALL.md +0 -21894
  115. package/plugins/core/jwt-auth-plugin.js +0 -897
  116. package/plugins/core/lib/jwt-auth-helpers.js +0 -130
  117. package/tests/jwt-auth.test.js +0 -692
@@ -1,8 +1,54 @@
1
1
  import { Api } from 'hooked-api';
2
- import { RestApiPlugin, RestApiKnexPlugin } from '../../index.js';
2
+ import { RestApiPlugin, RestApiKnexPlugin, RestApiAnyapiKnexPlugin } from '../../index.js';
3
+ import { AccessPlugin } from '../../plugins/core/rest-api-access.js';
3
4
  import { ExpressPlugin } from '../../plugins/core/connectors/express-plugin.js';
4
5
  import express from 'express';
5
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
+ }
18
+
19
+ async function useStoragePlugin(api, knex, { tenantId } = {}) {
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);
28
+ } else {
29
+ await api.use(RestApiKnexPlugin, { knex });
30
+ }
31
+ }
32
+
33
+ function mapTable(tableName, resourceName) {
34
+ if (storageMode.isAnyApi()) {
35
+ storageMode.registerTable(tableName, resourceName);
36
+ }
37
+ }
38
+
39
+ async function withTenantContext(tenantId, fn) {
40
+ if (!storageMode.isAnyApi()) {
41
+ return await fn();
42
+ }
43
+
44
+ const previousTenant = storageMode.currentTenant;
45
+ storageMode.setCurrentTenant(tenantId || storageMode.defaultTenant);
46
+ try {
47
+ return await fn();
48
+ } finally {
49
+ storageMode.setCurrentTenant(previousTenant);
50
+ }
51
+ }
6
52
 
7
53
  /**
8
54
  * Creates a basic API configuration with Countries, Publishers, Authors, Books
@@ -10,12 +56,22 @@ import { createServer } from 'http';
10
56
  export async function createBasicApi(knex, pluginOptions = {}) {
11
57
  const apiName = pluginOptions.apiName || `basic-test-api`;
12
58
  const tablePrefix = pluginOptions.tablePrefix || 'basic';
59
+ if (storageMode.isAnyApi()) {
60
+ storageMode.clearRegistry();
61
+ }
13
62
  const api = new Api({
14
63
  name: apiName,
15
- version: '1.0.0',
16
64
  log: { level: process.env.LOG_LEVEL || 'info' }
17
65
  });
18
66
 
67
+ const previousTenant = storageMode.currentTenant;
68
+ const tenantId = storageMode.isAnyApi()
69
+ ? (pluginOptions.tenantId || `${tablePrefix}_tenant`)
70
+ : storageMode.defaultTenant;
71
+ if (storageMode.isAnyApi()) {
72
+ storageMode.setCurrentTenant(tenantId);
73
+ }
74
+
19
75
  const restApiOptions = {
20
76
  simplifiedApi: false,
21
77
  simplifiedTransport: false,
@@ -34,9 +90,9 @@ export async function createBasicApi(knex, pluginOptions = {}) {
34
90
  };
35
91
 
36
92
  await api.use(RestApiPlugin, restApiOptions);
37
-
38
- await api.use(RestApiKnexPlugin, { knex });
39
-
93
+ await useStoragePlugin(api, knex, { tenantId });
94
+ await resetAnyApiTables(knex);
95
+
40
96
  // Add Express plugin if requested
41
97
  if (pluginOptions.includeExpress) {
42
98
  await api.use(ExpressPlugin, {
@@ -45,6 +101,7 @@ export async function createBasicApi(knex, pluginOptions = {}) {
45
101
  });
46
102
  }
47
103
 
104
+ try {
48
105
  // Countries table
49
106
  await api.addResource('countries', {
50
107
  schema: {
@@ -53,12 +110,13 @@ export async function createBasicApi(knex, pluginOptions = {}) {
53
110
  code: { type: 'string', max: 2, unique: true }
54
111
  },
55
112
  relationships: {
56
- publishers: { hasMany: 'publishers', foreignKey: 'country_id' },
57
- books: { hasMany: 'books', foreignKey: 'country_id' }
113
+ publishers: { type: 'hasMany', target: 'publishers', foreignKey: 'country_id' },
114
+ books: { type: 'hasMany', target: 'books', foreignKey: 'country_id' }
58
115
  },
59
116
  tableName: `${tablePrefix}_countries`
60
117
  });
61
118
  await api.resources.countries.createKnexTable();
119
+ mapTable(`${tablePrefix}_countries`, 'countries');
62
120
 
63
121
  // Publishers table
64
122
  await api.addResource('publishers', {
@@ -68,11 +126,12 @@ export async function createBasicApi(knex, pluginOptions = {}) {
68
126
  country_id: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' }
69
127
  },
70
128
  relationships: {
71
- books: { hasMany: 'books', foreignKey: 'publisher_id' }
129
+ books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' }
72
130
  },
73
131
  tableName: `${tablePrefix}_publishers`
74
132
  });
75
133
  await api.resources.publishers.createKnexTable();
134
+ mapTable(`${tablePrefix}_publishers`, 'publishers');
76
135
 
77
136
  // Authors table
78
137
  await api.addResource('authors', {
@@ -81,11 +140,12 @@ export async function createBasicApi(knex, pluginOptions = {}) {
81
140
  name: { type: 'string', required: true, max: 200 }
82
141
  },
83
142
  relationships: {
84
- books: { hasMany: 'books', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' }
143
+ books: { type: 'manyToMany', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' }
85
144
  },
86
145
  tableName: `${tablePrefix}_authors`
87
146
  });
88
147
  await api.resources.authors.createKnexTable();
148
+ mapTable(`${tablePrefix}_authors`, 'authors');
89
149
 
90
150
  // Books table
91
151
  await api.addResource('books', {
@@ -96,11 +156,12 @@ export async function createBasicApi(knex, pluginOptions = {}) {
96
156
  publisher_id: { type: 'number', belongsTo: 'publishers', as: 'publisher', search: true }
97
157
  },
98
158
  relationships: {
99
- authors: { hasMany: 'authors', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' }
159
+ authors: { type: 'manyToMany', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' }
100
160
  },
101
161
  tableName: `${tablePrefix}_books`
102
162
  });
103
163
  await api.resources.books.createKnexTable();
164
+ mapTable(`${tablePrefix}_books`, 'books');
104
165
 
105
166
  // Book-Authors pivot table
106
167
  await api.addResource('book_authors', {
@@ -112,8 +173,135 @@ export async function createBasicApi(knex, pluginOptions = {}) {
112
173
  tableName: `${tablePrefix}_book_authors`
113
174
  });
114
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
+ `${tablePrefix}_book_authors`,
185
+ 'books',
186
+ 'authors',
187
+ relationshipKey,
188
+ inverseRelationshipKey,
189
+ );
190
+ }
115
191
 
116
- return api;
192
+ return api;
193
+ } finally {
194
+ if (storageMode.isAnyApi()) {
195
+ storageMode.setCurrentTenant(previousTenant);
196
+ }
197
+ }
198
+ }
199
+
200
+ export async function createAccessControlApi(knex, pluginOptions = {}) {
201
+ const apiName = pluginOptions.apiName || 'access-test-api';
202
+ const tablePrefix = pluginOptions.tablePrefix || 'access';
203
+ if (storageMode.isAnyApi()) {
204
+ storageMode.clearRegistry();
205
+ }
206
+
207
+ const api = new Api({
208
+ name: apiName,
209
+ log: { level: process.env.LOG_LEVEL || 'info' }
210
+ });
211
+
212
+ const previousTenant = storageMode.currentTenant;
213
+ const tenantId = storageMode.isAnyApi()
214
+ ? (pluginOptions.tenantId || `${tablePrefix}_tenant`)
215
+ : storageMode.defaultTenant;
216
+ if (storageMode.isAnyApi()) {
217
+ storageMode.setCurrentTenant(tenantId);
218
+ }
219
+
220
+ await api.use(RestApiPlugin, {
221
+ simplifiedApi: false,
222
+ simplifiedTransport: false,
223
+ returnRecordApi: {
224
+ post: true,
225
+ put: false,
226
+ patch: false
227
+ },
228
+ returnRecordTransport: {
229
+ post: 'full',
230
+ put: 'no',
231
+ patch: 'minimal'
232
+ },
233
+ sortableFields: ['id', 'name', 'user_id']
234
+ });
235
+ await useStoragePlugin(api, knex, { tenantId });
236
+ await resetAnyApiTables(knex);
237
+
238
+ try {
239
+ await api.use(AccessPlugin, pluginOptions.access || {});
240
+
241
+ await api.addResource('users', {
242
+ schema: {
243
+ id: { type: 'id' },
244
+ email: { type: 'string', required: true },
245
+ display_name: { type: 'string', nullable: true }
246
+ },
247
+ tableName: `${tablePrefix}_users`
248
+ });
249
+ await api.resources.users.createKnexTable();
250
+ mapTable(`${tablePrefix}_users`, 'users');
251
+
252
+ await api.addResource('projects', {
253
+ schema: {
254
+ id: { type: 'id' },
255
+ name: { type: 'string', required: true },
256
+ status: { type: 'string', nullable: true },
257
+ user_id: { type: 'number', belongsTo: 'users', as: 'user', nullable: true }
258
+ },
259
+ relationships: {
260
+ user: { type: 'belongsTo', target: 'users', foreignKey: 'user_id' }
261
+ },
262
+ auth: {
263
+ query: ['authenticated'],
264
+ get: ['owns'],
265
+ post: ['authenticated'],
266
+ patch: ['owns'],
267
+ delete: ['owns']
268
+ },
269
+ ownership: true,
270
+ tableName: `${tablePrefix}_projects`
271
+ });
272
+ await api.resources.projects.createKnexTable();
273
+ mapTable(`${tablePrefix}_projects`, 'projects');
274
+
275
+ await api.addResource('notes', {
276
+ schema: {
277
+ id: { type: 'id' },
278
+ project_id: { type: 'number', belongsTo: 'projects', as: 'project', nullable: true },
279
+ body: { type: 'string', required: true },
280
+ user_id: { type: 'number', belongsTo: 'users', as: 'user', nullable: true }
281
+ },
282
+ relationships: {
283
+ project: { type: 'belongsTo', target: 'projects', foreignKey: 'project_id' },
284
+ user: { type: 'belongsTo', target: 'users', foreignKey: 'user_id' }
285
+ },
286
+ auth: {
287
+ query: ['authenticated'],
288
+ get: ['owns'],
289
+ post: ['authenticated'],
290
+ patch: ['owns'],
291
+ delete: ['owns']
292
+ },
293
+ ownership: true,
294
+ tableName: `${tablePrefix}_notes`
295
+ });
296
+ await api.resources.notes.createKnexTable();
297
+ mapTable(`${tablePrefix}_notes`, 'notes');
298
+
299
+ return api;
300
+ } finally {
301
+ if (storageMode.isAnyApi()) {
302
+ storageMode.setCurrentTenant(previousTenant);
303
+ }
304
+ }
117
305
  }
118
306
 
119
307
  /**
@@ -122,8 +310,11 @@ export async function createBasicApi(knex, pluginOptions = {}) {
122
310
  export async function createBulkOperationsApi(knex, pluginOptions = {}) {
123
311
  const { BulkOperationsPlugin } = await import('../../plugins/core/bulk-operations-plugin.js');
124
312
 
125
- const api = await createBasicApi(knex, pluginOptions);
126
-
313
+ const api = await createBasicApi(knex, {
314
+ ...pluginOptions,
315
+ tenantId: pluginOptions.tenantId || 'bulk_ops_tenant'
316
+ });
317
+
127
318
  // Add bulk operations plugin
128
319
  await api.use(BulkOperationsPlugin, {
129
320
  'bulk-operations': {
@@ -144,9 +335,10 @@ export async function createBulkOperationsApi(knex, pluginOptions = {}) {
144
335
  export async function createExtendedApi(knex) {
145
336
  const api = new Api({
146
337
  name: 'extended-test-api',
147
- version: '1.0.0'
148
338
  });
149
339
 
340
+ const tenantId = storageMode.isAnyApi() ? 'extended_tenant' : storageMode.defaultTenant;
341
+
150
342
  await api.use(RestApiPlugin, {
151
343
  simplifiedApi: false,
152
344
  simplifiedTransport: false,
@@ -162,11 +354,12 @@ export async function createExtendedApi(knex) {
162
354
  },
163
355
  sortableFields: ['id', 'title', 'country_id', 'publisher_id', 'price', 'language', 'population', 'name', 'code']
164
356
  });
165
-
166
- await api.use(RestApiKnexPlugin, { knex });
357
+ await withTenantContext(tenantId, async () => {
358
+ await useStoragePlugin(api, knex, { tenantId });
359
+ await resetAnyApiTables(knex);
167
360
 
168
- // Countries with extended fields
169
- await api.addResource('countries', {
361
+ // Countries with extended fields
362
+ await api.addResource('countries', {
170
363
  schema: {
171
364
  id: { type: 'id' },
172
365
  name: { type: 'string', required: true, max: 100 },
@@ -176,13 +369,14 @@ export async function createExtendedApi(knex) {
176
369
  currency: { type: 'string', max: 3 }
177
370
  },
178
371
  relationships: {
179
- publishers: { hasMany: 'publishers', foreignKey: 'country_id' },
180
- books: { hasMany: 'books', foreignKey: 'country_id' },
181
- authors: { hasMany: 'authors', foreignKey: 'nationality_id' }
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' }
182
375
  },
183
376
  tableName: 'ext_countries'
184
377
  });
185
378
  await api.resources.countries.createKnexTable();
379
+ mapTable('ext_countries', 'countries');
186
380
 
187
381
  // Publishers with extended fields
188
382
  await api.addResource('publishers', {
@@ -195,15 +389,17 @@ export async function createExtendedApi(knex) {
195
389
  active: { type: 'boolean', default: true }
196
390
  },
197
391
  relationships: {
198
- books: { hasMany: 'books', foreignKey: 'publisher_id' },
392
+ books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' },
199
393
  reviews: {
200
- hasMany: 'reviews',
394
+ type: 'hasMany',
395
+ target: 'reviews',
201
396
  via: 'reviewable'
202
397
  }
203
398
  },
204
399
  tableName: 'ext_publishers'
205
400
  });
206
401
  await api.resources.publishers.createKnexTable();
402
+ mapTable('ext_publishers', 'publishers');
207
403
 
208
404
  // Authors with extended fields
209
405
  await api.addResource('authors', {
@@ -215,15 +411,17 @@ export async function createExtendedApi(knex) {
215
411
  nationality_id: { type: 'number', belongsTo: 'countries', as: 'nationality' }
216
412
  },
217
413
  relationships: {
218
- books: { hasMany: 'books', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' },
414
+ books: { type: 'manyToMany', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' },
219
415
  reviews: {
220
- hasMany: 'reviews',
416
+ type: 'hasMany',
417
+ target: 'reviews',
221
418
  via: 'reviewable'
222
419
  }
223
420
  },
224
421
  tableName: 'ext_authors'
225
422
  });
226
423
  await api.resources.authors.createKnexTable();
424
+ mapTable('ext_authors', 'authors');
227
425
 
228
426
  // Books with extended fields
229
427
  await api.addResource('books', {
@@ -239,15 +437,17 @@ export async function createExtendedApi(knex) {
239
437
  publisher_id: { type: 'number', belongsTo: 'publishers', as: 'publisher', search: true }
240
438
  },
241
439
  relationships: {
242
- authors: { hasMany: 'authors', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' },
440
+ authors: { type: 'manyToMany', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' },
243
441
  reviews: {
244
- hasMany: 'reviews',
442
+ type: 'hasMany',
443
+ target: 'reviews',
245
444
  via: 'reviewable'
246
445
  }
247
446
  },
248
447
  tableName: 'ext_books'
249
448
  });
250
449
  await api.resources.books.createKnexTable();
450
+ mapTable('ext_books', 'books');
251
451
 
252
452
  // Book-Authors pivot with extended fields
253
453
  await api.addResource('book_authors', {
@@ -261,6 +461,14 @@ export async function createExtendedApi(knex) {
261
461
  tableName: 'ext_book_authors'
262
462
  });
263
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
+ }
264
472
 
265
473
  // Polymorphic reviews (can go on authors, books and publishers)
266
474
  await api.addResource('reviews', {
@@ -287,7 +495,8 @@ export async function createExtendedApi(knex) {
287
495
  tableName: 'ext_reviews'
288
496
  });
289
497
  await api.resources.reviews.createKnexTable();
290
-
498
+ mapTable('ext_reviews', 'reviews');
499
+ });
291
500
 
292
501
  return api;
293
502
  }
@@ -299,9 +508,10 @@ export async function createExtendedApi(knex) {
299
508
  export async function createLimitedDepthApi(knex) {
300
509
  const api = new Api({
301
510
  name: 'limited-depth-api',
302
- version: '1.0.0'
303
511
  });
304
512
 
513
+ const tenantId = storageMode.isAnyApi() ? 'limited_depth_tenant' : storageMode.defaultTenant;
514
+
305
515
  await api.use(RestApiPlugin, {
306
516
  simplifiedApi: false,
307
517
  simplifiedTransport: false,
@@ -318,23 +528,24 @@ export async function createLimitedDepthApi(knex) {
318
528
  sortableFields: ['id', 'title', 'country_id', 'publisher_id', 'name', 'code'],
319
529
  includeDepthLimit: 2 // Key difference: limit is 2 instead of default 3
320
530
  });
321
-
322
- await api.use(RestApiKnexPlugin, { knex });
531
+ await withTenantContext(tenantId, async () => {
532
+ await useStoragePlugin(api, knex, { tenantId });
323
533
 
324
- // Use different table names with 'limited_' prefix to avoid conflicts
325
- await api.addResource('countries', {
534
+ // Use different table names with 'limited_' prefix to avoid conflicts
535
+ await api.addResource('countries', {
326
536
  schema: {
327
537
  id: { type: 'id' },
328
538
  name: { type: 'string', required: true, max: 100 },
329
539
  code: { type: 'string', max: 2, unique: true }
330
540
  },
331
541
  relationships: {
332
- publishers: { hasMany: 'publishers', foreignKey: 'country_id' },
333
- books: { hasMany: 'books', foreignKey: 'country_id' }
542
+ publishers: { type: 'hasMany', target: 'publishers', foreignKey: 'country_id' },
543
+ books: { type: 'hasMany', target: 'books', foreignKey: 'country_id' }
334
544
  },
335
545
  tableName: 'limited_countries',
336
546
  });
337
547
  await api.resources.countries.createKnexTable();
548
+ mapTable('limited_countries', 'countries');
338
549
 
339
550
  await api.addResource('publishers', {
340
551
  schema: {
@@ -343,12 +554,13 @@ export async function createLimitedDepthApi(knex) {
343
554
  country_id: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' }
344
555
  },
345
556
  relationships: {
346
- books: { hasMany: 'books', foreignKey: 'publisher_id' },
347
- authors: { hasMany: 'authors', foreignKey: 'publisher_id' }
557
+ books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' },
558
+ authors: { type: 'hasMany', target: 'authors', foreignKey: 'publisher_id' }
348
559
  },
349
560
  tableName: 'limited_publishers'
350
561
  });
351
562
  await api.resources.publishers.createKnexTable();
563
+ mapTable('limited_publishers', 'publishers');
352
564
 
353
565
  await api.addResource('authors', {
354
566
  schema: {
@@ -357,7 +569,7 @@ export async function createLimitedDepthApi(knex) {
357
569
  publisher_id: { type: 'number', nullable: true, belongsTo: 'publishers', as: 'publisher' }
358
570
  },
359
571
  relationships: {
360
- books: { hasMany: 'books', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' }
572
+ books: { type: 'manyToMany', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' }
361
573
  },
362
574
  tableName: 'limited_authors'
363
575
  });
@@ -371,7 +583,7 @@ export async function createLimitedDepthApi(knex) {
371
583
  publisher_id: { type: 'number', belongsTo: 'publishers', as: 'publisher', search: true }
372
584
  },
373
585
  relationships: {
374
- authors: { hasMany: 'authors', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' }
586
+ authors: { type: 'manyToMany', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' }
375
587
  },
376
588
  tableName: 'limited_books'
377
589
  });
@@ -386,6 +598,15 @@ export async function createLimitedDepthApi(knex) {
386
598
  tableName: 'limited_book_authors'
387
599
  });
388
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
+ });
389
610
 
390
611
  return api;
391
612
  }
@@ -396,9 +617,10 @@ export async function createLimitedDepthApi(knex) {
396
617
  export async function createPaginationApi(knex, options = {}) {
397
618
  const api = new Api({
398
619
  name: 'pagination-test-api',
399
- version: '1.0.0'
400
620
  });
401
621
 
622
+ const tenantId = storageMode.isAnyApi() ? 'pagination_tenant' : storageMode.defaultTenant;
623
+
402
624
  const restApiOptions = {
403
625
  simplifiedApi: false,
404
626
  simplifiedTransport: false,
@@ -413,27 +635,28 @@ export async function createPaginationApi(knex, options = {}) {
413
635
  patch: 'minimal'
414
636
  },
415
637
  sortableFields: ['id', 'title', 'country_id', 'publisher_id', 'name', 'code'],
416
- ...options // Allow overriding options like publicBaseUrl, enablePaginationCounts
638
+ ...options // Allow overriding options likereturnBasePath, enablePaginationCounts
417
639
  };
418
640
 
419
641
  await api.use(RestApiPlugin, restApiOptions);
420
-
421
- await api.use(RestApiKnexPlugin, { knex });
642
+ await withTenantContext(tenantId, async () => {
643
+ await useStoragePlugin(api, knex, { tenantId });
422
644
 
423
- // Countries table
424
- await api.addResource('countries', {
645
+ // Countries table
646
+ await api.addResource('countries', {
425
647
  schema: {
426
648
  id: { type: 'id' },
427
649
  name: { type: 'string', required: true, max: 100 },
428
650
  code: { type: 'string', max: 2, unique: true }
429
651
  },
430
652
  relationships: {
431
- publishers: { hasMany: 'publishers', foreignKey: 'country_id' },
432
- books: { hasMany: 'books', foreignKey: 'country_id' }
653
+ publishers: { type: 'hasMany', target: 'publishers', foreignKey: 'country_id' },
654
+ books: { type: 'hasMany', target: 'books', foreignKey: 'country_id' }
433
655
  },
434
656
  tableName: 'pagination_countries'
435
657
  });
436
658
  await api.resources.countries.createKnexTable();
659
+ mapTable('pagination_countries', 'countries');
437
660
 
438
661
  // Publishers table
439
662
  await api.addResource('publishers', {
@@ -443,11 +666,12 @@ export async function createPaginationApi(knex, options = {}) {
443
666
  country_id: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' }
444
667
  },
445
668
  relationships: {
446
- books: { hasMany: 'books', foreignKey: 'publisher_id' }
669
+ books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' }
447
670
  },
448
671
  tableName: 'pagination_publishers'
449
672
  });
450
673
  await api.resources.publishers.createKnexTable();
674
+ mapTable('pagination_publishers', 'publishers');
451
675
 
452
676
  // Books table
453
677
  await api.addResource('books', {
@@ -460,6 +684,8 @@ export async function createPaginationApi(knex, options = {}) {
460
684
  tableName: 'pagination_books'
461
685
  });
462
686
  await api.resources.books.createKnexTable();
687
+ mapTable('pagination_books', 'books');
688
+ });
463
689
 
464
690
  return api;
465
691
  }
@@ -469,24 +695,57 @@ export async function createPaginationApi(knex, options = {}) {
469
695
  */
470
696
  export async function createWebSocketApi(knex, pluginOptions = {}) {
471
697
  const { SocketIOPlugin } = await import('../../plugins/core/socketio-plugin.js');
472
- const { JwtAuthPlugin } = await import('../../plugins/core/jwt-auth-plugin.js');
473
-
698
+ const { jwtVerify } = await import('jose');
699
+
474
700
  const api = await createBasicApi(knex, {
475
701
  ...pluginOptions,
702
+ tenantId: pluginOptions.tenantId || 'socketio_tenant',
476
703
  includeExpress: true,
477
704
  express: {
478
705
  port: 0 // Let OS assign a port
479
706
  }
480
707
  });
481
-
482
- // Add JWT auth plugin (required by SocketIO plugin)
483
- await api.use(JwtAuthPlugin, {
484
- secret: 'test-secret-key',
485
- expiresIn: '1h'
486
- });
487
-
488
- // Add SocketIO plugin
489
- await api.use(SocketIOPlugin, pluginOptions['socketio'] || {});
708
+
709
+ const socketioOptions = { ...(pluginOptions['socketio'] || {}) };
710
+ const authOptions = { ...(socketioOptions.auth || {}) };
711
+ const sharedSecret = authOptions.sharedSecret || socketioOptions.sharedSecret || 'test-secret-key';
712
+
713
+ if (!authOptions.authenticate) {
714
+ authOptions.authenticate = async ({ socket }) => {
715
+ const token = socket.handshake.auth?.token;
716
+ if (!token) {
717
+ throw new Error('Authentication required');
718
+ }
719
+
720
+ const encoder = new TextEncoder();
721
+ const key = encoder.encode(sharedSecret);
722
+ const { payload } = await jwtVerify(token, key);
723
+
724
+ const roles = Array.isArray(payload.roles)
725
+ ? payload.roles
726
+ : payload.role ? [payload.role] : [];
727
+
728
+ const authContext = {
729
+ userId: payload.userId || payload.sub,
730
+ roles,
731
+ token: payload
732
+ };
733
+
734
+ if (!authContext.userId) {
735
+ throw new Error('Token is missing required user identifier');
736
+ }
737
+
738
+ return authContext;
739
+ };
740
+ }
741
+
742
+ if (authOptions.requireAuth === undefined) {
743
+ authOptions.requireAuth = true;
744
+ }
745
+
746
+ socketioOptions.auth = authOptions;
747
+
748
+ await api.use(SocketIOPlugin, socketioOptions);
490
749
 
491
750
  // Create and start Express server
492
751
  const app = express();
@@ -517,84 +776,91 @@ export async function createWebSocketApi(knex, pluginOptions = {}) {
517
776
  export async function createComputedFieldsApi(knex, pluginOptions = {}) {
518
777
  const api = new Api({
519
778
  name: 'computed-fields-test-api',
520
- version: '1.0.0',
521
779
  log: { level: process.env.LOG_LEVEL || 'info' }
522
780
  });
523
781
 
782
+ const tenantId = storageMode.isAnyApi() ? 'computed_fields_tenant' : storageMode.defaultTenant;
783
+
524
784
  await api.use(RestApiPlugin, {
525
785
  simplifiedApi: true,
526
786
  simplifiedTransport: true
527
787
  });
528
-
529
- await api.use(RestApiKnexPlugin, { knex });
530
788
 
531
- // Products resource with computed fields
532
- await api.addResource('products', {
533
- schema: {
534
- id: { type: 'id' },
535
- name: { type: 'string', required: true, max: 255 },
536
- price: { type: 'number', required: true, min: 0 },
537
- cost: { type: 'number', required: true, min: 0, normallyHidden: true },
538
- internal_notes: { type: 'string', normallyHidden: true },
539
- profit_margin: {
540
- type: 'number',
541
- computed: true,
542
- dependencies: ['price', 'cost'],
543
- compute: ({ attributes }) => {
544
- if (!attributes.price || attributes.price === 0) return 0;
545
- return Number(((attributes.price - attributes.cost) / attributes.price * 100).toFixed(2));
789
+ await withTenantContext(tenantId, async () => {
790
+ await useStoragePlugin(api, knex, { tenantId });
791
+ await resetAnyApiTables(knex);
792
+
793
+ // Products resource with computed fields
794
+ await api.addResource('products', {
795
+ schema: {
796
+ id: { type: 'id' },
797
+ name: { type: 'string', required: true, max: 255 },
798
+ price: { type: 'number', required: true, min: 0 },
799
+ cost: { type: 'number', required: true, min: 0, normallyHidden: true },
800
+ internal_notes: { type: 'string', normallyHidden: true },
801
+ profit_margin: {
802
+ type: 'number',
803
+ computed: true,
804
+ dependencies: ['price', 'cost'],
805
+ compute: ({ attributes }) => {
806
+ if (!attributes.price || attributes.price === 0) return 0;
807
+ return Number(((attributes.price - attributes.cost) / attributes.price * 100).toFixed(2));
808
+ }
809
+ },
810
+ profit_amount: {
811
+ type: 'number',
812
+ computed: true,
813
+ dependencies: ['price', 'cost'],
814
+ compute: ({ attributes }) => {
815
+ return Number((attributes.price - attributes.cost).toFixed(2));
816
+ }
546
817
  }
547
818
  },
548
- profit_amount: {
549
- type: 'number',
550
- computed: true,
551
- dependencies: ['price', 'cost'],
552
- compute: ({ attributes }) => {
553
- return Number((attributes.price - attributes.cost).toFixed(2));
554
- }
555
- }
556
- },
557
- relationships: {
558
- reviews: { hasMany: 'reviews', foreignKey: 'product_id' }
559
- },
560
- tableName: 'test_products'
561
- });
562
- await api.resources.products.createKnexTable();
563
-
564
- // Reviews resource with computed fields
565
- await api.addResource('reviews', {
566
- schema: {
567
- id: { type: 'id' },
568
- product_id: { type: 'number', required: true, belongsTo: 'products', as: 'product' },
569
- reviewer_name: { type: 'string', required: true },
570
- rating: { type: 'number', required: true, min: 1, max: 5 },
571
- comment: { type: 'string', max: 1000 },
572
- helpful_votes: { type: 'number', default: 0 },
573
- total_votes: { type: 'number', default: 0 },
574
- spam_score: { type: 'number', default: 0, normallyHidden: true },
575
- helpfulness_score: {
576
- type: 'number',
577
- computed: true,
578
- dependencies: ['helpful_votes', 'total_votes'],
579
- compute: ({ attributes }) => {
580
- if (attributes.total_votes === 0) return null;
581
- return Number(((attributes.helpful_votes / attributes.total_votes) * 100).toFixed(0));
582
- }
819
+ relationships: {
820
+ reviews: { type: 'hasMany', target: 'reviews', foreignKey: 'product_id' }
583
821
  },
584
- is_helpful: {
585
- type: 'boolean',
586
- computed: true,
587
- dependencies: ['helpful_votes', 'total_votes', 'spam_score'],
588
- compute: ({ attributes }) => {
589
- if (attributes.total_votes < 10) return null;
590
- const helpfulnessScore = (attributes.helpful_votes / attributes.total_votes) * 100;
591
- return helpfulnessScore > 70 && attributes.spam_score < 0.5;
822
+ tableName: 'test_products'
823
+ });
824
+ await api.resources.products.createKnexTable();
825
+ mapTable('searchmerge_products', 'products');
826
+ mapTable('test_products', 'products');
827
+
828
+ // Reviews resource with computed fields
829
+ await api.addResource('reviews', {
830
+ schema: {
831
+ id: { type: 'id' },
832
+ product_id: { type: 'number', required: true, belongsTo: 'products', as: 'product' },
833
+ reviewer_name: { type: 'string', required: true },
834
+ rating: { type: 'number', required: true, min: 1, max: 5 },
835
+ comment: { type: 'string', max: 1000 },
836
+ helpful_votes: { type: 'number', default: 0 },
837
+ total_votes: { type: 'number', default: 0 },
838
+ spam_score: { type: 'number', default: 0, normallyHidden: true },
839
+ helpfulness_score: {
840
+ type: 'number',
841
+ computed: true,
842
+ dependencies: ['helpful_votes', 'total_votes'],
843
+ compute: ({ attributes }) => {
844
+ if (attributes.total_votes === 0) return null;
845
+ return Number(((attributes.helpful_votes / attributes.total_votes) * 100).toFixed(0));
846
+ }
847
+ },
848
+ is_helpful: {
849
+ type: 'boolean',
850
+ computed: true,
851
+ dependencies: ['helpful_votes', 'total_votes', 'spam_score'],
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;
856
+ }
592
857
  }
593
- }
594
- },
595
- tableName: 'test_reviews'
858
+ },
859
+ tableName: 'test_reviews'
860
+ });
861
+ await api.resources.reviews.createKnexTable();
862
+ mapTable('test_reviews', 'reviews');
596
863
  });
597
- await api.resources.reviews.createKnexTable();
598
864
 
599
865
  return api;
600
866
  }
@@ -605,16 +871,22 @@ export async function createComputedFieldsApi(knex, pluginOptions = {}) {
605
871
  export async function createFieldGettersApi(knex, pluginOptions = {}) {
606
872
  const api = new Api({
607
873
  name: 'field-getters-test-api',
608
- version: '1.0.0',
609
874
  log: { level: process.env.LOG_LEVEL || 'info' }
610
875
  });
611
876
 
877
+ const tenantId = storageMode.isAnyApi() ? 'field_getters_tenant' : storageMode.defaultTenant;
878
+ const previousTenant = storageMode.currentTenant;
879
+ if (storageMode.isAnyApi()) {
880
+ storageMode.setCurrentTenant(tenantId);
881
+ }
882
+
612
883
  await api.use(RestApiPlugin, {
613
884
  simplifiedApi: true,
614
885
  simplifiedTransport: true
615
886
  });
616
887
 
617
- await api.use(RestApiKnexPlugin, { knex });
888
+ await useStoragePlugin(api, knex, { tenantId });
889
+ await resetAnyApiTables(knex);
618
890
 
619
891
  // Users resource with basic getters
620
892
  await api.addResource('users', {
@@ -667,6 +939,8 @@ export async function createFieldGettersApi(knex, pluginOptions = {}) {
667
939
  tableName: 'getter_users'
668
940
  });
669
941
  await api.resources.users.createKnexTable();
942
+ mapTable('searchmerge_users', 'users');
943
+ mapTable('getter_users', 'users');
670
944
 
671
945
  // Products resource with getters and computed fields
672
946
  await api.addResource('products', {
@@ -703,11 +977,12 @@ export async function createFieldGettersApi(knex, pluginOptions = {}) {
703
977
  }
704
978
  },
705
979
  relationships: {
706
- reviews: { hasMany: 'reviews', foreignKey: 'product_id' }
980
+ reviews: { type: 'hasMany', target: 'reviews', foreignKey: 'product_id' }
707
981
  },
708
982
  tableName: 'getter_products'
709
983
  });
710
984
  await api.resources.products.createKnexTable();
985
+ mapTable('getter_products', 'products');
711
986
 
712
987
  // Reviews resource with getters
713
988
  await api.addResource('reviews', {
@@ -723,6 +998,7 @@ export async function createFieldGettersApi(knex, pluginOptions = {}) {
723
998
  tableName: 'getter_reviews'
724
999
  });
725
1000
  await api.resources.reviews.createKnexTable();
1001
+ mapTable('getter_reviews', 'reviews');
726
1002
 
727
1003
  // Resource with getter dependencies
728
1004
  await api.addResource('formatted_data', {
@@ -756,6 +1032,7 @@ export async function createFieldGettersApi(knex, pluginOptions = {}) {
756
1032
  tableName: 'getter_formatted'
757
1033
  });
758
1034
  await api.resources.formatted_data.createKnexTable();
1035
+ mapTable('getter_formatted', 'formatted_data');
759
1036
 
760
1037
  // Async getters example
761
1038
  await api.addResource('encrypted_data', {
@@ -785,6 +1062,11 @@ export async function createFieldGettersApi(knex, pluginOptions = {}) {
785
1062
  tableName: 'getter_encrypted'
786
1063
  });
787
1064
  await api.resources.encrypted_data.createKnexTable();
1065
+ mapTable('getter_encrypted', 'encrypted_data');
1066
+
1067
+ if (storageMode.isAnyApi()) {
1068
+ storageMode.setCurrentTenant(previousTenant);
1069
+ }
788
1070
 
789
1071
  return api;
790
1072
  }
@@ -795,10 +1077,15 @@ export async function createFieldGettersApi(knex, pluginOptions = {}) {
795
1077
  export async function createFieldSettersApi(knex, pluginOptions = {}) {
796
1078
  const api = new Api({
797
1079
  name: 'field-setters-test-api',
798
- version: '1.0.0',
799
1080
  log: { level: process.env.LOG_LEVEL || 'info' }
800
1081
  });
801
1082
 
1083
+ const tenantId = storageMode.isAnyApi() ? 'field_setters_tenant' : storageMode.defaultTenant;
1084
+ const previousTenant = storageMode.currentTenant;
1085
+ if (storageMode.isAnyApi()) {
1086
+ storageMode.setCurrentTenant(tenantId);
1087
+ }
1088
+
802
1089
  await api.use(RestApiPlugin, {
803
1090
  simplifiedApi: true,
804
1091
  simplifiedTransport: true,
@@ -809,7 +1096,8 @@ export async function createFieldSettersApi(knex, pluginOptions = {}) {
809
1096
  }
810
1097
  });
811
1098
 
812
- await api.use(RestApiKnexPlugin, { knex });
1099
+ await useStoragePlugin(api, knex, { tenantId });
1100
+ await resetAnyApiTables(knex);
813
1101
 
814
1102
  // Users resource with basic setters
815
1103
  await api.addResource('users', {
@@ -993,6 +1281,10 @@ export async function createFieldSettersApi(knex, pluginOptions = {}) {
993
1281
  });
994
1282
  await api.resources.validated_data.createKnexTable();
995
1283
 
1284
+ if (storageMode.isAnyApi()) {
1285
+ storageMode.setCurrentTenant(previousTenant);
1286
+ }
1287
+
996
1288
  return api;
997
1289
  }
998
1290
 
@@ -1001,9 +1293,14 @@ export async function createMultiHomeApi(knex, pluginOptions = {}) {
1001
1293
 
1002
1294
  const api = new Api({
1003
1295
  name: 'multihome-test-api',
1004
- version: '1.0.0'
1005
1296
  });
1006
1297
 
1298
+ const tenantId = storageMode.isAnyApi() ? 'multihome_tenant' : storageMode.defaultTenant;
1299
+ const previousTenant = storageMode.currentTenant;
1300
+ if (storageMode.isAnyApi()) {
1301
+ storageMode.setCurrentTenant(tenantId);
1302
+ }
1303
+
1007
1304
  await api.use(RestApiPlugin, {
1008
1305
  simplifiedApi: false,
1009
1306
  simplifiedTransport: false,
@@ -1020,7 +1317,8 @@ export async function createMultiHomeApi(knex, pluginOptions = {}) {
1020
1317
  sortableFields: ['id', 'title', 'name', 'tenant_id']
1021
1318
  });
1022
1319
 
1023
- await api.use(RestApiKnexPlugin, { knex });
1320
+ await useStoragePlugin(api, knex, { tenantId });
1321
+ await resetAnyApiTables(knex);
1024
1322
 
1025
1323
  // Add Express plugin if requested for transport testing
1026
1324
  if (pluginOptions.includeExpress) {
@@ -1049,11 +1347,12 @@ export async function createMultiHomeApi(knex, pluginOptions = {}) {
1049
1347
  tenant_id: { type: 'string', required: true }
1050
1348
  },
1051
1349
  relationships: {
1052
- tasks: { hasMany: 'tasks', foreignKey: 'project_id' }
1350
+ tasks: { type: 'hasMany', target: 'tasks', foreignKey: 'project_id' }
1053
1351
  },
1054
1352
  tableName: 'multihome_projects'
1055
1353
  });
1056
1354
  await api.resources.projects.createKnexTable();
1355
+ mapTable('multihome_projects', 'projects');
1057
1356
 
1058
1357
  await api.addResource('tasks', {
1059
1358
  schema: {
@@ -1066,6 +1365,7 @@ export async function createMultiHomeApi(knex, pluginOptions = {}) {
1066
1365
  tableName: 'multihome_tasks'
1067
1366
  });
1068
1367
  await api.resources.tasks.createKnexTable();
1368
+ mapTable('multihome_tasks', 'tasks');
1069
1369
 
1070
1370
  await api.addResource('users', {
1071
1371
  schema: {
@@ -1078,6 +1378,7 @@ export async function createMultiHomeApi(knex, pluginOptions = {}) {
1078
1378
  tableName: 'multihome_users'
1079
1379
  });
1080
1380
  await api.resources.users.createKnexTable();
1381
+ mapTable('multihome_users', 'users');
1081
1382
 
1082
1383
  // Global resource (excluded from multihome)
1083
1384
  await api.addResource('system_settings', {
@@ -1089,6 +1390,11 @@ export async function createMultiHomeApi(knex, pluginOptions = {}) {
1089
1390
  tableName: 'multihome_system_settings'
1090
1391
  });
1091
1392
  await api.resources.system_settings.createKnexTable();
1393
+ mapTable('multihome_system_settings', 'system_settings');
1394
+
1395
+ if (storageMode.isAnyApi()) {
1396
+ storageMode.setCurrentTenant(previousTenant);
1397
+ }
1092
1398
 
1093
1399
  return api;
1094
1400
  }
@@ -1101,7 +1407,6 @@ export async function createPositioningApi(knex, pluginOptions = {}) {
1101
1407
  const tablePrefix = pluginOptions.tablePrefix || 'positioning';
1102
1408
  const api = new Api({
1103
1409
  name: apiName,
1104
- version: '1.0.0'
1105
1410
  });
1106
1411
 
1107
1412
  const restApiOptions = {
@@ -1122,7 +1427,8 @@ export async function createPositioningApi(knex, pluginOptions = {}) {
1122
1427
  };
1123
1428
 
1124
1429
  await api.use(RestApiPlugin, restApiOptions);
1125
- await api.use(RestApiKnexPlugin, { knex });
1430
+ await useStoragePlugin(api, knex);
1431
+ await resetAnyApiTables(knex);
1126
1432
 
1127
1433
  // Categories (for grouping tasks)
1128
1434
  await api.addResource('categories', {
@@ -1133,6 +1439,7 @@ export async function createPositioningApi(knex, pluginOptions = {}) {
1133
1439
  tableName: `${tablePrefix}_categories`
1134
1440
  });
1135
1441
  await api.resources.categories.createKnexTable();
1442
+ mapTable(`${tablePrefix}_categories`, 'categories');
1136
1443
 
1137
1444
  // Tasks (main positioning test resource)
1138
1445
  await api.addResource('tasks', {
@@ -1151,6 +1458,7 @@ export async function createPositioningApi(knex, pluginOptions = {}) {
1151
1458
  tableName: `${tablePrefix}_tasks`
1152
1459
  });
1153
1460
  await api.resources.tasks.createKnexTable();
1461
+ mapTable(`${tablePrefix}_tasks`, 'tasks');
1154
1462
 
1155
1463
  // Projects (for multi-filter testing)
1156
1464
  await api.addResource('projects', {
@@ -1161,6 +1469,7 @@ export async function createPositioningApi(knex, pluginOptions = {}) {
1161
1469
  tableName: `${tablePrefix}_projects`
1162
1470
  });
1163
1471
  await api.resources.projects.createKnexTable();
1472
+ mapTable(`${tablePrefix}_projects`, 'projects');
1164
1473
 
1165
1474
  // Items (flexible resource for various positioning tests)
1166
1475
  await api.addResource('items', {
@@ -1180,6 +1489,7 @@ export async function createPositioningApi(knex, pluginOptions = {}) {
1180
1489
  tableName: `${tablePrefix}_items`
1181
1490
  });
1182
1491
  await api.resources.items.createKnexTable();
1492
+ mapTable(`${tablePrefix}_items`, 'items');
1183
1493
 
1184
1494
  return api;
1185
1495
  }
@@ -1193,10 +1503,13 @@ export async function createCustomIdPropertyApi(knex, pluginOptions = {}) {
1193
1503
  const tablePrefix = pluginOptions.tablePrefix || 'custom_id';
1194
1504
  const api = new Api({
1195
1505
  name: apiName,
1196
- version: '1.0.0',
1197
1506
  log: { level: process.env.LOG_LEVEL || 'info' }
1198
1507
  });
1199
1508
 
1509
+ if (storageMode.isAnyApi()) {
1510
+ storageMode.clearRegistry();
1511
+ }
1512
+
1200
1513
  const restApiOptions = {
1201
1514
  simplifiedApi: false,
1202
1515
  simplifiedTransport: false,
@@ -1215,7 +1528,8 @@ export async function createCustomIdPropertyApi(knex, pluginOptions = {}) {
1215
1528
  };
1216
1529
 
1217
1530
  await api.use(RestApiPlugin, restApiOptions);
1218
- await api.use(RestApiKnexPlugin, { knex });
1531
+ await useStoragePlugin(api, knex);
1532
+ await resetAnyApiTables(knex);
1219
1533
 
1220
1534
  // Countries table with custom idProperty
1221
1535
  await api.addResource('countries', {
@@ -1225,13 +1539,14 @@ export async function createCustomIdPropertyApi(knex, pluginOptions = {}) {
1225
1539
  code: { type: 'string', max: 2, unique: true }
1226
1540
  },
1227
1541
  relationships: {
1228
- publishers: { hasMany: 'publishers', foreignKey: 'country_id' },
1229
- books: { hasMany: 'books', foreignKey: 'country_id' }
1542
+ publishers: { type: 'hasMany', target: 'publishers', foreignKey: 'country_id' },
1543
+ books: { type: 'hasMany', target: 'books', foreignKey: 'country_id' }
1230
1544
  },
1231
1545
  tableName: `${tablePrefix}_countries`,
1232
1546
  idProperty: 'country_id' // Custom ID property
1233
1547
  });
1234
1548
  await api.resources.countries.createKnexTable();
1549
+ mapTable(`${tablePrefix}_countries`, 'countries');
1235
1550
 
1236
1551
  // Publishers table with custom idProperty
1237
1552
  await api.addResource('publishers', {
@@ -1240,9 +1555,10 @@ export async function createCustomIdPropertyApi(knex, pluginOptions = {}) {
1240
1555
  country_id: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' }
1241
1556
  },
1242
1557
  relationships: {
1243
- books: { hasMany: 'books', foreignKey: 'publisher_id' },
1558
+ books: { type: 'hasMany', target: 'books', foreignKey: 'publisher_id' },
1244
1559
  reviews: {
1245
- hasMany: 'reviews',
1560
+ type: 'hasMany',
1561
+ target: 'reviews',
1246
1562
  via: 'reviewable'
1247
1563
  }
1248
1564
  },
@@ -1250,6 +1566,7 @@ export async function createCustomIdPropertyApi(knex, pluginOptions = {}) {
1250
1566
  idProperty: 'publisher_id'
1251
1567
  });
1252
1568
  await api.resources.publishers.createKnexTable();
1569
+ mapTable(`${tablePrefix}_publishers`, 'publishers');
1253
1570
 
1254
1571
  // Authors table with custom idProperty
1255
1572
  await api.addResource('authors', {
@@ -1259,9 +1576,10 @@ export async function createCustomIdPropertyApi(knex, pluginOptions = {}) {
1259
1576
  country_id: { type: 'number', nullable: true, belongsTo: 'countries', as: 'country' }
1260
1577
  },
1261
1578
  relationships: {
1262
- books: { hasMany: 'books', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' },
1579
+ books: { type: 'manyToMany', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' },
1263
1580
  reviews: {
1264
- hasMany: 'reviews',
1581
+ type: 'hasMany',
1582
+ target: 'reviews',
1265
1583
  via: 'reviewable'
1266
1584
  }
1267
1585
  },
@@ -1269,6 +1587,7 @@ export async function createCustomIdPropertyApi(knex, pluginOptions = {}) {
1269
1587
  idProperty: 'author_id'
1270
1588
  });
1271
1589
  await api.resources.authors.createKnexTable();
1590
+ mapTable(`${tablePrefix}_authors`, 'authors');
1272
1591
 
1273
1592
  // Books table with custom idProperty
1274
1593
  await api.addResource('books', {
@@ -1281,9 +1600,10 @@ export async function createCustomIdPropertyApi(knex, pluginOptions = {}) {
1281
1600
  publisher_id: { type: 'number', nullable: true, belongsTo: 'publishers', as: 'publisher', search: true }
1282
1601
  },
1283
1602
  relationships: {
1284
- authors: { hasMany: 'authors', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' },
1603
+ authors: { type: 'manyToMany', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' },
1285
1604
  reviews: {
1286
- hasMany: 'reviews',
1605
+ type: 'hasMany',
1606
+ target: 'reviews',
1287
1607
  via: 'reviewable'
1288
1608
  }
1289
1609
  },
@@ -1291,6 +1611,7 @@ export async function createCustomIdPropertyApi(knex, pluginOptions = {}) {
1291
1611
  idProperty: 'book_id'
1292
1612
  });
1293
1613
  await api.resources.books.createKnexTable();
1614
+ mapTable(`${tablePrefix}_books`, 'books');
1294
1615
 
1295
1616
  // Book-Authors pivot table with custom idProperty
1296
1617
  await api.addResource('book_authors', {
@@ -1304,6 +1625,7 @@ export async function createCustomIdPropertyApi(knex, pluginOptions = {}) {
1304
1625
  idProperty: 'book_author_id'
1305
1626
  });
1306
1627
  await api.resources.book_authors.createKnexTable();
1628
+ mapTable(`${tablePrefix}_book_authors`, 'book_authors');
1307
1629
 
1308
1630
  // Polymorphic reviews with custom idProperty
1309
1631
  await api.addResource('reviews', {
@@ -1330,6 +1652,22 @@ export async function createCustomIdPropertyApi(knex, pluginOptions = {}) {
1330
1652
  idProperty: 'review_id'
1331
1653
  });
1332
1654
  await api.resources.reviews.createKnexTable();
1655
+ mapTable(`${tablePrefix}_reviews`, 'reviews');
1656
+
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;
1663
+ storageMode.registerLink(
1664
+ `${tablePrefix}_book_authors`,
1665
+ 'books',
1666
+ 'authors',
1667
+ relationshipKey,
1668
+ inverseRelationshipKey,
1669
+ );
1670
+ }
1333
1671
 
1334
1672
  return api;
1335
1673
  }
@@ -1341,7 +1679,6 @@ export async function createCustomIdPropertyApi(knex, pluginOptions = {}) {
1341
1679
  export async function createCursorPaginationApi(knex) {
1342
1680
  const api = new Api({
1343
1681
  name: 'cursor-pagination-test-api',
1344
- version: '1.0.0'
1345
1682
  });
1346
1683
 
1347
1684
  const restApiOptions = {
@@ -1354,11 +1691,12 @@ export async function createCursorPaginationApi(knex) {
1354
1691
  sortableFields: ['id', 'name', 'category', 'brand', 'price', 'sku', 'created_at', 'code', 'status', 'type'],
1355
1692
  queryDefaultLimit: 20,
1356
1693
  queryMaxLimit: 100,
1357
- publicBaseUrl: 'https://api.example.com/v1'
1694
+ returnBasePath: 'https://api.example.com'
1358
1695
  };
1359
1696
 
1360
1697
  await api.use(RestApiPlugin, restApiOptions);
1361
- await api.use(RestApiKnexPlugin, { knex });
1698
+ await useStoragePlugin(api, knex);
1699
+ await resetAnyApiTables(knex);
1362
1700
 
1363
1701
  // Create products table with fields suitable for multi-field sorting
1364
1702
  await api.addResource('products', {
@@ -1400,7 +1738,6 @@ export async function createVirtualFieldsApi(knex, pluginOptions = {}) {
1400
1738
  const apiName = pluginOptions.apiName || 'virtual-fields-test-api';
1401
1739
  const api = new Api({
1402
1740
  name: apiName,
1403
- version: '1.0.0',
1404
1741
  log: { level: process.env.LOG_LEVEL || 'info' }
1405
1742
  });
1406
1743
 
@@ -1421,7 +1758,8 @@ export async function createVirtualFieldsApi(knex, pluginOptions = {}) {
1421
1758
  };
1422
1759
 
1423
1760
  await api.use(RestApiPlugin, restApiOptions);
1424
- await api.use(RestApiKnexPlugin, { knex });
1761
+ await useStoragePlugin(api, knex);
1762
+ await resetAnyApiTables(knex);
1425
1763
 
1426
1764
  // Add a test resource with virtual fields
1427
1765
  await api.addResource('users', {
@@ -1445,4 +1783,92 @@ export async function createVirtualFieldsApi(knex, pluginOptions = {}) {
1445
1783
  await api.resources.users.createKnexTable();
1446
1784
 
1447
1785
  return api;
1448
- }
1786
+ }
1787
+
1788
+ /**
1789
+ * Creates an API configuration for testing searchSchema merge behavior
1790
+ */
1791
+ export async function createSearchSchemaMergeApi(knex, pluginOptions = {}) {
1792
+ const api = new Api({
1793
+ name: 'searchschema-merge-test-api',
1794
+ log: { level: process.env.LOG_LEVEL || 'info' }
1795
+ });
1796
+
1797
+ await api.use(RestApiPlugin, {
1798
+ simplifiedApi: false,
1799
+ simplifiedTransport: false,
1800
+ returnRecordApi: {
1801
+ post: true,
1802
+ put: false,
1803
+ patch: false
1804
+ },
1805
+ ...pluginOptions['rest-api']
1806
+ });
1807
+
1808
+ await useStoragePlugin(api, knex);
1809
+ await resetAnyApiTables(knex);
1810
+
1811
+ // Test resource with both search:true and searchSchema
1812
+ await api.addResource('products', {
1813
+ schema: {
1814
+ name: { type: 'string', search: true }, // Should be added to searchSchema
1815
+ description: { type: 'string', search: true }, // Should be added to searchSchema
1816
+ price: { type: 'number', search: true }, // Should be overridden by searchSchema
1817
+ category: { type: 'number' }, // Not searchable
1818
+ sku: { type: 'string', search: true }, // Should be added to searchSchema
1819
+ status: { type: 'string' } // Not searchable but added via searchSchema
1820
+ },
1821
+ searchSchema: {
1822
+ // This should override the price field from search:true
1823
+ // 'between' operator requires array type for validation [min, max]
1824
+ price: {
1825
+ type: 'array',
1826
+ filterOperator: 'between'
1827
+ },
1828
+ // This is a virtual field
1829
+ category_name: {
1830
+ type: 'string',
1831
+ actualField: 'category.name',
1832
+ filterOperator: 'like'
1833
+ },
1834
+ // This is an explicit field not marked with search:true
1835
+ // 'in' operator requires array type for validation
1836
+ status: {
1837
+ type: 'array',
1838
+ filterOperator: 'in'
1839
+ }
1840
+ },
1841
+ tableName: 'searchmerge_products'
1842
+ });
1843
+ await api.resources.products.createKnexTable();
1844
+
1845
+ // Test resource with only search:true fields
1846
+ await api.addResource('users', {
1847
+ schema: {
1848
+ username: { type: 'string', search: true },
1849
+ email: { type: 'string', search: true },
1850
+ age: { type: 'number' },
1851
+ bio: { type: 'string', search: { filterOperator: 'like' } }
1852
+ },
1853
+ tableName: 'searchmerge_users'
1854
+ });
1855
+ await api.resources.users.createKnexTable();
1856
+
1857
+ // Test resource with only explicit searchSchema
1858
+ await api.addResource('orders', {
1859
+ schema: {
1860
+ order_number: { type: 'string' },
1861
+ total: { type: 'number' },
1862
+ status: { type: 'string' }
1863
+ },
1864
+ searchSchema: {
1865
+ order_number: { type: 'string', filterOperator: '=' },
1866
+ status: { type: 'array', filterOperator: 'in' } // 'in' operator requires array type
1867
+ },
1868
+ tableName: 'searchmerge_orders'
1869
+ });
1870
+ await api.resources.orders.createKnexTable();
1871
+ mapTable('searchmerge_orders', 'orders');
1872
+
1873
+ return api;
1874
+ }