@g1cloud/api-gen 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/.claude/settings.local.json +22 -0
  2. package/CLAUDE.md +63 -0
  3. package/README.md +379 -0
  4. package/dist/analyzer/controllerAnalyzer.d.ts +20 -0
  5. package/dist/analyzer/controllerAnalyzer.d.ts.map +1 -0
  6. package/dist/analyzer/controllerAnalyzer.js +101 -0
  7. package/dist/analyzer/controllerAnalyzer.js.map +1 -0
  8. package/dist/analyzer/parameterAnalyzer.d.ts +19 -0
  9. package/dist/analyzer/parameterAnalyzer.d.ts.map +1 -0
  10. package/dist/analyzer/parameterAnalyzer.js +207 -0
  11. package/dist/analyzer/parameterAnalyzer.js.map +1 -0
  12. package/dist/analyzer/responseAnalyzer.d.ts +12 -0
  13. package/dist/analyzer/responseAnalyzer.d.ts.map +1 -0
  14. package/dist/analyzer/responseAnalyzer.js +116 -0
  15. package/dist/analyzer/responseAnalyzer.js.map +1 -0
  16. package/dist/analyzer/schemaGenerator.d.ts +6 -0
  17. package/dist/analyzer/schemaGenerator.d.ts.map +1 -0
  18. package/dist/analyzer/schemaGenerator.js +347 -0
  19. package/dist/analyzer/schemaGenerator.js.map +1 -0
  20. package/dist/analyzer/securityAnalyzer.d.ts +6 -0
  21. package/dist/analyzer/securityAnalyzer.d.ts.map +1 -0
  22. package/dist/analyzer/securityAnalyzer.js +177 -0
  23. package/dist/analyzer/securityAnalyzer.js.map +1 -0
  24. package/dist/generator/openapiGenerator.d.ts +14 -0
  25. package/dist/generator/openapiGenerator.d.ts.map +1 -0
  26. package/dist/generator/openapiGenerator.js +340 -0
  27. package/dist/generator/openapiGenerator.js.map +1 -0
  28. package/dist/index.d.ts +3 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/dist/index.js +218 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/lib.d.ts +61 -0
  33. package/dist/lib.d.ts.map +1 -0
  34. package/dist/lib.js +199 -0
  35. package/dist/lib.js.map +1 -0
  36. package/dist/mcp-server.d.ts +9 -0
  37. package/dist/mcp-server.d.ts.map +1 -0
  38. package/dist/mcp-server.js +257 -0
  39. package/dist/mcp-server.js.map +1 -0
  40. package/dist/mcp-server.mjs +45586 -0
  41. package/dist/parser/astAnalyzer.d.ts +87 -0
  42. package/dist/parser/astAnalyzer.d.ts.map +1 -0
  43. package/dist/parser/astAnalyzer.js +321 -0
  44. package/dist/parser/astAnalyzer.js.map +1 -0
  45. package/dist/parser/javaParser.d.ts +10 -0
  46. package/dist/parser/javaParser.d.ts.map +1 -0
  47. package/dist/parser/javaParser.js +805 -0
  48. package/dist/parser/javaParser.js.map +1 -0
  49. package/dist/types/index.d.ts +217 -0
  50. package/dist/types/index.d.ts.map +1 -0
  51. package/dist/types/index.js +3 -0
  52. package/dist/types/index.js.map +1 -0
  53. package/examples/CreateUserRequest.java +80 -0
  54. package/examples/DepartmentDTO.java +45 -0
  55. package/examples/Filter.java +39 -0
  56. package/examples/PaginatedList.java +71 -0
  57. package/examples/ProductController.java +136 -0
  58. package/examples/ProductDTO.java +129 -0
  59. package/examples/RoleDTO.java +47 -0
  60. package/examples/SearchParam.java +55 -0
  61. package/examples/Sort.java +70 -0
  62. package/examples/UpdateUserRequest.java +74 -0
  63. package/examples/UserController.java +98 -0
  64. package/examples/UserDTO.java +119 -0
  65. package/package.json +51 -0
  66. package/prompt/01_Initial.md +358 -0
  67. package/prompt/02_/354/266/224/352/260/200.md +31 -0
  68. package/src/analyzer/controllerAnalyzer.ts +125 -0
  69. package/src/analyzer/parameterAnalyzer.ts +259 -0
  70. package/src/analyzer/responseAnalyzer.ts +142 -0
  71. package/src/analyzer/schemaGenerator.ts +412 -0
  72. package/src/analyzer/securityAnalyzer.ts +200 -0
  73. package/src/generator/openapiGenerator.ts +378 -0
  74. package/src/index.ts +212 -0
  75. package/src/lib.ts +240 -0
  76. package/src/mcp-server.ts +310 -0
  77. package/src/parser/astAnalyzer.ts +373 -0
  78. package/src/parser/javaParser.ts +901 -0
  79. package/src/types/index.ts +238 -0
  80. package/test-boolean.yaml +607 -0
  81. package/test-filter.yaml +576 -0
  82. package/test-inner.ts +59 -0
  83. package/test-output.yaml +650 -0
  84. package/test-paginated.yaml +585 -0
  85. package/tsconfig.json +20 -0
  86. package/tsup.config.ts +30 -0
