diff --git a/backend/.env b/backend/.env index 4a7c42b..3713c4e 100644 --- a/backend/.env +++ b/backend/.env @@ -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= . + diff --git a/backend/GeneratedCode/admin.ts b/backend/GeneratedCode/admin.ts new file mode 100644 index 0000000..df6922d --- /dev/null +++ b/backend/GeneratedCode/admin.ts @@ -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 }> => { + return (await api.POST("/admin/users", data)) as { + data: users.User[]; + error: Nullable; + }; +}; + +// 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 }> => { + return (await api.PUT("/admin/users/:uuid/block", data)) as { + data: users.User; + error: Nullable; + }; +}; + +export interface BlockUserRequest { + action: string; +} + +export interface ListUsersRequest { + page: number; + pageSize: number; +} diff --git a/backend/GeneratedCode/api.ts b/backend/GeneratedCode/api.ts new file mode 100644 index 0000000..2c36a4a --- /dev/null +++ b/backend/GeneratedCode/api.ts @@ -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 { + 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"); diff --git a/backend/GeneratedCode/apiTypes.ts b/backend/GeneratedCode/apiTypes.ts new file mode 100644 index 0000000..e6d9a4b --- /dev/null +++ b/backend/GeneratedCode/apiTypes.ts @@ -0,0 +1,4 @@ +// API Declarations +export type Nullable = T | null; + +export type Record = { [P in K]: T }; diff --git a/backend/GeneratedCode/generatedTypescript.ts b/backend/GeneratedCode/generatedTypescript.ts deleted file mode 100644 index 20fdeef..0000000 --- a/backend/GeneratedCode/generatedTypescript.ts +++ /dev/null @@ -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 { - 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 | null; - -export type Record = { [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 }> => { - return (await api.GET(`/users/${uuid}`)) as { - data: UserProfile; - error: Nullable; - }; -}; - -// 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 }> => { - return (await api.POST("/users", data)) as { - data: UserProfile; - error: Nullable; - }; -}; - -// 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 }> => { - return (await api.PUT("/users/:uuid", data)) as { - data: UserProfile; - error: Nullable; - }; -}; - -// 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 }> => { - return (await api.DELETE(`/users/${uuid}`)) as { - data: SimpleResponse; - error: Nullable; - }; -}; - -// 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; -}> => { - return (await api.GET("/auth/me")) as { - data: UserShort; - error: Nullable; - }; -}; - -export interface UpdateUserRequest { - name: string; - email: string; - password: string; - roles: models.UserRoles; - status: models.UserStatus; - types: models.UserTypes; - avatar: Nullable; - details: Nullable; - preferences: Nullable; -} - -// -// 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; - preferences: Nullable; - avatar: Nullable; -} - -export interface UserCreateInput { - name: string; - email: string; - password: string; - roles: UserRoles; - status: UserStatus; - types: UserTypes; - avatar: Nullable; - details: Nullable; - preferences: Nullable; -} - -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 }> => { - return (await api.PUT("/admin/users/:uuid/block", data)) as { - data: UserShort; - error: Nullable; - }; -}; - -// 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 }> => { - return (await api.POST("/admin/users", data)) as { - data: UserShort[]; - error: Nullable; - }; -}; - -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; -}> => { - return (await api.GET("/health")) as { - data: string; - error: Nullable; - }; -}; - -// 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; -}> => { - return (await api.GET("/metrics")) as { - data: string; - error: Nullable; - }; -}; - -// 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; -}> => { - return (await api.GET("/maildebug")) as { - data: MailDebugItem[]; - error: Nullable; - }; -}; - -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 }> => { - return (await api.POST("/auth/password/reset", data)) as { - data: SimpleResponse; - error: Nullable; - }; -}; - -// 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 }> => { - return (await api.POST("/auth/password/valid", data)) as { - data: SimpleResponse; - error: Nullable; - }; -}; - -// 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 }> => { - return (await api.POST("/auth/login", data)) as { - data: TokenPair; - error: Nullable; - }; -}; - -// 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 }> => { - return (await api.POST("/auth/refresh", data)) as { - data: TokenPair; - error: Nullable; - }; -}; - -// 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 }> => { - return (await api.POST("/auth/register", data)) as { - data: UserShort; - error: Nullable; - }; -}; - -// 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 }> => { - return (await api.POST("/auth/password/forgot", data)) as { - data: SimpleResponse; - error: Nullable; - }; -}; - -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; -} diff --git a/backend/GeneratedCode/responses.ts b/backend/GeneratedCode/responses.ts new file mode 100644 index 0000000..939b5e5 --- /dev/null +++ b/backend/GeneratedCode/responses.ts @@ -0,0 +1,3 @@ +export interface SimpleResponse { + message: string; +} diff --git a/backend/GeneratedCode/systemUtils.ts b/backend/GeneratedCode/systemUtils.ts new file mode 100644 index 0000000..b398dd6 --- /dev/null +++ b/backend/GeneratedCode/systemUtils.ts @@ -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; +}> => { + return (await api.GET("/metrics")) as { + data: string; + error: Nullable; + }; +}; + +// 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; +}> => { + return (await api.GET("/maildebug")) as { + data: MailDebugItem[]; + error: Nullable; + }; +}; + +// 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; +}> => { + return (await api.GET("/health")) as { + data: string; + error: Nullable; + }; +}; + +export interface MailDebugItem { + name: string; + content: string; +} diff --git a/backend/GeneratedCode/tokens.ts b/backend/GeneratedCode/tokens.ts new file mode 100644 index 0000000..769e217 --- /dev/null +++ b/backend/GeneratedCode/tokens.ts @@ -0,0 +1,4 @@ +export interface TokenPair { + access_token: string; + refresh_token: string; +} diff --git a/backend/GeneratedCode/users.ts b/backend/GeneratedCode/users.ts new file mode 100644 index 0000000..9faa0cb --- /dev/null +++ b/backend/GeneratedCode/users.ts @@ -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 }> => { + return (await api.POST("/auth/refresh", data)) as { + data: tokens.TokenPair; + error: Nullable; + }; +}; + +// 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 }> => { + return (await api.POST("/auth/password/reset", data)) as { + data: responses.SimpleResponse; + error: Nullable; + }; +}; + +// 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 }> => { + return (await api.GET(`/users/${uuid}`)) as { + data: User; + error: Nullable; + }; +}; + +// 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; +}> => { + return (await api.GET("/auth/me")) as { data: User; error: Nullable }; +}; + +// 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 }> => { + return (await api.POST("/auth/register", data)) as { + data: User; + error: Nullable; + }; +}; + +// 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 }> => { + return (await api.POST("/auth/password/forgot", data)) as { + data: responses.SimpleResponse; + error: Nullable; + }; +}; + +// 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 }> => { + return (await api.PUT("/users/:uuid", data)) as { + data: User; + error: Nullable; + }; +}; + +// 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 }> => { + return (await api.DELETE(`/users/${uuid}`)) as { + data: responses.SimpleResponse; + error: Nullable; + }; +}; + +// 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 }> => { + return (await api.POST("/auth/login", data)) as { + data: tokens.TokenPair; + error: Nullable; + }; +}; + +// 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 }> => { + return (await api.POST("/auth/password/valid", data)) as { + data: responses.SimpleResponse; + error: Nullable; + }; +}; + +// 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 }> => { + return (await api.POST("/users", data)) as { + data: User; + error: Nullable; + }; +}; + +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; + preferences: Nullable; + avatar: Nullable; + createdAt: Date; + updatedAt: Date; +} + +export interface UserCreateInput { + name: string; + email: string; + password: string; + roles: UserRoles; + status: UserStatus; + types: UserTypes; + avatar: Nullable; + details: Nullable; + preferences: Nullable; +} + +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; + details: Nullable; + preferences: Nullable; +} + +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; + preferences: Nullable; + avatar: Nullable; + 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; diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 9de16e1..bd09ca4 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -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}) }, diff --git a/backend/internal/admin/routes.go b/backend/internal/admin/routes.go index d4dc70c..0b816dc 100644 --- a/backend/internal/admin/routes.go +++ b/backend/internal/admin/routes.go @@ -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)) } diff --git a/backend/internal/http/static/spa/index.html b/backend/internal/http/static/spa/index.html new file mode 100644 index 0000000..d28bb66 --- /dev/null +++ b/backend/internal/http/static/spa/index.html @@ -0,0 +1 @@ +ciao \ No newline at end of file diff --git a/backend/internal/systemUtils/routes.go b/backend/internal/systemUtils/routes.go index 7af40c6..8994d3b 100644 --- a/backend/internal/systemUtils/routes.go +++ b/backend/internal/systemUtils/routes.go @@ -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)) } diff --git a/backend/internal/tsgenerator/generator.go b/backend/internal/tsgenerator/generator.go index 54be96e..8b89d0d 100644 --- a/backend/internal/tsgenerator/generator.go +++ b/backend/internal/tsgenerator/generator.go @@ -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 l’errore 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 } diff --git a/backend/internal/user/model.go b/backend/internal/user/model.go index 4103e33..0ef9743 100644 --- a/backend/internal/user/model.go +++ b/backend/internal/user/model.go @@ -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"` diff --git a/backend/internal/user/routes.go b/backend/internal/user/routes.go index b2edb7a..b45f344 100644 --- a/backend/internal/user/routes.go +++ b/backend/internal/user/routes.go @@ -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 diff --git a/backend/pkg/ts-rpc/TSFiles.go b/backend/pkg/ts-rpc/TSFiles.go new file mode 100644 index 0000000..329de55 --- /dev/null +++ b/backend/pkg/ts-rpc/TSFiles.go @@ -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 l’errore reale + return "", fmt.Errorf("prettier error: %v\nstderr: %s", err, stderr.String()) + } + + return out.String(), nil +} diff --git a/backend/pkg/ts-rpc/tsApiTemplate.go b/backend/pkg/ts-rpc/tsApiTemplate.go index a70d76c..ec7dbe5 100644 --- a/backend/pkg/ts-rpc/tsApiTemplate.go +++ b/backend/pkg/ts-rpc/tsApiTemplate.go @@ -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 +} diff --git a/backend/pkg/ts-rpc/tsEndpoint.go b/backend/pkg/ts-rpc/tsEndpoint.go index 132afdb..82c6f87 100644 --- a/backend/pkg/ts-rpc/tsEndpoint.go +++ b/backend/pkg/ts-rpc/tsEndpoint.go @@ -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 }> => { - return await api.GET({{ .Path}}) as { data: {{ .E.ResponseTs}}; error: Nullable }; -}{{end}} -{{if eq .E.Method "DELETE"}}export const {{ .E.Name}} = async ({{range $v := .Params}}{{$v}}{{end}}):Promise<{ data:{{.E.ResponseTs}}; error: Nullable }> => { - return await api.DELETE({{ .Path}}) as { data: {{ .E.ResponseTs}}; error: Nullable }; -}{{end}} -{{if eq .E.Method "PUT"}}export const {{ .E.Name}} = async (data: {{ .E.RequestTs}}):Promise<{ data:{{.E.ResponseTs}}; error: Nullable }> => { - return await api.PUT("{{ .Path}}", data) as { data: {{ .E.ResponseTs}}; error: Nullable }; -}{{end}} -{{if eq .E.Method "POST"}}export const {{ .E.Name}} = async (data: {{ .E.RequestTs}}):Promise<{ data:{{.E.ResponseTs}}; error: Nullable }> => { - return await api.POST("{{ .Path}}", data) as { data: {{ .E.ResponseTs}}; error: Nullable }; +{{if eq .E.Method "GET"}}export const {{ .E.Name}} = async ({{range $v := .Params}}{{$v}}{{end}}):Promise<{ data:{{.E.Response}}; error: Nullable }> => { + return await api.GET({{ .Path}}) as { data: {{ .E.Response}}; error: Nullable }; +}{{end}}{{if eq .E.Method "DELETE"}}export const {{ .E.Name}} = async ({{range $v := .Params}}{{$v}}{{end}}):Promise<{ data:{{.E.Response}}; error: Nullable }> => { + return await api.DELETE({{ .Path}}) as { data: {{ .E.Response}}; error: Nullable }; +}{{end}}{{if eq .E.Method "PUT"}}export const {{ .E.Name}} = async (data: {{ .E.Request}}):Promise<{ data:{{.E.Response}}; error: Nullable }> => { + return await api.PUT("{{ .Path}}", data) as { data: {{ .E.Response}}; error: Nullable }; +}{{end}}{{if eq .E.Method "POST"}}export const {{ .E.Name}} = async (data: {{ .E.Request}}):Promise<{ data:{{.E.Response}}; error: Nullable }> => { + return await api.POST("{{ .Path}}", data) as { data: {{ .E.Response }}; error: Nullable }; }{{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", 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() } diff --git a/backend/pkg/ts-rpc/tsInfo.go b/backend/pkg/ts-rpc/tsInfo.go index 3227d80..0a88ff3 100644 --- a/backend/pkg/ts-rpc/tsInfo.go +++ b/backend/pkg/ts-rpc/tsInfo.go @@ -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 } diff --git a/backend/pkg/ts-rpc/tsTsInfo.go b/backend/pkg/ts-rpc/tsTsInfo.go index 4f13202..7ffa51b 100644 --- a/backend/pkg/ts-rpc/tsTsInfo.go +++ b/backend/pkg/ts-rpc/tsTsInfo.go @@ -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) + } + } } } diff --git a/backend/pkg/ts-rpc/tsrpc.go b/backend/pkg/ts-rpc/tsrpc.go index 104f7a5..b747229 100644 --- a/backend/pkg/ts-rpc/tsrpc.go +++ b/backend/pkg/ts-rpc/tsrpc.go @@ -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 }