json-rest-api 1.0.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +90 -32
- 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
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { RestApiResourceError } from '../../../../lib/rest-api-errors.js';
|
|
2
|
+
import { ROW_NUMBER_KEY, DEFAULT_QUERY_LIMIT, DEFAULT_MAX_QUERY_LIMIT, DEFAULT_MAX_INCLUDE_LIMIT } from '../querying-writing/knex-constants.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Builds a window function query for limited includes per parent record
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} knex - Knex instance
|
|
8
|
+
* @param {string} tableName - Target table name
|
|
9
|
+
* @param {string} foreignKey - Foreign key field
|
|
10
|
+
* @param {Array} parentIds - Parent record IDs
|
|
11
|
+
* @param {Array} fieldsToSelect - Fields to select
|
|
12
|
+
* @param {Object} includeConfig - Include configuration (limit, orderBy)
|
|
13
|
+
* @param {Object} capabilities - Database capabilities
|
|
14
|
+
* @param {Object} scopeVars - Scope variables for defaults and limits
|
|
15
|
+
* @returns {Object} Knex query
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // Input: Load max 3 comments per article, ordered by newest first
|
|
19
|
+
* const query = buildWindowedIncludeQuery(
|
|
20
|
+
* knex,
|
|
21
|
+
* 'comments',
|
|
22
|
+
* 'article_id',
|
|
23
|
+
* [1, 2, 3], // Article IDs
|
|
24
|
+
* ['id', 'text', 'created_at'],
|
|
25
|
+
* { limit: 3, orderBy: ['-created_at'] },
|
|
26
|
+
* { windowFunctions: true },
|
|
27
|
+
* { queryDefaultLimit: 10 }
|
|
28
|
+
* );
|
|
29
|
+
*
|
|
30
|
+
* // Generated SQL:
|
|
31
|
+
* // WITH _windowed AS (
|
|
32
|
+
* // SELECT id, text, created_at, article_id,
|
|
33
|
+
* // ROW_NUMBER() OVER (
|
|
34
|
+
* // PARTITION BY article_id
|
|
35
|
+
* // ORDER BY created_at DESC
|
|
36
|
+
* // ) as __$jsonrestapi_rn$__
|
|
37
|
+
* // FROM comments
|
|
38
|
+
* // WHERE article_id IN (1, 2, 3)
|
|
39
|
+
* // )
|
|
40
|
+
* // SELECT * FROM _windowed
|
|
41
|
+
* // WHERE __$jsonrestapi_rn$__ <= 3
|
|
42
|
+
*
|
|
43
|
+
* // Result: Each article gets max 3 comments, not 3 total
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // Input: Database doesn't support window functions
|
|
47
|
+
* const query = buildWindowedIncludeQuery(
|
|
48
|
+
* knex,
|
|
49
|
+
* 'comments',
|
|
50
|
+
* 'article_id',
|
|
51
|
+
* [1, 2, 3],
|
|
52
|
+
* '*',
|
|
53
|
+
* { limit: 5 },
|
|
54
|
+
* { windowFunctions: false, dbInfo: { client: 'mysql', version: '5.7' } }
|
|
55
|
+
* );
|
|
56
|
+
*
|
|
57
|
+
* // Throws RestApiResourceError:
|
|
58
|
+
* // "Include limits require window function support. Your database (mysql 5.7)
|
|
59
|
+
* // does not support this feature. Window functions are supported in:
|
|
60
|
+
* // PostgreSQL 8.4+, MySQL 8.0+, MariaDB 10.2+, SQLite 3.25+, SQL Server 2005+"
|
|
61
|
+
*
|
|
62
|
+
* @description
|
|
63
|
+
* Used by:
|
|
64
|
+
* - loadHasMany when strategy: 'window' and database supports window functions
|
|
65
|
+
* - Enables per-parent limits for one-to-many relationships
|
|
66
|
+
*
|
|
67
|
+
* Purpose:
|
|
68
|
+
* - Solves the "N+1 limit" problem where you want X records per parent
|
|
69
|
+
* - Without window functions, LIMIT 10 gives 10 total across all parents
|
|
70
|
+
* - With window functions, each parent gets up to 10 related records
|
|
71
|
+
* - Critical for consistent API responses with includes
|
|
72
|
+
*
|
|
73
|
+
* Data flow:
|
|
74
|
+
* 1. Creates subquery with ROW_NUMBER() partitioned by foreign key
|
|
75
|
+
* 2. Numbers rows within each partition based on orderBy
|
|
76
|
+
* 3. Outer query filters to keep only rows within limit
|
|
77
|
+
* 4. Returns query ready for execution
|
|
78
|
+
* 5. Caller removes the ROW_NUMBER column from results
|
|
79
|
+
*/
|
|
80
|
+
export const buildWindowedIncludeQuery = (
|
|
81
|
+
knex,
|
|
82
|
+
tableName,
|
|
83
|
+
foreignKey,
|
|
84
|
+
parentIds,
|
|
85
|
+
fieldsToSelect,
|
|
86
|
+
includeConfig,
|
|
87
|
+
capabilities,
|
|
88
|
+
scopeVars = {}
|
|
89
|
+
) => {
|
|
90
|
+
const { orderBy = [] } = includeConfig;
|
|
91
|
+
|
|
92
|
+
// Apply defaults for limit
|
|
93
|
+
const effectiveLimit = includeConfig.limit ?? scopeVars.queryDefaultLimit ?? DEFAULT_QUERY_LIMIT;
|
|
94
|
+
|
|
95
|
+
// Validate against max
|
|
96
|
+
if (scopeVars.queryMaxLimit && effectiveLimit > scopeVars.queryMaxLimit) {
|
|
97
|
+
throw new RestApiResourceError({
|
|
98
|
+
title: 'Include Limit Exceeds Maximum',
|
|
99
|
+
detail: `Requested include limit (${effectiveLimit}) exceeds queryMaxLimit (${scopeVars.queryMaxLimit})`,
|
|
100
|
+
status: 400
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check if window functions are supported
|
|
105
|
+
if (!capabilities.windowFunctions) {
|
|
106
|
+
const { dbInfo } = capabilities;
|
|
107
|
+
throw new RestApiResourceError(
|
|
108
|
+
`Include limits require window function support. Your database (${dbInfo.client} ${dbInfo.version}) does not support this feature. ` +
|
|
109
|
+
`Window functions are supported in: PostgreSQL 8.4+, MySQL 8.0+, MariaDB 10.2+, SQLite 3.25+, SQL Server 2005+`,
|
|
110
|
+
{
|
|
111
|
+
subtype: 'unsupported_operation',
|
|
112
|
+
database: dbInfo.client,
|
|
113
|
+
version: dbInfo.version,
|
|
114
|
+
requiredFeature: 'window_functions'
|
|
115
|
+
}
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Build the window function query
|
|
120
|
+
// This creates a subquery that partitions by the foreign key and numbers rows
|
|
121
|
+
const subquery = knex(tableName)
|
|
122
|
+
.select('*')
|
|
123
|
+
.select(
|
|
124
|
+
knex.raw(
|
|
125
|
+
'ROW_NUMBER() OVER (PARTITION BY ?? ORDER BY ' +
|
|
126
|
+
buildOrderByClause(orderBy) +
|
|
127
|
+
') as ' + ROW_NUMBER_KEY,
|
|
128
|
+
[foreignKey]
|
|
129
|
+
)
|
|
130
|
+
)
|
|
131
|
+
.whereIn(foreignKey, parentIds);
|
|
132
|
+
|
|
133
|
+
// Apply field selection if specified
|
|
134
|
+
if (fieldsToSelect !== '*' && Array.isArray(fieldsToSelect)) {
|
|
135
|
+
// Include the foreign key and row number in selection
|
|
136
|
+
const fieldsWithFK = [...new Set([...fieldsToSelect, foreignKey, ROW_NUMBER_KEY])];
|
|
137
|
+
subquery.select(fieldsWithFK);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Wrap in outer query to filter by row number
|
|
141
|
+
const query = knex
|
|
142
|
+
.select('*')
|
|
143
|
+
.from(subquery.as('_windowed'))
|
|
144
|
+
.where(ROW_NUMBER_KEY, '<=', effectiveLimit);
|
|
145
|
+
|
|
146
|
+
return query;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Builds ORDER BY clause from array of sort fields
|
|
151
|
+
*
|
|
152
|
+
* @param {Array<string>} orderBy - Array of field names, prefix with '-' for DESC
|
|
153
|
+
* @param {string} tablePrefix - Optional table name to prefix fields
|
|
154
|
+
* @returns {string} SQL ORDER BY clause
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* // Input: Simple ascending sort
|
|
158
|
+
* const clause = buildOrderByClause(['name', 'created_at']);
|
|
159
|
+
* // Output: "name ASC, created_at ASC"
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* // Input: Mixed ascending/descending with '-' prefix
|
|
163
|
+
* const clause = buildOrderByClause(['name', '-created_at', 'status']);
|
|
164
|
+
* // Output: "name ASC, created_at DESC, status ASC"
|
|
165
|
+
*
|
|
166
|
+
* @example
|
|
167
|
+
* // Input: With table prefix for joins
|
|
168
|
+
* const clause = buildOrderByClause(['-updated_at', 'title'], 'articles');
|
|
169
|
+
* // Output: "articles.updated_at DESC, articles.title ASC"
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* // Input: Empty array (default to id)
|
|
173
|
+
* const clause = buildOrderByClause([]);
|
|
174
|
+
* // Output: "id ASC"
|
|
175
|
+
*
|
|
176
|
+
* @description
|
|
177
|
+
* Used by:
|
|
178
|
+
* - buildWindowedIncludeQuery for PARTITION BY ordering
|
|
179
|
+
* - applyStandardIncludeConfig for regular ORDER BY
|
|
180
|
+
* - Query builders that need consistent sort syntax
|
|
181
|
+
*
|
|
182
|
+
* Purpose:
|
|
183
|
+
* - Converts API sort syntax (with '-' prefix) to SQL ORDER BY
|
|
184
|
+
* - Handles table prefixing for queries with joins
|
|
185
|
+
* - Provides default ordering by id when none specified
|
|
186
|
+
* - Ensures consistent sort behavior across the API
|
|
187
|
+
*
|
|
188
|
+
* Data flow:
|
|
189
|
+
* 1. Receives array of sort fields from API parameters
|
|
190
|
+
* 2. Detects DESC sorts by '-' prefix
|
|
191
|
+
* 3. Optionally prefixes with table name
|
|
192
|
+
* 4. Joins into SQL-compatible ORDER BY clause
|
|
193
|
+
*/
|
|
194
|
+
export const buildOrderByClause = (orderBy, tablePrefix) => {
|
|
195
|
+
if (!orderBy || orderBy.length === 0) {
|
|
196
|
+
const defaultField = tablePrefix ? `${tablePrefix}.id` : 'id';
|
|
197
|
+
return `${defaultField} ASC`; // Default ordering
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return orderBy.map(field => {
|
|
201
|
+
const isDesc = field.startsWith('-');
|
|
202
|
+
const fieldName = isDesc ? field.substring(1) : field;
|
|
203
|
+
const qualifiedField = tablePrefix ? `${tablePrefix}.${fieldName}` : fieldName;
|
|
204
|
+
return `${qualifiedField} ${isDesc ? 'DESC' : 'ASC'}`;
|
|
205
|
+
}).join(', ');
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Applies standard (non-windowed) include configuration to a query
|
|
210
|
+
*
|
|
211
|
+
* @param {Object} query - Knex query builder instance
|
|
212
|
+
* @param {Object} includeConfig - Include configuration object
|
|
213
|
+
* @param {Object} scopeVars - Scope variables with defaults and limits
|
|
214
|
+
* @param {Object} log - Logger instance
|
|
215
|
+
* @returns {Object} Modified query builder
|
|
216
|
+
*
|
|
217
|
+
* @example
|
|
218
|
+
* // Input: Basic include with limit and ordering
|
|
219
|
+
* let query = knex('comments').whereIn('article_id', [1, 2, 3]);
|
|
220
|
+
* query = applyStandardIncludeConfig(
|
|
221
|
+
* query,
|
|
222
|
+
* { limit: 20, orderBy: ['-created_at', 'id'] },
|
|
223
|
+
* { queryDefaultLimit: 100, queryMaxLimit: 1000 },
|
|
224
|
+
* logger
|
|
225
|
+
* );
|
|
226
|
+
*
|
|
227
|
+
* // Result: Query modified with:
|
|
228
|
+
* // ORDER BY created_at DESC, id ASC
|
|
229
|
+
* // LIMIT 20
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* // Input: No explicit limit, uses defaults
|
|
233
|
+
* let query = knex('tags');
|
|
234
|
+
* query = applyStandardIncludeConfig(
|
|
235
|
+
* query,
|
|
236
|
+
* { orderBy: ['name'] }, // No limit specified
|
|
237
|
+
* { queryDefaultLimit: 50, queryMaxLimit: 500 },
|
|
238
|
+
* logger
|
|
239
|
+
* );
|
|
240
|
+
*
|
|
241
|
+
* // Result: Uses default limit
|
|
242
|
+
* // ORDER BY name ASC
|
|
243
|
+
* // LIMIT 50
|
|
244
|
+
* // Log: "Using default limit"
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* // Input: Limit exceeds maximum
|
|
248
|
+
* let query = knex('reviews');
|
|
249
|
+
* query = applyStandardIncludeConfig(
|
|
250
|
+
* query,
|
|
251
|
+
* { limit: 5000 }, // Exceeds max
|
|
252
|
+
* { queryMaxLimit: 1000, maxIncludeLimit: 500 },
|
|
253
|
+
* logger
|
|
254
|
+
* );
|
|
255
|
+
*
|
|
256
|
+
* // Result: Clamped to effective maximum
|
|
257
|
+
* // LIMIT 500 (min of maxIncludeLimit and queryMaxLimit)
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* // Input: Explicitly disable limit
|
|
261
|
+
* let query = knex('categories');
|
|
262
|
+
* query = applyStandardIncludeConfig(
|
|
263
|
+
* query,
|
|
264
|
+
* { limit: null }, // Explicitly no limit
|
|
265
|
+
* { queryDefaultLimit: 100 },
|
|
266
|
+
* logger
|
|
267
|
+
* );
|
|
268
|
+
*
|
|
269
|
+
* // Result: No LIMIT clause added
|
|
270
|
+
* // Log: "No limit applied to include query (explicitly disabled)"
|
|
271
|
+
*
|
|
272
|
+
* @description
|
|
273
|
+
* Used by:
|
|
274
|
+
* - loadHasMany when window functions not available or not requested
|
|
275
|
+
* - loadReversePolymorphic for standard relationship queries
|
|
276
|
+
* - Any include loader that needs consistent limit/order handling
|
|
277
|
+
*
|
|
278
|
+
* Purpose:
|
|
279
|
+
* - Provides fallback when window functions unavailable
|
|
280
|
+
* - Applies consistent ordering based on API sort syntax
|
|
281
|
+
* - Enforces limit hierarchy: explicit > default > max
|
|
282
|
+
* - Respects both queryMaxLimit and maxIncludeLimit
|
|
283
|
+
* - Logs decisions for debugging
|
|
284
|
+
*
|
|
285
|
+
* Data flow:
|
|
286
|
+
* 1. Applies ORDER BY for each field in orderBy array
|
|
287
|
+
* 2. Determines effective limit from explicit/default/max values
|
|
288
|
+
* 3. Clamps limit to maximum allowed values
|
|
289
|
+
* 4. Adds LIMIT clause unless explicitly disabled
|
|
290
|
+
* 5. Logs the reasoning for the applied limit
|
|
291
|
+
*/
|
|
292
|
+
export const applyStandardIncludeConfig = (query, includeConfig, scopeVars, log) => {
|
|
293
|
+
const { orderBy = [] } = includeConfig;
|
|
294
|
+
|
|
295
|
+
// Apply ordering
|
|
296
|
+
orderBy.forEach(field => {
|
|
297
|
+
const desc = field.startsWith('-');
|
|
298
|
+
const column = desc ? field.substring(1) : field;
|
|
299
|
+
query = query.orderBy(column, desc ? 'desc' : 'asc');
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// Apply limit with defaults
|
|
303
|
+
const requestedLimit = includeConfig.limit;
|
|
304
|
+
const defaultLimit = scopeVars.queryDefaultLimit ?? DEFAULT_QUERY_LIMIT;
|
|
305
|
+
const limit = requestedLimit ?? defaultLimit;
|
|
306
|
+
|
|
307
|
+
// Allow explicit null/false to mean no limit
|
|
308
|
+
if (limit !== null && limit !== false) {
|
|
309
|
+
const maxInclude = scopeVars.maxIncludeLimit || DEFAULT_MAX_INCLUDE_LIMIT;
|
|
310
|
+
const maxQuery = scopeVars.queryMaxLimit || DEFAULT_MAX_QUERY_LIMIT;
|
|
311
|
+
const effectiveMax = Math.min(maxInclude, maxQuery);
|
|
312
|
+
const effectiveLimit = Math.min(limit, effectiveMax);
|
|
313
|
+
|
|
314
|
+
query = query.limit(effectiveLimit);
|
|
315
|
+
|
|
316
|
+
log.debug('Applied include limit:', {
|
|
317
|
+
requested: requestedLimit,
|
|
318
|
+
default: defaultLimit,
|
|
319
|
+
effective: effectiveLimit,
|
|
320
|
+
maxAllowed: effectiveMax,
|
|
321
|
+
note: requestedLimit !== undefined ? 'Using explicit limit' : 'Using default limit'
|
|
322
|
+
});
|
|
323
|
+
} else {
|
|
324
|
+
log.debug('No limit applied to include query (explicitly disabled)');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return query;
|
|
328
|
+
};
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { createSchema } from 'json-rest-schema';
|
|
2
|
+
import { ensureSearchFieldsAreIndexed, generateSearchSchemaFromSchema, sortFieldsByDependencies } from './schema-helpers.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compiles and enriches schemas for a resource scope
|
|
6
|
+
*
|
|
7
|
+
* @param {Object} scope - Scope containing raw schema options
|
|
8
|
+
* @param {Object} deps - Dependencies with context and hooks
|
|
9
|
+
* @returns {Promise<void>} Resolves when compilation complete
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // Input: Raw schema with computed field
|
|
13
|
+
* const rawSchema = {
|
|
14
|
+
* title: { type: 'string', required: true },
|
|
15
|
+
* content: { type: 'string' },
|
|
16
|
+
* author_id: { belongsTo: 'users', as: 'author' }, // Missing type
|
|
17
|
+
* word_count: {
|
|
18
|
+
* type: 'number',
|
|
19
|
+
* computed: true,
|
|
20
|
+
* compute: (record) => record.content.split(' ').length
|
|
21
|
+
* }
|
|
22
|
+
* };
|
|
23
|
+
*
|
|
24
|
+
* await compileSchemas(scope, deps);
|
|
25
|
+
*
|
|
26
|
+
* // Output in scope.vars.schemaInfo:
|
|
27
|
+
* // {
|
|
28
|
+
* // schema: Schema {}, // json-rest-schema instance
|
|
29
|
+
* // schemaStructure: {
|
|
30
|
+
* // title: { type: 'string', required: true },
|
|
31
|
+
* // content: { type: 'string' },
|
|
32
|
+
* // author_id: { type: 'id', belongsTo: 'users', as: 'author' } // Type added!
|
|
33
|
+
* // // Note: word_count removed (it's computed)
|
|
34
|
+
* // },
|
|
35
|
+
* // computed: {
|
|
36
|
+
* // word_count: { type: 'number', computed: true, compute: [Function] }
|
|
37
|
+
* // }
|
|
38
|
+
* // }
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* // Input: Schema with search fields
|
|
42
|
+
* const rawSchema = {
|
|
43
|
+
* title: { type: 'string', search: true }, // Searchable
|
|
44
|
+
* status: { type: 'string' }, // Not searchable
|
|
45
|
+
* author_id: { belongsTo: 'users', as: 'author' }
|
|
46
|
+
* };
|
|
47
|
+
*
|
|
48
|
+
* // Output: Auto-generated searchSchema
|
|
49
|
+
* // searchSchemaObject contains:
|
|
50
|
+
* // {
|
|
51
|
+
* // title: {
|
|
52
|
+
* // type: 'string',
|
|
53
|
+
* // indexed: true, // Added for DB optimization
|
|
54
|
+
* // filterOperator: '=' // Default operator
|
|
55
|
+
* // }
|
|
56
|
+
* // }
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* // Input: Pivot table auto-detection
|
|
60
|
+
* const pivotSchema = {
|
|
61
|
+
* article_id: { belongsTo: 'articles', as: 'article' },
|
|
62
|
+
* tag_id: { belongsTo: 'tags', as: 'tag' },
|
|
63
|
+
* display_order: { type: 'number' }
|
|
64
|
+
* };
|
|
65
|
+
*
|
|
66
|
+
* // Output: Both foreign keys become searchable
|
|
67
|
+
* // schemaStructure will have:
|
|
68
|
+
* // {
|
|
69
|
+
* // article_id: { type: 'id', belongsTo: 'articles', as: 'article', search: true },
|
|
70
|
+
* // tag_id: { type: 'id', belongsTo: 'tags', as: 'tag', search: true },
|
|
71
|
+
* // display_order: { type: 'number' }
|
|
72
|
+
* // }
|
|
73
|
+
* // This enables efficient many-to-many filtering!
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* // Input: Schema with dependent getters
|
|
77
|
+
* const schema = {
|
|
78
|
+
* first_name: { type: 'string' },
|
|
79
|
+
* last_name: { type: 'string' },
|
|
80
|
+
* full_name: {
|
|
81
|
+
* type: 'string',
|
|
82
|
+
* getter: (record) => `${record.first_name} ${record.last_name}`,
|
|
83
|
+
* runGetterAfter: ['first_name', 'last_name'] // Dependencies
|
|
84
|
+
* },
|
|
85
|
+
* email: {
|
|
86
|
+
* type: 'string',
|
|
87
|
+
* setter: (value) => value.toLowerCase().trim()
|
|
88
|
+
* }
|
|
89
|
+
* };
|
|
90
|
+
*
|
|
91
|
+
* // Output includes dependency-sorted fields:
|
|
92
|
+
* // {
|
|
93
|
+
* // fieldGetters: { full_name: { getter: [Function], runGetterAfter: [...] } },
|
|
94
|
+
* // sortedGetterFields: ['full_name'], // Topologically sorted
|
|
95
|
+
* // fieldSetters: { email: { setter: [Function], runSetterAfter: [] } },
|
|
96
|
+
* // sortedSetterFields: ['email']
|
|
97
|
+
* // }
|
|
98
|
+
*
|
|
99
|
+
* @description
|
|
100
|
+
* Used by:
|
|
101
|
+
* - Every REST method calls this before any operations
|
|
102
|
+
* - Compilation happens once per scope (cached)
|
|
103
|
+
*
|
|
104
|
+
* Purpose:
|
|
105
|
+
* - Separates computed fields from database fields
|
|
106
|
+
* - Adds missing types to belongsTo relationships
|
|
107
|
+
* - Auto-detects pivot tables for many-to-many
|
|
108
|
+
* - Generates search schemas from field markers
|
|
109
|
+
* - Validates getter/setter dependencies
|
|
110
|
+
* - Provides hooks for schema enrichment
|
|
111
|
+
* - Caches everything for performance
|
|
112
|
+
*
|
|
113
|
+
* Data flow:
|
|
114
|
+
* 1. Extracts computed fields to separate object
|
|
115
|
+
* 2. Adds type:'id' to belongsTo fields
|
|
116
|
+
* 3. Detects pivot tables (2+ foreign keys, 40%+ of fields)
|
|
117
|
+
* 4. Runs schema:enrich hook for plugins
|
|
118
|
+
* 5. Creates json-rest-schema validation instance
|
|
119
|
+
* 6. Generates searchSchema from search:true fields
|
|
120
|
+
* 7. Runs searchSchema:enrich hook
|
|
121
|
+
* 8. Extracts and topologically sorts getters/setters
|
|
122
|
+
* 9. Caches all results in scope.vars.schemaInfo
|
|
123
|
+
*/
|
|
124
|
+
export async function compileSchemas(scope, deps) {
|
|
125
|
+
// Extract scopeName from context
|
|
126
|
+
const { context, runHooks } = deps;
|
|
127
|
+
const scopeName = context.scopeName;
|
|
128
|
+
|
|
129
|
+
// Get raw schema
|
|
130
|
+
const rawSchema = scope.scopeOptions?.schema || {};
|
|
131
|
+
|
|
132
|
+
// Extract computed fields from schema and build enriched schema
|
|
133
|
+
const computedFields = {};
|
|
134
|
+
const enrichedSchema = {};
|
|
135
|
+
|
|
136
|
+
for (const [fieldName, fieldDef] of Object.entries(rawSchema)) {
|
|
137
|
+
if (fieldDef.computed === true) {
|
|
138
|
+
// Extract computed field - copy entire definition
|
|
139
|
+
computedFields[fieldName] = { ...fieldDef };
|
|
140
|
+
|
|
141
|
+
// Validate computed field
|
|
142
|
+
if (!fieldDef.type) {
|
|
143
|
+
throw new Error(`Computed field '${fieldName}' in scope '${scopeName}' must have a type`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (fieldDef.compute && typeof fieldDef.compute !== 'function') {
|
|
147
|
+
throw new Error(`Computed field '${fieldName}' in scope '${scopeName}' has invalid compute function`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Don't include computed fields in the validation schema
|
|
151
|
+
} else {
|
|
152
|
+
enrichedSchema[fieldName] = { ...fieldDef };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Default enrichment: Add type to belongsTo fields
|
|
157
|
+
for (const [fieldName, fieldDef] of Object.entries(enrichedSchema)) {
|
|
158
|
+
if (fieldDef.belongsTo && !fieldDef.type) {
|
|
159
|
+
fieldDef.type = 'id';
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ADD LOGIC TO MAKE FIELDS SEARCHABLE HERE
|
|
164
|
+
// Auto-detect pivot tables and make their foreign key fields searchable
|
|
165
|
+
// This is critical for many-to-many relationship operations which need to filter pivot records
|
|
166
|
+
const fields = Object.entries(enrichedSchema);
|
|
167
|
+
const belongsToFields = fields.filter(([_, def]) => def.belongsTo);
|
|
168
|
+
|
|
169
|
+
// If 2+ belongsTo fields and they make up 40%+ of non-id fields, likely a pivot table
|
|
170
|
+
const nonIdFields = fields.filter(([name, _]) => name !== 'id');
|
|
171
|
+
const isProbablyPivot = belongsToFields.length >= 2 &&
|
|
172
|
+
(nonIdFields.length === 0 || belongsToFields.length / nonIdFields.length >= 0.4);
|
|
173
|
+
|
|
174
|
+
if (isProbablyPivot) {
|
|
175
|
+
// Make all belongsTo fields searchable for pivot operations
|
|
176
|
+
for (const [fieldName, fieldDef] of belongsToFields) {
|
|
177
|
+
if (!fieldDef.search) {
|
|
178
|
+
fieldDef.search = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
// Hook: schema:enrich
|
|
185
|
+
const schemaContext = {
|
|
186
|
+
schema: enrichedSchema, // Mutable
|
|
187
|
+
originalSchema: rawSchema, // Read-only
|
|
188
|
+
scopeName
|
|
189
|
+
};
|
|
190
|
+
await runHooks('schema:enrich', schemaContext);
|
|
191
|
+
|
|
192
|
+
// Create schema object
|
|
193
|
+
const schemaObject = createSchema(schemaContext.schema);
|
|
194
|
+
|
|
195
|
+
// Generate searchSchema by merging explicit searchSchema with fields marked search:true.
|
|
196
|
+
// This allows two ways to define searchable fields: either mark fields with search:true
|
|
197
|
+
// in the main schema, or provide an explicit searchSchema with more control over filtering.
|
|
198
|
+
// The explicit searchSchema takes precedence and can define virtual fields for joins.
|
|
199
|
+
// Example: title: {search: true} auto-generates a searchable field with sensible defaults,
|
|
200
|
+
// while searchSchema can specify filterOperator: 'contains' or complex join configurations.
|
|
201
|
+
let rawSearchSchema = scope.scopeOptions.searchSchema ||
|
|
202
|
+
generateSearchSchemaFromSchema(schemaContext.schema);
|
|
203
|
+
|
|
204
|
+
if (rawSearchSchema) {
|
|
205
|
+
// Mark all search fields as indexed for database optimization.
|
|
206
|
+
// This ensures that any field used for filtering will have a database index created,
|
|
207
|
+
// dramatically improving query performance. Without indexes, filtering large tables
|
|
208
|
+
// would require full table scans. The storage plugin uses these hints to create indexes.
|
|
209
|
+
// Example: A status field marked for search gets indexed: true, enabling efficient
|
|
210
|
+
// WHERE status = 'published' queries that can use index lookups instead of scanning all rows.
|
|
211
|
+
ensureSearchFieldsAreIndexed(rawSearchSchema);
|
|
212
|
+
|
|
213
|
+
// Hook: searchSchema:enrich
|
|
214
|
+
const searchSchemaContext = {
|
|
215
|
+
searchSchema: rawSearchSchema, // Mutable
|
|
216
|
+
schema: schemaContext.schema, // Read-only enriched schema
|
|
217
|
+
scopeName
|
|
218
|
+
};
|
|
219
|
+
await runHooks('searchSchema:enrich', searchSchemaContext);
|
|
220
|
+
|
|
221
|
+
// Create searchSchema object
|
|
222
|
+
var searchSchemaObject = createSchema(searchSchemaContext.searchSchema);
|
|
223
|
+
} else {
|
|
224
|
+
var searchSchemaObject = null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Build schemaRelationships including polymorphic fields from schema
|
|
228
|
+
const schemaRelationships = { ...(scope.scopeOptions.relationships || {}) };
|
|
229
|
+
|
|
230
|
+
// Validate belongsTo fields
|
|
231
|
+
for (const [fieldName, fieldDef] of Object.entries(schemaContext.schema)) {
|
|
232
|
+
// Validate that belongsTo fields have 'as' property
|
|
233
|
+
if (fieldDef.belongsTo && !fieldDef.as) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
`Field '${fieldName}' in resource '${scopeName}' has belongsTo: '${fieldDef.belongsTo}' but is missing the required 'as' property. ` +
|
|
236
|
+
`The 'as' property defines the relationship name used in JSON:API payloads. ` +
|
|
237
|
+
`Example: ${fieldName}: { type: 'number', belongsTo: '${fieldDef.belongsTo}', as: '${fieldName.replace(/_id$/, '')}' }`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Extract and validate getter definitions
|
|
243
|
+
const fieldGetters = {};
|
|
244
|
+
const getterFields = [];
|
|
245
|
+
|
|
246
|
+
for (const [fieldName, fieldDef] of Object.entries(schemaContext.schema)) {
|
|
247
|
+
if (fieldDef.getter && typeof fieldDef.getter === 'function') {
|
|
248
|
+
fieldGetters[fieldName] = {
|
|
249
|
+
getter: fieldDef.getter,
|
|
250
|
+
runGetterAfter: fieldDef.runGetterAfter || [],
|
|
251
|
+
fieldDef: fieldDef
|
|
252
|
+
};
|
|
253
|
+
getterFields.push(fieldName);
|
|
254
|
+
|
|
255
|
+
// Validate dependencies exist
|
|
256
|
+
if (fieldDef.runGetterAfter && Array.isArray(fieldDef.runGetterAfter)) {
|
|
257
|
+
for (const dep of fieldDef.runGetterAfter) {
|
|
258
|
+
if (!schemaContext.schema[dep]) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`Field '${fieldName}' in resource '${scopeName}' has getter dependency '${dep}' that does not exist in schema`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Sort getters by dependencies
|
|
269
|
+
let sortedGetterFields = [];
|
|
270
|
+
if (Object.keys(fieldGetters).length > 0) {
|
|
271
|
+
try {
|
|
272
|
+
sortedGetterFields = sortFieldsByDependencies(fieldGetters, 'runGetterAfter');
|
|
273
|
+
} catch (error) {
|
|
274
|
+
throw new Error(`Invalid getter dependencies in ${scopeName}: ${error.message}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Extract and validate setter definitions
|
|
279
|
+
const fieldSetters = {};
|
|
280
|
+
const setterFields = [];
|
|
281
|
+
|
|
282
|
+
for (const [fieldName, fieldDef] of Object.entries(schemaContext.schema)) {
|
|
283
|
+
if (fieldDef.setter && typeof fieldDef.setter === 'function') {
|
|
284
|
+
fieldSetters[fieldName] = {
|
|
285
|
+
setter: fieldDef.setter,
|
|
286
|
+
runSetterAfter: fieldDef.runSetterAfter || [],
|
|
287
|
+
fieldDef: fieldDef
|
|
288
|
+
};
|
|
289
|
+
setterFields.push(fieldName);
|
|
290
|
+
|
|
291
|
+
// Validate dependencies exist
|
|
292
|
+
if (fieldDef.runSetterAfter && Array.isArray(fieldDef.runSetterAfter)) {
|
|
293
|
+
for (const dep of fieldDef.runSetterAfter) {
|
|
294
|
+
if (!schemaContext.schema[dep]) {
|
|
295
|
+
throw new Error(
|
|
296
|
+
`Field '${fieldName}' in resource '${scopeName}' has setter dependency '${dep}' that does not exist in schema`
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Sort setters by dependencies
|
|
305
|
+
let sortedSetterFields = [];
|
|
306
|
+
if (Object.keys(fieldSetters).length > 0) {
|
|
307
|
+
try {
|
|
308
|
+
sortedSetterFields = sortFieldsByDependencies(fieldSetters, 'runSetterAfter');
|
|
309
|
+
} catch (error) {
|
|
310
|
+
throw new Error(`Invalid setter dependencies in ${scopeName}: ${error.message}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Cache everything
|
|
315
|
+
scope.vars.schemaInfo = {
|
|
316
|
+
schema: schemaObject,
|
|
317
|
+
schemaStructure: schemaContext.schema,
|
|
318
|
+
computed: computedFields,
|
|
319
|
+
searchSchema: searchSchemaObject,
|
|
320
|
+
schemaRelationships: schemaRelationships,
|
|
321
|
+
tableName: scope.scopeOptions.tableName || scopeName,
|
|
322
|
+
idProperty: scope.scopeOptions.idProperty || scope.vars.idProperty,
|
|
323
|
+
fieldGetters: fieldGetters,
|
|
324
|
+
sortedGetterFields: sortedGetterFields,
|
|
325
|
+
fieldSetters: fieldSetters,
|
|
326
|
+
sortedSetterFields: sortedSetterFields
|
|
327
|
+
};
|
|
328
|
+
}
|