vite-plugin-openapi-codegen 1.2.2 → 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();
@@ -162,13 +196,29 @@ function createBodyRequestProperty(bodyChannel, bodyAssignment) {
162
196
  return ts.factory.createSpreadAssignment(ts.factory.createParenthesizedExpression(ts.factory.createConditionalExpression(ts.factory.createBinaryExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("options"), "body"), ts.factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), ts.factory.createIdentifier("undefined")), ts.factory.createToken(ts.SyntaxKind.QuestionToken), ts.factory.createObjectLiteralExpression([], false), ts.factory.createToken(ts.SyntaxKind.ColonToken), bodyAssignment)));
163
197
  }
164
198
  function createBuildSearchParamsFunction() {
165
- return ts.factory.createFunctionDeclaration(void 0, void 0, ts.factory.createIdentifier("buildSearchParams"), void 0, [ts.factory.createParameterDeclaration(void 0, void 0, ts.factory.createIdentifier("query"), void 0, createTypeNodeFromText("Record<string, unknown> | undefined"))], createTypeNodeFromText("URLSearchParams | undefined"), ts.factory.createBlock([
166
- ts.factory.createIfStatement(ts.factory.createBinaryExpression(ts.factory.createIdentifier("query"), ts.factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), ts.factory.createIdentifier("undefined")), ts.factory.createReturnStatement(ts.factory.createIdentifier("undefined"))),
167
- ts.factory.createVariableStatement(void 0, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(ts.factory.createIdentifier("entries"), void 0, void 0, parseExpression("Object.entries(query).filter(([, value]) => value != null)"))], ts.NodeFlags.Const)),
168
- ts.factory.createIfStatement(ts.factory.createBinaryExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("entries"), "length"), ts.factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), ts.factory.createNumericLiteral("0")), ts.factory.createReturnStatement(ts.factory.createIdentifier("undefined"))),
169
- ts.factory.createVariableStatement(void 0, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(ts.factory.createIdentifier("searchParams"), void 0, void 0, ts.factory.createNewExpression(ts.factory.createIdentifier("URLSearchParams"), void 0, []))], ts.NodeFlags.Const)),
170
- ts.factory.createForOfStatement(void 0, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(ts.factory.createArrayBindingPattern([ts.factory.createBindingElement(void 0, void 0, "key"), ts.factory.createBindingElement(void 0, void 0, "value")]))], ts.NodeFlags.Const), ts.factory.createIdentifier("entries"), ts.factory.createBlock([ts.factory.createExpressionStatement(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("searchParams"), "set"), void 0, [ts.factory.createIdentifier("key"), ts.factory.createCallExpression(ts.factory.createIdentifier("String"), void 0, [ts.factory.createIdentifier("value")])]))], true)),
171
- ts.factory.createReturnStatement(ts.factory.createIdentifier("searchParams"))
199
+ const queryIdentifier = ts.factory.createIdentifier("query");
200
+ const searchParamsIdentifier = ts.factory.createIdentifier("searchParams");
201
+ const hasParamsIdentifier = ts.factory.createIdentifier("hasParams");
202
+ const keyIdentifier = ts.factory.createIdentifier("key");
203
+ const valueIdentifier = ts.factory.createIdentifier("value");
204
+ const itemIdentifier = ts.factory.createIdentifier("item");
205
+ const isNullishOrEmptyString = (identifier) => ts.factory.createBinaryExpression(ts.factory.createBinaryExpression(identifier, ts.factory.createToken(ts.SyntaxKind.EqualsEqualsToken), ts.factory.createNull()), ts.factory.createToken(ts.SyntaxKind.BarBarToken), ts.factory.createBinaryExpression(identifier, ts.factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), ts.factory.createStringLiteral("")));
206
+ const assignHasParamsTrue = () => ts.factory.createExpressionStatement(ts.factory.createAssignment(hasParamsIdentifier, ts.factory.createTrue()));
207
+ return ts.factory.createFunctionDeclaration(void 0, void 0, ts.factory.createIdentifier("buildSearchParams"), void 0, [ts.factory.createParameterDeclaration(void 0, void 0, queryIdentifier, void 0, createTypeNodeFromText("Record<string, unknown> | undefined"))], createTypeNodeFromText("URLSearchParams | undefined"), ts.factory.createBlock([
208
+ ts.factory.createIfStatement(ts.factory.createBinaryExpression(queryIdentifier, ts.factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), ts.factory.createIdentifier("undefined")), ts.factory.createReturnStatement(ts.factory.createIdentifier("undefined"))),
209
+ ts.factory.createVariableStatement(void 0, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(searchParamsIdentifier, void 0, void 0, ts.factory.createNewExpression(ts.factory.createIdentifier("URLSearchParams"), void 0, []))], ts.NodeFlags.Const)),
210
+ ts.factory.createVariableStatement(void 0, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(hasParamsIdentifier, void 0, void 0, ts.factory.createFalse())], ts.NodeFlags.Let)),
211
+ ts.factory.createForOfStatement(void 0, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(ts.factory.createArrayBindingPattern([ts.factory.createBindingElement(void 0, void 0, keyIdentifier), ts.factory.createBindingElement(void 0, void 0, valueIdentifier)]))], ts.NodeFlags.Const), parseExpression("Object.entries(query)"), ts.factory.createBlock([
212
+ ts.factory.createIfStatement(isNullishOrEmptyString(valueIdentifier), ts.factory.createContinueStatement()),
213
+ ts.factory.createIfStatement(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("Array"), "isArray"), void 0, [valueIdentifier]), ts.factory.createBlock([ts.factory.createForOfStatement(void 0, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(itemIdentifier)], ts.NodeFlags.Const), valueIdentifier, ts.factory.createBlock([
214
+ ts.factory.createIfStatement(isNullishOrEmptyString(itemIdentifier), ts.factory.createContinueStatement()),
215
+ ts.factory.createExpressionStatement(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(searchParamsIdentifier, "append"), void 0, [keyIdentifier, ts.factory.createCallExpression(ts.factory.createIdentifier("String"), void 0, [itemIdentifier])])),
216
+ assignHasParamsTrue()
217
+ ], true)), ts.factory.createContinueStatement()], true)),
218
+ ts.factory.createExpressionStatement(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(searchParamsIdentifier, "set"), void 0, [keyIdentifier, ts.factory.createCallExpression(ts.factory.createIdentifier("String"), void 0, [valueIdentifier])])),
219
+ assignHasParamsTrue()
220
+ ], true)),
221
+ ts.factory.createReturnStatement(ts.factory.createConditionalExpression(hasParamsIdentifier, ts.factory.createToken(ts.SyntaxKind.QuestionToken), searchParamsIdentifier, ts.factory.createToken(ts.SyntaxKind.ColonToken), ts.factory.createIdentifier("undefined")))
172
222
  ], true));
