Skip to content

Commit 4c93015

Browse files
committed
Generate acl from operations
1 parent 006d47c commit 4c93015

28 files changed

Lines changed: 460 additions & 23 deletions

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@povio/openapi-codegen-cli",
3-
"version": "0.4.4",
3+
"version": "0.4.5",
44
"main": "./dist/index.js",
55
"bin": {
66
"openapi-codegen": "./dist/sh.js"

src/generators/const/acl.const.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Import } from "../types/generate";
2+
3+
export const ACL_APP_ABILITY_FILENAME = "acl/app.ability";
4+
export const ACL_ALL_ABILITIES = "AllAbilities";
5+
6+
export const CASL_ABILITY_BINDING = {
7+
abilityTuple: "AbilityTuple",
8+
pureAbility: "PureAbility",
9+
forcedSubject: "ForcedSubject",
10+
subject: "subject",
11+
};
12+
export const CASL_ABILITY_IMPORT: Import = {
13+
bindings: [
14+
CASL_ABILITY_BINDING.abilityTuple,
15+
CASL_ABILITY_BINDING.pureAbility,
16+
CASL_ABILITY_BINDING.forcedSubject,
17+
CASL_ABILITY_BINDING.subject,
18+
],
19+
from: "@casl/ability",
20+
};

src/generators/const/options.const.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ export const DEFAULT_GENERATE_OPTIONS: GenerateOptions = {
2525
outputFileNameSuffix: "queries",
2626
namespaceSuffix: "Queries",
2727
},
28+
[GenerateType.Acl]: {
29+
outputFileNameSuffix: "acl",
30+
namespaceSuffix: "Acl",
31+
},
2832
},
2933
// Zod options
3034
schemaSuffix: SCHEMA_SUFFIX,

src/generators/const/validation.const.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ export const VALIDATION_ERROR_TYPE_TITLE: Record<ValidationErrorType, string> =
66
"missing-path-parameter": "Missing Path Parameters",
77
"not-allowed-inline-enum": "Not Allowed Inline Enums",
88
"not-allowed-circular-schema": "Not Allowed Circular Schemas",
9+
"missing-acl-condition-property": "Missing x-acl Condition Property",
910
};

