feat(tsrpc): enhance TypeScript API generation with improved endpoint handling and imports

- Added a new function `GetApiFile` to generate the TypeScript API file dynamically based on environment variables.
- Updated `TSEndpoint` struct to include an `Imports` map for tracking TypeScript imports.
- Enhanced `VerifyTypes` method to manage request and response types more effectively, including nullable types.
- Modified `ToTs` method to generate TypeScript code with improved type handling.
- Introduced `TSFiles` struct for managing generated TypeScript files and saving them to the filesystem.
- Implemented formatting of generated TypeScript code using Prettier.
- Added new TypeScript files for various endpoints, including user management and system utilities.
- Updated existing TypeScript files to reflect changes in API structure and response types.
This commit is contained in:
fabio 2026-04-15 18:25:31 +02:00
parent 5b9fe6c9b7
commit 3461395eb3
22 changed files with 1006 additions and 814 deletions

View File

@ -13,3 +13,8 @@ DB_dsn=file:./data/data.db?_foreign_keys=on
# Auth # Auth
AUTH_SECRET=change-me AUTH_SECRET=change-me
# TS Generator
TS_GENERATOR_URL=http://localhost:3000
TS_GENERATOR_PATH= .

View File

@ -0,0 +1,34 @@
import { api } from "./api.ts";
import { Nullable } from "./apiTypes.ts";
import * as users from "./users.ts";
// Typescript: TSEndpoint= path=/admin/users; name=listUsers; method=POST; request=admin.ListUsersRequest; response=users.[]User
// internal/admin/routes.go Line: 12
export const listUsers = async (
data: ListUsersRequest,
): Promise<{ data: users.User[]; error: Nullable<string> }> => {
return (await api.POST("/admin/users", data)) as {
data: users.User[];
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/admin/users/:uuid/block; name=blockUser; method=PUT; request=admin.BlockUserRequest; response=users.User
// internal/admin/routes.go Line: 16
export const blockUser = async (
data: BlockUserRequest,
): Promise<{ data: users.User; error: Nullable<string> }> => {
return (await api.PUT("/admin/users/:uuid/block", data)) as {
data: users.User;
error: Nullable<string>;
};
};
export interface BlockUserRequest {
action: string;
}
export interface ListUsersRequest {
page: number;
pageSize: number;
}

View File

@ -0,0 +1,276 @@
//
// Typescript API generated from gofiber backend
// Copyright (C) 2022 - 2025 Fabio Prada
//
// This file was generated by github.com/millevolte/ts-rpc
//
// Apr 14, 2026 21:39:07 UTC
//
export interface ApiRestResponse {
data?: unknown;
error: string | null;
}
function isApiRestResponse(data: unknown): data is ApiRestResponse {
return (
typeof data === "object" &&
data !== null &&
Object.prototype.hasOwnProperty.call(data, "data") &&
Object.prototype.hasOwnProperty.call(data, "error")
);
}
function normalizeError(error: unknown): Error {
if (error instanceof DOMException && error.name === "AbortError") {
return new Error("api.error.timeouterror");
}
if (error instanceof TypeError && error.message === "Failed to fetch") {
return new Error("api.error.connectionerror");
}
if (error instanceof Error) {
return error;
}
return new Error(String(error));
}
export default class Api {
apiUrl: string;
localStorage: Storage | null;
constructor(apiurl: string) {
this.apiUrl = apiurl;
this.localStorage = window.localStorage;
}
async request(
method: string,
url: string,
data: unknown,
timeout = 7000,
upload = false,
): Promise<ApiRestResponse> {
const headers: { [key: string]: string } = {
"Cache-Control": "no-cache",
};
if (!upload) {
headers["Content-Type"] = "application/json";
}
if (this.localStorage) {
const auth = this.localStorage.getItem("Auth-Token");
if (auth) {
headers["Auth-Token"] = auth;
}
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const requestOptions: RequestInit = {
method,
cache: "no-store",
mode: "cors",
credentials: "include",
headers,
signal: controller.signal,
};
if (upload) {
requestOptions.body = data as FormData;
} else if (data !== null && data !== undefined) {
requestOptions.body = JSON.stringify(data);
}
try {
const response = await fetch(url, requestOptions);
if (!response.ok) {
throw new Error("api.error." + response.statusText);
}
if (this.localStorage) {
const jwt = response.headers.get("Auth-Token");
if (jwt) {
this.localStorage.setItem("Auth-Token", jwt);
}
}
const responseData = (await response.json()) as unknown;
if (!isApiRestResponse(responseData)) {
throw new Error("api.error.wrongdatatype");
}
if (responseData.error) {
throw new Error(responseData.error);
}
return responseData;
} catch (error: unknown) {
throw normalizeError(error);
} finally {
clearTimeout(timeoutId);
}
}
processResult(result: ApiRestResponse): {
data: unknown;
error: string | null;
} {
if (typeof result.data !== "object") {
return { data: result.data, error: null };
}
if (!result.data) {
result.data = {};
}
return { data: result.data, error: null };
}
processError(error: unknown): {
data: unknown;
error: string | null;
} {
const normalizedError = normalizeError(error);
if (normalizedError.message === "api.error.timeouterror") {
Object.defineProperty(normalizedError, "__api_error__", {
value: normalizedError.message,
writable: false,
});
return { data: null, error: normalizedError.message };
}
if (normalizedError.message === "api.error.connectionerror") {
Object.defineProperty(normalizedError, "__api_error__", {
value: normalizedError.message,
writable: false,
});
return { data: null, error: normalizedError.message };
}
return {
data: null,
error: normalizedError.message,
};
}
async POST(
url: string,
data: unknown,
timeout?: number,
): Promise<{
data: unknown;
error: string | null;
}> {
try {
const upload = url.includes("/upload/");
const result = await this.request(
"POST",
this.apiUrl + url,
data,
timeout,
upload,
);
return this.processResult(result);
} catch (error: unknown) {
return this.processError(error);
}
}
async PUT(
url: string,
data: unknown,
timeout?: number,
): Promise<{
data: unknown;
error: string | null;
}> {
try {
const upload = url.includes("/upload/");
const result = await this.request(
"PUT",
this.apiUrl + url,
data,
timeout,
upload,
);
return this.processResult(result);
} catch (error: unknown) {
return this.processError(error);
}
}
async GET(
url: string,
timeout?: number,
): Promise<{
data: unknown;
error: string | null;
}> {
try {
const result = await this.request(
"GET",
this.apiUrl + url,
null,
timeout,
);
return this.processResult(result);
} catch (error: unknown) {
return this.processError(error);
}
}
async DELETE(
url: string,
timeout?: number,
): Promise<{
data: unknown;
error: string | null;
}> {
try {
const result = await this.request(
"DELETE",
this.apiUrl + url,
null,
timeout,
);
return this.processResult(result);
} catch (error: unknown) {
return this.processError(error);
}
}
async UPLOAD(
url: string,
data: unknown,
timeout?: number,
): Promise<{
data: unknown;
error: string | null;
}> {
try {
const result = await this.request(
"POST",
this.apiUrl + url,
data,
timeout,
true,
);
return this.processResult(result);
} catch (error: unknown) {
return this.processError(error);
}
}
}
export const api = new Api("http://localhost:3000");

View File

@ -0,0 +1,4 @@
// API Declarations
export type Nullable<T> = T | null;
export type Record<K extends string | number | symbol, T> = { [P in K]: T };

View File

@ -1,621 +0,0 @@
//
// Typescript API generated from gofiber backend
// Copyright (C) 2022 - 2025 Fabio Prada
//
// This file was generated by github.com/millevolte/ts-rpc
//
// Apr 06, 2026 16:56:35 UTC
//
export interface ApiRestResponse {
data?: unknown;
error: string | null;
}
function isApiRestResponse(data: unknown): data is ApiRestResponse {
return (
typeof data === "object" &&
data !== null &&
Object.prototype.hasOwnProperty.call(data, "data") &&
Object.prototype.hasOwnProperty.call(data, "error")
);
}
function normalizeError(error: unknown): Error {
if (error instanceof DOMException && error.name === "AbortError") {
return new Error("api.error.timeouterror");
}
if (error instanceof TypeError && error.message === "Failed to fetch") {
return new Error("api.error.connectionerror");
}
if (error instanceof Error) {
return error;
}
return new Error(String(error));
}
export default class Api {
apiUrl: string;
localStorage: Storage | null;
constructor(apiurl: string) {
this.apiUrl = apiurl;
this.localStorage = window.localStorage;
}
async request(
method: string,
url: string,
data: unknown,
timeout = 7000,
upload = false,
): Promise<ApiRestResponse> {
const headers: { [key: string]: string } = {
"Cache-Control": "no-cache",
};
if (!upload) {
headers["Content-Type"] = "application/json";
}
if (this.localStorage) {
const auth = this.localStorage.getItem("Auth-Token");
if (auth) {
headers["Auth-Token"] = auth;
}
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
const requestOptions: RequestInit = {
method,
cache: "no-store",
mode: "cors",
credentials: "include",
headers,
signal: controller.signal,
};
if (upload) {
requestOptions.body = data as FormData;
} else if (data !== null && data !== undefined) {
requestOptions.body = JSON.stringify(data);
}
try {
const response = await fetch(url, requestOptions);
if (!response.ok) {
throw new Error("api.error." + response.statusText);
}
if (this.localStorage) {
const jwt = response.headers.get("Auth-Token");
if (jwt) {
this.localStorage.setItem("Auth-Token", jwt);
}
}
const responseData = (await response.json()) as unknown;
if (!isApiRestResponse(responseData)) {
throw new Error("api.error.wrongdatatype");
}
if (responseData.error) {
throw new Error(responseData.error);
}
return responseData;
} catch (error: unknown) {
throw normalizeError(error);
} finally {
clearTimeout(timeoutId);
}
}
processResult(result: ApiRestResponse): {
data: unknown;
error: string | null;
} {
if (typeof result.data !== "object") {
return { data: result.data, error: null };
}
if (!result.data) {
result.data = {};
}
return { data: result.data, error: null };
}
processError(error: unknown): {
data: unknown;
error: string | null;
} {
const normalizedError = normalizeError(error);
if (normalizedError.message === "api.error.timeouterror") {
Object.defineProperty(normalizedError, "__api_error__", {
value: normalizedError.message,
writable: false,
});
return { data: null, error: normalizedError.message };
}
if (normalizedError.message === "api.error.connectionerror") {
Object.defineProperty(normalizedError, "__api_error__", {
value: normalizedError.message,
writable: false,
});
return { data: null, error: normalizedError.message };
}
return {
data: null,
error: normalizedError.message,
};
}
async POST(
url: string,
data: unknown,
timeout?: number,
): Promise<{
data: unknown;
error: string | null;
}> {
try {
const upload = url.includes("/upload/");
const result = await this.request(
"POST",
this.apiUrl + url,
data,
timeout,
upload,
);
return this.processResult(result);
} catch (error: unknown) {
return this.processError(error);
}
}
async PUT(
url: string,
data: unknown,
timeout?: number,
): Promise<{
data: unknown;
error: string | null;
}> {
try {
const upload = url.includes("/upload/");
const result = await this.request(
"PUT",
this.apiUrl + url,
data,
timeout,
upload,
);
return this.processResult(result);
} catch (error: unknown) {
return this.processError(error);
}
}
async GET(
url: string,
timeout?: number,
): Promise<{
data: unknown;
error: string | null;
}> {
try {
const result = await this.request(
"GET",
this.apiUrl + url,
null,
timeout,
);
return this.processResult(result);
} catch (error: unknown) {
return this.processError(error);
}
}
async DELETE(
url: string,
timeout?: number,
): Promise<{
data: unknown;
error: string | null;
}> {
try {
const result = await this.request(
"DELETE",
this.apiUrl + url,
null,
timeout,
);
return this.processResult(result);
} catch (error: unknown) {
return this.processError(error);
}
}
async UPLOAD(
url: string,
data: unknown,
timeout?: number,
): Promise<{
data: unknown;
error: string | null;
}> {
try {
const result = await this.request(
"POST",
this.apiUrl + url,
data,
timeout,
true,
);
return this.processResult(result);
} catch (error: unknown) {
return this.processError(error);
}
}
}
const api = new Api("http://localhost:3000");
// Global Declarations
export type Nullable<T> = T | null;
export type Record<K extends string | number | symbol, T> = { [P in K]: T };
//
// package user
//
// Typescript: TSEndpoint= path=/users/:uuid; name=getUser; method=GET; response=models.UserProfile
// internal/user/routes.go Line: 13
export const getUser = async (
uuid: string,
): Promise<{ data: UserProfile; error: Nullable<string> }> => {
return (await api.GET(`/users/${uuid}`)) as {
data: UserProfile;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/users; name=createUser; method=POST; request=models.UserCreateInput; response=models.UserProfile
// internal/user/routes.go Line: 16
export const createUser = async (
data: UserCreateInput,
): Promise<{ data: UserProfile; error: Nullable<string> }> => {
return (await api.POST("/users", data)) as {
data: UserProfile;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/users/:uuid; name=updateUser; method=PUT; request=controllers.UpdateUserRequest; response=models.UserProfile
// internal/user/routes.go Line: 19
export const updateUser = async (
data: UpdateUserRequest,
): Promise<{ data: UserProfile; error: Nullable<string> }> => {
return (await api.PUT("/users/:uuid", data)) as {
data: UserProfile;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/users/:uuid; name=deleteUser; method=DELETE; response=controllers.SimpleResponse
// internal/user/routes.go Line: 22
export const deleteUser = async (
uuid: string,
): Promise<{ data: SimpleResponse; error: Nullable<string> }> => {
return (await api.DELETE(`/users/${uuid}`)) as {
data: SimpleResponse;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/auth/me; name=me; method=GET; response=models.UserShort
// internal/user/routes.go Line: 25
export const me = async (): Promise<{
data: UserShort;
error: Nullable<string>;
}> => {
return (await api.GET("/auth/me")) as {
data: UserShort;
error: Nullable<string>;
};
};
export interface UpdateUserRequest {
name: string;
email: string;
password: string;
roles: models.UserRoles;
status: models.UserStatus;
types: models.UserTypes;
avatar: Nullable<string>;
details: Nullable<models.UserDetailsShort>;
preferences: Nullable<models.UserPreferencesShort>;
}
//
// package models
//
export interface UserDetailsShort {
title: string;
firstName: string;
lastName: string;
address: string;
city: string;
zipCode: string;
country: string;
phone: string;
}
export interface UserPreferencesShort {
useIdle: boolean;
idleTimeout: number;
useIdlePassword: boolean;
idlePin: string;
useDirectLogin: boolean;
useQuadcodeLogin: boolean;
sendNoticesMail: boolean;
language: string;
}
export interface UserShort {
email: string;
name: string;
roles: UserRoles;
status: UserStatus;
uuid: string;
details: Nullable<UserDetailsShort>;
preferences: Nullable<UserPreferencesShort>;
avatar: Nullable<string>;
}
export interface UserCreateInput {
name: string;
email: string;
password: string;
roles: UserRoles;
status: UserStatus;
types: UserTypes;
avatar: Nullable<string>;
details: Nullable<UserDetailsShort>;
preferences: Nullable<UserPreferencesShort>;
}
export type UserRoles = string[];
export type UserStatus = (typeof EnumUserStatus)[keyof typeof EnumUserStatus];
export type UserTypes = string[];
export type UsersShort = UserShort[];
export const EnumUserStatus = {
UserStatusPending: "pending",
UserStatusActive: "active",
UserStatusDisabled: "disabled",
} as const;
//
// package admin
//
// Typescript: TSEndpoint= path=/admin/users/:uuid/block; name=blockUser; method=PUT; request=admin.BlockUserRequest; response=models.UserShort
// internal/admin/routes.go Line: 16
export const blockUser = async (
data: BlockUserRequest,
): Promise<{ data: UserShort; error: Nullable<string> }> => {
return (await api.PUT("/admin/users/:uuid/block", data)) as {
data: UserShort;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/admin/users; name=listUsers; method=POST; request=admin.ListUsersRequest; response=models.[]UserShort
// internal/admin/routes.go Line: 12
export const listUsers = async (
data: ListUsersRequest,
): Promise<{ data: UserShort[]; error: Nullable<string> }> => {
return (await api.POST("/admin/users", data)) as {
data: UserShort[];
error: Nullable<string>;
};
};
export interface BlockUserRequest {
action: string;
}
export interface ListUsersRequest {
page: number;
pageSize: number;
}
//
// package responses
//
export interface SimpleResponse {
message: string;
}
//
// package systemUtils
//
// Typescript: TSEndpoint= path=/health; name=health; method=GET; response=string
// internal/systemUtils/routes.go Line: 34
export const health = async (): Promise<{
data: string;
error: Nullable<string>;
}> => {
return (await api.GET("/health")) as {
data: string;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/metrics; name=metrics; method=GET; response=string
// internal/systemUtils/routes.go Line: 37
export const metrics = async (): Promise<{
data: string;
error: Nullable<string>;
}> => {
return (await api.GET("/metrics")) as {
data: string;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/maildebug; name=mailDebug; method=GET; response=routes.[]MailDebugItem
// internal/systemUtils/routes.go Line: 48
export const mailDebug = async (): Promise<{
data: MailDebugItem[];
error: Nullable<string>;
}> => {
return (await api.GET("/maildebug")) as {
data: MailDebugItem[];
error: Nullable<string>;
};
};
export interface MailDebugItem {
name: string;
content: string;
}
//
// package routes
//
export interface FormRequest {
req: string;
count: number;
}
export interface FormResponse {
test: string;
}
//
// package auth
//
// Typescript: TSEndpoint= path=/auth/password/reset; name=resetPassword; method=POST; request=model.ResetPasswordRequest; response=controllers.SimpleResponse
// internal/auth/routes.go Line: 32
export const resetPassword = async (
data: ResetPasswordRequest,
): Promise<{ data: SimpleResponse; error: Nullable<string> }> => {
return (await api.POST("/auth/password/reset", data)) as {
data: SimpleResponse;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/auth/password/valid; name=validToken; method=POST; request=string; response=controllers.SimpleResponse
// internal/auth/routes.go Line: 35
export const validToken = async (
data: string,
): Promise<{ data: SimpleResponse; error: Nullable<string> }> => {
return (await api.POST("/auth/password/valid", data)) as {
data: SimpleResponse;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/auth/login; name=login; method=POST; request=model.LoginRequest; response=model.TokenPair
// internal/auth/routes.go Line: 20
export const login = async (
data: LoginRequest,
): Promise<{ data: TokenPair; error: Nullable<string> }> => {
return (await api.POST("/auth/login", data)) as {
data: TokenPair;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/auth/refresh; name=refresh; method=POST; request=model.RefreshRequest; response=model.TokenPair
// internal/auth/routes.go Line: 23
export const refresh = async (
data: RefreshRequest,
): Promise<{ data: TokenPair; error: Nullable<string> }> => {
return (await api.POST("/auth/refresh", data)) as {
data: TokenPair;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/auth/register; name=register; method=POST; request=models.UserCreateInput; response=models.UserShort
// internal/auth/routes.go Line: 26
export const register = async (
data: UserCreateInput,
): Promise<{ data: UserShort; error: Nullable<string> }> => {
return (await api.POST("/auth/register", data)) as {
data: UserShort;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/auth/password/forgot; name=forgotPassword; method=POST; request=model.ForgotPasswordRequest; response=controllers.SimpleResponse
// internal/auth/routes.go Line: 29
export const forgotPassword = async (
data: ForgotPasswordRequest,
): Promise<{ data: SimpleResponse; error: Nullable<string> }> => {
return (await api.POST("/auth/password/forgot", data)) as {
data: SimpleResponse;
error: Nullable<string>;
};
};
export interface TokenPair {
access_token: string;
refresh_token: string;
}
export interface LoginRequest {
username: string;
password: string;
}
export interface ForgotPasswordRequest {
email: string;
}
export interface RefreshRequest {
refresh_token: string;
}
export interface ResetPasswordRequest {
token: string;
password: string;
}

View File

@ -0,0 +1,3 @@
export interface SimpleResponse {
message: string;
}

View File

@ -0,0 +1,43 @@
import { api } from "./api.ts";
import { Nullable } from "./apiTypes.ts";
// Typescript: TSEndpoint= path=/metrics; name=metrics; method=GET; response=string
// internal/systemUtils/routes.go Line: 41
export const metrics = async (): Promise<{
data: string;
error: Nullable<string>;
}> => {
return (await api.GET("/metrics")) as {
data: string;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/maildebug; name=mailDebug; method=GET; response=[]MailDebugItem
// internal/systemUtils/routes.go Line: 53
export const mailDebug = async (): Promise<{
data: MailDebugItem[];
error: Nullable<string>;
}> => {
return (await api.GET("/maildebug")) as {
data: MailDebugItem[];
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/health; name=health; method=GET; response=string
// internal/systemUtils/routes.go Line: 38
export const health = async (): Promise<{
data: string;
error: Nullable<string>;
}> => {
return (await api.GET("/health")) as {
data: string;
error: Nullable<string>;
};
};
export interface MailDebugItem {
name: string;
content: string;
}

View File

@ -0,0 +1,4 @@
export interface TokenPair {
access_token: string;
refresh_token: string;
}

View File

@ -0,0 +1,239 @@
import { api } from "./api.ts";
import { Nullable } from "./apiTypes.ts";
import * as responses from "./responses.ts";
import * as tokens from "./tokens.ts";
// Typescript: TSEndpoint= path=/auth/refresh; name=refresh; method=POST; request=users.RefreshRequest; response=tokens.TokenPair
// internal/user/routes.go Line: 46
export const refresh = async (
data: RefreshRequest,
): Promise<{ data: tokens.TokenPair; error: Nullable<string> }> => {
return (await api.POST("/auth/refresh", data)) as {
data: tokens.TokenPair;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/auth/password/reset; name=resetPassword; method=POST; request=users.ResetPasswordRequest; response=responses.SimpleResponse
// internal/user/routes.go Line: 55
export const resetPassword = async (
data: ResetPasswordRequest,
): Promise<{ data: responses.SimpleResponse; error: Nullable<string> }> => {
return (await api.POST("/auth/password/reset", data)) as {
data: responses.SimpleResponse;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/users/:uuid; name=getUser; method=GET; response=users.User
// internal/user/routes.go Line: 27
export const getUser = async (
uuid: string,
): Promise<{ data: User; error: Nullable<string> }> => {
return (await api.GET(`/users/${uuid}`)) as {
data: User;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/auth/me; name=me; method=GET; response=users.User
// internal/user/routes.go Line: 39
export const me = async (): Promise<{
data: User;
error: Nullable<string>;
}> => {
return (await api.GET("/auth/me")) as { data: User; error: Nullable<string> };
};
// Typescript: TSEndpoint= path=/auth/register; name=register; method=POST; request=users.UserCreateInput; response=users.User
// internal/user/routes.go Line: 49
export const register = async (
data: UserCreateInput,
): Promise<{ data: User; error: Nullable<string> }> => {
return (await api.POST("/auth/register", data)) as {
data: User;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/auth/password/forgot; name=forgotPassword; method=POST; request=users.ForgotPasswordRequest; response=responses.SimpleResponse
// internal/user/routes.go Line: 52
export const forgotPassword = async (
data: ForgotPasswordRequest,
): Promise<{ data: responses.SimpleResponse; error: Nullable<string> }> => {
return (await api.POST("/auth/password/forgot", data)) as {
data: responses.SimpleResponse;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/users/:uuid; name=updateUser; method=PUT; request=users.UpdateUserRequest; response=users.User
// internal/user/routes.go Line: 33
export const updateUser = async (
data: UpdateUserRequest,
): Promise<{ data: User; error: Nullable<string> }> => {
return (await api.PUT("/users/:uuid", data)) as {
data: User;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/users/:uuid; name=deleteUser; method=DELETE; response=responses.SimpleResponse
// internal/user/routes.go Line: 36
export const deleteUser = async (
uuid: string,
): Promise<{ data: responses.SimpleResponse; error: Nullable<string> }> => {
return (await api.DELETE(`/users/${uuid}`)) as {
data: responses.SimpleResponse;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/auth/login; name=login; method=POST; request=users.LoginRequest; response=tokens.TokenPair
// internal/user/routes.go Line: 43
export const login = async (
data: LoginRequest,
): Promise<{ data: tokens.TokenPair; error: Nullable<string> }> => {
return (await api.POST("/auth/login", data)) as {
data: tokens.TokenPair;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/auth/password/valid; name=validToken; method=POST; request=string; response=responses.SimpleResponse
// internal/user/routes.go Line: 58
export const validToken = async (
data: string,
): Promise<{ data: responses.SimpleResponse; error: Nullable<string> }> => {
return (await api.POST("/auth/password/valid", data)) as {
data: responses.SimpleResponse;
error: Nullable<string>;
};
};
// Typescript: TSEndpoint= path=/users; name=createUser; method=POST; request=users.UserCreateInput; response=users.User
// internal/user/routes.go Line: 30
export const createUser = async (
data: UserCreateInput,
): Promise<{ data: User; error: Nullable<string> }> => {
return (await api.POST("/users", data)) as {
data: User;
error: Nullable<string>;
};
};
export interface RefreshRequest {
refresh_token: string;
}
export interface User {
id: number;
email: string;
name: string;
roles: UserRoles;
types: UserTypes;
status: UserStatus;
activatedAt: Date;
uuid: string;
details: Nullable<UserDetails>;
preferences: Nullable<UserPreferences>;
avatar: Nullable<string>;
createdAt: Date;
updatedAt: Date;
}
export interface UserCreateInput {
name: string;
email: string;
password: string;
roles: UserRoles;
status: UserStatus;
types: UserTypes;
avatar: Nullable<string>;
details: Nullable<UserDetails>;
preferences: Nullable<UserPreferences>;
}
export interface UserPreferences {
id: number;
userId: number;
useIdle: boolean;
idleTimeout: number;
useIdlePassword: boolean;
idlePin: string;
useDirectLogin: boolean;
useQuadcodeLogin: boolean;
sendNoticesMail: boolean;
language: string;
createdAt: Date;
updatedAt: Date;
}
export interface ForgotPasswordRequest {
email: string;
}
export interface UpdateUserRequest {
name: string;
email: string;
password: string;
roles: UserRoles;
status: UserStatus;
types: UserTypes;
avatar: Nullable<string>;
details: Nullable<UserDetails>;
preferences: Nullable<UserPreferences>;
}
export interface LoginRequest {
username: string;
password: string;
}
export interface UserDetails {
id: number;
userId: number;
title: string;
firstName: string;
lastName: string;
address: string;
city: string;
zipCode: string;
country: string;
phone: string;
createdAt: Date;
updatedAt: Date;
}
export interface UserProfile {
id: number;
email: string;
name: string;
roles: UserRoles;
types: UserTypes;
status: UserStatus;
activatedAt: Date;
uuid: string;
details: Nullable<UserDetails>;
preferences: Nullable<UserPreferences>;
avatar: Nullable<string>;
createdAt: Date;
updatedAt: Date;
}
export interface ResetPasswordRequest {
token: string;
password: string;
}
export type UserRoles = string[];
export type UserTypes = string[];
export type UserStatus = (typeof EnumUserStatus)[keyof typeof EnumUserStatus];
export const EnumUserStatus = {
UserStatusPending: "pending",
UserStatusActive: "active",
UserStatusDisabled: "disabled",
} as const;

View File

@ -62,7 +62,7 @@ func main() {
IdleTimeout: time.Duration(cfg.IdleTimeoutSeconds) * time.Second, IdleTimeout: time.Duration(cfg.IdleTimeoutSeconds) * time.Second,
ErrorHandler: func(c fiber.Ctx, err error) error { ErrorHandler: func(c fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError code := fiber.StatusInternalServerError
msg := "internal server error" msg := "internal server error: " + err.Error()
if e, ok := err.(*fiber.Error); ok { if e, ok := err.(*fiber.Error); ok {
code = e.Code code = e.Code
msg = e.Message msg = e.Message
@ -70,7 +70,7 @@ func main() {
reqID := requestid.FromContext(c) reqID := requestid.FromContext(c)
log.Printf("error request_id=%s status=%d method=%s path=%s ip=%s ua=%q err=%v", reqID, code, c.Method(), c.Path(), c.IP(), c.Get("User-Agent"), err) log.Printf("error request_id=%s status=%d method=%s path=%s ip=%s ua=%q err=%v", reqID, code, c.Method(), c.Path(), c.IP(), c.Get("User-Agent"), err)
if code >= 500 { if code >= 500 {
msg = "internal server error" msg = "internal server error: " + err.Error()
} }
return c.Status(code).JSON(fiber.Map{"data": nil, "error": msg}) return c.Status(code).JSON(fiber.Map{"data": nil, "error": msg})
}, },

View File

@ -9,11 +9,11 @@ import (
func RegisterAdminRoutes(app *fiber.App) { func RegisterAdminRoutes(app *fiber.App) {
adminController := NewAdminController() adminController := NewAdminController()
// Typescript: TSEndpoint= path=/admin/users; name=listUsers; method=POST; request=admin.ListUsersRequest; response=models.[]UserShort // Typescript: TSEndpoint= path=/admin/users; name=listUsers; method=POST; request=admin.ListUsersRequest; response=users.[]User
app.Post("/admin/users", adminController.ListUsers) app.Post("/admin/users", adminController.ListUsers)
roles.RegisterEndpoint("POST/admin/users", int(roles.AdminPermission)) roles.RegisterEndpoint("POST/admin/users", int(roles.AdminPermission))
// Typescript: TSEndpoint= path=/admin/users/:uuid/block; name=blockUser; method=PUT; request=admin.BlockUserRequest; response=models.UserShort // Typescript: TSEndpoint= path=/admin/users/:uuid/block; name=blockUser; method=PUT; request=admin.BlockUserRequest; response=users.User
app.Put("/admin/users/:uuid/block", adminController.BlockUser) app.Put("/admin/users/:uuid/block", adminController.BlockUser)
roles.RegisterEndpoint("PUT/admin/users/:uuid/block", int(roles.AdminPermission)) roles.RegisterEndpoint("PUT/admin/users/:uuid/block", int(roles.AdminPermission))
} }

View File

@ -0,0 +1 @@
ciao

View File

@ -31,6 +31,10 @@ func healthHandler(c fiber.Ctx) error {
} }
func RegisterSystemRoutes(app *fiber.App) { func RegisterSystemRoutes(app *fiber.App) {
app.Get("/favicon.ico", func(c fiber.Ctx) error {
return c.SendString("boo")
})
// Typescript: TSEndpoint= path=/health; name=health; method=GET; response=string // Typescript: TSEndpoint= path=/health; name=health; method=GET; response=string
app.Get("/health", healthHandler) app.Get("/health", healthHandler)
@ -42,10 +46,11 @@ func RegisterSystemRoutes(app *fiber.App) {
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString(err.Error()) return c.Status(fiber.StatusInternalServerError).SendString(err.Error())
} }
c.Set(fiber.HeaderContentType, "text/plain")
return c.SendString(ts) return c.SendString(ts)
}) })
// Typescript: TSEndpoint= path=/maildebug; name=mailDebug; method=GET; response=routes.[]MailDebugItem // Typescript: TSEndpoint= path=/maildebug; name=mailDebug; method=GET; response=[]MailDebugItem
app.Get("/maildebug", func(c fiber.Ctx) error { app.Get("/maildebug", func(c fiber.Ctx) error {
entries, err := os.ReadDir("data/mail-debug") entries, err := os.ReadDir("data/mail-debug")
if err != nil { if err != nil {
@ -90,5 +95,6 @@ func RegisterSystemRoutes(app *fiber.App) {
app.Get("/", func(c fiber.Ctx) error { app.Get("/", func(c fiber.Ctx) error {
return c.SendFile(filepath.Join(spaDistPath, "index.html")) return c.SendFile(filepath.Join(spaDistPath, "index.html"))
}) })
app.Use("/", static.New(spaDistPath)) app.Use("/", static.New(spaDistPath))
} }

View File

@ -1,16 +1,16 @@
package tsgenerator package tsgenerator
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"os" "os"
"os/exec" "path/filepath"
tsrpc "server/pkg/ts-rpc" tsrpc "server/pkg/ts-rpc"
) )
func TsGenerate() (string, error) { func TsGenerate() (string, error) {
path := "GeneratedCode" path := "GeneratedCode"
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
err := os.Mkdir(path, os.ModePerm) err := os.Mkdir(path, os.ModePerm)
if err != nil { if err != nil {
@ -18,53 +18,26 @@ func TsGenerate() (string, error) {
} }
} }
var tsSource = tsrpc.GetTSSource(tsrpc.TSConfig{Url: "http://localhost:3000", TsApi: nil, Path: "."}) d, err := os.Open(path)
formatted, err1 := formatJS(tsSource)
if err1 != nil {
return "", err1
}
err := os.WriteFile("GeneratedCode/generatedTypescript.ts", []byte(formatted), 0644)
if err != nil { if err != nil {
return "", fmt.Errorf("write local generated typescript: %w", err) return "", fmt.Errorf("open GeneratedCode directory: %w", err)
} }
defer d.Close()
frontendAPIPath := os.Getenv("FRONTEND_API_PATH") names, err := d.Readdirnames(-1)
if frontendAPIPath == "" {
return "", errors.New("FRONTEND_API_PATH must be set")
}
fmt.Printf("Saving generated TypeScript to %s\n", frontendAPIPath)
err = os.WriteFile(frontendAPIPath+"/api.ts", []byte(formatted), 0644)
if err != nil { if err != nil {
return "", fmt.Errorf("write frontend api.ts: %w", err) return "", fmt.Errorf("read GeneratedCode directory: %w", err)
}
for _, name := range names {
err = os.RemoveAll(filepath.Join(path, name))
if err != nil {
return "", fmt.Errorf("remove GeneratedCode directory content: %w", err)
}
} }
return formatted, nil err = tsrpc.GetTSSource()
} if err != nil {
return "", fmt.Errorf("get ts source: %w", err)
func formatJS(src string) (string, error) { }
// Se hai prettier globale:
cmd := exec.Command("prettier", "--parser", "typescript", "--use-tabs", "false", "--tab-width", "2") return "Generation OK \n\n", nil
// Se hai prettier solo come devDependency:
// cmd := exec.Command("npx", "prettier", "--parser", "typescript")
cmd.Stdin = bytes.NewBufferString(src)
var out bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
// Stampa anche lo stderr di Prettier, così vedi lerrore reale
return "", fmt.Errorf("prettier error: %v\nstderr: %s", err, stderr.String())
}
return out.String(), nil
} }

View File

@ -11,11 +11,12 @@ import (
// Typescript: type // Typescript: type
type UserRoles []string type UserRoles []string
// Typescript: interface
type User struct { type User struct {
ID int `json:"id" gorm:"primaryKey"` ID int `json:"id" gorm:"primaryKey"`
Email string `json:"email" gorm:"uniqueIndex;size:255"` Email string `json:"email" gorm:"uniqueIndex;size:255"`
Name string `json:"name" gorm:"size:255"` Name string `json:"name" gorm:"size:255"`
Password string `json:"password" gorm:"size:255"` Password string `json:"-" gorm:"size:255"`
Roles UserRoles `json:"roles" gorm:"type:text;serializer:json"` Roles UserRoles `json:"roles" gorm:"type:text;serializer:json"`
Types UserTypes `json:"types" gorm:"type:text;serializer:json"` Types UserTypes `json:"types" gorm:"type:text;serializer:json"`
Status UserStatus `json:"status" gorm:"type:text;default:'pending'"` Status UserStatus `json:"status" gorm:"type:text;default:'pending'"`
@ -48,7 +49,7 @@ type UserCreateInput struct {
type UserTypes []string type UserTypes []string
// UserProfile is the safe full representation of a user returned by CRUD endpoints. // UserProfile is the safe full representation of a user returned by CRUD endpoints.
//
// Typescript: interface // Typescript: interface
type UserProfile struct { type UserProfile struct {
ID int `json:"id"` ID int `json:"id"`
@ -89,6 +90,8 @@ func ToUserProfile(u *User) UserProfile {
} }
// UserPreferences holds per-user settings stored as JSON. // UserPreferences holds per-user settings stored as JSON.
// Typescript: interface
type UserPreferences struct { type UserPreferences struct {
ID int `json:"id" gorm:"primaryKey"` ID int `json:"id" gorm:"primaryKey"`
UserID int `json:"userId" gorm:"index"` UserID int `json:"userId" gorm:"index"`
@ -104,9 +107,9 @@ type UserPreferences struct {
UpdatedAt time.Time `json:"updatedAt" ts:"type=Date"` UpdatedAt time.Time `json:"updatedAt" ts:"type=Date"`
} }
// UserPreferences holds per-user settings stored as JSON.
// UserDetails holds optional profile data. // UserDetails holds optional profile data.
// Typescript: interface
type UserDetails struct { type UserDetails struct {
ID int `json:"id" gorm:"primaryKey"` ID int `json:"id" gorm:"primaryKey"`
UserID int `json:"userId" gorm:"index"` UserID int `json:"userId" gorm:"index"`

View File

@ -24,13 +24,13 @@ func RegisterUserRoutes(app *fiber.App) {
userController := NewUserController(tockenService) userController := NewUserController(tockenService)
// Typescript: TSEndpoint= path=/users/:uuid; name=getUser; method=GET; response=users.UserProfile // Typescript: TSEndpoint= path=/users/:uuid; name=getUser; method=GET; response=users.User
app.Get("/users/:uuid", tockenService.Middleware(), userController.GetUser) app.Get("/users/:uuid", tockenService.Middleware(), userController.GetUser)
// Typescript: TSEndpoint= path=/users; name=createUser; method=POST; request=users.UserCreateInput; response=users.UserProfile // Typescript: TSEndpoint= path=/users; name=createUser; method=POST; request=users.UserCreateInput; response=users.User
app.Post("/users", tockenService.Middleware(), userController.CreateUser) app.Post("/users", tockenService.Middleware(), userController.CreateUser)
// Typescript: TSEndpoint= path=/users/:uuid; name=updateUser; method=PUT; request=users.UpdateUserRequest; response=users.UserProfile // Typescript: TSEndpoint= path=/users/:uuid; name=updateUser; method=PUT; request=users.UpdateUserRequest; response=users.User
app.Put("/users/:uuid", tockenService.Middleware(), userController.UpdateUser) app.Put("/users/:uuid", tockenService.Middleware(), userController.UpdateUser)
// Typescript: TSEndpoint= path=/users/:uuid; name=deleteUser; method=DELETE; response=responses.SimpleResponse // Typescript: TSEndpoint= path=/users/:uuid; name=deleteUser; method=DELETE; response=responses.SimpleResponse
@ -43,7 +43,7 @@ func RegisterUserRoutes(app *fiber.App) {
// Typescript: TSEndpoint= path=/auth/login; name=login; method=POST; request=users.LoginRequest; response=tokens.TokenPair // Typescript: TSEndpoint= path=/auth/login; name=login; method=POST; request=users.LoginRequest; response=tokens.TokenPair
app.Post("/auth/login", authRateLimiter, userController.Login) app.Post("/auth/login", authRateLimiter, userController.Login)
// Typescript: TSEndpoint= path=/auth/refresh; name=refresh; method=POST; request=tokens.RefreshRequest; response=tokens.TokenPair // Typescript: TSEndpoint= path=/auth/refresh; name=refresh; method=POST; request=users.RefreshRequest; response=tokens.TokenPair
app.Post("/auth/refresh", authRateLimiter, userController.Refresh) app.Post("/auth/refresh", authRateLimiter, userController.Refresh)
// Typescript: TSEndpoint= path=/auth/register; name=register; method=POST; request=users.UserCreateInput; response=users.User // Typescript: TSEndpoint= path=/auth/register; name=register; method=POST; request=users.UserCreateInput; response=users.User

View File

@ -0,0 +1,75 @@
package tsrpc
import (
"bytes"
"fmt"
"os"
"os/exec"
)
type TSFiles map[string]string
func (t *TSFiles) Add(name string, source string) {
if t == nil || *t == nil {
*t = make(map[string]string)
}
if (*t)[name] == "" {
(*t)[name] = source
} else {
(*t)[name] += source
}
}
func (t *TSFiles) Get(name string) (string, bool) {
if t == nil || *t == nil {
return "", false
}
s, ok := (*t)[name]
return s, ok
}
func (t *TSFiles) Save() error {
if t == nil || *t == nil {
return fmt.Errorf("TS Files not initialized")
}
for k, v := range *t {
fmt.Printf("file: %s\nsource:\n%s\n\n", k, v)
}
for name, source := range *t {
formatted, err := formatJS(source)
if err != nil {
return fmt.Errorf("format JS: %w", err)
}
err = os.WriteFile(fmt.Sprintf("GeneratedCode/%s", name), []byte(formatted), 0644)
if err != nil {
return fmt.Errorf("write local generated typescript: %w", err)
}
}
return nil
}
func formatJS(src string) (string, error) {
// Se hai prettier globale:
cmd := exec.Command("prettier", "--parser", "typescript", "--use-tabs", "false", "--tab-width", "2")
// Se hai prettier solo come devDependency:
// cmd := exec.Command("npx", "prettier", "--parser", "typescript")
cmd.Stdin = bytes.NewBufferString(src)
var out bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
// Stampa anche lo stderr di Prettier, così vedi lerrore reale
return "", fmt.Errorf("prettier error: %v\nstderr: %s", err, stderr.String())
}
return out.String(), nil
}

View File

@ -3,6 +3,14 @@
package tsrpc package tsrpc
import (
"bytes"
"fmt"
"os"
"text/template"
"time"
)
const TsApiTemplate = ` const TsApiTemplate = `
// //
// Typescript API generated from gofiber backend // Typescript API generated from gofiber backend
@ -248,5 +256,39 @@ export default class Api {
} }
} }
const api = new Api('{{ .APIURL }}'); export const api = new Api('{{ .APIURL }}');
` `
type templateData struct {
APIURL string
CreatedOn time.Time
}
func GetApiFile() (string, error) {
apiurl := ""
if value, exists := os.LookupEnv("TS_GENERATOR_URL"); exists {
apiurl = value
} else {
return "", fmt.Errorf("TS Generator URL environment variable not set")
}
// API file
tsApiSource := ""
data := TsApiTemplate
tpl, err := template.New("tsRpc").Parse(data)
if err != nil {
panic(err)
}
var templateData = templateData{
APIURL: apiurl,
CreatedOn: time.Now(),
}
var result bytes.Buffer
err = tpl.Execute(&result, templateData)
if err != nil {
panic(err)
}
tsApiSource += result.String()
return tsApiSource, nil
}

View File

@ -1,4 +1,4 @@
// exportable typescript generated from golang // able typescript generated from golang
// Copyright (C) 2022 Fabio Prada // Copyright (C) 2022 Fabio Prada
package tsrpc package tsrpc
@ -6,21 +6,21 @@ package tsrpc
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"slices"
"strings" "strings"
"text/template" "text/template"
) )
type TSEndpoint struct { type TSEndpoint struct {
Name string Name string
Path string Path string
Method string Method string
Request string Request string
Response string Response string
RequestTs string Source string
ResponseTs string File string
Source string Line int
File string Imports map[string][]string
Line int
} }
func ParseEndpoint(source string, file string, line int) TSEndpoint { func ParseEndpoint(source string, file string, line int) TSEndpoint {
@ -32,6 +32,7 @@ func ParseEndpoint(source string, file string, line int) TSEndpoint {
endpoint.Source = strings.Trim(source, "\t") endpoint.Source = strings.Trim(source, "\t")
endpoint.File = file endpoint.File = file
endpoint.Line = line endpoint.Line = line
endpoint.Imports = make(map[string][]string)
for _, v := range a { for _, v := range a {
t := strings.Split(v, "=") t := strings.Split(v, "=")
@ -83,84 +84,116 @@ type tplData struct {
} }
func (e *TSEndpoint) VerifyTypes(info TSInfo, p string) { func (e *TSEndpoint) VerifyTypes(info TSInfo, p string) {
kind := ""
if e.Name == "listUsers" {
fmt.Println("endpoint request", e.Request)
}
a := strings.Split(e.Request, ".") a := strings.Split(e.Request, ".")
if e.Request != "" { if e.Request != "" {
if len(a) == 2 { if len(a) == 2 {
e.RequestTs = a[1] kind = a[1]
if strings.HasPrefix(a[1], "[]") { if strings.HasPrefix(a[1], "[]") {
a[1] = strings.TrimPrefix(a[1], "[]") kind = fmt.Sprintf("%s[]", strings.TrimPrefix(a[1], "[]"))
e.RequestTs = fmt.Sprintf("%s[]", a[1])
} }
// nullable pointer
if strings.HasPrefix(a[1], "*") { if strings.HasPrefix(a[1], "*") {
a[1] = strings.TrimPrefix(a[1], "*") kind = fmt.Sprintf("Nullable<%s>", strings.TrimPrefix(a[1], "*"))
e.RequestTs = fmt.Sprintf("Nullable<%s>", a[1])
} }
e.Request = fmt.Sprintf("%s.%s", a[0], a[1])
if !info.find(a[0], a[1]) { if !info.find(a[0], a[1]) {
exitOnError(fmt.Errorf("endpoint request not found: %s AT %s Line: %d ", e.Request, e.File, e.Line)) exitOnError(fmt.Errorf("endpoint request not found: %s AT %s Line: %d ", e.Request, e.File, e.Line))
} }
if a[0] == p {
e.Request = fmt.Sprintf("%s", kind)
} else {
e.Request = fmt.Sprintf("%s.%s", kind, a[1])
if _, ok := e.Imports[a[0]]; !ok {
e.Imports[a[0]] = []string{}
}
allreadyImported := slices.Contains(e.Imports[a[0]], strings.Trim(a[1], "[]*"))
if !allreadyImported {
e.Imports[a[0]] = append(e.Imports[a[0]], strings.Trim(a[1], "[]*"))
}
}
info.setTypescript(a[0], a[1], true) info.setTypescript(a[0], a[1], true)
} }
if len(a) == 1 { if len(a) == 1 {
e.RequestTs = a[0] kind = a[0]
if strings.HasPrefix(a[0], "[]") { if strings.HasPrefix(a[0], "[]") {
a[0] = strings.TrimPrefix(a[0], "[]") kind = fmt.Sprintf("%s[]", strings.TrimPrefix(a[0], "[]"))
e.RequestTs = fmt.Sprintf("%s[]", a[0])
} }
// nullable pointer
if strings.HasPrefix(a[0], "*") { if strings.HasPrefix(a[0], "*") {
a[0] = strings.TrimPrefix(a[0], "*") kind = fmt.Sprintf("Nullable<%s>", strings.TrimPrefix(a[0], "*"))
e.RequestTs = fmt.Sprintf("Nullable<%s>", a[0])
} }
e.Request = a[0] e.Request = fmt.Sprintf("%s", kind)
} }
// if request is a struct, set it as typescript to generate the ts struct
} }
kind = ""
a = strings.Split(e.Response, ".") a = strings.Split(e.Response, ".")
if len(a) == 2 { if len(a) == 2 {
if !info.find(a[0], a[1]) { kind = a[1]
exitOnError(fmt.Errorf("endpoint response not found: %s AT %s Line: %d ", e.Request, e.File, e.Line))
}
e.ResponseTs = a[1]
if strings.HasPrefix(a[1], "[]") { if strings.HasPrefix(a[1], "[]") {
a[1] = strings.TrimPrefix(a[1], "[]") kind = fmt.Sprintf("%s[]", strings.TrimPrefix(a[1], "[]"))
e.ResponseTs = fmt.Sprintf("%s[]", a[1])
} }
// nullable pointer
if strings.HasPrefix(a[1], "*") { if strings.HasPrefix(a[1], "*") {
a[1] = strings.TrimPrefix(a[1], "*") kind = fmt.Sprintf("Nullable<%s>", strings.TrimPrefix(a[1], "*"))
e.ResponseTs = fmt.Sprintf("Nullable<%s>", a[1])
} }
e.Response = fmt.Sprintf("%s.%s", a[0], a[1])
if !info.find(a[0], a[1]) {
exitOnError(fmt.Errorf("endpoint response not found: %s AT %s Line: %d ", e.Response, e.File, e.Line))
}
if a[0] == p {
e.Response = fmt.Sprintf("%s", kind)
} else {
e.Response = fmt.Sprintf("%s.%s", a[0], kind)
if _, ok := e.Imports[a[0]]; !ok {
e.Imports[a[0]] = []string{}
}
allreadyImported := slices.Contains(e.Imports[a[0]], strings.Trim(a[1], "[]*"))
if !allreadyImported {
e.Imports[a[0]] = append(e.Imports[a[0]], strings.Trim(a[1], "[]*"))
fmt.Printf("endpoint %s response import: %s.%s\n", e.Name, a[0], a[1])
}
}
info.setTypescript(a[0], a[1], true)
} }
if len(a) == 1 { if len(a) == 1 {
e.ResponseTs = a[0]
if strings.HasPrefix(a[0], "[]") { if strings.HasPrefix(a[0], "[]") {
a[0] = strings.TrimPrefix(a[0], "[]") kind = fmt.Sprintf("%s[]", strings.TrimPrefix(a[0], "[]"))
e.ResponseTs = fmt.Sprintf("%s[]", a[0])
} }
// nullable pointer
if strings.HasPrefix(a[0], "*") { if strings.HasPrefix(a[0], "*") {
a[0] = strings.TrimPrefix(a[0], "*") kind = fmt.Sprintf("Nullable<%s>", strings.TrimPrefix(a[0], "*"))
e.ResponseTs = fmt.Sprintf("Nullable<%s>", a[0])
} }
e.Response = a[0] if IsNativeType(a[0]) {
e.Response = fmt.Sprintf("%s%s", a[0], kind)
return
}
e.Response = fmt.Sprintf("%s", kind)
// if request is a struct, set it as typescript to generate the ts struct
} }
} }
func (e *TSEndpoint) ToTs(pkg string) string { func (e *TSEndpoint) ToTs() string {
data := tplData{E: e, Path: e.Path, Params: []string{}} data := tplData{E: e, Path: e.Path, Params: []string{}}
tpl := ` tpl := `
{{ .E.Source }} {{ .E.Source }}
// {{ .E.File }} Line: {{ .E.Line }} // {{ .E.File }} Line: {{ .E.Line }}
{{if eq .E.Method "GET"}}export const {{ .E.Name}} = async ({{range $v := .Params}}{{$v}}{{end}}):Promise<{ data:{{.E.ResponseTs}}; error: Nullable<string> }> => { {{if eq .E.Method "GET"}}export const {{ .E.Name}} = async ({{range $v := .Params}}{{$v}}{{end}}):Promise<{ data:{{.E.Response}}; error: Nullable<string> }> => {
return await api.GET({{ .Path}}) as { data: {{ .E.ResponseTs}}; error: Nullable<string> }; return await api.GET({{ .Path}}) as { data: {{ .E.Response}}; error: Nullable<string> };
}{{end}} }{{end}}{{if eq .E.Method "DELETE"}}export const {{ .E.Name}} = async ({{range $v := .Params}}{{$v}}{{end}}):Promise<{ data:{{.E.Response}}; error: Nullable<string> }> => {
{{if eq .E.Method "DELETE"}}export const {{ .E.Name}} = async ({{range $v := .Params}}{{$v}}{{end}}):Promise<{ data:{{.E.ResponseTs}}; error: Nullable<string> }> => { return await api.DELETE({{ .Path}}) as { data: {{ .E.Response}}; error: Nullable<string> };
return await api.DELETE({{ .Path}}) as { data: {{ .E.ResponseTs}}; error: Nullable<string> }; }{{end}}{{if eq .E.Method "PUT"}}export const {{ .E.Name}} = async (data: {{ .E.Request}}):Promise<{ data:{{.E.Response}}; error: Nullable<string> }> => {
}{{end}} return await api.PUT("{{ .Path}}", data) as { data: {{ .E.Response}}; error: Nullable<string> };
{{if eq .E.Method "PUT"}}export const {{ .E.Name}} = async (data: {{ .E.RequestTs}}):Promise<{ data:{{.E.ResponseTs}}; error: Nullable<string> }> => { }{{end}}{{if eq .E.Method "POST"}}export const {{ .E.Name}} = async (data: {{ .E.Request}}):Promise<{ data:{{.E.Response}}; error: Nullable<string> }> => {
return await api.PUT("{{ .Path}}", data) as { data: {{ .E.ResponseTs}}; error: Nullable<string> }; return await api.POST("{{ .Path}}", data) as { data: {{ .E.Response }}; error: Nullable<string> };
}{{end}}
{{if eq .E.Method "POST"}}export const {{ .E.Name}} = async (data: {{ .E.RequestTs}}):Promise<{ data:{{.E.ResponseTs}}; error: Nullable<string> }> => {
return await api.POST("{{ .Path}}", data) as { data: {{ .E.ResponseTs}}; error: Nullable<string> };
}{{end}}` }{{end}}`
if e.Method == "GET" || e.Method == "DELETE" { if e.Method == "GET" || e.Method == "DELETE" {
@ -177,11 +210,12 @@ func (e *TSEndpoint) ToTs(pkg string) string {
c = ", " c = ", "
} }
if prefix == ":" { switch prefix {
case ":":
f = true f = true
data.Params = append(data.Params, fmt.Sprintf("%s%s: string", c, v[1:])) data.Params = append(data.Params, fmt.Sprintf("%s%s: string", c, v[1:]))
data.Path = strings.Replace(data.Path, v, fmt.Sprintf("${%s}", v[1:]), 1) data.Path = strings.Replace(data.Path, v, fmt.Sprintf("${%s}", v[1:]), 1)
} else if prefix == "*" { case "*":
f = true f = true
data.Params = append(data.Params, fmt.Sprintf("%s%s: Nullable<string>", c, v[1:])) data.Params = append(data.Params, fmt.Sprintf("%s%s: Nullable<string>", c, v[1:]))
data.Path = strings.Replace(data.Path, v, fmt.Sprintf("${%s}", v[1:]), 1) data.Path = strings.Replace(data.Path, v, fmt.Sprintf("${%s}", v[1:]), 1)
@ -203,6 +237,5 @@ func (e *TSEndpoint) ToTs(pkg string) string {
if err != nil { if err != nil {
panic(err) panic(err)
} }
return result.String() return result.String()
} }

View File

@ -22,6 +22,8 @@ type TSInfoPakage struct {
consts map[string]TSConst consts map[string]TSConst
decs map[string]TSDec decs map[string]TSDec
endpoints map[string]TSEndpoint endpoints map[string]TSEndpoint
imports map[string][]string
isUsed bool
} }
type TSDec struct { type TSDec struct {
@ -349,8 +351,11 @@ func (i *TSInfo) Populate(path string) {
if _, ok := i.Packages[pkg].endpoints[e.Name]; ok { if _, ok := i.Packages[pkg].endpoints[e.Name]; ok {
exitOnError(fmt.Errorf("enpoint name %s allready in use: %s", e.Name, l.Source)) exitOnError(fmt.Errorf("enpoint name %s allready in use: %s", e.Name, l.Source))
} }
pkg_info := i.Packages[pkg]
i.Packages[pkg].endpoints[e.Name] = e pkg_info.endpoints[e.Name] = e
pkg_info.imports = e.Imports
pkg_info.isUsed = true
i.Packages[pkg] = pkg_info
} }
} }
@ -388,9 +393,6 @@ func (i *TSInfo) Populate(path string) {
func (ts *TSInfo) findAvailableStruct(n string) (TSStruct, bool) { func (ts *TSInfo) findAvailableStruct(n string) (TSStruct, bool) {
a := strings.Split(n, ".") a := strings.Split(n, ".")
if len(a) == 2 { if len(a) == 2 {
if n == "[]MailDebugItem" {
fmt.Printf("MailDebugItem found\n")
}
a[1] = strings.TrimPrefix(a[1], "[]") a[1] = strings.TrimPrefix(a[1], "[]")
a[1] = strings.TrimPrefix(a[1], "*") a[1] = strings.TrimPrefix(a[1], "*")
if _, ok := ts.Packages[a[0]]; ok { if _, ok := ts.Packages[a[0]]; ok {
@ -399,6 +401,15 @@ func (ts *TSInfo) findAvailableStruct(n string) (TSStruct, bool) {
} }
} }
} }
if len(a) == 1 {
n = strings.TrimPrefix(n, "[]")
n = strings.TrimPrefix(n, "*")
for p := range ts.Packages {
if _, ok := ts.Packages[p].structs[n]; ok {
return ts.Packages[p].structs[n], true
}
}
}
if n == "" || IsNativeType(n) { if n == "" || IsNativeType(n) {
return TSStruct{}, true return TSStruct{}, true
} }

View File

@ -16,8 +16,12 @@ type TSModule struct {
Consts map[string]string Consts map[string]string
GTypes map[string]string GTypes map[string]string
Endpoints map[string]string Endpoints map[string]string
Imports map[string][]string
Source string
} }
type TSOutputSources []string
type TSSouces struct { type TSSouces struct {
Pakages map[string]TSModule Pakages map[string]TSModule
Errors []string Errors []string
@ -32,6 +36,7 @@ func (ts *TSSouces) ensurePackage(p string) {
Consts: make(map[string]string), Consts: make(map[string]string),
GTypes: make(map[string]string), GTypes: make(map[string]string),
Endpoints: make(map[string]string), Endpoints: make(map[string]string),
Imports: make(map[string][]string),
} }
return return
} }
@ -214,9 +219,11 @@ func (ts *TSSouces) AddDependencies(info TSInfo, p string, s string, dependencie
func (ts *TSSouces) Populate(info TSInfo) { func (ts *TSSouces) Populate(info TSInfo) {
ts.Pakages = make(map[string]TSModule) ts.Pakages = make(map[string]TSModule)
ts.Errors = []string{} ts.Errors = []string{}
for p, _ := range info.Packages { for p := range info.Packages {
ts.ensurePackage(p) ts.ensurePackage(p)
ts.Errors = append(ts.Errors, fmt.Sprintf("Process pakage %s\n", p))
for _, st := range info.Packages[p].structs { for _, st := range info.Packages[p].structs {
if st.Typescript { if st.Typescript {
if len(st.Fields) == 0 { if len(st.Fields) == 0 {
@ -228,6 +235,8 @@ func (ts *TSSouces) Populate(info TSInfo) {
} }
ts.Pakages[p].Structs[st.Name] = s ts.Pakages[p].Structs[st.Name] = s
ts.AddDependencies(info, p, st.Name, dependencies) ts.AddDependencies(info, p, st.Name, dependencies)
// check depenndecies
} }
} }
@ -248,38 +257,56 @@ func (ts *TSSouces) Populate(info TSInfo) {
ts.Pakages[p].Types[t.Name] = fmt.Sprintf("export type %s = %s\n", t.Name, t.TsType) ts.Pakages[p].Types[t.Name] = fmt.Sprintf("export type %s = %s\n", t.Name, t.TsType)
} }
} }
if len(info.Packages[p].endpoints) > 0 {
for _, e := range info.Packages[p].endpoints {
/* if e.Request != "" { for _, e := range info.Packages[p].endpoints {
a := strings.Split(e.Request, ".")
if len(a) == 2 { /* if e.Request != "" {
s, d, err := structToTs(info, a[0], a[1]) a := strings.Split(e.Request, ".")
if err != nil { if len(a) == 2 {
ts.Errors = append(ts.Errors, err.Error()) s, d, err := structToTs(info, a[0], a[1])
} if err != nil {
fmt.Println(s, d) ts.Errors = append(ts.Errors, err.Error())
} }
fmt.Println(s, d)
} }
if e.Response != "" {
a := strings.Split(e.Response, ".")
if len(a) == 2 {
s, d, err := structToTs(info, a[0], a[1])
if err != nil {
ts.Errors = append(ts.Errors, err.Error())
}
fmt.Println(s, d)
}
} */
e.VerifyTypes(info, p)
endpoint := e.ToTs(p)
ts.Pakages[p].Endpoints[e.Name] = endpoint
} }
if e.Response != "" {
a := strings.Split(e.Response, ".")
if len(a) == 2 {
s, d, err := structToTs(info, a[0], a[1])
if err != nil {
ts.Errors = append(ts.Errors, err.Error())
}
fmt.Println(s, d)
}
} */
responseAndRequest := ""
if e.Request != "" {
responseAndRequest += fmt.Sprintf(" request: %s ", e.Request)
}
if e.Response != "" {
responseAndRequest += fmt.Sprintf(" response: %s ", e.Response)
}
// TSReport += fmt.Sprintf("verify %s %s%s %s\n", e.Name, e.Method, e.Path, responseAndRequest)
e.VerifyTypes(info, p)
pkg := ts.Pakages[p]
for k, v := range e.Imports {
if _, ok := pkg.Imports[k]; !ok {
pkg.Imports[k] = v
}
}
pkg.Endpoints[e.Name] = e.ToTs()
ts.Pakages[p] = pkg
if p == "users" {
fmt.Printf("endpoint %s imports: %v\n", e.Name, pkg.Imports)
}
} }
} }
} }

View File

@ -4,34 +4,29 @@
package tsrpc package tsrpc
import ( import (
"bytes" "strings"
"fmt" "fmt"
"os" "os"
"text/template"
"time"
) )
// configuration // configuration
type TSConfig struct { var TSReport = ""
Url string
TsApi *string
Path string
}
type tsTemplateData struct { var tsFiles = TSFiles{}
APIURL string
CreatedOn time.Time
}
var tsConfig TSConfig func GetTSSource() error {
path := ""
if value, exists := os.LookupEnv("TS_GENERATOR_PATH"); exists {
path = value
} else {
return fmt.Errorf("TS Generator PATH environment variable not set")
}
func GetTSSource(config TSConfig) string {
tsConfig = config
var tsInfoData = TSInfo{} var tsInfoData = TSInfo{}
var tsSourcesData = TSSouces{} var tsSourcesData = TSSouces{}
tsInfoData.Populate(tsConfig.Path) tsInfoData.Populate(path)
tsInfoData.TestEndpoints() tsInfoData.TestEndpoints()
tsSourcesData.Populate(tsInfoData) tsSourcesData.Populate(tsInfoData)
@ -43,43 +38,33 @@ func GetTSSource(config TSConfig) string {
exitOnError(fmt.Errorf("some errors...\n %s", err)) exitOnError(fmt.Errorf("some errors...\n %s", err))
} }
tsSource := "" // api file
data := "" tsApi, err := GetApiFile()
if tsConfig.TsApi == nil {
data = TsApiTemplate
} else {
d, err := os.ReadFile(*tsConfig.TsApi)
if err != nil {
panic(err)
}
data = string(d)
}
t, err := template.New("tsRpc").Parse(data)
if err != nil { if err != nil {
panic(err) exitOnError(fmt.Errorf("some errors...\n %s", err))
} }
var templateData = tsTemplateData{ tsFiles.Add("api.ts", tsApi)
APIURL: config.Url,
CreatedOn: time.Now(),
}
var result bytes.Buffer
err = t.Execute(&result, templateData)
if err != nil {
panic(err)
}
tsSource += result.String()
tsSource += fmt.Sprintln("\n// Global Declarations ") // index file
tsApiDeclarations := []string{}
tsIndexSource := ""
tsIndexSource += fmt.Sprintln("\n// API Declarations ")
for p := range tsSourcesData.Pakages { for p := range tsSourcesData.Pakages {
for _, v1 := range tsSourcesData.Pakages[p].GTypes { for _, v1 := range tsSourcesData.Pakages[p].GTypes {
tsSource += fmt.Sprintln(v1) tsIndexSource += fmt.Sprintln(v1)
}
for _, v1 := range tsInfoData.Packages[p].decs {
tsApiDeclarations = append(tsApiDeclarations, v1.Name[:strings.Index(v1.Name, "<")])
} }
} }
tsFiles.Add("apiTypes.ts", tsIndexSource)
for p := range tsSourcesData.Pakages { for p := range tsSourcesData.Pakages {
if p == "users" {
fmt.Println("users package")
}
source := "" source := ""
head := fmt.Sprintf("\n//\n// package %s\n//\n", p)
for _, v1 := range tsSourcesData.Pakages[p].Endpoints { for _, v1 := range tsSourcesData.Pakages[p].Endpoints {
source += fmt.Sprintln(v1) source += fmt.Sprintln(v1)
} }
@ -95,10 +80,59 @@ func GetTSSource(config TSConfig) string {
for _, v1 := range tsSourcesData.Pakages[p].Consts { for _, v1 := range tsSourcesData.Pakages[p].Consts {
source += fmt.Sprintln(v1) source += fmt.Sprintln(v1)
} }
if len(source) > 0 { if len(source) > 0 {
tsSource += head + source + "\n\n" tmp := ""
found := false
for _, sentence := range strings.Split(source, "\n") {
if strings.Contains(sentence, "api.") {
found = true
}
}
if found {
tmp += "import { api } from './api.ts'\n"
}
if len(tsApiDeclarations) > 0 {
decs := []string{}
for _, d := range tsApiDeclarations {
found := false
for _, sentence := range strings.Split(source, "\n") {
if strings.Contains(sentence, d) {
found = true
}
}
if found {
decs = append(decs, d)
}
}
if len(decs) > 0 {
TSApiDeclarations := "import { "
for _, d := range decs {
TSApiDeclarations += d + ", "
}
TSApiDeclarations = TSApiDeclarations[:len(TSApiDeclarations)-2]
TSApiDeclarations += " } from './apiTypes.ts'\n"
fmt.Println("tsApiDeclarations", TSApiDeclarations)
tmp += TSApiDeclarations
}
}
imports := ""
for f := range tsSourcesData.Pakages[p].Imports {
imports += "import * as " + f + " from './" + f + ".ts'\n"
}
tmp += imports
tmp += source
tsFiles.Add(p+".ts", tmp)
} }
} }
return tsSource
err = tsFiles.Save()
if err != nil {
fmt.Printf("save ts files: %s\n", err)
}
return err
} }