@snteam/amplify-angular-core 1.0.36 → 1.0.37
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.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import {
|
|
2
|
+
import { Injectable, Component, Pipe, inject, Input, ChangeDetectionStrategy, model, signal, EventEmitter, Output } from '@angular/core';
|
|
3
3
|
import * as i3$1 from '@angular/common';
|
|
4
4
|
import { CommonModule } from '@angular/common';
|
|
5
5
|
import * as i1 from '@angular/forms';
|
|
@@ -13,27 +13,2045 @@ import { MatSelectModule } from '@angular/material/select';
|
|
|
13
13
|
import * as i6 from '@angular/material/datepicker';
|
|
14
14
|
import { MatDatepickerModule } from '@angular/material/datepicker';
|
|
15
15
|
import { provideNativeDateAdapter } from '@angular/material/core';
|
|
16
|
+
import { Observable } from 'rxjs';
|
|
16
17
|
import * as i3$2 from '@angular/material/divider';
|
|
17
18
|
import { MatDividerModule } from '@angular/material/divider';
|
|
18
|
-
import * as
|
|
19
|
+
import * as i3$4 from '@angular/material/toolbar';
|
|
19
20
|
import { MatToolbarModule } from '@angular/material/toolbar';
|
|
20
21
|
import * as i3$3 from '@angular/material/button';
|
|
21
22
|
import { MatButtonModule } from '@angular/material/button';
|
|
22
|
-
import * as
|
|
23
|
+
import * as i5$1 from '@angular/material/icon';
|
|
23
24
|
import { MatIconModule } from '@angular/material/icon';
|
|
24
|
-
import * as
|
|
25
|
+
import * as i6$1 from '@angular/material/list';
|
|
25
26
|
import { MatListModule } from '@angular/material/list';
|
|
26
27
|
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogTitle, MatDialogContent, MatDialogActions, MatDialogClose, MatDialog } from '@angular/material/dialog';
|
|
27
28
|
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
|
28
|
-
import * as i4$
|
|
29
|
+
import * as i4$1 from '@angular/router';
|
|
29
30
|
import { ActivatedRoute, Router } from '@angular/router';
|
|
30
31
|
import * as i5$2 from '@angular/material/tooltip';
|
|
31
32
|
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
32
33
|
import * as i1$1 from '@angular/material/expansion';
|
|
33
34
|
import { MatExpansionModule } from '@angular/material/expansion';
|
|
34
|
-
import * as i3$
|
|
35
|
+
import * as i3$5 from '@angular/material/card';
|
|
35
36
|
import { MatCardModule } from '@angular/material/card';
|
|
36
37
|
|
|
38
|
+
// Dynamic Relationship Loader Interfaces
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Selection set generation patterns
|
|
42
|
+
*/
|
|
43
|
+
var SelectionSetPattern;
|
|
44
|
+
(function (SelectionSetPattern) {
|
|
45
|
+
/** Basic relationship loading with id and common display fields */
|
|
46
|
+
SelectionSetPattern["BASIC"] = "basic";
|
|
47
|
+
/** Comprehensive field loading using wildcard selection */
|
|
48
|
+
SelectionSetPattern["COMPREHENSIVE"] = "comprehensive";
|
|
49
|
+
/** Selective field loading based on schema analysis */
|
|
50
|
+
SelectionSetPattern["SELECTIVE"] = "selective";
|
|
51
|
+
/** Minimal fallback pattern for error cases */
|
|
52
|
+
SelectionSetPattern["FALLBACK"] = "fallback";
|
|
53
|
+
})(SelectionSetPattern || (SelectionSetPattern = {}));
|
|
54
|
+
/**
|
|
55
|
+
* Field classification for display optimization
|
|
56
|
+
*/
|
|
57
|
+
var FieldClassification;
|
|
58
|
+
(function (FieldClassification) {
|
|
59
|
+
/** Essential fields that must always be included */
|
|
60
|
+
FieldClassification["ESSENTIAL"] = "essential";
|
|
61
|
+
/** Display fields commonly shown in UI */
|
|
62
|
+
FieldClassification["DISPLAY"] = "display";
|
|
63
|
+
/** Metadata fields that provide additional context */
|
|
64
|
+
FieldClassification["METADATA"] = "metadata";
|
|
65
|
+
/** Relationship fields that link to other models */
|
|
66
|
+
FieldClassification["RELATIONSHIP"] = "relationship";
|
|
67
|
+
/** System fields used internally */
|
|
68
|
+
FieldClassification["SYSTEM"] = "system";
|
|
69
|
+
/** Unknown or unclassified fields */
|
|
70
|
+
FieldClassification["UNKNOWN"] = "unknown";
|
|
71
|
+
})(FieldClassification || (FieldClassification = {}));
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Types of errors that can occur during selection set generation
|
|
75
|
+
*/
|
|
76
|
+
var SelectionSetErrorType;
|
|
77
|
+
(function (SelectionSetErrorType) {
|
|
78
|
+
/** Configuration is invalid or incomplete */
|
|
79
|
+
SelectionSetErrorType["INVALID_CONFIGURATION"] = "invalid_configuration";
|
|
80
|
+
/** Target model not found in schema */
|
|
81
|
+
SelectionSetErrorType["MODEL_NOT_FOUND"] = "model_not_found";
|
|
82
|
+
/** Field not found in target model */
|
|
83
|
+
SelectionSetErrorType["FIELD_NOT_FOUND"] = "field_not_found";
|
|
84
|
+
/** Schema introspection data unavailable */
|
|
85
|
+
SelectionSetErrorType["SCHEMA_UNAVAILABLE"] = "schema_unavailable";
|
|
86
|
+
/** Generated selection set exceeds complexity limits */
|
|
87
|
+
SelectionSetErrorType["COMPLEXITY_EXCEEDED"] = "complexity_exceeded";
|
|
88
|
+
/** Circular reference detected in relationship chain */
|
|
89
|
+
SelectionSetErrorType["CIRCULAR_REFERENCE"] = "circular_reference";
|
|
90
|
+
/** GraphQL query failed with generated selection set */
|
|
91
|
+
SelectionSetErrorType["GRAPHQL_ERROR"] = "graphql_error";
|
|
92
|
+
})(SelectionSetErrorType || (SelectionSetErrorType = {}));
|
|
93
|
+
|
|
94
|
+
// Dynamic Relationship Loader Types
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Centralized error handling and logging service for the dynamic relationship loader system
|
|
98
|
+
*
|
|
99
|
+
* This service provides comprehensive error handling, logging, and recovery mechanisms
|
|
100
|
+
* for all relationship loading failures across the system.
|
|
101
|
+
*/
|
|
102
|
+
class ErrorHandlerService {
|
|
103
|
+
/**
|
|
104
|
+
* Maximum number of error logs to keep in memory
|
|
105
|
+
*/
|
|
106
|
+
MAX_ERROR_LOGS = 100;
|
|
107
|
+
/**
|
|
108
|
+
* In-memory error log storage for debugging
|
|
109
|
+
*/
|
|
110
|
+
errorLogs = [];
|
|
111
|
+
/**
|
|
112
|
+
* Error statistics for monitoring
|
|
113
|
+
*/
|
|
114
|
+
errorStats = {
|
|
115
|
+
totalErrors: 0,
|
|
116
|
+
errorsByType: new Map(),
|
|
117
|
+
lastError: null,
|
|
118
|
+
recoveryAttempts: 0,
|
|
119
|
+
successfulRecoveries: 0
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Log a selection set generation error with comprehensive context
|
|
123
|
+
*/
|
|
124
|
+
logSelectionSetError(type, message, config, selectionSet, additionalContext) {
|
|
125
|
+
const error = {
|
|
126
|
+
type,
|
|
127
|
+
message,
|
|
128
|
+
configuration: config ? { ...config } : undefined,
|
|
129
|
+
selectionSet: selectionSet ? [...selectionSet] : undefined,
|
|
130
|
+
context: {
|
|
131
|
+
...additionalContext,
|
|
132
|
+
userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'unknown',
|
|
133
|
+
timestamp: new Date().toISOString(),
|
|
134
|
+
stackTrace: new Error().stack
|
|
135
|
+
},
|
|
136
|
+
timestamp: new Date()
|
|
137
|
+
};
|
|
138
|
+
// Add to error logs
|
|
139
|
+
this.addErrorLog(error);
|
|
140
|
+
// Update statistics
|
|
141
|
+
this.updateErrorStats(error);
|
|
142
|
+
// Log to console with structured format
|
|
143
|
+
this.logToConsole(error);
|
|
144
|
+
return error;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Log a GraphQL query error with retry context
|
|
148
|
+
*/
|
|
149
|
+
logGraphQLError(error, config, selectionSet, retryAttempt, previousErrors = []) {
|
|
150
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
151
|
+
return this.logSelectionSetError(SelectionSetErrorType.GRAPHQL_ERROR, `GraphQL query failed: ${errorMessage}`, config, selectionSet, {
|
|
152
|
+
originalError: {
|
|
153
|
+
message: errorMessage,
|
|
154
|
+
name: error instanceof Error ? error.name : 'Unknown',
|
|
155
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
156
|
+
},
|
|
157
|
+
retryAttempt,
|
|
158
|
+
previousErrors,
|
|
159
|
+
queryContext: {
|
|
160
|
+
modelName: config.relationshipModelName,
|
|
161
|
+
fieldName: config.fieldName,
|
|
162
|
+
selectionSetLength: selectionSet.length
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Log a configuration analysis error
|
|
168
|
+
*/
|
|
169
|
+
logConfigurationError(config, errors, additionalContext) {
|
|
170
|
+
return this.logSelectionSetError(SelectionSetErrorType.INVALID_CONFIGURATION, `Configuration analysis failed: ${errors.join(', ')}`, config || undefined, undefined, {
|
|
171
|
+
validationErrors: errors,
|
|
172
|
+
...additionalContext
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Log a schema introspection error
|
|
177
|
+
*/
|
|
178
|
+
logSchemaError(modelName, fieldName, additionalContext) {
|
|
179
|
+
const message = fieldName
|
|
180
|
+
? `Schema validation failed for field "${fieldName}" in model "${modelName}"`
|
|
181
|
+
: `Schema data unavailable or invalid for model "${modelName}"`;
|
|
182
|
+
return this.logSelectionSetError(fieldName ? SelectionSetErrorType.FIELD_NOT_FOUND : SelectionSetErrorType.SCHEMA_UNAVAILABLE, message, undefined, undefined, {
|
|
183
|
+
modelName,
|
|
184
|
+
fieldName,
|
|
185
|
+
...additionalContext
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Log a complexity validation error
|
|
190
|
+
*/
|
|
191
|
+
logComplexityError(selectionSet, validationErrors, config) {
|
|
192
|
+
return this.logSelectionSetError(SelectionSetErrorType.COMPLEXITY_EXCEEDED, `Selection set complexity exceeded limits: ${validationErrors.join(', ')}`, config, selectionSet, {
|
|
193
|
+
complexityIssues: validationErrors,
|
|
194
|
+
selectionSetSize: selectionSet.length,
|
|
195
|
+
maxDepth: Math.max(...selectionSet.map(s => (s.match(/\./g) || []).length))
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Log a circular reference error
|
|
200
|
+
*/
|
|
201
|
+
logCircularReferenceError(circularPath, selectionSet, config) {
|
|
202
|
+
return this.logSelectionSetError(SelectionSetErrorType.CIRCULAR_REFERENCE, `Circular reference detected in path: ${circularPath}`, config, selectionSet, {
|
|
203
|
+
circularPath,
|
|
204
|
+
pathDepth: circularPath.split('.').length
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Log a successful error recovery
|
|
209
|
+
*/
|
|
210
|
+
logSuccessfulRecovery(originalError, recoveryMethod, finalSelectionSet) {
|
|
211
|
+
this.errorStats.recoveryAttempts++;
|
|
212
|
+
this.errorStats.successfulRecoveries++;
|
|
213
|
+
console.log('ErrorHandler: Successful error recovery:', {
|
|
214
|
+
originalError: {
|
|
215
|
+
type: originalError.type,
|
|
216
|
+
message: originalError.message
|
|
217
|
+
},
|
|
218
|
+
recoveryMethod,
|
|
219
|
+
finalSelectionSet,
|
|
220
|
+
recoveryRate: this.getRecoveryRate(),
|
|
221
|
+
timestamp: new Date().toISOString()
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Log a failed error recovery attempt
|
|
226
|
+
*/
|
|
227
|
+
logFailedRecovery(originalError, recoveryAttempts, finalError) {
|
|
228
|
+
this.errorStats.recoveryAttempts++;
|
|
229
|
+
const failureError = this.logSelectionSetError(SelectionSetErrorType.GRAPHQL_ERROR, `All recovery attempts failed: ${finalError instanceof Error ? finalError.message : String(finalError)}`, originalError.configuration, originalError.selectionSet, {
|
|
230
|
+
originalError: {
|
|
231
|
+
type: originalError.type,
|
|
232
|
+
message: originalError.message
|
|
233
|
+
},
|
|
234
|
+
recoveryAttempts,
|
|
235
|
+
finalError: {
|
|
236
|
+
message: finalError instanceof Error ? finalError.message : String(finalError),
|
|
237
|
+
type: typeof finalError
|
|
238
|
+
},
|
|
239
|
+
recoveryRate: this.getRecoveryRate()
|
|
240
|
+
});
|
|
241
|
+
console.error('ErrorHandler: All recovery attempts failed:', failureError);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Create a SelectionSetResult for a successful operation
|
|
245
|
+
*/
|
|
246
|
+
createSuccessResult(selectionSet, patternUsed, usedFallback = false) {
|
|
247
|
+
return {
|
|
248
|
+
success: true,
|
|
249
|
+
selectionSet: [...selectionSet],
|
|
250
|
+
usedFallback,
|
|
251
|
+
patternUsed
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Create a SelectionSetResult for a failed operation
|
|
256
|
+
*/
|
|
257
|
+
createErrorResult(error) {
|
|
258
|
+
return {
|
|
259
|
+
success: false,
|
|
260
|
+
error,
|
|
261
|
+
usedFallback: false
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Get error statistics for monitoring and debugging
|
|
266
|
+
*/
|
|
267
|
+
getErrorStats() {
|
|
268
|
+
const errorsByType = {};
|
|
269
|
+
this.errorStats.errorsByType.forEach((count, type) => {
|
|
270
|
+
errorsByType[type] = count;
|
|
271
|
+
});
|
|
272
|
+
return {
|
|
273
|
+
totalErrors: this.errorStats.totalErrors,
|
|
274
|
+
errorsByType,
|
|
275
|
+
lastError: this.errorStats.lastError,
|
|
276
|
+
recoveryAttempts: this.errorStats.recoveryAttempts,
|
|
277
|
+
successfulRecoveries: this.errorStats.successfulRecoveries,
|
|
278
|
+
recoveryRate: this.getRecoveryRate(),
|
|
279
|
+
recentErrors: this.getRecentErrors(10)
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Get recent error logs for debugging
|
|
284
|
+
*/
|
|
285
|
+
getRecentErrors(count = 10) {
|
|
286
|
+
return this.errorLogs
|
|
287
|
+
.slice(-count)
|
|
288
|
+
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Clear all error logs and reset statistics
|
|
292
|
+
*/
|
|
293
|
+
clearErrorLogs() {
|
|
294
|
+
this.errorLogs = [];
|
|
295
|
+
this.errorStats = {
|
|
296
|
+
totalErrors: 0,
|
|
297
|
+
errorsByType: new Map(),
|
|
298
|
+
lastError: null,
|
|
299
|
+
recoveryAttempts: 0,
|
|
300
|
+
successfulRecoveries: 0
|
|
301
|
+
};
|
|
302
|
+
console.log('ErrorHandler: Error logs and statistics cleared');
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Check if the system is experiencing high error rates
|
|
306
|
+
*/
|
|
307
|
+
isHighErrorRate(timeWindowMinutes = 5, threshold = 10) {
|
|
308
|
+
const cutoffTime = new Date(Date.now() - timeWindowMinutes * 60 * 1000);
|
|
309
|
+
const recentErrors = this.errorLogs.filter(error => error.timestamp > cutoffTime);
|
|
310
|
+
return recentErrors.length >= threshold;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Get error patterns for analysis
|
|
314
|
+
*/
|
|
315
|
+
getErrorPatterns() {
|
|
316
|
+
if (this.errorLogs.length === 0) {
|
|
317
|
+
return {
|
|
318
|
+
mostCommonErrorType: null,
|
|
319
|
+
mostProblematicModel: null,
|
|
320
|
+
mostProblematicField: null,
|
|
321
|
+
errorTrends: []
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
// Find most common error type
|
|
325
|
+
let mostCommonErrorType = null;
|
|
326
|
+
let maxErrorCount = 0;
|
|
327
|
+
this.errorStats.errorsByType.forEach((count, type) => {
|
|
328
|
+
if (count > maxErrorCount) {
|
|
329
|
+
maxErrorCount = count;
|
|
330
|
+
mostCommonErrorType = type;
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
// Find most problematic model
|
|
334
|
+
const modelCounts = new Map();
|
|
335
|
+
this.errorLogs.forEach(error => {
|
|
336
|
+
if (error.configuration?.relationshipModelName) {
|
|
337
|
+
const count = modelCounts.get(error.configuration.relationshipModelName) || 0;
|
|
338
|
+
modelCounts.set(error.configuration.relationshipModelName, count + 1);
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
let mostProblematicModel = null;
|
|
342
|
+
let maxModelCount = 0;
|
|
343
|
+
modelCounts.forEach((count, model) => {
|
|
344
|
+
if (count > maxModelCount) {
|
|
345
|
+
maxModelCount = count;
|
|
346
|
+
mostProblematicModel = model;
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
// Find most problematic field
|
|
350
|
+
const fieldCounts = new Map();
|
|
351
|
+
this.errorLogs.forEach(error => {
|
|
352
|
+
if (error.configuration?.fieldName) {
|
|
353
|
+
const count = fieldCounts.get(error.configuration.fieldName) || 0;
|
|
354
|
+
fieldCounts.set(error.configuration.fieldName, count + 1);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
let mostProblematicField = null;
|
|
358
|
+
let maxFieldCount = 0;
|
|
359
|
+
fieldCounts.forEach((count, field) => {
|
|
360
|
+
if (count > maxFieldCount) {
|
|
361
|
+
maxFieldCount = count;
|
|
362
|
+
mostProblematicField = field;
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
// Generate error trends by hour
|
|
366
|
+
const hourCounts = new Map();
|
|
367
|
+
this.errorLogs.forEach(error => {
|
|
368
|
+
const hour = error.timestamp.toISOString().substring(0, 13); // YYYY-MM-DDTHH
|
|
369
|
+
const count = hourCounts.get(hour) || 0;
|
|
370
|
+
hourCounts.set(hour, count + 1);
|
|
371
|
+
});
|
|
372
|
+
const errorTrends = Array.from(hourCounts.entries())
|
|
373
|
+
.map(([hour, errorCount]) => ({ hour, errorCount }))
|
|
374
|
+
.sort((a, b) => a.hour.localeCompare(b.hour));
|
|
375
|
+
return {
|
|
376
|
+
mostCommonErrorType,
|
|
377
|
+
mostProblematicModel,
|
|
378
|
+
mostProblematicField,
|
|
379
|
+
errorTrends
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Add error to the log with size management
|
|
384
|
+
*/
|
|
385
|
+
addErrorLog(error) {
|
|
386
|
+
this.errorLogs.push(error);
|
|
387
|
+
// Maintain maximum log size
|
|
388
|
+
if (this.errorLogs.length > this.MAX_ERROR_LOGS) {
|
|
389
|
+
this.errorLogs = this.errorLogs.slice(-this.MAX_ERROR_LOGS);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
/**
|
|
393
|
+
* Update error statistics
|
|
394
|
+
*/
|
|
395
|
+
updateErrorStats(error) {
|
|
396
|
+
this.errorStats.totalErrors++;
|
|
397
|
+
this.errorStats.lastError = error;
|
|
398
|
+
const currentCount = this.errorStats.errorsByType.get(error.type) || 0;
|
|
399
|
+
this.errorStats.errorsByType.set(error.type, currentCount + 1);
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Calculate recovery rate percentage
|
|
403
|
+
*/
|
|
404
|
+
getRecoveryRate() {
|
|
405
|
+
if (this.errorStats.recoveryAttempts === 0) {
|
|
406
|
+
return 0;
|
|
407
|
+
}
|
|
408
|
+
return Math.round((this.errorStats.successfulRecoveries / this.errorStats.recoveryAttempts) * 100);
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Log error to console with structured format
|
|
412
|
+
*/
|
|
413
|
+
logToConsole(error) {
|
|
414
|
+
const logData = {
|
|
415
|
+
type: error.type,
|
|
416
|
+
message: error.message,
|
|
417
|
+
timestamp: error.timestamp.toISOString(),
|
|
418
|
+
configuration: error.configuration ? {
|
|
419
|
+
relationshipModel: error.configuration.relationshipModelName,
|
|
420
|
+
baseModel: error.configuration.baseModelName,
|
|
421
|
+
fieldName: error.configuration.fieldName,
|
|
422
|
+
associatedWith: error.configuration.associatedWith
|
|
423
|
+
} : undefined,
|
|
424
|
+
selectionSet: error.selectionSet,
|
|
425
|
+
context: error.context
|
|
426
|
+
};
|
|
427
|
+
// Use appropriate console method based on error severity
|
|
428
|
+
switch (error.type) {
|
|
429
|
+
case SelectionSetErrorType.INVALID_CONFIGURATION:
|
|
430
|
+
case SelectionSetErrorType.GRAPHQL_ERROR:
|
|
431
|
+
console.error('ErrorHandler: Critical error:', logData);
|
|
432
|
+
break;
|
|
433
|
+
case SelectionSetErrorType.SCHEMA_UNAVAILABLE:
|
|
434
|
+
case SelectionSetErrorType.MODEL_NOT_FOUND:
|
|
435
|
+
case SelectionSetErrorType.FIELD_NOT_FOUND:
|
|
436
|
+
console.warn('ErrorHandler: Schema-related warning:', logData);
|
|
437
|
+
break;
|
|
438
|
+
case SelectionSetErrorType.COMPLEXITY_EXCEEDED:
|
|
439
|
+
case SelectionSetErrorType.CIRCULAR_REFERENCE:
|
|
440
|
+
console.warn('ErrorHandler: Complexity warning:', logData);
|
|
441
|
+
break;
|
|
442
|
+
default:
|
|
443
|
+
console.log('ErrorHandler: General error:', logData);
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ErrorHandlerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
448
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ErrorHandlerService, providedIn: 'root' });
|
|
449
|
+
}
|
|
450
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ErrorHandlerService, decorators: [{
|
|
451
|
+
type: Injectable,
|
|
452
|
+
args: [{
|
|
453
|
+
providedIn: 'root'
|
|
454
|
+
}]
|
|
455
|
+
}] });
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Service for analyzing relationship configurations to extract field mappings
|
|
459
|
+
*
|
|
460
|
+
* This service analyzes relationship configurations to determine target models
|
|
461
|
+
* and generate appropriate field selectors for GraphQL queries.
|
|
462
|
+
*/
|
|
463
|
+
class ConfigurationAnalyzerService {
|
|
464
|
+
errorHandler;
|
|
465
|
+
/**
|
|
466
|
+
* Common display field names to look for in models
|
|
467
|
+
*/
|
|
468
|
+
COMMON_DISPLAY_FIELDS = ['name', 'title', 'label', 'displayName', 'description'];
|
|
469
|
+
constructor(errorHandler) {
|
|
470
|
+
this.errorHandler = errorHandler;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Analyze a relationship configuration to extract field mappings and validate structure
|
|
474
|
+
* Enhanced with comprehensive error handling and logging
|
|
475
|
+
*/
|
|
476
|
+
analyzeConfiguration(config) {
|
|
477
|
+
const errors = [];
|
|
478
|
+
try {
|
|
479
|
+
// Validate configuration completeness
|
|
480
|
+
if (!config) {
|
|
481
|
+
const error = 'Configuration is null or undefined';
|
|
482
|
+
errors.push(error);
|
|
483
|
+
this.errorHandler.logConfigurationError(null, [error], {
|
|
484
|
+
method: 'analyzeConfiguration',
|
|
485
|
+
step: 'initial_validation'
|
|
486
|
+
});
|
|
487
|
+
return {
|
|
488
|
+
targetModelName: '',
|
|
489
|
+
fieldSelectors: [],
|
|
490
|
+
isValid: false,
|
|
491
|
+
errors
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
// Validate required fields
|
|
495
|
+
const requiredFields = [
|
|
496
|
+
{ field: 'relationshipModelName', value: config.relationshipModelName },
|
|
497
|
+
{ field: 'baseModelName', value: config.baseModelName },
|
|
498
|
+
{ field: 'fieldName', value: config.fieldName },
|
|
499
|
+
{ field: 'associatedWith', value: config.associatedWith }
|
|
500
|
+
];
|
|
501
|
+
for (const { field, value } of requiredFields) {
|
|
502
|
+
if (!value) {
|
|
503
|
+
errors.push(`${field} is required`);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (errors.length > 0) {
|
|
507
|
+
this.errorHandler.logConfigurationError(config, errors, {
|
|
508
|
+
method: 'analyzeConfiguration',
|
|
509
|
+
step: 'field_validation',
|
|
510
|
+
providedFields: {
|
|
511
|
+
relationshipModelName: !!config.relationshipModelName,
|
|
512
|
+
baseModelName: !!config.baseModelName,
|
|
513
|
+
fieldName: !!config.fieldName,
|
|
514
|
+
associatedWith: !!config.associatedWith
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
return {
|
|
518
|
+
targetModelName: '',
|
|
519
|
+
fieldSelectors: [],
|
|
520
|
+
isValid: false,
|
|
521
|
+
errors
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
// Extract target model name from the configuration
|
|
525
|
+
const targetModelName = this.extractTargetModelName(config);
|
|
526
|
+
if (!targetModelName) {
|
|
527
|
+
const error = 'Could not determine target model name from configuration';
|
|
528
|
+
errors.push(error);
|
|
529
|
+
this.errorHandler.logConfigurationError(config, [error], {
|
|
530
|
+
method: 'analyzeConfiguration',
|
|
531
|
+
step: 'target_model_extraction',
|
|
532
|
+
fieldName: config.fieldName
|
|
533
|
+
});
|
|
534
|
+
return {
|
|
535
|
+
targetModelName: '',
|
|
536
|
+
fieldSelectors: [],
|
|
537
|
+
isValid: false,
|
|
538
|
+
errors
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
// Generate field selectors for the target model
|
|
542
|
+
const fieldSelectors = this.determineFieldSelectorsEnhanced(targetModelName, config.fieldName);
|
|
543
|
+
if (!fieldSelectors || fieldSelectors.length === 0) {
|
|
544
|
+
const error = 'Could not generate field selectors for target model';
|
|
545
|
+
errors.push(error);
|
|
546
|
+
this.errorHandler.logConfigurationError(config, [error], {
|
|
547
|
+
method: 'analyzeConfiguration',
|
|
548
|
+
step: 'field_selector_generation',
|
|
549
|
+
targetModelName,
|
|
550
|
+
fieldName: config.fieldName
|
|
551
|
+
});
|
|
552
|
+
return {
|
|
553
|
+
targetModelName,
|
|
554
|
+
fieldSelectors: [],
|
|
555
|
+
isValid: false,
|
|
556
|
+
errors
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
console.log('ConfigurationAnalyzer: Successfully analyzed configuration:', {
|
|
560
|
+
targetModelName,
|
|
561
|
+
fieldSelectorsCount: fieldSelectors.length,
|
|
562
|
+
fieldName: config.fieldName
|
|
563
|
+
});
|
|
564
|
+
return {
|
|
565
|
+
targetModelName,
|
|
566
|
+
fieldSelectors,
|
|
567
|
+
isValid: true,
|
|
568
|
+
errors: []
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
catch (error) {
|
|
572
|
+
const errorMessage = `Analysis failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
573
|
+
errors.push(errorMessage);
|
|
574
|
+
this.errorHandler.logSelectionSetError(SelectionSetErrorType.INVALID_CONFIGURATION, errorMessage, config, undefined, {
|
|
575
|
+
method: 'analyzeConfiguration',
|
|
576
|
+
step: 'exception_handling',
|
|
577
|
+
originalError: {
|
|
578
|
+
message: error instanceof Error ? error.message : String(error),
|
|
579
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
580
|
+
type: typeof error
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
return {
|
|
584
|
+
targetModelName: '',
|
|
585
|
+
fieldSelectors: [],
|
|
586
|
+
isValid: false,
|
|
587
|
+
errors
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Extract the target model name from a relationship configuration
|
|
593
|
+
* Enhanced with error handling and validation
|
|
594
|
+
*
|
|
595
|
+
* For a FormViewField relationship, this would extract the target model
|
|
596
|
+
* that we want to display (e.g., "Field" from the FormViewField junction)
|
|
597
|
+
*/
|
|
598
|
+
extractTargetModelName(config) {
|
|
599
|
+
try {
|
|
600
|
+
if (!config || !config.fieldName) {
|
|
601
|
+
this.errorHandler.logSelectionSetError(SelectionSetErrorType.INVALID_CONFIGURATION, 'Cannot extract target model name: configuration or fieldName is missing', config, undefined, {
|
|
602
|
+
method: 'extractTargetModelName',
|
|
603
|
+
hasConfig: !!config,
|
|
604
|
+
hasFieldName: !!(config?.fieldName)
|
|
605
|
+
});
|
|
606
|
+
return '';
|
|
607
|
+
}
|
|
608
|
+
// The fieldName typically corresponds to the target model we want to access
|
|
609
|
+
// For example, if fieldName is "formView", the target model is "FormView"
|
|
610
|
+
// If fieldName is "service", the target model is "Service"
|
|
611
|
+
// Convert fieldName to PascalCase to match Amplify model naming conventions
|
|
612
|
+
const targetModelName = this.toPascalCase(config.fieldName);
|
|
613
|
+
if (!targetModelName) {
|
|
614
|
+
this.errorHandler.logSelectionSetError(SelectionSetErrorType.INVALID_CONFIGURATION, 'Failed to convert fieldName to PascalCase for target model name', config, undefined, {
|
|
615
|
+
method: 'extractTargetModelName',
|
|
616
|
+
fieldName: config.fieldName,
|
|
617
|
+
conversionResult: targetModelName
|
|
618
|
+
});
|
|
619
|
+
return '';
|
|
620
|
+
}
|
|
621
|
+
console.log(`ConfigurationAnalyzer: Extracted target model name "${targetModelName}" from field "${config.fieldName}"`);
|
|
622
|
+
return targetModelName;
|
|
623
|
+
}
|
|
624
|
+
catch (error) {
|
|
625
|
+
this.errorHandler.logSelectionSetError(SelectionSetErrorType.INVALID_CONFIGURATION, `Error extracting target model name: ${error instanceof Error ? error.message : String(error)}`, config, undefined, {
|
|
626
|
+
method: 'extractTargetModelName',
|
|
627
|
+
originalError: {
|
|
628
|
+
message: error instanceof Error ? error.message : String(error),
|
|
629
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
return '';
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Determine appropriate field selectors for a target model
|
|
637
|
+
*
|
|
638
|
+
* This generates GraphQL field selectors that include the relationship field
|
|
639
|
+
* and common display properties like id, name, title, etc.
|
|
640
|
+
*/
|
|
641
|
+
determineFieldSelectors(targetModel) {
|
|
642
|
+
if (!targetModel) {
|
|
643
|
+
return [];
|
|
644
|
+
}
|
|
645
|
+
const selectors = [];
|
|
646
|
+
// Always include the base record ID
|
|
647
|
+
selectors.push('id');
|
|
648
|
+
// Convert target model to camelCase for field access
|
|
649
|
+
const fieldName = this.toCamelCase(targetModel);
|
|
650
|
+
// Add the relationship field with its ID
|
|
651
|
+
selectors.push(`${fieldName}.id`);
|
|
652
|
+
// Add common display fields for the relationship
|
|
653
|
+
for (const displayField of this.COMMON_DISPLAY_FIELDS) {
|
|
654
|
+
selectors.push(`${fieldName}.${displayField}`);
|
|
655
|
+
}
|
|
656
|
+
return selectors;
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Parse and validate nested relationship paths
|
|
660
|
+
*
|
|
661
|
+
* Handles complex relationship configurations like "formView.user.profile"
|
|
662
|
+
* and validates for circular references
|
|
663
|
+
*/
|
|
664
|
+
parseNestedRelationshipPath(fieldName, maxDepth = 3) {
|
|
665
|
+
if (!fieldName) {
|
|
666
|
+
return [];
|
|
667
|
+
}
|
|
668
|
+
// Split the field name by dots to handle nested paths
|
|
669
|
+
const pathSegments = fieldName.split('.');
|
|
670
|
+
// Limit nesting depth to prevent overly complex queries
|
|
671
|
+
if (pathSegments.length > maxDepth) {
|
|
672
|
+
console.warn(`ConfigurationAnalyzer: Nested relationship path "${fieldName}" exceeds maximum depth of ${maxDepth}. Truncating.`);
|
|
673
|
+
pathSegments.splice(maxDepth);
|
|
674
|
+
}
|
|
675
|
+
// Check for circular references (same segment appearing multiple times)
|
|
676
|
+
const uniqueSegments = new Set(pathSegments);
|
|
677
|
+
if (uniqueSegments.size !== pathSegments.length) {
|
|
678
|
+
console.warn(`ConfigurationAnalyzer: Circular reference detected in path "${fieldName}". Using only unique segments.`);
|
|
679
|
+
return Array.from(uniqueSegments);
|
|
680
|
+
}
|
|
681
|
+
return pathSegments;
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Generate field selectors for nested relationship paths
|
|
685
|
+
*
|
|
686
|
+
* Creates GraphQL selectors that can handle nested relationships
|
|
687
|
+
* like "formView.user.profile.name"
|
|
688
|
+
*/
|
|
689
|
+
generateNestedFieldSelectors(fieldName, targetModel) {
|
|
690
|
+
const pathSegments = this.parseNestedRelationshipPath(fieldName);
|
|
691
|
+
const selectors = [];
|
|
692
|
+
// Always include the base record ID
|
|
693
|
+
selectors.push('id');
|
|
694
|
+
if (pathSegments.length === 0) {
|
|
695
|
+
return selectors;
|
|
696
|
+
}
|
|
697
|
+
// For single-level relationships, use the existing logic
|
|
698
|
+
if (pathSegments.length === 1) {
|
|
699
|
+
const camelCaseField = this.toCamelCase(pathSegments[0]);
|
|
700
|
+
selectors.push(`${camelCaseField}.id`);
|
|
701
|
+
// Add common display fields
|
|
702
|
+
for (const displayField of this.COMMON_DISPLAY_FIELDS) {
|
|
703
|
+
selectors.push(`${camelCaseField}.${displayField}`);
|
|
704
|
+
}
|
|
705
|
+
return selectors;
|
|
706
|
+
}
|
|
707
|
+
// For nested relationships, build the path progressively
|
|
708
|
+
let currentPath = '';
|
|
709
|
+
for (let i = 0; i < pathSegments.length; i++) {
|
|
710
|
+
const segment = this.toCamelCase(pathSegments[i]);
|
|
711
|
+
if (i === 0) {
|
|
712
|
+
currentPath = segment;
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
currentPath += `.${segment}`;
|
|
716
|
+
}
|
|
717
|
+
// Add ID for each level
|
|
718
|
+
selectors.push(`${currentPath}.id`);
|
|
719
|
+
// Add display fields only for the final level to avoid overly complex queries
|
|
720
|
+
if (i === pathSegments.length - 1) {
|
|
721
|
+
for (const displayField of this.COMMON_DISPLAY_FIELDS) {
|
|
722
|
+
selectors.push(`${currentPath}.${displayField}`);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return selectors;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Enhanced field selector determination that supports nested paths
|
|
730
|
+
*/
|
|
731
|
+
determineFieldSelectorsEnhanced(targetModel, fieldName) {
|
|
732
|
+
if (!targetModel) {
|
|
733
|
+
return [];
|
|
734
|
+
}
|
|
735
|
+
// If no specific field name provided, use the target model name
|
|
736
|
+
const effectiveFieldName = fieldName || this.toCamelCase(targetModel);
|
|
737
|
+
// Check if this is a nested relationship path
|
|
738
|
+
if (effectiveFieldName.includes('.')) {
|
|
739
|
+
return this.generateNestedFieldSelectors(effectiveFieldName, targetModel);
|
|
740
|
+
}
|
|
741
|
+
// Use the standard field selector generation for simple relationships
|
|
742
|
+
return this.determineFieldSelectors(targetModel);
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Convert a string to PascalCase (first letter uppercase, rest camelCase)
|
|
746
|
+
*/
|
|
747
|
+
toPascalCase(str) {
|
|
748
|
+
if (!str)
|
|
749
|
+
return '';
|
|
750
|
+
// Handle camelCase to PascalCase
|
|
751
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Convert a string to camelCase (first letter lowercase)
|
|
755
|
+
*/
|
|
756
|
+
toCamelCase(str) {
|
|
757
|
+
if (!str)
|
|
758
|
+
return '';
|
|
759
|
+
// Handle PascalCase to camelCase
|
|
760
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
761
|
+
}
|
|
762
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ConfigurationAnalyzerService, deps: [{ token: ErrorHandlerService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
763
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ConfigurationAnalyzerService, providedIn: 'root' });
|
|
764
|
+
}
|
|
765
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ConfigurationAnalyzerService, decorators: [{
|
|
766
|
+
type: Injectable,
|
|
767
|
+
args: [{
|
|
768
|
+
providedIn: 'root'
|
|
769
|
+
}]
|
|
770
|
+
}], ctorParameters: () => [{ type: ErrorHandlerService }] });
|
|
771
|
+
|
|
772
|
+
/**
|
|
773
|
+
* Service for introspecting Amplify schema data to validate and enhance selection sets
|
|
774
|
+
*
|
|
775
|
+
* This service leverages Amplify's schema introspection data to validate field existence,
|
|
776
|
+
* extract model information, and provide schema-aware field selection for GraphQL queries.
|
|
777
|
+
*/
|
|
778
|
+
class SchemaIntrospectorService {
|
|
779
|
+
errorHandler;
|
|
780
|
+
amplifyOutputs = null;
|
|
781
|
+
/**
|
|
782
|
+
* Common display field names that are typically used for showing model data
|
|
783
|
+
*/
|
|
784
|
+
COMMON_DISPLAY_FIELDS = ['name', 'title', 'label', 'displayName', 'description'];
|
|
785
|
+
/**
|
|
786
|
+
* System fields that are typically not used for display
|
|
787
|
+
*/
|
|
788
|
+
SYSTEM_FIELDS = ['id', 'createdAt', 'updatedAt', 'owner'];
|
|
789
|
+
constructor(errorHandler) {
|
|
790
|
+
this.errorHandler = errorHandler;
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Initialize the service with Amplify outputs data
|
|
794
|
+
* Enhanced with error handling and validation
|
|
795
|
+
* This should be called by consuming applications to provide schema data
|
|
796
|
+
*/
|
|
797
|
+
initializeSchema(amplifyOutputs) {
|
|
798
|
+
try {
|
|
799
|
+
if (!amplifyOutputs) {
|
|
800
|
+
this.errorHandler.logSchemaError('', undefined, {
|
|
801
|
+
method: 'initializeSchema',
|
|
802
|
+
error: 'amplifyOutputs is null or undefined'
|
|
803
|
+
});
|
|
804
|
+
console.warn('SchemaIntrospectorService: Cannot initialize with null or undefined amplifyOutputs');
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
// Validate schema structure
|
|
808
|
+
if (!amplifyOutputs.data || !amplifyOutputs.data.model_introspection || !amplifyOutputs.data.model_introspection.models) {
|
|
809
|
+
this.errorHandler.logSchemaError('', undefined, {
|
|
810
|
+
method: 'initializeSchema',
|
|
811
|
+
error: 'Invalid schema structure',
|
|
812
|
+
hasData: !!amplifyOutputs.data,
|
|
813
|
+
hasModelIntrospection: !!(amplifyOutputs.data?.model_introspection),
|
|
814
|
+
hasModels: !!(amplifyOutputs.data?.model_introspection?.models)
|
|
815
|
+
});
|
|
816
|
+
console.warn('SchemaIntrospectorService: Invalid schema structure in amplifyOutputs');
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
this.amplifyOutputs = amplifyOutputs;
|
|
820
|
+
const modelCount = Object.keys(amplifyOutputs.data.model_introspection.models).length;
|
|
821
|
+
console.log(`SchemaIntrospectorService: Successfully initialized with schema data (${modelCount} models)`);
|
|
822
|
+
}
|
|
823
|
+
catch (error) {
|
|
824
|
+
this.errorHandler.logSelectionSetError(SelectionSetErrorType.SCHEMA_UNAVAILABLE, `Error initializing schema: ${error instanceof Error ? error.message : String(error)}`, undefined, undefined, {
|
|
825
|
+
method: 'initializeSchema',
|
|
826
|
+
originalError: {
|
|
827
|
+
message: error instanceof Error ? error.message : String(error),
|
|
828
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
console.error('SchemaIntrospectorService: Error initializing schema:', error);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Get field information for a specific model from the schema
|
|
836
|
+
* Enhanced with comprehensive error handling and logging
|
|
837
|
+
*/
|
|
838
|
+
getModelFields(modelName) {
|
|
839
|
+
try {
|
|
840
|
+
if (!this.isSchemaAvailable()) {
|
|
841
|
+
this.errorHandler.logSchemaError(modelName, undefined, {
|
|
842
|
+
method: 'getModelFields',
|
|
843
|
+
error: 'Schema data not available'
|
|
844
|
+
});
|
|
845
|
+
console.warn('SchemaIntrospectorService: Schema data not available');
|
|
846
|
+
return [];
|
|
847
|
+
}
|
|
848
|
+
if (!modelName) {
|
|
849
|
+
this.errorHandler.logSchemaError('', undefined, {
|
|
850
|
+
method: 'getModelFields',
|
|
851
|
+
error: 'Model name is empty or undefined'
|
|
852
|
+
});
|
|
853
|
+
return [];
|
|
854
|
+
}
|
|
855
|
+
const models = this.amplifyOutputs.data.model_introspection.models;
|
|
856
|
+
const modelData = models[modelName];
|
|
857
|
+
if (!modelData) {
|
|
858
|
+
this.errorHandler.logSchemaError(modelName, undefined, {
|
|
859
|
+
method: 'getModelFields',
|
|
860
|
+
error: 'Model not found in schema',
|
|
861
|
+
availableModels: Object.keys(models)
|
|
862
|
+
});
|
|
863
|
+
console.warn(`SchemaIntrospectorService: Model "${modelName}" not found in schema`);
|
|
864
|
+
return [];
|
|
865
|
+
}
|
|
866
|
+
if (!modelData.fields) {
|
|
867
|
+
this.errorHandler.logSchemaError(modelName, undefined, {
|
|
868
|
+
method: 'getModelFields',
|
|
869
|
+
error: 'Model has no fields property',
|
|
870
|
+
modelData: Object.keys(modelData)
|
|
871
|
+
});
|
|
872
|
+
console.warn(`SchemaIntrospectorService: Model "${modelName}" has no fields`);
|
|
873
|
+
return [];
|
|
874
|
+
}
|
|
875
|
+
const fields = [];
|
|
876
|
+
const fieldValues = Object.values(modelData.fields);
|
|
877
|
+
for (const field of fieldValues) {
|
|
878
|
+
try {
|
|
879
|
+
const fieldData = field;
|
|
880
|
+
if (!fieldData.name) {
|
|
881
|
+
console.warn(`SchemaIntrospectorService: Field in model "${modelName}" has no name property`);
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
fields.push({
|
|
885
|
+
name: fieldData.name,
|
|
886
|
+
type: this.determineFieldType(fieldData.type),
|
|
887
|
+
isRequired: fieldData.isRequired || false,
|
|
888
|
+
isRelationship: this.isRelationshipField(fieldData)
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
catch (fieldError) {
|
|
892
|
+
console.warn(`SchemaIntrospectorService: Error processing field in model "${modelName}":`, fieldError);
|
|
893
|
+
// Continue processing other fields
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
console.log(`SchemaIntrospectorService: Retrieved ${fields.length} fields for model "${modelName}"`);
|
|
897
|
+
return fields;
|
|
898
|
+
}
|
|
899
|
+
catch (error) {
|
|
900
|
+
this.errorHandler.logSelectionSetError(SelectionSetErrorType.MODEL_NOT_FOUND, `Error getting fields for model "${modelName}": ${error instanceof Error ? error.message : String(error)}`, undefined, undefined, {
|
|
901
|
+
method: 'getModelFields',
|
|
902
|
+
modelName,
|
|
903
|
+
originalError: {
|
|
904
|
+
message: error instanceof Error ? error.message : String(error),
|
|
905
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
console.error(`SchemaIntrospectorService: Error getting fields for model "${modelName}":`, error);
|
|
909
|
+
return [];
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Validate that a specific field exists in a model
|
|
914
|
+
*/
|
|
915
|
+
validateFieldExists(modelName, fieldName) {
|
|
916
|
+
if (!this.isSchemaAvailable() || !modelName || !fieldName) {
|
|
917
|
+
return false;
|
|
918
|
+
}
|
|
919
|
+
try {
|
|
920
|
+
const models = this.amplifyOutputs.data.model_introspection.models;
|
|
921
|
+
const modelData = models[modelName];
|
|
922
|
+
if (!modelData || !modelData.fields) {
|
|
923
|
+
return false;
|
|
924
|
+
}
|
|
925
|
+
return fieldName in modelData.fields;
|
|
926
|
+
}
|
|
927
|
+
catch (error) {
|
|
928
|
+
console.error(`SchemaIntrospectorService: Error validating field "${fieldName}" in model "${modelName}":`, error);
|
|
929
|
+
return false;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* Get common display fields for a model (e.g., name, title, label)
|
|
934
|
+
*/
|
|
935
|
+
getCommonDisplayFields(modelName) {
|
|
936
|
+
if (!this.isSchemaAvailable()) {
|
|
937
|
+
// Fallback: return conservative display fields when schema is unavailable
|
|
938
|
+
return this.getConservativeDisplayFields(modelName);
|
|
939
|
+
}
|
|
940
|
+
const modelFields = this.getModelFields(modelName);
|
|
941
|
+
const displayFields = [];
|
|
942
|
+
// Always include id for relationships
|
|
943
|
+
if (this.validateFieldExists(modelName, 'id')) {
|
|
944
|
+
displayFields.push('id');
|
|
945
|
+
}
|
|
946
|
+
// Look for common display fields in the model
|
|
947
|
+
for (const commonField of this.COMMON_DISPLAY_FIELDS) {
|
|
948
|
+
const field = modelFields.find(f => f.name === commonField);
|
|
949
|
+
if (field && !field.isRelationship) {
|
|
950
|
+
displayFields.push(commonField);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
// If no common display fields found, include the first non-system, non-relationship field
|
|
954
|
+
if (displayFields.length <= 1) { // Only id was added
|
|
955
|
+
const firstDisplayField = modelFields.find(f => !this.SYSTEM_FIELDS.includes(f.name) &&
|
|
956
|
+
!f.isRelationship &&
|
|
957
|
+
f.type === 'string');
|
|
958
|
+
if (firstDisplayField && !displayFields.includes(firstDisplayField.name)) {
|
|
959
|
+
displayFields.push(firstDisplayField.name);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
return displayFields;
|
|
963
|
+
}
|
|
964
|
+
/**
|
|
965
|
+
* Check if schema introspection data is available
|
|
966
|
+
*/
|
|
967
|
+
isSchemaAvailable() {
|
|
968
|
+
return !!(this.amplifyOutputs &&
|
|
969
|
+
this.amplifyOutputs.data &&
|
|
970
|
+
this.amplifyOutputs.data.model_introspection &&
|
|
971
|
+
this.amplifyOutputs.data.model_introspection.models);
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Get conservative display fields when schema data is unavailable
|
|
975
|
+
* This provides a safe fallback that works with most Amplify models
|
|
976
|
+
*/
|
|
977
|
+
getConservativeDisplayFields(modelName) {
|
|
978
|
+
console.warn(`SchemaIntrospectorService: Schema unavailable, using conservative display fields for model "${modelName}"`);
|
|
979
|
+
// Return a conservative set of fields that are commonly available
|
|
980
|
+
const conservativeFields = ['id'];
|
|
981
|
+
// Add all common display field names - the GraphQL query will simply ignore non-existent fields
|
|
982
|
+
conservativeFields.push(...this.COMMON_DISPLAY_FIELDS);
|
|
983
|
+
return conservativeFields;
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Get model fields with fallback behavior when schema is unavailable
|
|
987
|
+
*/
|
|
988
|
+
getModelFieldsWithFallback(modelName) {
|
|
989
|
+
if (this.isSchemaAvailable()) {
|
|
990
|
+
return this.getModelFields(modelName);
|
|
991
|
+
}
|
|
992
|
+
// Fallback: return conservative field set
|
|
993
|
+
console.warn(`SchemaIntrospectorService: Schema unavailable, using conservative field set for model "${modelName}"`);
|
|
994
|
+
const conservativeFields = [
|
|
995
|
+
{ name: 'id', type: 'id', isRequired: true, isRelationship: false }
|
|
996
|
+
];
|
|
997
|
+
// Add common display fields as optional string fields
|
|
998
|
+
for (const fieldName of this.COMMON_DISPLAY_FIELDS) {
|
|
999
|
+
conservativeFields.push({
|
|
1000
|
+
name: fieldName,
|
|
1001
|
+
type: 'string',
|
|
1002
|
+
isRequired: false,
|
|
1003
|
+
isRelationship: false
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
return conservativeFields;
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Validate field existence with fallback behavior
|
|
1010
|
+
* When schema is unavailable, assumes common fields exist
|
|
1011
|
+
*/
|
|
1012
|
+
validateFieldExistsWithFallback(modelName, fieldName) {
|
|
1013
|
+
if (this.isSchemaAvailable()) {
|
|
1014
|
+
return this.validateFieldExists(modelName, fieldName);
|
|
1015
|
+
}
|
|
1016
|
+
// Fallback: assume common fields exist
|
|
1017
|
+
const commonFields = ['id', ...this.COMMON_DISPLAY_FIELDS];
|
|
1018
|
+
const fieldExists = commonFields.includes(fieldName);
|
|
1019
|
+
if (!fieldExists) {
|
|
1020
|
+
console.warn(`SchemaIntrospectorService: Schema unavailable, cannot validate field "${fieldName}" in model "${modelName}". Assuming it does not exist.`);
|
|
1021
|
+
}
|
|
1022
|
+
return fieldExists;
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Get safe field selectors that work even when schema data is unavailable
|
|
1026
|
+
* This method prioritizes reliability over completeness
|
|
1027
|
+
*/
|
|
1028
|
+
getSafeFieldSelectors(modelName, fieldName) {
|
|
1029
|
+
const selectors = ['id']; // Always safe to include
|
|
1030
|
+
if (!fieldName) {
|
|
1031
|
+
return selectors;
|
|
1032
|
+
}
|
|
1033
|
+
// Add the relationship field with id (safe even if field doesn't exist)
|
|
1034
|
+
selectors.push(`${fieldName}.id`);
|
|
1035
|
+
if (this.isSchemaAvailable()) {
|
|
1036
|
+
// Use schema-aware field selection
|
|
1037
|
+
const displayFields = this.getCommonDisplayFields(modelName);
|
|
1038
|
+
for (const displayField of displayFields) {
|
|
1039
|
+
if (displayField !== 'id') { // Already added
|
|
1040
|
+
selectors.push(`${fieldName}.${displayField}`);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
else {
|
|
1045
|
+
// Use conservative field selection
|
|
1046
|
+
const conservativeFields = this.getConservativeDisplayFields(modelName);
|
|
1047
|
+
for (const conservativeField of conservativeFields) {
|
|
1048
|
+
if (conservativeField !== 'id') { // Already added
|
|
1049
|
+
selectors.push(`${fieldName}.${conservativeField}`);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
return selectors;
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Get schema status information for debugging
|
|
1057
|
+
*/
|
|
1058
|
+
getSchemaStatus() {
|
|
1059
|
+
const status = {
|
|
1060
|
+
isAvailable: this.isSchemaAvailable(),
|
|
1061
|
+
modelCount: 0,
|
|
1062
|
+
availableModels: [],
|
|
1063
|
+
lastError: undefined
|
|
1064
|
+
};
|
|
1065
|
+
if (status.isAvailable) {
|
|
1066
|
+
try {
|
|
1067
|
+
status.availableModels = this.getAvailableModels();
|
|
1068
|
+
status.modelCount = status.availableModels.length;
|
|
1069
|
+
}
|
|
1070
|
+
catch (error) {
|
|
1071
|
+
status.lastError = error instanceof Error ? error.message : String(error);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
else {
|
|
1075
|
+
status.lastError = 'Schema data not available or invalid structure';
|
|
1076
|
+
}
|
|
1077
|
+
return status;
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* Get all available model names from the schema
|
|
1081
|
+
*/
|
|
1082
|
+
getAvailableModels() {
|
|
1083
|
+
if (!this.isSchemaAvailable()) {
|
|
1084
|
+
return [];
|
|
1085
|
+
}
|
|
1086
|
+
try {
|
|
1087
|
+
return Object.keys(this.amplifyOutputs.data.model_introspection.models);
|
|
1088
|
+
}
|
|
1089
|
+
catch (error) {
|
|
1090
|
+
console.error('SchemaIntrospectorService: Error getting available models:', error);
|
|
1091
|
+
return [];
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
/**
|
|
1095
|
+
* Get detailed information about a specific model
|
|
1096
|
+
*/
|
|
1097
|
+
getModelInfo(modelName) {
|
|
1098
|
+
if (!this.isSchemaAvailable() || !modelName) {
|
|
1099
|
+
return null;
|
|
1100
|
+
}
|
|
1101
|
+
try {
|
|
1102
|
+
const models = this.amplifyOutputs.data.model_introspection.models;
|
|
1103
|
+
return models[modelName] || null;
|
|
1104
|
+
}
|
|
1105
|
+
catch (error) {
|
|
1106
|
+
console.error(`SchemaIntrospectorService: Error getting model info for "${modelName}":`, error);
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Determine the simplified type of a field from Amplify schema data
|
|
1112
|
+
*/
|
|
1113
|
+
determineFieldType(fieldType) {
|
|
1114
|
+
if (typeof fieldType === 'string') {
|
|
1115
|
+
return fieldType.toLowerCase();
|
|
1116
|
+
}
|
|
1117
|
+
if (typeof fieldType === 'object') {
|
|
1118
|
+
if (fieldType.model) {
|
|
1119
|
+
return 'relationship';
|
|
1120
|
+
}
|
|
1121
|
+
if (fieldType.enum) {
|
|
1122
|
+
return 'enum';
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
return 'unknown';
|
|
1126
|
+
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Check if a field represents a relationship to another model
|
|
1129
|
+
*/
|
|
1130
|
+
isRelationshipField(fieldData) {
|
|
1131
|
+
return !!(fieldData.type &&
|
|
1132
|
+
typeof fieldData.type === 'object' &&
|
|
1133
|
+
fieldData.type.model);
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* Get relationship information for a field
|
|
1137
|
+
*/
|
|
1138
|
+
getRelationshipInfo(modelName, fieldName) {
|
|
1139
|
+
if (!this.validateFieldExists(modelName, fieldName)) {
|
|
1140
|
+
return null;
|
|
1141
|
+
}
|
|
1142
|
+
try {
|
|
1143
|
+
const models = this.amplifyOutputs.data.model_introspection.models;
|
|
1144
|
+
const modelData = models[modelName];
|
|
1145
|
+
const fieldData = modelData.fields[fieldName];
|
|
1146
|
+
if (!this.isRelationshipField(fieldData)) {
|
|
1147
|
+
return null;
|
|
1148
|
+
}
|
|
1149
|
+
return {
|
|
1150
|
+
targetModel: fieldData.type.model,
|
|
1151
|
+
connectionType: fieldData.association?.connectionType,
|
|
1152
|
+
associatedWith: fieldData.association?.associatedWith,
|
|
1153
|
+
targetNames: fieldData.association?.targetNames
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
catch (error) {
|
|
1157
|
+
console.error(`SchemaIntrospectorService: Error getting relationship info for "${modelName}.${fieldName}":`, error);
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SchemaIntrospectorService, deps: [{ token: ErrorHandlerService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1162
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SchemaIntrospectorService, providedIn: 'root' });
|
|
1163
|
+
}
|
|
1164
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SchemaIntrospectorService, decorators: [{
|
|
1165
|
+
type: Injectable,
|
|
1166
|
+
args: [{
|
|
1167
|
+
providedIn: 'root'
|
|
1168
|
+
}]
|
|
1169
|
+
}], ctorParameters: () => [{ type: ErrorHandlerService }] });
|
|
1170
|
+
|
|
1171
|
+
/**
|
|
1172
|
+
* Service for generating dynamic GraphQL selection sets for Amplify relationships
|
|
1173
|
+
*
|
|
1174
|
+
* This service replaces hardcoded selection set logic with dynamic generation
|
|
1175
|
+
* based on relationship configurations, schema introspection, and intelligent caching.
|
|
1176
|
+
*/
|
|
1177
|
+
class SelectionSetGeneratorService {
|
|
1178
|
+
configurationAnalyzer;
|
|
1179
|
+
schemaIntrospector;
|
|
1180
|
+
errorHandler;
|
|
1181
|
+
/**
|
|
1182
|
+
* Cache for generated selection sets to improve performance
|
|
1183
|
+
*/
|
|
1184
|
+
selectionSetCache = new Map();
|
|
1185
|
+
/**
|
|
1186
|
+
* Default configuration for selection set generation
|
|
1187
|
+
*/
|
|
1188
|
+
DEFAULT_CONFIG = {
|
|
1189
|
+
pattern: SelectionSetPattern.SELECTIVE,
|
|
1190
|
+
maxDepth: 3,
|
|
1191
|
+
maxFields: 10,
|
|
1192
|
+
validateWithSchema: true,
|
|
1193
|
+
requiredFields: ['id'],
|
|
1194
|
+
complexityLimits: {
|
|
1195
|
+
maxNestingDepth: 3,
|
|
1196
|
+
maxFieldCount: 20,
|
|
1197
|
+
maxWildcardSelections: 2,
|
|
1198
|
+
preventCircularReferences: true,
|
|
1199
|
+
maxSelectorLength: 100
|
|
1200
|
+
}
|
|
1201
|
+
};
|
|
1202
|
+
/**
|
|
1203
|
+
* Default display optimization configuration
|
|
1204
|
+
*/
|
|
1205
|
+
DEFAULT_DISPLAY_CONFIG = {
|
|
1206
|
+
enabled: true,
|
|
1207
|
+
commonDisplayFields: ['id', 'name', 'title', 'label', 'displayName', 'description'],
|
|
1208
|
+
excludeFromOptimization: ['id', 'createdAt', 'updatedAt'],
|
|
1209
|
+
prioritizeDisplayFields: true,
|
|
1210
|
+
maxNonDisplayFields: 5
|
|
1211
|
+
};
|
|
1212
|
+
constructor(configurationAnalyzer, schemaIntrospector, errorHandler) {
|
|
1213
|
+
this.configurationAnalyzer = configurationAnalyzer;
|
|
1214
|
+
this.schemaIntrospector = schemaIntrospector;
|
|
1215
|
+
this.errorHandler = errorHandler;
|
|
1216
|
+
}
|
|
1217
|
+
/**
|
|
1218
|
+
* Generate a selection set for a given relationship configuration
|
|
1219
|
+
* Enhanced with comprehensive error handling and logging
|
|
1220
|
+
*/
|
|
1221
|
+
generateSelectionSet(config) {
|
|
1222
|
+
try {
|
|
1223
|
+
// Validate input configuration
|
|
1224
|
+
if (!config) {
|
|
1225
|
+
const error = this.errorHandler.logSelectionSetError(SelectionSetErrorType.INVALID_CONFIGURATION, 'Configuration is null or undefined', config);
|
|
1226
|
+
return this.handleGenerationError(error, '');
|
|
1227
|
+
}
|
|
1228
|
+
// Check cache first
|
|
1229
|
+
const cacheKey = this.generateCacheKey(config);
|
|
1230
|
+
const cached = this.getCachedSelectionSet(cacheKey);
|
|
1231
|
+
if (cached) {
|
|
1232
|
+
console.log('SelectionSetGenerator: Using cached selection set for config:', config.fieldName);
|
|
1233
|
+
return cached.selectionSet;
|
|
1234
|
+
}
|
|
1235
|
+
// Analyze the configuration
|
|
1236
|
+
const analysisResult = this.configurationAnalyzer.analyzeConfiguration(config);
|
|
1237
|
+
if (!analysisResult || !analysisResult.isValid) {
|
|
1238
|
+
const errors = analysisResult?.errors || ['Invalid configuration'];
|
|
1239
|
+
const error = this.errorHandler.logConfigurationError(config, errors, {
|
|
1240
|
+
analysisResult,
|
|
1241
|
+
cacheKey
|
|
1242
|
+
});
|
|
1243
|
+
return this.handleGenerationError(error, config.fieldName);
|
|
1244
|
+
}
|
|
1245
|
+
// Generate selection set based on analysis and schema availability
|
|
1246
|
+
const selectionSet = this.generateSelectionSetFromAnalysis(config, analysisResult);
|
|
1247
|
+
// Validate the generated selection set
|
|
1248
|
+
if (!this.validateSelectionSet(selectionSet)) {
|
|
1249
|
+
const error = this.errorHandler.logSelectionSetError(SelectionSetErrorType.INVALID_CONFIGURATION, 'Generated selection set failed validation', config, selectionSet, { analysisResult });
|
|
1250
|
+
return this.handleGenerationError(error, config.fieldName);
|
|
1251
|
+
}
|
|
1252
|
+
// Cache the result
|
|
1253
|
+
this.cacheSelectionSet(cacheKey, selectionSet);
|
|
1254
|
+
console.log('SelectionSetGenerator: Successfully generated selection set:', {
|
|
1255
|
+
config: config.fieldName,
|
|
1256
|
+
selectionSetLength: selectionSet.length,
|
|
1257
|
+
selectionSet
|
|
1258
|
+
});
|
|
1259
|
+
return selectionSet;
|
|
1260
|
+
}
|
|
1261
|
+
catch (error) {
|
|
1262
|
+
const selectionSetError = this.errorHandler.logSelectionSetError(SelectionSetErrorType.GRAPHQL_ERROR, `Unexpected error during selection set generation: ${error instanceof Error ? error.message : String(error)}`, config, undefined, {
|
|
1263
|
+
originalError: {
|
|
1264
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1265
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
1266
|
+
type: typeof error
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
return this.handleGenerationError(selectionSetError, config?.fieldName || '');
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Generate a fallback selection set when primary generation fails
|
|
1274
|
+
* Uses progressive fallback strategies: comprehensive → selective → minimal
|
|
1275
|
+
* Enhanced with comprehensive error handling and logging
|
|
1276
|
+
*/
|
|
1277
|
+
generateFallbackSelectionSet(fieldName) {
|
|
1278
|
+
try {
|
|
1279
|
+
if (!fieldName) {
|
|
1280
|
+
const error = this.errorHandler.logSelectionSetError(SelectionSetErrorType.INVALID_CONFIGURATION, 'Field name is empty or undefined for fallback generation', undefined, undefined, { fieldName });
|
|
1281
|
+
return this.generateMinimalFallbackSelectionSet();
|
|
1282
|
+
}
|
|
1283
|
+
console.warn(`SelectionSetGenerator: Using fallback selection set for field "${fieldName}"`);
|
|
1284
|
+
// Try progressive fallback strategies
|
|
1285
|
+
try {
|
|
1286
|
+
// Strategy 1: Comprehensive fallback (if schema is available)
|
|
1287
|
+
if (this.schemaIntrospector.isSchemaAvailable()) {
|
|
1288
|
+
const result = this.generateComprehensiveFallbackSelectionSet(fieldName);
|
|
1289
|
+
console.log(`SelectionSetGenerator: Comprehensive fallback successful for field "${fieldName}"`);
|
|
1290
|
+
return result;
|
|
1291
|
+
}
|
|
1292
|
+
// Strategy 2: Selective fallback (configuration-based)
|
|
1293
|
+
const result = this.generateSelectiveFallbackSelectionSet(fieldName);
|
|
1294
|
+
console.log(`SelectionSetGenerator: Selective fallback successful for field "${fieldName}"`);
|
|
1295
|
+
return result;
|
|
1296
|
+
}
|
|
1297
|
+
catch (error) {
|
|
1298
|
+
const selectionSetError = this.errorHandler.logSelectionSetError(SelectionSetErrorType.GRAPHQL_ERROR, `Error in fallback generation, using minimal fallback: ${error instanceof Error ? error.message : String(error)}`, undefined, undefined, {
|
|
1299
|
+
fieldName,
|
|
1300
|
+
originalError: {
|
|
1301
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1302
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
1303
|
+
},
|
|
1304
|
+
schemaAvailable: this.schemaIntrospector.isSchemaAvailable()
|
|
1305
|
+
});
|
|
1306
|
+
// Strategy 3: Minimal fallback (last resort)
|
|
1307
|
+
return this.generateMinimalFallbackSelectionSet(fieldName);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
catch (error) {
|
|
1311
|
+
// Absolute last resort - return basic selection set
|
|
1312
|
+
const selectionSetError = this.errorHandler.logSelectionSetError(SelectionSetErrorType.GRAPHQL_ERROR, `Critical error in fallback generation: ${error instanceof Error ? error.message : String(error)}`, undefined, undefined, {
|
|
1313
|
+
fieldName,
|
|
1314
|
+
criticalError: {
|
|
1315
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1316
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
1317
|
+
}
|
|
1318
|
+
});
|
|
1319
|
+
console.error('SelectionSetGenerator: Critical error in fallback generation, returning absolute minimal fallback');
|
|
1320
|
+
return ['id'];
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
/**
|
|
1324
|
+
* Generate comprehensive fallback selection set using schema data
|
|
1325
|
+
* This is the first fallback strategy when schema is available
|
|
1326
|
+
*/
|
|
1327
|
+
generateComprehensiveFallbackSelectionSet(fieldName) {
|
|
1328
|
+
console.log(`SelectionSetGenerator: Using comprehensive fallback strategy for field "${fieldName}"`);
|
|
1329
|
+
const selectionSet = ['id'];
|
|
1330
|
+
try {
|
|
1331
|
+
// Use schema introspector to get safe field selectors
|
|
1332
|
+
const safeSelectors = this.schemaIntrospector.getSafeFieldSelectors('', fieldName);
|
|
1333
|
+
// Add all safe selectors
|
|
1334
|
+
for (const selector of safeSelectors) {
|
|
1335
|
+
if (!selectionSet.includes(selector)) {
|
|
1336
|
+
selectionSet.push(selector);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
// Apply display optimization for fallback
|
|
1340
|
+
const mockConfig = {
|
|
1341
|
+
relationshipModelName: '',
|
|
1342
|
+
baseModelName: '',
|
|
1343
|
+
fieldName,
|
|
1344
|
+
associatedWith: ''
|
|
1345
|
+
};
|
|
1346
|
+
const optimizedSet = this.applyDisplayOptimization(selectionSet, mockConfig);
|
|
1347
|
+
// Apply complexity limits to prevent overly expensive queries
|
|
1348
|
+
return this.applyComplexityLimits(optimizedSet);
|
|
1349
|
+
}
|
|
1350
|
+
catch (error) {
|
|
1351
|
+
console.warn('SelectionSetGenerator: Comprehensive fallback failed, falling back to selective:', error);
|
|
1352
|
+
return this.generateSelectiveFallbackSelectionSet(fieldName);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
/**
|
|
1356
|
+
* Generate selective fallback selection set using common field patterns
|
|
1357
|
+
* This is the second fallback strategy when schema is unavailable
|
|
1358
|
+
*/
|
|
1359
|
+
generateSelectiveFallbackSelectionSet(fieldName) {
|
|
1360
|
+
console.log(`SelectionSetGenerator: Using selective fallback strategy for field "${fieldName}"`);
|
|
1361
|
+
const selectionSet = ['id'];
|
|
1362
|
+
// Add the relationship field with id (always safe)
|
|
1363
|
+
selectionSet.push(`${fieldName}.id`);
|
|
1364
|
+
// Add common display fields that are likely to exist
|
|
1365
|
+
const commonDisplayFields = ['name', 'title', 'label', 'displayName'];
|
|
1366
|
+
for (const field of commonDisplayFields) {
|
|
1367
|
+
selectionSet.push(`${fieldName}.${field}`);
|
|
1368
|
+
}
|
|
1369
|
+
// Add some additional fields that might be useful for display
|
|
1370
|
+
const additionalFields = ['description', 'status', 'type'];
|
|
1371
|
+
for (const field of additionalFields) {
|
|
1372
|
+
selectionSet.push(`${fieldName}.${field}`);
|
|
1373
|
+
}
|
|
1374
|
+
return selectionSet;
|
|
1375
|
+
}
|
|
1376
|
+
/**
|
|
1377
|
+
* Generate minimal fallback selection set for critical error cases
|
|
1378
|
+
* This is the last resort fallback strategy
|
|
1379
|
+
*/
|
|
1380
|
+
generateMinimalFallbackSelectionSet(fieldName) {
|
|
1381
|
+
console.log(`SelectionSetGenerator: Using minimal fallback strategy${fieldName ? ` for field "${fieldName}"` : ''}`);
|
|
1382
|
+
const selectionSet = ['id'];
|
|
1383
|
+
if (fieldName) {
|
|
1384
|
+
// Include only the most basic relationship data
|
|
1385
|
+
selectionSet.push(`${fieldName}.id`);
|
|
1386
|
+
// Add one common field that's most likely to exist
|
|
1387
|
+
selectionSet.push(`${fieldName}.name`);
|
|
1388
|
+
}
|
|
1389
|
+
return selectionSet;
|
|
1390
|
+
}
|
|
1391
|
+
/**
|
|
1392
|
+
* Generate fallback selection set with retry logic
|
|
1393
|
+
* Attempts multiple fallback strategies and validates each one
|
|
1394
|
+
* Enhanced with comprehensive error handling and recovery tracking
|
|
1395
|
+
*/
|
|
1396
|
+
generateFallbackSelectionSetWithRetry(fieldName, previousErrors = []) {
|
|
1397
|
+
try {
|
|
1398
|
+
if (!fieldName) {
|
|
1399
|
+
const error = this.errorHandler.logSelectionSetError(SelectionSetErrorType.INVALID_CONFIGURATION, 'Field name is empty for fallback retry generation', undefined, undefined, { previousErrors });
|
|
1400
|
+
return ['id'];
|
|
1401
|
+
}
|
|
1402
|
+
console.log(`SelectionSetGenerator: Generating fallback with retry for field "${fieldName}", previous errors: ${previousErrors.length}`);
|
|
1403
|
+
const strategies = [
|
|
1404
|
+
() => this.generateComprehensiveFallbackSelectionSet(fieldName),
|
|
1405
|
+
() => this.generateSelectiveFallbackSelectionSet(fieldName),
|
|
1406
|
+
() => this.generateMinimalFallbackSelectionSet(fieldName)
|
|
1407
|
+
];
|
|
1408
|
+
// Skip strategies that have already failed
|
|
1409
|
+
const availableStrategies = strategies.slice(previousErrors.length);
|
|
1410
|
+
for (let i = 0; i < availableStrategies.length; i++) {
|
|
1411
|
+
try {
|
|
1412
|
+
const strategy = availableStrategies[i];
|
|
1413
|
+
const selectionSet = strategy();
|
|
1414
|
+
// Validate the selection set
|
|
1415
|
+
if (this.validateSelectionSet(selectionSet)) {
|
|
1416
|
+
const strategyName = ['comprehensive', 'selective', 'minimal'][previousErrors.length + i];
|
|
1417
|
+
console.log(`SelectionSetGenerator: Successfully generated ${strategyName} fallback selection set`);
|
|
1418
|
+
// Log successful recovery if this was after previous failures
|
|
1419
|
+
if (previousErrors.length > 0) {
|
|
1420
|
+
this.errorHandler.logSuccessfulRecovery(this.errorHandler.logSelectionSetError(SelectionSetErrorType.GRAPHQL_ERROR, `Previous errors: ${previousErrors.join(', ')}`, undefined, undefined, { fieldName, previousErrors }), strategyName, selectionSet);
|
|
1421
|
+
}
|
|
1422
|
+
return selectionSet;
|
|
1423
|
+
}
|
|
1424
|
+
else {
|
|
1425
|
+
const strategyName = ['comprehensive', 'selective', 'minimal'][previousErrors.length + i];
|
|
1426
|
+
const validationError = `Generated selection set failed validation for ${strategyName} strategy`;
|
|
1427
|
+
console.warn(`SelectionSetGenerator: ${validationError}`);
|
|
1428
|
+
previousErrors.push(validationError);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
catch (error) {
|
|
1432
|
+
const strategyName = ['comprehensive', 'selective', 'minimal'][previousErrors.length + i];
|
|
1433
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1434
|
+
this.errorHandler.logSelectionSetError(SelectionSetErrorType.GRAPHQL_ERROR, `${strategyName} fallback strategy failed: ${errorMessage}`, undefined, undefined, {
|
|
1435
|
+
fieldName,
|
|
1436
|
+
strategyName,
|
|
1437
|
+
strategyIndex: previousErrors.length + i,
|
|
1438
|
+
originalError: {
|
|
1439
|
+
message: errorMessage,
|
|
1440
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
1441
|
+
}
|
|
1442
|
+
});
|
|
1443
|
+
console.warn(`SelectionSetGenerator: ${strategyName} fallback strategy failed:`, error);
|
|
1444
|
+
previousErrors.push(errorMessage);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
// If all strategies fail, log the complete failure and return absolute minimal fallback
|
|
1448
|
+
this.errorHandler.logFailedRecovery(this.errorHandler.logSelectionSetError(SelectionSetErrorType.GRAPHQL_ERROR, 'All fallback strategies failed', undefined, undefined, { fieldName, previousErrors }), ['comprehensive', 'selective', 'minimal'], new Error('All fallback strategies exhausted'));
|
|
1449
|
+
console.error('SelectionSetGenerator: All fallback strategies failed, using absolute minimal fallback');
|
|
1450
|
+
return ['id'];
|
|
1451
|
+
}
|
|
1452
|
+
catch (error) {
|
|
1453
|
+
// Critical error in retry logic itself
|
|
1454
|
+
this.errorHandler.logSelectionSetError(SelectionSetErrorType.GRAPHQL_ERROR, `Critical error in fallback retry logic: ${error instanceof Error ? error.message : String(error)}`, undefined, undefined, {
|
|
1455
|
+
fieldName,
|
|
1456
|
+
previousErrors,
|
|
1457
|
+
criticalError: {
|
|
1458
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1459
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
console.error('SelectionSetGenerator: Critical error in fallback retry logic:', error);
|
|
1463
|
+
return ['id'];
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
/**
|
|
1467
|
+
* Handle generation errors by attempting fallback with comprehensive logging
|
|
1468
|
+
*/
|
|
1469
|
+
handleGenerationError(error, fieldName) {
|
|
1470
|
+
try {
|
|
1471
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1472
|
+
console.warn(`SelectionSetGenerator: Handling generation error for field "${fieldName}":`, errorMessage);
|
|
1473
|
+
// Attempt fallback with retry logic
|
|
1474
|
+
const fallbackResult = this.generateFallbackSelectionSetWithRetry(fieldName, [errorMessage]);
|
|
1475
|
+
// Log the fallback attempt
|
|
1476
|
+
console.log(`SelectionSetGenerator: Fallback generated for field "${fieldName}":`, fallbackResult);
|
|
1477
|
+
return fallbackResult;
|
|
1478
|
+
}
|
|
1479
|
+
catch (fallbackError) {
|
|
1480
|
+
// Even fallback failed - return absolute minimal
|
|
1481
|
+
this.errorHandler.logSelectionSetError(SelectionSetErrorType.GRAPHQL_ERROR, `Both primary generation and fallback failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`, undefined, undefined, {
|
|
1482
|
+
fieldName,
|
|
1483
|
+
originalError: error,
|
|
1484
|
+
fallbackError: {
|
|
1485
|
+
message: fallbackError instanceof Error ? fallbackError.message : String(fallbackError),
|
|
1486
|
+
stack: fallbackError instanceof Error ? fallbackError.stack : undefined
|
|
1487
|
+
}
|
|
1488
|
+
});
|
|
1489
|
+
console.error('SelectionSetGenerator: Both primary generation and fallback failed, returning minimal selection set');
|
|
1490
|
+
return ['id'];
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Validate a selection set for basic correctness
|
|
1495
|
+
*/
|
|
1496
|
+
validateSelectionSet(selectionSet) {
|
|
1497
|
+
if (!Array.isArray(selectionSet) || selectionSet.length === 0) {
|
|
1498
|
+
return false;
|
|
1499
|
+
}
|
|
1500
|
+
// Must include 'id' field
|
|
1501
|
+
if (!selectionSet.includes('id')) {
|
|
1502
|
+
return false;
|
|
1503
|
+
}
|
|
1504
|
+
// Check for obviously invalid selectors
|
|
1505
|
+
for (const selector of selectionSet) {
|
|
1506
|
+
if (typeof selector !== 'string' || selector.trim() === '') {
|
|
1507
|
+
return false;
|
|
1508
|
+
}
|
|
1509
|
+
// Check for dangerous patterns
|
|
1510
|
+
if (selector.includes('..') || selector.includes('*') || selector.length > 100) {
|
|
1511
|
+
return false;
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
return true;
|
|
1515
|
+
}
|
|
1516
|
+
/**
|
|
1517
|
+
* Clear the internal cache of generated selection sets
|
|
1518
|
+
*/
|
|
1519
|
+
clearCache() {
|
|
1520
|
+
this.selectionSetCache.clear();
|
|
1521
|
+
console.log('SelectionSetGenerator: Cache cleared');
|
|
1522
|
+
}
|
|
1523
|
+
/**
|
|
1524
|
+
* Generate selection set from configuration analysis result
|
|
1525
|
+
*/
|
|
1526
|
+
generateSelectionSetFromAnalysis(config, analysisResult) {
|
|
1527
|
+
const selectionSet = [];
|
|
1528
|
+
// Always include the base record ID
|
|
1529
|
+
selectionSet.push('id');
|
|
1530
|
+
if (this.schemaIntrospector.isSchemaAvailable()) {
|
|
1531
|
+
// Use schema-aware generation
|
|
1532
|
+
return this.generateSchemaAwareSelectionSet(config, analysisResult);
|
|
1533
|
+
}
|
|
1534
|
+
else {
|
|
1535
|
+
// Use configuration-based generation
|
|
1536
|
+
return this.generateConfigurationBasedSelectionSet(config, analysisResult);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Generate selection set using schema introspection data
|
|
1541
|
+
*/
|
|
1542
|
+
generateSchemaAwareSelectionSet(config, analysisResult) {
|
|
1543
|
+
const selectionSet = ['id'];
|
|
1544
|
+
// Get safe field selectors from schema introspector
|
|
1545
|
+
const safeSelectors = this.schemaIntrospector.getSafeFieldSelectors(analysisResult.targetModelName, config.fieldName);
|
|
1546
|
+
// Add schema-validated selectors
|
|
1547
|
+
for (const selector of safeSelectors) {
|
|
1548
|
+
if (!selectionSet.includes(selector)) {
|
|
1549
|
+
selectionSet.push(selector);
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
// Apply display optimization if enabled
|
|
1553
|
+
const optimizedSet = this.applyDisplayOptimization(selectionSet, config);
|
|
1554
|
+
// Apply complexity limits
|
|
1555
|
+
return this.applyComplexityLimits(optimizedSet);
|
|
1556
|
+
}
|
|
1557
|
+
/**
|
|
1558
|
+
* Generate selection set using configuration analysis only
|
|
1559
|
+
*/
|
|
1560
|
+
generateConfigurationBasedSelectionSet(config, analysisResult) {
|
|
1561
|
+
const selectionSet = ['id'];
|
|
1562
|
+
// Use the field selectors from configuration analysis
|
|
1563
|
+
for (const selector of analysisResult.fieldSelectors) {
|
|
1564
|
+
if (!selectionSet.includes(selector)) {
|
|
1565
|
+
selectionSet.push(selector);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
// Apply display optimization if enabled
|
|
1569
|
+
const optimizedSet = this.applyDisplayOptimization(selectionSet, config);
|
|
1570
|
+
// Apply complexity limits
|
|
1571
|
+
return this.applyComplexityLimits(optimizedSet);
|
|
1572
|
+
}
|
|
1573
|
+
/**
|
|
1574
|
+
* Apply display optimization to prioritize display-relevant fields
|
|
1575
|
+
*/
|
|
1576
|
+
applyDisplayOptimization(selectionSet, config) {
|
|
1577
|
+
const displayConfig = this.DEFAULT_DISPLAY_CONFIG;
|
|
1578
|
+
if (!displayConfig.enabled) {
|
|
1579
|
+
return selectionSet;
|
|
1580
|
+
}
|
|
1581
|
+
// Analyze each field for display relevance
|
|
1582
|
+
const fieldAnalyses = selectionSet.map(selector => this.analyzeFieldForDisplay(selector, config));
|
|
1583
|
+
// Sort by priority (higher priority first)
|
|
1584
|
+
fieldAnalyses.sort((a, b) => b.priority - a.priority);
|
|
1585
|
+
// Build optimized selection set
|
|
1586
|
+
const optimizedSet = [];
|
|
1587
|
+
const essentialFields = [];
|
|
1588
|
+
const displayFields = [];
|
|
1589
|
+
const otherFields = [];
|
|
1590
|
+
// Categorize fields
|
|
1591
|
+
for (const analysis of fieldAnalyses) {
|
|
1592
|
+
switch (analysis.classification) {
|
|
1593
|
+
case FieldClassification.ESSENTIAL:
|
|
1594
|
+
essentialFields.push(analysis.selector);
|
|
1595
|
+
break;
|
|
1596
|
+
case FieldClassification.DISPLAY:
|
|
1597
|
+
displayFields.push(analysis.selector);
|
|
1598
|
+
break;
|
|
1599
|
+
default:
|
|
1600
|
+
otherFields.push(analysis.selector);
|
|
1601
|
+
break;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
// Add essential fields first (always included)
|
|
1605
|
+
optimizedSet.push(...essentialFields);
|
|
1606
|
+
// Add display fields (prioritized)
|
|
1607
|
+
optimizedSet.push(...displayFields);
|
|
1608
|
+
// Add other fields up to the limit
|
|
1609
|
+
const remainingSlots = Math.max(0, displayConfig.maxNonDisplayFields - (optimizedSet.length - essentialFields.length));
|
|
1610
|
+
optimizedSet.push(...otherFields.slice(0, remainingSlots));
|
|
1611
|
+
console.log(`SelectionSetGenerator: Display optimization reduced selection set from ${selectionSet.length} to ${optimizedSet.length} fields`);
|
|
1612
|
+
return optimizedSet;
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Analyze a field selector for display optimization
|
|
1616
|
+
*/
|
|
1617
|
+
analyzeFieldForDisplay(selector, config) {
|
|
1618
|
+
const displayConfig = this.DEFAULT_DISPLAY_CONFIG;
|
|
1619
|
+
const analysis = {
|
|
1620
|
+
selector,
|
|
1621
|
+
classification: FieldClassification.UNKNOWN,
|
|
1622
|
+
priority: 0,
|
|
1623
|
+
cost: 1,
|
|
1624
|
+
isDisplayRelevant: false
|
|
1625
|
+
};
|
|
1626
|
+
// Essential fields (always required)
|
|
1627
|
+
if (displayConfig.excludeFromOptimization.some(field => selector.includes(field))) {
|
|
1628
|
+
analysis.classification = FieldClassification.ESSENTIAL;
|
|
1629
|
+
analysis.priority = 100;
|
|
1630
|
+
analysis.isDisplayRelevant = true;
|
|
1631
|
+
return analysis;
|
|
1632
|
+
}
|
|
1633
|
+
// Check if it's a common display field
|
|
1634
|
+
const isDisplayField = displayConfig.commonDisplayFields.some(field => selector.endsWith(`.${field}`) || selector === field);
|
|
1635
|
+
if (isDisplayField) {
|
|
1636
|
+
analysis.classification = FieldClassification.DISPLAY;
|
|
1637
|
+
analysis.priority = 80;
|
|
1638
|
+
analysis.isDisplayRelevant = true;
|
|
1639
|
+
}
|
|
1640
|
+
else if (this.isRelationshipField(selector)) {
|
|
1641
|
+
analysis.classification = FieldClassification.RELATIONSHIP;
|
|
1642
|
+
analysis.priority = 60;
|
|
1643
|
+
analysis.cost = 2; // Relationships are more expensive
|
|
1644
|
+
analysis.isDisplayRelevant = this.isDisplayRelevantRelationship(selector);
|
|
1645
|
+
}
|
|
1646
|
+
else if (this.isMetadataField(selector)) {
|
|
1647
|
+
analysis.classification = FieldClassification.METADATA;
|
|
1648
|
+
analysis.priority = 40;
|
|
1649
|
+
analysis.isDisplayRelevant = false;
|
|
1650
|
+
}
|
|
1651
|
+
else if (this.isSystemField(selector)) {
|
|
1652
|
+
analysis.classification = FieldClassification.SYSTEM;
|
|
1653
|
+
analysis.priority = 20;
|
|
1654
|
+
analysis.isDisplayRelevant = false;
|
|
1655
|
+
}
|
|
1656
|
+
else {
|
|
1657
|
+
analysis.classification = FieldClassification.UNKNOWN;
|
|
1658
|
+
analysis.priority = 30;
|
|
1659
|
+
analysis.isDisplayRelevant = false;
|
|
1660
|
+
}
|
|
1661
|
+
// Adjust priority based on field depth (deeper fields are less likely to be displayed)
|
|
1662
|
+
const depth = (selector.match(/\./g) || []).length;
|
|
1663
|
+
analysis.priority -= depth * 5;
|
|
1664
|
+
analysis.cost += depth;
|
|
1665
|
+
// Boost priority for fields that match the relationship field name
|
|
1666
|
+
if (selector.includes(config.fieldName)) {
|
|
1667
|
+
analysis.priority += 10;
|
|
1668
|
+
analysis.isDisplayRelevant = true;
|
|
1669
|
+
}
|
|
1670
|
+
return analysis;
|
|
1671
|
+
}
|
|
1672
|
+
/**
|
|
1673
|
+
* Check if a selector represents a relationship field
|
|
1674
|
+
*/
|
|
1675
|
+
isRelationshipField(selector) {
|
|
1676
|
+
// Relationship fields typically have dots (nested access) or end with common relationship suffixes
|
|
1677
|
+
return selector.includes('.') ||
|
|
1678
|
+
selector.endsWith('Id') ||
|
|
1679
|
+
selector.endsWith('Ids') ||
|
|
1680
|
+
selector.endsWith('Connection');
|
|
1681
|
+
}
|
|
1682
|
+
/**
|
|
1683
|
+
* Check if a relationship field is likely to be displayed
|
|
1684
|
+
*/
|
|
1685
|
+
isDisplayRelevantRelationship(selector) {
|
|
1686
|
+
// Display-relevant relationships typically access display fields of related models
|
|
1687
|
+
const displayFieldPatterns = ['name', 'title', 'label', 'displayName', 'description'];
|
|
1688
|
+
return displayFieldPatterns.some(pattern => selector.includes(pattern));
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* Check if a selector represents a metadata field
|
|
1692
|
+
*/
|
|
1693
|
+
isMetadataField(selector) {
|
|
1694
|
+
const metadataPatterns = ['createdAt', 'updatedAt', 'version', 'owner', 'lastModified'];
|
|
1695
|
+
return metadataPatterns.some(pattern => selector.includes(pattern));
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Check if a selector represents a system field
|
|
1699
|
+
*/
|
|
1700
|
+
isSystemField(selector) {
|
|
1701
|
+
const systemPatterns = ['__typename', '_version', '_deleted', '_lastChangedAt'];
|
|
1702
|
+
return systemPatterns.some(pattern => selector.includes(pattern));
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Apply complexity limits to prevent overly expensive GraphQL queries
|
|
1706
|
+
* Enhanced with comprehensive error handling and logging
|
|
1707
|
+
*/
|
|
1708
|
+
applyComplexityLimits(selectionSet) {
|
|
1709
|
+
try {
|
|
1710
|
+
// First validate the selection set for complexity
|
|
1711
|
+
const validationResult = this.validateSelectionSetComplexity(selectionSet);
|
|
1712
|
+
if (validationResult.isValid) {
|
|
1713
|
+
console.log('SelectionSetGenerator: Selection set passed complexity validation');
|
|
1714
|
+
return selectionSet;
|
|
1715
|
+
}
|
|
1716
|
+
// Log validation issues
|
|
1717
|
+
if (validationResult.errors.length > 0) {
|
|
1718
|
+
this.errorHandler.logComplexityError(selectionSet, validationResult.errors);
|
|
1719
|
+
console.warn('SelectionSetGenerator: Complexity validation errors:', validationResult.errors);
|
|
1720
|
+
}
|
|
1721
|
+
if (validationResult.warnings.length > 0) {
|
|
1722
|
+
console.warn('SelectionSetGenerator: Complexity validation warnings:', validationResult.warnings);
|
|
1723
|
+
}
|
|
1724
|
+
// Apply fixes to make the selection set compliant
|
|
1725
|
+
const fixedSet = this.fixComplexityIssues(selectionSet, validationResult);
|
|
1726
|
+
// Validate the fixed set
|
|
1727
|
+
const revalidationResult = this.validateSelectionSetComplexity(fixedSet);
|
|
1728
|
+
if (!revalidationResult.isValid) {
|
|
1729
|
+
this.errorHandler.logSelectionSetError(SelectionSetErrorType.COMPLEXITY_EXCEEDED, 'Selection set still exceeds complexity limits after fixes', undefined, fixedSet, {
|
|
1730
|
+
originalSelectionSet: selectionSet,
|
|
1731
|
+
validationResult,
|
|
1732
|
+
revalidationResult
|
|
1733
|
+
});
|
|
1734
|
+
// Return minimal fallback if fixes didn't work
|
|
1735
|
+
console.warn('SelectionSetGenerator: Complexity fixes failed, returning minimal selection set');
|
|
1736
|
+
return ['id'];
|
|
1737
|
+
}
|
|
1738
|
+
console.log(`SelectionSetGenerator: Applied complexity fixes, reduced from ${selectionSet.length} to ${fixedSet.length} fields`);
|
|
1739
|
+
return fixedSet;
|
|
1740
|
+
}
|
|
1741
|
+
catch (error) {
|
|
1742
|
+
this.errorHandler.logSelectionSetError(SelectionSetErrorType.COMPLEXITY_EXCEEDED, `Error applying complexity limits: ${error instanceof Error ? error.message : String(error)}`, undefined, selectionSet, {
|
|
1743
|
+
originalError: {
|
|
1744
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1745
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
1746
|
+
}
|
|
1747
|
+
});
|
|
1748
|
+
console.error('SelectionSetGenerator: Error applying complexity limits:', error);
|
|
1749
|
+
// Return original set if complexity checking fails
|
|
1750
|
+
return selectionSet;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* Validate selection set complexity against configured limits
|
|
1755
|
+
*/
|
|
1756
|
+
validateSelectionSetComplexity(selectionSet) {
|
|
1757
|
+
const limits = this.DEFAULT_CONFIG.complexityLimits;
|
|
1758
|
+
const result = {
|
|
1759
|
+
isValid: true,
|
|
1760
|
+
errors: [],
|
|
1761
|
+
warnings: [],
|
|
1762
|
+
complexityScore: 0,
|
|
1763
|
+
circularReferences: []
|
|
1764
|
+
};
|
|
1765
|
+
// Track field paths for circular reference detection
|
|
1766
|
+
const fieldPaths = new Set();
|
|
1767
|
+
const pathComponents = new Map();
|
|
1768
|
+
let wildcardCount = 0;
|
|
1769
|
+
let maxDepth = 0;
|
|
1770
|
+
for (const selector of selectionSet) {
|
|
1771
|
+
// Check selector length
|
|
1772
|
+
if (selector.length > limits.maxSelectorLength) {
|
|
1773
|
+
result.errors.push(`Selector "${selector}" exceeds maximum length of ${limits.maxSelectorLength}`);
|
|
1774
|
+
result.isValid = false;
|
|
1775
|
+
}
|
|
1776
|
+
// Check for wildcard selections
|
|
1777
|
+
if (selector.includes('*')) {
|
|
1778
|
+
wildcardCount++;
|
|
1779
|
+
if (wildcardCount > limits.maxWildcardSelections) {
|
|
1780
|
+
result.errors.push(`Too many wildcard selections (${wildcardCount}), maximum allowed: ${limits.maxWildcardSelections}`);
|
|
1781
|
+
result.isValid = false;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
// Calculate nesting depth
|
|
1785
|
+
const depth = (selector.match(/\./g) || []).length;
|
|
1786
|
+
maxDepth = Math.max(maxDepth, depth);
|
|
1787
|
+
if (depth > limits.maxNestingDepth) {
|
|
1788
|
+
result.errors.push(`Selector "${selector}" exceeds maximum nesting depth of ${limits.maxNestingDepth}`);
|
|
1789
|
+
result.isValid = false;
|
|
1790
|
+
}
|
|
1791
|
+
// Track field paths for circular reference detection
|
|
1792
|
+
if (limits.preventCircularReferences && depth > 0) {
|
|
1793
|
+
const components = selector.split('.');
|
|
1794
|
+
pathComponents.set(selector, components);
|
|
1795
|
+
// Check for potential circular references
|
|
1796
|
+
const circularRef = this.detectCircularReference(components, pathComponents);
|
|
1797
|
+
if (circularRef) {
|
|
1798
|
+
result.circularReferences.push(circularRef);
|
|
1799
|
+
result.warnings.push(`Potential circular reference detected: ${circularRef}`);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
fieldPaths.add(selector);
|
|
1803
|
+
}
|
|
1804
|
+
// Check field count limit
|
|
1805
|
+
if (selectionSet.length > limits.maxFieldCount) {
|
|
1806
|
+
result.errors.push(`Selection set has ${selectionSet.length} fields, maximum allowed: ${limits.maxFieldCount}`);
|
|
1807
|
+
result.isValid = false;
|
|
1808
|
+
}
|
|
1809
|
+
// Calculate complexity score
|
|
1810
|
+
result.complexityScore = this.calculateComplexityScore(selectionSet, maxDepth, wildcardCount);
|
|
1811
|
+
// Add warnings for high complexity
|
|
1812
|
+
if (result.complexityScore > 50) {
|
|
1813
|
+
result.warnings.push(`High complexity score: ${result.complexityScore}`);
|
|
1814
|
+
}
|
|
1815
|
+
return result;
|
|
1816
|
+
}
|
|
1817
|
+
/**
|
|
1818
|
+
* Detect circular references in field paths
|
|
1819
|
+
*/
|
|
1820
|
+
detectCircularReference(components, allPaths) {
|
|
1821
|
+
// Simple circular reference detection: check if any component appears multiple times in the path
|
|
1822
|
+
const componentCounts = new Map();
|
|
1823
|
+
for (const component of components) {
|
|
1824
|
+
const count = componentCounts.get(component) || 0;
|
|
1825
|
+
componentCounts.set(component, count + 1);
|
|
1826
|
+
if (count > 0) {
|
|
1827
|
+
return components.join('.');
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
return null;
|
|
1831
|
+
}
|
|
1832
|
+
/**
|
|
1833
|
+
* Calculate complexity score for a selection set
|
|
1834
|
+
*/
|
|
1835
|
+
calculateComplexityScore(selectionSet, maxDepth, wildcardCount) {
|
|
1836
|
+
let score = 0;
|
|
1837
|
+
// Base score from field count
|
|
1838
|
+
score += selectionSet.length;
|
|
1839
|
+
// Penalty for depth
|
|
1840
|
+
score += maxDepth * 5;
|
|
1841
|
+
// Heavy penalty for wildcards
|
|
1842
|
+
score += wildcardCount * 10;
|
|
1843
|
+
// Penalty for complex selectors
|
|
1844
|
+
for (const selector of selectionSet) {
|
|
1845
|
+
if (selector.includes('*')) {
|
|
1846
|
+
score += 5;
|
|
1847
|
+
}
|
|
1848
|
+
if (selector.split('.').length > 2) {
|
|
1849
|
+
score += 3;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
return score;
|
|
1853
|
+
}
|
|
1854
|
+
/**
|
|
1855
|
+
* Fix complexity issues in a selection set
|
|
1856
|
+
*/
|
|
1857
|
+
fixComplexityIssues(selectionSet, validationResult) {
|
|
1858
|
+
const limits = this.DEFAULT_CONFIG.complexityLimits;
|
|
1859
|
+
let fixedSet = [...selectionSet];
|
|
1860
|
+
// Remove selectors that are too long
|
|
1861
|
+
fixedSet = fixedSet.filter(selector => {
|
|
1862
|
+
if (selector.length > limits.maxSelectorLength) {
|
|
1863
|
+
console.warn(`SelectionSetGenerator: Removing selector "${selector}" due to excessive length`);
|
|
1864
|
+
return false;
|
|
1865
|
+
}
|
|
1866
|
+
return true;
|
|
1867
|
+
});
|
|
1868
|
+
// Remove selectors that are too deeply nested
|
|
1869
|
+
fixedSet = fixedSet.filter(selector => {
|
|
1870
|
+
const depth = (selector.match(/\./g) || []).length;
|
|
1871
|
+
if (depth > limits.maxNestingDepth) {
|
|
1872
|
+
console.warn(`SelectionSetGenerator: Removing selector "${selector}" due to excessive nesting depth`);
|
|
1873
|
+
return false;
|
|
1874
|
+
}
|
|
1875
|
+
return true;
|
|
1876
|
+
});
|
|
1877
|
+
// Limit wildcard selections
|
|
1878
|
+
let wildcardCount = 0;
|
|
1879
|
+
fixedSet = fixedSet.filter(selector => {
|
|
1880
|
+
if (selector.includes('*')) {
|
|
1881
|
+
wildcardCount++;
|
|
1882
|
+
if (wildcardCount > limits.maxWildcardSelections) {
|
|
1883
|
+
console.warn(`SelectionSetGenerator: Removing wildcard selector "${selector}" due to limit`);
|
|
1884
|
+
return false;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
return true;
|
|
1888
|
+
});
|
|
1889
|
+
// Remove circular references
|
|
1890
|
+
if (limits.preventCircularReferences && validationResult.circularReferences.length > 0) {
|
|
1891
|
+
const circularSelectors = new Set(validationResult.circularReferences);
|
|
1892
|
+
fixedSet = fixedSet.filter(selector => {
|
|
1893
|
+
if (circularSelectors.has(selector)) {
|
|
1894
|
+
console.warn(`SelectionSetGenerator: Removing selector "${selector}" due to circular reference`);
|
|
1895
|
+
return false;
|
|
1896
|
+
}
|
|
1897
|
+
return true;
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
// Truncate to field count limit
|
|
1901
|
+
if (fixedSet.length > limits.maxFieldCount) {
|
|
1902
|
+
console.warn(`SelectionSetGenerator: Truncating selection set from ${fixedSet.length} to ${limits.maxFieldCount} fields`);
|
|
1903
|
+
fixedSet = fixedSet.slice(0, limits.maxFieldCount);
|
|
1904
|
+
}
|
|
1905
|
+
// Ensure required fields are still present
|
|
1906
|
+
for (const requiredField of this.DEFAULT_CONFIG.requiredFields) {
|
|
1907
|
+
if (!fixedSet.includes(requiredField)) {
|
|
1908
|
+
fixedSet.unshift(requiredField);
|
|
1909
|
+
}
|
|
1910
|
+
}
|
|
1911
|
+
return fixedSet;
|
|
1912
|
+
}
|
|
1913
|
+
/**
|
|
1914
|
+
* Generate a cache key for a relationship configuration
|
|
1915
|
+
*/
|
|
1916
|
+
generateCacheKey(config) {
|
|
1917
|
+
const key = {
|
|
1918
|
+
relationshipModel: config.relationshipModelName,
|
|
1919
|
+
fieldName: config.fieldName,
|
|
1920
|
+
targetModel: this.configurationAnalyzer.extractTargetModelName(config),
|
|
1921
|
+
schemaVersion: this.getSchemaVersion()
|
|
1922
|
+
};
|
|
1923
|
+
return JSON.stringify(key);
|
|
1924
|
+
}
|
|
1925
|
+
/**
|
|
1926
|
+
* Get cached selection set if available
|
|
1927
|
+
*/
|
|
1928
|
+
getCachedSelectionSet(cacheKey) {
|
|
1929
|
+
const cached = this.selectionSetCache.get(cacheKey);
|
|
1930
|
+
if (cached) {
|
|
1931
|
+
// Update access count
|
|
1932
|
+
cached.accessCount++;
|
|
1933
|
+
return cached;
|
|
1934
|
+
}
|
|
1935
|
+
return null;
|
|
1936
|
+
}
|
|
1937
|
+
/**
|
|
1938
|
+
* Cache a generated selection set
|
|
1939
|
+
*/
|
|
1940
|
+
cacheSelectionSet(cacheKey, selectionSet) {
|
|
1941
|
+
const cached = {
|
|
1942
|
+
key: JSON.parse(cacheKey),
|
|
1943
|
+
selectionSet: [...selectionSet],
|
|
1944
|
+
createdAt: new Date(),
|
|
1945
|
+
accessCount: 1
|
|
1946
|
+
};
|
|
1947
|
+
this.selectionSetCache.set(cacheKey, cached);
|
|
1948
|
+
// Implement simple cache size limit
|
|
1949
|
+
if (this.selectionSetCache.size > 100) {
|
|
1950
|
+
this.evictOldestCacheEntries();
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
/**
|
|
1954
|
+
* Evict oldest cache entries when cache gets too large
|
|
1955
|
+
*/
|
|
1956
|
+
evictOldestCacheEntries() {
|
|
1957
|
+
const entries = Array.from(this.selectionSetCache.entries());
|
|
1958
|
+
entries.sort((a, b) => a[1].createdAt.getTime() - b[1].createdAt.getTime());
|
|
1959
|
+
// Remove oldest 20% of entries
|
|
1960
|
+
const toRemove = Math.floor(entries.length * 0.2);
|
|
1961
|
+
for (let i = 0; i < toRemove; i++) {
|
|
1962
|
+
this.selectionSetCache.delete(entries[i][0]);
|
|
1963
|
+
}
|
|
1964
|
+
console.log(`SelectionSetGenerator: Evicted ${toRemove} old cache entries`);
|
|
1965
|
+
}
|
|
1966
|
+
/**
|
|
1967
|
+
* Get schema version for cache invalidation
|
|
1968
|
+
*/
|
|
1969
|
+
getSchemaVersion() {
|
|
1970
|
+
// In a real implementation, this could be based on schema hash or version
|
|
1971
|
+
// For now, we'll use schema availability as a simple version indicator
|
|
1972
|
+
return this.schemaIntrospector.isSchemaAvailable() ? 'schema-available' : 'schema-unavailable';
|
|
1973
|
+
}
|
|
1974
|
+
/**
|
|
1975
|
+
* Get cache statistics for debugging
|
|
1976
|
+
*/
|
|
1977
|
+
getCacheStats() {
|
|
1978
|
+
const entries = Array.from(this.selectionSetCache.values());
|
|
1979
|
+
const totalAccesses = entries.reduce((sum, entry) => sum + entry.accessCount, 0);
|
|
1980
|
+
return {
|
|
1981
|
+
size: this.selectionSetCache.size,
|
|
1982
|
+
totalAccesses,
|
|
1983
|
+
entries: entries.map(entry => ({
|
|
1984
|
+
key: entry.key,
|
|
1985
|
+
selectionSet: [...entry.selectionSet],
|
|
1986
|
+
createdAt: entry.createdAt,
|
|
1987
|
+
accessCount: entry.accessCount
|
|
1988
|
+
}))
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Get optimization statistics for debugging
|
|
1993
|
+
*/
|
|
1994
|
+
getOptimizationStats() {
|
|
1995
|
+
return {
|
|
1996
|
+
displayOptimizationEnabled: this.DEFAULT_DISPLAY_CONFIG.enabled,
|
|
1997
|
+
complexityLimits: { ...this.DEFAULT_CONFIG.complexityLimits },
|
|
1998
|
+
displayConfig: { ...this.DEFAULT_DISPLAY_CONFIG }
|
|
1999
|
+
};
|
|
2000
|
+
}
|
|
2001
|
+
/**
|
|
2002
|
+
* Analyze a selection set for optimization opportunities
|
|
2003
|
+
*/
|
|
2004
|
+
analyzeSelectionSetOptimization(selectionSet) {
|
|
2005
|
+
const mockConfig = {
|
|
2006
|
+
relationshipModelName: '',
|
|
2007
|
+
baseModelName: '',
|
|
2008
|
+
fieldName: 'mock',
|
|
2009
|
+
associatedWith: ''
|
|
2010
|
+
};
|
|
2011
|
+
const analyses = selectionSet.map(selector => this.analyzeFieldForDisplay(selector, mockConfig));
|
|
2012
|
+
const displayRelevantCount = analyses.filter(a => a.isDisplayRelevant).length;
|
|
2013
|
+
const complexityScore = this.calculateComplexityScore(selectionSet, Math.max(...selectionSet.map(s => (s.match(/\./g) || []).length)), selectionSet.filter(s => s.includes('*')).length);
|
|
2014
|
+
const opportunities = [];
|
|
2015
|
+
// Identify optimization opportunities
|
|
2016
|
+
if (displayRelevantCount < selectionSet.length * 0.5) {
|
|
2017
|
+
opportunities.push('Many non-display fields could be removed');
|
|
2018
|
+
}
|
|
2019
|
+
if (complexityScore > 30) {
|
|
2020
|
+
opportunities.push('High complexity score suggests simplification needed');
|
|
2021
|
+
}
|
|
2022
|
+
const deepFields = selectionSet.filter(s => (s.match(/\./g) || []).length > 2);
|
|
2023
|
+
if (deepFields.length > 0) {
|
|
2024
|
+
opportunities.push(`${deepFields.length} deeply nested fields could be simplified`);
|
|
2025
|
+
}
|
|
2026
|
+
const wildcardFields = selectionSet.filter(s => s.includes('*'));
|
|
2027
|
+
if (wildcardFields.length > 1) {
|
|
2028
|
+
opportunities.push(`${wildcardFields.length} wildcard selections could be made more specific`);
|
|
2029
|
+
}
|
|
2030
|
+
return {
|
|
2031
|
+
totalFields: selectionSet.length,
|
|
2032
|
+
displayRelevantFields: displayRelevantCount,
|
|
2033
|
+
complexityScore,
|
|
2034
|
+
optimizationOpportunities: opportunities
|
|
2035
|
+
};
|
|
2036
|
+
}
|
|
2037
|
+
/**
|
|
2038
|
+
* Initialize the service with schema data
|
|
2039
|
+
*/
|
|
2040
|
+
initializeWithSchema(amplifyOutputs) {
|
|
2041
|
+
this.schemaIntrospector.initializeSchema(amplifyOutputs);
|
|
2042
|
+
this.clearCache(); // Clear cache when schema changes
|
|
2043
|
+
console.log('SelectionSetGenerator: Initialized with schema data');
|
|
2044
|
+
}
|
|
2045
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SelectionSetGeneratorService, deps: [{ token: ConfigurationAnalyzerService }, { token: SchemaIntrospectorService }, { token: ErrorHandlerService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2046
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SelectionSetGeneratorService, providedIn: 'root' });
|
|
2047
|
+
}
|
|
2048
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: SelectionSetGeneratorService, decorators: [{
|
|
2049
|
+
type: Injectable,
|
|
2050
|
+
args: [{
|
|
2051
|
+
providedIn: 'root'
|
|
2052
|
+
}]
|
|
2053
|
+
}], ctorParameters: () => [{ type: ConfigurationAnalyzerService }, { type: SchemaIntrospectorService }, { type: ErrorHandlerService }] });
|
|
2054
|
+
|
|
37
2055
|
class AmplifyAngularCore {
|
|
38
2056
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AmplifyAngularCore, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
39
2057
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: AmplifyAngularCore, isStandalone: true, selector: "snteam-amplify-angular-core", ngImport: i0, template: `
|
|
@@ -44,7 +2062,7 @@ class AmplifyAngularCore {
|
|
|
44
2062
|
}
|
|
45
2063
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AmplifyAngularCore, decorators: [{
|
|
46
2064
|
type: Component,
|
|
47
|
-
args: [{ selector: 'snteam-amplify-angular-core', imports: [], template: `
|
|
2065
|
+
args: [{ selector: 'snteam-amplify-angular-core', standalone: true, imports: [], template: `
|
|
48
2066
|
<p>
|
|
49
2067
|
amplify-angular-core works!
|
|
50
2068
|
</p>
|
|
@@ -236,8 +2254,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
236
2254
|
}] });
|
|
237
2255
|
|
|
238
2256
|
class AmplifyModelService {
|
|
2257
|
+
selectionSetGenerator;
|
|
2258
|
+
errorHandler;
|
|
239
2259
|
client;
|
|
240
|
-
constructor() {
|
|
2260
|
+
constructor(selectionSetGenerator, errorHandler) {
|
|
2261
|
+
this.selectionSetGenerator = selectionSetGenerator;
|
|
2262
|
+
this.errorHandler = errorHandler;
|
|
241
2263
|
// Client will be initialized when Amplify is configured by the consuming application
|
|
242
2264
|
}
|
|
243
2265
|
/**
|
|
@@ -424,14 +2446,65 @@ class AmplifyModelService {
|
|
|
424
2446
|
return condition;
|
|
425
2447
|
}
|
|
426
2448
|
}
|
|
2449
|
+
/**
|
|
2450
|
+
* Generate dynamic selection set for relationship queries
|
|
2451
|
+
* Replaces hardcoded selection set logic with dynamic generation
|
|
2452
|
+
* Enhanced with comprehensive error handling and logging
|
|
2453
|
+
* @param config Relationship configuration object
|
|
2454
|
+
* @param baseId Base record ID (for compatibility, not used in generation)
|
|
2455
|
+
* @returns Array of GraphQL field selectors
|
|
2456
|
+
*/
|
|
427
2457
|
getAmplifySelectionSet(config, baseId) {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
2458
|
+
try {
|
|
2459
|
+
if (!config) {
|
|
2460
|
+
const error = this.errorHandler.logSelectionSetError(SelectionSetErrorType.INVALID_CONFIGURATION, 'Configuration is null or undefined', config, undefined, {
|
|
2461
|
+
method: 'getAmplifySelectionSet',
|
|
2462
|
+
baseId,
|
|
2463
|
+
clientAvailable: !!this.client
|
|
2464
|
+
});
|
|
2465
|
+
// Return minimal fallback
|
|
2466
|
+
return ['id'];
|
|
2467
|
+
}
|
|
2468
|
+
console.log('AmplifyModelService: Generating dynamic selection set for config:', {
|
|
2469
|
+
relationshipModel: config.relationshipModelName,
|
|
2470
|
+
fieldName: config.fieldName,
|
|
2471
|
+
baseId
|
|
2472
|
+
});
|
|
2473
|
+
// Use the SelectionSetGenerator to create dynamic selection sets
|
|
2474
|
+
const selectionSet = this.selectionSetGenerator.generateSelectionSet(config);
|
|
2475
|
+
if (!selectionSet || selectionSet.length === 0) {
|
|
2476
|
+
const error = this.errorHandler.logSelectionSetError(SelectionSetErrorType.GRAPHQL_ERROR, 'Selection set generator returned empty or null result', config, selectionSet, {
|
|
2477
|
+
method: 'getAmplifySelectionSet',
|
|
2478
|
+
baseId,
|
|
2479
|
+
generatorAvailable: !!this.selectionSetGenerator
|
|
2480
|
+
});
|
|
2481
|
+
// Return minimal fallback
|
|
2482
|
+
return ['id'];
|
|
2483
|
+
}
|
|
2484
|
+
console.log('AmplifyModelService: Generated selection set:', {
|
|
2485
|
+
config: config.fieldName,
|
|
2486
|
+
selectionSetLength: selectionSet.length,
|
|
2487
|
+
selectionSet
|
|
2488
|
+
});
|
|
2489
|
+
return selectionSet;
|
|
2490
|
+
}
|
|
2491
|
+
catch (error) {
|
|
2492
|
+
const selectionSetError = this.errorHandler.logSelectionSetError(SelectionSetErrorType.GRAPHQL_ERROR, `Unexpected error generating selection set: ${error instanceof Error ? error.message : String(error)}`, config, undefined, {
|
|
2493
|
+
method: 'getAmplifySelectionSet',
|
|
2494
|
+
baseId,
|
|
2495
|
+
originalError: {
|
|
2496
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2497
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
2498
|
+
type: typeof error
|
|
2499
|
+
},
|
|
2500
|
+
context: {
|
|
2501
|
+
clientAvailable: !!this.client,
|
|
2502
|
+
selectionSetGeneratorAvailable: !!this.selectionSetGenerator
|
|
2503
|
+
}
|
|
2504
|
+
});
|
|
2505
|
+
console.error('AmplifyModelService: Error generating selection set, using fallback:', error);
|
|
2506
|
+
// Use fallback selection set generation with retry logic
|
|
2507
|
+
return this.selectionSetGenerator.generateFallbackSelectionSetWithRetry(config?.fieldName || '', [error instanceof Error ? error.message : String(error)]);
|
|
435
2508
|
}
|
|
436
2509
|
}
|
|
437
2510
|
setRelatedSub(config, baseId) {
|
|
@@ -439,38 +2512,347 @@ class AmplifyModelService {
|
|
|
439
2512
|
console.error('AmplifyModelService: Client not initialized. Call initializeClient() first.');
|
|
440
2513
|
return null;
|
|
441
2514
|
}
|
|
442
|
-
let filter = this.getRelationshipFilter(config, baseId);
|
|
443
|
-
const modelName = config.relationshipModelName;
|
|
444
|
-
if (this.client.models[modelName]) {
|
|
445
|
-
return this.client.models[modelName].onCreate({ filter: filter });
|
|
2515
|
+
let filter = this.getRelationshipFilter(config, baseId);
|
|
2516
|
+
const modelName = config.relationshipModelName;
|
|
2517
|
+
if (this.client.models[modelName]) {
|
|
2518
|
+
return this.client.models[modelName].onCreate({ filter: filter });
|
|
2519
|
+
}
|
|
2520
|
+
else {
|
|
2521
|
+
console.error(`Relationship model ${modelName} not found in client`);
|
|
2522
|
+
return null;
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
/**
|
|
2526
|
+
* Get related items with enhanced error handling and retry logic
|
|
2527
|
+
* @param config Relationship configuration object
|
|
2528
|
+
* @param baseId Base record ID to filter relationships
|
|
2529
|
+
* @returns Observable of related items with retry logic on GraphQL errors
|
|
2530
|
+
*/
|
|
2531
|
+
getRelatedItems(config, baseId) {
|
|
2532
|
+
if (!this.client) {
|
|
2533
|
+
console.error('AmplifyModelService: Client not initialized. Call initializeClient() first.');
|
|
2534
|
+
return null;
|
|
2535
|
+
}
|
|
2536
|
+
const filter = this.getRelationshipFilter(config, baseId);
|
|
2537
|
+
const modelName = config.relationshipModelName;
|
|
2538
|
+
if (!this.client.models[modelName]) {
|
|
2539
|
+
console.error(`AmplifyModelService: Relationship model ${modelName} not found in client`);
|
|
2540
|
+
return null;
|
|
2541
|
+
}
|
|
2542
|
+
// Attempt to get related items with progressive retry logic
|
|
2543
|
+
return this.getRelatedItemsWithRetry(config, baseId, filter, modelName);
|
|
2544
|
+
}
|
|
2545
|
+
/**
|
|
2546
|
+
* Get related items with progressive retry logic for GraphQL errors
|
|
2547
|
+
* Implements progressive fallback: original → comprehensive → selective → minimal
|
|
2548
|
+
*/
|
|
2549
|
+
getRelatedItemsWithRetry(config, baseId, filter, modelName, retryAttempt = 0, previousErrors = []) {
|
|
2550
|
+
const maxRetries = 3;
|
|
2551
|
+
try {
|
|
2552
|
+
// Generate selection set based on retry attempt
|
|
2553
|
+
let selectionSet;
|
|
2554
|
+
if (retryAttempt === 0) {
|
|
2555
|
+
// First attempt: Use dynamic generation
|
|
2556
|
+
selectionSet = this.getAmplifySelectionSet(config, baseId);
|
|
2557
|
+
}
|
|
2558
|
+
else {
|
|
2559
|
+
// Subsequent attempts: Use progressive fallback
|
|
2560
|
+
console.log(`AmplifyModelService: Retry attempt ${retryAttempt} for ${modelName}, using fallback selection set`);
|
|
2561
|
+
selectionSet = this.selectionSetGenerator.generateFallbackSelectionSetWithRetry(config.fieldName, previousErrors);
|
|
2562
|
+
}
|
|
2563
|
+
// Log the attempt
|
|
2564
|
+
this.logQueryAttempt(config, selectionSet, retryAttempt, previousErrors);
|
|
2565
|
+
// Prepare query options
|
|
2566
|
+
const queryOptions = { filter: filter };
|
|
2567
|
+
// Only include selectionSet if it's not empty to avoid GraphQL syntax errors
|
|
2568
|
+
if (selectionSet && selectionSet.length > 0) {
|
|
2569
|
+
queryOptions.selectionSet = [...selectionSet];
|
|
2570
|
+
}
|
|
2571
|
+
// Execute the query
|
|
2572
|
+
const queryObservable = this.client.models[modelName].observeQuery(queryOptions);
|
|
2573
|
+
// Wrap the observable to handle GraphQL errors with retry logic
|
|
2574
|
+
return new Observable(subscriber => {
|
|
2575
|
+
const subscription = queryObservable.subscribe({
|
|
2576
|
+
next: (result) => {
|
|
2577
|
+
// Log successful query
|
|
2578
|
+
this.logQuerySuccess(config, selectionSet, retryAttempt, result);
|
|
2579
|
+
// Validate that relationship field data is populated
|
|
2580
|
+
if (this.validateRelationshipFieldPopulation(result, config)) {
|
|
2581
|
+
subscriber.next(result);
|
|
2582
|
+
}
|
|
2583
|
+
else {
|
|
2584
|
+
// Relationship field not populated, try retry if possible
|
|
2585
|
+
const error = new Error(`Relationship field '${config.fieldName}' not populated in query results`);
|
|
2586
|
+
this.handleQueryError(error, config, baseId, filter, modelName, retryAttempt, previousErrors, subscriber);
|
|
2587
|
+
}
|
|
2588
|
+
},
|
|
2589
|
+
error: (error) => {
|
|
2590
|
+
this.handleQueryError(error, config, baseId, filter, modelName, retryAttempt, previousErrors, subscriber);
|
|
2591
|
+
},
|
|
2592
|
+
complete: () => {
|
|
2593
|
+
subscriber.complete();
|
|
2594
|
+
}
|
|
2595
|
+
});
|
|
2596
|
+
// Return cleanup function
|
|
2597
|
+
return () => subscription.unsubscribe();
|
|
2598
|
+
});
|
|
2599
|
+
}
|
|
2600
|
+
catch (error) {
|
|
2601
|
+
console.error(`AmplifyModelService: Error in retry attempt ${retryAttempt}:`, error);
|
|
2602
|
+
// If we've exhausted retries, return null
|
|
2603
|
+
if (retryAttempt >= maxRetries) {
|
|
2604
|
+
this.logFinalFailure(config, previousErrors, error);
|
|
2605
|
+
return null;
|
|
2606
|
+
}
|
|
2607
|
+
// Try next retry level
|
|
2608
|
+
const newErrors = [...previousErrors, error instanceof Error ? error.message : String(error)];
|
|
2609
|
+
return this.getRelatedItemsWithRetry(config, baseId, filter, modelName, retryAttempt + 1, newErrors);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
/**
|
|
2613
|
+
* Handle GraphQL query errors with retry logic
|
|
2614
|
+
* Enhanced with comprehensive error logging and context
|
|
2615
|
+
*/
|
|
2616
|
+
handleQueryError(error, config, baseId, filter, modelName, retryAttempt, previousErrors, subscriber) {
|
|
2617
|
+
const maxRetries = 3;
|
|
2618
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2619
|
+
// Log comprehensive error details using ErrorHandlerService
|
|
2620
|
+
const selectionSetError = this.errorHandler.logGraphQLError(error, config, [], // Selection set will be logged separately in retry attempts
|
|
2621
|
+
retryAttempt, previousErrors);
|
|
2622
|
+
console.error(`AmplifyModelService: GraphQL query error on attempt ${retryAttempt + 1}:`, error);
|
|
2623
|
+
// Check if we should retry
|
|
2624
|
+
if (retryAttempt < maxRetries && this.shouldRetryOnError(error)) {
|
|
2625
|
+
console.log(`AmplifyModelService: Retrying with simpler selection set (attempt ${retryAttempt + 1}/${maxRetries})`);
|
|
2626
|
+
// Add current error to the list
|
|
2627
|
+
const newErrors = [...previousErrors, errorMessage];
|
|
2628
|
+
// Attempt retry with next fallback level
|
|
2629
|
+
const retryObservable = this.getRelatedItemsWithRetry(config, baseId, filter, modelName, retryAttempt + 1, newErrors);
|
|
2630
|
+
if (retryObservable) {
|
|
2631
|
+
// Subscribe to retry attempt
|
|
2632
|
+
const retrySubscription = retryObservable.subscribe({
|
|
2633
|
+
next: (result) => {
|
|
2634
|
+
// Log successful recovery
|
|
2635
|
+
this.errorHandler.logSuccessfulRecovery(selectionSetError, `retry_attempt_${retryAttempt + 1}`, [] // Selection set would be determined in retry
|
|
2636
|
+
);
|
|
2637
|
+
subscriber.next(result);
|
|
2638
|
+
},
|
|
2639
|
+
error: (retryError) => subscriber.error(retryError),
|
|
2640
|
+
complete: () => subscriber.complete()
|
|
2641
|
+
});
|
|
2642
|
+
// Handle cleanup
|
|
2643
|
+
subscriber.add(() => retrySubscription.unsubscribe());
|
|
2644
|
+
}
|
|
2645
|
+
else {
|
|
2646
|
+
// No more retries possible
|
|
2647
|
+
this.errorHandler.logFailedRecovery(selectionSetError, newErrors, new Error('Retry observable creation failed'));
|
|
2648
|
+
subscriber.error(error);
|
|
2649
|
+
}
|
|
446
2650
|
}
|
|
447
2651
|
else {
|
|
448
|
-
|
|
449
|
-
|
|
2652
|
+
// No more retries or error is not retryable
|
|
2653
|
+
const finalErrors = [...previousErrors, errorMessage];
|
|
2654
|
+
this.errorHandler.logFailedRecovery(selectionSetError, finalErrors, error);
|
|
2655
|
+
console.error('AmplifyModelService: All retry attempts exhausted or error not retryable');
|
|
2656
|
+
subscriber.error(error);
|
|
450
2657
|
}
|
|
451
2658
|
}
|
|
452
2659
|
/**
|
|
453
|
-
*
|
|
2660
|
+
* Determine if an error should trigger a retry
|
|
2661
|
+
* Enhanced with comprehensive error pattern matching
|
|
454
2662
|
*/
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
2663
|
+
shouldRetryOnError(error) {
|
|
2664
|
+
try {
|
|
2665
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
2666
|
+
const errorString = errorMessage.toLowerCase();
|
|
2667
|
+
// Retry on GraphQL field errors, selection set errors, but not on auth errors
|
|
2668
|
+
const retryableErrors = [
|
|
2669
|
+
'field',
|
|
2670
|
+
'selection',
|
|
2671
|
+
'cannot query',
|
|
2672
|
+
'unknown field',
|
|
2673
|
+
'syntax error',
|
|
2674
|
+
'invalid selection set',
|
|
2675
|
+
'validation error',
|
|
2676
|
+
'parse error'
|
|
2677
|
+
];
|
|
2678
|
+
const nonRetryableErrors = [
|
|
2679
|
+
'unauthorized',
|
|
2680
|
+
'forbidden',
|
|
2681
|
+
'access denied',
|
|
2682
|
+
'authentication',
|
|
2683
|
+
'permission',
|
|
2684
|
+
'not authorized',
|
|
2685
|
+
'invalid credentials'
|
|
2686
|
+
];
|
|
2687
|
+
// Don't retry on auth errors
|
|
2688
|
+
if (nonRetryableErrors.some(pattern => errorString.includes(pattern))) {
|
|
2689
|
+
console.log(`AmplifyModelService: Not retrying due to auth error: ${errorMessage}`);
|
|
2690
|
+
return false;
|
|
2691
|
+
}
|
|
2692
|
+
// Retry on field/selection errors
|
|
2693
|
+
const shouldRetry = retryableErrors.some(pattern => errorString.includes(pattern));
|
|
2694
|
+
console.log(`AmplifyModelService: Error retry decision: ${shouldRetry} for error: ${errorMessage}`);
|
|
2695
|
+
return shouldRetry;
|
|
2696
|
+
}
|
|
2697
|
+
catch (retryDecisionError) {
|
|
2698
|
+
this.errorHandler.logSelectionSetError(SelectionSetErrorType.GRAPHQL_ERROR, `Error determining retry eligibility: ${retryDecisionError instanceof Error ? retryDecisionError.message : String(retryDecisionError)}`, undefined, undefined, {
|
|
2699
|
+
method: 'shouldRetryOnError',
|
|
2700
|
+
originalError: error,
|
|
2701
|
+
retryDecisionError: {
|
|
2702
|
+
message: retryDecisionError instanceof Error ? retryDecisionError.message : String(retryDecisionError),
|
|
2703
|
+
stack: retryDecisionError instanceof Error ? retryDecisionError.stack : undefined
|
|
2704
|
+
}
|
|
2705
|
+
});
|
|
2706
|
+
// Default to not retrying if we can't determine
|
|
2707
|
+
return false;
|
|
459
2708
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
2709
|
+
}
|
|
2710
|
+
/**
|
|
2711
|
+
* Validate that relationship field data is populated in query results
|
|
2712
|
+
* Enhanced with comprehensive error logging
|
|
2713
|
+
*/
|
|
2714
|
+
validateRelationshipFieldPopulation(result, config) {
|
|
2715
|
+
try {
|
|
2716
|
+
if (!result || !result.data) {
|
|
2717
|
+
this.errorHandler.logSelectionSetError(SelectionSetErrorType.GRAPHQL_ERROR, 'Query result is missing or has no data property', config, undefined, {
|
|
2718
|
+
method: 'validateRelationshipFieldPopulation',
|
|
2719
|
+
hasResult: !!result,
|
|
2720
|
+
hasData: !!(result?.data)
|
|
2721
|
+
});
|
|
2722
|
+
return false;
|
|
2723
|
+
}
|
|
2724
|
+
// Check if any records have the relationship field populated
|
|
2725
|
+
const records = Array.isArray(result.data) ? result.data : [result.data];
|
|
2726
|
+
let populatedCount = 0;
|
|
2727
|
+
for (const record of records) {
|
|
2728
|
+
if (record && record[config.fieldName] !== undefined && record[config.fieldName] !== null) {
|
|
2729
|
+
populatedCount++;
|
|
2730
|
+
}
|
|
468
2731
|
}
|
|
469
|
-
|
|
2732
|
+
if (populatedCount === 0) {
|
|
2733
|
+
// Log warning if no relationship fields are populated
|
|
2734
|
+
this.errorHandler.logSelectionSetError(SelectionSetErrorType.FIELD_NOT_FOUND, `No records have relationship field '${config.fieldName}' populated`, config, undefined, {
|
|
2735
|
+
method: 'validateRelationshipFieldPopulation',
|
|
2736
|
+
recordCount: records.length,
|
|
2737
|
+
populatedCount,
|
|
2738
|
+
sampleRecord: records[0] ? Object.keys(records[0]) : [],
|
|
2739
|
+
fieldName: config.fieldName
|
|
2740
|
+
});
|
|
2741
|
+
console.warn(`AmplifyModelService: No records have relationship field '${config.fieldName}' populated`, {
|
|
2742
|
+
config,
|
|
2743
|
+
recordCount: records.length,
|
|
2744
|
+
sampleRecord: records[0]
|
|
2745
|
+
});
|
|
2746
|
+
return false;
|
|
2747
|
+
}
|
|
2748
|
+
console.log(`AmplifyModelService: Relationship field validation successful: ${populatedCount}/${records.length} records have populated '${config.fieldName}' field`);
|
|
2749
|
+
return true;
|
|
470
2750
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
2751
|
+
catch (error) {
|
|
2752
|
+
this.errorHandler.logSelectionSetError(SelectionSetErrorType.GRAPHQL_ERROR, `Error validating relationship field population: ${error instanceof Error ? error.message : String(error)}`, config, undefined, {
|
|
2753
|
+
method: 'validateRelationshipFieldPopulation',
|
|
2754
|
+
originalError: {
|
|
2755
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2756
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
2757
|
+
}
|
|
2758
|
+
});
|
|
2759
|
+
console.error('AmplifyModelService: Error validating relationship field population:', error);
|
|
2760
|
+
return false;
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
/**
|
|
2764
|
+
* Log query attempt details
|
|
2765
|
+
*/
|
|
2766
|
+
logQueryAttempt(config, selectionSet, retryAttempt, previousErrors) {
|
|
2767
|
+
console.log('AmplifyModelService: Query attempt details:', {
|
|
2768
|
+
attempt: retryAttempt + 1,
|
|
2769
|
+
config: {
|
|
2770
|
+
relationshipModelName: config.relationshipModelName,
|
|
2771
|
+
fieldName: config.fieldName
|
|
2772
|
+
},
|
|
2773
|
+
selectionSet,
|
|
2774
|
+
previousErrors: previousErrors.length,
|
|
2775
|
+
timestamp: new Date().toISOString()
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
/**
|
|
2779
|
+
* Log successful query details
|
|
2780
|
+
*/
|
|
2781
|
+
logQuerySuccess(config, selectionSet, retryAttempt, result) {
|
|
2782
|
+
const recordCount = result?.data ? (Array.isArray(result.data) ? result.data.length : 1) : 0;
|
|
2783
|
+
console.log('AmplifyModelService: Query successful:', {
|
|
2784
|
+
attempt: retryAttempt + 1,
|
|
2785
|
+
config: {
|
|
2786
|
+
relationshipModelName: config.relationshipModelName,
|
|
2787
|
+
fieldName: config.fieldName
|
|
2788
|
+
},
|
|
2789
|
+
selectionSet,
|
|
2790
|
+
recordCount,
|
|
2791
|
+
timestamp: new Date().toISOString()
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
/**
|
|
2795
|
+
* Log query error details
|
|
2796
|
+
*/
|
|
2797
|
+
logQueryError(config, error, retryAttempt) {
|
|
2798
|
+
console.error('AmplifyModelService: Query error details:', {
|
|
2799
|
+
attempt: retryAttempt + 1,
|
|
2800
|
+
config: {
|
|
2801
|
+
relationshipModelName: config.relationshipModelName,
|
|
2802
|
+
baseModelName: config.baseModelName,
|
|
2803
|
+
fieldName: config.fieldName,
|
|
2804
|
+
associatedWith: config.associatedWith
|
|
2805
|
+
},
|
|
2806
|
+
error: {
|
|
2807
|
+
message: error instanceof Error ? error.message : String(error),
|
|
2808
|
+
type: typeof error,
|
|
2809
|
+
name: error instanceof Error ? error.name : undefined
|
|
2810
|
+
},
|
|
2811
|
+
timestamp: new Date().toISOString()
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
2814
|
+
/**
|
|
2815
|
+
* Log final failure when all retries are exhausted
|
|
2816
|
+
* Enhanced with comprehensive error context and recovery suggestions
|
|
2817
|
+
*/
|
|
2818
|
+
logFinalFailure(config, allErrors, finalError) {
|
|
2819
|
+
try {
|
|
2820
|
+
const errorSummary = {
|
|
2821
|
+
config: {
|
|
2822
|
+
relationshipModelName: config.relationshipModelName,
|
|
2823
|
+
baseModelName: config.baseModelName,
|
|
2824
|
+
fieldName: config.fieldName,
|
|
2825
|
+
associatedWith: config.associatedWith
|
|
2826
|
+
},
|
|
2827
|
+
totalAttempts: allErrors.length + 1,
|
|
2828
|
+
allErrors,
|
|
2829
|
+
finalError: {
|
|
2830
|
+
message: finalError instanceof Error ? finalError.message : String(finalError),
|
|
2831
|
+
type: typeof finalError,
|
|
2832
|
+
name: finalError instanceof Error ? finalError.name : undefined
|
|
2833
|
+
},
|
|
2834
|
+
timestamp: new Date().toISOString(),
|
|
2835
|
+
suggestions: [
|
|
2836
|
+
'Check if the relationship model exists in your Amplify schema',
|
|
2837
|
+
'Verify that the field name matches the relationship field in your model',
|
|
2838
|
+
'Ensure the relationship is properly configured in your GraphQL schema',
|
|
2839
|
+
'Check if there are any authorization rules preventing access to the relationship data'
|
|
2840
|
+
]
|
|
2841
|
+
};
|
|
2842
|
+
// Log using ErrorHandlerService for comprehensive tracking
|
|
2843
|
+
this.errorHandler.logFailedRecovery(this.errorHandler.logSelectionSetError(SelectionSetErrorType.GRAPHQL_ERROR, 'All retry attempts failed for relationship query', config, undefined, {
|
|
2844
|
+
method: 'logFinalFailure',
|
|
2845
|
+
errorSummary
|
|
2846
|
+
}), allErrors, finalError);
|
|
2847
|
+
console.error('AmplifyModelService: All retry attempts failed:', errorSummary);
|
|
2848
|
+
}
|
|
2849
|
+
catch (loggingError) {
|
|
2850
|
+
// Even logging failed - use basic console error
|
|
2851
|
+
console.error('AmplifyModelService: Critical error - failed to log final failure:', {
|
|
2852
|
+
originalError: finalError,
|
|
2853
|
+
loggingError,
|
|
2854
|
+
config: config?.fieldName || 'unknown'
|
|
2855
|
+
});
|
|
474
2856
|
}
|
|
475
2857
|
}
|
|
476
2858
|
createItemPromise(model, payload) {
|
|
@@ -559,7 +2941,7 @@ class AmplifyModelService {
|
|
|
559
2941
|
return null;
|
|
560
2942
|
}
|
|
561
2943
|
}
|
|
562
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AmplifyModelService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2944
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AmplifyModelService, deps: [{ token: SelectionSetGeneratorService }, { token: ErrorHandlerService }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
563
2945
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AmplifyModelService, providedIn: 'root' });
|
|
564
2946
|
}
|
|
565
2947
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AmplifyModelService, decorators: [{
|
|
@@ -567,7 +2949,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
567
2949
|
args: [{
|
|
568
2950
|
providedIn: 'root'
|
|
569
2951
|
}]
|
|
570
|
-
}], ctorParameters: () => [] });
|
|
2952
|
+
}], ctorParameters: () => [{ type: SelectionSetGeneratorService }, { type: ErrorHandlerService }] });
|
|
571
2953
|
|
|
572
2954
|
class DynamicFormQuestionComponent {
|
|
573
2955
|
rootFormGroup;
|
|
@@ -798,46 +3180,441 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
798
3180
|
// import outputs from '../../../../amplify_outputs.json';
|
|
799
3181
|
class DynamicRelationshipBuilderComponent {
|
|
800
3182
|
ams;
|
|
3183
|
+
errorHandler;
|
|
801
3184
|
m2m;
|
|
802
3185
|
item;
|
|
803
3186
|
model;
|
|
804
|
-
m2mItems;
|
|
805
|
-
|
|
3187
|
+
m2mItems = [];
|
|
3188
|
+
errorMessage = '';
|
|
3189
|
+
isLoading = false;
|
|
3190
|
+
constructor(ams, errorHandler) {
|
|
806
3191
|
this.ams = ams;
|
|
3192
|
+
this.errorHandler = errorHandler;
|
|
807
3193
|
}
|
|
808
3194
|
modelItem; // = signal('');
|
|
809
3195
|
dialog = inject(MatDialog);
|
|
810
3196
|
ngOnInit() {
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
3197
|
+
try {
|
|
3198
|
+
console.log('DynamicRelationshipBuilderComponent initializing with m2m config:', this.m2m);
|
|
3199
|
+
// Validate required inputs with comprehensive error logging
|
|
3200
|
+
if (!this.m2m) {
|
|
3201
|
+
const error = this.errorHandler.logSelectionSetError(SelectionSetErrorType.INVALID_CONFIGURATION, 'Relationship configuration (m2m) is missing', undefined, undefined, {
|
|
3202
|
+
component: 'DynamicRelationshipBuilderComponent',
|
|
3203
|
+
method: 'ngOnInit',
|
|
3204
|
+
hasItem: !!this.item,
|
|
3205
|
+
hasModel: !!this.model
|
|
3206
|
+
});
|
|
3207
|
+
this.setError('Relationship configuration is missing');
|
|
3208
|
+
return;
|
|
3209
|
+
}
|
|
3210
|
+
if (!this.item || !this.item.id) {
|
|
3211
|
+
const error = this.errorHandler.logSelectionSetError(SelectionSetErrorType.INVALID_CONFIGURATION, 'Item data is missing or invalid (no ID)', this.m2m, undefined, {
|
|
3212
|
+
component: 'DynamicRelationshipBuilderComponent',
|
|
3213
|
+
method: 'ngOnInit',
|
|
3214
|
+
hasItem: !!this.item,
|
|
3215
|
+
itemId: this.item?.id,
|
|
3216
|
+
hasModel: !!this.model
|
|
3217
|
+
});
|
|
3218
|
+
this.setError('Item data is missing or invalid');
|
|
3219
|
+
return;
|
|
3220
|
+
}
|
|
3221
|
+
// Validate m2m configuration structure
|
|
3222
|
+
const requiredM2MFields = ['relationshipModelName', 'fieldName', 'baseModelName', 'associatedWith'];
|
|
3223
|
+
const missingFields = requiredM2MFields.filter(field => !this.m2m[field]);
|
|
3224
|
+
if (missingFields.length > 0) {
|
|
3225
|
+
const error = this.errorHandler.logSelectionSetError(SelectionSetErrorType.INVALID_CONFIGURATION, `M2M configuration is missing required fields: ${missingFields.join(', ')}`, this.m2m, undefined, {
|
|
3226
|
+
component: 'DynamicRelationshipBuilderComponent',
|
|
3227
|
+
method: 'ngOnInit',
|
|
3228
|
+
missingFields,
|
|
3229
|
+
providedFields: Object.keys(this.m2m || {})
|
|
3230
|
+
});
|
|
3231
|
+
this.setError(`Configuration is incomplete: missing ${missingFields.join(', ')}`);
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
3234
|
+
this.clearError();
|
|
3235
|
+
console.log('DynamicRelationshipBuilderComponent: Configuration validated successfully');
|
|
3236
|
+
// Initialize relationship data loading
|
|
3237
|
+
this.listItems();
|
|
3238
|
+
this.setCreateSubscription();
|
|
3239
|
+
}
|
|
3240
|
+
catch (error) {
|
|
3241
|
+
const componentError = this.errorHandler.logSelectionSetError(SelectionSetErrorType.GRAPHQL_ERROR, `Unexpected error during component initialization: ${error instanceof Error ? error.message : String(error)}`, this.m2m, undefined, {
|
|
3242
|
+
component: 'DynamicRelationshipBuilderComponent',
|
|
3243
|
+
method: 'ngOnInit',
|
|
3244
|
+
originalError: {
|
|
3245
|
+
message: error instanceof Error ? error.message : String(error),
|
|
3246
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
3247
|
+
}
|
|
3248
|
+
});
|
|
3249
|
+
console.error('DynamicRelationshipBuilderComponent: Critical initialization error:', error);
|
|
3250
|
+
this.setError('Failed to initialize relationship component');
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
/**
|
|
3254
|
+
* Set error message and clear loading state
|
|
3255
|
+
* Enhanced with comprehensive error context logging
|
|
3256
|
+
*/
|
|
3257
|
+
setError(message, additionalContext) {
|
|
3258
|
+
try {
|
|
3259
|
+
this.errorMessage = message;
|
|
3260
|
+
this.isLoading = false;
|
|
3261
|
+
// Log error with context using ErrorHandlerService
|
|
3262
|
+
this.errorHandler.logSelectionSetError(SelectionSetErrorType.GRAPHQL_ERROR, `Component error: ${message}`, this.m2m, undefined, {
|
|
3263
|
+
component: 'DynamicRelationshipBuilderComponent',
|
|
3264
|
+
method: 'setError',
|
|
3265
|
+
userMessage: message,
|
|
3266
|
+
componentState: {
|
|
3267
|
+
hasM2M: !!this.m2m,
|
|
3268
|
+
hasItem: !!this.item,
|
|
3269
|
+
itemId: this.item?.id,
|
|
3270
|
+
m2mItemsCount: Array.isArray(this.m2mItems) ? this.m2mItems.length : 0,
|
|
3271
|
+
isLoading: this.isLoading
|
|
3272
|
+
},
|
|
3273
|
+
...additionalContext
|
|
3274
|
+
});
|
|
3275
|
+
console.error('DynamicRelationshipBuilderComponent error:', {
|
|
3276
|
+
message,
|
|
3277
|
+
context: additionalContext,
|
|
3278
|
+
timestamp: new Date().toISOString()
|
|
3279
|
+
});
|
|
3280
|
+
}
|
|
3281
|
+
catch (loggingError) {
|
|
3282
|
+
// Fallback if even error logging fails
|
|
3283
|
+
console.error('DynamicRelationshipBuilderComponent: Critical error in setError:', {
|
|
3284
|
+
originalMessage: message,
|
|
3285
|
+
loggingError,
|
|
3286
|
+
timestamp: new Date().toISOString()
|
|
3287
|
+
});
|
|
3288
|
+
this.errorMessage = message;
|
|
3289
|
+
this.isLoading = false;
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
/**
|
|
3293
|
+
* Clear error message and log recovery
|
|
3294
|
+
*/
|
|
3295
|
+
clearError() {
|
|
3296
|
+
if (this.errorMessage) {
|
|
3297
|
+
console.log('DynamicRelationshipBuilderComponent: Clearing previous error:', this.errorMessage);
|
|
3298
|
+
}
|
|
3299
|
+
this.errorMessage = '';
|
|
3300
|
+
}
|
|
3301
|
+
/**
|
|
3302
|
+
* Get display name for a relationship item, handling various field names
|
|
3303
|
+
*/
|
|
3304
|
+
getDisplayName(relationshipItem) {
|
|
3305
|
+
if (!relationshipItem) {
|
|
3306
|
+
return 'Unknown item';
|
|
3307
|
+
}
|
|
3308
|
+
// Try common display field names
|
|
3309
|
+
return relationshipItem.name ||
|
|
3310
|
+
relationshipItem.title ||
|
|
3311
|
+
relationshipItem.label ||
|
|
3312
|
+
relationshipItem.displayName ||
|
|
3313
|
+
`Item ${relationshipItem.id || 'Unknown'}`;
|
|
3314
|
+
}
|
|
3315
|
+
/**
|
|
3316
|
+
* Get display name for items with partial or missing relationship data
|
|
3317
|
+
*/
|
|
3318
|
+
getPartialDisplayName(item) {
|
|
3319
|
+
if (!item) {
|
|
3320
|
+
return 'Unknown item';
|
|
3321
|
+
}
|
|
3322
|
+
// If we have an ID, show it
|
|
3323
|
+
if (item.id) {
|
|
3324
|
+
return `Item ${item.id} (incomplete data)`;
|
|
3325
|
+
}
|
|
3326
|
+
return 'Incomplete relationship data';
|
|
3327
|
+
}
|
|
3328
|
+
/**
|
|
3329
|
+
* Check if an item has complete relationship data
|
|
3330
|
+
*/
|
|
3331
|
+
hasCompleteRelationshipData(item) {
|
|
3332
|
+
if (!item || !this.m2m?.fieldName) {
|
|
3333
|
+
return false;
|
|
3334
|
+
}
|
|
3335
|
+
const relationshipData = item[this.m2m.fieldName];
|
|
3336
|
+
return relationshipData && (relationshipData.name || relationshipData.title || relationshipData.id);
|
|
3337
|
+
}
|
|
3338
|
+
/**
|
|
3339
|
+
* Check if an item has partial relationship data (item exists but relationship data is incomplete)
|
|
3340
|
+
*/
|
|
3341
|
+
hasPartialRelationshipData(item) {
|
|
3342
|
+
if (!item || !item.id) {
|
|
3343
|
+
return false;
|
|
3344
|
+
}
|
|
3345
|
+
return !this.hasCompleteRelationshipData(item);
|
|
3346
|
+
}
|
|
3347
|
+
/**
|
|
3348
|
+
* Get loading state indicator for relationship data
|
|
3349
|
+
*/
|
|
3350
|
+
getRelationshipLoadingState(item) {
|
|
3351
|
+
if (!item) {
|
|
3352
|
+
return 'missing';
|
|
3353
|
+
}
|
|
3354
|
+
if (this.isLoading) {
|
|
3355
|
+
return 'loading';
|
|
3356
|
+
}
|
|
3357
|
+
if (this.hasCompleteRelationshipData(item)) {
|
|
3358
|
+
return 'complete';
|
|
3359
|
+
}
|
|
3360
|
+
if (item.id) {
|
|
3361
|
+
return 'partial';
|
|
3362
|
+
}
|
|
3363
|
+
return 'missing';
|
|
3364
|
+
}
|
|
3365
|
+
/**
|
|
3366
|
+
* Get appropriate CSS class for relationship item based on data completeness
|
|
3367
|
+
*/
|
|
3368
|
+
getRelationshipItemClass(item) {
|
|
3369
|
+
const state = this.getRelationshipLoadingState(item);
|
|
3370
|
+
return `relationship-item relationship-item--${state}`;
|
|
3371
|
+
}
|
|
3372
|
+
/**
|
|
3373
|
+
* Get tooltip text for relationship items based on their data state
|
|
3374
|
+
*/
|
|
3375
|
+
getRelationshipTooltip(item) {
|
|
3376
|
+
const state = this.getRelationshipLoadingState(item);
|
|
3377
|
+
switch (state) {
|
|
3378
|
+
case 'complete':
|
|
3379
|
+
return 'Complete relationship data loaded';
|
|
3380
|
+
case 'partial':
|
|
3381
|
+
return 'Relationship exists but some data is missing or failed to load';
|
|
3382
|
+
case 'loading':
|
|
3383
|
+
return 'Loading relationship data...';
|
|
3384
|
+
case 'missing':
|
|
3385
|
+
return 'No relationship data available';
|
|
3386
|
+
default:
|
|
3387
|
+
return '';
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
/**
|
|
3391
|
+
* Get summary of data states for all relationship items
|
|
3392
|
+
*/
|
|
3393
|
+
getDataStateSummary() {
|
|
3394
|
+
if (!Array.isArray(this.m2mItems)) {
|
|
3395
|
+
return {
|
|
3396
|
+
completeCount: 0,
|
|
3397
|
+
partialCount: 0,
|
|
3398
|
+
missingCount: 0,
|
|
3399
|
+
hasPartialOrMissing: false
|
|
3400
|
+
};
|
|
3401
|
+
}
|
|
3402
|
+
let completeCount = 0;
|
|
3403
|
+
let partialCount = 0;
|
|
3404
|
+
let missingCount = 0;
|
|
3405
|
+
this.m2mItems.forEach(item => {
|
|
3406
|
+
const state = this.getRelationshipLoadingState(item);
|
|
3407
|
+
switch (state) {
|
|
3408
|
+
case 'complete':
|
|
3409
|
+
completeCount++;
|
|
3410
|
+
break;
|
|
3411
|
+
case 'partial':
|
|
3412
|
+
partialCount++;
|
|
3413
|
+
break;
|
|
3414
|
+
case 'missing':
|
|
3415
|
+
missingCount++;
|
|
3416
|
+
break;
|
|
3417
|
+
}
|
|
3418
|
+
});
|
|
3419
|
+
return {
|
|
3420
|
+
completeCount,
|
|
3421
|
+
partialCount,
|
|
3422
|
+
missingCount,
|
|
3423
|
+
hasPartialOrMissing: partialCount > 0 || missingCount > 0
|
|
3424
|
+
};
|
|
814
3425
|
}
|
|
815
3426
|
async perhapsAddM2MItem(item) {
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
3427
|
+
try {
|
|
3428
|
+
if (!item || !item.id) {
|
|
3429
|
+
console.warn('Invalid item received in perhapsAddM2MItem:', item);
|
|
3430
|
+
return;
|
|
3431
|
+
}
|
|
3432
|
+
let m2mItem = { id: item.id };
|
|
3433
|
+
// Safely attempt to get relationship data
|
|
3434
|
+
if (this.m2m?.fieldName && typeof item[this.m2m.fieldName] === 'function') {
|
|
3435
|
+
try {
|
|
3436
|
+
const { data: retItem } = await item[this.m2m.fieldName]();
|
|
3437
|
+
m2mItem[this.m2m.fieldName] = retItem;
|
|
3438
|
+
}
|
|
3439
|
+
catch (relationshipError) {
|
|
3440
|
+
console.warn('Failed to load relationship data for item:', item.id, relationshipError);
|
|
3441
|
+
// Keep the item but mark it as having incomplete data
|
|
3442
|
+
m2mItem[this.m2m.fieldName] = null;
|
|
3443
|
+
}
|
|
824
3444
|
}
|
|
3445
|
+
else {
|
|
3446
|
+
console.warn('Invalid fieldName or item method for relationship:', this.m2m?.fieldName);
|
|
3447
|
+
m2mItem[this.m2m.fieldName] = null;
|
|
3448
|
+
}
|
|
3449
|
+
// Ensure m2mItems is initialized as array
|
|
3450
|
+
if (!Array.isArray(this.m2mItems)) {
|
|
3451
|
+
this.m2mItems = [];
|
|
3452
|
+
}
|
|
3453
|
+
let existingInd = this.m2mItems.findIndex((x) => x?.id === item.id);
|
|
3454
|
+
if (existingInd > -1) {
|
|
3455
|
+
if (!this.m2mItems[existingInd][this.m2m.fieldName]) {
|
|
3456
|
+
console.log('Removing existing item with no relationship data', this.m2mItems[existingInd]);
|
|
3457
|
+
this.m2mItems.splice(existingInd, 1);
|
|
3458
|
+
}
|
|
3459
|
+
}
|
|
3460
|
+
this.m2mItems.push(m2mItem);
|
|
3461
|
+
}
|
|
3462
|
+
catch (error) {
|
|
3463
|
+
console.error('Error in perhapsAddM2MItem:', error);
|
|
3464
|
+
// Don't let individual item errors break the entire component
|
|
825
3465
|
}
|
|
826
|
-
this.m2mItems.push(m2mItem);
|
|
827
|
-
//console.log('perhapsAddM2MItem', item, 'm2m', this.m2m);
|
|
828
3466
|
}
|
|
829
3467
|
setCreateSubscription() {
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
3468
|
+
try {
|
|
3469
|
+
if (!this.m2m || !this.item?.id) {
|
|
3470
|
+
console.warn('Cannot set subscription: missing m2m or item.id');
|
|
3471
|
+
return;
|
|
3472
|
+
}
|
|
3473
|
+
const subscription = this.ams.setRelatedSub(this.m2m, this.item.id);
|
|
3474
|
+
if (subscription) {
|
|
3475
|
+
subscription.subscribe({
|
|
3476
|
+
next: (data) => {
|
|
3477
|
+
this.perhapsAddM2MItem(data);
|
|
3478
|
+
},
|
|
3479
|
+
error: (error) => {
|
|
3480
|
+
console.warn('Subscription error for related items:', error);
|
|
3481
|
+
this.setError('Failed to receive real-time updates for relationships');
|
|
3482
|
+
},
|
|
3483
|
+
});
|
|
3484
|
+
}
|
|
3485
|
+
else {
|
|
3486
|
+
console.warn('Failed to create subscription for related items');
|
|
3487
|
+
}
|
|
3488
|
+
}
|
|
3489
|
+
catch (error) {
|
|
3490
|
+
console.error('Error setting up subscription:', error);
|
|
3491
|
+
this.setError('Failed to set up real-time updates');
|
|
3492
|
+
}
|
|
834
3493
|
}
|
|
835
3494
|
listItems() {
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
3495
|
+
try {
|
|
3496
|
+
if (!this.m2m || !this.item?.id) {
|
|
3497
|
+
const error = 'Cannot load relationships: missing configuration or item data';
|
|
3498
|
+
this.setError(error, {
|
|
3499
|
+
method: 'listItems',
|
|
3500
|
+
hasM2M: !!this.m2m,
|
|
3501
|
+
hasItem: !!this.item,
|
|
3502
|
+
itemId: this.item?.id
|
|
3503
|
+
});
|
|
3504
|
+
return;
|
|
3505
|
+
}
|
|
3506
|
+
this.isLoading = true;
|
|
3507
|
+
this.clearError();
|
|
3508
|
+
console.log('DynamicRelationshipBuilderComponent: Loading relationship items for:', {
|
|
3509
|
+
relationshipModel: this.m2m.relationshipModelName,
|
|
3510
|
+
fieldName: this.m2m.fieldName,
|
|
3511
|
+
itemId: this.item.id
|
|
3512
|
+
});
|
|
3513
|
+
const observable = this.ams.getRelatedItems(this.m2m, this.item.id);
|
|
3514
|
+
if (observable) {
|
|
3515
|
+
observable.subscribe({
|
|
3516
|
+
next: ({ items, isSynced }) => {
|
|
3517
|
+
try {
|
|
3518
|
+
this.isLoading = false;
|
|
3519
|
+
// Ensure items is an array
|
|
3520
|
+
if (Array.isArray(items)) {
|
|
3521
|
+
this.m2mItems = items;
|
|
3522
|
+
console.log('DynamicRelationshipBuilderComponent: Successfully loaded relationship items:', {
|
|
3523
|
+
count: items.length,
|
|
3524
|
+
isSynced,
|
|
3525
|
+
sampleItem: items[0] ? Object.keys(items[0]) : []
|
|
3526
|
+
});
|
|
3527
|
+
}
|
|
3528
|
+
else {
|
|
3529
|
+
console.warn('DynamicRelationshipBuilderComponent: Received non-array items:', items);
|
|
3530
|
+
this.m2mItems = [];
|
|
3531
|
+
this.errorHandler.logSelectionSetError(SelectionSetErrorType.GRAPHQL_ERROR, 'Received non-array items from relationship query', this.m2m, undefined, {
|
|
3532
|
+
component: 'DynamicRelationshipBuilderComponent',
|
|
3533
|
+
method: 'listItems',
|
|
3534
|
+
receivedType: typeof items,
|
|
3535
|
+
receivedValue: items,
|
|
3536
|
+
isSynced
|
|
3537
|
+
});
|
|
3538
|
+
}
|
|
3539
|
+
// Analyze data quality
|
|
3540
|
+
const dataStateSummary = this.getDataStateSummary();
|
|
3541
|
+
if (dataStateSummary.hasPartialOrMissing) {
|
|
3542
|
+
console.warn('DynamicRelationshipBuilderComponent: Some relationship data is incomplete:', dataStateSummary);
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
catch (processingError) {
|
|
3546
|
+
this.isLoading = false;
|
|
3547
|
+
const error = `Error processing relationship data: ${processingError instanceof Error ? processingError.message : String(processingError)}`;
|
|
3548
|
+
this.setError('Failed to process relationship data', {
|
|
3549
|
+
method: 'listItems',
|
|
3550
|
+
step: 'data_processing',
|
|
3551
|
+
originalError: {
|
|
3552
|
+
message: processingError instanceof Error ? processingError.message : String(processingError),
|
|
3553
|
+
stack: processingError instanceof Error ? processingError.stack : undefined
|
|
3554
|
+
}
|
|
3555
|
+
});
|
|
3556
|
+
this.m2mItems = [];
|
|
3557
|
+
}
|
|
3558
|
+
},
|
|
3559
|
+
error: (error) => {
|
|
3560
|
+
this.isLoading = false;
|
|
3561
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3562
|
+
console.error('DynamicRelationshipBuilderComponent: Error loading relationship items:', error);
|
|
3563
|
+
// Log comprehensive error details
|
|
3564
|
+
this.errorHandler.logSelectionSetError(SelectionSetErrorType.GRAPHQL_ERROR, `Failed to load relationship data: ${errorMessage}`, this.m2m, undefined, {
|
|
3565
|
+
component: 'DynamicRelationshipBuilderComponent',
|
|
3566
|
+
method: 'listItems',
|
|
3567
|
+
step: 'observable_error',
|
|
3568
|
+
originalError: {
|
|
3569
|
+
message: errorMessage,
|
|
3570
|
+
stack: error instanceof Error ? error.stack : undefined,
|
|
3571
|
+
name: error instanceof Error ? error.name : undefined
|
|
3572
|
+
},
|
|
3573
|
+
itemId: this.item.id
|
|
3574
|
+
});
|
|
3575
|
+
this.setError('Failed to load relationship data. Please try refreshing the page.', {
|
|
3576
|
+
method: 'listItems',
|
|
3577
|
+
originalError: errorMessage
|
|
3578
|
+
});
|
|
3579
|
+
// Initialize empty array so component doesn't break
|
|
3580
|
+
this.m2mItems = [];
|
|
3581
|
+
}
|
|
3582
|
+
});
|
|
3583
|
+
}
|
|
3584
|
+
else {
|
|
3585
|
+
this.isLoading = false;
|
|
3586
|
+
this.errorHandler.logSelectionSetError(SelectionSetErrorType.GRAPHQL_ERROR, 'AmplifyModelService returned null observable for relationship query', this.m2m, undefined, {
|
|
3587
|
+
component: 'DynamicRelationshipBuilderComponent',
|
|
3588
|
+
method: 'listItems',
|
|
3589
|
+
step: 'observable_creation',
|
|
3590
|
+
itemId: this.item.id
|
|
3591
|
+
});
|
|
3592
|
+
this.setError('Unable to create relationship query', {
|
|
3593
|
+
method: 'listItems',
|
|
3594
|
+
step: 'observable_creation'
|
|
3595
|
+
});
|
|
3596
|
+
this.m2mItems = [];
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
catch (error) {
|
|
3600
|
+
this.isLoading = false;
|
|
3601
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3602
|
+
console.error('DynamicRelationshipBuilderComponent: Critical error in listItems:', error);
|
|
3603
|
+
this.errorHandler.logSelectionSetError(SelectionSetErrorType.GRAPHQL_ERROR, `Critical error in listItems: ${errorMessage}`, this.m2m, undefined, {
|
|
3604
|
+
component: 'DynamicRelationshipBuilderComponent',
|
|
3605
|
+
method: 'listItems',
|
|
3606
|
+
step: 'critical_error',
|
|
3607
|
+
originalError: {
|
|
3608
|
+
message: errorMessage,
|
|
3609
|
+
stack: error instanceof Error ? error.stack : undefined
|
|
3610
|
+
}
|
|
3611
|
+
});
|
|
3612
|
+
this.setError('An unexpected error occurred while loading relationships', {
|
|
3613
|
+
method: 'listItems',
|
|
3614
|
+
originalError: errorMessage
|
|
3615
|
+
});
|
|
3616
|
+
this.m2mItems = [];
|
|
3617
|
+
}
|
|
841
3618
|
}
|
|
842
3619
|
/**
|
|
843
3620
|
Customize this function to include all desired models from your file
|
|
@@ -848,44 +3625,111 @@ class DynamicRelationshipBuilderComponent {
|
|
|
848
3625
|
this.modelItem = signal(this.ams.getEmptyObjectForModel(mdl), ...(ngDevMode ? [{ debugName: "modelItem" }] : []));
|
|
849
3626
|
}
|
|
850
3627
|
async deleteRelatedItem(item) {
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
3628
|
+
try {
|
|
3629
|
+
if (!item || !item.id) {
|
|
3630
|
+
console.warn('Cannot delete item: invalid item data', item);
|
|
3631
|
+
this.setError('Cannot delete item: invalid data');
|
|
3632
|
+
return;
|
|
3633
|
+
}
|
|
3634
|
+
if (!this.m2m?.relationshipModelName) {
|
|
3635
|
+
console.warn('Cannot delete item: missing relationship model name');
|
|
3636
|
+
this.setError('Cannot delete item: missing configuration');
|
|
3637
|
+
return;
|
|
3638
|
+
}
|
|
3639
|
+
console.log('deleteRelatedItem for item', item, 'm2m', this.m2m);
|
|
3640
|
+
await this.ams.deleteRelationship(this.m2m.relationshipModelName, item.id);
|
|
3641
|
+
// Remove item from local array on successful deletion
|
|
3642
|
+
if (Array.isArray(this.m2mItems)) {
|
|
3643
|
+
const index = this.m2mItems.findIndex(x => x?.id === item.id);
|
|
3644
|
+
if (index > -1) {
|
|
3645
|
+
this.m2mItems.splice(index, 1);
|
|
3646
|
+
}
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
catch (error) {
|
|
3650
|
+
console.error('Error deleting relationship item:', error);
|
|
3651
|
+
this.setError('Failed to delete relationship. Please try again.');
|
|
3652
|
+
}
|
|
854
3653
|
}
|
|
855
3654
|
openDialog(rel) {
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
3655
|
+
try {
|
|
3656
|
+
if (!rel) {
|
|
3657
|
+
console.warn('Cannot open dialog: missing relationship configuration');
|
|
3658
|
+
this.setError('Cannot add relationship: missing configuration');
|
|
3659
|
+
return;
|
|
3660
|
+
}
|
|
3661
|
+
console.log('openDialog clicked for rel', rel);
|
|
3662
|
+
this.setModelItem(rel.partnerModelName);
|
|
3663
|
+
const dialogRef = this.dialog.open(AddRelationshipDialogComponent, {
|
|
3664
|
+
data: { rel: rel, modelItem: this.modelItem(), selectedItems: this.m2mItems || [] },
|
|
3665
|
+
});
|
|
3666
|
+
dialogRef.afterClosed().subscribe({
|
|
3667
|
+
next: (result) => {
|
|
3668
|
+
if (result) {
|
|
3669
|
+
this.handleDialogResult(result, rel);
|
|
3670
|
+
}
|
|
3671
|
+
},
|
|
3672
|
+
error: (error) => {
|
|
3673
|
+
console.error('Dialog error:', error);
|
|
3674
|
+
this.setError('An error occurred while adding the relationship');
|
|
3675
|
+
}
|
|
3676
|
+
});
|
|
3677
|
+
}
|
|
3678
|
+
catch (error) {
|
|
3679
|
+
console.error('Error opening dialog:', error);
|
|
3680
|
+
this.setError('Failed to open relationship dialog');
|
|
3681
|
+
}
|
|
865
3682
|
}
|
|
866
3683
|
handleDialogResult(result, rel) {
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
this.
|
|
3684
|
+
try {
|
|
3685
|
+
if (!result) {
|
|
3686
|
+
return;
|
|
3687
|
+
}
|
|
3688
|
+
if (!rel || !this.item?.id) {
|
|
3689
|
+
console.warn('Cannot handle dialog result: missing relationship or item data');
|
|
3690
|
+
this.setError('Cannot create relationship: missing data');
|
|
3691
|
+
return;
|
|
3692
|
+
}
|
|
3693
|
+
console.log('handleDialogResult', result, 'rel', rel);
|
|
3694
|
+
if (result !== undefined) {
|
|
3695
|
+
this.modelItem.set(result);
|
|
3696
|
+
}
|
|
3697
|
+
let relIdName = rel.partnerModelName?.toLowerCase() + 'Id';
|
|
3698
|
+
let thisIdName = rel.baseModelName?.toLowerCase() + 'Id';
|
|
3699
|
+
if (!relIdName || !thisIdName) {
|
|
3700
|
+
console.warn('Cannot determine relationship field names');
|
|
3701
|
+
this.setError('Cannot create relationship: invalid configuration');
|
|
3702
|
+
return;
|
|
3703
|
+
}
|
|
3704
|
+
let newRelObj = {};
|
|
3705
|
+
newRelObj[relIdName] = result.id;
|
|
3706
|
+
newRelObj[thisIdName] = this.item.id;
|
|
3707
|
+
const createPromise = this.ams.createRelationship(rel.relationshipModelName, newRelObj);
|
|
3708
|
+
if (createPromise) {
|
|
3709
|
+
createPromise.catch((error) => {
|
|
3710
|
+
console.error('Error creating relationship:', error);
|
|
3711
|
+
this.setError('Failed to create relationship. Please try again.');
|
|
3712
|
+
});
|
|
3713
|
+
}
|
|
3714
|
+
else {
|
|
3715
|
+
console.warn('Failed to create relationship: service returned null');
|
|
3716
|
+
this.setError('Failed to create relationship. Please try again.');
|
|
3717
|
+
}
|
|
3718
|
+
}
|
|
3719
|
+
catch (error) {
|
|
3720
|
+
console.error('Error handling dialog result:', error);
|
|
3721
|
+
this.setError('An error occurred while creating the relationship');
|
|
872
3722
|
}
|
|
873
|
-
let relIdName = rel.partnerModelName.toLowerCase() + 'Id';
|
|
874
|
-
let thisIdName = rel.baseModelName.toLowerCase() + 'Id';
|
|
875
|
-
let newRelObj = {};
|
|
876
|
-
newRelObj[relIdName] = result.id;
|
|
877
|
-
newRelObj[thisIdName] = this.item.id;
|
|
878
|
-
this.ams.createRelationship(rel.relationshipModelName, newRelObj);
|
|
879
3723
|
}
|
|
880
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DynamicRelationshipBuilderComponent, deps: [{ token: AmplifyModelService }], target: i0.ɵɵFactoryTarget.Component });
|
|
881
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: DynamicRelationshipBuilderComponent, isStandalone: true, selector: "snteam-dynamic-relationship-builder", inputs: { m2m: "m2m", item: "item", model: "model" }, ngImport: i0, template: "<div class=\"record-relationship-holder\">\n <div class=\"relationship-type\">\n <mat-toolbar>\n <span>{{m2m
|
|
3724
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DynamicRelationshipBuilderComponent, deps: [{ token: AmplifyModelService }, { token: ErrorHandlerService }], target: i0.ɵɵFactoryTarget.Component });
|
|
3725
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: DynamicRelationshipBuilderComponent, isStandalone: true, selector: "snteam-dynamic-relationship-builder", inputs: { m2m: "m2m", item: "item", model: "model" }, ngImport: i0, template: "<div class=\"record-relationship-holder\">\n <div class=\"relationship-type\">\n <mat-toolbar>\n <span>{{m2m?.partnerModelName || 'Unknown'}} Management</span>\n <span class=\"rel-title-spacer\"></span>\n <button mat-icon-button [attr.aria-label]=\"'Add ' + (m2m?.name || 'item')\" (click)=\"openDialog(m2m)\" [disabled]=\"!m2m\">\n <mat-icon>add</mat-icon>\n </button>\n </mat-toolbar>\n \n <!-- Error message display -->\n @if (errorMessage) {\n <div class=\"error-message\" role=\"alert\">\n <mat-icon>error</mat-icon>\n <span>{{ errorMessage }}</span>\n </div>\n }\n \n <!-- Loading state -->\n @if (isLoading) {\n <div class=\"loading-message\">\n <mat-icon>hourglass_empty</mat-icon>\n <span>Loading relationships...</span>\n </div>\n }\n \n <!-- Relationship items display -->\n @if (m2mItems && m2mItems.length > 0) {\n <mat-list>\n @for (item of m2mItems; track item?.id) {\n <mat-list-item [class]=\"getRelationshipItemClass(item)\" [title]=\"getRelationshipTooltip(item)\">\n @if (hasCompleteRelationshipData(item)) {\n <!-- Complete relationship data -->\n <span matListItemTitle>{{ getDisplayName(item[m2m.fieldName]) }}</span>\n <span matListItemLine class=\"relationship-status complete\">Complete data</span>\n <button mat-icon-button (click)=\"deleteRelatedItem(item)\" matListItemMeta [attr.aria-label]=\"'Delete ' + getDisplayName(item[m2m.fieldName])\">\n <mat-icon>delete</mat-icon>\n </button>\n } @else if (hasPartialRelationshipData(item)) {\n <!-- Partial relationship data -->\n <span matListItemTitle class=\"partial-data\">\n {{ getPartialDisplayName(item) }}\n <mat-icon class=\"warning-icon\" title=\"Incomplete relationship data\">warning</mat-icon>\n </span>\n <span matListItemLine class=\"relationship-status partial\">Partial data - some information may be missing</span>\n <button mat-icon-button (click)=\"deleteRelatedItem(item)\" matListItemMeta [attr.aria-label]=\"'Delete incomplete item'\">\n <mat-icon>delete</mat-icon>\n </button>\n } @else {\n <!-- Missing or invalid relationship data -->\n <span matListItemTitle class=\"missing-data\">\n Invalid relationship data\n <mat-icon class=\"error-icon\" title=\"Invalid or missing data\">error</mat-icon>\n </span>\n <span matListItemLine class=\"relationship-status missing\">Data could not be loaded</span>\n <button mat-icon-button (click)=\"deleteRelatedItem(item)\" matListItemMeta [attr.aria-label]=\"'Delete invalid item'\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </mat-list-item>\n }\n </mat-list>\n \n <!-- Summary of data states -->\n @if (getDataStateSummary().hasPartialOrMissing) {\n <div class=\"data-summary\">\n <mat-icon>info</mat-icon>\n <span>\n {{ getDataStateSummary().completeCount }} of {{ m2mItems.length }} relationships loaded completely.\n @if (getDataStateSummary().partialCount > 0) {\n {{ getDataStateSummary().partialCount }} have partial data.\n }\n @if (getDataStateSummary().missingCount > 0) {\n {{ getDataStateSummary().missingCount }} failed to load.\n }\n </span>\n </div>\n }\n } @else if (!isLoading && !errorMessage) {\n <div class=\"empty-state\">\n Add your first {{ m2m?.partnerModelName || 'item' }}\n </div>\n }\n </div>\n</div>", styles: [".relationship-type{margin-top:20px}.rel-title-spacer{flex:1 1 auto}mat-toolbar button{background:transparent;color:#000}.error-message{display:flex;align-items:center;gap:8px;padding:12px;background-color:#ffebee;color:#c62828;border-left:4px solid #c62828;margin:8px 0}.loading-message{display:flex;align-items:center;gap:8px;padding:12px;background-color:#f5f5f5;color:#666;margin:8px 0}.partial-data{display:flex;align-items:center;gap:4px;color:#ff9800}.missing-data{display:flex;align-items:center;gap:4px;color:#f44336}.warning-icon{font-size:16px;width:16px;height:16px;color:#ff9800}.error-icon{font-size:16px;width:16px;height:16px;color:#f44336}.empty-state{padding:20px;text-align:center;color:#666;font-style:italic}.relationship-status{font-size:12px;opacity:.7}.relationship-status.complete{color:#4caf50}.relationship-status.partial{color:#ff9800}.relationship-status.missing{color:#f44336}.relationship-item--complete{border-left:3px solid #4caf50}.relationship-item--partial{border-left:3px solid #ff9800;background-color:#fff8e1}.relationship-item--missing{border-left:3px solid #f44336;background-color:#ffebee}.relationship-item--loading{border-left:3px solid #2196f3;background-color:#e3f2fd}.data-summary{display:flex;align-items:center;gap:8px;padding:12px;background-color:#e8f5e8;color:#2e7d32;border-radius:4px;margin:8px 0;font-size:14px}\n"], dependencies: [{ kind: "ngmodule", type: MatToolbarModule }, { kind: "component", type: i3$4.MatToolbar, selector: "mat-toolbar", inputs: ["color"], exportAs: ["matToolbar"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatInputModule }, { kind: "ngmodule", type: MatListModule }, { kind: "component", type: i6$1.MatList, selector: "mat-list", exportAs: ["matList"] }, { kind: "component", type: i6$1.MatListItem, selector: "mat-list-item, a[mat-list-item], button[mat-list-item]", inputs: ["activated"], exportAs: ["matListItem"] }, { kind: "directive", type: i6$1.MatListItemLine, selector: "[matListItemLine]" }, { kind: "directive", type: i6$1.MatListItemTitle, selector: "[matListItemTitle]" }, { kind: "directive", type: i6$1.MatListItemMeta, selector: "[matListItemMeta]" }, { kind: "ngmodule", type: FormsModule }, { kind: "ngmodule", type: CommonModule }] });
|
|
882
3726
|
}
|
|
883
3727
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DynamicRelationshipBuilderComponent, decorators: [{
|
|
884
3728
|
type: Component,
|
|
885
3729
|
args: [{ selector: 'snteam-dynamic-relationship-builder', standalone: true, imports: [MatToolbarModule, MatButtonModule, MatIconModule, MatFormFieldModule, MatInputModule, MatListModule,
|
|
886
3730
|
FormsModule,
|
|
887
|
-
CommonModule], template: "<div class=\"record-relationship-holder\">\n <div class=\"relationship-type\">\n <mat-toolbar>\n <span>{{m2m
|
|
888
|
-
}], ctorParameters: () => [{ type: AmplifyModelService }], propDecorators: { m2m: [{
|
|
3731
|
+
CommonModule], template: "<div class=\"record-relationship-holder\">\n <div class=\"relationship-type\">\n <mat-toolbar>\n <span>{{m2m?.partnerModelName || 'Unknown'}} Management</span>\n <span class=\"rel-title-spacer\"></span>\n <button mat-icon-button [attr.aria-label]=\"'Add ' + (m2m?.name || 'item')\" (click)=\"openDialog(m2m)\" [disabled]=\"!m2m\">\n <mat-icon>add</mat-icon>\n </button>\n </mat-toolbar>\n \n <!-- Error message display -->\n @if (errorMessage) {\n <div class=\"error-message\" role=\"alert\">\n <mat-icon>error</mat-icon>\n <span>{{ errorMessage }}</span>\n </div>\n }\n \n <!-- Loading state -->\n @if (isLoading) {\n <div class=\"loading-message\">\n <mat-icon>hourglass_empty</mat-icon>\n <span>Loading relationships...</span>\n </div>\n }\n \n <!-- Relationship items display -->\n @if (m2mItems && m2mItems.length > 0) {\n <mat-list>\n @for (item of m2mItems; track item?.id) {\n <mat-list-item [class]=\"getRelationshipItemClass(item)\" [title]=\"getRelationshipTooltip(item)\">\n @if (hasCompleteRelationshipData(item)) {\n <!-- Complete relationship data -->\n <span matListItemTitle>{{ getDisplayName(item[m2m.fieldName]) }}</span>\n <span matListItemLine class=\"relationship-status complete\">Complete data</span>\n <button mat-icon-button (click)=\"deleteRelatedItem(item)\" matListItemMeta [attr.aria-label]=\"'Delete ' + getDisplayName(item[m2m.fieldName])\">\n <mat-icon>delete</mat-icon>\n </button>\n } @else if (hasPartialRelationshipData(item)) {\n <!-- Partial relationship data -->\n <span matListItemTitle class=\"partial-data\">\n {{ getPartialDisplayName(item) }}\n <mat-icon class=\"warning-icon\" title=\"Incomplete relationship data\">warning</mat-icon>\n </span>\n <span matListItemLine class=\"relationship-status partial\">Partial data - some information may be missing</span>\n <button mat-icon-button (click)=\"deleteRelatedItem(item)\" matListItemMeta [attr.aria-label]=\"'Delete incomplete item'\">\n <mat-icon>delete</mat-icon>\n </button>\n } @else {\n <!-- Missing or invalid relationship data -->\n <span matListItemTitle class=\"missing-data\">\n Invalid relationship data\n <mat-icon class=\"error-icon\" title=\"Invalid or missing data\">error</mat-icon>\n </span>\n <span matListItemLine class=\"relationship-status missing\">Data could not be loaded</span>\n <button mat-icon-button (click)=\"deleteRelatedItem(item)\" matListItemMeta [attr.aria-label]=\"'Delete invalid item'\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n </mat-list-item>\n }\n </mat-list>\n \n <!-- Summary of data states -->\n @if (getDataStateSummary().hasPartialOrMissing) {\n <div class=\"data-summary\">\n <mat-icon>info</mat-icon>\n <span>\n {{ getDataStateSummary().completeCount }} of {{ m2mItems.length }} relationships loaded completely.\n @if (getDataStateSummary().partialCount > 0) {\n {{ getDataStateSummary().partialCount }} have partial data.\n }\n @if (getDataStateSummary().missingCount > 0) {\n {{ getDataStateSummary().missingCount }} failed to load.\n }\n </span>\n </div>\n }\n } @else if (!isLoading && !errorMessage) {\n <div class=\"empty-state\">\n Add your first {{ m2m?.partnerModelName || 'item' }}\n </div>\n }\n </div>\n</div>", styles: [".relationship-type{margin-top:20px}.rel-title-spacer{flex:1 1 auto}mat-toolbar button{background:transparent;color:#000}.error-message{display:flex;align-items:center;gap:8px;padding:12px;background-color:#ffebee;color:#c62828;border-left:4px solid #c62828;margin:8px 0}.loading-message{display:flex;align-items:center;gap:8px;padding:12px;background-color:#f5f5f5;color:#666;margin:8px 0}.partial-data{display:flex;align-items:center;gap:4px;color:#ff9800}.missing-data{display:flex;align-items:center;gap:4px;color:#f44336}.warning-icon{font-size:16px;width:16px;height:16px;color:#ff9800}.error-icon{font-size:16px;width:16px;height:16px;color:#f44336}.empty-state{padding:20px;text-align:center;color:#666;font-style:italic}.relationship-status{font-size:12px;opacity:.7}.relationship-status.complete{color:#4caf50}.relationship-status.partial{color:#ff9800}.relationship-status.missing{color:#f44336}.relationship-item--complete{border-left:3px solid #4caf50}.relationship-item--partial{border-left:3px solid #ff9800;background-color:#fff8e1}.relationship-item--missing{border-left:3px solid #f44336;background-color:#ffebee}.relationship-item--loading{border-left:3px solid #2196f3;background-color:#e3f2fd}.data-summary{display:flex;align-items:center;gap:8px;padding:12px;background-color:#e8f5e8;color:#2e7d32;border-radius:4px;margin:8px 0;font-size:14px}\n"] }]
|
|
3732
|
+
}], ctorParameters: () => [{ type: AmplifyModelService }, { type: ErrorHandlerService }], propDecorators: { m2m: [{
|
|
889
3733
|
type: Input
|
|
890
3734
|
}], item: [{
|
|
891
3735
|
type: Input
|
|
@@ -2036,7 +4880,7 @@ class DynamicFormComponent {
|
|
|
2036
4880
|
return this.form.get(question.key);
|
|
2037
4881
|
return true;
|
|
2038
4882
|
}
|
|
2039
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DynamicFormComponent, deps: [{ token: QuestionControlService }, { token: AmplifyFormBuilderService }, { token: i3$1.Location }, { token: i4$
|
|
4883
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DynamicFormComponent, deps: [{ token: QuestionControlService }, { token: AmplifyFormBuilderService }, { token: i3$1.Location }, { token: i4$1.ActivatedRoute }], target: i0.ɵɵFactoryTarget.Component });
|
|
2040
4884
|
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: DynamicFormComponent, isStandalone: true, selector: "snteam-dynamic-form", inputs: { model: "model", itemId: "itemId", valueMap: "valueMap", hideRelationships: "hideRelationships", hideSaveButton: "hideSaveButton", amplifyOutputs: "amplifyOutputs", formViewId: "formViewId" }, outputs: { itemOutput: "itemOutput", submitOutput: "submitOutput", formLoaded: "formLoaded" }, ngImport: i0, template: "<div class=\"dynamic-form-holder\">\n @if (form && questions) {\n <form (ngSubmit)=\"onSubmit()\" [formGroup]=\"form\">\n @for (question of questions; track question) {\n @if (this.switchInputs.includes(question.controlType) || this.materialInputs.includes(question.controlType) || this.refInputs.includes(question.controlType)) {\n <div class=\"form-row\">\n <snteam-dynamic-form-question [question]=\"question\" [formGroup]=\"form\"></snteam-dynamic-form-question>\n </div>\n }\n @if (this.formGroupInputs.includes(question.controlType)) {\n <div class=\"group-row\">\n <snteam-dynamic-form-group [question]=\"question\" [formGroup]=\"form\" [formGroupName]=\"question.key\"></snteam-dynamic-form-group>\n </div>\n }\n }\n @if (!hideSaveButton) {\n <div class=\"form-row\">\n <button mat-raised-button color=\"primary\" type=\"submit\" [disabled]=\"!form.valid\">Save</button>\n </div>\n }\n </form>\n } @else {\n <div class=\"loading\">Loading...</div>\n }\n\n @if(item && !hideRelationships){\n <div class=\"relationship-holder\">\n <snteam-record-relationships [model]=\"mdl\" [id]=\"id\" [item]=\"item\" [amplifyOutputs]=\"outputs\"></snteam-record-relationships>\n </div>\n }\n</div>", styles: [".dynamic-form-holder{padding:20px;width:100%;display:flex;justify-content:space-between;gap:20px}form{width:100%;flex-basis:40%}.relationship-holder{flex-basis:40%}.full-width{width:100%}.form-row,.group-row{margin-top:20px}.loading{padding:20px;text-align:center;color:#666}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: DynamicFormQuestionComponent, selector: "snteam-dynamic-form-question", inputs: ["question", "formGroup"] }, { kind: "component", type: DynamicFormGroupComponent, selector: "snteam-dynamic-form-group", inputs: ["formGroupName", "question", "formGroup"] }, { kind: "component", type: RecordRelationshipsComponent, selector: "snteam-record-relationships", inputs: ["model", "id", "item", "amplifyOutputs"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormGroupName, selector: "[formGroupName]", inputs: ["formGroupName"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "ngmodule", type: MatInputModule }, { kind: "ngmodule", type: MatSelectModule }, { kind: "ngmodule", type: MatSnackBarModule }, { kind: "ngmodule", type: MatDividerModule }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }] });
|
|
2041
4885
|
}
|
|
2042
4886
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: DynamicFormComponent, decorators: [{
|
|
@@ -2055,7 +4899,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
2055
4899
|
MatDividerModule,
|
|
2056
4900
|
MatButtonModule
|
|
2057
4901
|
], template: "<div class=\"dynamic-form-holder\">\n @if (form && questions) {\n <form (ngSubmit)=\"onSubmit()\" [formGroup]=\"form\">\n @for (question of questions; track question) {\n @if (this.switchInputs.includes(question.controlType) || this.materialInputs.includes(question.controlType) || this.refInputs.includes(question.controlType)) {\n <div class=\"form-row\">\n <snteam-dynamic-form-question [question]=\"question\" [formGroup]=\"form\"></snteam-dynamic-form-question>\n </div>\n }\n @if (this.formGroupInputs.includes(question.controlType)) {\n <div class=\"group-row\">\n <snteam-dynamic-form-group [question]=\"question\" [formGroup]=\"form\" [formGroupName]=\"question.key\"></snteam-dynamic-form-group>\n </div>\n }\n }\n @if (!hideSaveButton) {\n <div class=\"form-row\">\n <button mat-raised-button color=\"primary\" type=\"submit\" [disabled]=\"!form.valid\">Save</button>\n </div>\n }\n </form>\n } @else {\n <div class=\"loading\">Loading...</div>\n }\n\n @if(item && !hideRelationships){\n <div class=\"relationship-holder\">\n <snteam-record-relationships [model]=\"mdl\" [id]=\"id\" [item]=\"item\" [amplifyOutputs]=\"outputs\"></snteam-record-relationships>\n </div>\n }\n</div>", styles: [".dynamic-form-holder{padding:20px;width:100%;display:flex;justify-content:space-between;gap:20px}form{width:100%;flex-basis:40%}.relationship-holder{flex-basis:40%}.full-width{width:100%}.form-row,.group-row{margin-top:20px}.loading{padding:20px;text-align:center;color:#666}\n"] }]
|
|
2058
|
-
}], ctorParameters: () => [{ type: QuestionControlService }, { type: AmplifyFormBuilderService }, { type: i3$1.Location }, { type: i4$
|
|
4902
|
+
}], ctorParameters: () => [{ type: QuestionControlService }, { type: AmplifyFormBuilderService }, { type: i3$1.Location }, { type: i4$1.ActivatedRoute }], propDecorators: { model: [{
|
|
2059
4903
|
type: Input
|
|
2060
4904
|
}], itemId: [{
|
|
2061
4905
|
type: Input
|
|
@@ -2194,7 +5038,7 @@ class ListViewComponent {
|
|
|
2194
5038
|
this.listItems();
|
|
2195
5039
|
}
|
|
2196
5040
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ListViewComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2197
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: ListViewComponent, isStandalone: true, selector: "snteam-list-view", inputs: { modelName: "modelName", customItemTemplate: "customItemTemplate", hideNewButton: "hideNewButton", title: "title", useRouter: "useRouter", showRowActions: "showRowActions", showDeleteAction: "showDeleteAction", customRowActions: "customRowActions" }, outputs: { itemClick: "itemClick", newClick: "newClick", itemsLoaded: "itemsLoaded", itemDeleted: "itemDeleted" }, ngImport: i0, template: "<div class=\"list-view-container\">\n <div class=\"header\">\n <h2>{{ title || modelName }}</h2>\n @if (!hideNewButton) {\n <button mat-raised-button color=\"primary\" (click)=\"onNewClick()\">\n <mat-icon>add</mat-icon>\n New {{ modelName }}\n </button>\n }\n </div>\n\n @if (loading) {\n <div class=\"loading\">Loading {{ modelName }}...</div>\n }\n\n @if (error) {\n <div class=\"error\">\n {{ error }}\n <button mat-button (click)=\"refresh()\">Retry</button>\n </div>\n }\n\n @if (!loading && !error && itemsArr.length === 0) {\n <div class=\"empty-state\">\n <p>No {{ modelName }} found</p>\n @if (!hideNewButton) {\n <button mat-raised-button color=\"primary\" (click)=\"onNewClick()\">\n Create First {{ modelName }}\n </button>\n }\n </div>\n }\n\n @if (!loading && !error && itemsArr.length > 0) {\n <mat-list class=\"items-list\">\n @for (item of itemsArr; track item.id) {\n <mat-list-item (click)=\"onItemClick(item, $event)\" class=\"clickable-item\">\n @if (customItemTemplate) {\n <ng-container [ngTemplateOutlet]=\"customItemTemplate\" [ngTemplateOutletContext]=\"{ $implicit: item }\"></ng-container>\n } @else {\n <div class=\"item-content\">\n <div class=\"item-text\">\n <span matListItemTitle>{{ getDisplayText(item) }}</span>\n <span matListItemLine>{{ item.description || (item.createdAt | date) }}</span>\n </div>\n @if (showRowActions || showDeleteAction || customRowActions.length > 0) {\n <div class=\"row-actions\">\n @if (showDeleteAction) {\n <button mat-icon-button color=\"warn\" (click)=\"onDeleteItem(item, $event)\" \n matTooltip=\"Delete\" class=\"action-button\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n @for (action of customRowActions; track action.label) {\n <button mat-icon-button [color]=\"action.color || 'primary'\" \n (click)=\"onCustomAction(action, item, $event)\"\n [matTooltip]=\"action.label\" class=\"action-button\">\n <mat-icon>{{ action.icon }}</mat-icon>\n </button>\n }\n </div>\n }\n </div>\n }\n </mat-list-item>\n }\n </mat-list>\n }\n</div>", styles: [".list-view-container{padding:20px;width:100%}.header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.header h2{margin:0;color:#333}.loading,.error,.empty-state{padding:40px 20px;text-align:center;color:#666}.error{color:#d32f2f}.empty-state p{margin-bottom:20px;font-size:16px}.items-list{border:1px solid #e0e0e0;border-radius:4px}.clickable-item{cursor:pointer;transition:background-color .2s}.clickable-item:hover{background-color:#f5f5f5}.clickable-item:not(:last-child){border-bottom:1px solid #e0e0e0}.item-content{display:flex;justify-content:space-between;align-items:center;width:100%;padding:8px 0}.item-text{flex:1;display:flex;flex-direction:column}.row-actions{display:flex;gap:4px;margin-left:16px}.action-button{width:32px;height:32px;line-height:32px}.action-button mat-icon{font-size:18px;width:18px;height:18px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: MatListModule }, { kind: "component", type:
|
|
5041
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: ListViewComponent, isStandalone: true, selector: "snteam-list-view", inputs: { modelName: "modelName", customItemTemplate: "customItemTemplate", hideNewButton: "hideNewButton", title: "title", useRouter: "useRouter", showRowActions: "showRowActions", showDeleteAction: "showDeleteAction", customRowActions: "customRowActions" }, outputs: { itemClick: "itemClick", newClick: "newClick", itemsLoaded: "itemsLoaded", itemDeleted: "itemDeleted" }, ngImport: i0, template: "<div class=\"list-view-container\">\n <div class=\"header\">\n <h2>{{ title || modelName }}</h2>\n @if (!hideNewButton) {\n <button mat-raised-button color=\"primary\" (click)=\"onNewClick()\">\n <mat-icon>add</mat-icon>\n New {{ modelName }}\n </button>\n }\n </div>\n\n @if (loading) {\n <div class=\"loading\">Loading {{ modelName }}...</div>\n }\n\n @if (error) {\n <div class=\"error\">\n {{ error }}\n <button mat-button (click)=\"refresh()\">Retry</button>\n </div>\n }\n\n @if (!loading && !error && itemsArr.length === 0) {\n <div class=\"empty-state\">\n <p>No {{ modelName }} found</p>\n @if (!hideNewButton) {\n <button mat-raised-button color=\"primary\" (click)=\"onNewClick()\">\n Create First {{ modelName }}\n </button>\n }\n </div>\n }\n\n @if (!loading && !error && itemsArr.length > 0) {\n <mat-list class=\"items-list\">\n @for (item of itemsArr; track item.id) {\n <mat-list-item (click)=\"onItemClick(item, $event)\" class=\"clickable-item\">\n @if (customItemTemplate) {\n <ng-container [ngTemplateOutlet]=\"customItemTemplate\" [ngTemplateOutletContext]=\"{ $implicit: item }\"></ng-container>\n } @else {\n <div class=\"item-content\">\n <div class=\"item-text\">\n <span matListItemTitle>{{ getDisplayText(item) }}</span>\n <span matListItemLine>{{ item.description || (item.createdAt | date) }}</span>\n </div>\n @if (showRowActions || showDeleteAction || customRowActions.length > 0) {\n <div class=\"row-actions\">\n @if (showDeleteAction) {\n <button mat-icon-button color=\"warn\" (click)=\"onDeleteItem(item, $event)\" \n matTooltip=\"Delete\" class=\"action-button\">\n <mat-icon>delete</mat-icon>\n </button>\n }\n @for (action of customRowActions; track action.label) {\n <button mat-icon-button [color]=\"action.color || 'primary'\" \n (click)=\"onCustomAction(action, item, $event)\"\n [matTooltip]=\"action.label\" class=\"action-button\">\n <mat-icon>{{ action.icon }}</mat-icon>\n </button>\n }\n </div>\n }\n </div>\n }\n </mat-list-item>\n }\n </mat-list>\n }\n</div>", styles: [".list-view-container{padding:20px;width:100%}.header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.header h2{margin:0;color:#333}.loading,.error,.empty-state{padding:40px 20px;text-align:center;color:#666}.error{color:#d32f2f}.empty-state p{margin-bottom:20px;font-size:16px}.items-list{border:1px solid #e0e0e0;border-radius:4px}.clickable-item{cursor:pointer;transition:background-color .2s}.clickable-item:hover{background-color:#f5f5f5}.clickable-item:not(:last-child){border-bottom:1px solid #e0e0e0}.item-content{display:flex;justify-content:space-between;align-items:center;width:100%;padding:8px 0}.item-text{flex:1;display:flex;flex-direction:column}.row-actions{display:flex;gap:4px;margin-left:16px}.action-button{width:32px;height:32px;line-height:32px}.action-button mat-icon{font-size:18px;width:18px;height:18px}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3$1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: MatListModule }, { kind: "component", type: i6$1.MatList, selector: "mat-list", exportAs: ["matList"] }, { kind: "component", type: i6$1.MatListItem, selector: "mat-list-item, a[mat-list-item], button[mat-list-item]", inputs: ["activated"], exportAs: ["matListItem"] }, { kind: "directive", type: i6$1.MatListItemLine, selector: "[matListItemLine]" }, { kind: "directive", type: i6$1.MatListItemTitle, selector: "[matListItemTitle]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i3$3.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatSnackBarModule }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i5$2.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "pipe", type: i3$1.DatePipe, name: "date" }] });
|
|
2198
5042
|
}
|
|
2199
5043
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ListViewComponent, decorators: [{
|
|
2200
5044
|
type: Component,
|
|
@@ -2528,7 +5372,7 @@ return fields;
|
|
|
2528
5372
|
return 'text'; // Default fallback
|
|
2529
5373
|
}
|
|
2530
5374
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ConfigurationsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2531
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: ConfigurationsComponent, isStandalone: true, selector: "snteam-configurations", inputs: { amplifyOutputs: "amplifyOutputs" }, ngImport: i0, template: "<div class=\"configurations-container\">\n <h2>System Configurations</h2>\n \n <mat-accordion multi=\"true\">\n \n <!-- Table Configs Section -->\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title>\n <mat-icon>table_chart</mat-icon>\n <span class=\"panel-title\">Table Configs</span>\n </mat-panel-title>\n <mat-panel-description>\n Manage table configurations and settings\n </mat-panel-description>\n </mat-expansion-panel-header>\n \n <div class=\"panel-content\">\n @if (hasTableConfig) {\n <div class=\"section-actions\">\n <button mat-raised-button color=\"accent\" (click)=\"populateDefaultTableConfigs()\" class=\"populate-button\">\n <mat-icon>auto_fix_high</mat-icon>\n Populate Default Table Configs\n </button>\n <button mat-raised-button color=\"warn\" (click)=\"deleteTableConfigs()\" class=\"delete-button\">\n <mat-icon>delete</mat-icon>\n Delete All Table Configs\n </button>\n </div>\n <snteam-list-view \n [modelName]=\"'TableConfig'\"\n [useRouter]=\"true\"\n [showDeleteAction]=\"true\">\n </snteam-list-view>\n } @else {\n <mat-card class=\"error-card\">\n <mat-card-content>\n <mat-icon class=\"error-icon\">error_outline</mat-icon>\n <h3>TableConfig Model Not Found</h3>\n <p>The TableConfig model is not available in your Amplify schema. Please add it to your data model to manage table configurations.</p>\n <button mat-raised-button color=\"primary\" class=\"help-button\">\n <mat-icon>help</mat-icon>\n View Documentation\n </button>\n </mat-card-content>\n </mat-card>\n }\n </div>\n </mat-expansion-panel>\n\n <!-- Form Views Section -->\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title>\n <mat-icon>dynamic_form</mat-icon>\n <span class=\"panel-title\">Form Views</span>\n </mat-panel-title>\n <mat-panel-description>\n Configure custom form layouts and field selections\n </mat-panel-description>\n </mat-expansion-panel-header>\n \n <div class=\"panel-content\">\n @if (hasFormView) {\n <div class=\"section-actions\">\n <button mat-raised-button color=\"warn\" (click)=\"deleteFormViews()\" class=\"delete-button\">\n <mat-icon>delete</mat-icon>\n Delete All Form Views\n </button>\n </div>\n <snteam-list-view \n [modelName]=\"'FormView'\"\n [useRouter]=\"true\"\n [showDeleteAction]=\"true\">\n </snteam-list-view>\n } @else {\n <mat-card class=\"error-card\">\n <mat-card-content>\n <mat-icon class=\"error-icon\">error_outline</mat-icon>\n <h3>FormView Model Not Found</h3>\n <p>The FormView model is not available in your Amplify schema. Please add it to your data model to create custom form configurations.</p>\n <button mat-raised-button color=\"primary\" class=\"help-button\">\n <mat-icon>help</mat-icon>\n View Documentation\n </button>\n </mat-card-content>\n </mat-card>\n }\n </div>\n </mat-expansion-panel>\n\n <!-- List Views Section -->\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title>\n <mat-icon>view_list</mat-icon>\n <span class=\"panel-title\">List Views</span>\n </mat-panel-title>\n <mat-panel-description>\n Customize list displays and column configurations\n </mat-panel-description>\n </mat-expansion-panel-header>\n \n <div class=\"panel-content\">\n @if (hasListView) {\n <div class=\"section-actions\">\n <button mat-raised-button color=\"warn\" (click)=\"deleteListViews()\" class=\"delete-button\">\n <mat-icon>delete</mat-icon>\n Delete All List Views\n </button>\n </div>\n <snteam-list-view \n [modelName]=\"'ListView'\"\n [useRouter]=\"true\"\n [showDeleteAction]=\"true\">\n </snteam-list-view>\n } @else {\n <mat-card class=\"error-card\">\n <mat-card-content>\n <mat-icon class=\"error-icon\">error_outline</mat-icon>\n <h3>ListView Model Not Found</h3>\n <p>The ListView model is not available in your Amplify schema. Please add it to your data model to create custom list view configurations.</p>\n <button mat-raised-button color=\"primary\" class=\"help-button\">\n <mat-icon>help</mat-icon>\n View Documentation\n </button>\n </mat-card-content>\n </mat-card>\n }\n </div>\n </mat-expansion-panel>\n\n <!-- Field Configs Section -->\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title>\n <mat-icon>settings</mat-icon>\n <span class=\"panel-title\">Field Configs</span>\n </mat-panel-title>\n <mat-panel-description>\n Configure field behavior, validation, and choices\n </mat-panel-description>\n </mat-expansion-panel-header>\n \n <div class=\"panel-content\">\n @if (hasFieldConfig) {\n <div class=\"section-actions\">\n <button mat-raised-button color=\"accent\" (click)=\"populateDefaultFieldConfigs()\" class=\"populate-button\">\n <mat-icon>auto_fix_high</mat-icon>\n Populate Default Field Configs\n </button>\n <button mat-raised-button color=\"warn\" (click)=\"deleteFieldConfigs()\" class=\"delete-button\">\n <mat-icon>delete</mat-icon>\n Delete All Field Configs\n </button>\n </div>\n <snteam-list-view \n [modelName]=\"'FieldConfig'\"\n [useRouter]=\"true\"\n [showDeleteAction]=\"true\">\n </snteam-list-view>\n } @else {\n <mat-card class=\"error-card\">\n <mat-card-content>\n <mat-icon class=\"error-icon\">error_outline</mat-icon>\n <h3>FieldConfig Model Not Found</h3>\n <p>The FieldConfig model is not available in your Amplify schema. Please add it to your data model to configure field behavior and validation.</p>\n <button mat-raised-button color=\"primary\" class=\"help-button\">\n <mat-icon>help</mat-icon>\n View Documentation\n </button>\n </mat-card-content>\n </mat-card>\n }\n </div>\n </mat-expansion-panel>\n\n </mat-accordion>\n</div>", styles: [".configurations-container{padding:20px;max-width:1200px;margin:0 auto}.configurations-container h2{margin-bottom:24px;color:#333;font-weight:500}.mat-expansion-panel{margin-bottom:16px;border-radius:8px;box-shadow:0 2px 4px #0000001a}.mat-expansion-panel-header{padding:16px 24px}.panel-title{margin-left:12px;font-weight:500;font-size:16px}.mat-panel-description{color:#666;font-size:14px}.panel-content{padding:16px 24px 24px;background-color:#fafafa}.error-card{text-align:center;padding:24px;background-color:#fff;border:2px dashed #ddd;border-radius:8px}.error-card mat-card-content{padding:0}.error-icon{font-size:48px;width:48px;height:48px;color:#ff9800;margin-bottom:16px}.error-card h3{margin:0 0 12px;color:#333;font-weight:500}.error-card p{color:#666;line-height:1.5;max-width:500px;margin:0 auto 20px}.help-button{margin-top:8px}.help-button mat-icon{margin-right:8px}.section-actions{margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid #e0e0e0;display:flex;gap:12px;flex-wrap:wrap}.generate-button{background-color:#2196f3;color:#fff}.generate-button mat-icon{margin-right:8px}.delete-button{background-color:#f44336;color:#fff}.delete-button mat-icon{margin-right:8px}.populate-button mat-icon{margin-right:8px}@media (max-width: 768px){.configurations-container{padding:16px}.panel-content{padding:12px 16px 16px}.error-card{padding:16px}.error-icon{font-size:36px;width:36px;height:36px}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "directive", type: i1$1.MatAccordion, selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i1$1.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i1$1.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i1$1.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "directive", type: i1$1.MatExpansionPanelDescription, selector: "mat-panel-description" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i4$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3$4.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3$4.MatCardContent, selector: "mat-card-content" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatSnackBarModule }, { kind: "component", type: ListViewComponent, selector: "snteam-list-view", inputs: ["modelName", "customItemTemplate", "hideNewButton", "title", "useRouter", "showRowActions", "showDeleteAction", "customRowActions"], outputs: ["itemClick", "newClick", "itemsLoaded", "itemDeleted"] }] });
|
|
5375
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: ConfigurationsComponent, isStandalone: true, selector: "snteam-configurations", inputs: { amplifyOutputs: "amplifyOutputs" }, ngImport: i0, template: "<div class=\"configurations-container\">\n <h2>System Configurations</h2>\n \n <mat-accordion multi=\"true\">\n \n <!-- Table Configs Section -->\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title>\n <mat-icon>table_chart</mat-icon>\n <span class=\"panel-title\">Table Configs</span>\n </mat-panel-title>\n <mat-panel-description>\n Manage table configurations and settings\n </mat-panel-description>\n </mat-expansion-panel-header>\n \n <div class=\"panel-content\">\n @if (hasTableConfig) {\n <div class=\"section-actions\">\n <button mat-raised-button color=\"accent\" (click)=\"populateDefaultTableConfigs()\" class=\"populate-button\">\n <mat-icon>auto_fix_high</mat-icon>\n Populate Default Table Configs\n </button>\n <button mat-raised-button color=\"warn\" (click)=\"deleteTableConfigs()\" class=\"delete-button\">\n <mat-icon>delete</mat-icon>\n Delete All Table Configs\n </button>\n </div>\n <snteam-list-view \n [modelName]=\"'TableConfig'\"\n [useRouter]=\"true\"\n [showDeleteAction]=\"true\">\n </snteam-list-view>\n } @else {\n <mat-card class=\"error-card\">\n <mat-card-content>\n <mat-icon class=\"error-icon\">error_outline</mat-icon>\n <h3>TableConfig Model Not Found</h3>\n <p>The TableConfig model is not available in your Amplify schema. Please add it to your data model to manage table configurations.</p>\n <button mat-raised-button color=\"primary\" class=\"help-button\">\n <mat-icon>help</mat-icon>\n View Documentation\n </button>\n </mat-card-content>\n </mat-card>\n }\n </div>\n </mat-expansion-panel>\n\n <!-- Form Views Section -->\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title>\n <mat-icon>dynamic_form</mat-icon>\n <span class=\"panel-title\">Form Views</span>\n </mat-panel-title>\n <mat-panel-description>\n Configure custom form layouts and field selections\n </mat-panel-description>\n </mat-expansion-panel-header>\n \n <div class=\"panel-content\">\n @if (hasFormView) {\n <div class=\"section-actions\">\n <button mat-raised-button color=\"warn\" (click)=\"deleteFormViews()\" class=\"delete-button\">\n <mat-icon>delete</mat-icon>\n Delete All Form Views\n </button>\n </div>\n <snteam-list-view \n [modelName]=\"'FormView'\"\n [useRouter]=\"true\"\n [showDeleteAction]=\"true\">\n </snteam-list-view>\n } @else {\n <mat-card class=\"error-card\">\n <mat-card-content>\n <mat-icon class=\"error-icon\">error_outline</mat-icon>\n <h3>FormView Model Not Found</h3>\n <p>The FormView model is not available in your Amplify schema. Please add it to your data model to create custom form configurations.</p>\n <button mat-raised-button color=\"primary\" class=\"help-button\">\n <mat-icon>help</mat-icon>\n View Documentation\n </button>\n </mat-card-content>\n </mat-card>\n }\n </div>\n </mat-expansion-panel>\n\n <!-- List Views Section -->\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title>\n <mat-icon>view_list</mat-icon>\n <span class=\"panel-title\">List Views</span>\n </mat-panel-title>\n <mat-panel-description>\n Customize list displays and column configurations\n </mat-panel-description>\n </mat-expansion-panel-header>\n \n <div class=\"panel-content\">\n @if (hasListView) {\n <div class=\"section-actions\">\n <button mat-raised-button color=\"warn\" (click)=\"deleteListViews()\" class=\"delete-button\">\n <mat-icon>delete</mat-icon>\n Delete All List Views\n </button>\n </div>\n <snteam-list-view \n [modelName]=\"'ListView'\"\n [useRouter]=\"true\"\n [showDeleteAction]=\"true\">\n </snteam-list-view>\n } @else {\n <mat-card class=\"error-card\">\n <mat-card-content>\n <mat-icon class=\"error-icon\">error_outline</mat-icon>\n <h3>ListView Model Not Found</h3>\n <p>The ListView model is not available in your Amplify schema. Please add it to your data model to create custom list view configurations.</p>\n <button mat-raised-button color=\"primary\" class=\"help-button\">\n <mat-icon>help</mat-icon>\n View Documentation\n </button>\n </mat-card-content>\n </mat-card>\n }\n </div>\n </mat-expansion-panel>\n\n <!-- Field Configs Section -->\n <mat-expansion-panel>\n <mat-expansion-panel-header>\n <mat-panel-title>\n <mat-icon>settings</mat-icon>\n <span class=\"panel-title\">Field Configs</span>\n </mat-panel-title>\n <mat-panel-description>\n Configure field behavior, validation, and choices\n </mat-panel-description>\n </mat-expansion-panel-header>\n \n <div class=\"panel-content\">\n @if (hasFieldConfig) {\n <div class=\"section-actions\">\n <button mat-raised-button color=\"accent\" (click)=\"populateDefaultFieldConfigs()\" class=\"populate-button\">\n <mat-icon>auto_fix_high</mat-icon>\n Populate Default Field Configs\n </button>\n <button mat-raised-button color=\"warn\" (click)=\"deleteFieldConfigs()\" class=\"delete-button\">\n <mat-icon>delete</mat-icon>\n Delete All Field Configs\n </button>\n </div>\n <snteam-list-view \n [modelName]=\"'FieldConfig'\"\n [useRouter]=\"true\"\n [showDeleteAction]=\"true\">\n </snteam-list-view>\n } @else {\n <mat-card class=\"error-card\">\n <mat-card-content>\n <mat-icon class=\"error-icon\">error_outline</mat-icon>\n <h3>FieldConfig Model Not Found</h3>\n <p>The FieldConfig model is not available in your Amplify schema. Please add it to your data model to configure field behavior and validation.</p>\n <button mat-raised-button color=\"primary\" class=\"help-button\">\n <mat-icon>help</mat-icon>\n View Documentation\n </button>\n </mat-card-content>\n </mat-card>\n }\n </div>\n </mat-expansion-panel>\n\n </mat-accordion>\n</div>", styles: [".configurations-container{padding:20px;max-width:1200px;margin:0 auto}.configurations-container h2{margin-bottom:24px;color:#333;font-weight:500}.mat-expansion-panel{margin-bottom:16px;border-radius:8px;box-shadow:0 2px 4px #0000001a}.mat-expansion-panel-header{padding:16px 24px}.panel-title{margin-left:12px;font-weight:500;font-size:16px}.mat-panel-description{color:#666;font-size:14px}.panel-content{padding:16px 24px 24px;background-color:#fafafa}.error-card{text-align:center;padding:24px;background-color:#fff;border:2px dashed #ddd;border-radius:8px}.error-card mat-card-content{padding:0}.error-icon{font-size:48px;width:48px;height:48px;color:#ff9800;margin-bottom:16px}.error-card h3{margin:0 0 12px;color:#333;font-weight:500}.error-card p{color:#666;line-height:1.5;max-width:500px;margin:0 auto 20px}.help-button{margin-top:8px}.help-button mat-icon{margin-right:8px}.section-actions{margin-bottom:16px;padding-bottom:16px;border-bottom:1px solid #e0e0e0;display:flex;gap:12px;flex-wrap:wrap}.generate-button{background-color:#2196f3;color:#fff}.generate-button mat-icon{margin-right:8px}.delete-button{background-color:#f44336;color:#fff}.delete-button mat-icon{margin-right:8px}.populate-button mat-icon{margin-right:8px}@media (max-width: 768px){.configurations-container{padding:16px}.panel-content{padding:12px 16px 16px}.error-card{padding:16px}.error-icon{font-size:36px;width:36px;height:36px}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: MatExpansionModule }, { kind: "directive", type: i1$1.MatAccordion, selector: "mat-accordion", inputs: ["hideToggle", "displayMode", "togglePosition"], exportAs: ["matAccordion"] }, { kind: "component", type: i1$1.MatExpansionPanel, selector: "mat-expansion-panel", inputs: ["hideToggle", "togglePosition"], outputs: ["afterExpand", "afterCollapse"], exportAs: ["matExpansionPanel"] }, { kind: "component", type: i1$1.MatExpansionPanelHeader, selector: "mat-expansion-panel-header", inputs: ["expandedHeight", "collapsedHeight", "tabIndex"] }, { kind: "directive", type: i1$1.MatExpansionPanelTitle, selector: "mat-panel-title" }, { kind: "directive", type: i1$1.MatExpansionPanelDescription, selector: "mat-panel-description" }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i5$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatCardModule }, { kind: "component", type: i3$5.MatCard, selector: "mat-card", inputs: ["appearance"], exportAs: ["matCard"] }, { kind: "directive", type: i3$5.MatCardContent, selector: "mat-card-content" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i3$3.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatSnackBarModule }, { kind: "component", type: ListViewComponent, selector: "snteam-list-view", inputs: ["modelName", "customItemTemplate", "hideNewButton", "title", "useRouter", "showRowActions", "showDeleteAction", "customRowActions"], outputs: ["itemClick", "newClick", "itemsLoaded", "itemDeleted"] }] });
|
|
2532
5376
|
}
|
|
2533
5377
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: ConfigurationsComponent, decorators: [{
|
|
2534
5378
|
type: Component,
|
|
@@ -2554,5 +5398,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
2554
5398
|
* Generated bundle index. Do not edit.
|
|
2555
5399
|
*/
|
|
2556
5400
|
|
|
2557
|
-
export { AddRelationshipDialogComponent, AmplifyAngularCore, AmplifyFormBuilderService, AmplifyModelService, AsyncDropdownQuestion, ConfigurationsComponent, DatePickerQuestion, DateTimePickerQuestion, DropdownQuestion, DynamicFormComponent, DynamicFormGroupComponent, DynamicFormQuestionComponent, DynamicNestedFormQuestionComponent, DynamicRelationshipBuilderComponent, EmailQuestion, FormGroupQuestion, ListViewComponent, MyErrorStateMatcher, NumberQuestion, PhoneQuestion, QuestionBase, QuestionControlService, RecordRelationshipsComponent, SliderQuestion, TextboxQuestion, TimePickerQuestion, ValToTitlePipe, phoneNumberValidator };
|
|
5401
|
+
export { AddRelationshipDialogComponent, AmplifyAngularCore, AmplifyFormBuilderService, AmplifyModelService, AsyncDropdownQuestion, ConfigurationAnalyzerService, ConfigurationsComponent, DatePickerQuestion, DateTimePickerQuestion, DropdownQuestion, DynamicFormComponent, DynamicFormGroupComponent, DynamicFormQuestionComponent, DynamicNestedFormQuestionComponent, DynamicRelationshipBuilderComponent, EmailQuestion, FieldClassification, FormGroupQuestion, ListViewComponent, MyErrorStateMatcher, NumberQuestion, PhoneQuestion, QuestionBase, QuestionControlService, RecordRelationshipsComponent, SchemaIntrospectorService, SelectionSetErrorType, SelectionSetGeneratorService, SelectionSetPattern, SliderQuestion, TextboxQuestion, TimePickerQuestion, ValToTitlePipe, phoneNumberValidator };
|
|
2558
5402
|
//# sourceMappingURL=snteam-amplify-angular-core.mjs.map
|