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
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module compileSchemas
|
|
3
|
-
* @description Schema compilation and enrichment for REST API resources
|
|
4
|
-
*
|
|
5
|
-
* This module handles the lazy compilation of JSON schemas for resources,
|
|
6
|
-
* including enrichment through hooks, search schema generation, and caching.
|
|
7
|
-
* It ensures schemas are processed only once per resource for performance.
|
|
8
|
-
*
|
|
9
|
-
* Why this is useful upstream:
|
|
10
|
-
* - Provides lazy compilation to avoid processing unused schemas
|
|
11
|
-
* - Enables schema enrichment through hooks for dynamic behavior
|
|
12
|
-
* - Automatically generates search schemas from field definitions
|
|
13
|
-
* - Ensures database indexes are created for searchable fields
|
|
14
|
-
* - Caches compiled schemas for performance
|
|
15
|
-
* - Supports both explicit and implicit search field definitions
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { createSchema } from 'json-rest-schema';
|
|
19
|
-
import { ensureSearchFieldsAreIndexed, generateSearchSchemaFromSchema } from './schema-helpers.js';
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Compiles and enriches schemas for a given scope.
|
|
23
|
-
* This function must be called at the beginning of each REST method to ensure
|
|
24
|
-
* schemas are processed and cached before use.
|
|
25
|
-
*
|
|
26
|
-
* The compilation process:
|
|
27
|
-
* 1. Clones the raw schema to avoid mutations
|
|
28
|
-
* 2. Adds default type: 'id' to belongsTo fields
|
|
29
|
-
* 3. Runs schema:enrich hooks for custom enrichment
|
|
30
|
-
* 4. Creates schema objects using json-rest-schema
|
|
31
|
-
* 5. Generates and enriches searchSchema if needed
|
|
32
|
-
* 6. Caches all compiled schemas for the scope
|
|
33
|
-
*
|
|
34
|
-
* @param {Object} scope - The scope object containing vars, runHooks, scopeOptions, etc.
|
|
35
|
-
* @param {Object} deps - Dependencies object
|
|
36
|
-
* @param {Object} deps.context - Request context containing scopeName
|
|
37
|
-
* @returns {Promise<void>} Resolves when schemas are compiled
|
|
38
|
-
*
|
|
39
|
-
* @example <caption>Basic usage in REST method</caption>
|
|
40
|
-
* // At the beginning of a REST method:
|
|
41
|
-
* import { compileSchemas } from './lib/compile-schemas.js';
|
|
42
|
-
*
|
|
43
|
-
* addScopeMethod('query', async (scope, params) => {
|
|
44
|
-
* const deps = { context: { scopeName: 'articles' } };
|
|
45
|
-
* await compileSchemas(scope, deps);
|
|
46
|
-
* const schemaInfo = scope.vars.schemaInfo.schema;
|
|
47
|
-
* // Now schema, searchSchema, and relationships are available
|
|
48
|
-
* });
|
|
49
|
-
*
|
|
50
|
-
* @example <caption>Schema enrichment through hooks</caption>
|
|
51
|
-
* // A plugin can enrich schemas dynamically:
|
|
52
|
-
* api.on('schema:enrich', ({ schema, scopeName }) => {
|
|
53
|
-
* if (scopeName === 'articles') {
|
|
54
|
-
* // Add computed field
|
|
55
|
-
* schema.wordCount = {
|
|
56
|
-
* type: 'number',
|
|
57
|
-
* compute: (article) => article.content.split(' ').length
|
|
58
|
-
* };
|
|
59
|
-
* // Make title required
|
|
60
|
-
* schema.title.required = true;
|
|
61
|
-
* }
|
|
62
|
-
* });
|
|
63
|
-
*
|
|
64
|
-
* @example <caption>Search schema generation</caption>
|
|
65
|
-
* // Original schema:
|
|
66
|
-
* const schema = {
|
|
67
|
-
* title: { type: 'string', search: true }, // Marked searchable
|
|
68
|
-
* content: { type: 'string' }, // Not searchable
|
|
69
|
-
* status: { type: 'string' } // Not searchable by default
|
|
70
|
-
* };
|
|
71
|
-
*
|
|
72
|
-
* // Explicit searchSchema can override:
|
|
73
|
-
* const searchSchema = {
|
|
74
|
-
* title: { type: 'string', filterUsing: 'contains' },
|
|
75
|
-
* status: { type: 'string', filterUsing: '=' },
|
|
76
|
-
* author_name: { // Virtual field for joins
|
|
77
|
-
* type: 'string',
|
|
78
|
-
* filterUsing: 'contains',
|
|
79
|
-
* actualTable: 'users',
|
|
80
|
-
* actualField: 'name'
|
|
81
|
-
* }
|
|
82
|
-
* };
|
|
83
|
-
*
|
|
84
|
-
* @example <caption>Why this is useful upstream</caption>
|
|
85
|
-
* // The REST API plugin uses this to:
|
|
86
|
-
* // 1. Lazy-load schemas only when needed (performance optimization)
|
|
87
|
-
* // 2. Allow dynamic schema modifications through hooks
|
|
88
|
-
* // 3. Automatically set up database indexes for searchable fields
|
|
89
|
-
* // 4. Generate search schemas from field definitions (DRY principle)
|
|
90
|
-
* // 5. Cache compiled schemas to avoid reprocessing
|
|
91
|
-
* // 6. Support virtual search fields that join to other tables
|
|
92
|
-
* // 7. Ensure belongsTo fields have proper type definitions
|
|
93
|
-
*/
|
|
94
|
-
export async function compileSchemas(scope, deps) {
|
|
95
|
-
// Extract scopeName from context
|
|
96
|
-
const { context, runHooks } = deps;
|
|
97
|
-
const scopeName = context.scopeName;
|
|
98
|
-
|
|
99
|
-
// Get raw schema, computed fields, and virtual fields
|
|
100
|
-
const rawSchema = scope.scopeOptions?.schema || {};
|
|
101
|
-
const computed = scope.scopeOptions?.computed || {};
|
|
102
|
-
const virtual = scope.scopeOptions?.virtual || {};
|
|
103
|
-
|
|
104
|
-
// Validate computed fields to ensure they're properly defined
|
|
105
|
-
// Example: profit_margin: { type: 'number', dependencies: ['price', 'cost'], compute: (ctx) => ... }
|
|
106
|
-
Object.entries(computed).forEach(([fieldName, fieldDef]) => {
|
|
107
|
-
// Every computed field must have a type (for JSON:API serialization)
|
|
108
|
-
if (!fieldDef.type) {
|
|
109
|
-
throw new Error(`Computed field '${fieldName}' in scope '${scopeName}' must have a type`);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// If a compute function is provided, it must be a function
|
|
113
|
-
// Note: compute is optional - you might use enrichAttributes hook instead
|
|
114
|
-
if (fieldDef.compute && typeof fieldDef.compute !== 'function') {
|
|
115
|
-
throw new Error(`Computed field '${fieldName}' in scope '${scopeName}' has invalid compute function`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Dependencies are optional but recommended for automatic fetching
|
|
119
|
-
// Example: dependencies: ['price', 'cost'] ensures these fields are
|
|
120
|
-
// fetched from DB even if not explicitly requested
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
// Validate virtual fields to ensure they're properly defined
|
|
124
|
-
// Virtual fields accept input and are returned in output but not stored in DB
|
|
125
|
-
// Example: passwordConfirmation: { type: 'string', virtual: true }
|
|
126
|
-
Object.entries(virtual).forEach(([fieldName, fieldDef]) => {
|
|
127
|
-
// Every virtual field must have a type (for JSON:API serialization)
|
|
128
|
-
if (!fieldDef.type) {
|
|
129
|
-
throw new Error(`Virtual field '${fieldName}' in scope '${scopeName}' must have a type`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Virtual fields shouldn't have compute functions
|
|
133
|
-
if (fieldDef.compute) {
|
|
134
|
-
throw new Error(`Virtual field '${fieldName}' should not have a compute function. Use 'computed' instead.`);
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// Deep clone schema while preserving functions
|
|
139
|
-
const enrichedSchema = {};
|
|
140
|
-
for (const [fieldName, fieldDef] of Object.entries(rawSchema)) {
|
|
141
|
-
enrichedSchema[fieldName] = { ...fieldDef };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Default enrichment: Add type to belongsTo fields
|
|
145
|
-
for (const [fieldName, fieldDef] of Object.entries(enrichedSchema)) {
|
|
146
|
-
if (fieldDef.belongsTo && !fieldDef.type) {
|
|
147
|
-
fieldDef.type = 'id';
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// ADD LOGIC TO MAKE FIELDS SEARCHABLE HERE
|
|
152
|
-
// Auto-detect pivot tables and make their foreign key fields searchable
|
|
153
|
-
// This is critical for many-to-many relationship operations which need to filter pivot records
|
|
154
|
-
const fields = Object.entries(enrichedSchema);
|
|
155
|
-
const belongsToFields = fields.filter(([_, def]) => def.belongsTo);
|
|
156
|
-
|
|
157
|
-
// If 2+ belongsTo fields and they make up 40%+ of non-id fields, likely a pivot table
|
|
158
|
-
const nonIdFields = fields.filter(([name, _]) => name !== 'id');
|
|
159
|
-
const isProbablyPivot = belongsToFields.length >= 2 &&
|
|
160
|
-
(nonIdFields.length === 0 || belongsToFields.length / nonIdFields.length >= 0.4);
|
|
161
|
-
|
|
162
|
-
if (isProbablyPivot) {
|
|
163
|
-
// Make all belongsTo fields searchable for pivot operations
|
|
164
|
-
for (const [fieldName, fieldDef] of belongsToFields) {
|
|
165
|
-
if (!fieldDef.search) {
|
|
166
|
-
fieldDef.search = true;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
// Hook: schema:enrich
|
|
173
|
-
const schemaContext = {
|
|
174
|
-
schema: enrichedSchema, // Mutable
|
|
175
|
-
originalSchema: rawSchema, // Read-only
|
|
176
|
-
scopeName
|
|
177
|
-
};
|
|
178
|
-
await runHooks('schema:enrich', schemaContext);
|
|
179
|
-
|
|
180
|
-
// Create schema object
|
|
181
|
-
const schemaObject = createSchema(schemaContext.schema);
|
|
182
|
-
|
|
183
|
-
// Generate searchSchema by merging explicit searchSchema with fields marked search:true.
|
|
184
|
-
// This allows two ways to define searchable fields: either mark fields with search:true
|
|
185
|
-
// in the main schema, or provide an explicit searchSchema with more control over filtering.
|
|
186
|
-
// The explicit searchSchema takes precedence and can define virtual fields for joins.
|
|
187
|
-
// Example: title: {search: true} auto-generates a searchable field with sensible defaults,
|
|
188
|
-
// while searchSchema can specify filterUsing: 'contains' or complex join configurations.
|
|
189
|
-
let rawSearchSchema = scope.scopeOptions.searchSchema ||
|
|
190
|
-
generateSearchSchemaFromSchema(schemaContext.schema);
|
|
191
|
-
|
|
192
|
-
if (rawSearchSchema) {
|
|
193
|
-
// Mark all search fields as indexed for database optimization.
|
|
194
|
-
// This ensures that any field used for filtering will have a database index created,
|
|
195
|
-
// dramatically improving query performance. Without indexes, filtering large tables
|
|
196
|
-
// would require full table scans. The storage plugin uses these hints to create indexes.
|
|
197
|
-
// Example: A status field marked for search gets indexed: true, enabling efficient
|
|
198
|
-
// WHERE status = 'published' queries that can use index lookups instead of scanning all rows.
|
|
199
|
-
ensureSearchFieldsAreIndexed(rawSearchSchema);
|
|
200
|
-
|
|
201
|
-
// Hook: searchSchema:enrich
|
|
202
|
-
const searchSchemaContext = {
|
|
203
|
-
searchSchema: rawSearchSchema, // Mutable
|
|
204
|
-
schema: schemaContext.schema, // Read-only enriched schema
|
|
205
|
-
scopeName
|
|
206
|
-
};
|
|
207
|
-
await runHooks('searchSchema:enrich', searchSchemaContext);
|
|
208
|
-
|
|
209
|
-
// Create searchSchema object
|
|
210
|
-
var searchSchemaObject = createSchema(searchSchemaContext.searchSchema);
|
|
211
|
-
} else {
|
|
212
|
-
var searchSchemaObject = null;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Build schemaRelationships including polymorphic fields from schema
|
|
216
|
-
const schemaRelationships = { ...(scope.scopeOptions.relationships || {}) };
|
|
217
|
-
|
|
218
|
-
// Extract polymorphic relationships from schema fields and validate belongsTo fields
|
|
219
|
-
for (const [fieldName, fieldDef] of Object.entries(schemaContext.schema)) {
|
|
220
|
-
// Validate that belongsTo fields have 'as' property
|
|
221
|
-
if (fieldDef.belongsTo && !fieldDef.belongsToPolymorphic && !fieldDef.as) {
|
|
222
|
-
throw new Error(
|
|
223
|
-
`Field '${fieldName}' in resource '${scopeName}' has belongsTo: '${fieldDef.belongsTo}' but is missing the required 'as' property. ` +
|
|
224
|
-
`The 'as' property defines the relationship name used in JSON:API payloads. ` +
|
|
225
|
-
`Example: ${fieldName}: { type: 'number', belongsTo: '${fieldDef.belongsTo}', as: '${fieldName.replace(/_id$/, '')}' }`
|
|
226
|
-
);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (fieldDef.belongsToPolymorphic && fieldDef.as) {
|
|
230
|
-
schemaRelationships[fieldDef.as] = fieldDef;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Cache everything
|
|
235
|
-
scope.vars.schemaInfo = {
|
|
236
|
-
schema: schemaObject,
|
|
237
|
-
schemaStructure: schemaContext.schema,
|
|
238
|
-
computed: computed,
|
|
239
|
-
virtual: virtual,
|
|
240
|
-
searchSchema: searchSchemaObject,
|
|
241
|
-
schemaRelationships: schemaRelationships,
|
|
242
|
-
tableName: scope.scopeOptions.tableName || scopeName,
|
|
243
|
-
idProperty: scope.scopeOptions.idProperty || scope.vars.idProperty
|
|
244
|
-
};
|
|
245
|
-
}
|
|
@@ -1,192 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Helper functions to normalize values returned from the database.
|
|
3
|
-
*
|
|
4
|
-
* Handles:
|
|
5
|
-
* - Boolean values: Converts 1/0 to true/false (some databases don't have native boolean support)
|
|
6
|
-
* - Date values: Ensures JavaScript Date objects are properly handled
|
|
7
|
-
* - date: Already Date objects from Knex, no conversion needed
|
|
8
|
-
* - dateTime: Already Date objects from Knex, no conversion needed
|
|
9
|
-
* - time: Returned as HH:MM:SS strings from the database
|
|
10
|
-
*
|
|
11
|
-
* Note: With the updated json-rest-schema, dates are stored as Date objects in the database,
|
|
12
|
-
* and Knex returns them as Date objects when fetching. We mainly need to handle edge cases
|
|
13
|
-
* and ensure consistency.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Normalizes a date value to a JavaScript Date object.
|
|
18
|
-
* Handles various input formats from different databases and fixes timezone issues.
|
|
19
|
-
*
|
|
20
|
-
* @param {*} value - The date value from the database (could be Date, string, or number)
|
|
21
|
-
* @param {string} type - The field type ('date', 'dateTime', or 'time')
|
|
22
|
-
* @returns {Date|string|null} Date object for date/dateTime, string for time, or null
|
|
23
|
-
*/
|
|
24
|
-
function normalizeDateValue(value, type) {
|
|
25
|
-
// Handle null/undefined
|
|
26
|
-
if (value === null || value === undefined) {
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Handle time type specially - always return as string
|
|
31
|
-
if (type === 'time') {
|
|
32
|
-
// If it's already a properly formatted time string, return as-is
|
|
33
|
-
if (typeof value === 'string' && /^\d{2}:\d{2}:\d{2}/.test(value)) {
|
|
34
|
-
return value;
|
|
35
|
-
}
|
|
36
|
-
// If we got a Date object for a time field, extract the time portion
|
|
37
|
-
if (value instanceof Date) {
|
|
38
|
-
return value.toISOString().slice(11, 19); // Extract HH:MM:SS
|
|
39
|
-
}
|
|
40
|
-
// Try to parse and extract time
|
|
41
|
-
if (typeof value === 'string' || typeof value === 'number') {
|
|
42
|
-
const d = new Date(value);
|
|
43
|
-
if (!isNaN(d.getTime())) {
|
|
44
|
-
return d.toISOString().slice(11, 19);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// For date and dateTime: Already a Date object? Return as-is
|
|
51
|
-
if (value instanceof Date) {
|
|
52
|
-
return value;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Handle string values
|
|
56
|
-
if (typeof value === 'string') {
|
|
57
|
-
// Detect MySQL datetime format: 'YYYY-MM-DD HH:MM:SS'
|
|
58
|
-
// These have no T separator and no timezone indicator
|
|
59
|
-
const isMySQLDateTime = type === 'dateTime' &&
|
|
60
|
-
/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/.test(value) &&
|
|
61
|
-
!value.includes('T') &&
|
|
62
|
-
!value.includes('Z') &&
|
|
63
|
-
!value.includes('+') &&
|
|
64
|
-
!value.includes('-', 10); // Don't match date separators
|
|
65
|
-
|
|
66
|
-
if (isMySQLDateTime) {
|
|
67
|
-
// Convert to ISO format and force UTC interpretation
|
|
68
|
-
// '2024-01-15 10:30:00' becomes '2024-01-15T10:30:00Z'
|
|
69
|
-
return new Date(value.replace(' ', 'T') + 'Z');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// For date-only fields, ensure parsing at UTC midnight
|
|
73
|
-
// This prevents timezone shifts when parsing dates
|
|
74
|
-
if (type === 'date' && /^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
75
|
-
return new Date(value + 'T00:00:00Z');
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Handle numeric values (Unix timestamps)
|
|
80
|
-
if (typeof value === 'number') {
|
|
81
|
-
return new Date(value);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// Try to parse any other format
|
|
85
|
-
const dateObj = new Date(value);
|
|
86
|
-
|
|
87
|
-
// Check if date is valid
|
|
88
|
-
if (isNaN(dateObj.getTime())) {
|
|
89
|
-
console.warn(`Invalid date value for field type '${type}': ${value}`);
|
|
90
|
-
return null; // Return null for invalid dates
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return dateObj;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Normalizes database values in an attributes object.
|
|
98
|
-
* Handles:
|
|
99
|
-
* - Boolean normalization (1/0 to true/false)
|
|
100
|
-
* - Date/dateTime/time normalization to Date objects with proper timezone handling
|
|
101
|
-
*
|
|
102
|
-
* @param {Object} attributes - The attributes object to normalize
|
|
103
|
-
* @param {Object} schemaStructure - The schema structure defining field types
|
|
104
|
-
* @returns {Object} The normalized attributes object with Date objects for date fields
|
|
105
|
-
*/
|
|
106
|
-
export function normalizeAttributes(attributes, schemaStructure) {
|
|
107
|
-
if (!attributes || !schemaStructure) {
|
|
108
|
-
return attributes;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const normalized = { ...attributes };
|
|
112
|
-
|
|
113
|
-
// Iterate through each attribute
|
|
114
|
-
for (const [fieldName, value] of Object.entries(attributes)) {
|
|
115
|
-
const fieldDef = schemaStructure[fieldName];
|
|
116
|
-
|
|
117
|
-
// Skip if no field definition found
|
|
118
|
-
if (!fieldDef) continue;
|
|
119
|
-
|
|
120
|
-
// Normalize boolean values
|
|
121
|
-
if (fieldDef.type === 'boolean') {
|
|
122
|
-
if (value === 1 || value === '1') {
|
|
123
|
-
normalized[fieldName] = true;
|
|
124
|
-
} else if (value === 0 || value === '0') {
|
|
125
|
-
normalized[fieldName] = false;
|
|
126
|
-
}
|
|
127
|
-
// null/undefined remain as-is
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Normalize date/dateTime/time values
|
|
131
|
-
if (fieldDef.type === 'date' || fieldDef.type === 'dateTime' || fieldDef.type === 'time') {
|
|
132
|
-
normalized[fieldName] = normalizeDateValue(value, fieldDef.type);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return normalized;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Normalizes all records in a JSON:API response.
|
|
141
|
-
* Handles both main data records and included records.
|
|
142
|
-
*
|
|
143
|
-
* @param {Object} record - The JSON:API record object with data and optional included arrays
|
|
144
|
-
* @param {Object} scopes - All available scopes for looking up schema structures
|
|
145
|
-
* @returns {Object} The record with normalized attribute values
|
|
146
|
-
*/
|
|
147
|
-
export function normalizeRecordAttributes(record, scopes) {
|
|
148
|
-
if (!record || !scopes) {
|
|
149
|
-
return record;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Normalize main data records
|
|
153
|
-
if (record.data) {
|
|
154
|
-
if (Array.isArray(record.data)) {
|
|
155
|
-
// Handle array of records (query result)
|
|
156
|
-
for (const entry of record.data) {
|
|
157
|
-
const scope = scopes[entry.type];
|
|
158
|
-
if (scope?.vars?.schemaInfo?.schemaStructure && entry.attributes) {
|
|
159
|
-
entry.attributes = normalizeAttributes(
|
|
160
|
-
entry.attributes,
|
|
161
|
-
scope.vars.schemaInfo.schemaStructure
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
} else {
|
|
166
|
-
// Handle single record (get result)
|
|
167
|
-
const entry = record.data;
|
|
168
|
-
const scope = scopes[entry.type];
|
|
169
|
-
if (scope?.vars?.schemaInfo?.schemaStructure && entry.attributes) {
|
|
170
|
-
entry.attributes = normalizeAttributes(
|
|
171
|
-
entry.attributes,
|
|
172
|
-
scope.vars.schemaInfo.schemaStructure
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Normalize included records
|
|
179
|
-
if (record.included && Array.isArray(record.included)) {
|
|
180
|
-
for (const entry of record.included) {
|
|
181
|
-
const scope = scopes[entry.type];
|
|
182
|
-
if (scope?.vars?.schemaInfo?.schemaStructure && entry.attributes) {
|
|
183
|
-
entry.attributes = normalizeAttributes(
|
|
184
|
-
entry.attributes,
|
|
185
|
-
scope.vars.schemaInfo.schemaStructure
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return record;
|
|
192
|
-
}
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @module defaultDataHelpers
|
|
3
|
-
* @description Default data helper implementations that throw errors
|
|
4
|
-
*
|
|
5
|
-
* This module provides default implementations of data helpers that throw
|
|
6
|
-
* errors instructing users to install a storage plugin. These are used
|
|
7
|
-
* when no storage plugin has been installed.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export const createDefaultDataHelpers = (api) => {
|
|
11
|
-
return {
|
|
12
|
-
/**
|
|
13
|
-
* Default implementation of dataExists that throws an error
|
|
14
|
-
* @param {Object} scope - The scope object
|
|
15
|
-
* @param {Object} deps - Dependencies object
|
|
16
|
-
* @param {Object} deps.context - Request context with id, transaction, etc
|
|
17
|
-
* @throws {Error} Always throws error about missing storage plugin
|
|
18
|
-
*/
|
|
19
|
-
dataExists: async function(scope, deps) {
|
|
20
|
-
throw new Error(`No storage implementation for exists. Install a storage plugin.`);
|
|
21
|
-
},
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Default implementation of dataGet that throws an error
|
|
25
|
-
* @param {Object} scope - The scope object
|
|
26
|
-
* @param {Object} deps - Dependencies object
|
|
27
|
-
* @param {Object} deps.context - Request context with id, queryParams, transaction, etc
|
|
28
|
-
* @throws {Error} Always throws error about missing storage plugin
|
|
29
|
-
*/
|
|
30
|
-
dataGet: async function(scope, deps) {
|
|
31
|
-
throw new Error(`No storage implementation for get. Install a storage plugin.`);
|
|
32
|
-
},
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Default implementation of dataGetMinimal that throws an error
|
|
36
|
-
* @param {Object} scope - The scope object
|
|
37
|
-
* @param {Object} deps - Dependencies object
|
|
38
|
-
* @param {Object} deps.context - Request context with id, transaction, etc
|
|
39
|
-
* @throws {Error} Always throws error about missing storage plugin
|
|
40
|
-
*/
|
|
41
|
-
dataGetMinimal: async function(scope, deps) {
|
|
42
|
-
throw new Error(`No storage implementation for getMinimal. Install a storage plugin.`);
|
|
43
|
-
},
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Default implementation of dataQuery that throws an error
|
|
47
|
-
* @param {Object} scope - The scope object
|
|
48
|
-
* @param {Object} deps - Dependencies object
|
|
49
|
-
* @param {Object} deps.context - Request context with queryParams, transaction, etc
|
|
50
|
-
* @param {Function} deps.runHooks - Hook runner function
|
|
51
|
-
* @throws {Error} Always throws error about missing storage plugin
|
|
52
|
-
*/
|
|
53
|
-
dataQuery: async function(scope, deps) {
|
|
54
|
-
throw new Error(`No storage implementation for query. Install a storage plugin.`);
|
|
55
|
-
},
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Default implementation of dataPost that throws an error
|
|
59
|
-
* @param {Object} scope - The scope object
|
|
60
|
-
* @param {Object} deps - Dependencies object
|
|
61
|
-
* @param {Object} deps.context - Request context with inputRecord, transaction, etc
|
|
62
|
-
* @throws {Error} Always throws error about missing storage plugin
|
|
63
|
-
*/
|
|
64
|
-
dataPost: async function(scope, deps) {
|
|
65
|
-
throw new Error(`No storage implementation for post. Install a storage plugin.`);
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Default implementation of dataPatch that throws an error
|
|
70
|
-
* @param {Object} scope - The scope object
|
|
71
|
-
* @param {Object} deps - Dependencies object
|
|
72
|
-
* @param {Object} deps.context - Request context with id, inputRecord, queryParams, transaction, etc
|
|
73
|
-
* @throws {Error} Always throws error about missing storage plugin
|
|
74
|
-
*/
|
|
75
|
-
dataPatch: async function(scope, deps) {
|
|
76
|
-
throw new Error(`No storage implementation for patch. Install a storage plugin.`);
|
|
77
|
-
},
|
|
78
|
-
/**
|
|
79
|
-
* Default implementation of dataPut that throws an error
|
|
80
|
-
* @param {Object} scope - The scope object
|
|
81
|
-
* @param {Object} deps - Dependencies object
|
|
82
|
-
* @param {Object} deps.context - Request context with id, inputRecord, isCreate, transaction, etc
|
|
83
|
-
* @throws {Error} Always throws error about missing storage plugin
|
|
84
|
-
*/
|
|
85
|
-
dataPut: async function(scope, deps) {
|
|
86
|
-
throw new Error(`No storage implementation for put. Install a storage plugin.`);
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Default implementation of dataDelete that throws an error
|
|
91
|
-
* @param {Object} scope - The scope object
|
|
92
|
-
* @param {Object} deps - Dependencies object
|
|
93
|
-
* @param {Object} deps.context - Request context with id, transaction, etc
|
|
94
|
-
* @throws {Error} Always throws error about missing storage plugin
|
|
95
|
-
*/
|
|
96
|
-
dataDelete: async function(scope, deps) {
|
|
97
|
-
throw new Error(`No storage implementation for delete. Install a storage plugin.`);
|
|
98
|
-
}
|
|
99
|
-
};
|
|
100
|
-
};
|