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 +81 -1
- package/dist/index.d.mts +7 -0
- package/dist/index.mjs +81 -1
- package/package.json +1 -1
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
|
|
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
|
|
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
|
}
|