vite-plugin-openapi-codegen 2.0.0 → 3.0.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.
package/dist/cli.mjs CHANGED
@@ -52,6 +52,40 @@ function renderOperationTypeAliases(typeAliases) {
52
52
  if (typeAliases.length === 0) return "";
53
53
  return `${typeAliases.map((alias) => `export type ${alias.typeName} = ${alias.definitionExpr};`).join("\n")}\n`;
54
54
  }
55
+ function renderAccessPoliciesSource(entries, generatedHeader) {
56
+ if (entries.length === 0) return "";
57
+ const lines = [
58
+ ...generatedHeader,
59
+ "",
60
+ "export type AccessPolicyKind = \"authenticated\" | \"internal\" | \"public\" | \"role\";",
61
+ "",
62
+ "export interface AccessPolicy {",
63
+ " kind: AccessPolicyKind;",
64
+ " roles?: readonly string[];",
65
+ "}",
66
+ "",
67
+ "export interface OperationAccessPolicy extends AccessPolicy {",
68
+ " apiPath: string;",
69
+ " method: string;",
70
+ " operationId: string;",
71
+ " path: string;",
72
+ "}",
73
+ "",
74
+ "export const accessPolicies = {"
75
+ ];
76
+ for (const entry of entries) {
77
+ lines.push(` ${entry.funcName}: {`);
78
+ lines.push(` apiPath: ${JSON.stringify(entry.apiPath)},`);
79
+ lines.push(` kind: ${JSON.stringify(entry.kind)},`);
80
+ lines.push(` method: ${JSON.stringify(entry.methodUpper)},`);
81
+ lines.push(` operationId: ${JSON.stringify(entry.operationId)},`);
82
+ lines.push(` path: ${JSON.stringify(entry.strippedPath)},`);
83
+ if (entry.roles.length > 0) lines.push(` roles: [${entry.roles.map((role) => JSON.stringify(role)).join(", ")}],`);
84
+ lines.push(" },");
85
+ }
86
+ lines.push("} as const satisfies Record<string, OperationAccessPolicy>;", "", "export type AccessPolicyKey = keyof typeof accessPolicies;", "");
87
+ return `${lines.join("\n")}`;
88
+ }
55
89
  function printGeneratedFile(statements, generatedHeader) {
56
90
  const sourceFile = ts.factory.createSourceFile(statements, ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
57
91
  const printed = AST_PRINTER.printFile(sourceFile).trim();
@@ -613,6 +647,44 @@ function createClientRenderModel(model, useTypeAliases) {
613
647
  }))
614
648
  };
615
649
  }