173
223
  }
174
224
  function capitalize$1(value) {
@@ -486,6 +536,7 @@ function renderPrimitiveSchemaType(schema) {
486
536
  const type = schema?.type;
487
537
  if (!type) return "unknown";
488
538
  if (Array.isArray(type)) return type.map((memberType) => mapPrimitiveType(memberType)).join(" | ");
539
+ if (type === "array") return `${renderPrimitiveSchemaType(schema.items)}[]`;
489
540
  return mapPrimitiveType(type);
490
541
  }
491
542
  function allocateOperationTypeName(context, funcName, suffix) {
@@ -596,6 +647,48 @@ function createClientRenderModel(model, useTypeAliases) {
596
647
  }))
597
648
  };
598
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
+ }
599
692
  function resolveChannel(channel, useTypeAliases) {
600
693
  if (!channel.typeRef) return channel;
601
694
  return {
@@ -611,14 +704,17 @@ function renderGeneratedArtifacts(spec, options, preCollectedOperations) {
611
704
  const stripPrefix = options.stripPrefix ?? true;
612
705
  const httpClient = resolveHttpClientConfig(options.httpClient);
613
706
  const useTypeAliases = options.typeAliases ?? false;
614
- const clientModel = buildClientRenderModelFromOperations(preCollectedOperations ?? collectOperations(spec, pathPrefix, stripPrefix), spec, {
707
+ const operations = preCollectedOperations ?? collectOperations(spec, pathPrefix, stripPrefix);
708
+ const clientModel = buildClientRenderModelFromOperations(operations, spec, {
615
709
  json: httpClient.jsonFunction,
616
710
  void: httpClient.voidFunction
617
711
  });
618
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);
619
714
  return {
620
715
  api: renderApiSource(createApiEntries(clientModel.operations, useTypeAliases), GENERATED_HEADER),
621
716
  client: renderClientSource(createClientRenderModel(clientModel, useTypeAliases), GENERATED_HEADER, httpClient),
717
+ ...accessPolicies ? { accessPolicies } : {},
622
718
  ...useTypeAliases && clientModel.typeAliases.length > 0 ? { apiTypes: renderOperationTypeAliases(clientModel.typeAliases) } : {}
623
719
  };
624
720
  }
@@ -664,6 +760,7 @@ async function generateOpenAPIArtifacts(root, options) {
664
760
  warnOnParameterLocationMismatch(operations);
665
761
  await generateApiTypes(apiTypesSource, outputDir, options.typeAliases ?? false);
666
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);
667
764
  writeFileSync(resolve(outputDir, "api.ts"), artifacts.api);
668
765
  writeFileSync(resolve(outputDir, "client.ts"), artifacts.client);
669
766
  }
package/dist/index.d.mts CHANGED
@@ -9,6 +9,9 @@ interface OpenAPIParameter {
9
9
  name: string;
10
10
  required?: boolean;
11
11
  schema?: {
12
+ items?: {
13
+ type?: string | string[];
14
+ };
12
15
  type?: string | string[];
13
16
  };
14
17
  }
@@ -23,15 +26,22 @@ interface OpenAPIResponse {
23
26
  content?: Record<string, OpenAPIContent>;
24
27
  description?: string;
25
28
  }
29
+ type OpenAPIAccessKind = "authenticated" | "internal" | "public" | "role";
30
+ interface OpenAPIAccessExtension {
31
+ kind: OpenAPIAccessKind;
32
+ roles?: string[];
33
+ }
26
34
  interface OpenAPIOperation {
27
35
  operationId?: string;
28
36
  parameters?: OpenAPIParameter[];
29
37
  requestBody?: OpenAPIRequestBody;
30
38
  responses?: Record<string, OpenAPIResponse>;
31
39
  tags?: string[];
40
+ "x-access"?: OpenAPIAccessExtension;
32
41
  }
33
42
  type OpenAPIPathItem = Partial<Record<HttpMethod, OpenAPIOperation>>;
34
43
  interface OpenAPISchema {
44
+ items?: unknown;
35
45
  properties?: Record<string, unknown>;
36
46
  required?: string[];
37
47
  type?: string;
@@ -86,6 +96,7 @@ interface Options {
86
96
  interface GeneratedArtifacts {
87
97
  api: string;
88
98
  apiTypes?: string;
99
+ accessPolicies?: string;
89
100
  client: string;
90
101
  }
91
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();
@@ -160,13 +194,29 @@ function createBodyRequestProperty(bodyChannel, bodyAssignment) {
160
194
  return ts.factory.createSpreadAssignment(ts.factory.createParenthesizedExpression(ts.factory.createConditionalExpression(ts.factory.createBinaryExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("options"), "body"), ts.factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), ts.factory.createIdentifier("undefined")), ts.factory.createToken(ts.SyntaxKind.QuestionToken), ts.factory.createObjectLiteralExpression([], false), ts.factory.createToken(ts.SyntaxKind.ColonToken), bodyAssignment)));
161
195
  }
162
196
  function createBuildSearchParamsFunction() {
163
- return ts.factory.createFunctionDeclaration(void 0, void 0, ts.factory.createIdentifier("buildSearchParams"), void 0, [ts.factory.createParameterDeclaration(void 0, void 0, ts.factory.createIdentifier("query"), void 0, createTypeNodeFromText("Record<string, unknown> | undefined"))], createTypeNodeFromText("URLSearchParams | undefined"), ts.factory.createBlock([
164
- ts.factory.createIfStatement(ts.factory.createBinaryExpression(ts.factory.createIdentifier("query"), ts.factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), ts.factory.createIdentifier("undefined")), ts.factory.createReturnStatement(ts.factory.createIdentifier("undefined"))),
165
- ts.factory.createVariableStatement(void 0, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(ts.factory.createIdentifier("entries"), void 0, void 0, parseExpression("Object.entries(query).filter(([, value]) => value != null)"))], ts.NodeFlags.Const)),
166
- ts.factory.createIfStatement(ts.factory.createBinaryExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("entries"), "length"), ts.factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), ts.factory.createNumericLiteral("0")), ts.factory.createReturnStatement(ts.factory.createIdentifier("undefined"))),
167
- ts.factory.createVariableStatement(void 0, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(ts.factory.createIdentifier("searchParams"), void 0, void 0, ts.factory.createNewExpression(ts.factory.createIdentifier("URLSearchParams"), void 0, []))], ts.NodeFlags.Const)),
168
- ts.factory.createForOfStatement(void 0, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(ts.factory.createArrayBindingPattern([ts.factory.createBindingElement(void 0, void 0, "key"), ts.factory.createBindingElement(void 0, void 0, "value")]))], ts.NodeFlags.Const), ts.factory.createIdentifier("entries"), ts.factory.createBlock([ts.factory.createExpressionStatement(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("searchParams"), "set"), void 0, [ts.factory.createIdentifier("key"), ts.factory.createCallExpression(ts.factory.createIdentifier("String"), void 0, [ts.factory.createIdentifier("value")])]))], true)),
169
- ts.factory.createReturnStatement(ts.factory.createIdentifier("searchParams"))
197
+ const queryIdentifier = ts.factory.createIdentifier("query");
198
+ const searchParamsIdentifier = ts.factory.createIdentifier("searchParams");
199
+ const hasParamsIdentifier = ts.factory.createIdentifier("hasParams");
200
+ const keyIdentifier = ts.factory.createIdentifier("key");
201
+ const valueIdentifier = ts.factory.createIdentifier("value");
202
+ const itemIdentifier = ts.factory.createIdentifier("item");
203
+ const isNullishOrEmptyString = (identifier) => ts.factory.createBinaryExpression(ts.factory.createBinaryExpression(identifier, ts.factory.createToken(ts.SyntaxKind.EqualsEqualsToken), ts.factory.createNull()), ts.factory.createToken(ts.SyntaxKind.BarBarToken), ts.factory.createBinaryExpression(identifier, ts.factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), ts.factory.createStringLiteral("")));
204
+ const assignHasParamsTrue = () => ts.factory.createExpressionStatement(ts.factory.createAssignment(hasParamsIdentifier, ts.factory.createTrue()));
205
+ return ts.factory.createFunctionDeclaration(void 0, void 0, ts.factory.createIdentifier("buildSearchParams"), void 0, [ts.factory.createParameterDeclaration(void 0, void 0, queryIdentifier, void 0, createTypeNodeFromText("Record<string, unknown> | undefined"))], createTypeNodeFromText("URLSearchParams | undefined"), ts.factory.createBlock([
206
+ ts.factory.createIfStatement(ts.factory.createBinaryExpression(queryIdentifier, ts.factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), ts.factory.createIdentifier("undefined")), ts.factory.createReturnStatement(ts.factory.createIdentifier("undefined"))),
207
+ ts.factory.createVariableStatement(void 0, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(searchParamsIdentifier, void 0, void 0, ts.factory.createNewExpression(ts.factory.createIdentifier("URLSearchParams"), void 0, []))], ts.NodeFlags.Const)),
208
+ ts.factory.createVariableStatement(void 0, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(hasParamsIdentifier, void 0, void 0, ts.factory.createFalse())], ts.NodeFlags.Let)),
209
+ ts.factory.createForOfStatement(void 0, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(ts.factory.createArrayBindingPattern([ts.factory.createBindingElement(void 0, void 0, keyIdentifier), ts.factory.createBindingElement(void 0, void 0, valueIdentifier)]))], ts.NodeFlags.Const), parseExpression("Object.entries(query)"), ts.factory.createBlock([
210
+ ts.factory.createIfStatement(isNullishOrEmptyString(valueIdentifier), ts.factory.createContinueStatement()),
211
+ ts.factory.createIfStatement(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(ts.factory.createIdentifier("Array"), "isArray"), void 0, [valueIdentifier]), ts.factory.createBlock([ts.factory.createForOfStatement(void 0, ts.factory.createVariableDeclarationList([ts.factory.createVariableDeclaration(itemIdentifier)], ts.NodeFlags.Const), valueIdentifier, ts.factory.createBlock([
212
+ ts.factory.createIfStatement(isNullishOrEmptyString(itemIdentifier), ts.factory.createContinueStatement()),
213
+ ts.factory.createExpressionStatement(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(searchParamsIdentifier, "append"), void 0, [keyIdentifier, ts.factory.createCallExpression(ts.factory.createIdentifier("String"), void 0, [itemIdentifier])])),
214
+ assignHasParamsTrue()
215
+ ], true)), ts.factory.createContinueStatement()], true)),
216
+ ts.factory.createExpressionStatement(ts.factory.createCallExpression(ts.factory.createPropertyAccessExpression(searchParamsIdentifier, "set"), void 0, [keyIdentifier, ts.factory.createCallExpression(ts.factory.createIdentifier("String"), void 0, [valueIdentifier])])),
217
+ assignHasParamsTrue()
218
+ ], true)),
219
+ ts.factory.createReturnStatement(ts.factory.createConditionalExpression(hasParamsIdentifier, ts.factory.createToken(ts.SyntaxKind.QuestionToken), searchParamsIdentifier, ts.factory.createToken(ts.SyntaxKind.ColonToken), ts.factory.createIdentifier("undefined")))
170
220
  ], true));
