json-rest-api 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/library-guide-expert.md +29 -0
- package/.claude/agents/rest-api-internals-expert.md +53 -0
- package/CLAUDE.md +139 -1
- package/README.md +2 -5
- package/docTest.js +238 -0
- package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
- package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
- package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
- package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
- package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
- package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
- package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
- package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
- package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
- package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
- package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
- package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
- package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
- package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
- package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
- package/docs/GUIDE/index.md +58 -0
- package/docs/GUIDE.md +61 -2238
- package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
- package/docs/README.md +57 -2239
- package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
- package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
- package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
- package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
- package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
- package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
- package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
- package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
- package/docs/assets/css/style.scss +78 -5
- package/docs/index.md +2 -12
- package/helper-reorganization-plan.md +85 -0
- package/helper-usage-analysis.md +94 -0
- package/package.json +5 -4
- package/plugins/core/bulk-operations-plugin.js +2 -2
- package/plugins/core/connectors/express-plugin.js +99 -33
- package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
- package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
- package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
- package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
- package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
- package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
- package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
- package/plugins/core/lib/querying/knex-window-queries.js +328 -0
- package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
- package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
- package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
- package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
- package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
- package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
- package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
- package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
- package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
- package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
- package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
- package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
- package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
- package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
- package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
- package/plugins/core/lib/writing/relationship-processor.js +252 -0
- package/plugins/core/rest-api-knex-plugin.js +183 -62
- package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
- package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
- package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
- package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
- package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
- package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
- package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
- package/plugins/core/rest-api-plugin-methods/common.js +605 -0
- package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
- package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
- package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
- package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
- package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
- package/plugins/core/rest-api-plugin-methods/get.js +187 -0
- package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
- package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
- package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
- package/plugins/core/rest-api-plugin-methods/post.js +157 -0
- package/plugins/core/rest-api-plugin-methods/put.js +257 -0
- package/plugins/core/rest-api-plugin-methods/query.js +208 -0
- package/plugins/core/rest-api-plugin.js +108 -2498
- package/plugins/core/socketio-plugin.js +28 -5
- package/plugins/storage/local-storage.js +2 -2
- package/tests/computed-fields.test.js +483 -0
- package/tests/cors.test.js +4 -4
- package/tests/curl.test.js +5 -4
- package/tests/custom-idproperty-relationships.test.js +891 -0
- package/tests/field-getters.test.js +353 -0
- package/tests/field-setters.test.js +310 -0
- package/tests/fixtures/api-configs.js +800 -28
- package/tests/jwt-auth.test.js +0 -2
- package/tests/nested-includes.test.js +1 -1
- package/tests/pagination-cursor-multifield.test.js +514 -0
- package/tests/pagination-enhanced.test.js +12 -9
- package/tests/polymorphic-relationships.test.js +10 -8
- package/tests/polymorphic-simple.test.js +6 -4
- package/tests/positioning-advanced.test.js +12 -12
- package/tests/positioning-extended.test.js +25 -25
- package/tests/positioning-multifilter.test.js +24 -24
- package/tests/positioning.test.js +3 -3
- package/tests/query-limits.test.js +24 -8
- package/tests/relationship-endpoints.test.js +4 -5
- package/tests/relationships.test.js +0 -1
- package/tests/socketio.test.js +12 -1
- package/tests/virtual-fields.test.js +13 -13
- package/PLAN_FOR_WEBSOCKETS.md +0 -398
- package/README_1_Initial_Setup.md +0 -393
- package/README_X_Hooks.md +0 -647
- package/TODO_rest-api-plugin-extras.todo +0 -23
- package/connectors_ideas/express-plugin.js +0 -120
- package/connectors_ideas/http-plugin.js +0 -138
- package/docs/examples/express-plugin.js +0 -366
- package/example.js +0 -301
- package/plugins/core/HELPER_MODULES.md +0 -285
- package/plugins/core/lib/compile-schemas.js +0 -245
- package/plugins/core/lib/database-value-normalizers.js +0 -192
- package/plugins/core/lib/default-data-helpers.js +0 -100
- package/plugins/core/lib/knex-cross-table-search.js +0 -442
- package/plugins/core/lib/knex-json-api-helpers.js +0 -84
- package/plugins/core/lib/knex-json-api-transformers.js +0 -449
- package/plugins/core/lib/knex-pagination-helpers.js +0 -193
- package/plugins/core/lib/knex-window-queries.js +0 -142
- package/plugins/core/lib/many-to-many-manipulations.js +0 -343
- package/plugins/core/lib/relationship-processor.js +0 -278
- package/plugins/core/lib/schema-helpers.js +0 -343
- package/plugins/core/lib/scope-validations.js +0 -252
- package/plugins/core/rest-api-relationships-plugin.js +0 -700
- package/plugins/core/utils/knex-query-helpers-base.js +0 -56
- /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
- /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
- /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
- /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
- /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
- /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
- /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
- /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
- /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
- /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
- /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
- /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
- /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
- /package/{readmeSnippets → docs/README_VARIOUS}/test_put_fields_without_defaults.js +0 -0
|
@@ -1,38 +1,51 @@
|
|
|
1
1
|
import { requirePackage } from 'hooked-api';
|
|
2
2
|
import { createSchema, createKnexTable } from 'json-rest-schema';
|
|
3
|
-
import { createCrossTableSearchHelpers } from './lib/knex-cross-table-search.js';
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
3
|
+
import { createCrossTableSearchHelpers } from './lib/querying/knex-cross-table-search.js';
|
|
4
|
+
import { buildFieldSelection, isNonDatabaseField } from './lib/querying-writing/knex-field-helpers.js';
|
|
5
|
+
import { getForeignKeyFields } from './lib/querying-writing/field-utils.js';
|
|
6
|
+
import { buildQuerySelection } from './lib/querying/knex-query-helpers-base.js';
|
|
7
|
+
import { toJsonApiRecord, buildJsonApiResponse } from './lib/querying/knex-json-api-transformers-querying.js';
|
|
8
|
+
import { processBelongsToRelationships } from './lib/writing/knex-json-api-transformers-writing.js';
|
|
9
|
+
import { toJsonApiRecordWithBelongsTo } from './lib/querying-writing/knex-json-api-transformers.js';
|
|
10
|
+
import { processIncludes } from './lib/querying/knex-process-includes.js';
|
|
11
|
+
import { loadRelationshipIdentifiers } from './lib/querying/knex-relationship-includes.js';
|
|
8
12
|
import {
|
|
9
13
|
polymorphicFiltersHook,
|
|
10
14
|
crossTableFiltersHook,
|
|
11
15
|
basicFiltersHook
|
|
12
|
-
} from './lib/knex-query-helpers.js';
|
|
16
|
+
} from './lib/querying/knex-query-helpers.js';
|
|
13
17
|
import { RestApiResourceError, RestApiValidationError } from '../../lib/rest-api-errors.js';
|
|
14
|
-
import { supportsWindowFunctions, getDatabaseInfo } from './lib/database-capabilities.js';
|
|
15
|
-
import { ERROR_SUBTYPES, DEFAULT_QUERY_LIMIT, DEFAULT_MAX_QUERY_LIMIT } from './
|
|
18
|
+
import { supportsWindowFunctions, getDatabaseInfo } from './lib/querying-writing/database-capabilities.js';
|
|
19
|
+
import { ERROR_SUBTYPES, DEFAULT_QUERY_LIMIT, DEFAULT_MAX_QUERY_LIMIT } from './lib/querying-writing/knex-constants.js';
|
|
16
20
|
import {
|
|
17
21
|
calculatePaginationMeta,
|
|
18
22
|
generatePaginationLinks,
|
|
19
23
|
generateCursorPaginationLinks,
|
|
20
24
|
buildCursorMeta,
|
|
21
25
|
parseCursor
|
|
22
|
-
} from './lib/knex-pagination-helpers.js';
|
|
26
|
+
} from './lib/querying/knex-pagination-helpers.js';
|
|
23
27
|
|
|
24
28
|
/**
|
|
25
29
|
* Strips non-database fields (computed and virtual) from attributes before database operations
|
|
26
30
|
* @param {Object} attributes - The attributes object
|
|
27
|
-
* @param {Object} schemaInfo - The schema info containing computed and
|
|
31
|
+
* @param {Object} schemaInfo - The schema info containing computed field definitions and schema structure
|
|
28
32
|
* @returns {Object} Attributes with computed and virtual fields removed
|
|
29
33
|
*/
|
|
30
34
|
const stripNonDatabaseFields = (attributes, schemaInfo) => {
|
|
31
35
|
if (!attributes || !schemaInfo) return attributes || {};
|
|
32
36
|
|
|
33
|
-
const { computed = {},
|
|
37
|
+
const { computed = {}, schemaStructure = {} } = schemaInfo;
|
|
34
38
|
return Object.entries(attributes)
|
|
35
|
-
.filter(([key]) =>
|
|
39
|
+
.filter(([key]) => {
|
|
40
|
+
// Remove computed fields
|
|
41
|
+
if (key in computed) return false;
|
|
42
|
+
// Remove fields marked as virtual in the schema
|
|
43
|
+
const fieldDef = schemaStructure[key];
|
|
44
|
+
if (fieldDef && fieldDef.virtual === true) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
})
|
|
36
49
|
.reduce((acc, [key, val]) => ({ ...acc, [key]: val }), {});
|
|
37
50
|
};
|
|
38
51
|
|
|
@@ -113,8 +126,22 @@ export const RestApiKnexPlugin = {
|
|
|
113
126
|
api.knex.helpers.crossTableSearch = crossTableSearchHelpers;
|
|
114
127
|
|
|
115
128
|
// Helper scope method to get all schema-related information
|
|
116
|
-
addScopeMethod('createKnexTable', async ({ vars, scope, scopeName, scopeOptions, runHooks }) => {
|
|
117
|
-
|
|
129
|
+
addScopeMethod('createKnexTable', async ({ vars, scope, scopeName, scopeOptions, runHooks }) => {
|
|
130
|
+
// Create a filtered schema that excludes virtual fields
|
|
131
|
+
const schemaStructure = vars.schemaInfo.schemaStructure || {};
|
|
132
|
+
const filteredSchema = {};
|
|
133
|
+
|
|
134
|
+
// Copy only non-virtual fields to the filtered schema
|
|
135
|
+
Object.entries(schemaStructure).forEach(([fieldName, fieldDef]) => {
|
|
136
|
+
if (!fieldDef.virtual) {
|
|
137
|
+
filteredSchema[fieldName] = fieldDef;
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Create schema object from filtered fields
|
|
142
|
+
const tableSchema = createSchema(filteredSchema);
|
|
143
|
+
|
|
144
|
+
await createKnexTable(api.knex.instance, vars.schemaInfo.tableName, tableSchema, vars.schemaInfo.idProperty)
|
|
118
145
|
})
|
|
119
146
|
|
|
120
147
|
helpers.newTransaction = async () => {
|
|
@@ -149,9 +176,12 @@ export const RestApiKnexPlugin = {
|
|
|
149
176
|
|
|
150
177
|
log.debug(`[Knex] EXISTS ${tableName}/${id}`);
|
|
151
178
|
|
|
179
|
+
// Select with alias if idProperty is not 'id'
|
|
180
|
+
const selectClause = idProperty !== 'id' ? `${idProperty} as id` : 'id';
|
|
181
|
+
|
|
152
182
|
const record = await db(tableName)
|
|
153
183
|
.where(idProperty, id)
|
|
154
|
-
.select(
|
|
184
|
+
.select(selectClause)
|
|
155
185
|
.first();
|
|
156
186
|
|
|
157
187
|
return !!record;
|
|
@@ -231,8 +261,11 @@ export const RestApiKnexPlugin = {
|
|
|
231
261
|
);
|
|
232
262
|
}
|
|
233
263
|
|
|
234
|
-
//
|
|
264
|
+
// Load relationship identifiers for all hasMany relationships
|
|
235
265
|
const records = [record]; // Wrap in array for processing
|
|
266
|
+
await loadRelationshipIdentifiers(records, scopeName, scopes, db);
|
|
267
|
+
|
|
268
|
+
// Process includes
|
|
236
269
|
const included = await processIncludes(scope, records, {
|
|
237
270
|
log,
|
|
238
271
|
scopes,
|
|
@@ -241,7 +274,7 @@ export const RestApiKnexPlugin = {
|
|
|
241
274
|
});
|
|
242
275
|
|
|
243
276
|
// Build and return response
|
|
244
|
-
return buildJsonApiResponse(scope, records, included, true, scopeName);
|
|
277
|
+
return buildJsonApiResponse(scope, records, included, true, scopeName, context);
|
|
245
278
|
};
|
|
246
279
|
|
|
247
280
|
/**
|
|
@@ -268,7 +301,12 @@ export const RestApiKnexPlugin = {
|
|
|
268
301
|
|
|
269
302
|
// Build query - no filtering hooks for single records
|
|
270
303
|
// Permission checks will handle access control
|
|
271
|
-
|
|
304
|
+
let query = db(tableName).where(idProperty, id);
|
|
305
|
+
|
|
306
|
+
// Add alias if idProperty is not 'id'
|
|
307
|
+
if (idProperty !== 'id') {
|
|
308
|
+
query = query.select('*', `${idProperty} as id`);
|
|
309
|
+
}
|
|
272
310
|
|
|
273
311
|
// Execute query
|
|
274
312
|
const record = await query.first();
|
|
@@ -337,6 +375,7 @@ export const RestApiKnexPlugin = {
|
|
|
337
375
|
|
|
338
376
|
// Start building query with table prefix (for JOIN support)
|
|
339
377
|
let query = db(tableName);
|
|
378
|
+
|
|
340
379
|
query = buildQuerySelection(query, tableName, fieldSelectionInfo.fieldsToSelect, true);
|
|
341
380
|
|
|
342
381
|
/* ═══════════════════════════════════════════════════════════════════
|
|
@@ -430,6 +469,9 @@ export const RestApiKnexPlugin = {
|
|
|
430
469
|
// Fetch one extra record to determine if there are more
|
|
431
470
|
query.limit(pageSize + 1);
|
|
432
471
|
|
|
472
|
+
// Get the configured ID property
|
|
473
|
+
const idProperty = context.schemaInfo.idProperty || 'id';
|
|
474
|
+
|
|
433
475
|
if (queryParams.page.after) {
|
|
434
476
|
let cursorData;
|
|
435
477
|
try {
|
|
@@ -447,28 +489,58 @@ export const RestApiKnexPlugin = {
|
|
|
447
489
|
}
|
|
448
490
|
);
|
|
449
491
|
}
|
|
450
|
-
// Add where conditions for cursor
|
|
451
|
-
// Use the actual sort from query params or default
|
|
452
|
-
let sortField = 'id';
|
|
453
|
-
let sortDirection = 'ASC';
|
|
454
492
|
|
|
493
|
+
// Build sort fields array with directions
|
|
494
|
+
const sortFields = [];
|
|
455
495
|
if (queryParams.sort && queryParams.sort.length > 0) {
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
496
|
+
queryParams.sort.forEach(sortField => {
|
|
497
|
+
const desc = sortField.startsWith('-');
|
|
498
|
+
const field = desc ? sortField.substring(1) : sortField;
|
|
499
|
+
sortFields.push({ field, direction: desc ? 'DESC' : 'ASC' });
|
|
500
|
+
});
|
|
459
501
|
} else if (scope.vars.defaultSort) {
|
|
460
|
-
|
|
461
|
-
|
|
502
|
+
sortFields.push({
|
|
503
|
+
field: scope.vars.defaultSort.field || idProperty,
|
|
504
|
+
direction: scope.vars.defaultSort.direction || 'ASC'
|
|
505
|
+
});
|
|
506
|
+
} else {
|
|
507
|
+
sortFields.push({ field: idProperty, direction: 'ASC' });
|
|
462
508
|
}
|
|
463
509
|
|
|
464
|
-
//
|
|
465
|
-
|
|
510
|
+
// Build compound WHERE clause for multi-field cursor
|
|
511
|
+
query.where(function() {
|
|
512
|
+
sortFields.forEach((sortInfo, index) => {
|
|
513
|
+
const { field, direction } = sortInfo;
|
|
514
|
+
const qualifiedField = `${tableName}.${field}`;
|
|
515
|
+
const cursorValue = cursorData[field];
|
|
516
|
+
|
|
517
|
+
if (cursorValue === undefined) {
|
|
518
|
+
log.warn(`Cursor missing value for sort field: ${field}`);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Build condition for this level and all previous levels
|
|
523
|
+
this.orWhere(function() {
|
|
524
|
+
// All previous fields must be equal
|
|
525
|
+
for (let i = 0; i < index; i++) {
|
|
526
|
+
const prevField = sortFields[i].field;
|
|
527
|
+
const prevQualifiedField = `${tableName}.${prevField}`;
|
|
528
|
+
const prevValue = cursorData[prevField];
|
|
529
|
+
if (prevValue !== undefined) {
|
|
530
|
+
this.where(prevQualifiedField, '=', prevValue);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Current field must be greater/less than cursor value
|
|
535
|
+
if (direction === 'DESC') {
|
|
536
|
+
this.where(qualifiedField, '<', cursorValue);
|
|
537
|
+
} else {
|
|
538
|
+
this.where(qualifiedField, '>', cursorValue);
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
});
|
|
466
543
|
|
|
467
|
-
if (sortDirection === 'DESC') {
|
|
468
|
-
query.where(qualifiedField, '<', cursorData[sortField]);
|
|
469
|
-
} else {
|
|
470
|
-
query.where(qualifiedField, '>', cursorData[sortField]);
|
|
471
|
-
}
|
|
472
544
|
} else if (queryParams.page.before) {
|
|
473
545
|
let cursorData;
|
|
474
546
|
try {
|
|
@@ -486,27 +558,57 @@ export const RestApiKnexPlugin = {
|
|
|
486
558
|
}
|
|
487
559
|
);
|
|
488
560
|
}
|
|
489
|
-
// Use the actual sort from query params or default
|
|
490
|
-
let sortField = 'id';
|
|
491
|
-
let sortDirection = 'ASC';
|
|
492
561
|
|
|
562
|
+
// Build sort fields array with directions
|
|
563
|
+
const sortFields = [];
|
|
493
564
|
if (queryParams.sort && queryParams.sort.length > 0) {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
565
|
+
queryParams.sort.forEach(sortField => {
|
|
566
|
+
const desc = sortField.startsWith('-');
|
|
567
|
+
const field = desc ? sortField.substring(1) : sortField;
|
|
568
|
+
sortFields.push({ field, direction: desc ? 'DESC' : 'ASC' });
|
|
569
|
+
});
|
|
497
570
|
} else if (scope.vars.defaultSort) {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
// Ensure the field is properly prefixed when JOINs might be present
|
|
503
|
-
const qualifiedField = `${tableName}.${sortField}`;
|
|
504
|
-
|
|
505
|
-
if (sortDirection === 'DESC') {
|
|
506
|
-
query.where(qualifiedField, '>', cursorData[sortField]);
|
|
571
|
+
sortFields.push({
|
|
572
|
+
field: scope.vars.defaultSort.field || idProperty,
|
|
573
|
+
direction: scope.vars.defaultSort.direction || 'ASC'
|
|
574
|
+
});
|
|
507
575
|
} else {
|
|
508
|
-
|
|
576
|
+
sortFields.push({ field: idProperty, direction: 'ASC' });
|
|
509
577
|
}
|
|
578
|
+
|
|
579
|
+
// Build compound WHERE clause for multi-field cursor (reversed for before)
|
|
580
|
+
query.where(function() {
|
|
581
|
+
sortFields.forEach((sortInfo, index) => {
|
|
582
|
+
const { field, direction } = sortInfo;
|
|
583
|
+
const qualifiedField = `${tableName}.${field}`;
|
|
584
|
+
const cursorValue = cursorData[field];
|
|
585
|
+
|
|
586
|
+
if (cursorValue === undefined) {
|
|
587
|
+
log.warn(`Cursor missing value for sort field: ${field}`);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Build condition for this level and all previous levels
|
|
592
|
+
this.orWhere(function() {
|
|
593
|
+
// All previous fields must be equal
|
|
594
|
+
for (let i = 0; i < index; i++) {
|
|
595
|
+
const prevField = sortFields[i].field;
|
|
596
|
+
const prevQualifiedField = `${tableName}.${prevField}`;
|
|
597
|
+
const prevValue = cursorData[prevField];
|
|
598
|
+
if (prevValue !== undefined) {
|
|
599
|
+
this.where(prevQualifiedField, '=', prevValue);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Current field must be less/greater than cursor value (reversed)
|
|
604
|
+
if (direction === 'DESC') {
|
|
605
|
+
this.where(qualifiedField, '>', cursorValue);
|
|
606
|
+
} else {
|
|
607
|
+
this.where(qualifiedField, '<', cursorValue);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
});
|
|
510
612
|
}
|
|
511
613
|
}
|
|
512
614
|
// Default pagination if only size is specified - treat as cursor-based for "load more"
|
|
@@ -552,7 +654,9 @@ export const RestApiKnexPlugin = {
|
|
|
552
654
|
}
|
|
553
655
|
});
|
|
554
656
|
|
|
555
|
-
|
|
657
|
+
// Initialize returnMeta namespace for thread-safe metadata
|
|
658
|
+
context.returnMeta = context.returnMeta || {};
|
|
659
|
+
context.returnMeta.queryString = queryParts.length > 0 ? `?${queryParts.join('&')}` : '';
|
|
556
660
|
|
|
557
661
|
// Execute count query for pagination if offset-based pagination is used
|
|
558
662
|
if (queryParams.page?.number !== undefined || (queryParams.page?.size !== undefined && !queryParams.page?.after && !queryParams.page?.before)) {
|
|
@@ -585,10 +689,10 @@ export const RestApiKnexPlugin = {
|
|
|
585
689
|
const total = parseInt(countResult.total);
|
|
586
690
|
|
|
587
691
|
// Calculate pagination metadata with total
|
|
588
|
-
|
|
692
|
+
context.returnMeta.paginationMeta = calculatePaginationMeta(total, page, pageSize);
|
|
589
693
|
} else {
|
|
590
694
|
// Without count, we can still provide basic pagination info
|
|
591
|
-
|
|
695
|
+
context.returnMeta.paginationMeta = {
|
|
592
696
|
page,
|
|
593
697
|
pageSize
|
|
594
698
|
// No total, pageCount, or hasMore when counts are disabled
|
|
@@ -596,11 +700,12 @@ export const RestApiKnexPlugin = {
|
|
|
596
700
|
}
|
|
597
701
|
|
|
598
702
|
// Generate links
|
|
599
|
-
scope.vars.
|
|
600
|
-
|
|
703
|
+
const urlPrefix = scope.vars.publicBaseUrl || scope.vars.transport?.mountPath || '';
|
|
704
|
+
context.returnMeta.paginationLinks = generatePaginationLinks(
|
|
705
|
+
urlPrefix,
|
|
601
706
|
scopeName,
|
|
602
707
|
queryParams,
|
|
603
|
-
|
|
708
|
+
context.returnMeta.paginationMeta
|
|
604
709
|
);
|
|
605
710
|
}
|
|
606
711
|
|
|
@@ -620,14 +725,16 @@ export const RestApiKnexPlugin = {
|
|
|
620
725
|
}
|
|
621
726
|
|
|
622
727
|
// Determine sort fields for cursor
|
|
623
|
-
|
|
728
|
+
const idProperty = context.schemaInfo.idProperty || 'id';
|
|
729
|
+
let sortFields = [idProperty];
|
|
624
730
|
if (queryParams.sort && queryParams.sort.length > 0) {
|
|
625
731
|
sortFields = queryParams.sort.map(s => s.startsWith('-') ? s.substring(1) : s);
|
|
626
732
|
}
|
|
627
733
|
|
|
628
|
-
|
|
629
|
-
scope.vars.
|
|
630
|
-
|
|
734
|
+
context.returnMeta.paginationMeta = buildCursorMeta(records, pageSize, hasMore, sortFields);
|
|
735
|
+
const urlPrefix = scope.vars.publicBaseUrl || scope.vars.transport?.mountPath || '';
|
|
736
|
+
context.returnMeta.paginationLinks = generateCursorPaginationLinks(
|
|
737
|
+
urlPrefix,
|
|
631
738
|
scopeName,
|
|
632
739
|
queryParams,
|
|
633
740
|
records,
|
|
@@ -637,6 +744,9 @@ export const RestApiKnexPlugin = {
|
|
|
637
744
|
);
|
|
638
745
|
}
|
|
639
746
|
|
|
747
|
+
// Load relationship identifiers for all hasMany relationships
|
|
748
|
+
await loadRelationshipIdentifiers(records, scopeName, scopes, db);
|
|
749
|
+
|
|
640
750
|
// Process includes
|
|
641
751
|
const included = await processIncludes(scope, records, {
|
|
642
752
|
log,
|
|
@@ -648,7 +758,7 @@ export const RestApiKnexPlugin = {
|
|
|
648
758
|
|
|
649
759
|
|
|
650
760
|
// Build and return response
|
|
651
|
-
return buildJsonApiResponse(scope, records, included, false, scopeName);
|
|
761
|
+
return buildJsonApiResponse(scope, records, included, false, scopeName, context);
|
|
652
762
|
};
|
|
653
763
|
|
|
654
764
|
/**
|
|
@@ -730,7 +840,12 @@ export const RestApiKnexPlugin = {
|
|
|
730
840
|
|
|
731
841
|
// Strip non-database fields (computed and virtual) before database operation
|
|
732
842
|
const finalAttributes = stripNonDatabaseFields(mergedAttributes, context.schemaInfo);
|
|
733
|
-
|
|
843
|
+
|
|
844
|
+
// Map 'id' to actual idProperty if needed (for PUT with specific ID)
|
|
845
|
+
if (idProperty !== 'id' && inputRecord.data.id) {
|
|
846
|
+
finalAttributes[idProperty] = inputRecord.data.id;
|
|
847
|
+
}
|
|
848
|
+
|
|
734
849
|
if (isCreate) {
|
|
735
850
|
// Create mode - insert new record with specified ID
|
|
736
851
|
const recordData = {
|
|
@@ -756,6 +871,9 @@ export const RestApiKnexPlugin = {
|
|
|
756
871
|
);
|
|
757
872
|
}
|
|
758
873
|
|
|
874
|
+
// Remove the idProperty from attributes to prevent updating the primary key
|
|
875
|
+
delete finalAttributes[idProperty];
|
|
876
|
+
|
|
759
877
|
// Update the record (replace all fields)
|
|
760
878
|
if (Object.keys(finalAttributes).length > 0) {
|
|
761
879
|
await db(tableName)
|
|
@@ -823,6 +941,9 @@ export const RestApiKnexPlugin = {
|
|
|
823
941
|
|
|
824
942
|
log.debug(`[Knex] PATCH finalAttributes:`, finalAttributes);
|
|
825
943
|
|
|
944
|
+
// Remove the idProperty from attributes to prevent updating the primary key
|
|
945
|
+
delete finalAttributes[idProperty];
|
|
946
|
+
|
|
826
947
|
// Update only if there are changes
|
|
827
948
|
if (Object.keys(finalAttributes).length > 0) {
|
|
828
949
|
await db(tableName)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { compileSchemas } from '../lib/querying-writing/compile-schemas.js';
|
|
2
|
+
|
|
3
|
+
export default async function compileResourceSchemas({ context, scopes, runHooks }) {
|
|
4
|
+
const scope = scopes[context.scopeName];
|
|
5
|
+
// Pass scopeOptions and vars from context since scope object structure is different
|
|
6
|
+
return compileSchemas({ ...scope, scopeOptions: context.scopeOptions, vars: context.vars }, { context: { scopeName: context.scopeName }, runHooks });
|
|
7
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
|
|
2
|
+
import { parseJsonApiQuery } from "../lib/querying-writing/connectors-query-parser.js";
|
|
3
|
+
|
|
4
|
+
export default async function registerRelationshipRoutes ({ context, api, log }) {
|
|
5
|
+
const { scopeName } = context;
|
|
6
|
+
const basePath = api.scopes[scopeName].vars.transport?.mountPath || '';
|
|
7
|
+
const scopePath = `${basePath}/${scopeName}`;
|
|
8
|
+
|
|
9
|
+
// Helper to create route handlers
|
|
10
|
+
const createRouteHandler = (methodName) => {
|
|
11
|
+
return async ({ params, body, queryString }) => {
|
|
12
|
+
const scope = api.scopes[scopeName];
|
|
13
|
+
|
|
14
|
+
const methodParams = {
|
|
15
|
+
id: params.id,
|
|
16
|
+
relationshipName: params.relationshipName,
|
|
17
|
+
isTransport: true
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Add query params for getRelated
|
|
21
|
+
if (methodName === 'getRelated' && queryString) {
|
|
22
|
+
methodParams.queryParams = parseJsonApiQuery(queryString);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Add body data for write operations
|
|
26
|
+
if (body && body.data !== undefined) {
|
|
27
|
+
methodParams.relationshipData = body.data;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return await scope[methodName](methodParams);
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Register relationship routes
|
|
35
|
+
|
|
36
|
+
// GET /api/{scope}/{id}/relationships/{relationshipName}
|
|
37
|
+
await api.addRoute({
|
|
38
|
+
method: 'GET',
|
|
39
|
+
path: `${scopePath}/:id/relationships/:relationshipName`,
|
|
40
|
+
handler: createRouteHandler('getRelationship')
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// GET /api/{scope}/{id}/{relationshipName}
|
|
44
|
+
await api.addRoute({
|
|
45
|
+
method: 'GET',
|
|
46
|
+
path: `${scopePath}/:id/:relationshipName`,
|
|
47
|
+
handler: createRouteHandler('getRelated')
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// POST /api/{scope}/{id}/relationships/{relationshipName}
|
|
51
|
+
await api.addRoute({
|
|
52
|
+
method: 'POST',
|
|
53
|
+
path: `${scopePath}/:id/relationships/:relationshipName`,
|
|
54
|
+
handler: createRouteHandler('postRelationship')
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// PATCH /api/{scope}/{id}/relationships/{relationshipName}
|
|
58
|
+
await api.addRoute({
|
|
59
|
+
method: 'PATCH',
|
|
60
|
+
path: `${scopePath}/:id/relationships/:relationshipName`,
|
|
61
|
+
handler: createRouteHandler('patchRelationship')
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// DELETE /api/{scope}/{id}/relationships/{relationshipName}
|
|
65
|
+
await api.addRoute({
|
|
66
|
+
method: 'DELETE',
|
|
67
|
+
path: `${scopePath}/:id/relationships/:relationshipName`,
|
|
68
|
+
handler: createRouteHandler('deleteRelationship')
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
log.trace(`Registered relationship routes for scope: ${scopeName}`);
|
|
72
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { RestApiResourceError } from '../../../lib/rest-api-errors.js';
|
|
2
|
+
import { parseJsonApiQuery } from '../lib/querying-writing/connectors-query-parser.js';
|
|
3
|
+
|
|
4
|
+
export default async function registerScopeRoutes({ context, api, vars, log }) {
|
|
5
|
+
const { scopeName } = context;
|
|
6
|
+
const basePath = vars.transport?.mountPath || '';
|
|
7
|
+
|
|
8
|
+
// Helper to create route handlers
|
|
9
|
+
const createRouteHandler = (scopeName, methodName) => {
|
|
10
|
+
return async ({ queryString, headers, params, body, context }) => {
|
|
11
|
+
const scope = api.scopes[scopeName];
|
|
12
|
+
if (!scope) {
|
|
13
|
+
throw new RestApiResourceError(
|
|
14
|
+
`Scope '${scopeName}' not found`,
|
|
15
|
+
{
|
|
16
|
+
subtype: 'not_found',
|
|
17
|
+
resourceType: 'scope',
|
|
18
|
+
resourceId: scopeName
|
|
19
|
+
}
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Build parameters for the scope method
|
|
24
|
+
const methodParams = {};
|
|
25
|
+
|
|
26
|
+
// Add ID for single-resource operations
|
|
27
|
+
if (['get', 'put', 'patch', 'delete'].includes(methodName)) {
|
|
28
|
+
methodParams.id = params.id;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Parse query parameters for read operations
|
|
32
|
+
if (['query', 'get'].includes(methodName)) {
|
|
33
|
+
methodParams.queryParams = parseJsonApiQuery(queryString);
|
|
34
|
+
methodParams.isTransport = true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Add body for write operations
|
|
38
|
+
if (['post', 'put', 'patch'].includes(methodName)) {
|
|
39
|
+
methodParams.inputRecord = body;
|
|
40
|
+
methodParams.isTransport = true;
|
|
41
|
+
|
|
42
|
+
// Add query params for includes/fields on write operations
|
|
43
|
+
if (queryString) {
|
|
44
|
+
methodParams.queryParams = parseJsonApiQuery(queryString);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Call the scope method
|
|
49
|
+
const result = await scope[methodName](methodParams, context);
|
|
50
|
+
|
|
51
|
+
// Return the result (transport plugin handles response formatting)
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const scopePath = `${basePath}/${scopeName}`;
|
|
57
|
+
|
|
58
|
+
// Register routes for each HTTP method
|
|
59
|
+
// GET /api/{scope} - Query collection
|
|
60
|
+
await api.addRoute({
|
|
61
|
+
method: 'GET',
|
|
62
|
+
path: scopePath,
|
|
63
|
+
handler: createRouteHandler(scopeName, 'query')
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// GET /api/{scope}/{id} - Get single resource
|
|
67
|
+
await api.addRoute({
|
|
68
|
+
method: 'GET',
|
|
69
|
+
path: `${scopePath}/:id`,
|
|
70
|
+
handler: createRouteHandler(scopeName, 'get')
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// POST /api/{scope} - Create resource
|
|
74
|
+
await api.addRoute({
|
|
75
|
+
method: 'POST',
|
|
76
|
+
path: scopePath,
|
|
77
|
+
handler: createRouteHandler(scopeName, 'post')
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// PUT /api/{scope}/{id} - Replace resource
|
|
81
|
+
await api.addRoute({
|
|
82
|
+
method: 'PUT',
|
|
83
|
+
path: `${scopePath}/:id`,
|
|
84
|
+
handler: createRouteHandler(scopeName, 'put')
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// PATCH /api/{scope}/{id} - Update resource
|
|
88
|
+
await api.addRoute({
|
|
89
|
+
method: 'PATCH',
|
|
90
|
+
path: `${scopePath}/:id`,
|
|
91
|
+
handler: createRouteHandler(scopeName, 'patch')
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// DELETE /api/{scope}/{id} - Delete resource
|
|
95
|
+
await api.addRoute({
|
|
96
|
+
method: 'DELETE',
|
|
97
|
+
path: `${scopePath}/:id`,
|
|
98
|
+
handler: createRouteHandler(scopeName, 'delete')
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
log.info(`Routes registered for scope '${scopeName}'`);
|
|
102
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export default async function turnScopeInitIntoVars({ context, scopes, vars: apiVars }) {
|
|
2
|
+
// Refer to the scope's vars
|
|
3
|
+
const scope = scopes[context.scopeName];
|
|
4
|
+
const scopeOptions = scope?.scopeOptions || context.scopeOptions || {};
|
|
5
|
+
const vars = scope?.vars || apiVars;
|
|
6
|
+
|
|
7
|
+
// The scope-specific ones
|
|
8
|
+
vars.sortableFields = scopeOptions.sortableFields || [];
|
|
9
|
+
vars.defaultSort = scopeOptions.defaultSort || null;
|
|
10
|
+
|
|
11
|
+
// The general ones that are also set at api level, but overrideable
|
|
12
|
+
if (typeof scopeOptions.queryDefaultLimit !== 'undefined') vars.queryDefaultLimit = scopeOptions.queryDefaultLimit;
|
|
13
|
+
if (typeof scopeOptions.queryMaxLimit !== 'undefined') vars.queryMaxLimit = scopeOptions.queryMaxLimit;
|
|
14
|
+
if (typeof scopeOptions.includeDepthLimit !== 'undefined') vars.includeDepthLimit = scopeOptions.includeDepthLimit;
|
|
15
|
+
if (typeof scopeOptions.publicBaseUrl !== 'undefined') vars.publicBaseUrl = scopeOptions.publicBaseUrl;
|
|
16
|
+
if (typeof scopeOptions.enablePaginationCounts !== 'undefined') vars.enablePaginationCounts = scopeOptions.enablePaginationCounts;
|
|
17
|
+
|
|
18
|
+
// Set simplified settings as scope vars
|
|
19
|
+
if (typeof scopeOptions.simplifiedApi !== 'undefined') vars.simplifiedApi = scopeOptions.simplifiedApi;
|
|
20
|
+
if (typeof scopeOptions.simplifiedTransport !== 'undefined') vars.simplifiedTransport = scopeOptions.simplifiedTransport;
|
|
21
|
+
|
|
22
|
+
// Set returnRecord settings as scope vars
|
|
23
|
+
if (typeof scopeOptions.returnRecordApi !== 'undefined') vars.returnRecordApi = scopeOptions.returnRecordApi;
|
|
24
|
+
if (typeof scopeOptions.returnRecordTransport !== 'undefined') vars.returnRecordTransport = scopeOptions.returnRecordTransport;
|
|
25
|
+
|
|
26
|
+
// Set idProperty as scope var
|
|
27
|
+
if (typeof scopeOptions.idProperty !== 'undefined') vars.idProperty = scopeOptions.idProperty;
|
|
28
|
+
|
|
29
|
+
// Add validation for query limits
|
|
30
|
+
if (vars.queryDefaultLimit && vars.queryMaxLimit) {
|
|
31
|
+
if (vars.queryDefaultLimit > vars.queryMaxLimit) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Invalid scope '${context.scopeName}' configuration: ` +
|
|
34
|
+
`queryDefaultLimit (${vars.queryDefaultLimit}) cannot exceed queryMaxLimit (${vars.queryMaxLimit})`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Validate relationship include limits at scope creation time
|
|
40
|
+
Object.entries(scopeOptions.relationships || {}).forEach(([relName, relDef]) => {
|
|
41
|
+
if (relDef.include?.limit && vars.queryMaxLimit) {
|
|
42
|
+
if (relDef.include.limit > vars.queryMaxLimit) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Invalid relationship '${context.scopeName}.${relName}' configuration: ` +
|
|
45
|
+
`include.limit (${relDef.include.limit}) cannot exceed queryMaxLimit (${vars.queryMaxLimit})`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
}
|