@pikku/openapi-parser 0.12.6 → 0.12.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @pikku/openapi-parser
2
2
 
3
+ ## 0.12.8
4
+
5
+ ### Patch Changes
6
+
7
+ - 94ceecd: Fix duplicate property error in generated code when body and path/query/header params share the same name. Skips the body property with a warning.
8
+
9
+ ## 0.12.7
10
+
11
+ ### Patch Changes
12
+
13
+ - 5dd1996: Fix credentials command crash when state.credentials is undefined, and add --credential flag to `pikku new addon` for per-user credential wiring (apikey, bearer, oauth2).
14
+
3
15
  ## 0.12.6
4
16
 
5
17
  ### Patch Changes
package/dist/codegen.d.ts CHANGED
@@ -14,6 +14,7 @@ interface AddonVars {
14
14
  interface CodegenFlags {
15
15
  oauth: boolean;
16
16
  secret: boolean;
17
+ credential?: 'apikey' | 'bearer' | 'oauth2';
17
18
  mcp?: boolean;
18
19
  }
19
20
  export declare function generateAddonFromOpenAPI(spec: ParsedSpec, vars: AddonVars, flags: CodegenFlags): Record<string, string>;
package/dist/codegen.js CHANGED
@@ -179,11 +179,21 @@ function buildInputSchema(parsed, ctx) {
179
179
  }
180
180
  if (parsed.requestBody) {
181
181
  if (parsed.requestBody.properties) {
182
+ const paramNames = new Set([
183
+ ...parsed.pathParams.map((p) => p.name),
184
+ ...parsed.queryParams.map((p) => p.name),
185
+ ...parsed.headerParams.map((p) => p.name),
186
+ ]);
182
187
  const requiredSet = new Set(parsed.requestBody.required ?? []);
183
188
  for (const [key, propSchema] of Object.entries(parsed.requestBody.properties)) {
184
189
  // Skip readOnly properties from input
185
190
  if (propSchema.readOnly)
186
191
  continue;
192
+ // Skip body properties that collide with path/query/header params
193
+ if (paramNames.has(key)) {
194
+ console.warn(`[openapi] Skipping body property '${key}' — collides with path/query/header param`);
195
+ continue;
196
+ }
187
197
  const isOptional = !requiredSet.has(key);
188
198
  const zodCode = schemaToZod(propSchema, ctx, { optional: isOptional });
189
199
  props.push(` ${safeKey(key)}: ${zodCode},`);
@@ -235,7 +245,10 @@ function generateServiceFile(spec, opPairs, vars, flags) {
235
245
  const lines = [];
236
246
  // Always import all error classes used in the switch statement
237
247
  const allErrorClasses = new Set(Object.values(STATUS_TO_ERROR));
238
- if (flags.oauth) {
248
+ if (flags.credential && flags.credential !== 'oauth2') {
249
+ // Per-user credential: no special imports needed, creds passed via constructor
250
+ }
251
+ else if (flags.oauth || flags.credential === 'oauth2') {
239
252
  lines.push("import { OAuth2Client } from '@pikku/core/oauth2'");
240
253
  lines.push("import type { TypedSecretService } from '#pikku/secrets/pikku-secrets.gen.js'");
241
254
  }
@@ -286,7 +299,14 @@ function generateServiceFile(spec, opPairs, vars, flags) {
286
299
  // Class declaration
287
300
  lines.push(`export class ${pascalName}Service {`);
288
301
  lines.push(' private baseUrl: string');
289
- if (flags.oauth) {
302
+ if (flags.credential && flags.credential !== 'oauth2') {
303
+ const credField = flags.credential === 'bearer' ? 'token' : 'apiKey';
304
+ lines.push('');
305
+ lines.push(` constructor(private creds: { ${credField}: string }, variables: TypedVariablesService) {`);
306
+ lines.push(` this.baseUrl = variables.get('${screamingName}_BASE_URL') as string`);
307
+ lines.push(' }');
308
+ }
309
+ else if (flags.oauth || flags.credential === 'oauth2') {
290
310
  lines.push(' private oauth: OAuth2Client');
291
311
  lines.push('');
292
312
  lines.push(` constructor(secrets: TypedSecretService, variables: TypedVariablesService) {`);
@@ -359,7 +379,29 @@ function generateServiceFile(spec, opPairs, vars, flags) {
359
379
  lines.push(' url.searchParams.set(key, value)');
360
380
  lines.push(' }');
361
381
  lines.push('');
362
- if (flags.oauth) {
382
+ if (flags.credential && flags.credential !== 'oauth2') {
383
+ // Per-user credential: use creds from wire.getCredentials()
384
+ if (flags.credential === 'bearer') {
385
+ lines.push(' headers.Authorization = `Bearer ${this.creds.token}`');
386
+ }
387
+ else {
388
+ // apikey: check spec for custom header name
389
+ const apiKeyScheme = Object.values(spec.securitySchemes).find((s) => s.type === 'apiKey');
390
+ if (apiKeyScheme?.name && apiKeyScheme?.in === 'header') {
391
+ lines.push(` headers[${JSON.stringify(apiKeyScheme.name)}] = this.creds.apiKey`);
392
+ }
393
+ else {
394
+ lines.push(' headers.Authorization = `Bearer ${this.creds.apiKey}`');
395
+ }
396
+ }
397
+ lines.push('');
398
+ lines.push(' const response = await fetch(url.toString(), {');
399
+ lines.push(' method,');
400
+ lines.push(' headers,');
401
+ lines.push(' body: body ? JSON.stringify(body) : undefined,');
402
+ lines.push(' })');
403
+ }
404
+ else if (flags.oauth || flags.credential === 'oauth2') {
363
405
  lines.push(' const response = await this.oauth.request(url.toString(), {');
364
406
  lines.push(' method,');
365
407
  lines.push(' headers,');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/openapi-parser",
3
- "version": "0.12.6",
3
+ "version": "0.12.8",
4
4
  "description": "OpenAPI spec parser and code generator for Pikku",
5
5
  "author": "yasser.fadl@gmail.com",
6
6
  "license": "MIT",
package/src/codegen.ts CHANGED
@@ -32,6 +32,7 @@ interface AddonVars {
32
32
  interface CodegenFlags {
33
33
  oauth: boolean
34
34
  secret: boolean
35
+ credential?: 'apikey' | 'bearer' | 'oauth2'
35
36
  mcp?: boolean
36
37
  }
37
38
 
@@ -296,12 +297,24 @@ function buildInputSchema(
296
297
 
297
298
  if (parsed.requestBody) {
298
299
  if (parsed.requestBody.properties) {
300
+ const paramNames = new Set([
301
+ ...parsed.pathParams.map((p) => p.name),
302
+ ...parsed.queryParams.map((p) => p.name),
303
+ ...parsed.headerParams.map((p) => p.name),
304
+ ])
299
305
  const requiredSet = new Set(parsed.requestBody.required ?? [])
300
306
  for (const [key, propSchema] of Object.entries(
301
307
  parsed.requestBody.properties
302
308
  )) {
303
309
  // Skip readOnly properties from input
304
310
  if (propSchema.readOnly) continue
311
+ // Skip body properties that collide with path/query/header params
312
+ if (paramNames.has(key)) {
313
+ console.warn(
314
+ `[openapi] Skipping body property '${key}' — collides with path/query/header param`
315
+ )
316
+ continue
317
+ }
305
318
  const isOptional = !requiredSet.has(key)
306
319
  const zodCode = schemaToZod(propSchema, ctx, { optional: isOptional })
307
320
  props.push(` ${safeKey(key)}: ${zodCode},`)
@@ -386,7 +399,9 @@ function generateServiceFile(
386
399
  // Always import all error classes used in the switch statement
387
400
  const allErrorClasses = new Set<string>(Object.values(STATUS_TO_ERROR))
388
401
 
389
- if (flags.oauth) {
402
+ if (flags.credential && flags.credential !== 'oauth2') {
403
+ // Per-user credential: no special imports needed, creds passed via constructor
404
+ } else if (flags.oauth || flags.credential === 'oauth2') {
390
405
  lines.push("import { OAuth2Client } from '@pikku/core/oauth2'")
391
406
  lines.push(
392
407
  "import type { TypedSecretService } from '#pikku/secrets/pikku-secrets.gen.js'"
@@ -458,7 +473,17 @@ function generateServiceFile(
458
473
  lines.push(`export class ${pascalName}Service {`)
459
474
  lines.push(' private baseUrl: string')
460
475
 
461
- if (flags.oauth) {
476
+ if (flags.credential && flags.credential !== 'oauth2') {
477
+ const credField = flags.credential === 'bearer' ? 'token' : 'apiKey'
478
+ lines.push('')
479
+ lines.push(
480
+ ` constructor(private creds: { ${credField}: string }, variables: TypedVariablesService) {`
481
+ )
482
+ lines.push(
483
+ ` this.baseUrl = variables.get('${screamingName}_BASE_URL') as string`
484
+ )
485
+ lines.push(' }')
486
+ } else if (flags.oauth || flags.credential === 'oauth2') {
462
487
  lines.push(' private oauth: OAuth2Client')
463
488
  lines.push('')
464
489
  lines.push(
@@ -548,7 +573,30 @@ function generateServiceFile(
548
573
  lines.push(' }')
549
574
  lines.push('')
550
575
 
551
- if (flags.oauth) {
576
+ if (flags.credential && flags.credential !== 'oauth2') {
577
+ // Per-user credential: use creds from wire.getCredentials()
578
+ if (flags.credential === 'bearer') {
579
+ lines.push(' headers.Authorization = `Bearer ${this.creds.token}`')
580
+ } else {
581
+ // apikey: check spec for custom header name
582
+ const apiKeyScheme = Object.values(spec.securitySchemes).find(
583
+ (s) => s.type === 'apiKey'
584
+ )
585
+ if (apiKeyScheme?.name && apiKeyScheme?.in === 'header') {
586
+ lines.push(
587
+ ` headers[${JSON.stringify(apiKeyScheme.name)}] = this.creds.apiKey`
588
+ )
589
+ } else {
590
+ lines.push(' headers.Authorization = `Bearer ${this.creds.apiKey}`')
591
+ }
592
+ }
593
+ lines.push('')
594
+ lines.push(' const response = await fetch(url.toString(), {')
595
+ lines.push(' method,')
596
+ lines.push(' headers,')
597
+ lines.push(' body: body ? JSON.stringify(body) : undefined,')
598
+ lines.push(' })')
599
+ } else if (flags.oauth || flags.credential === 'oauth2') {
552
600
  lines.push(
553
601
  ' const response = await this.oauth.request(url.toString(), {'
554
602
  )