171
221
  }
172
222
  function capitalize$1(value) {
@@ -484,6 +534,7 @@ function renderPrimitiveSchemaType(schema) {
484
534
  const type = schema?.type;
485
535
  if (!type) return "unknown";
486
536
  if (Array.isArray(type)) return type.map((memberType) => mapPrimitiveType(memberType)).join(" | ");
537
+ if (type === "array") return `${renderPrimitiveSchemaType(schema.items)}[]`;
487
538
  return mapPrimitiveType(type);
488
539
  }
489
540
  function allocateOperationTypeName(context, funcName, suffix) {
@@ -594,6 +645,48 @@ function createClientRenderModel(model, useTypeAliases) {
594
645
  }))
595
646
  };
596
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
+ }
597
690
  function resolveChannel(channel, useTypeAliases) {
598
691
  if (!channel.typeRef) return channel;
599
692
  return {
@@ -609,14 +702,17 @@ function renderGeneratedArtifacts(spec, options, preCollectedOperations) {
609
702
  const stripPrefix = options.stripPrefix ?? true;
610
703
  const httpClient = resolveHttpClientConfig(options.httpClient);
611
704
  const useTypeAliases = options.typeAliases ?? false;
612
- const clientModel = buildClientRenderModelFromOperations(preCollectedOperations ?? collectOperations(spec, pathPrefix, stripPrefix), spec, {
705
+ const operations = preCollectedOperations ?? collectOperations(spec, pathPrefix, stripPrefix);
706
+ const clientModel = buildClientRenderModelFromOperations(operations, spec, {
613
707
  json: httpClient.jsonFunction,
614
708
  void: httpClient.voidFunction
615
709
  });
616
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);
617
712
  return {
618
713
  api: renderApiSource(createApiEntries(clientModel.operations, useTypeAliases), GENERATED_HEADER),
619
714
  client: renderClientSource(createClientRenderModel(clientModel, useTypeAliases), GENERATED_HEADER, httpClient),
715
+ ...accessPolicies ? { accessPolicies } : {},
620
716
  ...useTypeAliases && clientModel.typeAliases.length > 0 ? { apiTypes: renderOperationTypeAliases(clientModel.typeAliases) } : {}
621
717
  };
622
718
  }
@@ -662,6 +758,7 @@ async function generateOpenAPIArtifacts(root, options) {
662
758
  warnOnParameterLocationMismatch(operations);
663
759
  await generateApiTypes(apiTypesSource, outputDir, options.typeAliases ?? false);
664
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);
665
762
  writeFileSync(resolve(outputDir, "api.ts"), artifacts.api);
666
763
  writeFileSync(resolve(outputDir, "client.ts"), artifacts.client);
667
764
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-openapi-codegen",
3
- "version": "1.2.2",
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",