@@ -0,0 +1,412 @@
1
+ import {
2
+ JavaClass,
3
+ JavaField,
4
+ JavaAnnotation,
5
+ SchemaInfo,
6
+ ProcessingContext,
7
+ } from '../types';
8
+ import { ASTAnalyzer } from '../parser/astAnalyzer';
9
+
10
+ /**
11
+ * Generate OpenAPI schemas for all referenced types
12
+ */
13
+ export function generateSchemas(context: ProcessingContext): Map<string, SchemaInfo> {
14
+ const schemas = new Map<string, SchemaInfo>(context.dtoSchemas);
15
+
16
+ // Add standard schemas for SearchParam, Filter, Sort
17
+ addStandardSchemas(schemas);
18
+
19
+ // Process all referenced types
20
+ const processedTypes = new Set<string>();
21
+ const typesToProcess = [...context.referencedTypes];
22
+
23
+ while (typesToProcess.length > 0) {
24
+ const typeName = typesToProcess.pop()!;
25
+
26
+ if (processedTypes.has(typeName)) {
27
+ continue;
28
+ }
29
+
30
+ processedTypes.add(typeName);
31
+
32
+ // Skip primitive types
33
+ if (ASTAnalyzer.isPrimitiveOrWrapper(typeName)) {
34
+ continue;
35
+ }
36
+
37
+ // Skip if already in schemas
38
+ if (schemas.has(typeName)) {
39
+ continue;
40
+ }
41
+
42
+ // Find the Java class for this type
43
+ const javaClass = context.javaClasses.get(typeName);
44
+
45
+ if (javaClass) {
46
+ // Handle enum types
47
+ if (javaClass.isEnum && javaClass.enumValues) {
48
+ schemas.set(typeName, {
49
+ type: 'string',
50
+ enum: javaClass.enumValues,
51
+ description: javaClass.javadoc,
52
+ });
53
+ } else {
54
+ const { schema, nestedTypes } = generateSchemaForClass(javaClass, context);
55
+ schemas.set(typeName, schema);
56
+
57
+ // Add nested types to process
58
+ for (const nestedType of nestedTypes) {
59
+ if (!processedTypes.has(nestedType)) {
60
+ typesToProcess.push(nestedType);
61
+ }
62
+ }
63
+ }
64
+ } else {
65
+ // Type not found, create a generic object schema
66
+ schemas.set(typeName, {
67
+ type: 'object',
68
+ description: `Schema for ${typeName}`,
69
+ });
70
+ }
71
+ }
72
+
73
+ return schemas;
74
+ }
75
+
76
+ /**
77
+ * Add standard schemas for SearchParam, Filter, Sort
78
+ */
79
+ function addStandardSchemas(schemas: Map<string, SchemaInfo>): void {
80
+ // Filter schema
81
+ if (!schemas.has('Filter')) {
82
+ schemas.set('Filter', {
83
+ type: 'object',
84
+ description: 'Filter conditions',
85
+ });
86
+ }
87
+
88
+ // Sort schema
89
+ if (!schemas.has('Sort')) {
90
+ schemas.set('Sort', {
91
+ type: 'object',
92
+ description: 'Sort conditions',
93
+ });
94
+ }
95
+
96
+ // SearchParam schema (for reference)
97
+ if (!schemas.has('SearchParam')) {
98
+ schemas.set('SearchParam', {
99
+ type: 'object',
100
+ description: 'Search parameters for pagination and filtering',
101
+ properties: {
102
+ offset: { type: 'integer', description: 'Pagination offset' },
103
+ limit: { type: 'integer', description: 'Pagination limit' },
104
+ filter: { $ref: '#/components/schemas/Filter' },
105
+ sort: { $ref: '#/components/schemas/Sort' },
106
+ },
107
+ });
108
+ }
109
+
110
+ // MultiLangString schema - Map<String, String>
111
+ if (!schemas.has('MultiLangString')) {
112
+ schemas.set('MultiLangString', {
113
+ type: 'object',
114
+ description: 'Multi-language string (Map<String, String>)',
115
+ additionalProperties: { type: 'string' },
116
+ });
117
+ }
118
+
119
+ // StoredFile schema
120
+ if (!schemas.has('StoredFile')) {
121
+ schemas.set('StoredFile', {
122
+ type: 'object',
123
+ description: 'Stored file information',
124
+ properties: {
125
+ fileUrl: { type: 'string', description: 'File URL' },
126
+ mediaType: { type: 'MediaType', enum: ['Image', 'Vide', 'Youtube', 'Unknown'], description: 'Media type' },
127
+ thumbnailUrl: { type: 'string', description: 'Thumbnail URL' },
128
+ fileName: { type: 'string', description: 'Original file name' },
129
+ altText: { type: 'string', description: 'Alternative text for the file' },
130
+ width: { type: 'integer', description: 'Width of the media' },
131
+ height: { type: 'integer', description: 'Height of the media' },
132
+ },
133
+ });
134
+ }
135
+
136
+ // MultiLangStoredFile schema - Map<String, StoredFile>
137
+ if (!schemas.has('MultiLangStoredFile')) {
138
+ schemas.set('MultiLangStoredFile', {
139
+ type: 'object',
140
+ description: 'Multi-language stored file (Map<String, StoredFile>)',
141
+ additionalProperties: { $ref: '#/components/schemas/StoredFile' },
142
+ });
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Generate schema for a Java class, including inherited fields from superclass
148
+ */
149
+ function generateSchemaForClass(
150
+ javaClass: JavaClass,
151
+ context: ProcessingContext
152
+ ): { schema: SchemaInfo; nestedTypes: string[] } {
153
+ const properties: Record<string, SchemaInfo> = {};
154
+ const required: string[] = [];
155
+ const nestedTypes: string[] = [];
156
+
157
+ // Collect all fields including inherited ones
158
+ const allFields = collectAllFields(javaClass, context);
159
+
160
+ for (const field of allFields) {
161
+ const { propertySchema, isRequired, referencedType } = generatePropertySchema(field);
162
+
163
+ properties[field.name] = propertySchema;
164
+
165
+ if (isRequired) {
166
+ required.push(field.name);
167
+ }
168
+
169
+ if (referencedType) {
170
+ nestedTypes.push(referencedType);
171
+ }
172
+ }
173
+
174
+ const schema: SchemaInfo = {
175
+ type: 'object',
176
+ properties,
177
+ };
178
+
179
+ if (required.length > 0) {
180
+ schema.required = required;
181
+ }
182
+
183
+ return { schema, nestedTypes };
184
+ }
185
+
186
+ /**
187
+ * Collect all fields from a class and its superclasses
188
+ */
189
+ function collectAllFields(javaClass: JavaClass, context: ProcessingContext): JavaField[] {
190
+ const fields: JavaField[] = [];
191
+ const visitedClasses = new Set<string>();
192
+
193
+ let currentClass: JavaClass | undefined = javaClass;
194
+
195
+ while (currentClass && !visitedClasses.has(currentClass.name)) {
196
+ visitedClasses.add(currentClass.name);
197
+
198
+ // Add fields from current class (prepend to maintain inheritance order: parent fields first)
199
+ fields.unshift(...currentClass.fields);
200
+
201
+ // Move to superclass
202
+ if (currentClass.superClass) {
203
+ currentClass = context.javaClasses.get(currentClass.superClass);
204
+ } else {
205
+ break;
206
+ }
207
+ }
208
+
209
+ return fields;
210
+ }
211
+
212
+ /**
213
+ * Generate schema for a field
214
+ */
215
+ function generatePropertySchema(field: JavaField): {
216
+ propertySchema: SchemaInfo;
217
+ isRequired: boolean;
218
+ referencedType?: string;
219
+ } {
220
+ let isRequired = false;
221
+ let referencedType: string | undefined;
222
+
223
+ // Check for validation annotations
224
+ const constraints = extractValidationConstraints(field.annotations);
225
+ isRequired = constraints.required;
226
+
227
+ // Generate base schema based on type
228
+ let propertySchema: SchemaInfo;
229
+
230
+ if (ASTAnalyzer.isPrimitiveOrWrapper(field.type)) {
231
+ propertySchema = ASTAnalyzer.javaTypeToOpenAPI(field.type);
232
+ } else if (field.type.endsWith('[]')) {
233
+ const elementType = field.type.slice(0, -2);
234
+ if (ASTAnalyzer.isPrimitiveOrWrapper(elementType)) {
235
+ propertySchema = {
236
+ type: 'array',
237
+ items: ASTAnalyzer.javaTypeToOpenAPI(elementType),
238
+ };
239
+ } else {
240
+ referencedType = elementType;
241
+ propertySchema = {
242
+ type: 'array',
243
+ items: { $ref: `#/components/schemas/${elementType}` },
244
+ };
245
+ }
246
+ } else if (ASTAnalyzer.isCollectionType(field.type)) {
247
+ const itemType = field.genericType ? extractMainType(field.genericType) : 'Object';
248
+ if (ASTAnalyzer.isPrimitiveOrWrapper(itemType)) {
249
+ propertySchema = {
250
+ type: 'array',
251
+ items: ASTAnalyzer.javaTypeToOpenAPI(itemType),
252
+ };
253
+ } else {
254
+ referencedType = itemType;
255
+ propertySchema = {
256
+ type: 'array',
257
+ items: { $ref: `#/components/schemas/${itemType}` },
258
+ };
259
+ }
260
+ } else if (ASTAnalyzer.isMapType(field.type)) {
261
+ propertySchema = {
262
+ type: 'object',
263
+ additionalProperties: true,
264
+ };
265
+ } else if (isEnumType(field)) {
266
+ propertySchema = {
267
+ type: 'string',
268
+ enum: constraints.enumValues,
269
+ };
270
+ } else {
271
+ // Reference type
272
+ referencedType = field.type.split('.').pop() || field.type;
273
+ propertySchema = { $ref: `#/components/schemas/${referencedType}` };
274
+ }
275
+
276
+ // Apply constraints to schema
277
+ if (constraints.minLength !== undefined && propertySchema.type === 'string') {
278
+ propertySchema.minLength = constraints.minLength;
279
+ }
280
+ if (constraints.maxLength !== undefined && propertySchema.type === 'string') {
281
+ propertySchema.maxLength = constraints.maxLength;
282
+ }
283
+ if (constraints.minimum !== undefined && (propertySchema.type === 'integer' || propertySchema.type === 'number')) {
284
+ propertySchema.minimum = constraints.minimum;
285
+ }
286
+ if (constraints.maximum !== undefined && (propertySchema.type === 'integer' || propertySchema.type === 'number')) {
287
+ propertySchema.maximum = constraints.maximum;
288
+ }
289
+ if (constraints.pattern) {
290
+ propertySchema.pattern = constraints.pattern;
291
+ }
292
+
293
+ // Apply description: annotation description takes priority, then javadoc
294
+ if (constraints.description) {
295
+ propertySchema.description = constraints.description;
296
+ } else if (field.javadoc) {
297
+ propertySchema.description = field.javadoc;
298
+ }
299
+
300
+ return { propertySchema, isRequired, referencedType };
301
+ }
302
+
303
+ interface ValidationConstraints {
304
+ required: boolean;
305
+ minLength?: number;
306
+ maxLength?: number;
307
+ minimum?: number;
308
+ maximum?: number;
309
+ pattern?: string;
310
+ description?: string;
311
+ enumValues?: string[];
312
+ }
313
+
314
+ /**
315
+ * Extract validation constraints from field annotations
316
+ */
317
+ function extractValidationConstraints(annotations: JavaAnnotation[]): ValidationConstraints {
318
+ const constraints: ValidationConstraints = { required: false };
319
+
320
+ for (const annotation of annotations) {
321
+ switch (annotation.name) {
322
+ case 'NotNull':
323
+ case 'NotEmpty':
324
+ case 'NotBlank':
325
+ constraints.required = true;
326
+ break;
327
+
328
+ case 'Size':
329
+ if (annotation.values['min']) {
330
+ constraints.minLength = parseInt(String(annotation.values['min']), 10);
331
+ }
332
+ if (annotation.values['max']) {
333
+ constraints.maxLength = parseInt(String(annotation.values['max']), 10);
334
+ }
335
+ break;
336
+
337
+ case 'Min':
338
+ if (annotation.values['value']) {
339
+ constraints.minimum = parseInt(String(annotation.values['value']), 10);
340
+ }
341
+ break;
342
+
343
+ case 'Max':
344
+ if (annotation.values['value']) {
345
+ constraints.maximum = parseInt(String(annotation.values['value']), 10);
346
+ }
347
+ break;
348
+
349
+ case 'Pattern':
350
+ if (annotation.values['regexp']) {
351
+ constraints.pattern = String(annotation.values['regexp']);
352
+ }
353
+ break;
354
+
355
+ case 'Email':
356
+ constraints.pattern = '^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$';
357
+ break;
358
+
359
+ case 'Length':
360
+ if (annotation.values['min']) {
361
+ constraints.minLength = parseInt(String(annotation.values['min']), 10);
362
+ }
363
+ if (annotation.values['max']) {
364
+ constraints.maxLength = parseInt(String(annotation.values['max']), 10);
365
+ }
366
+ break;
367
+
368
+ case 'Range':
369
+ if (annotation.values['min']) {
370
+ constraints.minimum = parseInt(String(annotation.values['min']), 10);
371
+ }
372
+ if (annotation.values['max']) {
373
+ constraints.maximum = parseInt(String(annotation.values['max']), 10);
374
+ }
375
+ break;
376
+
377
+ case 'ApiModelProperty':
378
+ case 'Schema':
379
+ if (annotation.values['description']) {
380
+ constraints.description = String(annotation.values['description']);
381
+ }
382
+ if (annotation.values['required'] === 'true' || annotation.values['required'] === true) {
383
+ constraints.required = true;
384
+ }
385
+ break;
386
+ }
387
+ }
388
+
389
+ return constraints;
390
+ }
391
+
392
+ /**
393
+ * Check if field is an enum type
394
+ */
395
+ function isEnumType(field: JavaField): boolean {
396
+ // This is a simplified check - in practice, you'd need to analyze the type
397
+ const enumAnnotations = field.annotations.filter(
398
+ (a) => a.name === 'Enumerated' || (a.name === 'Schema' && a.values['allowableValues'])
399
+ );
400
+ return enumAnnotations.length > 0;
401
+ }
402
+
403
+ /**
404
+ * Extract main type from generic type string
405
+ */
406
+ function extractMainType(genericType: string): string {
407
+ const match = genericType.match(/^([^<]+)/);
408
+ if (match) {
409
+ return match[1].trim();
410
+ }
411
+ return genericType;
412
+ }
@@ -0,0 +1,200 @@
1
+ import { JavaMethod, JavaAnnotation, SecurityInfo } from '../types';
2
+ import { ASTAnalyzer } from '../parser/astAnalyzer';
3
+
4
+ /**
5
+ * Analyze method security annotations and extract roles/authorities
6
+ */
7
+ export function analyzeMethodSecurity(method: JavaMethod): SecurityInfo {
8
+ const securityInfo: SecurityInfo = {
9
+ roles: [],
10
+ authorities: [],
11
+ hasComplexExpression: false,
12
+ };
13
+
14
+ const securityAnnotations = ASTAnalyzer.getSecurityAnnotations(method);
15
+
16
+ for (const annotation of securityAnnotations) {
17
+ switch (annotation.name) {
18
+ case 'PreAuthorize':
19
+ case 'PostAuthorize':
20
+ analyzeSpELExpression(annotation, securityInfo);
21
+ break;
22
+ case 'Secured':
23
+ analyzeSecuredAnnotation(annotation, securityInfo);
24
+ break;
25
+ case 'RolesAllowed':
26
+ analyzeRolesAllowedAnnotation(annotation, securityInfo);
27
+ break;
28
+ }
29
+ }
30
+
31
+ // Remove duplicates
32
+ securityInfo.roles = [...new Set(securityInfo.roles)];
33
+ securityInfo.authorities = [...new Set(securityInfo.authorities)];
34
+
35
+ return securityInfo;
36
+ }
37
+
38
+ /**
39
+ * Analyze @PreAuthorize / @PostAuthorize SpEL expression
40
+ */
41
+ function analyzeSpELExpression(annotation: JavaAnnotation, securityInfo: SecurityInfo): void {
42
+ const expression = getAnnotationValue(annotation);
43
+ if (!expression) return;
44
+
45
+ // Extract hasRole() calls
46
+ const hasRoleMatches = expression.matchAll(/hasRole\s*\(\s*'([^']+)'\s*\)/g);
47
+ for (const match of hasRoleMatches) {
48
+ const role = normalizeRole(match[1]);
49
+ securityInfo.roles.push(role);
50
+ }
51
+
52
+ // Extract hasAnyRole() calls
53
+ const hasAnyRoleMatches = expression.matchAll(/hasAnyRole\s*\(\s*([^)]+)\s*\)/g);
54
+ for (const match of hasAnyRoleMatches) {
55
+ const rolesString = match[1];
56
+ const roles = extractRolesFromString(rolesString);
57
+ securityInfo.roles.push(...roles.map(normalizeRole));
58
+ }
59
+
60
+ // Extract hasAuthority() calls
61
+ const hasAuthorityMatches = expression.matchAll(/hasAuthority\s*\(\s*'([^']+)'\s*\)/g);
62
+ for (const match of hasAuthorityMatches) {
63
+ securityInfo.authorities.push(match[1]);
64
+ }
65
+
66
+ // Extract hasAnyAuthority() calls
67
+ const hasAnyAuthorityMatches = expression.matchAll(/hasAnyAuthority\s*\(\s*([^)]+)\s*\)/g);
68
+ for (const match of hasAnyAuthorityMatches) {
69
+ const authoritiesString = match[1];
70
+ const authorities = extractRolesFromString(authoritiesString);
71
+ securityInfo.authorities.push(...authorities);
72
+ }
73
+
74
+ // Check if the expression is complex (has more than just role/authority checks)
75
+ const isComplex = isComplexSpELExpression(expression);
76
+ if (isComplex) {
77
+ securityInfo.hasComplexExpression = true;
78
+ securityInfo.securityExpression = expression;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Analyze @Secured annotation
84
+ */
85
+ function analyzeSecuredAnnotation(annotation: JavaAnnotation, securityInfo: SecurityInfo): void {
86
+ const value = annotation.values['value'];
87
+ if (!value) return;
88
+
89
+ const roles = Array.isArray(value) ? value : [value];
90
+ for (const role of roles) {
91
+ if (typeof role === 'string') {
92
+ securityInfo.roles.push(normalizeRole(role));
93
+ }
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Analyze @RolesAllowed annotation
99
+ */
100
+ function analyzeRolesAllowedAnnotation(annotation: JavaAnnotation, securityInfo: SecurityInfo): void {
101
+ const value = annotation.values['value'];
102
+ if (!value) return;
103
+
104
+ const roles = Array.isArray(value) ? value : [value];
105
+ for (const role of roles) {
106
+ if (typeof role === 'string') {
107
+ securityInfo.roles.push(normalizeRole(role));
108
+ }
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Get annotation value
114
+ */
115
+ function getAnnotationValue(annotation: JavaAnnotation): string | undefined {
116
+ const value = annotation.values['value'];
117
+ if (value === undefined) return undefined;
118
+
119
+ if (Array.isArray(value)) {
120
+ return value[0];
121
+ }
122
+
123
+ return typeof value === 'string' ? value : String(value);
124
+ }
125
+
126
+ /**
127
+ * Normalize role name to include ROLE_ prefix if not present
128
+ */
129
+ function normalizeRole(role: string): string {
130
+ const trimmedRole = role.trim().replace(/^['"]|['"]$/g, '');
131
+
132
+ if (trimmedRole.startsWith('ROLE_')) {
133
+ return trimmedRole;
134
+ }
135
+
136
+ return `ROLE_${trimmedRole}`;
137
+ }
138
+
139
+ /**
140
+ * Extract roles from a comma-separated string with quotes
141
+ * e.g., "'USER', 'ADMIN'" -> ["USER", "ADMIN"]
142
+ */
143
+ function extractRolesFromString(rolesString: string): string[] {
144
+ const roles: string[] = [];
145
+ const matches = rolesString.matchAll(/'([^']+)'/g);
146
+
147
+ for (const match of matches) {
148
+ roles.push(match[1]);
149
+ }
150
+
151
+ return roles;
152
+ }
153
+
154
+ /**
155
+ * Check if a SpEL expression is complex
156
+ * (contains more than just hasRole/hasAnyRole/hasAuthority/hasAnyAuthority)
157
+ */
158
+ function isComplexSpELExpression(expression: string): boolean {
159
+ // Remove all simple role/authority checks
160
+ let simplified = expression
161
+ .replace(/hasRole\s*\(\s*'[^']+'\s*\)/g, '')
162
+ .replace(/hasAnyRole\s*\(\s*[^)]+\s*\)/g, '')
163
+ .replace(/hasAuthority\s*\(\s*'[^']+'\s*\)/g, '')
164
+ .replace(/hasAnyAuthority\s*\(\s*[^)]+\s*\)/g, '')
165
+ .replace(/\s+/g, ' ')
166
+ .trim();
167
+
168
+ // Remove boolean operators that might remain
169
+ simplified = simplified
170
+ .replace(/^\s*and\s*/gi, '')
171
+ .replace(/\s*and\s*$/gi, '')
172
+ .replace(/^\s*or\s*/gi, '')
173
+ .replace(/\s*or\s*$/gi, '')
174
+ .replace(/^\s*&&\s*/g, '')
175
+ .replace(/\s*&&\s*$/g, '')
176
+ .replace(/^\s*\|\|\s*/g, '')
177
+ .replace(/\s*\|\|\s*$/g, '')
178
+ .replace(/^\s*\(\s*\)\s*$/g, '')
179
+ .trim();
180
+
181
+ // If anything remains (other than parentheses and whitespace), it's complex
182
+ const hasOtherContent = simplified.replace(/[()]/g, '').trim().length > 0;
183
+
184
+ // Also check for specific complex patterns
185
+ const complexPatterns = [
186
+ /#\w+/, // Variable references like #user, #id
187
+ /\.\w+/, // Property access
188
+ /==|!=|<|>|<=|>=/, // Comparison operators
189
+ /isAuthenticated\(\)/, // Authentication checks
190
+ /isAnonymous\(\)/, // Anonymous checks
191
+ /permitAll\(\)/, // Permit all
192
+ /denyAll\(\)/, // Deny all
193
+ /authentication\./i, // Authentication object access
194
+ /principal\./i, // Principal object access
195
+ ];
196
+
197
+ const hasComplexPattern = complexPatterns.some((pattern) => pattern.test(expression));
198
+
199
+ return hasOtherContent || hasComplexPattern;
200
+ }