vite-plugin-openapi-codegen 2.0.0 → 3.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.
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,48 @@ function createClientRenderModel(model, useTypeAliases) {
613
647
  }))
614
648
  };
615
649
  }
650
+ const ACCESS_POLICY_KINDS = new Set([
651
+ "authenticated",
652
+ "internal",
653
+ "public",
654
+ "role"
655
+ ]);
656
+ function createAccessPolicyEntries(operations) {
657
+ return operations.flatMap((entry) => {
658
+ const accessPolicy = readAccessPolicyExtension(entry);
659
+ if (!accessPolicy) return [];
660
+ return [{
661
+ apiPath: entry.apiPath,
662
+ funcName: entry.funcName,
663
+ kind: accessPolicy.kind,
664
+ methodUpper: entry.method.toUpperCase(),
665
+ operationId: entry.operationId,
666
+ roles: accessPolicy.roles ?? [],
667
+ strippedPath: entry.strippedPath
668
+ }];
669
+ });
670
+ }
671
+ function readAccessPolicyExtension(entry) {
672
+ const accessPolicy = entry.operation["x-access"];
673
+ if (accessPolicy == null) return null;
674
+ if (!isRecord(accessPolicy)) throw new Error(`Operation "${entry.operationId}" has invalid x-access extension`);
675
+ const kind = accessPolicy.kind;
676
+ if (typeof kind !== "string" || !ACCESS_POLICY_KINDS.has(kind)) throw new Error(`Operation "${entry.operationId}" has invalid x-access kind`);
677
+ const rolesValue = accessPolicy.roles;
678
+ if (rolesValue == null) {
679
+ if (kind === "role") throw new Error(`Operation "${entry.operationId}" role access requires x-access.roles`);
680
+ return { kind };
681
+ }
682
+ if (!Array.isArray(rolesValue) || rolesValue.some((role) => typeof role !== "string")) throw new Error(`Operation "${entry.operationId}" has invalid x-access.roles`);
683
+ if (kind !== "role") throw new Error(`Operation "${entry.operationId}" non-role access must not include roles`);
684
+ return {
685
+ kind,
686
+ roles: rolesValue
687
+ };
688
+ }
689
+ function isRecord(value) {
690
+ return typeof value === "object" && value !== null && !Array.isArray(value);
691
+ }
616
692
  function resolveChannel(channel, useTypeAliases) {
617
693
  if (!channel.typeRef) return channel;
618
694
  return {
@@ -628,14 +704,17 @@ function renderGeneratedArtifacts(spec, options, preCollectedOperations) {
628
704
  const stripPrefix = options.stripPrefix ?? true;
629
705
  const httpClient = resolveHttpClientConfig(options.httpClient);
630
706
  const useTypeAliases = options.typeAliases ?? false;
631
- const clientModel = buildClientRenderModelFromOperations(preCollectedOperations ?? collectOperations(spec, pathPrefix, stripPrefix), spec, {
707
+ const operations = preCollectedOperations ?? collectOperations(spec, pathPrefix, stripPrefix);
708
+ const clientModel = buildClientRenderModelFromOperations(operations, spec, {
632
709
  json: httpClient.jsonFunction,
633
710
  void: httpClient.voidFunction
634
711
  });
635
712
  if (clientModel.operations.length === 0) throw new Error(`No paths matching prefix "${pathPrefix}" found in openapi.json`);
713
+ const accessPolicies = renderAccessPoliciesSource(createAccessPolicyEntries(operations), GENERATED_HEADER);
636
714
  return {
637
715
  api: renderApiSource(createApiEntries(clientModel.operations, useTypeAliases), GENERATED_HEADER),
638
716
  client: renderClientSource(createClientRenderModel(clientModel, useTypeAliases), GENERATED_HEADER, httpClient),
717
+ ...accessPolicies ? { accessPolicies } : {},
639
718
  ...useTypeAliases && clientModel.typeAliases.length > 0 ? { apiTypes: renderOperationTypeAliases(clientModel.typeAliases) } : {}
640
719
  };
641
720
  }
@@ -681,6 +760,7 @@ async function generateOpenAPIArtifacts(root, options) {
681
760
  warnOnParameterLocationMismatch(operations);
682
761
  await generateApiTypes(apiTypesSource, outputDir, options.typeAliases ?? false);
683
762
  if (artifacts.apiTypes) writeFileSync(resolve(outputDir, "api-types.d.ts"), artifacts.apiTypes, { flag: "a" });
763
+ if (artifacts.accessPolicies) writeFileSync(resolve(outputDir, "access-policies.ts"), artifacts.accessPolicies);
684
764
  writeFileSync(resolve(outputDir, "api.ts"), artifacts.api);
685
765
  writeFileSync(resolve(outputDir, "client.ts"), artifacts.client);
686
766
  }
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 OpenAPIAccessKind = "authenticated" | "internal" | "public" | "role";
30
+ interface OpenAPIAccessExtension {
31
+ kind: OpenAPIAccessKind;
32
+ roles?: 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
+ "x-access"?: OpenAPIAccessExtension;
35
41
  }
36
42
  type OpenAPIPathItem = Partial<Record<HttpMethod, OpenAPIOperation>>;
37
43
  interface OpenAPISchema {
@@ -90,6 +96,7 @@ interface Options {
90
96
  interface GeneratedArtifacts {
91
97
  api: string;
92
98
  apiTypes?: string;
99
+ accessPolicies?: string;
93
100
  client: string;
94
101
  }
95
102
  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,48 @@ function createClientRenderModel(model, useTypeAliases) {
611
645
  }))
612
646
  };
613
647
  }
648
+ const ACCESS_POLICY_KINDS = new Set([
649
+ "authenticated",
650
+ "internal",
651
+ "public",
652
+ "role"
653
+ ]);
654
+ function createAccessPolicyEntries(operations) {
655
+ return operations.flatMap((entry) => {
656
+ const accessPolicy = readAccessPolicyExtension(entry);
657
+ if (!accessPolicy) return [];
658
+ return [{
659
+ apiPath: entry.apiPath,
660
+ funcName: entry.funcName,
661
+ kind: accessPolicy.kind,
662
+ methodUpper: entry.method.toUpperCase(),
663
+ operationId: entry.operationId,
664
+ roles: accessPolicy.roles ?? [],
665
+ strippedPath: entry.strippedPath
666
+ }];
667
+ });
668
+ }
669
+ function readAccessPolicyExtension(entry) {
670
+ const accessPolicy = entry.operation["x-access"];
671
+ if (accessPolicy == null) return null;
672
+ if (!isRecord(accessPolicy)) throw new Error(`Operation "${entry.operationId}" has invalid x-access extension`);
673
+ const kind = accessPolicy.kind;
674
+ if (typeof kind !== "string" || !ACCESS_POLICY_KINDS.has(kind)) throw new Error(`Operation "${entry.operationId}" has invalid x-access kind`);
675
+ const rolesValue = accessPolicy.roles;
676
+ if (rolesValue == null) {
677
+ if (kind === "role") throw new Error(`Operation "${entry.operationId}" role access requires x-access.roles`);
678
+ return { kind };
679
+ }
680
+ if (!Array.isArray(rolesValue) || rolesValue.some((role) => typeof role !== "string")) throw new Error(`Operation "${entry.operationId}" has invalid x-access.roles`);
681
+ if (kind !== "role") throw new Error(`Operation "${entry.operationId}" non-role access must not include roles`);
682
+ return {
683
+ kind,
684
+ roles: rolesValue
685
+ };
686
+ }
687
+ function isRecord(value) {
688
+ return typeof value === "object" && value !== null && !Array.isArray(value);
689
+ }
614
690
  function resolveChannel(channel, useTypeAliases) {
615
691
  if (!channel.typeRef) return channel;
616
692
  return {
@@ -626,14 +702,17 @@ function renderGeneratedArtifacts(spec, options, preCollectedOperations) {
626
702
  const stripPrefix = options.stripPrefix ?? true;
627
703
  const httpClient = resolveHttpClientConfig(options.httpClient);
628
704
  const useTypeAliases = options.typeAliases ?? false;
629
- const clientModel = buildClientRenderModelFromOperations(preCollectedOperations ?? collectOperations(spec, pathPrefix, stripPrefix), spec, {
705
+ const operations = preCollectedOperations ?? collectOperations(spec, pathPrefix, stripPrefix);
706
+ const clientModel = buildClientRenderModelFromOperations(operations, spec, {
630
707
  json: httpClient.jsonFunction,
631
708
  void: httpClient.voidFunction
632
709
  });
633
710
  if (clientModel.operations.length === 0) throw new Error(`No paths matching prefix "${pathPrefix}" found in openapi.json`);
711
+ const accessPolicies = renderAccessPoliciesSource(createAccessPolicyEntries(operations), GENERATED_HEADER);
634
712
  return {
635
713
  api: renderApiSource(createApiEntries(clientModel.operations, useTypeAliases), GENERATED_HEADER),
636
714
  client: renderClientSource(createClientRenderModel(clientModel, useTypeAliases), GENERATED_HEADER, httpClient),
715
+ ...accessPolicies ? { accessPolicies } : {},
637
716
  ...useTypeAliases && clientModel.typeAliases.length > 0 ? { apiTypes: renderOperationTypeAliases(clientModel.typeAliases) } : {}
638
717
  };
639
718
  }
@@ -679,6 +758,7 @@ async function generateOpenAPIArtifacts(root, options) {
679
758
  warnOnParameterLocationMismatch(operations);
680
759
  await generateApiTypes(apiTypesSource, outputDir, options.typeAliases ?? false);
681
760
  if (artifacts.apiTypes) writeFileSync(resolve(outputDir, "api-types.d.ts"), artifacts.apiTypes, { flag: "a" });
761
+ if (artifacts.accessPolicies) writeFileSync(resolve(outputDir, "access-policies.ts"), artifacts.accessPolicies);
682
762
  writeFileSync(resolve(outputDir, "api.ts"), artifacts.api);
683
763
  writeFileSync(resolve(outputDir, "client.ts"), artifacts.client);
684
764
  }
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.0",
4
4
  "description": "Vite plugin that generates typed API clients and route builders from OpenAPI specs",
5
5
  "keywords": [
6
6
  "api-client",