json-rest-api 1.0.10 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +9 -1
- package/AGENTS.md +34 -0
- package/CLAUDE.md +26 -1
- package/CRUSH.md +55 -0
- package/README.md +3 -3
- package/TODO.todo +8 -0
- package/docs/API.md +17 -18
- package/docs/COMPARISON.md +18 -13
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +17 -21
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +33 -2
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +61 -7
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +58 -61
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +37 -37
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +103 -87
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +37 -34
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +45 -42
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +3 -3
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +1 -1
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +5 -5
- package/docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md +498 -755
- package/docs/GUIDE/GUIDE_6_Positioning.md +1 -3
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +4 -5
- package/docs/GUIDE/GUIDE_X_Bulk_Operations.md +15 -6
- package/docs/GUIDE/GUIDE_X_Cors.md +2 -5
- package/docs/GUIDE/GUIDE_X_File_Uploads.md +2 -3
- package/docs/GUIDE/GUIDE_X_Multihome.md +1 -4
- package/docs/GUIDE/GUIDE_X_SocketIO.md +0 -2
- package/docs/GUIDE/GUIDE_X_URL_Management.md +317 -0
- package/docs/GUIDE/index.md +3 -0
- package/docs/QUICKSTART.md +12 -12
- package/docs/index.md +2 -2
- package/docs/mini-guides/file-uploads-complete.md +0 -1
- package/examples/url-override-example.js +137 -0
- package/index.js +7 -1
- package/package.json +5 -9
- package/plugins/core/bulk-operations-plugin.js +2 -0
- package/plugins/core/connectors/express-plugin.js +43 -24
- package/plugins/core/connectors/lib/request-helpers.js +2 -2
- package/plugins/core/lib/anyapi/anyapi-registry.js +821 -0
- package/plugins/core/lib/anyapi/query/anyapi-query-adapter.js +647 -0
- package/plugins/core/lib/anyapi/schema-utils.js +186 -0
- package/plugins/core/lib/anyapi/utils/descriptor-helpers.js +113 -0
- package/plugins/core/lib/dbTablesOperations.js +305 -0
- package/plugins/core/lib/querying/knex-cross-table-search.js +64 -108
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +21 -27
- package/plugins/core/lib/querying/knex-pagination-helpers.js +36 -20
- package/plugins/core/lib/querying/knex-query-helpers-base.js +49 -14
- package/plugins/core/lib/querying/knex-query-helpers.js +378 -222
- package/plugins/core/lib/querying/knex-relationship-includes.js +262 -87
- package/plugins/core/lib/querying/url-helpers.js +119 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +36 -28
- package/plugins/core/lib/querying-writing/database-capabilities.js +10 -9
- package/plugins/core/lib/querying-writing/knex-field-helpers.js +9 -3
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +2 -2
- package/plugins/core/lib/querying-writing/payload-validators.js +10 -16
- package/plugins/core/lib/querying-writing/schema-helpers.js +52 -60
- package/plugins/core/lib/querying-writing/scope-validations.js +78 -22
- package/plugins/core/lib/querying-writing/simplified-helpers.js +208 -61
- package/plugins/core/lib/storage/storage-adapter.js +160 -0
- package/plugins/core/lib/writing/relationship-processor.js +4 -11
- package/plugins/core/rest-api-access.js +456 -0
- package/plugins/core/rest-api-anyapi-knex-plugin.js +2285 -0
- package/plugins/core/rest-api-knex-plugin.js +226 -87
- package/plugins/core/rest-api-multihome-plugin.js +89 -27
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +0 -1
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +1 -5
- package/plugins/core/rest-api-plugin-methods/common.js +46 -10
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +31 -27
- package/plugins/core/rest-api-plugin-methods/delete.js +1 -4
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +13 -1
- package/plugins/core/rest-api-plugin-methods/get-related.js +114 -56
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +4 -3
- package/plugins/core/rest-api-plugin-methods/get.js +2 -5
- package/plugins/core/rest-api-plugin-methods/patch.js +15 -16
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +21 -14
- package/plugins/core/rest-api-plugin-methods/post.js +23 -11
- package/plugins/core/rest-api-plugin-methods/put.js +18 -21
- package/plugins/core/rest-api-plugin-methods/query.js +3 -4
- package/plugins/core/rest-api-plugin-methods/release.js +13 -0
- package/plugins/core/rest-api-plugin.js +3 -4
- package/plugins/core/rest-api-positioning-plugin.js +253 -76
- package/plugins/core/socketio-plugin.js +436 -502
- package/quickTest.js +2 -2
- package/scripts/debug-anyapi-review-filter.mjs +63 -0
- package/scripts/inspect-anyapi-descriptor-country.mjs +19 -0
- package/scripts/inspect-anyapi-descriptor.mjs +19 -0
- package/test-debug.js +59 -0
- package/tests/README.md +7 -1
- package/tests/anyapi-basic.test.js +102 -0
- package/tests/anyapi-cursor-pagination.test.js +144 -0
- package/tests/computed-fields.test.js +27 -12
- package/tests/cors.test.js +5 -6
- package/tests/curl.test.js +1 -1
- package/tests/custom-idproperty-relationships.test.js +37 -29
- package/tests/field-getters.test.js +25 -7
- package/tests/fixtures/api-configs.js +576 -150
- package/tests/helpers/storage-mode.js +65 -0
- package/tests/helpers/test-utils.js +85 -3
- package/tests/include-depth-validation.test.js +35 -1
- package/tests/nested-includes.test.js +130 -103
- package/tests/pagination-cursor-multifield.test.js +17 -13
- package/tests/pagination-enhanced.test.js +19 -24
- package/tests/polymorphic-simple.test.js +9 -9
- package/tests/positioning-advanced.test.js +19 -19
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +62 -62
- package/tests/positioning.test.js +7 -7
- package/tests/queries.test.js +5 -5
- package/tests/query-limits.test.js +77 -92
- package/tests/rest-api-access.test.js +206 -0
- package/tests/searchschema-merge.test.js +182 -0
- package/tests/socketio.test.js +33 -14
- package/tests/virtual-fields.test.js +0 -1
- package/docs/DOCS_ALL.md +0 -21894
- package/plugins/core/jwt-auth-plugin.js +0 -897
- package/plugins/core/lib/jwt-auth-helpers.js +0 -130
- 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
|
|
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: {
|
|
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: {
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
357
|
+
await withTenantContext(tenantId, async () => {
|
|
358
|
+
await useStoragePlugin(api, knex, { tenantId });
|
|
359
|
+
await resetAnyApiTables(knex);
|
|
167
360
|
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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: {
|
|
414
|
+
books: { type: 'manyToMany', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' },
|
|
219
415
|
reviews: {
|
|
220
|
-
|
|
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: {
|
|
440
|
+
authors: { type: 'manyToMany', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' },
|
|
243
441
|
reviews: {
|
|
244
|
-
|
|
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
|
-
|
|
531
|
+
await withTenantContext(tenantId, async () => {
|
|
532
|
+
await useStoragePlugin(api, knex, { tenantId });
|
|
323
533
|
|
|
324
|
-
|
|
325
|
-
|
|
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: {
|
|
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: {
|
|
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
|
|
638
|
+
...options // Allow overriding options likereturnBasePath, enablePaginationCounts
|
|
417
639
|
};
|
|
418
640
|
|
|
419
641
|
await api.use(RestApiPlugin, restApiOptions);
|
|
420
|
-
|
|
421
|
-
|
|
642
|
+
await withTenantContext(tenantId, async () => {
|
|
643
|
+
await useStoragePlugin(api, knex, { tenantId });
|
|
422
644
|
|
|
423
|
-
|
|
424
|
-
|
|
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 {
|
|
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
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
type: 'number',
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
549
|
-
type: '
|
|
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
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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: {
|
|
1579
|
+
books: { type: 'manyToMany', through: 'book_authors', foreignKey: 'author_id', otherKey: 'book_id' },
|
|
1263
1580
|
reviews: {
|
|
1264
|
-
|
|
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: {
|
|
1603
|
+
authors: { type: 'manyToMany', through: 'book_authors', foreignKey: 'book_id', otherKey: 'author_id' },
|
|
1285
1604
|
reviews: {
|
|
1286
|
-
|
|
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
|
-
|
|
1694
|
+
returnBasePath: 'https://api.example.com'
|
|
1358
1695
|
};
|
|
1359
1696
|
|
|
1360
1697
|
await api.use(RestApiPlugin, restApiOptions);
|
|
1361
|
-
await api
|
|
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
|
|
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
|
+
}
|