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.
Files changed (146) hide show
  1. package/.claude/agents/library-guide-expert.md +29 -0
  2. package/.claude/agents/rest-api-internals-expert.md +53 -0
  3. package/CLAUDE.md +139 -1
  4. package/README.md +2 -5
  5. package/docTest.js +238 -0
  6. package/docs/GUIDE/GUIDE_1_Initial_Setup.md +955 -0
  7. package/docs/GUIDE/GUIDE_2_1_The_Starting_Point.md +54 -0
  8. package/docs/GUIDE/GUIDE_2_2_Manipulating_And_Searching_Tables.md +750 -0
  9. package/docs/GUIDE/GUIDE_2_3_BelongsTo_Relationships.md +494 -0
  10. package/docs/GUIDE/GUIDE_2_4_HasMany_Records.md +398 -0
  11. package/docs/GUIDE/GUIDE_2_5_HasMany_Polymorphic.md +882 -0
  12. package/docs/GUIDE/GUIDE_2_6_Many_To_Many.md +709 -0
  13. package/docs/GUIDE/GUIDE_2_7_Pagination_And_Ordering.md +1233 -0
  14. package/docs/GUIDE/GUIDE_2_8_Effects_of_PUT_and_PATCH.md +796 -0
  15. package/{README_X_Relationship_Plugin.md → docs/GUIDE/GUIDE_2_9_Relationships_Urls.md} +1 -4
  16. package/docs/GUIDE/GUIDE_2_Data_And_Relations.md +66 -0
  17. package/docs/GUIDE/GUIDE_3_Field_Transformations.md +1246 -0
  18. package/docs/GUIDE/GUIDE_5_Non-Db_Stores.md +0 -0
  19. package/docs/GUIDE/GUIDE_7_Hooks_Data_Management_And_Plugins.md +1490 -0
  20. package/{README_X_SocketIO.md → docs/GUIDE/GUIDE_X_SocketIO.md} +17 -17
  21. package/docs/GUIDE/index.md +58 -0
  22. package/docs/GUIDE.md +61 -2238
  23. package/docs/ONBOARDING/Computed_Properties_And_Sparse_Fields.md +398 -0
  24. package/docs/README.md +57 -2239
  25. package/{README_2_Computed_and_hidden_fields.md → docs/README_VARIOUS/GUIDE_2_Computed_and_hidden_fields.md} +22 -17
  26. package/docs/README_VARIOUS/GUIDE_IN_DOCS.md +2277 -0
  27. package/{README.ORIG.md → docs/README_VARIOUS/README.ORIG.md} +8 -8
  28. package/{README_8_Polymorphic_Relationships.md → docs/README_VARIOUS/README_8_Polymorphic_Relationships.md} +1 -1
  29. package/docs/README_VARIOUS/README_IN_DOCS.ms +2277 -0
  30. package/{RELATIONSHIPS_ADVANCED.md → docs/README_VARIOUS/RELATIONSHIPS_ADVANCED.md} +3 -3
  31. package/{correct-schema-format.md → docs/README_VARIOUS/correct-schema-format.md} +13 -13
  32. package/{pagination.md → docs/README_VARIOUS/pagination.md} +11 -8
  33. package/docs/assets/css/style.scss +78 -5
  34. package/docs/index.md +2 -12
  35. package/helper-reorganization-plan.md +85 -0
  36. package/helper-usage-analysis.md +94 -0
  37. package/package.json +5 -4
  38. package/plugins/core/bulk-operations-plugin.js +2 -2
  39. package/plugins/core/connectors/express-plugin.js +99 -33
  40. package/plugins/core/lib/querying/knex-cross-table-search.js +609 -0
  41. package/plugins/core/lib/querying/knex-json-api-transformers-querying.js +491 -0
  42. package/plugins/core/lib/querying/knex-pagination-helpers.js +544 -0
  43. package/plugins/core/lib/{knex-process-includes.js → querying/knex-process-includes.js} +65 -50
  44. package/plugins/core/lib/querying/knex-query-helpers-base.js +74 -0
  45. package/plugins/core/lib/{knex-query-helpers.js → querying/knex-query-helpers.js} +416 -180
  46. package/plugins/core/lib/{knex-relationship-includes.js → querying/knex-relationship-includes.js} +430 -114
  47. package/plugins/core/lib/querying/knex-window-queries.js +328 -0
  48. package/plugins/core/lib/querying-writing/compile-schemas.js +328 -0
  49. package/plugins/core/{utils → lib/querying-writing}/connectors-query-parser.js +62 -57
  50. package/plugins/core/lib/{database-capabilities.js → querying-writing/database-capabilities.js} +113 -7
  51. package/plugins/core/lib/querying-writing/database-value-normalizers.js +369 -0
  52. package/plugins/core/lib/querying-writing/default-data-helpers.js +72 -0
  53. package/plugins/core/{utils → lib/querying-writing}/field-utils.js +41 -31
  54. package/plugins/core/{utils → lib/querying-writing}/knex-constants.js +1 -0
  55. package/plugins/core/lib/{knex-field-helpers.js → querying-writing/knex-field-helpers.js} +190 -57
  56. package/plugins/core/lib/querying-writing/knex-json-api-transformers.js +162 -0
  57. package/plugins/core/lib/{payload-validators.js → querying-writing/payload-validators.js} +297 -217
  58. package/plugins/core/lib/querying-writing/schema-helpers.js +497 -0
  59. package/plugins/core/lib/querying-writing/scope-validations.js +431 -0
  60. package/plugins/core/lib/{simplified-helpers.js → querying-writing/simplified-helpers.js} +216 -176
  61. package/plugins/core/lib/writing/knex-json-api-transformers-writing.js +110 -0
  62. package/plugins/core/lib/writing/many-to-many-manipulations.js +300 -0
  63. package/plugins/core/lib/writing/relationship-processor.js +252 -0
  64. package/plugins/core/rest-api-knex-plugin.js +183 -62
  65. package/plugins/core/rest-api-plugin-hooks/compile-resource-schemas.js +7 -0
  66. package/plugins/core/rest-api-plugin-hooks/register-relationship-routes.js +72 -0
  67. package/plugins/core/rest-api-plugin-hooks/register-scope-routes.js +102 -0
  68. package/plugins/core/rest-api-plugin-hooks/turn-scope-init-into-vars.js +50 -0
  69. package/plugins/core/rest-api-plugin-hooks/validate-include-configurations.js +39 -0
  70. package/plugins/core/rest-api-plugin-methods/add-route.js +23 -0
  71. package/plugins/core/rest-api-plugin-methods/check-permissions.js +18 -0
  72. package/plugins/core/rest-api-plugin-methods/common.js +605 -0
  73. package/plugins/core/rest-api-plugin-methods/delete-relationship.js +115 -0
  74. package/plugins/core/rest-api-plugin-methods/delete.js +109 -0
  75. package/plugins/core/rest-api-plugin-methods/enrich-attributes.js +186 -0
  76. package/plugins/core/rest-api-plugin-methods/get-related.js +295 -0
  77. package/plugins/core/rest-api-plugin-methods/get-relationship.js +62 -0
  78. package/plugins/core/rest-api-plugin-methods/get.js +187 -0
  79. package/plugins/core/rest-api-plugin-methods/patch-relationship.js +56 -0
  80. package/plugins/core/rest-api-plugin-methods/patch.js +193 -0
  81. package/plugins/core/rest-api-plugin-methods/post-relationship.js +104 -0
  82. package/plugins/core/rest-api-plugin-methods/post.js +157 -0
  83. package/plugins/core/rest-api-plugin-methods/put.js +257 -0
  84. package/plugins/core/rest-api-plugin-methods/query.js +208 -0
  85. package/plugins/core/rest-api-plugin.js +108 -2498
  86. package/plugins/core/socketio-plugin.js +28 -5
  87. package/plugins/storage/local-storage.js +2 -2
  88. package/tests/computed-fields.test.js +483 -0
  89. package/tests/cors.test.js +4 -4
  90. package/tests/curl.test.js +5 -4
  91. package/tests/custom-idproperty-relationships.test.js +891 -0
  92. package/tests/field-getters.test.js +353 -0
  93. package/tests/field-setters.test.js +310 -0
  94. package/tests/fixtures/api-configs.js +800 -28
  95. package/tests/jwt-auth.test.js +0 -2
  96. package/tests/nested-includes.test.js +1 -1
  97. package/tests/pagination-cursor-multifield.test.js +514 -0
  98. package/tests/pagination-enhanced.test.js +12 -9
  99. package/tests/polymorphic-relationships.test.js +10 -8
  100. package/tests/polymorphic-simple.test.js +6 -4
  101. package/tests/positioning-advanced.test.js +12 -12
  102. package/tests/positioning-extended.test.js +25 -25
  103. package/tests/positioning-multifilter.test.js +24 -24
  104. package/tests/positioning.test.js +3 -3
  105. package/tests/query-limits.test.js +24 -8
  106. package/tests/relationship-endpoints.test.js +4 -5
  107. package/tests/relationships.test.js +0 -1
  108. package/tests/socketio.test.js +12 -1
  109. package/tests/virtual-fields.test.js +13 -13
  110. package/PLAN_FOR_WEBSOCKETS.md +0 -398
  111. package/README_1_Initial_Setup.md +0 -393
  112. package/README_X_Hooks.md +0 -647
  113. package/TODO_rest-api-plugin-extras.todo +0 -23
  114. package/connectors_ideas/express-plugin.js +0 -120
  115. package/connectors_ideas/http-plugin.js +0 -138
  116. package/docs/examples/express-plugin.js +0 -366
  117. package/example.js +0 -301
  118. package/plugins/core/HELPER_MODULES.md +0 -285
  119. package/plugins/core/lib/compile-schemas.js +0 -245
  120. package/plugins/core/lib/database-value-normalizers.js +0 -192
  121. package/plugins/core/lib/default-data-helpers.js +0 -100
  122. package/plugins/core/lib/knex-cross-table-search.js +0 -442
  123. package/plugins/core/lib/knex-json-api-helpers.js +0 -84
  124. package/plugins/core/lib/knex-json-api-transformers.js +0 -449
  125. package/plugins/core/lib/knex-pagination-helpers.js +0 -193
  126. package/plugins/core/lib/knex-window-queries.js +0 -142
  127. package/plugins/core/lib/many-to-many-manipulations.js +0 -343
  128. package/plugins/core/lib/relationship-processor.js +0 -278
  129. package/plugins/core/lib/schema-helpers.js +0 -343
  130. package/plugins/core/lib/scope-validations.js +0 -252
  131. package/plugins/core/rest-api-relationships-plugin.js +0 -700
  132. package/plugins/core/utils/knex-query-helpers-base.js +0 -56
  133. /package/{README_X_Permissions.md → docs/GUIDE/GUIDE_4_Authentication_And_Permissions.md} +0 -0
  134. /package/{README_X_Positioning.md → docs/GUIDE/GUIDE_6_Positioning.md} +0 -0
  135. /package/{README_X_Bulk_Operations.md → docs/GUIDE/GUIDE_X_Bulk_Operations.md} +0 -0
  136. /package/{README_X_Cors.md → docs/GUIDE/GUIDE_X_Cors.md} +0 -0
  137. /package/{README_X_Multihome.md → docs/GUIDE/GUIDE_X_Multihome.md} +0 -0
  138. /package/{README_X_Appendices.md → docs/GUIDE/GUIDE_Y_Appendices.md} +0 -0
  139. /package/{QUERY_DEPTH_PROTECTION.md → docs/README_VARIOUS/QUERY_DEPTH_PROTECTION.md} +0 -0
  140. /package/{RELATIONSHIPS.md → docs/README_VARIOUS/RELATIONSHIPS.md} +0 -0
  141. /package/{WEBSOCKET_IMPLEMENTATION_STATUS.md → docs/README_VARIOUS/WEBSOCKET_IMPLEMENTATION_STATUS.md} +0 -0
  142. /package/{helper-functions-chart.md → docs/README_VARIOUS/helper-functions-chart.md} +0 -0
  143. /package/docs/{implementation-hidden-computed-fields.md → README_VARIOUS/implementation-hidden-computed-fields.md} +0 -0
  144. /package/docs/{include-limits-migration-guide.md → README_VARIOUS/include-limits-migration-guide.md} +0 -0
  145. /package/{readmeSnippets → docs/README_VARIOUS}/test_put_behavior.js +0 -0
  146. /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
- };