src/generators/core/SchemaResolver.class.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { OpenAPIV3 } from "openapi-types";
22
import { ALLOWED_METHODS } from "../const/openapi.const";
3+
import { OperationObject } from "../types/openapi";
34
import { GenerateOptions } from "../types/options";
45
import { ValidationError } from "../types/validation";
56
import { getUniqueArray } from "../utils/array.utils";
@@ -72,7 +73,7 @@ export class SchemaResolver {
7273

7374
readonly dependencyGraph: DependencyGraph;
7475

75-
readonly operationsByTag: Record<string, OpenAPIV3.OperationObject[]> = {};
76+
readonly operationsByTag: Record<string, OperationObject[]> = {};
7677
readonly operationNames: string[] = [];
7778

7879
readonly validationErrors: ValidationError[] = [];
@@ -242,7 +243,7 @@ export class SchemaResolver {
242243

243244
const pathItem = pick(pathItemObj, ALLOWED_METHODS);
244245
for (const method in pathItem) {
245-
const operation = pathItem[method as keyof typeof pathItem] as OpenAPIV3.OperationObject | undefined;
246+
const operation = pathItem[method as keyof typeof pathItem] as OperationObject | undefined;
246247

247248
if (!operation || (operation.deprecated && !this.options?.withDeprecatedEndpoints)) {
248249
continue;
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import { OpenAPIV3 } from "openapi-types";
2+
import { AclConditionsPropertyType, Endpoint, EndpointAclInfo } from "src/generators/types/endpoint";
3+
import { OperationAclInfo, OperationObject } from "src/generators/types/openapi";
4+
import { isParamMediaTypeAllowed, isReferenceObject } from "src/generators/utils/openapi.utils";
5+
import { isQuery } from "src/generators/utils/queries.utils";
6+
import { decapitalize } from "src/generators/utils/string.utils";
7+
import { getMissingAclConditionPropertyError } from "src/generators/utils/validation.utils";
8+
import { SchemaResolver } from "../SchemaResolver.class";
9+
10+
export function getEndpointAcl({
11+
resolver,
12+
endpoint,
13+
operation,
14+
}: {
15+
resolver: SchemaResolver;
16+
endpoint: Endpoint;
17+
operation: OperationObject;
18+
}): EndpointAclInfo[] | undefined {
19+
const acl = operation["x-acl"];
20+
21+
return acl?.map((item) => {
22+
const conditionsTypes = Object.keys(item.conditions ?? {}).reduce((acc, name) => {
23+
const propertyType = getEndpointAclConditionPropertyType({ resolver, endpoint, acl, name });
24+
if (!propertyType) {
25+
resolver.validationErrors.push(
26+
getMissingAclConditionPropertyError(name, operation.operationId ?? `${endpoint.method} ${endpoint.path}`),
27+
);
28+
return acc;
29+
}
30+
return [...acc, propertyType];
31+
}, [] as AclConditionsPropertyType[]);
32+
33+
return { ...item, conditionsTypes };
34+
});
35+
}
36+
37+
function getEndpointAclConditionPropertyType({
38+
resolver,
39+
endpoint,
40+
acl,
41+
name,
42+
}: {
43+
resolver: SchemaResolver;
44+
endpoint: Endpoint;
45+
acl: OperationAclInfo[];
46+
name: string;
47+
}): AclConditionsPropertyType | undefined {
48+
const conditionPropertyPath = acl[0]?.conditions?.[name];
49+
if (!conditionPropertyPath) {
50+
return;
51+
}
52+
53+
const pathSplits = conditionPropertyPath.replace(/^\$[^.]*\./, "").split(".");
54+
55+
let schema: OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject | undefined = undefined;
56+
let required: boolean | undefined = undefined;
57+
let info: string | undefined = undefined;
58+
let index = 0;
59+
60+
const parameter = endpoint.parameters.find(({ name }) => name === pathSplits[index]);
61+
if (parameter) {
62+
required = parameter.parameterObject?.required;
63+
schema = parameter.parameterObject?.schema;
64+
info = `${parameter.name} ${decapitalize(parameter.type)} parameter`;
65+
index++;
66+
} else {
67+
const bodyParameter = endpoint.parameters.find(({ bodyObject }) => !!bodyObject);
68+
const mediaTypes = Object.keys(bodyParameter?.bodyObject?.content ?? {});
69+
const matchingMediaType = mediaTypes.find(isParamMediaTypeAllowed);
70+
if (matchingMediaType) {
71+
schema = bodyParameter?.bodyObject?.content?.[matchingMediaType]?.schema;
72+
info = `${isQuery(endpoint) ? "query" : "mutation"} data`;
73+
}
74+
}
75+
76+
while (schema && index < pathSplits.length) {
77+
const resolvedSchema = resolver.resolveObject(schema);
78+
const propertySchema = Object.entries(resolvedSchema.properties ?? {}).find(
79+
([key]) => key === pathSplits[index],
80+
)?.[1] as OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject;
81+
schema = propertySchema;
82+
required = resolvedSchema.required?.includes(pathSplits[index]) ?? false;
83+
index++;
84+
}
85+
86+
if (!schema) {
87+
return;
88+
}
89+
90+
return {
91+
name,
92+
type: isReferenceObject(schema) ? undefined : schema.type,
93+
zodSchemaName: isReferenceObject(schema) ? resolver.getZodSchemaNameByRef(schema.$ref) : undefined,
94+
required,
95+
info,
96+
};
97+
}

src/generators/core/endpoints/getEndpointBody.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { OpenAPIV3 } from "openapi-types";
21
import { BODY_PARAMETER_NAME } from "src/generators/const/endpoints.const";
32
import { EndpointParameter } from "src/generators/types/endpoint";
3+
import { OperationObject } from "src/generators/types/openapi";
44
import { isParamMediaTypeAllowed } from "src/generators/utils/openapi.utils";
55
import { getBodyZodSchemaName, getZodSchemaOperationName } from "src/generators/utils/zod-schema.utils";
66
import { SchemaResolver } from "../SchemaResolver.class";
@@ -16,7 +16,7 @@ export function getEndpointBody({
1616
tag,
1717
}: {
1818
resolver: SchemaResolver;
19-
operation: OpenAPIV3.OperationObject;
19+
operation: OperationObject;
2020
operationName: string;
2121
isUniqueOperationName: boolean;
2222
tag: string;

src/generators/core/endpoints/getEndpointsFromOpenAPIDoc.test.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ describe("getEndpointsFromOpenAPIDoc", () => {
121121
const resolver = new SchemaResolver(openApiDoc, generateOptions);
122122
const endpoints = getEndpointsFromOpenAPIDoc(resolver);
123123

124-
expect(endpoints).toStrictEqual([
124+
expect(endpoints).toEqual([
125125
{
126126
description: "Place a new order in the store",
127127
errors: [
@@ -232,7 +232,7 @@ describe("getEndpointsFromOpenAPIDoc", () => {
232232
const resolver = new SchemaResolver(openApiDoc, generateOptions);
233233
const endpoints = getEndpointsFromOpenAPIDoc(resolver);
234234

235-
expect(endpoints).toStrictEqual([
235+
expect(endpoints).toEqual([
236236
{
237237
description: "Update an existing pet by Id",
238238
errors: [
@@ -390,7 +390,7 @@ describe("getEndpointsFromOpenAPIDoc", () => {
390390

391391
const resolver = new SchemaResolver(openApiDoc, generateOptions);
392392
const endpoints = getEndpointsFromOpenAPIDoc(resolver);
393-
expect(endpoints).toStrictEqual([
393+
expect(endpoints).toEqual([
394394
{
395395
description: "Update an existing pet by Id",
396396
errors: [
@@ -586,7 +586,7 @@ describe("getEndpointsFromOpenAPIDoc", () => {
586586

587587
const resolver = new SchemaResolver(openApiDoc, generateOptions);
588588
const endpoints = getEndpointsFromOpenAPIDoc(resolver);
589-
expect(endpoints).toStrictEqual([
589+
expect(endpoints).toEqual([
590590
{
591591
description: "Multiple status values can be provided with comma separated strings",
592592
errors: [
@@ -734,7 +734,7 @@ describe("getEndpointsFromOpenAPIDoc", () => {
734734
const openApiDoc = (await SwaggerParser.parse("./test/petstore.yaml")) as OpenAPIV3.Document;
735735
const resolver = new SchemaResolver(openApiDoc, generateOptions);
736736
const endpoints = getEndpointsFromOpenAPIDoc(resolver);
737-
expect(endpoints).toStrictEqual([
737+
expect(endpoints).toEqual([
738738
{
739739
description: "Update an existing pet by Id",
740740
errors: [
@@ -1687,7 +1687,7 @@ describe("getEndpointsFromOpenAPIDoc", () => {
16871687
};
16881688
const resolver = new SchemaResolver(openApiDoc, generateOptions);
16891689
const endpoints = getEndpointsFromOpenAPIDoc(resolver);
1690-
expect(endpoints).toStrictEqual([
1690+
expect(endpoints).toEqual([
16911691
{
16921692
description: "Multiple status values can be provided with comma separated strings",
16931693
errors: [

src/generators/core/endpoints/getEndpointsFromOpenAPIDoc.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { OpenAPIV3 } from "openapi-types";
22
import { ALLOWED_METHODS } from "src/generators/const/openapi.const";
33
import { VOID_SCHEMA } from "src/generators/const/zod.const";
4+
import { OperationObject } from "src/generators/types/openapi";
45
import { invalidVariableNameCharactersToCamel } from "src/generators/utils/js.utils";
56
import { formatTag, getOperationTag } from "src/generators/utils/tag.utils";
67
import { getInvalidOperationIdError, getMissingPathParameterError } from "src/generators/utils/validation.utils";
@@ -19,6 +20,7 @@ import { SchemaResolver } from "../SchemaResolver.class";
1920
import { getZodChain } from "../zod/getZodChain";
2021
import { getZodSchema } from "../zod/getZodSchema";
2122
import { resolveZodSchemaName } from "../zod/resolveZodSchemaName";
23+
import { getEndpointAcl } from "./getEndpointAcl";
2224
import { getEndpointBody } from "./getEndpointBody";
2325
import { getEndpointParameter } from "./getEndpointParameter";
2426

@@ -31,7 +33,7 @@ export function getEndpointsFromOpenAPIDoc(resolver: SchemaResolver) {
3133
const pathParameters = getParameters(pathItemObj.parameters ?? []);
3234

3335
for (const method in pathItem) {
34-
const operation = pathItem[method as keyof typeof pathItem] as OpenAPIV3.OperationObject | undefined;
36+
const operation = pathItem[method as keyof typeof pathItem] as OperationObject | undefined;
3537
if (!operation || (operation.deprecated && !resolver.options.withDeprecatedEndpoints)) {
3638
continue;
3739
}
@@ -154,6 +156,8 @@ export function getEndpointsFromOpenAPIDoc(resolver: SchemaResolver) {
154156
endpoint.response = VOID_SCHEMA;
155157
}
156158

159+
endpoint.acl = getEndpointAcl({ resolver, endpoint, operation });
160+
157161
endpoints.push(endpoint);
158162
}
159163
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { ACL_ALL_ABILITIES, CASL_ABILITY_BINDING, CASL_ABILITY_IMPORT } from "../const/acl.const";
2+
import { SchemaResolver } from "../core/SchemaResolver.class";
3+
import { GenerateType, GenerateTypeParams, Import } from "../types/generate";
4+
import { getUniqueArray } from "../utils/array.utils";
5+
import { getTagAllAbilitiesName } from "../utils/generate/generate.acl.utils";
6+
import { getAclImports, getModelsImports } from "../utils/generate/generate.imports.utils";
7+
import { getNamespaceName } from "../utils/generate/generate.utils";
8+
import { getHbsTemplateDelegate } from "../utils/hbs/hbs-template.utils";
9+
10+
export function generateAcl({ resolver, data, tag = "" }: GenerateTypeParams) {
11+
const endpoints = data.get(tag)?.endpoints.filter(({ acl }) => acl && acl.length > 0);
12+
if (!endpoints || endpoints.length === 0) {
13+
return;
14+
}
15+
16+
const caslAbilityTupleImport: Import = {
17+
...CASL_ABILITY_IMPORT,
18+
bindings: [
19+
CASL_ABILITY_BINDING.abilityTuple,
20+
...(endpoints.filter(({ acl }) => acl?.[0].conditions)
21+
? [CASL_ABILITY_BINDING.forcedSubject, CASL_ABILITY_BINDING.subject]
22+
: []),
23+
],
24+
};
25+
26+
const aclZodSchemas = endpoints.reduce((acc, endpoint) => {
27+
const zodSchemas = endpoint.acl?.[0].conditionsTypes?.reduce(
28+
(acc, propertyType) => [...acc, ...(propertyType?.zodSchemaName ? [propertyType.zodSchemaName] : [])],
29+
[] as string[],
30+
);
31+
return [...acc, ...(zodSchemas ?? [])];
32+
}, [] as string[]);
33+
34+
const modelsImports = getModelsImports({
35+
resolver,
36+
tag,
37+
zodSchemasAsTypes: getUniqueArray(aclZodSchemas),
38+
});
39+
40+
const hbsTemplate = getHbsTemplateDelegate(resolver, "acl");
41+
42+
return hbsTemplate({
43+
caslAbilityTupleImport,
44+
modelsImports,
45+
includeNamespace: resolver.options.includeNamespaces,
46+
namespace: getNamespaceName({ type: GenerateType.Acl, tag, options: resolver.options }),
47+
endpoints,
48+
});
49+
}
50+
51+
export function generateAppAcl(resolver: SchemaResolver, tags: string[]) {
52+
const caslAbilityTupleImport: Import = {
53+
...CASL_ABILITY_IMPORT,
54+
bindings: [CASL_ABILITY_BINDING.pureAbility],
55+
};
56+
57+
const imports = getAclImports({
58+
tags,
59+
entityName: ACL_ALL_ABILITIES,
60+
getAliasEntityName: getTagAllAbilitiesName,
61+
options: resolver.options,
62+
});
63+
64+
const namespaces = tags.map((tag) => getNamespaceName({ type: GenerateType.Acl, tag, options: resolver.options }));
65+
66+
const hbsTemplate = getHbsTemplateDelegate(resolver, "app-acl");
67+
68+
return hbsTemplate({
69+
caslAbilityTupleImport,
70+
imports,
71+
allAbilities: ACL_ALL_ABILITIES,
72+
includeNamespace: resolver.options.includeNamespaces,
73+
tags,
74+
namespaces,
75+
});
76+
}

0 commit comments

Comments
 (0)