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 {
return formatted, nil err = os.RemoveAll(filepath.Join(path, name))
}
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 { if err != nil {
// Stampa anche lo stderr di Prettier, così vedi lerrore reale return "", fmt.Errorf("remove GeneratedCode directory content: %w", err)
return "", fmt.Errorf("prettier error: %v\nstderr: %s", err, stderr.String()) }
} }
return out.String(), nil err = tsrpc.GetTSSource()
if err != nil {
return "", fmt.Errorf("get ts source: %w", err)
}
return "Generation OK \n\n", 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,6 +6,7 @@ package tsrpc
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"slices"
"strings" "strings"
"text/template" "text/template"
) )
@ -16,11 +17,10 @@ type TSEndpoint struct {
Method string Method string
Request string Request string
Response string Response string
RequestTs string
ResponseTs string
Source string Source string
File string File string
Line int Line int
Imports map[string][]string
} }
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]) }
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
} }
e.Response = a[0]
} }
} func (e *TSEndpoint) ToTs() string {
func (e *TSEndpoint) ToTs(pkg string) 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,7 +257,7 @@ 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 { for _, e := range info.Packages[p].endpoints {
/* if e.Request != "" { /* if e.Request != "" {
@ -274,12 +283,30 @@ func (ts *TSSouces) Populate(info TSInfo) {
} }
} */ } */
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) 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)
}
endpoint := e.ToTs(p)
ts.Pakages[p].Endpoints[e.Name] = endpoint
}
} }
} }
} }

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 var tsFiles = TSFiles{}
Path string
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")
} }
type tsTemplateData struct {
APIURL string
CreatedOn time.Time
}
var tsConfig TSConfig
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 { if err != nil {
panic(err) exitOnError(fmt.Errorf("some errors...\n %s", err))
}
data = string(d)
} }
tsFiles.Add("api.ts", tsApi)
t, err := template.New("tsRpc").Parse(data) // index file
if err != nil { tsApiDeclarations := []string{}
panic(err) tsIndexSource := ""
} tsIndexSource += fmt.Sprintln("\n// API Declarations ")
var templateData = tsTemplateData{
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 ")
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
} }
} }
return tsSource 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)
}
}
err = tsFiles.Save()
if err != nil {
fmt.Printf("save ts files: %s\n", err)
}
return err
} }