nuxt-openapi-hyperfetch 0.1.7-alpha.1 → 0.2.7-alpha.1

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 (97) hide show
  1. package/CONTRIBUTING.md +291 -292
  2. package/INSTRUCTIONS.md +327 -327
  3. package/LICENSE +202 -202
  4. package/README.md +231 -227
  5. package/dist/cli/logger.d.ts +26 -0
  6. package/dist/cli/logger.js +36 -0
  7. package/dist/cli/logo.js +5 -5
  8. package/dist/generators/components/connector-generator/generator.d.ts +12 -0
  9. package/dist/generators/components/connector-generator/generator.js +116 -0
  10. package/dist/generators/components/connector-generator/templates.d.ts +18 -0
  11. package/dist/generators/components/connector-generator/templates.js +222 -0
  12. package/dist/generators/components/connector-generator/types.d.ts +32 -0
  13. package/dist/generators/components/connector-generator/types.js +7 -0
  14. package/dist/generators/components/schema-analyzer/index.d.ts +17 -0
  15. package/dist/generators/components/schema-analyzer/index.js +20 -0
  16. package/dist/generators/components/schema-analyzer/intent-detector.d.ts +17 -0
  17. package/dist/generators/components/schema-analyzer/intent-detector.js +143 -0
  18. package/dist/generators/components/schema-analyzer/openapi-reader.d.ts +11 -0
  19. package/dist/generators/components/schema-analyzer/openapi-reader.js +76 -0
  20. package/dist/generators/components/schema-analyzer/resource-grouper.d.ts +6 -0
  21. package/dist/generators/components/schema-analyzer/resource-grouper.js +132 -0
  22. package/dist/generators/components/schema-analyzer/schema-field-mapper.d.ts +35 -0
  23. package/dist/generators/components/schema-analyzer/schema-field-mapper.js +220 -0
  24. package/dist/generators/components/schema-analyzer/types.d.ts +156 -0
  25. package/dist/generators/components/schema-analyzer/types.js +7 -0
  26. package/dist/generators/nuxt-server/generator.d.ts +2 -1
  27. package/dist/generators/nuxt-server/generator.js +21 -21
  28. package/dist/generators/shared/runtime/apiHelpers.d.ts +81 -41
  29. package/dist/generators/shared/runtime/apiHelpers.js +97 -104
  30. package/dist/generators/shared/runtime/pagination.d.ts +168 -0
  31. package/dist/generators/shared/runtime/pagination.js +179 -0
  32. package/dist/generators/shared/runtime/useDeleteConnector.d.ts +16 -0
  33. package/dist/generators/shared/runtime/useDeleteConnector.js +93 -0
  34. package/dist/generators/shared/runtime/useDetailConnector.d.ts +14 -0
  35. package/dist/generators/shared/runtime/useDetailConnector.js +50 -0
  36. package/dist/generators/shared/runtime/useFormConnector.d.ts +19 -0
  37. package/dist/generators/shared/runtime/useFormConnector.js +113 -0
  38. package/dist/generators/shared/runtime/useListConnector.d.ts +25 -0
  39. package/dist/generators/shared/runtime/useListConnector.js +125 -0
  40. package/dist/generators/shared/runtime/zod-error-merger.d.ts +23 -0
  41. package/dist/generators/shared/runtime/zod-error-merger.js +106 -0
  42. package/dist/generators/shared/templates/api-callbacks-plugin.js +54 -11
  43. package/dist/generators/shared/templates/api-pagination-plugin.d.ts +51 -0
  44. package/dist/generators/shared/templates/api-pagination-plugin.js +152 -0
  45. package/dist/generators/use-async-data/generator.d.ts +2 -1
  46. package/dist/generators/use-async-data/generator.js +14 -14
  47. package/dist/generators/use-async-data/runtime/useApiAsyncData.js +114 -13
  48. package/dist/generators/use-async-data/runtime/useApiAsyncDataRaw.js +88 -10
  49. package/dist/generators/use-async-data/templates.js +17 -17
  50. package/dist/generators/use-fetch/generator.d.ts +2 -1
  51. package/dist/generators/use-fetch/generator.js +12 -12
  52. package/dist/generators/use-fetch/runtime/useApiRequest.js +149 -40
  53. package/dist/generators/use-fetch/templates.js +14 -14
  54. package/dist/index.js +25 -0
  55. package/dist/module/index.d.ts +4 -0
  56. package/dist/module/index.js +93 -0
  57. package/dist/module/types.d.ts +27 -0
  58. package/dist/module/types.js +1 -0
  59. package/docs/API-REFERENCE.md +886 -887
  60. package/docs/generated-components.md +615 -0
  61. package/docs/headless-composables-ui.md +569 -0
  62. package/eslint.config.js +13 -0
  63. package/package.json +29 -2
  64. package/src/cli/config.ts +140 -140
  65. package/src/cli/logger.ts +124 -66
  66. package/src/cli/logo.ts +25 -25
  67. package/src/cli/types.ts +50 -50
  68. package/src/generators/components/connector-generator/generator.ts +138 -0
  69. package/src/generators/components/connector-generator/templates.ts +254 -0
  70. package/src/generators/components/connector-generator/types.ts +34 -0
  71. package/src/generators/components/schema-analyzer/index.ts +44 -0
  72. package/src/generators/components/schema-analyzer/intent-detector.ts +187 -0
  73. package/src/generators/components/schema-analyzer/openapi-reader.ts +96 -0
  74. package/src/generators/components/schema-analyzer/resource-grouper.ts +166 -0
  75. package/src/generators/components/schema-analyzer/schema-field-mapper.ts +268 -0
  76. package/src/generators/components/schema-analyzer/types.ts +177 -0
  77. package/src/generators/nuxt-server/generator.ts +272 -270
  78. package/src/generators/shared/runtime/apiHelpers.ts +535 -507
  79. package/src/generators/shared/runtime/pagination.ts +323 -0
  80. package/src/generators/shared/runtime/useDeleteConnector.ts +109 -0
  81. package/src/generators/shared/runtime/useDetailConnector.ts +64 -0
  82. package/src/generators/shared/runtime/useFormConnector.ts +139 -0
  83. package/src/generators/shared/runtime/useListConnector.ts +148 -0
  84. package/src/generators/shared/runtime/zod-error-merger.ts +119 -0
  85. package/src/generators/shared/templates/api-callbacks-plugin.ts +399 -352
  86. package/src/generators/shared/templates/api-pagination-plugin.ts +158 -0
  87. package/src/generators/use-async-data/generator.ts +205 -204
  88. package/src/generators/use-async-data/runtime/useApiAsyncData.ts +329 -229
  89. package/src/generators/use-async-data/runtime/useApiAsyncDataRaw.ts +324 -245
  90. package/src/generators/use-async-data/templates.ts +257 -257
  91. package/src/generators/use-fetch/generator.ts +170 -169
  92. package/src/generators/use-fetch/runtime/useApiRequest.ts +354 -234
  93. package/src/generators/use-fetch/templates.ts +214 -214
  94. package/src/index.ts +303 -265
  95. package/src/module/index.ts +133 -0
  96. package/src/module/types.ts +31 -0
  97. package/src/generators/tanstack-query/generator.ts +0 -11
