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_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,
ErrorHandler: func(c fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError
msg := "internal server error"
msg := "internal server error: " + err.Error()
if e, ok := err.(*fiber.Error); ok {
code = e.Code
msg = e.Message
@ -70,7 +70,7 @@ func main() {
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)
if code >= 500 {
msg = "internal server error"
msg = "internal server error: " + err.Error()
}
return c.Status(code).JSON(fiber.Map{"data": nil, "error": msg})
},

View File

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

View File

@ -1,16 +1,16 @@
package tsgenerator
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
tsrpc "server/pkg/ts-rpc"
)
func TsGenerate() (string, error) {
path := "GeneratedCode"
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
err := os.Mkdir(path, os.ModePerm)
if err != nil {
@ -18,53 +18,26 @@ func TsGenerate() (string, error) {
}
}
var tsSource = tsrpc.GetTSSource(tsrpc.TSConfig{Url: "http://localhost:3000", TsApi: nil, Path: "."})
formatted, err1 := formatJS(tsSource)
if err1 != nil {
return "", err1
}
err := os.WriteFile("GeneratedCode/generatedTypescript.ts", []byte(formatted), 0644)
d, err := os.Open(path)
if err != nil {
return "", fmt.Errorf("write local generated typescript: %w", err)
return "", fmt.Errorf("open GeneratedCode directory: %w", err)
}
frontendAPIPath := os.Getenv("FRONTEND_API_PATH")
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)
defer d.Close()
names, err := d.Readdirnames(-1)
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
}
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
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
type UserRoles []string
// Typescript: interface
type User struct {
ID int `json:"id" gorm:"primaryKey"`
Email string `json:"email" gorm:"uniqueIndex;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"`
Types UserTypes `json:"types" gorm:"type:text;serializer:json"`
Status UserStatus `json:"status" gorm:"type:text;default:'pending'"`
@ -48,7 +49,7 @@ type UserCreateInput struct {
type UserTypes []string
// UserProfile is the safe full representation of a user returned by CRUD endpoints.
//
// Typescript: interface
type UserProfile struct {
ID int `json:"id"`
@ -89,6 +90,8 @@ func ToUserProfile(u *User) UserProfile {
}
// UserPreferences holds per-user settings stored as JSON.
// Typescript: interface
type UserPreferences struct {
ID int `json:"id" gorm:"primaryKey"`
UserID int `json:"userId" gorm:"index"`
@ -104,9 +107,9 @@ type UserPreferences struct {
UpdatedAt time.Time `json:"updatedAt" ts:"type=Date"`
}
// UserPreferences holds per-user settings stored as JSON.
// UserDetails holds optional profile data.
// Typescript: interface
type UserDetails struct {
ID int `json:"id" gorm:"primaryKey"`
UserID int `json:"userId" gorm:"index"`

View File

@ -24,13 +24,13 @@ func RegisterUserRoutes(app *fiber.App) {
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)
// 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)
// 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)
// 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
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)
// 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
import (
"bytes"
"fmt"
"os"
"text/template"
"time"
)
const TsApiTemplate = `
//
// 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
package tsrpc
@ -6,21 +6,21 @@ package tsrpc
import (
"bytes"
"fmt"
"slices"
"strings"
"text/template"
)
type TSEndpoint struct {
Name string
Path string
Method string
Request string
Response string
RequestTs string
ResponseTs string
Source string
File string
Line int
Name string
Path string
Method string
Request string
Response string
Source string
File string
Line int
Imports map[string][]string
}
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.File = file
endpoint.Line = line
endpoint.Imports = make(map[string][]string)
for _, v := range a {
t := strings.Split(v, "=")
@ -83,84 +84,116 @@ type tplData struct {
}
func (e *TSEndpoint) VerifyTypes(info TSInfo, p string) {
kind := ""
if e.Name == "listUsers" {
fmt.Println("endpoint request", e.Request)
}
a := strings.Split(e.Request, ".")
if e.Request != "" {
if len(a) == 2 {
e.RequestTs = a[1]
kind = a[1]
if strings.HasPrefix(a[1], "[]") {
a[1] = strings.TrimPrefix(a[1], "[]")
e.RequestTs = fmt.Sprintf("%s[]", a[1])
kind = fmt.Sprintf("%s[]", strings.TrimPrefix(a[1], "[]"))
}
// nullable pointer
if strings.HasPrefix(a[1], "*") {
a[1] = strings.TrimPrefix(a[1], "*")
e.RequestTs = fmt.Sprintf("Nullable<%s>", a[1])
kind = fmt.Sprintf("Nullable<%s>", strings.TrimPrefix(a[1], "*"))
}
e.Request = fmt.Sprintf("%s.%s", 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))
}
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)
}
if len(a) == 1 {
e.RequestTs = a[0]
kind = a[0]
if strings.HasPrefix(a[0], "[]") {
a[0] = strings.TrimPrefix(a[0], "[]")
e.RequestTs = fmt.Sprintf("%s[]", a[0])
kind = fmt.Sprintf("%s[]", strings.TrimPrefix(a[0], "[]"))
}
// nullable pointer
if strings.HasPrefix(a[0], "*") {
a[0] = strings.TrimPrefix(a[0], "*")
e.RequestTs = fmt.Sprintf("Nullable<%s>", a[0])
kind = fmt.Sprintf("Nullable<%s>", strings.TrimPrefix(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, ".")
if len(a) == 2 {
if !info.find(a[0], a[1]) {
exitOnError(fmt.Errorf("endpoint response not found: %s AT %s Line: %d ", e.Request, e.File, e.Line))
}
e.ResponseTs = a[1]
kind = a[1]
if strings.HasPrefix(a[1], "[]") {
a[1] = strings.TrimPrefix(a[1], "[]")
e.ResponseTs = fmt.Sprintf("%s[]", a[1])
kind = fmt.Sprintf("%s[]", strings.TrimPrefix(a[1], "[]"))
}
// nullable pointer
if strings.HasPrefix(a[1], "*") {
a[1] = strings.TrimPrefix(a[1], "*")
e.ResponseTs = fmt.Sprintf("Nullable<%s>", a[1])
kind = fmt.Sprintf("Nullable<%s>", strings.TrimPrefix(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 {
e.ResponseTs = a[0]
if strings.HasPrefix(a[0], "[]") {
a[0] = strings.TrimPrefix(a[0], "[]")
e.ResponseTs = fmt.Sprintf("%s[]", a[0])
kind = fmt.Sprintf("%s[]", strings.TrimPrefix(a[0], "[]"))
}
// nullable pointer
if strings.HasPrefix(a[0], "*") {
a[0] = strings.TrimPrefix(a[0], "*")
e.ResponseTs = fmt.Sprintf("Nullable<%s>", a[0])
kind = fmt.Sprintf("Nullable<%s>", strings.TrimPrefix(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{}}
tpl := `
{{ .E.Source }}
// {{ .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> }> => {
return await api.GET({{ .Path}}) as { data: {{ .E.ResponseTs}}; error: Nullable<string> };
}{{end}}
{{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.ResponseTs}}; error: Nullable<string> };
}{{end}}
{{if eq .E.Method "PUT"}}export const {{ .E.Name}} = async (data: {{ .E.RequestTs}}):Promise<{ data:{{.E.ResponseTs}}; error: Nullable<string> }> => {
return await api.PUT("{{ .Path}}", data) as { data: {{ .E.ResponseTs}}; 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> };
{{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.Response}}; error: Nullable<string> };
}{{end}}{{if eq .E.Method "DELETE"}}export const {{ .E.Name}} = async ({{range $v := .Params}}{{$v}}{{end}}):Promise<{ data:{{.E.Response}}; error: Nullable<string> }> => {
return await api.DELETE({{ .Path}}) as { data: {{ .E.Response}}; error: Nullable<string> };
}{{end}}{{if eq .E.Method "PUT"}}export const {{ .E.Name}} = async (data: {{ .E.Request}}):Promise<{ data:{{.E.Response}}; error: Nullable<string> }> => {
return await api.PUT("{{ .Path}}", data) as { data: {{ .E.Response}}; 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.POST("{{ .Path}}", data) as { data: {{ .E.Response }}; error: Nullable<string> };
}{{end}}`
if e.Method == "GET" || e.Method == "DELETE" {
@ -177,11 +210,12 @@ func (e *TSEndpoint) ToTs(pkg string) string {
c = ", "
}
if prefix == ":" {
switch prefix {
case ":":
f = true
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)
} else if prefix == "*" {
case "*":
f = true
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)
@ -203,6 +237,5 @@ func (e *TSEndpoint) ToTs(pkg string) string {
if err != nil {
panic(err)
}
return result.String()
}

View File

@ -22,6 +22,8 @@ type TSInfoPakage struct {
consts map[string]TSConst
decs map[string]TSDec
endpoints map[string]TSEndpoint
imports map[string][]string
isUsed bool
}
type TSDec struct {
@ -349,8 +351,11 @@ func (i *TSInfo) Populate(path string) {
if _, ok := i.Packages[pkg].endpoints[e.Name]; ok {
exitOnError(fmt.Errorf("enpoint name %s allready in use: %s", e.Name, l.Source))
}
i.Packages[pkg].endpoints[e.Name] = e
pkg_info := i.Packages[pkg]
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) {
a := strings.Split(n, ".")
if len(a) == 2 {
if n == "[]MailDebugItem" {
fmt.Printf("MailDebugItem found\n")
}
a[1] = strings.TrimPrefix(a[1], "[]")
a[1] = strings.TrimPrefix(a[1], "*")
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) {
return TSStruct{}, true
}

View File

@ -16,8 +16,12 @@ type TSModule struct {
Consts map[string]string
GTypes map[string]string
Endpoints map[string]string
Imports map[string][]string
Source string
}
type TSOutputSources []string
type TSSouces struct {
Pakages map[string]TSModule
Errors []string
@ -32,6 +36,7 @@ func (ts *TSSouces) ensurePackage(p string) {
Consts: make(map[string]string),
GTypes: make(map[string]string),
Endpoints: make(map[string]string),
Imports: make(map[string][]string),
}
return
}
@ -214,9 +219,11 @@ func (ts *TSSouces) AddDependencies(info TSInfo, p string, s string, dependencie
func (ts *TSSouces) Populate(info TSInfo) {
ts.Pakages = make(map[string]TSModule)
ts.Errors = []string{}
for p, _ := range info.Packages {
for p := range info.Packages {
ts.ensurePackage(p)
ts.Errors = append(ts.Errors, fmt.Sprintf("Process pakage %s\n", p))
for _, st := range info.Packages[p].structs {
if st.Typescript {
if len(st.Fields) == 0 {
@ -228,6 +235,8 @@ func (ts *TSSouces) Populate(info TSInfo) {
}
ts.Pakages[p].Structs[st.Name] = s
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)
}
}
if len(info.Packages[p].endpoints) > 0 {
for _, e := range info.Packages[p].endpoints {
/* if e.Request != "" {
a := strings.Split(e.Request, ".")
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)
for _, e := range info.Packages[p].endpoints {
/* if e.Request != "" {
a := strings.Split(e.Request, ".")
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)
}
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
import (
"bytes"
"strings"
"fmt"
"os"
"text/template"
"time"
)
// configuration
type TSConfig struct {
Url string
TsApi *string
Path string
}
var TSReport = ""
type tsTemplateData struct {
APIURL string
CreatedOn time.Time
}
var tsFiles = TSFiles{}
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 tsSourcesData = TSSouces{}
tsInfoData.Populate(tsConfig.Path)
tsInfoData.Populate(path)
tsInfoData.TestEndpoints()
tsSourcesData.Populate(tsInfoData)
@ -43,43 +38,33 @@ func GetTSSource(config TSConfig) string {
exitOnError(fmt.Errorf("some errors...\n %s", err))
}
tsSource := ""
data := ""
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)
// api file
tsApi, err := GetApiFile()
if err != nil {
panic(err)
exitOnError(fmt.Errorf("some errors...\n %s", err))
}
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()
tsFiles.Add("api.ts", tsApi)
tsSource += fmt.Sprintln("\n// Global Declarations ")
// index file
tsApiDeclarations := []string{}
tsIndexSource := ""
tsIndexSource += fmt.Sprintln("\n// API Declarations ")
for p := range tsSourcesData.Pakages {
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 {
if p == "users" {
fmt.Println("users package")
}
source := ""
head := fmt.Sprintf("\n//\n// package %s\n//\n", p)
for _, v1 := range tsSourcesData.Pakages[p].Endpoints {
source += fmt.Sprintln(v1)
}
@ -95,10 +80,59 @@ func GetTSSource(config TSConfig) string {
for _, v1 := range tsSourcesData.Pakages[p].Consts {
source += fmt.Sprintln(v1)
}
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
}