Skip to content

Commit 5baef4f

Browse files
Feature/gsk 1649 use openapi typescript generation tools instead of j2ts (#1359)
* added openapi generator * added api-v2 * changed openapi doc gen giskard port * migrated getLicense * migrated getLicense * cleanup * give backend more time to start before generating openapi.json: default github runner is slow * Update backend/build.gradle.kts Co-authored-by: Googleton <hugo@giskard.ai> * generateOpenApiClient before building frontend * generateOpenApiClient before building frontend * cleanup * migrated /authorize * migrated save general settings * migrated ml worker actions * fixes * more migrations --------- Co-authored-by: Googleton <hugo@giskard.ai>
1 parent e866fa4 commit 5baef4f

13 files changed

Lines changed: 304 additions & 109 deletions

File tree

backend/build.gradle.kts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ plugins {
2929
id("io.freefair.lombok") version "6.5.0.3"
3030
id("org.liquibase.gradle") version "2.1.1"
3131
id("com.github.andygoossens.gradle-modernizer-plugin") version "1.6.2"
32+
id("org.openapi.generator") version "7.0.0"
33+
id("org.springdoc.openapi-gradle-plugin") version "1.7.0"
3234
}
3335

3436

@@ -134,6 +136,21 @@ gitProperties {
134136
keys = listOf("git.branch", "git.commit.id.abbrev", "git.commit.id.describe", "git.commit.time")
135137
}
136138

139+
openApi {
140+
apiDocsUrl.set("http://localhost:11337/v3/api-docs")
141+
outputDir.set(file("$buildDir/docs"))
142+
outputFileName.set("openapi.json")
143+
waitTimeInSeconds.set(60)
144+
customBootRun {
145+
args.set(
146+
listOf(
147+
"--spring.profiles.active=dev",
148+
"--server.port=11337",
149+
)
150+
)
151+
}
152+
}
153+
137154
val liquibaseHibernate6Version: String by project.extra.properties
138155
val jaxbRuntimeVersion: String by project.extra.properties
139156
val archunitJunit5Version: String by project.extra.properties
@@ -223,6 +240,10 @@ tasks {
223240
finalizedBy(jacocoTestReport)
224241
}
225242

243+
build {
244+
finalizedBy("generateWebClient")
245+
}
246+
226247
jacocoTestReport {
227248
dependsOn(test)
228249
}
@@ -284,6 +305,9 @@ tasks {
284305
create<Delete>("distClean") {
285306
delete(buildDir)
286307
}
308+
create<Delete>("cleanOpenApiDocs") {
309+
delete(file("$buildDir/docs/openapi.json"))
310+
}
287311
create<Delete>("deleteLiquibaseH2DB") {
288312
delete(liquibaseH2db)
289313
}
@@ -327,6 +351,14 @@ tasks {
327351
register("package") {
328352
dependsOn("bootJar")
329353
}
354+
355+
create<org.openapitools.generator.gradle.plugin.tasks.GenerateTask>("generateWebClient") {
356+
dependsOn("compileJava", "cleanOpenApiDocs", "generateOpenApiDocs")
357+
358+
generatorName.set("typescript-fetch")
359+
inputSpec.set(file("$buildDir/docs/openapi.json").toString())
360+
outputDir.set("$buildDir/../../frontend/src/generated/client")
361+
}
330362
}
331363

332364
defaultTasks("bootRun")

backend/src/main/java/ai/giskard/web/rest/controllers/SlicingFunctionController.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public SlicingFunctionDTO getSlicingFunction(
3535

3636
@PutMapping({"project/{projectKey}/slices/{uuid}", "slices/{uuid}"})
3737
public SlicingFunctionDTO createSlicingFunction(@PathVariable(value = "projectKey", required = false) String projectKey,
38+
@PathVariable("uuid") @NotNull UUID uuid,
3839
@Valid @RequestBody SlicingFunctionDTO slicingFunction) {
3940
slicingFunction.setProjectKey(projectKey);
4041
return slicingFunctionService.save(slicingFunction);

backend/src/main/java/ai/giskard/web/rest/controllers/TestFunctionController.java

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@
44
import ai.giskard.service.TestFunctionService;
55
import ai.giskard.web.dto.TestFunctionDTO;
66
import ai.giskard.web.dto.mapper.GiskardMapper;
7+
import jakarta.validation.Valid;
8+
import jakarta.validation.constraints.NotNull;
79
import lombok.RequiredArgsConstructor;
810
import org.springframework.transaction.annotation.Transactional;
911
import org.springframework.web.bind.annotation.*;
1012

11-
import jakarta.validation.Valid;
12-
import jakarta.validation.constraints.NotNull;
1313
import java.util.UUID;
1414

1515
@RestController
@@ -23,14 +23,19 @@ public class TestFunctionController {
2323

2424
@GetMapping({"/tests/{testUuid}", "/project/{projectKey}/tests/{testUuid}"})
2525
@Transactional(readOnly = true)
26-
public TestFunctionDTO getTestFunction(@PathVariable("testUuid") @NotNull UUID testUuid) {
26+
public TestFunctionDTO getTestFunction(
27+
@PathVariable("testUuid") @NotNull UUID testUuid,
28+
@PathVariable(value = "projectKey", required = false) @NotNull String projectKey
29+
) {
2730
return giskardMapper.toDTO(testFunctionRepository.getMandatoryById(testUuid));
2831
}
2932

3033
@PutMapping({"/tests/{testUuid}", "/project/{projectKey}/tests/{testUuid}"})
3134
@Transactional
32-
public TestFunctionDTO updateTestFunction(@PathVariable("testUuid") @NotNull UUID testUuid,
33-
@Valid @RequestBody TestFunctionDTO testFunction) {
35+
public TestFunctionDTO updateTestFunction(
36+
@PathVariable("testUuid") @NotNull UUID testUuid,
37+
@PathVariable(value = "projectKey", required = false) @NotNull String projectKey,
38+
@Valid @RequestBody TestFunctionDTO testFunction) {
3439
return testFunctionService.save(testFunction);
3540
}
3641

frontend/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,5 @@ yarn-error.log*
1818
*.njsproj
1919
*.sln
2020
*.sw*
21+
22+
src/generated

frontend/build.gradle.kts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ tasks {
1616
val distDir = "dist"
1717

1818
"npm_run_build" {
19+
dependsOn(":backend:generateWebClient")
20+
1921
inputs.dir("$projectDir/src")
2022
inputs.dir("$projectDir/public")
2123
inputs.file("$projectDir/package.json")
@@ -33,7 +35,7 @@ tasks {
3335

3436
clean {
3537
doFirst {
36-
delete(buildDir, "node_modules", distDir)
38+
delete(buildDir, "node_modules", distDir, "src/generated")
3739
}
3840
}
3941

frontend/src/api-v2.ts

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import axios from 'axios';
2+
import {apiURL} from '@/env';
3+
import {getLocalHFToken, getLocalToken, removeLocalToken} from '@/utils';
4+
import Vue from 'vue';
5+
import {TYPE} from 'vue-toastification';
6+
import ErrorToast from '@/views/main/utils/ErrorToast.vue';
7+
import router from '@/router';
8+
import mixpanel from 'mixpanel-browser';
9+
import {useUserStore} from '@/stores/user';
10+
import {
11+
AccountControllerApi,
12+
CatalogControllerApi,
13+
Configuration,
14+
DatasetsControllerApi,
15+
DevControllerApi,
16+
DownloadControllerApi,
17+
ErrorContext,
18+
FeedbackControllerApi,
19+
InspectionControllerApi,
20+
LicenseControllerApi,
21+
Middleware,
22+
MlWorkerControllerApi,
23+
MlWorkerJobControllerApi,
24+
ModelControllerApi,
25+
ProjectControllerApi,
26+
PublicUserControllerApi,
27+
ResponseContext,
28+
SettingsControllerApi,
29+
SetupControllerApi,
30+
SlicingFunctionControllerApi,
31+
TestControllerApi,
32+
TestFunctionControllerApi,
33+
TestSuiteControllerApi,
34+
TransformationFunctionControllerApi,
35+
UploadControllerApi,
36+
UserAdminControllerApi,
37+
UserJwtControllerApi
38+
} from "@/generated/client";
39+
40+
41+
function hfRequestInterceptor(config) {
42+
// Do something before request is sent
43+
let hfToken = getLocalHFToken();
44+
if (hfToken && config && config.headers) {
45+
config.headers.Authorization = `Bearer ${hfToken}`;
46+
}
47+
return config;
48+
}
49+
50+
const huggingface = axios.create();
51+
huggingface.interceptors.request.use(hfRequestInterceptor);
52+
53+
function replacePlaceholders(detail: string) {
54+
return detail.replaceAll('GISKARD_ADDRESS', window.location.hostname);
55+
}
56+
57+
async function trackError(ctx: ResponseContext, data: any) {
58+
let response = ctx.response;
59+
try {
60+
const trackingData = {
61+
method: ctx.init.method,
62+
code: response.status,
63+
message: response.statusText,
64+
...data
65+
};
66+
mixpanel.track('API error', trackingData);
67+
} catch (e) {
68+
console.error('Failed to track API Error', e);
69+
}
70+
}
71+
72+
73+
async function errorInterceptor(context: ResponseContext): Promise<Response | void> {
74+
const response = context.response;
75+
let data: any;
76+
try {
77+
data = await response.json();
78+
} catch (e) {
79+
data = {};
80+
}
81+
debugger
82+
await trackError(context, data);
83+
84+
if (response.status === 401) {
85+
const userStore = useUserStore();
86+
removeLocalToken();
87+
userStore.token = '';
88+
userStore.isLoggedIn = false;
89+
if (router.currentRoute.path !== '/auth/login') {
90+
await router.push('/auth/login');
91+
}
92+
} else {
93+
let title: string;
94+
let detail: string;
95+
let stack: string | undefined = undefined;
96+
if (response.status === 502) {
97+
title = response.statusText;
98+
detail = "Error while connecting to Giskard server, check that it's running";
99+
} else {
100+
title = data.title || response.statusText;
101+
detail = data.detail || response.url;
102+
stack = data.stack;
103+
}
104+
105+
detail = replacePlaceholders(detail);
106+
107+
Vue.$toast(
108+
{
109+
component: ErrorToast,
110+
props: {
111+
title: title || 'Error',
112+
detail: detail,
113+
stack: stack,
114+
},
115+
},
116+
{
117+
toastClassName: 'response-toast',
118+
type: TYPE.ERROR,
119+
}
120+
);
121+
}
122+
123+
return Promise.reject(response);
124+
}
125+
126+
127+
export class APIMiddleware implements Middleware {
128+
async post(context: ResponseContext): Promise<Response | void> {
129+
if (context.response.ok) {
130+
return Promise.resolve(context.response)
131+
}
132+
return await errorInterceptor(context);
133+
}
134+
135+
onError(context: ErrorContext): Promise<Response | void> {
136+
Vue.$toast(
137+
{
138+
component: ErrorToast,
139+
props: {
140+
title: (context.error as any).message || 'Error',
141+
detail: context.url,
142+
},
143+
},
144+
{
145+
toastClassName: 'response-toast',
146+
type: TYPE.ERROR,
147+
}
148+
);
149+
return Promise.resolve(context.response);
150+
}
151+
}
152+
153+
154+
const configuration = new Configuration({
155+
basePath: apiURL,
156+
accessToken: (_name?: string) => getLocalToken() || '',
157+
middleware: [new APIMiddleware()],
158+
});
159+
export const openapi = {
160+
settings: new SettingsControllerApi(configuration),
161+
projects: new ProjectControllerApi(configuration),
162+
account: new AccountControllerApi(configuration),
163+
catalog: new CatalogControllerApi(configuration),
164+
datasets: new DatasetsControllerApi(configuration),
165+
dev: new DevControllerApi(configuration),
166+
download: new DownloadControllerApi(configuration),
167+
feedback: new FeedbackControllerApi(configuration),
168+
inspection: new InspectionControllerApi(configuration),
169+
license: new LicenseControllerApi(configuration),
170+
mlWorker: new MlWorkerControllerApi(configuration),
171+
mlWorkerJob: new MlWorkerJobControllerApi(configuration),
172+
model: new ModelControllerApi(configuration),
173+
project: new ProjectControllerApi(configuration),
174+
publicUser: new PublicUserControllerApi(configuration),
175+
setup: new SetupControllerApi(configuration),
176+
slicingFunction: new SlicingFunctionControllerApi(configuration),
177+
test: new TestControllerApi(configuration),
178+
testFunction: new TestFunctionControllerApi(configuration),
179+
testSuite: new TestSuiteControllerApi(configuration),
180+
transformationFunction: new TransformationFunctionControllerApi(configuration),
181+
upload: new UploadControllerApi(configuration),
182+
userAdmin: new UserAdminControllerApi(configuration),
183+
userJwt: new UserJwtControllerApi(configuration),
184+
};

0 commit comments

Comments
 (0)