Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@povio/openapi-codegen-cli",
"version": "2.0.1",
"version": "2.0.2-rc.4",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
Expand Down Expand Up @@ -59,12 +59,13 @@
},
"license": "BSD-3-Clause",
"dependencies": {
"i18next": "^25.7.3",
"import-fresh": "^3.3.1"
},
"peerDependencies": {
"@casl/ability": "^6.7.3",
"@casl/react": "^5.0.0",
"@tanstack/react-query": "^5.85.9",
"@tanstack/react-query": "^5.89.0",
"axios": "^1.13.1",
"react": "^19.1.0",
"zod": "^4.1.12"
Expand Down
12 changes: 6 additions & 6 deletions src/generators/templates/partials/query-use-mutation.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ export const {{queryName endpoint mutation=true}} = (options?: AppMutationOption
return {{queryHook}}({
mutationFn: {{#if endpoint.mediaUpload}}async {{/if}}({{#if (endpointParams endpoint includeFileParam=true)}} { {{{endpointArgs endpoint includeFileParam=true}}}{{#if endpoint.mediaUpload}}, abortController, onUploadProgress{{/if}} } {{/if}}) => {{#if hasMutationFnBody}} { {{/if}}
{{#if hasAclCheck}}{{{genAclCheckCall endpoint}}}{{/if}}
{{#if endpoint.mediaUpload}}const uploadInstructions = await {{importedEndpointName endpoint}}({{{endpointArgs endpoint}}}{{#if hasAxiosRequestConfig}}{{#if (endpointArgs endpoint)}}, {{/if}}{{axiosRequestConfigName}}{{/if}});
{{#if endpoint.mediaUpload}}const uploadInstructions = await {{importedEndpointName endpoint}}({{{endpointArgs endpoint}}}{{#if hasAxiosRequestConfig}}{{#if (endpointArgs endpoint)}}, {{/if}}{{axiosRequestConfigName}}{{/if}});

if (file && uploadInstructions.url) {
const method = (data?.method?.toLowerCase() ?? "put") as 'put' | 'post';
let dataToSend: File | FormData = file;
Expand All @@ -34,21 +34,21 @@ export const {{queryName endpoint mutation=true}} = (options?: AppMutationOption
: undefined,
});
}

return uploadInstructions;
{{else}}
{{#if hasMutationFnBody}}return {{/if}}{{importedEndpointName endpoint}}({{{endpointArgs endpoint}}}{{#if hasAxiosRequestConfig}}{{#if (endpointArgs endpoint)}}, {{/if}}{{axiosRequestConfigName}}{{/if}})
{{/if}}
{{#if hasMutationFnBody}} }{{/if}},
...options, {{#if hasMutationEffects}}
onSuccess: async (resData, variables, context) => {
onSuccess: async (resData, variables, onMutateResult, context) => {
{{! Mutation effects }}
{{#if updateQueryEndpoints}}
{{#if destructuredVariables}}const { {{commaSeparated destructuredVariables }} } = variables;{{/if}}
const updateKeys = [{{#each updateQueryEndpoints as | endpoint |}}keys.{{endpointName endpoint}}({{{endpointArgs endpoint includeOnlyRequiredParams=true}}}), {{/each}}];
{{/if}}
await runMutationEffects(resData, options{{#if updateQueryEndpoints}}, updateKeys{{/if}});
options?.onSuccess?.(resData, variables, context);
options?.onSuccess?.(resData, variables, onMutateResult, context);
},{{/if}}
});
};
};
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ export type { AppQueryOptions, AppMutationOptions, AppInfiniteQueryOptions } fro
export { OpenApiRouter } from "./lib/config/router.context";
export { OpenApiQueryConfig } from "./lib/config/queryConfig.context";

// i18n resources (for consumer apps to merge into their i18n config)
export { ns, resources } from "./lib/config/i18n";

// Auth
export { AuthContext } from "./lib/auth/auth.context";
export { AuthGuard } from "./lib/auth/AuthGuard";
Expand Down
12 changes: 12 additions & 0 deletions src/lib/assets/locales/en/translation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"openapi": {
"sharedErrors": {
"dataValidation": "An error occurred while validating the data",
"internalError": "An internal error occurred. This is most likely a bug on our end. Please try again later.",
"networkError": "A network error occurred. Are you connected to the internet?",
"canceledError": "The request was canceled.",
"unknownError": "An unknown error occurred. Please try again later.",
"unknownErrorWithCode": "An unknown error occurred. Error code: \"{{code}}\""
}
}
}
12 changes: 12 additions & 0 deletions src/lib/assets/locales/sl/translation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"openapi": {
"sharedErrors": {
"dataValidation": "Pri preverjanju podatkov je prišlo do napake",
"internalError": "Prišlo je do notranje napake.",
"networkError": "Prišlo je do napake v omrežju.",
"canceledError": "Zahteva je bila preklicana.",
"unknownError": "Prišlo je do neznane napake.",
"unknownErrorWithCode": "Prišlo je do neznane napake. Koda napake: \"{{code}}\""
}
}
}
31 changes: 31 additions & 0 deletions src/lib/config/i18n.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import i18next from "i18next";

import translationEN from "src/lib/assets/locales/en/translation.json";
import translationSL from "src/lib/assets/locales/sl/translation.json";

export const ns = "openapi";
export const resources = {
en: {
[ns]: translationEN,
},
sl: {
[ns]: translationSL,
},
} as const;

const defaultLanguage = "en";

const i18n = i18next.createInstance();
i18n.init({
compatibilityJSON: "v4",
lng: defaultLanguage,
fallbackLng: defaultLanguage,
resources,
ns: Object.keys(resources.en),
defaultNS: ns,
interpolation: {
escapeValue: false,
},
});

export const defaultT = i18n.t.bind(i18n);
56 changes: 41 additions & 15 deletions src/lib/rest/error-handling.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import axios from "axios";
import { type TFunction } from "i18next";
import { z } from "zod";

import { defaultT } from "src/lib/config/i18n";
import { RestUtils } from "./rest.utils";

// codes that we want to handle in every scenario
export type GeneralErrorCodes =
| "DATA_VALIDATION_ERROR"
| "NETWORK_ERROR"
Expand All @@ -26,31 +27,32 @@ export class ApplicationException<CodeT> extends Error {

export interface ErrorEntry<CodeT> {
code: CodeT;
condition: (error: unknown) => boolean;
getMessage: (error: unknown) => string;
condition?: (error: unknown) => boolean;
getMessage: (t: TFunction, error: unknown) => string;
}

export interface ErrorHandlerOptions<CodeT extends string> {
entries: ErrorEntry<CodeT>[];
t?: TFunction;
onRethrowError?: (error: unknown, exception: ApplicationException<CodeT | GeneralErrorCodes>) => void;
}

export class ErrorHandler<CodeT extends string> {
entries: ErrorEntry<CodeT | GeneralErrorCodes>[] = [];
private t: TFunction;
private onRethrowError?: (error: unknown, exception: ApplicationException<CodeT | GeneralErrorCodes>) => void;

constructor({ entries, onRethrowError }: ErrorHandlerOptions<CodeT>) {
constructor({ entries, t = defaultT, onRethrowError }: ErrorHandlerOptions<CodeT>) {
this.t = t;
this.onRethrowError = onRethrowError;
type ICodeT = CodeT | GeneralErrorCodes;

// implement checking for each of the general errors

const dataValidationError: ErrorEntry<ICodeT> = {
code: "DATA_VALIDATION_ERROR",
condition: (e) => {
return e instanceof z.ZodError;
},
getMessage: () => "An error occurred while validating the data",
getMessage: () => this.t("openapi.sharedErrors.dataValidation"),
};

const internalError: ErrorEntry<ICodeT> = {
Expand All @@ -62,7 +64,7 @@ export class ErrorHandler<CodeT extends string> {

return false;
},
getMessage: () => "An internal error occurred. This is most likely a bug on our end. Please try again later.",
getMessage: () => this.t("openapi.sharedErrors.internalError"),
};

const networkError: ErrorEntry<ICodeT> = {
Expand All @@ -74,7 +76,7 @@ export class ErrorHandler<CodeT extends string> {

return false;
},
getMessage: () => "A network error occurred. Are you connected to the internet?",
getMessage: () => this.t("openapi.sharedErrors.networkError"),
};

const canceledError: ErrorEntry<ICodeT> = {
Expand All @@ -90,25 +92,49 @@ export class ErrorHandler<CodeT extends string> {

return false;
},
getMessage: () => "The request was canceled.",
getMessage: () => this.t("openapi.sharedErrors.canceledError"),
};

const unknownError: ErrorEntry<ICodeT> = {
code: "UNKNOWN_ERROR",
condition: () => true,
getMessage: () => "An unknown error occurred. Please try again later.",
getMessage: (_, e) => {
const code = RestUtils.extractServerResponseCode(e);
const serverMessage = RestUtils.extractServerErrorMessage(e);

if (code) {
let message = `Unknown error, message from server: ${code}`;
if (serverMessage) {
message += ` ${serverMessage}`;
}
return message;
}

return this.t("openapi.sharedErrors.unknownError");
},
};

// general errors have the lowest priority
this.entries = [...entries, dataValidationError, internalError, networkError, canceledError, unknownError];
}

// convert the error into an application exception
private matchesEntry(error: unknown, entry: ErrorEntry<CodeT | GeneralErrorCodes>, code: string | null): boolean {
if (entry.condition) {
return entry.condition(error);
}
return code === entry.code;
}

public setTranslateFunction(t: TFunction) {
this.t = t;
}

public rethrowError(error: unknown): ApplicationException<CodeT | GeneralErrorCodes> {
const errorEntry = this.entries.find((entry) => entry.condition(error ?? {}))!;
const code = RestUtils.extractServerResponseCode(error);
const errorEntry = this.entries.find((entry) => this.matchesEntry(error, entry, code))!;

const serverMessage = RestUtils.extractServerErrorMessage(error);
const exception = new ApplicationException(errorEntry.getMessage(error), errorEntry.code, serverMessage);
const exception = new ApplicationException(errorEntry.getMessage(this.t, error), errorEntry.code, serverMessage);

this.onRethrowError?.(error, exception);

Expand Down Expand Up @@ -145,7 +171,7 @@ export class ErrorHandler<CodeT extends string> {
}

if (fallbackToUnknown) {
return "An unknown error occurred. Please try again later.";
return defaultT("openapi.sharedErrors.unknownError");
}

return null;
Expand Down
24 changes: 23 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,13 @@ __metadata:
languageName: node
linkType: hard

"@babel/runtime@npm:^7.28.4":
version: 7.28.4
resolution: "@babel/runtime@npm:7.28.4"
checksum: 10c0/792ce7af9750fb9b93879cc9d1db175701c4689da890e6ced242ea0207c9da411ccf16dc04e689cc01158b28d7898c40d75598f4559109f761c12ce01e959bf7
languageName: node
linkType: hard

"@casl/ability@npm:^6.7.3":
version: 6.7.5
resolution: "@casl/ability@npm:6.7.5"
Expand Down Expand Up @@ -651,6 +658,7 @@ __metadata:
eslint-plugin-no-relative-import-paths: "npm:^1.6.1"
eslint-plugin-prettier: "npm:^5.1.3"
handlebars: "npm:^4.7.8"
i18next: "npm:^25.7.3"
import-fresh: "npm:^3.3.1"
openapi-types: "npm:^12.1.3"
prettier: "npm:^3.2.5"
Expand All @@ -668,7 +676,7 @@ __metadata:
peerDependencies:
"@casl/ability": ^6.7.3
"@casl/react": ^5.0.0
"@tanstack/react-query": ^5.85.9
"@tanstack/react-query": ^5.89.0
axios: ^1.13.1
react: ^19.1.0
zod: ^4.1.12
Expand Down Expand Up @@ -2507,6 +2515,20 @@ __metadata:
languageName: node
linkType: hard

"i18next@npm:^25.7.3":
version: 25.7.4
resolution: "i18next@npm:25.7.4"
dependencies:
"@babel/runtime": "npm:^7.28.4"
peerDependencies:
typescript: ^5
peerDependenciesMeta:
typescript:
optional: true
checksum: 10c0/d30e4f9a1c31adc0570c9f4a22ab226257620f2465e2275f8cdd5ef7f52668716cc9be3b741031aeacb39e2196be5a5f656e12d89787b4248098f681e99f72ea
languageName: node
linkType: hard

"iconv-lite@npm:^0.6.2":
version: 0.6.3
resolution: "iconv-lite@npm:0.6.3"
Expand Down