@@ -5,18 +5,18 @@ import { getApiFiles as getApiFilesOfficial, parseApiFile as parseApiFileOfficia
5
5
  import { getApiFiles as getApiFilesHeyApi, parseApiFile as parseApiFileHeyApi, } from '../shared/parsers/heyapi-parser.js';
6
6
  import { generateServerRouteFile, generateRouteFilePath, generateRoutesIndexFile, } from './templates.js';
7
7
  import { generateAuthContextStub, generateAuthTypesStub, generateTransformerStub, generateTransformerExamples, generateBffReadme, } from './bff-templates.js';
8
- import { p, logSuccess, logError, logNote } from '../../cli/logger.js';
8
+ import { createClackLogger } from '../../cli/logger.js';
9
9
  /**
10
10
  * Main function to generate Nuxt Server Routes
11
11
  */
12
- export async function generateNuxtServerRoutes(inputDir, serverRoutePath, options) {
13
- const mainSpinner = p.spinner();
12
+ export async function generateNuxtServerRoutes(inputDir, serverRoutePath, options, logger = createClackLogger()) {
13
+ const mainSpinner = logger.spinner();
14
14
  // Select parser based on chosen backend
15
15
  const getApiFiles = options?.backend === 'heyapi' ? getApiFilesHeyApi : getApiFilesOfficial;
16
16
  const parseApiFile = options?.backend === 'heyapi' ? parseApiFileHeyApi : parseApiFileOfficial;
17
17
  const enableBff = options?.enableBff ?? false;
18
18
  if (enableBff) {
19
- p.log.info('BFF Mode: Enabled (transformers + auth)');
19
+ logger.log.info('BFF Mode: Enabled (transformers + auth)');
20
20
  }
21
21
  // 1. Get all API files
22
22
  mainSpinner.start('Scanning API files');
@@ -35,12 +35,12 @@ export async function generateNuxtServerRoutes(inputDir, serverRoutePath, option
35
35
  allMethods.push(...apiInfo.methods);
36
36
  }
37
37
  catch (error) {
38
- logError(`Error parsing ${fileName}: ${String(error)}`);
38
+ logger.log.error(`Error parsing ${fileName}: ${String(error)}`);
39
39
  }
40
40
  }
41
41
  mainSpinner.stop(`Found ${allMethods.length} routes to generate`);
42
42
  if (allMethods.length === 0) {
43
- p.log.warn('No methods found to generate');
43
+ logger.log.warn('No methods found to generate');
44
44
  return;
45
45
  }
46
46
  // 3. Clean and create output directory
@@ -49,7 +49,7 @@ export async function generateNuxtServerRoutes(inputDir, serverRoutePath, option
49
49
  mainSpinner.stop('Output directory ready');
50
50
  // 4. Generate BFF structure if enabled
51
51
  if (enableBff) {
52
- await generateBffStructure(allMethods, serverRoutePath, inputDir);
52
+ await generateBffStructure(allMethods, serverRoutePath, inputDir, logger);
53
53
  }
54
54
  // 5. Calculate relative import path from server routes to APIs
55
55
  const relativePath = calculateRelativeImportPath(serverRoutePath, inputDir);
@@ -65,7 +65,7 @@ export async function generateNuxtServerRoutes(inputDir, serverRoutePath, option
65
65
  enableBff: enableBff,
66
66
  resource: resource,
67
67
  });
68
- const formattedCode = await formatCode(code);
68
+ const formattedCode = await formatCode(code, logger);
69
69
  const routeFilePath = generateRouteFilePath(method);
70
70
  const fullPath = path.join(serverRoutePath, routeFilePath);
71
71
  // Ensure directory exists
@@ -74,7 +74,7 @@ export async function generateNuxtServerRoutes(inputDir, serverRoutePath, option
74
74
  successCount++;
75
75
  }
76
76
  catch (error) {
77
- logError(`Error generating ${method.path} [${method.httpMethod}]: ${String(error)}`);
77
+ logger.log.error(`Error generating ${method.path} [${method.httpMethod}]: ${String(error)}`);
78
78
  errorCount++;
79
79
  }
80
80
  }
@@ -83,14 +83,14 @@ export async function generateNuxtServerRoutes(inputDir, serverRoutePath, option
83
83
  mainSpinner.start('Generating configuration files');
84
84
  // Generate routes index (documentation)
85
85
  const routesIndexCode = generateRoutesIndexFile(allMethods);
86
- const formattedRoutesIndex = await formatCode(routesIndexCode);
86
+ const formattedRoutesIndex = await formatCode(routesIndexCode, logger);
87
87
  await fs.writeFile(path.join(serverRoutePath, '_routes.ts'), formattedRoutesIndex, 'utf-8');
88
88
  mainSpinner.stop('Configuration files generated');
89
89
  // 8. Summary and Next Steps
90
90
  if (errorCount > 0) {
91
- p.log.warn(`Completed with ${errorCount} error(s)`);
91
+ logger.log.warn(`Completed with ${errorCount} error(s)`);
92
92
  }
93
- logSuccess(`Generated ${successCount} server route(s) in ${serverRoutePath}`);
93
+ logger.log.success(`Generated ${successCount} server route(s) in ${serverRoutePath}`);
94
94
  // Build next steps message
95
95
  let nextSteps = '1. Configure API_BASE_URL and API_SECRET in your .env\n';
96
96
  nextSteps += '2. Update nuxt.config.ts with runtimeConfig (add apiBaseUrl and apiSecret)';
@@ -103,7 +103,7 @@ export async function generateNuxtServerRoutes(inputDir, serverRoutePath, option
103
103
  else {
104
104
  nextSteps += '\n3. Start your Nuxt dev server and test the routes';
105
105
  }
106
- logNote(nextSteps, 'Next steps');
106
+ logger.note(nextSteps, 'Next steps');
107
107
  }
108
108
  /**
109
109
  * Calculate relative import path from server routes to APIs
@@ -118,7 +118,7 @@ function calculateRelativeImportPath(serverRoutePath, inputDir) {
118
118
  /**
119
119
  * Format code with Prettier
120
120
  */
121
- async function formatCode(code) {
121
+ async function formatCode(code, logger) {
122
122
  try {
123
123
  return await format(code, {
124
124
  parser: 'typescript',
@@ -129,15 +129,15 @@ async function formatCode(code) {
129
129
  });
130
130
  }
131
131
  catch {
132
- p.log.warn('Prettier formatting failed, using unformatted code');
132
+ logger.log.warn('Prettier formatting failed, using unformatted code');
133
133
  return code;
134
134
  }
135
135
  }
136
136
  /**
137
137
  * Generate BFF structure (auth + transformers)
138
138
  */
139
- async function generateBffStructure(allMethods, serverRoutePath, inputDir) {
140
- const bffSpinner = p.spinner();
139
+ async function generateBffStructure(allMethods, serverRoutePath, inputDir, logger) {
140
+ const bffSpinner = logger.spinner();
141
141
  bffSpinner.start('Generating BFF structure (auth + transformers)');
142
142
  const serverRoot = path.dirname(serverRoutePath);
143
143
  // 1. Generate auth files (only if they don't exist)
@@ -146,13 +146,13 @@ async function generateBffStructure(allMethods, serverRoutePath, inputDir) {
146
146
  const authContextPath = path.join(authDir, 'context.ts');
147
147
  if (!fs.existsSync(authContextPath)) {
148
148
  const authContextCode = generateAuthContextStub();
149
- const formattedAuthContext = await formatCode(authContextCode);
149
+ const formattedAuthContext = await formatCode(authContextCode, logger);
150
150
  await fs.writeFile(authContextPath, formattedAuthContext, 'utf-8');
151
151
  }
152
152
  const authTypesPath = path.join(authDir, 'types.ts');
153
153
  if (!fs.existsSync(authTypesPath)) {
154
154
  const authTypesCode = generateAuthTypesStub();
155
- const formattedAuthTypes = await formatCode(authTypesCode);
155
+ const formattedAuthTypes = await formatCode(authTypesCode, logger);
156
156
  await fs.writeFile(authTypesPath, formattedAuthTypes, 'utf-8');
157
157
  }
158
158
  // 2. Generate transformer stubs (only if they don't exist)
@@ -173,14 +173,14 @@ async function generateBffStructure(allMethods, serverRoutePath, inputDir) {
173
173
  const transformerPath = path.join(transformersDir, `${resource}.ts`);
174
174
  if (!fs.existsSync(transformerPath)) {
175
175
  const transformerCode = generateTransformerStub(resource, methods, inputDir);
176
- const formattedTransformer = await formatCode(transformerCode);
176
+ const formattedTransformer = await formatCode(transformerCode, logger);
177
177
  await fs.writeFile(transformerPath, formattedTransformer, 'utf-8');
178
178
  }
179
179
  }
180
180
  // 3. Generate examples file (always regenerated)
181
181
  const examplesPath = path.join(bffDir, '_transformers.example.ts');
182
182
  const examplesCode = generateTransformerExamples();
183
- const formattedExamples = await formatCode(examplesCode);
183
+ const formattedExamples = await formatCode(examplesCode, logger);
184
184
  await fs.writeFile(examplesPath, formattedExamples, 'utf-8');
185
185
  // 4. Generate BFF README (always regenerated)
186
186
  const bffReadmePath = path.join(bffDir, 'README.md');
@@ -40,39 +40,62 @@ export interface FinishContext<T> {
40
40
  success: boolean;
41
41
  }
42
42
  /**
43
- * Global callbacks configuration
44
- * Can be provided via Nuxt plugin: $getGlobalApiCallbacks
43
+ * A single rule in the global callbacks configuration.
44
+ * Each rule independently targets specific URL patterns and/or HTTP methods.
45
+ * Rules are executed in order; any rule may return false to suppress the local callback.
45
46
  */
46
- export interface GlobalCallbacksConfig {
47
+ export interface GlobalCallbacksRule {
47
48
  /**
48
- * Optional URL patterns to match (Opción 3)
49
- * Only apply global callbacks to URLs matching these patterns
49
+ * URL glob patterns — only apply this rule to matching URLs.
50
50
  * Supports wildcards: '/api/**', '/api/public/*', etc.
51
- * If omitted, callbacks apply to all requests
51
+ * If omitted, the rule applies to all URLs.
52
52
  */
53
53
  patterns?: string[];
54
54
  /**
55
- * Called before every request matching patterns
56
- * Return false to prevent local callback execution (Opción 2)
57
- * Return modified context to change request
55
+ * HTTP methods only apply this rule to matching methods (case-insensitive).
56
+ * Example: ['DELETE', 'POST']
57
+ * If omitted, the rule applies to all methods.
58
+ */
59
+ methods?: string[];
60
+ /**
61
+ * Called before the request is sent.
62
+ * Return modified context (headers/body/query) to alter the request.
63
+ * Return false to prevent local onRequest execution (Opción 2).
58
64
  */
59
65
  onRequest?: (context: RequestContext) => void | Promise<void> | ModifiedRequestContext | Promise<ModifiedRequestContext> | boolean | Promise<boolean>;
60
66
  /**
61
- * Called when request succeeds
62
- * Return false to prevent local callback execution (Opción 2)
67
+ * Called when the request succeeds.
68
+ * Return false to prevent local onSuccess execution (Opción 2).
63
69
  */
64
70
  onSuccess?: (data: any, context?: any) => void | Promise<void> | boolean | Promise<boolean>;
65
71
  /**
66
- * Called when request fails
67
- * Return false to prevent local callback execution (Opción 2)
72
+ * Called when the request fails.
73
+ * Return false to prevent local onError execution (Opción 2).
68
74
  */
69
75
  onError?: (error: any, context?: any) => void | Promise<void> | boolean | Promise<boolean>;
70
76
  /**
71
- * Called when request finishes (success or error)
72
- * Return false to prevent local callback execution (Opción 2)
77
+ * Called when the request finishes (success or error).
78
+ * Return false to prevent local onFinish execution (Opción 2).
73
79
  */
74
80
  onFinish?: (context: FinishContext<any>) => void | Promise<void> | boolean | Promise<boolean>;
75
81
  }
82
+ /**
83
+ * Global callbacks configuration.
84
+ * Accepts a single rule (backward-compatible) or an array of rules.
85
+ * Each rule can independently target URLs and HTTP methods.
86
+ * Provided via Nuxt plugin: $getGlobalApiCallbacks
87
+ *
88
+ * @example Single rule (backward-compatible)
89
+ * getGlobalApiCallbacks: () => ({ onError: (e) => console.error(e) })
90
+ *
91
+ * @example Multiple rules with method/pattern targeting
92
+ * getGlobalApiCallbacks: () => [
93
+ * { onRequest: (ctx) => console.log(ctx.url) },
94
+ * { methods: ['DELETE'], onSuccess: () => toast.success('Deleted!') },
95
+ * { patterns: ['/api/private/**'], onRequest: () => ({ headers: { Authorization: '...' } }) },
96
+ * ]
97
+ */
98
+ export type GlobalCallbacksConfig = GlobalCallbacksRule | GlobalCallbacksRule[];
76
99
  /**
77
100
  * Type for skipGlobalCallbacks option (Opción 1)
78
101
  * - true: skip all global callbacks
@@ -124,6 +147,28 @@ export interface ApiRequestOptions<T = any> {
124
147
  * Useful for manual cache control or sharing cache between components.
125
148
  */
126
149
  cacheKey?: string;
150
+ /**
151
+ * Enable pagination for this request.
152
+ * When true, the composable injects page/perPage params and exposes `pagination` state + helpers.
153
+ * Uses global pagination config by default (set via plugins/api-pagination.ts).
154
+ * @example
155
+ * const { data, pagination, goToPage, nextPage, prevPage, setPerPage } = useGetPets(params, { paginated: true })
156
+ */
157
+ paginated?: boolean;
158
+ /**
159
+ * Initial page number. Defaults to global config default (usually 1).
160
+ */
161
+ initialPage?: number;
162
+ /**
163
+ * Initial page size. Defaults to global config default (usually 20).
164
+ */
165
+ initialPerPage?: number;
166
+ /**
167
+ * Per-request pagination config override.
168
+ * Takes priority over the global pagination config set in plugins/api-pagination.ts.
169
+ * Useful when one specific endpoint has a different pagination convention.
170
+ */
171
+ paginationConfig?: import('./pagination.js').PaginationConfig;
127
172
  /** Base URL prepended to every request URL. Overrides runtimeConfig.public.apiBaseUrl. */
128
173
  baseURL?: string;
129
174
  /** HTTP method (GET, POST, PUT, PATCH, DELETE, etc.) */
@@ -154,52 +199,47 @@ export declare function applyPick<T>(data: T, paths: ReadonlyArray<string>): any
154
199
  */
155
200
  export declare function getGlobalHeaders(): Record<string, string>;
156
201
  /**
157
- * Helper function to get global callbacks from user configuration
202
+ * Helper function to get global callback rules from user configuration.
203
+ * Always returns a normalized array — wraps legacy single-object config automatically for
204
+ * full backward compatibility.
158
205
  * Uses Nuxt plugin provide: plugins/api-callbacks.ts with $getGlobalApiCallbacks
159
206
  */
160
- export declare function getGlobalCallbacks(): GlobalCallbacksConfig;
207
+ export declare function getGlobalCallbacks(): GlobalCallbacksRule[];
161
208
  /**
162
209
  * Helper function to get the global base URL from runtimeConfig.public.apiBaseUrl
163
210
  * Returns the configured URL or undefined if not set or not in a Nuxt context.
164
211
  */
165
212
  export declare function getGlobalBaseUrl(): string | undefined;
166
213
  /**
167
- * Check if a global callback should be applied to a specific request
168
- * Implements Opción 1 (skipGlobalCallbacks) and Opción 3 (pattern matching)
214
+ * Check if a global rule should be applied to a specific request.
215
+ * Implements Opción 1 (skipGlobalCallbacks), URL pattern matching, and HTTP method matching.
169
216
  */
170
- export declare function shouldApplyGlobalCallback(url: string, callbackName: 'onRequest' | 'onSuccess' | 'onError' | 'onFinish', patterns?: string[], skipConfig?: SkipGlobalCallbacks): boolean;
217
+ export declare function shouldApplyGlobalCallback(url: string, method: string, callbackName: 'onRequest' | 'onSuccess' | 'onError' | 'onFinish', rule: GlobalCallbacksRule, skipConfig?: SkipGlobalCallbacks): boolean;
171
218
  /**
172
- * Merge local and global callbacks with proper execution order
219
+ * Merge local and global callback rules with proper execution order.
220
+ * Global rules are iterated in definition order. Any rule returning false suppresses the local callback.
173
221
  * Implements all 3 options:
174
- * - Opción 1: skipGlobalCallbacks to disable global callbacks
175
- * - Opción 2: global callbacks can return false to prevent local execution
176
- * - Opción 3: pattern matching to apply callbacks only to matching URLs
222
+ * - Opción 1: skipGlobalCallbacks to disable all global rules per request
223
+ * - Opción 2: a rule callback can return false to prevent local callback execution
224
+ * - Opción 3: per-rule URL pattern matching and HTTP method filtering
177
225
  */
178
- export declare function mergeCallbacks(url: string, localCallbacks: {
226
+ export declare function mergeCallbacks(url: string, method: string, localCallbacks: {
179
227
  onRequest?: Function;
180
228
  onSuccess?: Function;
181
229
  onError?: Function;
182
230
  onFinish?: Function;
183
231
  }, skipConfig?: SkipGlobalCallbacks): {
184
232
  /**
185
- * Merged onRequest callback
186
- * Executes global first, then local
187
- * Global can return modifications or false to cancel local
188
- */
189
- onRequest: (ctx: RequestContext) => Promise<any>;
190
- /**
191
- * Merged onSuccess callback
192
- * Executes global first, then local (if global doesn't return false)
233
+ * Merged onRequest: runs all applicable global rules collecting and deep-merging
234
+ * modifications (headers and query are merged; body is last-write-wins).
235
+ * Local onRequest runs after all rules unless any returns false, and its
236
+ * modifications take highest priority.
193
237
  */
238
+ onRequest: (ctx: RequestContext) => Promise<ModifiedRequestContext | undefined>;
239
+ /** Merged onSuccess: global rules first in order, then local (unless suppressed). */
194
240
  onSuccess: (data: any, context?: any) => Promise<void>;
195
- /**
196
- * Merged onError callback
197
- * Executes global first, then local (if global doesn't return false)
198
- */
241
+ /** Merged onError: global rules first in order, then local (unless suppressed). */
199
242
  onError: (error: any, context?: any) => Promise<void>;
200
- /**
201
- * Merged onFinish callback
202
- * Executes global first, then local (if global doesn't return false)
203
- */
243
+ /** Merged onFinish: global rules first in order, then local (unless suppressed). */
204
244
  onFinish: (context: any) => Promise<void>;
205
245
  };
@@ -101,7 +101,9 @@ export function getGlobalHeaders() {
101
101
  return headers;
102
102
  }
103
103
  /**
104
- * Helper function to get global callbacks from user configuration
104
+ * Helper function to get global callback rules from user configuration.
105
+ * Always returns a normalized array — wraps legacy single-object config automatically for
106
+ * full backward compatibility.
105
107
  * Uses Nuxt plugin provide: plugins/api-callbacks.ts with $getGlobalApiCallbacks
106
108
  */
107
109
  export function getGlobalCallbacks() {
@@ -110,16 +112,17 @@ export function getGlobalCallbacks() {
110
112
  // @ts-ignore - $getGlobalApiCallbacks may or may not exist (user-defined)
111
113
  if (nuxtApp.$getGlobalApiCallbacks) {
112
114
  // @ts-ignore
113
- const callbacks = nuxtApp.$getGlobalApiCallbacks();
114
- if (callbacks && typeof callbacks === 'object') {
115
- return callbacks;
115
+ const config = nuxtApp.$getGlobalApiCallbacks();
116
+ if (config && typeof config === 'object') {
117
+ // Normalize: wrap single-rule object in array for backward compatibility
118
+ return Array.isArray(config) ? config : [config];
116
119
  }
117
120
  }
118
121
  }
119
122
  catch (e) {
120
123
  // useNuxtApp not available or plugin not configured, that's OK
121
124
  }
122
- return {};
125
+ return [];
123
126
  }
124
127
  /**
125
128
  * Helper function to get the global base URL from runtimeConfig.public.apiBaseUrl
@@ -137,144 +140,134 @@ export function getGlobalBaseUrl() {
137
140
  }
138
141
  }
139
142
  /**
140
- * Check if a global callback should be applied to a specific request
141
- * Implements Opción 1 (skipGlobalCallbacks) and Opción 3 (pattern matching)
143
+ * Check if a global rule should be applied to a specific request.
144
+ * Implements Opción 1 (skipGlobalCallbacks), URL pattern matching, and HTTP method matching.
142
145
  */
143
- export function shouldApplyGlobalCallback(url, callbackName, patterns, skipConfig) {
146
+ export function shouldApplyGlobalCallback(url, method, callbackName, rule, skipConfig) {
144
147
  // Opción 1: Check if callback is skipped via skipGlobalCallbacks
145
- if (skipConfig === true) {
146
- return false; // Skip all global callbacks
147
- }
148
- if (Array.isArray(skipConfig) && skipConfig.includes(callbackName)) {
149
- return false; // Skip this specific callback
150
- }
151
- // Opción 3: Check pattern matching
152
- if (patterns && patterns.length > 0) {
153
- return patterns.some((pattern) => {
154
- // Convert glob pattern to regex
155
- // ** matches any characters including /
156
- // * matches any characters except /
148
+ if (skipConfig === true)
149
+ return false;
150
+ if (Array.isArray(skipConfig) && skipConfig.includes(callbackName))
151
+ return false;
152
+ // URL pattern matching if patterns defined, URL must match at least one
153
+ if (rule.patterns && rule.patterns.length > 0) {
154
+ const matchesUrl = rule.patterns.some((pattern) => {
155
+ // Convert glob pattern to regex: ** = any path, * = single segment
157
156
  const regexPattern = pattern
158
157
  .replace(/\*\*/g, '@@DOUBLE_STAR@@')
159
158
  .replace(/\*/g, '[^/]*')
160
159
  .replace(/@@DOUBLE_STAR@@/g, '.*');
161
- const regex = new RegExp('^' + regexPattern + '$');
162
- return regex.test(url);
160
+ return new RegExp('^' + regexPattern + '$').test(url);
163
161
  });
162
+ if (!matchesUrl)
163
+ return false;
164
+ }
165
+ // Method matching — if methods defined, request method must match at least one
166
+ if (rule.methods && rule.methods.length > 0) {
167
+ if (!rule.methods.map((m) => m.toUpperCase()).includes(method.toUpperCase()))
168
+ return false;
164
169
  }
165
- // By default, apply global callback
166
170
  return true;
167
171
  }
168
172
  /**
169
- * Merge local and global callbacks with proper execution order
173
+ * Merge local and global callback rules with proper execution order.
174
+ * Global rules are iterated in definition order. Any rule returning false suppresses the local callback.
170
175
  * Implements all 3 options:
171
- * - Opción 1: skipGlobalCallbacks to disable global callbacks
172
- * - Opción 2: global callbacks can return false to prevent local execution
173
- * - Opción 3: pattern matching to apply callbacks only to matching URLs
176
+ * - Opción 1: skipGlobalCallbacks to disable all global rules per request
177
+ * - Opción 2: a rule callback can return false to prevent local callback execution
178
+ * - Opción 3: per-rule URL pattern matching and HTTP method filtering
174
179
  */
175
- export function mergeCallbacks(url, localCallbacks, skipConfig) {
176
- const global = getGlobalCallbacks();
180
+ export function mergeCallbacks(url, method, localCallbacks, skipConfig) {
181
+ const rules = getGlobalCallbacks();
182
+ /**
183
+ * Iterate all applicable global rules for onSuccess, onError, or onFinish.
184
+ * Returns true if the local callback should still execute.
185
+ */
186
+ async function runGlobalRules(callbackName, ...args) {
187
+ let continueLocal = true;
188
+ for (const rule of rules) {
189
+ const cb = rule[callbackName];
190
+ if (!cb || !shouldApplyGlobalCallback(url, method, callbackName, rule, skipConfig))
191
+ continue;
192
+ try {
193
+ const result = await cb(...args);
194
+ // Opción 2: returning false from any rule suppresses the local callback
195
+ if (result === false)
196
+ continueLocal = false;
197
+ }
198
+ catch (error) {
199
+ console.error(`Error in global ${callbackName} callback:`, error);
200
+ }
201
+ }
202
+ return continueLocal;
203
+ }
177
204
  return {
178
205
  /**
179
- * Merged onRequest callback
180
- * Executes global first, then local
181
- * Global can return modifications or false to cancel local
206
+ * Merged onRequest: runs all applicable global rules collecting and deep-merging
207
+ * modifications (headers and query are merged; body is last-write-wins).
208
+ * Local onRequest runs after all rules unless any returns false, and its
209
+ * modifications take highest priority.
182
210
  */
183
211
  onRequest: async (ctx) => {
184
- // Execute global onRequest
185
- if (shouldApplyGlobalCallback(url, 'onRequest', global.patterns, skipConfig) &&
186
- global.onRequest) {
212
+ let mergedMods;
213
+ let continueLocal = true;
214
+ for (const rule of rules) {
215
+ if (!rule.onRequest || !shouldApplyGlobalCallback(url, method, 'onRequest', rule, skipConfig))
216
+ continue;
187
217
  try {
188
- const result = await global.onRequest(ctx);
189
- // Opción 2: If global returns false, don't execute local
218
+ const result = await rule.onRequest(ctx);
190
219
  if (result === false) {
191
- return;
220
+ continueLocal = false;
192
221
  }
193
- // If global returns modified context, use it
194
- if (result && typeof result === 'object' && !('then' in result)) {
195
- return result;
222
+ else if (result && typeof result === 'object') {
223
+ const mod = result;
224
+ // Deep-merge headers and query; body is last-write-wins
225
+ mergedMods = {
226
+ ...(mergedMods ?? {}),
227
+ ...mod,
228
+ headers: { ...(mergedMods?.headers ?? {}), ...(mod.headers ?? {}) },
229
+ query: { ...(mergedMods?.query ?? {}), ...(mod.query ?? {}) },
230
+ };
196
231
  }
197
232
  }
198
233
  catch (error) {
199
234
  console.error('Error in global onRequest callback:', error);
200
235
  }
201
236
  }
202
- // Execute local onRequest
203
- if (localCallbacks.onRequest) {
204
- return await localCallbacks.onRequest(ctx);
237
+ // Execute local onRequest — its modifications take highest priority
238
+ if (continueLocal && localCallbacks.onRequest) {
239
+ const localResult = await localCallbacks.onRequest(ctx);
240
+ if (localResult && typeof localResult === 'object') {
241
+ const localMod = localResult;
242
+ return mergedMods
243
+ ? {
244
+ ...mergedMods,
245
+ ...localMod,
246
+ headers: { ...(mergedMods.headers ?? {}), ...(localMod.headers ?? {}) },
247
+ query: { ...(mergedMods.query ?? {}), ...(localMod.query ?? {}) },
248
+ }
249
+ : localMod;
250
+ }
205
251
  }
252
+ return mergedMods;
206
253
  },
207
- /**
208
- * Merged onSuccess callback
209
- * Executes global first, then local (if global doesn't return false)
210
- */
254
+ /** Merged onSuccess: global rules first in order, then local (unless suppressed). */
211
255
  onSuccess: async (data, context) => {
212
- let continueLocal = true;
213
- // Execute global onSuccess
214
- if (shouldApplyGlobalCallback(url, 'onSuccess', global.patterns, skipConfig) &&
215
- global.onSuccess) {
216
- try {
217
- const result = await global.onSuccess(data, context);
218
- // Opción 2: If global returns false, don't execute local
219
- if (result === false) {
220
- continueLocal = false;
221
- }
222
- }
223
- catch (error) {
224
- console.error('Error in global onSuccess callback:', error);
225
- }
226
- }
227
- // Execute local onSuccess (if not cancelled)
256
+ const continueLocal = await runGlobalRules('onSuccess', data, context);
228
257
  if (continueLocal && localCallbacks.onSuccess) {
229
258
  await localCallbacks.onSuccess(data, context);
230
259
  }
231
260
  },
232
- /**
233
- * Merged onError callback
234
- * Executes global first, then local (if global doesn't return false)
235
- */
261
+ /** Merged onError: global rules first in order, then local (unless suppressed). */
236
262
  onError: async (error, context) => {
237
- let continueLocal = true;
238
- // Execute global onError
239
- if (shouldApplyGlobalCallback(url, 'onError', global.patterns, skipConfig) &&
240
- global.onError) {
241
- try {
242
- const result = await global.onError(error, context);
243
- // Opción 2: If global returns false, don't execute local
244
- if (result === false) {
245
- continueLocal = false;
246
- }
247
- }
248
- catch (error) {
249
- console.error('Error in global onError callback:', error);
250
- }
251
- }
252
- // Execute local onError (if not cancelled)
263
+ const continueLocal = await runGlobalRules('onError', error, context);
253
264
  if (continueLocal && localCallbacks.onError) {
254
265
  await localCallbacks.onError(error, context);
255
266
  }
256
267
  },
257
- /**
258
- * Merged onFinish callback
259
- * Executes global first, then local (if global doesn't return false)
260
- */
268
+ /** Merged onFinish: global rules first in order, then local (unless suppressed). */
261
269
  onFinish: async (context) => {
262
- let continueLocal = true;
263
- // Execute global onFinish
264
- if (shouldApplyGlobalCallback(url, 'onFinish', global.patterns, skipConfig) &&
265
- global.onFinish) {
266
- try {
267
- const result = await global.onFinish(context);
268
- // Opción 2: If global returns false, don't execute local
269
- if (result === false) {
270
- continueLocal = false;
271
- }
272
- }
273
- catch (error) {
274
- console.error('Error in global onFinish callback:', error);
275
- }
276
- }
277
- // Execute local onFinish (if not cancelled)
270
+ const continueLocal = await runGlobalRules('onFinish', context);
278
271
  if (continueLocal && localCallbacks.onFinish) {
279
272
  await localCallbacks.onFinish(context);
280
273
  }