650
+ function createAccessPolicyEntries(operations, securitySchemes, topLevelSecurity) {
651
+ return operations.flatMap((entry) => {
652
+ const accessPolicy = readSecurityRequirement(entry, securitySchemes, topLevelSecurity);
653
+ if (!accessPolicy) return [];
654
+ return [{
655
+ apiPath: entry.apiPath,
656
+ funcName: entry.funcName,
657
+ kind: accessPolicy.kind,
658
+ methodUpper: entry.method.toUpperCase(),
659
+ operationId: entry.operationId,
660
+ roles: accessPolicy.roles ?? [],
661
+ strippedPath: entry.strippedPath
662
+ }];
663
+ });
664
+ }
665
+ function readSecurityRequirement(entry, securitySchemes, topLevelSecurity) {
666
+ const security = entry.operation.security ?? topLevelSecurity;
667
+ if (security === void 0) return null;
668
+ if (!Array.isArray(security)) throw new Error(`Operation "${entry.operationId}" has invalid security`);
669
+ if (security.length === 0) return { kind: "public" };
670
+ const requirement = security[0];
671
+ if (!isRecord(requirement)) throw new Error(`Operation "${entry.operationId}" has an invalid security requirement`);
672
+ for (const [schemeName, scopes] of Object.entries(requirement)) {
673
+ const type = securitySchemes?.[schemeName]?.type;
674
+ if (type === "apiKey") return { kind: "internal" };
675
+ if (type === "http") {
676
+ const roles = Array.isArray(scopes) ? scopes.filter((scope) => typeof scope === "string") : [];
677
+ return roles.length === 0 ? { kind: "authenticated" } : {
678
+ kind: "role",
679
+ roles
680
+ };
681
+ }
682
+ }
683
+ throw new Error(`Operation "${entry.operationId}" has an unrecognized security scheme`);
684
+ }
685
+ function isRecord(value) {
686
+ return typeof value === "object" && value !== null && !Array.isArray(value);
687
+ }
616
688
  function resolveChannel(channel, useTypeAliases) {
617
689
  if (!channel.typeRef) return channel;
618
690
  return {
@@ -628,14 +700,17 @@ function renderGeneratedArtifacts(spec, options, preCollectedOperations) {
628
700
  const stripPrefix = options.stripPrefix ?? true;
629
701
  const httpClient = resolveHttpClientConfig(options.httpClient);
630
702
  const useTypeAliases = options.typeAliases ?? false;
631
- const clientModel = buildClientRenderModelFromOperations(preCollectedOperations ?? collectOperations(spec, pathPrefix, stripPrefix), spec, {
703
+ const operations = preCollectedOperations ?? collectOperations(spec, pathPrefix, stripPrefix);
704
+ const clientModel = buildClientRenderModelFromOperations(operations, spec, {
632
705
  json: httpClient.jsonFunction,
633
706
  void: httpClient.voidFunction
634
707
  });
635
708
  if (clientModel.operations.length === 0) throw new Error(`No paths matching prefix "${pathPrefix}" found in openapi.json`);
709
+ const accessPolicies = renderAccessPoliciesSource(createAccessPolicyEntries(operations, spec.components?.securitySchemes, spec.security), GENERATED_HEADER);
636
710
  return {
637
711
  api: renderApiSource(createApiEntries(clientModel.operations, useTypeAliases), GENERATED_HEADER),
638
712
  client: renderClientSource(createClientRenderModel(clientModel, useTypeAliases), GENERATED_HEADER, httpClient),
713
+ ...accessPolicies ? { accessPolicies } : {},
639
714
  ...useTypeAliases && clientModel.typeAliases.length > 0 ? { apiTypes: renderOperationTypeAliases(clientModel.typeAliases) } : {}
640
715
  };
641
716
  }
@@ -681,6 +756,7 @@ async function generateOpenAPIArtifacts(root, options) {
681
756
  warnOnParameterLocationMismatch(operations);
682
757
  await generateApiTypes(apiTypesSource, outputDir, options.typeAliases ?? false);
683
758
  if (artifacts.apiTypes) writeFileSync(resolve(outputDir, "api-types.d.ts"), artifacts.apiTypes, { flag: "a" });
759
+ if (artifacts.accessPolicies) writeFileSync(resolve(outputDir, "access-policies.ts"), artifacts.accessPolicies);
684
760
  writeFileSync(resolve(outputDir, "api.ts"), artifacts.api);
685
761
  writeFileSync(resolve(outputDir, "client.ts"), artifacts.client);
686
762
  }
package/dist/index.d.mts CHANGED
@@ -26,12 +26,18 @@ interface OpenAPIResponse {
26
26
  content?: Record<string, OpenAPIContent>;
27
27
  description?: string;
28
28
  }
29
+ type OpenAPISecurityRequirement = Record<string, string[]>;
30
+ interface OpenAPISecurityScheme {
31
+ type?: string;
32
+ scheme?: string;
33
+ }
29
34
  interface OpenAPIOperation {
30
35
  operationId?: string;
31
36
  parameters?: OpenAPIParameter[];
32
37
  requestBody?: OpenAPIRequestBody;
33
38
  responses?: Record<string, OpenAPIResponse>;
34
39
  tags?: string[];
40
+ security?: OpenAPISecurityRequirement[];
35
41
  }
36
42
  type OpenAPIPathItem = Partial<Record<HttpMethod, OpenAPIOperation>>;
37
43
  interface OpenAPISchema {
@@ -42,8 +48,10 @@ interface OpenAPISchema {
42
48
  }
43
49
  interface OpenAPISpec {
44
50
  paths?: Record<string, OpenAPIPathItem>;
51
+ security?: OpenAPISecurityRequirement[];
45
52
  components?: {
46
53
  schemas?: Record<string, OpenAPISchema>;
54
+ securitySchemes?: Record<string, OpenAPISecurityScheme>;
47
55
  };
48
56
  }
49
57
  interface OperationEntry {
@@ -90,6 +98,7 @@ interface Options {
90
98
  interface GeneratedArtifacts {
91
99
  api: string;
92
100
  apiTypes?: string;
101
+ accessPolicies?: string;
93
102
  client: string;
94
103
  }
95
104
  declare function renderGeneratedArtifacts(spec: OpenAPISpec, options: Pick<Options, "httpClient" | "pathPrefix" | "stripPrefix" | "typeAliases">, preCollectedOperations?: OperationEntry[]): GeneratedArtifacts;
package/dist/index.mjs CHANGED
@@ -50,6 +50,40 @@ function renderOperationTypeAliases(typeAliases) {
50
50
  if (typeAliases.length === 0) return "";
51
51
  return `${typeAliases.map((alias) => `export type ${alias.typeName} = ${alias.definitionExpr};`).join("\n")}\n`;
52
52
  }
53
+ function renderAccessPoliciesSource(entries, generatedHeader) {
54
+ if (entries.length === 0) return "";
55
+ const lines = [
56
+ ...generatedHeader,
57
+ "",
58
+ "export type AccessPolicyKind = \"authenticated\" | \"internal\" | \"public\" | \"role\";",
59
+ "",
60
+ "export interface AccessPolicy {",
61
+ " kind: AccessPolicyKind;",
62
+ " roles?: readonly string[];",
63
+ "}",
64
+ "",
65
+ "export interface OperationAccessPolicy extends AccessPolicy {",
66
+ " apiPath: string;",
67
+ " method: string;",
68
+ " operationId: string;",
69
+ " path: string;",
70
+ "}",
71
+ "",
72
+ "export const accessPolicies = {"
73
+ ];
74
+ for (const entry of entries) {
75
+ lines.push(` ${entry.funcName}: {`);
76
+ lines.push(` apiPath: ${JSON.stringify(entry.apiPath)},`);
77
+ lines.push(` kind: ${JSON.stringify(entry.kind)},`);
78
+ lines.push(` method: ${JSON.stringify(entry.methodUpper)},`);
79
+ lines.push(` operationId: ${JSON.stringify(entry.operationId)},`);
80
+ lines.push(` path: ${JSON.stringify(entry.strippedPath)},`);
81
+ if (entry.roles.length > 0) lines.push(` roles: [${entry.roles.map((role) => JSON.stringify(role)).join(", ")}],`);
82
+ lines.push(" },");
83
+ }
84
+ lines.push("} as const satisfies Record<string, OperationAccessPolicy>;", "", "export type AccessPolicyKey = keyof typeof accessPolicies;", "");
85
+ return `${lines.join("\n")}`;
86
+ }
53
87
  function printGeneratedFile(statements, generatedHeader) {
54
88
  const sourceFile = ts.factory.createSourceFile(statements, ts.factory.createToken(ts.SyntaxKind.EndOfFileToken), ts.NodeFlags.None);
55
89
  const printed = AST_PRINTER.printFile(sourceFile).trim();
@@ -611,6 +645,44 @@ function createClientRenderModel(model, useTypeAliases) {
611
645
  }))
612
646
  };
613
647
  }
648
+ function createAccessPolicyEntries(operations, securitySchemes, topLevelSecurity) {
649
+ return operations.flatMap((entry) => {
650
+ const accessPolicy = readSecurityRequirement(entry, securitySchemes, topLevelSecurity);
651
+ if (!accessPolicy) return [];
652
+ return [{
653
+ apiPath: entry.apiPath,
654
+ funcName: entry.funcName,
655
+ kind: accessPolicy.kind,
656
+ methodUpper: entry.method.toUpperCase(),
657
+ operationId: entry.operationId,
658
+ roles: accessPolicy.roles ?? [],
659
+ strippedPath: entry.strippedPath
660
+ }];
661
+ });
662
+ }
663
+ function readSecurityRequirement(entry, securitySchemes, topLevelSecurity) {
664
+ const security = entry.operation.security ?? topLevelSecurity;
665
+ if (security === void 0) return null;
666
+ if (!Array.isArray(security)) throw new Error(`Operation "${entry.operationId}" has invalid security`);
667
+ if (security.length === 0) return { kind: "public" };
668
+ const requirement = security[0];
669
+ if (!isRecord(requirement)) throw new Error(`Operation "${entry.operationId}" has an invalid security requirement`);
670
+ for (const [schemeName, scopes] of Object.entries(requirement)) {
671
+ const type = securitySchemes?.[schemeName]?.type;
672
+ if (type === "apiKey") return { kind: "internal" };
673
+ if (type === "http") {
674
+ const roles = Array.isArray(scopes) ? scopes.filter((scope) => typeof scope === "string") : [];
675
+ return roles.length === 0 ? { kind: "authenticated" } : {
676
+ kind: "role",
677
+ roles
678
+ };
679
+ }
680
+ }
681
+ throw new Error(`Operation "${entry.operationId}" has an unrecognized security scheme`);
682
+ }
683
+ function isRecord(value) {
684
+ return typeof value === "object" && value !== null && !Array.isArray(value);
685
+ }
614
686
  function resolveChannel(channel, useTypeAliases) {
615
687
  if (!channel.typeRef) return channel;
616
688
  return {
@@ -626,14 +698,17 @@ function renderGeneratedArtifacts(spec, options, preCollectedOperations) {
626
698
  const stripPrefix = options.stripPrefix ?? true;
627
699
  const httpClient = resolveHttpClientConfig(options.httpClient);
628
700
  const useTypeAliases = options.typeAliases ?? false;
629
- const clientModel = buildClientRenderModelFromOperations(preCollectedOperations ?? collectOperations(spec, pathPrefix, stripPrefix), spec, {
701
+ const operations = preCollectedOperations ?? collectOperations(spec, pathPrefix, stripPrefix);
702
+ const clientModel = buildClientRenderModelFromOperations(operations, spec, {
630
703
  json: httpClient.jsonFunction,
631
704
  void: httpClient.voidFunction
632
705
  });
633
706
  if (clientModel.operations.length === 0) throw new Error(`No paths matching prefix "${pathPrefix}" found in openapi.json`);
707
+ const accessPolicies = renderAccessPoliciesSource(createAccessPolicyEntries(operations, spec.components?.securitySchemes, spec.security), GENERATED_HEADER);
634
708
  return {
635
709
  api: renderApiSource(createApiEntries(clientModel.operations, useTypeAliases), GENERATED_HEADER),
636
710
  client: renderClientSource(createClientRenderModel(clientModel, useTypeAliases), GENERATED_HEADER, httpClient),
711
+ ...accessPolicies ? { accessPolicies } : {},
637
712
  ...useTypeAliases && clientModel.typeAliases.length > 0 ? { apiTypes: renderOperationTypeAliases(clientModel.typeAliases) } : {}
638
713
  };
639
714
  }
@@ -679,6 +754,7 @@ async function generateOpenAPIArtifacts(root, options) {
679
754
  warnOnParameterLocationMismatch(operations);
680
755
  await generateApiTypes(apiTypesSource, outputDir, options.typeAliases ?? false);
681
756
  if (artifacts.apiTypes) writeFileSync(resolve(outputDir, "api-types.d.ts"), artifacts.apiTypes, { flag: "a" });
757
+ if (artifacts.accessPolicies) writeFileSync(resolve(outputDir, "access-policies.ts"), artifacts.accessPolicies);
682
758
  writeFileSync(resolve(outputDir, "api.ts"), artifacts.api);
683
759
  writeFileSync(resolve(outputDir, "client.ts"), artifacts.client);
684
760
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-openapi-codegen",
3
- "version": "2.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "Vite plugin that generates typed API clients and route builders from OpenAPI specs",
5
5
  "keywords": [
6
6
  "api-client",