- Packages and Directories
- Each sub-directory under server/api defines a package
- Go prefers lowercase single-word package names
- Package names
- Any file that contains a func main() must use the package 'main'
- Files in a directory that has main.go must all use the package 'main'
- Working directory
cd ~/Workspace/ChenWeb go run server/cmd/dataservice/main.go
In the above example, the Go working directory is ~/Workspace/ChenWeb.
- .env
The .env is created in the Go root directory, or the directory in which go.mod resides. In my environment, it is ~/Workspace/ChenWeb.
- direnv
After made some changes, say editted .envrc, you need to run: direnv allow /Users/cding/Workspace/ChenWeb/.envrc
{
field: "price",
editable: true,
singleClickEdit: false,
valueFormatter: (params) => { return '£' + params.value.toLocaleString(); },
// Keep track of edits using 'tableDirty' variable
onCellValueChanged: (event) => {
console.log(New Cell Value: ${event.newValue}, Old value: ${event.oldValue})
if (event.newValue != event.oldValue) {
tableDirty = true
console.log('Value changed');
}
},
},
We use "Email Sign Up" as an example.
- In the Login Page (.svelte), define the function handleEmailSignup in the <script> section in the same field
- There should be a form: . Clicking Submit will call handleEmailSignup(...)
- The form should have fields: User Name, Email, Password. These should bind to user_name, email and password, which should be $state variables.
- handleEmailSignup(...) must be an async function. It fetches (POST) through a link, such as: const res = await fetch("http://localhost:8080/auth/email/signup", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name, email, password }) });
- Backend needs to register "http://localhost:8080/auth/email/signup" with a Go function
- Backend needs to implement the Go function
When defining a type in Go, we can specify how the struct converts to/from JSON through json.Marshal(xxx) and json.Unmarshal(xxx). The backtick-quoted strings are optional. If not specified, field names are used as key names in JSON. Using backtick-quoted strings offers the folloing:
- Omit fields conditionally: json:"_, omitempty"
- Omit fields unconditionally
Example:
type User struct {
UserName string json:"user_name"
Password string json:"-"
Age int json:"age,omitempty"
}
- UserName is converted to "user_name" in JSON
- Omit the field Password
- Omit Age if it is 0 (empty). Otherwise, use the key 'age'
Example:
type User struct {
UserName string `json:"user_name"`
Age int `json:"age"`
}
// Marshal
user := User{UserName: "alice", Age: 30}
data, _ := json.Marshal(user) // {"user_name":"alice","age":30}
// Unmarshal
var user User
json.Unmarshal(data, &user)
- Marshal converts objects to string
- Unmarshal converts strings to objects
Converting from strings to Go object is a common operation. There are three ways to do so.
Method 1: Define the struct: Use this method if we know the types. import ( "encoding/json" )
type User struct {
UserName string `json:"user_name"`
Password string `json:"-"`
Age int `json:"age,omitempty"`
}
// Marshal
user := User{UserName: "alice", Age: 30}
data, _ := json.Marshal(user) // {"user_name":"alice","age":30}
// Unmarshal
var user User
json.Unmarshal(data, &user)
Method 2: Use map[string]interface{} Use this method if we know it is a JSON but do not know the struct. In other word, it handles dynamic JSON, or JSON that can have any members.
Example: data := map[string]interface{}{ "user_name": "alice", "age": 30, }
// Marshal
jsonData, _ := json.Marshal(data)
// Unmarshal
var result map[string]interface{}
json.Unmarshal(jsonData, &result)
Method 3: Use interface{} Use this method if we do not even know whether it is a JSON or not. Note that interface{} is 'any', or it can be anything.
Example:
var var1 interface{}
var var2 interface{}
json.Unmarshal([]byte({"user_name":"alice","age":30}), &var1)
json.Unmarshal([]byte({"12345"}), &var2)
'var1' type is map[string]interface{} and 'var2' is int64.
JSON object → map[string]interface{}
JSON array → []interface{}
JSON string → string
JSON number → float64
Note that Go does not convert values to int. If you want it to be int: Method 1: var myvar int json.Unmarshal([]byte("1234"), &myvar)
Method 2: Use interface{}, check and convert (safer) var myvar interface{} json.Unmarshal([]byte("1234"), &myvar) if myfloat, ok := myvar.(float64); ok { myint := int(myfloat) }
http.StatusOk 200 http.StatusBadRequest 400 http.StatusNotFound 404 Resource not found http.StatusUnauthorized 401 Unauthorized http.StatusForbidden 403 Unauthorized http.StatusConflict 409 http.StatusInternalServerError 500
import { StatusCodes } from 'http-status-codes'; StatusCodes.OK // 200 StatusCodes.NOT_FOUND: // 404 StatusCodes.INTERNAL_SERVER_ERROR: // 500
Start the service: colima start Then pull the image
Below is a JavaScript code snippie. What it does is:
-
Get the information from 'node'
-
Use the information to create a div to show the contents.
-
When detailsContainer is retrieved, Browser will delete all its children. It then (re)creates the div. function showNodeInfo(node: nodeType) { const detailsContainer = document.getElementById('node-details'); const xLevel = knowledgeSystem.dimensions.x.levels[node.position.x]; const yLevel = knowledgeSystem.dimensions.y.levels[node.position.y]; const zLevel = knowledgeSystem.dimensions.z.levels[node.position.z];
detailsContainer.innerHTML = ` <div class="node-info"> <h3>${node.title}</h3> <div class="position-info"> <strong>位置信息:</strong><br> X轴(${knowledgeSystem.dimensions.x.name}):${xLevel}<br> Y轴(${knowledgeSystem.dimensions.y.name}):${yLevel}<br> Z轴(${knowledgeSystem.dimensions.z.name}):${zLevel} </div> <p><strong>描述:</strong>${node.description}</p> <div class="tags"> ${node.tags.map(tag => `<span class="tag">${tag}</span>`).join('')} </div> </div> `; }
In Svelte, we need to change the code in two parts:
- create a state variable 'nodeSelected'
- the function 'showNodeInfo(...)' breaks in two parts. The first
part updates nodeSelected. The second part is {#if ...}, making the div controlled by nodeSelected.
Created: {createdDate.toLocaleString()}
Note: Go’s time.Time uses UTC by default (Z suffix). If the frontend is in a different timezone, new Date() will auto-convert to local time — which is usually what we want.
If the above standard way is not what we want, we can define CustomDate format, such as:
type JSONTime time.Time
func (t JSONTime) MarshalJSON() ([]byte, error) {
// Format as "2025-10-29 01:34:21" (PostgreSQL default)
return []byte(time.Time(t).Format("2006-01-02 15:04:05")), nil
}
type Document struct {
ID string json:"id"
UserID string json:"userID"
FileURL string json:"fileURL"
Valid string json:"valid"
CreatedAt JSONTime json:"createdAt"
UpdatedAt JSONTime json:"updatedAt"
}
When frontend sends request to the backend, the browser will automatically attach cookies to the request, but the fetch MUST be configured correctly: async function fetchDocuments() { try { const res = await fetch('/api/v1/documents', { method: 'GET', headers: { 'Accept': 'application/json', }, credentials: 'include' // ← REQUIRED for cookies to be sent });
// ... rest unchanged
} catch (err) { console.error('Failed to fetch documents:', err); } }
It is important to include "credentials: 'include'.
When the requests reaches the backend (Go), the AuthMiddleware (defined in Shared/server/auth-middleware/auth.go) will check the auth (i.e., whether the user has logged in). It currently does:
- If the requested is static asset (such as .js, ...), pass
- Otherwise, it checks whether the user has logged in. If not, it re-directs to the default page ("/")
In addition, CORS must be configured as (in main.go): e.Use(middleware.CORSWithConfig(middleware.CORSConfig{ AllowOrigins: []string{"http://localhost:5173"}, AllowCredentials: true, // ← required }))
and cookies MUST be set as: c.SetCookie(&http.Cookie{ Name: "session_id", Value: sessionID, Path: "/", HttpOnly: true, Secure: false, // true in production (HTTPS) SameSite: http.SameSiteLaxMode, })
Retrieving data is done by a Go function, such as api/database/retrieve_documents. func func RetrieveDocuments(c echo.Context) error { // Query the database for dashboard data userID, ok := c.Request().Context().Value("userID").(string) if !ok || userID == "" { // This should never happen if middleware is correct, but be safe return c.JSON(http.StatusUnauthorized, map[string]string{"error": "User not authenticated"}) }
...
stmt += "where user_id = xxx"
Go has two functions to query the database:
- Query(...) return *sql.Rows
- QueryRow return *sql.Row When using Query(...), we should hold the rows and close it by calling rows.Close(): rows, err := db.Query(...) defer rows.Close() When using QueryRow(...), there is no need to close it. It is, however, important do not use QueryRow(...) unless the query will return at most one record. Otherwise, it may lead to connection leak (normally it will not since Go handles the case properly, but it is advised not to rely on it because anything can happen!)
When QueryRow(...) returns no record and you try to scan it, it will generate a row, err := db.QueryRow(...) if err != nil { }
err1 := row.Scan(...) if err1 != nil { It failed the scan. If it selected no record, calling its scan will generate a sql.ErrNoRows error. }
Unlike Query(...), QueryRow(...) does not return *sql.Rows. When you call the row's Scan function, it will automatically close the result. If you do not call its Close() function, it will be garbage collected.
QueryRow(...) is syntactic sugar over Query(...). Internally, it ensures the connection is released.
Send response in Go: if !authenticated { return c.JSON(http.StatusUnauthorized, echo.Map{ "error": "User not authenticated", "loc": "(MID_AME_016)"}) }
return c.JSON(http.StatusOK, echo.Map{
"authenticated": true,
"user_name": user_name,
"loc": "(MID_AME_023)",
})
It sends a JSON of different shapes, depending on OK or not.
In frontend, if (res.ok) { const data = await res.json(); // success shape // You can use 'data' } else if (res.status === 401) { const err = await res.json(); // error shape // You can use 'err' } else { // handle other errors (500, etc.) }
There are two methods:
Method 1: func a-go-function(...) error { ... return c.JSON(http.StatusOK, echo.Map{ "authenticated": true, "user_name": user_name, "loc": "(MID_AME_023)", }) }
Informational (1xx) Constant Value http.StatusContinue 100 http.StatusSwitchingProtocols 101
Success (2xx) Constant Value http.StatusOK 200 http.StatusCreated 201 http.StatusAccepted 202 http.StatusNoContent 204
Redirection (3xx) Constant Value http.StatusMultipleChoices 300 http.StatusMovedPermanently 301 http.StatusFound 302 http.StatusNotModified 304 http.StatusTemporaryRedirect 307 http.StatusPermanentRedirect 308
Client Error (4xx) Constant Value http.StatusBadRequest 400 http.StatusUnauthorized 401 http.StatusForbidden 403 http.StatusNotFound 404 http.StatusMethodNotAllowed 405 http.StatusRequestTimeout 408 http.StatusConflict 409 http.StatusTeapot 418 http.StatusTooManyRequests 429
Server Error (5xx) Constant Value http.StatusInternalServerError 500 http.StatusNotImplemented 501 http.StatusBadGateway 502 http.StatusServiceUnavailable 503 http.StatusGatewayTimeout 504
When we build a library, the library is defined in a directory. The directory may contain multiple files. When others use the library, we do not want callers know the internal file structures. "index.ts" is used as the entrance point for the library so that callers only need to import from its directory. As an example, Shared/src/lib has an index.ts, which has the line: export { isAuthenticated, setIsAuthenticated, clearAuthCache } from './utils/auth.js'; Callers can import: import { isAuthenticated } from '@chendingplano/shared';
Svelte's svelte-package (used when you run "bun run build") works as follows:
- Starts from src/lib/index.ts
- Buddles only the code that is exported from index.ts
- Output it to dist/ So if you do not export a function in index.ts, it won't appear in dist/, and others cannot see it even if you do have it in the files.
Shared/tsconfig.json: When "moduleResolution" is defined as "buddle" in tsconfig.json, you do not need the extensions in import, such as: import { isAuthenticated } from './utils/auth';
Hydration is the process that restores the server-side rendered application on the client (browser). This includes things like reusing the server rendered DOM structures, persisting the application state, transferring data that was retrieved by the server.
If user.avatar is only set on the client (e.g., from a store that initializes after mount),
- Server renders:
// It will fallback, if fallback is defined
- Client renders:
-> will show the logo This leads DOM structure mismatches, leading to Hydration error! The source of the problem is that the logo is not available on the server. Frontend (browsers) may use store to persist resources. Store is not accessible on the server, leading to hydration problem.
- The hint is: a button inside a button, which is incorrect HTML.
- The difficulty is to locate the source.
- ChatGPT suggested to search for <Button
- The application is (app). Start from tax/web/src/routes/(app)/layout.svelte
- (app)/layout.svelte: <Sidebar.Provider style="--sidebar-width: calc(var(--spacing) * 72); --header-height: calc(var(--spacing) * 12);">
Log Out
ChatGPT recommended to change it to: <Tooltip.Provider> <Tooltip.Root>
Log Out
</Tooltip.Content> </Tooltip.Root> </Tooltip.Provider> It addedIn general: <Component <prop_name>/> => <Component <prop_name>={true} <Component <prop_name>={value}/> => Pass the value for <prop_name> <Component <prop_name>="text"/> => Pass the text (string), so the prop is a string with the 'text' => The prop is either undefined or default by the component
If a component defines a prop as $bindable, its parent can bind this prop so that when the value of this prop changes, the parent can see the changes.
Example: Below defines a component:
<script lang="ts"> import { Avatar as AvatarPrimitive } from "bits-ui"; import { cn } from "$lib/utils.js"; let { ref = $bindable(null), loadingStatus = $bindable("loading"), class: className, ...restProps }: AvatarPrimitive.RootProps = $props(); </script><AvatarPrimitive.Root bind:ref bind:loadingStatus data-slot="avatar" class={cn("relative flex size-8 shrink-0 overflow-hidden rounded-full", className)} {...restProps} />
Below is its parent. It has an HTML element 'avatarEl' and a variable 'status'. These two are bound to the component's 'ref' and 'loadingStatus'. The binding is two-way: parent passes the value to the bound props; the component can change their values, such as changing 'status' from true to false. The parent can see the changes.
<script lang="ts"> let avatarEl: HTMLElement; let status: "loading" | "loaded" | "error"; </script>{#if status === "loading"} Loading... {/if}
We may treat bindable props as parameters so that parents (or the module that uses the component) and pass values to it (not bindable) or pass value to it and get the changes back (bindable).
Since we are using bun, run: bun run build
The bun equivalent of npm install -D baseline-browser-mapping@latest is: bun add -d baseline-browser-mapping@latest The rule is:
- 'install' == 'add'
- 'D' == 'd'
pnpm dlx shadcn-svelte@latest add sidebar-07
Do the following: cd ChenWeb/web bunx shadcn-svelte@latest add sidebar-07
bunx is Bun's version of npx / pnmp dlx. When running: bunx shadcn-svelte@latest add sidebar-07 it does the following:
- bunx expects the package has CLI (or has "bin" entry in its package.json) If a package does not have CLI, bunx will fail
- Download the shadcn-svelte package into a temporary cache (note: it caches the package. Once finished, the cache is gone. There is no issue of removing it)
- Locate the CLI binaries defined in "bin"
- bunx runs the binary using your parameters (such as 'add sidebar-07')
- Remove it afterward (sort of, it actually does not remove because the package is cached only)
shadcn-svelte shadcn-svelte vite vite eslint eslint tailwindcss tailwindcss create-svelte create-svelte prisma prisma
bunx itself does not install the dependencies. Instead:
- bunx runs its CLI, installs the component (such as sidebar-07)
- shadcn-svelte edits your Sveltekit project
- shadcn-svelte runs 'bun add ...' to add all the dependencies In other word, bunx itself does not install the dependencies, but the package does.
bunx does the following:
- Downloads the npm package (if needed)
- Runs its CLI
- Does not require the package insalled in your project
bun exec
- It does not download anything
- It assumes the package has been downloaded
- It does not execute a package by a package name
Step 1: bunx shadcn-svelte/latest init Run this command only when your project has never initialized shadcn-svelte.
Step 2: bunx shadcn-svelte/latest add [component] This command installs the component and all its dependencies.
We will use an actual example to show how to make a function in Shared available for applications to use it.
- In ~/Workspace/Shared/src/lib/utils/auth.js, it defines a function "isAuthenticated"
- In ~/Workspace/Shared/src/lib/index.js, export it
- In ~/Workspace/Shared, run "bun run build", which will compile the code into ~/Workspace/Shared/dist/utils/index.js
- In ~/Workspace/tax/web/src/routes/(app)/+layout.svelte: import { isAuthenticated } from '@chendingplano/shared';
- In ~/Workspace/tax/web/package.json: "dependences": { ... "@chendingplano/shared": "file:../../Shared" }
- In ~/Workspace/tax/web, run: bun install
Example: ~/Workspace/tax/web/src/routes/(auth)
- (auth) denotes a route group (by SvelteKit)
- (auth) is not part of the URL (or it does not affect URLs), so: ~/Workspace/tax/web/src/routes/(auth)/login/+page.svelte => served at /login ~/Workspace/tax/web/src/routes/(auth)/signup/+page.svelte => served at /signup
- The file +layout.svelte wraps all pages inside (auth). It applies to /login, /signup and any other pages defined in a sub-directory.
- It does not apply to routes outside the group.
SvelteKit uses +layout.svelte to define a template or pattern. Pages inside the directory plugs their content to the specified slot. Normally, there shall be only one in a +layout.svelt file. But technically, you may have multiple named slots, such as:
Then a child may have:Main content
It is automatically generated by SvelteKit at build/dev time. ./$types.js (and its TypeScript counterpart ./$types.d.ts) is a virtual module generated per route directory by SvelteKit. It contains type definitions for:
- PageLoad, PageServerLoad
- LayoutLoad, LayoutServerLoad
- Actions
- PageData, LayoutData
- etc. These types are inferred from your actual +page.ts, +layout.ts, +page.server.ts, etc. (Refer to tax/web/src/routes/(auth)/signup)
Let's use an example: tax/web/src/routes/(app) layout.svelte dashboard/+page.svelte chat/+page.svelte ... In layout.svelte: let {children} = $props()
{@render children()}
Svelte children() is dynamic. When clicking "dashboard", "dashboard/+page.svelte" is added to layout.svelte children. When "chat/+page.svelte" is clicked, its children contains the chat, not the dashboard.
Note that layout.svelte defines a template or framework. Its children plugs their contents into the template.
Refer to Shared/server/auth/email.Go Method 1: func HandleEmailVerify(c echo.Context) error { ... redirect_url := AuthInfo.HomeURL if redirect_url == "" { log.Printf("***** Alarm missing home_url config (SHD_GGL_094)") redirect_url = "localhost:5173" }
response := map[string]string{
"name": user_info.UserName, // or user.Email if that's what you use
"email": user_info.Email,
"redirect_url": redirect_url,
"loc": "SHD_EML_210",
}
w := c.Response()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(response)
return nil
}
Method 2: func HandleEmailVerify(c echo.Context) error { ... if user_status != "user not found" { msg := "user already exists (SHD_EML_224)" log.Printf("%s", msg) return c.JSON(http.StatusOK, map[string]string{"message": msg}) }
Integration with Echo ❌ Bypasses Echo’s response handling ✅ Fully integrated Header handling You must set Content-Type manually ✅ Automatically sets Content-Type: application/json Status code handling You call WriteHeader() directly ✅ Safe: respects middleware, error handling Response already sent? ❌ Risk of writing after headers sent ✅ Echo prevents double-write (panic or silent failure) Error handling ❌ If Encode() fails, you may not ✅ Echo handles encoding errors gracefully handle it cleanly Middleware compatibility ❌ May break response-wrapping ✅ Works with all Echo middleware middleware (e.g., logging, compression) Code clarity More verbose Clean and idiomatic
One way to retrieve config items in frontend is to fetch it from the backend. Refer to "How to Send Response with Context".
Assuming a page wants to send a request to the backend with certain data. This is an example: const res = await fetch('http://localhost:8080/auth/email/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token }) }); It sends a request to /auth/email/verify, with a JSON {token}
The receiver:
type EmailVerifyRequest struct {
Token string json:"token"
}
var req EmailVerifyRequest
if err := c.Bind(&req); err != nil {
log.Printf("Failed to bind request body: %v (SHD_EML_199)", err)
return c.String(http.StatusBadRequest, "Invalid request body (SHD_EML_199)")
}
token := req.Token
- Each app should have a line: In its first RegisterRoutes() such as tax/server/api/router.go: e, err := api.RegisterRoutes()
- api.RegisterRoutes(): should have a line: e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {...} This function is called whenever a request is received by the backend.
- The function does: a. Check whether the request needs authentication. Static resources such as .js, .ts, etc., do not need authentication. b. If the resource requires login, it uses Shared/server/auth-middleware/auth.go function AuthMiddleware(). Note that this is a helper function, called for each request. It checks whether the requester has logged in by retrieving the cookie/token, check the cookie vadility, and set userID (should be user_name) to the context. ctx := context.WithValue(c.Request().Context(), "userID", user_name) c.SetRequest(c.Request().WithContext(ctx)) log.Printf("User authenicated, proceed (SHD_MAT_045):%s", path) return next(c) c. If the resource requires login but the request has not logged in yet, if it is an HTML request, redirect it to /login. Otherwise, returns an error.
- If it passes HandlerFunc, the backend routes the request to the corresponding handler, if it is a valid route. Otherwise, it returns an error code (normally 404).
- The handler can retrieve the userID (user_name), such as tax/server/api/database/retrieve_documents.go: func RetrieveDocuments(c echo.Context) error userID, ok := c.Request().Context().Value("userID").(string) ...
When a Go function receives a request, if it needs to get user ID: userID, ok := c.Request().Context().Value("userID").(string) if !ok || userID == "" { // This should never happen if middleware is correct, but be safe log.Printf("***** Alarm Failed retrieving user (SHOULD NEVER HAPPEN) (MID_RDC_041)") return c.JSON(http.StatusUnauthorized, map[string]string{"error": "User not authenticated"}) }
'userID' is set by Shared/server/auth-middleware/auth.go. Refer to "How Backend Handle Authentication" for information about how userID is set. 'userID' is 'user_name' in 'users' table. We can use 'userID' to retrieve user information by: GetUserByUserName(...)
cookie, err := c.Cookie("session_id")
In Go, we can define a string variable as:
- var sv string // Non-nullable string
- var sv *string // Nullable String
When scanning data from a select statement:
- If the database value is null but the corresponding field type is not a nullable string, the driver will report an error.
- The same is true for other data types.
In general, when a value connects to a database field, it is important to handle the null because data types in programming languages normally do not have the concept of null, but pointers can be null.
There is a special data type in sql, sql.NullString, which is a structure that has two member data: .Valid and .String: var my_nullable_str sql.NullString
Method 1:
<script> export let onClick = () => {}; export let title = 'Hello'; </script>✅ Defines a public prop that parent components can pass. ✅ Supports default values (evaluated once when the component is created). ✅ Works in all Svelte versions (v3, v4, v5). ✅ Fully reactive: if the parent changes the prop, the child sees the update. ✅ Can be written to (two-way binding with bind:prop). This is what you should use 99% of the time, especially in libraries.
Method 2:
<script> const { onClick = () => {}, title = 'Hello' } = $props(); </script>🔸 Does not declare props — it only reads whatever was passed. 🔸 Best used when you want to destructure many props at once or build highly dynamic wrappers. 🔸 No default values at the declaration level (you must handle them in destructuring). 🔸 Cannot be used with bind: (since props aren’t explicitly declared). 🔸 Only available in Svelte 5+.
-
Shared is a library. It contains both Svelte code and Go code. Its file structure is: ├── go/ │ ├── go.mod │ ├── go.sum (will be generated) │ ├── api/ │ └── auth-middleware/ ├── svelte/ │ ├── package.json │ ├── src/lib/ │ └── (optional) svelte.config.js └── README.md All Go code goes to "go" sub-directory and Svelte code to 'svelte' sub-directory.
-
Initialize go/ ch ~/Workspace/Shared/go go mod init github.com/chendingplano/shared/go If the directory already has go.mod, you can remove it and run the init.
-
Build (in ~/Workspace/Shared/go) go build ./...
-
Remove SvelteKit Files A library MUST contain only Svelte files. SvelteKit is a development tool. It should be used for applications only, not for libraries.
If you’re not using Tailwind CSS in the library itself, you can delete svelte.config.js. Otherwise, svelte.config.js should contain: import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default { preprocess: vitePreprocess() };
-
Build (~/Workspace/Shared/svelte/) bun install bun run build
-
Link
cd ~/Workspace/Shared/svelte bun link
cd ~/Workspace/ChenWeb bun link @chendingplano/shared
- go.mod Make sure ChenWeb/go.mod contains: module chenweb
go 1.23
require github.com/chendingplano/shared/go v0.0.0
replace github.com/chendingplano/shared/go => ../Shared/go
Normally frontend data types are defined in <project_name>/web/lib/types index.ts (or types.ts) api.ts documents.ts ...
They are imported by: import type { ApiResponse } from '$lib/types/api';
Assume there is a data structure in Go:
type DocumentsRecord struct {
ID string json:"id"
UserId string json:"user_id"
FileURL string json:"file_url"
Valid string json:"valid"
CreatedAt time.Time json:"created_at"
UpdatedAt time.Time json:"updated_at"
}
The TypeScript data type (should be in the types/ directory): export interface DocumentsRecord { id: string; user_id: string; file_url: string; valid: string; created_at: string; // or Date, see below updated_at: string; } IMPORTANT: the field names MUST match the JSON field names!!! IMPORTANT: Make sure the two are kept in sync. When Go changes its data type definition, DO NOT FORGET UPDATE THE TYPESCRIPT data type!!!
If there is a file: Shared/svelte/src/lib/types/documents Then:
-
Export the data types in Shared/svelte/src/lib/index.ts, such as: // Re-export types for public consumption export type { DocumentsRecord } from './types/documents'; export type { UserInfo } from './types/auth';
// Re-export components/utils too export { default as EmailVerifyPage } from './components/EmailVerifyPage.svelte';
-
Applications, such as tax or ChenWeb, can consume it as: import type { DocumentsRecord } from '@chendingplano/shared';
Below is an example (Shared/svelte/lib/stores/auth.svelte.ts): function createAuthStore(): AuthStore { // Initialize state from current PocketBase auth store let isLoggedIn = $state(pb.authStore.record !== null); let user = $state(pb.authStore.record); ... This function defines two state variables: isLoggedIn and user. Unlike normal local variables in functions, state variables are not local. They are reactive variables live outside functions.
Every '.svelte' defines a component, regardless of where it is.
A component:
- A self-contained UI unit defined in a .svelte file
- A component has:
- Markup: ...
- Reactive logic: <script> with let, $state, etc.
- Styles: <style>
- Public API: export let ...
- Markup:
- It compiles to a JavaScript class
- When export { default as EmailVerifyPage } from './components/EmailVerifyPage.svelte'; it means to export the component (the whole file)
It is possible to define state variables in functions, such as: (File: Shared/svelte/src/lib/stores/auth.svelte.ts) function createAuthStore(): AuthStore { // Initialize state from current PocketBase auth store let isLoggedIn = $state(false); let user = $state<UserInfo | null>(null); let error_msg = $state<string | null>(null); let status = $state<'login' | 'register' | 'forgot' | 'loggedin' | 'error' | 'pending'>('login');
These state variables are similar to member data in C++/Java, but not quite. They are actually not member data. TypeScript/JavaScript uses closure-based encapsulation. The returned object has only getter methods.
Closures are new concept. We can view closures as a way of wrapping execution environment (or stack). When: authStore1 = createAuthStore() authStore2 = createAuthStore() It will create two execution stacks (or closures). authStore1.isLoggedIn and authStore2.isLoggedIn are two different variables.
Below is an example:
<script> import { authStore } from 'shared-svelte'; import { goto } from '$app/navigation'; // Reactively watch auth state $: { if (!$authStore.isLoggedIn && $page.url.pathname !== '/login') { goto('/login'); } } </script>$: is a Svelte syntax. It means: execute the code in {...} when any of the state variables changes its value.
-
$authStore.isLoggedIn accesses its isLoggedIn state variable (note the $ ) - $page.url.pathname: is SvelteKit’s built-in reactive page info.
For lengthy operations, we want to define them as async function, such as: async function register(...): Promise {...} To call this function: (Shared/svelte/src/lib/stores/auth.svelte.ts) async function handleRegister() { if (password !== passwordConfirm) { errorMessage = 'Passwords do not match'; return; }
isSubmitting = true;
errorMessage = '';
try {
await authStore.register({
email,
password,
passwordConfirm,
firstName,
lastName
});
// Registration successful!
// The authStore will automatically update isLoggedIn and user
console.log('Registration successful');
} catch (error) {
// Handle registration errors
errorMessage = error instanceof Error ? error.message : 'Registration failed';
} finally {
isSubmitting = false;
}
}
When it calls "await authStore.register(...)", it pauses the current stack, returns the control to the event loop so that the system can handle other events. When the function finishes (i.e., returns), it resumes the interrupted execution.
You may also use authStore.register(...).then(() => {...}).catch(err => {...}). This is called 'call and forget', such as: ... authStore.register(...) .then(() => {...}) .catch(err => {...}) console.log("register started") // This line runs immediately
Using await is more natural unless we want to run multiple such functions simultaneously.
If the promise returns a value, such as Promise: const result : boolean = await authStore.register(...) or authStore.register(...).then((result:boolean) => {...})
Example 1:
Promise: the function should have some async actions, or actions that may
take time to finish. Use await on these actions (or functions).
async function login(email: string, password: string): Promise {
// Simulate an async operation (e.g., API call)
await fetch('/api/login', { method: 'POST', body: JSON.stringify({ email, password }) });
// No return value → resolves to undefined (compatible with void)
}
Example 2: Promise: async function login(email: string, password: string): Promise { const response = await fetch('/api/login', { method: 'POST', body: JSON.stringify({ email, password }) }); return response.ok; // Returns a boolean → Promise resolves to boolean (matches declared type) }
In tsconfig.json, add: { "extends": "./.svelte-kit/tsconfig.json", "compilerOptions": { "allowJs": true, "checkJs": true, "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true, "skipLibCheck": true, "sourceMap": true, "strict": true, "module": "ES2022", "moduleResolution": "bundler", "noEmit": true // 👈 ADD THIS } }
There are three methods to transfer values between frontend and backend:
- URL (path and query)
- Request body
- Request Header Go Echo adds an additional method, mainly used to pass information among Go objects.
(Refer to Shared/go/api/auth-middleware) // Add (UserContextKey, user_name) to its context ctx := context.WithValue(c.Request().Context(), ApiUtils.UserContextKey, user_name)
// Set (update) the context
c.SetRequest(c.Request().WithContext(ctx))
log.Printf("User authenicated, proceed (SHD_MAT_045):%s", path)
It uses c.Request().Context().Value(key) to retrieve values from context.
Example: func RetrieveDocuments(c echo.Context) error { userID, ok := c.Request().Context().Value("userID").(string) if !ok || userID == "" { ... } }
The function "c.Request().Context()" retrieves the context from its request. It then retrieves a value from it: val := c.Request.Context().Value(userContextKey)
IMPORTANT: Go retrieves the value based on not only key value but key data type, too. In this example, the key value is "user_name" and its data type is userContextKey. If there are no parameters named "user_name" or their data types are not 'userContextKey', it will return nil.
The syntax: userID, ok := c.Request().Context().Value("userID").(string) adds an assert (string) and returns val and ok == true if the assert is true. Otherwise, it returns nil, ok == false.
Channels manage the communications of goroutines. A channel is a typed, synchronized pipe between two goroutines. Key properties of channels:
- Typed: A channel is declared with a type. It can only carry values of that type.
- Unbuffered: sender will block on sending a value until there is a receiver. In other word, unbuffered channels do not internally cache values. All communications are synchronous.
- Buffered: one can create a channel is a buffer, such as ch := make(chan int, 5), which creates a channel of data type int with a buffer of max size 5. Sending values to this channel will not block until the buffer is full. When that happens, sending will be blocked until the cache is available.
- Closed: A channel can be explicitly closed by close(ch). Once closed, no more values can be sent to the channel, but the pending values are still available.
Select allows a goroutine to monitor multiple challens simultaneously for either sending
and receiving.
func (c *ActivityLogCache) flush_loop() {
...
select {
case <-channel_01:
// It cares only about events, not the event values
...
case <-channel_02:
// It cares only about events, not the event values
...
case v := <-channel_03:
// Retrieve values from channel_03
...
case channel_4 <- :
// Send a value to channel_4
...
}
}
Goroutines are light and user-land 'threads'. They are managed by Go runtime, much lighter than OS threads, which has their stacks and participates context switching. It is possible to run thousands or even millions of goroutines simultaneously.
Each goroutine has a function, either an anonymous function or a named function. go func() {...} // Anonymous function
sync.WaitGroup implements a counter. It is used to manage goroutines. It works as:
- It is normally initialized as a member data of struct, such as: type ActivityLogCache struct { ... wg sync.WaitGroup }
- When start a new goroutine in the group, call: wg.Add(1)
- You can add N routines: wg.Add(n)
- When a goroutine finishes: wg.Done()
- Wait on a WaitGroup: wg.Wait() It will not return until all goroutines are finished (i.e., its counter comes to 0)
Example: import ( "log" "sync" )
func main() { var wg sync.WaitGroup
// Track 2 goroutines
wg.Add(2)
// Launch goroutine 1
go func() {
defer wg.Done() // Decrement counter when done
log.Println("Goroutine 1 running...")
time.Sleep(1 * time.Second)
}()
// Launch goroutine 2
go func() {
defer wg.Done() // Decrement counter when done
log.Println("Goroutine 2 running...")
time.Sleep(2 * time.Second)
}()
// Block until both goroutines complete
wg.Wait()
log.Println("All goroutines finished!")
}
Go does not support call-by-reference, directly. All parameters are passed as call-by-value. That is, a copy of the parameter is made. Changes to parameters do not affect the original parameter.
We can simulate call-by-reference by passing pointers (through '*'). Example: func (c *ActivityLogCache) AddToCache(record *ActivityLogDef) {...}
The parameter 'record' is now a pointer. It won't copy the value.
IMPORTANT: For cache, it is important to make a copy of the record instead of storing the pointers.
A function can have multiple defer statements. The execution order of the deferred functions is the reverse order in which they appear.
Defer functions are executed after the sorrounding functions return. This means that defer functions cannot change the returned values. If something happens in a deferred function, we can logged it, but cannot send them back.
Another important thing to notice is that a deferred function can read local variables. If it does read local variables, it is important to keep the variable name the same. For instance, if the purpose of a deferred function is to rollback a transaction: tx, err := db.Begin() ... defer func() { if tx && err != nil { tx.Rollback() } } err1 := tx.Commit()
This can cause problem if it begins a transaction successfully (i.e., err is nil) but failed the commit, the error returned by the commit is not err but err1. The deferred function only handles err, not err1. This can cause the transaction not committed.
To avoid this problem, make sure all errors use 'err'. DO NOT NAME NEW err!
%d: for all integral values %o: octal (base 8) integral values %x: hex (or %X) integral values %b: Binary (base 2) %v: Generic value
func handler(c echo.Context) error { // Retrieve the parsed URL struct parsedURL := c.URL()
// Extract components from the parsed URL
path := parsedURL.Path // e.g., "/api/logs"
query := parsedURL.Query() // e.g., url.Values{"page": []string{"1"}, "size": []string{"10"}}
host := parsedURL.Host // e.g., "localhost:8080"
scheme := parsedURL.Scheme // e.g., "http"
Or retrieve it through its Request: func handler(c echo.Context) error { req := c.Request() // Get the underlying http.Request
// Full URL string (scheme + host + path + query)
fullURL := req.URL.String() // e.g., "http://localhost:8080/api/logs?page=1&size=10"
fmt.Printf("Full URL: %s\n", fullURL)
// Path (without host/scheme/query)
path := req.URL.Path // Same as parsedURL.Path from method 1
return c.String(http.StatusOK, "Full URL retrieved")
}
Components of URL The url.URL struct (returned by c.URL()) contains the following fields/methods:
Scheme: Protocol (e.g., "http", "https"). Host: Hostname:port (e.g., "localhost:8080"). Path: Path component (e.g., "/api/logs"). RawPath: Unescaped path (rarely needed). Query() url.Values: Returns query parameters as a map of []string (e.g., {"page": ["1"], "size": ["10"]}).
To get a specific query parameter from url, use url.Query().Get("") func handler(c echo.Context) error { parsedURL := c.URL() page := parsedURL.Query().Get("page") // Returns "1" if query is "page=1&size=10" fmt.Printf("Page parameter: %s\n", page)
return c.String(http.StatusOK, "Query parameter extracted")
}
If you Echo server runs a proxy (e.g., Nginx, Cloudflare), the host, and schema in req.URL reflect the proxy's details, not the original ones. Check proxy headers like x-Forwarded-proto or x-Forwarded-host.
func handler(c echo.Context) error { req := c.Request()
// Get original scheme (if proxy sets X-Forwarded-Proto)
forwardedProto := req.Header.Get(echo.HeaderXForwardedProto)
if forwardedProto != "" {
scheme = forwardedProto
}
// Get original host (if proxy sets X-Forwarded-Host)
forwardedHost := req.Header.Get(echo.HeaderXForwardedHost)
if forwardedHost != "" {
host = forwardedHost
}
// Rebuild the full URL with original scheme/host
originalURL := fmt.Sprintf("%s://%s%s?%s", scheme, host, req.URL.Path, req.URL.Query())
fmt.Printf("Original URL (behind proxy): %s\n", originalURL)
return c.String(http.StatusOK, "Original URL retrieved")
}
In Go, structs are defined in the following fashion:
type RequestInfo struct {
FullURL string json:"full_url"
PATH string json:"path"
Scheme string json:"scheme"
Host string json:"host"
Query string json:"query"
OriginalScheme string json:"original_scheme"
OriginalHost string json:"original_host"
}
json:"xxx" is called struct tag.
- Strings to Go JSON Object:
The data type of JSON documents in Go is 'interface{}'. The data type of JSON documents in TypeScript is {[key:string]:any} The function below converts a string into a JSON object (interface{})
func convertToJSON(json_str string) (interface{}, error) { var jsonData interface{} err := json.Unmarshal([]byte(json_str), &jsonData) // Deserialization: string -> Go object if err != nil { return nil, fmt.Errorf("Deserialization error: %v\n", err) } return jsonData, nil } When using interface{}, the jsonData variable will contain a Go representation of the JSON data. - JSON objects become map[string]interface{} - arrays become []interface{} - strings remain string - numbers become float64
- Strings to Go JSON Struct:
import ( "encoding/json" "fmt" )
var decodedReq RequestInfo err := json.Unmarshal([]byte(jsonData), &decodedReq) // Convert JSON string to struct if err != nil { fmt.Printf("Deserialization error: %v\n", err) return } // decodedReq now contains the populated struct
- omitempty
Use this to omit fields whose values are 'empty' (string: empty, integer: 0)
Use JSON.parse(json_str)
We provide two utility functions in Shared/svelte/lib/utils/UtilFuncs.ts: SafeJsonParseAsObject(json_string: string) ParseObjectOrArray(json_string: string)
You can declare a JSON object in one of the following two ways:
- Record<string, unknown/any/other data type>
- [key:string]: unknown/any/other data type
Example: type MyObj = { name: string; [key: string]: any; }; This is a map that has 'name' and any addition properties whose keys are string and values can be any. This is another way of defining a JSON object with common properties and extentable with additional keys.
Note that using any or unknown will yield different results:
- TypeScript does not check types if the type is any
- TypeScript will report error if you access any attribute of unknown type without first checking the type, such as: if (typeof MyObj['account_number'] === 'string') { // Now you can access MyObj['account_numebr'] as string. }
In the Go code below, it complains "session_log_once" is not used, but it is actually used (session_log_once.Do(...)). var ( session_log_singleton *SessionLogCache session_log_once sync.Once // Ensures InitCache runs once ) ... func InitSessionLogCache(db_type string, table_name string, db *sql.DB) error { session_log_once.Do(func() { session_log_singleton = newSessionLogCache(db_type, table_name, db) session_log_singleton.start() }) return nil }
In Go, if a variable is defined at package level, the variable is considered not used if the variable is accessed in member functions but none of these member functions are ever called. In the above example, the reason is that 'session_log_once' is used in InitSessionLogCache(...) only but this function is never called.
Below declares a function variable. The function takes one parameter (userInfo). The parameter defaults to null. The function does not return anything (void) setUserInfo: (userInfo: UserInfo | null) => void;
Below declares a function variable (login). The function takes two parameters (email: string, password: string) and returns a Promise login: (email: string, password: string) => Promise;
The format is: const func_var = (parms) => {<func_body>}
Below defines a function variable. The function takes one parameter (UserInfo). The parameter can be null. The function body is in the {...}.
const setUserInfoSet = (userInfo: UserInfo | null) => {
const { status, isAdmin } = $authStore;
const newState: AuthStoreState = {
user: userInfo,
isLoggedIn: userInfo !== null,
isAdmin: userInfo?.role === 'admin' || false,
status: status, // Must explicitly preserve status
};
authStore.set(newState);
};
Email signup involves:
- In a login-form page, such as tax/web/src/routes/(auth)/login/+page.svelte, it should have a function to do the signup: const res = await fetch("http://localhost:8080/auth/email/signup", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ first_name, last_name, email, password }) });
- The backend receives the request, create a pending user, and send a link to the email The link currently: http://localhost:5173/verifyemail?token=%s this means that the app should have a routes/verifyemail/+page.svelte page. TODO: need to make it configurable so that apps can use their own verification page.
- The user clicks on the link
- The routes/verifyemail/+page.svelte calls shared/svelte/lib/components/EmailVerifyPage.svelte (unless the app wants to implement the verify on its own)
- The EmailVerifyPage.svelte should call shared/svelte/lib/stores/auth.svelte.ts to set userInfo.
Svelte offers a utility 'svelte/store' to manage reactive states across components. Below is an example:
Step 1: Define the store Assume the store is 'AuthStoreSTate'. It contains all the state variables for the store. import { writable } from 'svelte/store'; import type { UserInfo } from './user-info.js'; // Assume this type is defined
interface AuthStoreState { isLoggedIn: boolean; status: string; user: UserInfo | null; isAdmin: boolean; }
interface AuthStore { subscribe: (run: (state: AuthStoreState) => void, invalidate?: (state: AuthStoreState) => void) => () => void; setUserInfo: (userInfo: UserInfo | null) => void; login: (email: string, password: string) => Promise; logout: () => void; register: (userData: { /* ... */ }) => Promise; } It defines a store 'AuthStore'.
Step 3: Initialize the Store function createAuthStore(): AuthStore { // Initialize the writable store with the state object const { subscribe, set, update } = writable({ isLoggedIn: false, status: 'login', user: null, isAdmin: false, });
Note that this is the function to create an instance of AuthStore. The first thing this function does is to initialize its store by setting the store member initial values. The function writable({...}) returns three functions: subscribe, set and update. Use these functions to subscribe, set and update its store variables.
Step 4: Define a function to let other components update its store variables:
// Method to set user info (uses update to preserve other state properties)
const setUserInfo = (userInfo: UserInfo | null) => {
// update ensures we use the latest state (avoids race conditions)
update(currentState => ({
...currentState, // Preserve existing properties (isLoggedIn, status, isAdmin)
user: userInfo, // Override user
isLoggedIn: userInfo !== null, // Override isLoggedIn based on userInfo
isAdmin: userInfo?.role === 'admin' || false, // Example: Update isAdmin
}));
};
Example: const { status, isAdmin } = $authStore;
The $ prefix in a variable declaration is Svelte-specific syntax that:
- Automatically subscribe the variable to the store (authStore)
- Ensure the variable hold the current state value of the store
- Mark the variable as reactive.
- $authStore represents the current state object of the authStore (type AuthStoreState)
- Destructuring (const {status, isAdmin}) extracts status and isAdmin from this state object.
- These extracted variables inherit the reactivity of $authStore
If without $: const {status, isAdmin} = authStore
- authStore is the store instance, Not the state
Arrow functions (=> ) can be written in two ways:
- Shorthand (Expression Body)
- Block (Statement Body)
This applies to functions whose function body is only one expression. You can omit the curly braces and return keyword, such as: const func = (parm) => expression; // Implicit return This function variable assumes the function takes 'parm' as its parameters and returns 'expression'.
const func = (parm) => { }
Example: const setUserInfo = (userInfo: UserInfo | null) => { update(currentState => ({ ...currentState, user: userInfo, isLoggedIn: userInfo !== null, isAdmin: userInfo?.role === 'admin' || false, })); };
Note that this example uses Shorthand, which means it does not have the curly braces and return. The function body is an expression in (...), which in this example is ({object initialize}). Note that the the curly braces is not for statements but for initializing a data object and then returns the data object.
If we want to do something (Statement body), it can be written as: const setUserInfo = (userInfo: UserInfo | null) => { update(currentState => { ... return { ..currentState, user: userInfo, isLoggedIn: userInfo !== null, isAdmin: userInfo?.role === 'admin' || false, } }); }
Example: type DocumentsResponse<Texpand = unknown> = Required & BaseSystemFields It defines a new data type DocumentsResponse. The data type includes:
- All fields from DocumentsRecord
- Required is a utility that converts all fields in T to be required (i.e., removing the ? from its definition)
- All fields in BaseSystemFields + a field named 'expanded T'
- If Texpand is not specified, it defaults to unknown
When we want to have a base type with customized expansion, we can use: your_data_type
Possible values of typeof in JavaScript: 'undefined' (for undefined). 'boolean' (for booleans). 'number' (for numbers). 'string' (for strings). 'symbol' (for symbols). 'function' (for functions). 'object' (for objects, arrays, null, etc.).
Example: interface ExpandedData { author: string; version: number; } function isExpandedData(value: unknown): value is ExpandedData { return ( typeof value === "object" && value !== null && "author" in value && typeof value.author === "string" && "version" in value && typeof value.version === "number" ); }
- "value is ExpandedData" is a type assertion
- The function serves as a Type Guard, which means that if the function returns true, the data type of the parameter 'value' is ExpandedData.
Below shows how to use it: const maybeExpandedData: unknown = { author: "Charlie", version: 3 };
if (isExpandedData(maybeExpandedData)) { // TypeScript now knows 'maybeExpandedData' is of type 'ExpandedData' console.log(maybeExpandedData.author); // Safe operation }
Both can define and name types.
type can define any type, including:
- Primitive types: type Id = string
- Union types (enum types): type status = 'pending' | 'success' | 'failed'
- Intersection types: type A = {x:number} & {y:string}
- Tuple types: type T = [number, string]
- Function types: type F = (parm1:string, parm2:number) => boolean
- Alias types: type MyNumber = number
- type can extend
- interface can only define object types, including member data and member functions
- interface can extend another interface
- A type defines a type of objects, which have member data and (optionall) member functions. An interface is similar to Java Interface. It has only member functions. No member data. An interface is purely a 'contract': a collection of member functions. Any object that implements the member functions of an interface can be used where the interface is required.
- You can create an instance of type, such as: person := Person // Person is a type But you cannot create an instance of an interface: person := Person // Person is an interface
type alias: type myfunc = (parm1:string, parm2:number) => Promise 'myfunc' is a type alias for function that takes two parameters and returns a promise of boolean value.
interface is used to define classes. Member functions are defined as: myclass { mem_data1: string, print():void // print is a member function that takes no parameters and returns nothing. }
We often want to define a data type that has some fields in common and lets callers to extend it to customized types. Below is an example: // Base type (non-generic) type CommonDataType = { name: string, phone: string, age: number };
// Expanded type (explicit) type MyExpanded = { address: string, ssn: string };
// Generic type that merges BaseMyDataType with any expandable type T type MyDataType = CommonDataType & T;
In this example:
- CommonDataType defines the common part
- MyDataType defines a new data type that combines CommonDataType with the customized data type T.
var a:MyDataType = { name:"xxx", phone:"xxx", age:23, address:"xxx", ssn:"xxx" }
When extending a type with another, JavaScript merges their fields together. If duplicate names are found, JavaScript will merge the type. As an example, if 'name' appears in both CommonDataType and MyExpanded and their types are both 'string', they are merged into one field with type 'string'.
If CommonDataType.name is string but MyExpanded.name is number, TypeScript will merge them: string & number = never.
Note that 'never' is a TypeScript type only (static type only). Any operation on a 'never' type variable will cause a compile-time error. The 'never' type is, however, stripped off when the code is converted to JavaScript.
When composing a type by combining two or more types, the Required always take precedence over optional.
TypeScript offers type assert using "as", such as: var abc = { name: "Alice" as string }
There are two concepts regarding types: Compile Time Types and Runtime Types. TypeScript offers compile time types, which means that it checks the types at compile time. Some types exist in compile time only. In the above example, "as string" is a compile time type. It does not change the fact that 'name' type is 'string', even if one asserts it with another type, such as: var abc = { name: "Alice" as number }
Assume: type Customer { name: string, age: number }
var c1 Customer { name: "Alice", // a string age: 15 // a number }
var c2 Customer { name: "Alice" as number, // No compile error, but name is still string age: "abc" as number, // Compiler thinks "abc" is number, no errors. But will // have errors at runtime. }
The type assert is similar to cast in C++. C++ cast, however, physically converts the type, while TypeScript only suppress the compiling error.
IMPORTANT: use type assert with care, and use it rarely!
JavaScript is dynamically typed. The type is dynamically determined by the value it holds. TypeScript adds static type checking, which is, unfortunately, missed in JavaScript. When declaring variables, it can be declared in JavaScript style or TypeScript style: var foo1 // JavaScript, no static type checking var foo2: string // TypeScript, with static type checking
foo1 = "abc" // type: string foo2 = "abc" // type: string
foo1 = 123 // type: number foo2 = 123 // static type check error!
Need to stop at the files in dist, not the original files!
When we see something like: import { LoginForm } from '$components'; $components is an alias. It should be defined in svelte.config.js or vite.config.js
Note that $components is only the path. You need to check $components/index.js to determine the localtion of the symbol "LoginForm".
It defines an index signature. It means that the struct is an associated array. Any member whose type is string must be associated with ChartData. If we define: interface MyInterface { status: boolean; error_msg: string; [key:string]: ChartData } Compiler will report errors. The correct syntax is: interface MyInterface { status: boolean; error_msg: string; map_data: {[key:string]:ChartData} }
Docorator is a Python design pattern. It works as a wrapper on a function. Below is an example: def log_decorator(func): def wrapper(*args, **kwargs): # Wrapper function wraps the original print(f"Calling function: {func.name}") # Pre-function logic result = func(*args, **kwargs) # Execute original function print(f"Function {func.name} completed.") # Post-function logic return result # Return original result return wrapper # Decorator returns the wrapper
Use the decorator: @log_decorator def greet(name): print(f"Hello, {name}!")
The prints are: Calling function: greet Hellow, {name} Function greet completed
Example: from tenacity import retry, stop_after_attempt, wait_exponential @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=4, max=60) ) async def call_llm_api(prompt): response = await client.chat.completions.create(...) return response
This function uses @retry decorator, which uses tenacity's retry, stop_after_attemp, wait_exponential. The decorator should look like: def retry(num_attempts, min, max, multiplier, func): def wrapper(*args, **kwargs): # Wrapper function wraps the original // First attempt result = func(*args, **kwargs) # Execute original function if result: return result
// Second attempt
<the wait logic>
result = func(*args, **kwargs) # Execute original function
if result:
return result
...
return wrapper # Decorator returns the wrapper
When defining props, you can specify them mandatory or optional (name followed by ?). Below is an example: let { items, onselect }: { items: {...}[]; onselect: (item: { title: string; url: string; icon?: Icon }) => void; } = $props(); Both 'items' and 'onselect' are mandatory.
let {
items,
onselect
}: {
items: {...}[];
onselect?: (item: { title: string; url: string; icon?: Icon }) => void;
} = $props();
'items' is mandatory but 'onselect' is optional.
When calling a mandatory function, you can call it directly: function handleSelect(item: { title: string; url: string; icon?: Icon }) { onselect(item); }
But if it is an optional function, you need to check it: function handleSelect(item: { title: string; url: string; icon?: Icon }) { onselect?.(item); }
Prior to Svelte 5, props are specified using 'export let': export let <prop_name> <data_type> [ = default-value] such as: export let componentName: string = "Resource"; // Optional export let fetchRequest : JimoRequestInfo // Mandatory
In Svelte 5 (recommended):
First of all, we need to design the menu system data structure. We assume all menu items are processed by a main page, which should have a function such as:
<script lang="ts"> ... function handleMenuSelect(menu_item:???) </script>The function accepts one parameter. We need to design the parameter data type to handle different types of menu items.
There are a few methods in TypeScript to handle diversify data structures. The most common pattern is to use Unions. type BasicMenuItem = { id: string, title: string, icon: Icon, }
type NavigationMenuItem = { type: 'navigation', url: string }
type ActionMenuItem = { type: 'action', action: () => void }
type DataMenuItem = BaseMenuItem & { type: 'data'; data: any; // or a more specific type };
type MenuItem = NavigationMenuItem | ActionMenuItem | DataMenuItem;
let { menuItems }: { menuItems: MenuItem[] } = $props(); let activeView = $state<MenuItem | null>(null);
function handleMenuSelect(item: MenuItem) { switch (item.type) { case 'navigation': activeView = item.title; // Navigate to URL break; case 'action': item.action(); break; case 'data': activeView = item.title; // Handle data-specific logic console.log('Data:', item.data); break; } }
We will use routes/sidebar-07 as an example. sidebar-07 has two level menus. The first level is a menu container. The second level are actual menu items. sidebar-07 app-sidebar-07 - TeamSwitcher - - NavMain - NavProjects - NavUser
Svelte uses file-based routing convention:
- web/src/routes: each sub-directory defines a route. Its +page.svelte is the page for the route.
- web/src/routes/+page.svelte is the default route, or the route for localhost:port
Edit project/web/src/routes/+page.svelte. Below is an example:
<script lang="ts"> import { onMount } from 'svelte'; import { goto } from '$app/navigation'; onMount(() => { goto('/sidebar-01', { replaceState: true }); }); </script>In Svelte, reactive statements are the ones that will re-run automatically when any of the variables such a function references changes. Reactive statements are declared by $: let count = 0 let multiplier = 2
$: sum = count * multiplier
This is the Go notation of: () =>
In Go, a function is specified in form of: func()
HMR (Hot Module Replacement) is a development feature that allows you to see changes in your browser instantly without a full page reload. Under the hood, it uses WebSocket. The configuration: hmr: { // Connect HMR WebSocket to :5173 directly host: 'localhost', port: 5173, protocol: 'ws' }, configures the WebSocket: host, port and the protocol ('ws' for web socket)
In a production environment:
- It will not use HMR
- All accesses will go through port 8080 or 80 depending on the port the Go Echo listens to.
- The port 5173 is purely for development.
- It is important to note that port 5173 may by pass something (some of the Echo middleware check) that the production environment will always do. To test these features in development environments, you need to run it on the production port.
Please create a Prompt Store UI:
- Prompts are stored in the following table: Table Name: prompt_store, with the following fields: PromptID int64 # A unique sequence number PromptName string # Prompt name, unique, a string of letters, digts and underscores only PromptDesc string # Prompt description PromptContent string # Prompt content Status string # Prompt status, enum: Active, Deleted, Suspended PromptPurpose string # Prompt purpose PromptSource string # Prompt source, enum: Upload, User Entered, Web Crawled PromptKeywords string # Keywords for the prompt, used for searching PromptTags string # Tags for the prompt, used for searching Creator string # The user who created the prompt Updater string # The user who last updated the prompt CreatorLoc string # The caller (program location) that inserts the record UpdaterLoc string # The caller (program location) that last updates the record CreatedAt *string # The record creation time UpdatedAt *string # The record last update time
- Record Viewer: this page views prompts. It includes a query area. Users can query prompts by PromptID, PromptName, PromptDesc (use like), PromptContent (use like), Status, PromptKeywords, PromptTags, Creator, Updater, CreatedAt and UpdatedAt.
- Add new prompt: it should be a form.
- Upload prompts: it should be a form and a component to let users upload files
- Delete records
First of all, let's look at (cond) ? : If cond is truthy use ; otherwise falsy (null, undefined, false, 0, NaN, "")
(cond) ? ?? The differences are is false if it is null or undefined.
const v = def.default ?? ""
It means:
- If def.default is not null and not undefined, return def.default.
- Otherwise, return ""
Object.entries(...) takes an object and returns an array of string-keyed property pairs: [key, value].
Example:
It will generate the error because 'label' is not properly related to a control (in this case, the input).
.form-group input.error { ... }
It applies to elements that are decendent of class:"form-group" and the elements are input. .form-group .error { ... }
It is similar to the above except that it applies to any element, not just input.
Astro is a web framework that takes a unique approach by allowing you to build websites using multiple UI frameworks (including Svelte, Vue, and React) within the same project.
Framework Agnostic: Astro lets you use components written in React, Vue, Svelte, Solid, or other frameworks within a single project. You're not locked into one. In your example, the main page structure uses Astro's own syntax (.astro file), but it incorporates Vue components (.vue files) for the header, footer, and main content viewer.
Island Architecture: Astro uses an "Island Architecture" where interactive components (like your AppHeader, PromptRecordViewer, and AppFooter) are delivered as isolated "islands" of interactivity. Non-interactive parts of the page are rendered statically. This often leads to faster loading times compared to traditional SPA frameworks because less JavaScript needs to be shipped to the browser initially.
Static Site Generation (SSG) & Server-Side Rendering (SSR): Astro primarily focuses on building static sites (SSG) but also supports SSR. It pre-renders your pages to HTML at build time, which can improve performance and SEO. This contrasts with Svelte, Vue, or React when used traditionally as SPA frameworks, where rendering often happens client-side in the browser.
UI Frameworks: Svelte, Vue, and React are primarily concerned with building user interfaces and managing component state. They typically run entirely in the browser (though they support SSR via frameworks like Next.js for React or Nuxt.js for Vue). Astro uses these as components but manages the overall application structure, routing, and rendering strategy differently, often reducing client-side JavaScript bundle size.
In summary: While Svelte, Vue, and React are UI libraries/frameworks for building components and interfaces, Astro is a web framework that can use those UI frameworks as building blocks but provides its own rendering model focused on performance and flexibility in choosing your component technology.
Example:
...'preventDefault' is a Svelte's event modifier. Svelte provides the following event modifiers: preventDefault: Automatically calls event.preventDefault() stopPropagation: Automatically calls event.stopPropagation() stopImmediatePropagation: Automatically calls event.stopImmediatePropagation() once: The handler will only run once self: Only triggers if the event originates from the element itself, not child elements
The vertical bar is Svelte's way of applying event modifiers to events.
on is the standard HTML way to handle events. It espects a string containing JavaScript code. on: is Svelte's way to handle events. It expects a function, not a string.
In Svelte, we should always use on:
However, in Svelte 5, it reversed the syntax. Svelte 5 requires using on, not on:.
Event modifiers no longer work in Svelte 5. In other word,
will not work. If we need to handle the event modifiers, do it in the function, such as: func handleSubmit(event: Event) { ... }Note that the form can be specified in one of the following ways:
or: handleSubmit(e)}GET means "retrieve data" POST means "create a new resource" PUT means "update/replaces a resource" DELETE means "remove a resource"
In C++, we use a member data to tell the actual type. In Go, it can retrieve type name. Below is an example: 'condition' data type = AtomicCondition | ComplexCondition switch cond := condition.(type) { // retrieve the data type name case AtomicCondition: return buildAtomicCondition(cond)
case ComplexCondition:
return buildComplexCondition(cond)
case map[string]interface{}: // Note that map[string]interface{} is a data type (for JSON)
// Handle if condition comes as map (from JSON unmarshaling)
if op, exists := cond["operator"]; exists {
if _, isLogicalOp := op.(string); isLogicalOp {
// This is likely a ComplexCondition
return buildComplexConditionFromMap(cond)
}
}
// Otherwise treat as AtomicCondition
return buildAtomicConditionFromMap(cond)
default:
return "", fmt.Errorf("unknown condition type: %T", condition)
}
-
Send Request:
-
Create an instance of ApiTypes.JimoRequestInfo const req = { request_type: "db_opr", opr: "query", resource_name: "prompt_store", conditions: (JSON string), }
is_loading = true const resp = await fetch("/shared_api/v1/jimo_req", { method: "GET", headers: { "Content-Type": "application/json" }, body: JSON.stringify(req), credentials: 'include' }); if (!resp.ok) { console.log('Failed to fetch chart data (CWB_PST_044)'); is_loading = false no_data = true return }
-
-
The route is handled by Shared/go/RequestHandlers/HandleJimoRequest.go
-
Handler:
- Get the resource definition from the resource store
- Must have resource_def.ResourceType
- Must have resource_def.ResourceDef (JSON object)
- ResourceDef json: { "db_name":"xxx", "table_name":"xxx", "selected_field_names":["xxx",...], "field_data_types":["xxx",...], }
Example: class:error={errors.PromptName}
This (Svelte) is a conditional style binding.
Svelte translates: class:error={errors.PromptName} into: class:error = {errors.PromptName? true : false}
The above is implemented by the following JavaScript code: if (errors.PromptName) { element.classList.add("error"); } else { element.classList.remove("error"); }
Intuitively: if errors.PromptName is truthy: else:
const dataset = [ {...}, {...}, ... ] for (const data of dataset) { const value = newResource[data.id]; const validation = data.validation; ... }
You can define a generic type as: Record<string, unknown> or Record<string, any>
This is safe. It tells TypeScript that Record is a
Example: const UserFormData = { type: "select", required: true, default: "" } as const
When "UserFormData" is interpreted by TypeScript:
- It infers the data type of the above literal to: { type: string, required: boolean, default: string } This is widened types. The leading const is kind of defeating the meaning. It is possible to modify the value, such as: UserFormData.type = "input" The leading const disallows: UserFormData = {}
- If it is declared as const UserFormData = {...} as const, TypeScript will infer the type as narrowed type: { readonly type: "select"; readonly required: true; readonly default: ""; } Now it is not allowed to alter its values, such as: UserFormData.selet = "input" // disallowed
Example: const { schema } = $props<{ schema: Record<string, FormFieldDef>; }>();
let model = $state<Record<string, unknown>> ( Object.fromEntries(Object.entries(schema).map(([key, def]) => [ key, def.default ?? "" ]) ) );
In this example, 'schema is clearly declared as Record<string, FormFieldDef>. But 'Object.entries(schema)' will widen the data type of 'schema' to: [string, unknown][] This results in accessing the value 'def' will generate compiling errors. 'def' is of type 'unknown'.
One way to solve this problem is to tell TypeScript the data types, such as: let model = $state<Record<string, unknown>> ( Object.fromEntries(Object.entries(schema).map(([key, def])) => { const field = def as FormFieldDef return [ key, field.default ?? ""] }))
Example:
The above will have the errors: 'def' is of type 'unknown'. The reason is the same: TypeScript converts Object.entries(schema) to [string, unknown][]. This time, we can't simply modifies this code. Instead, we need to create a new variable 'items': const items = (Object.entries(schema) as [string, FormFieldDef][]) .map(([field, def]) => ({ field, def, Comp: FormItemMap[def.type] }));
then changes the {#each ...} to use items instead of schema: {#each items as item} <item.Comp bind:value={model[item.field]} label={item.def.label} required={item.def.required} helpText={item.def.helpText} options={item.def.options} /> {/each}
schema: Record<string, FormFieldDef>
Object.entries(schema).map(([key, def]) => {...}
- Object.entries(schema) generates a list of [key, def] ()
- For each entry, map(...) iterates over the entries to call the function {...} with the parameter [key, def] for each entry.
If def is a Zod object, the above won't work. It should use Zod's shape: const shape = schema.shape; // Get the shape object Object.entries(shape).map(([key, def]) => { // Process each field definition }); In other word, schema.shape converts Zod data structure into a normal JavaScript object.
- The data is a free-form JSON: data: any = {};
- Create a Zod data structure
- Call the Zod data structure's function safeParse(data)
Below is an example. import z from 'zod'
const MyCustomData = z.object( name: z.string(), age: z.number().optional )
const result = MyCustomData.safeParse(data) if (result.success) { const validated_data = result.data } else { console.log("Failed parsing:" + result.error.flattern()) }
Note that Zod offers two parsing functions: parse(data) and safeParse(data). The latter does not throw exception but returns a structure that has two members: success (boolean) and data ({})
Making Zod dynamic means we can let users define their schemas through web pages, serialize the zod object to database, load the schema back to memory, convert it to a zod object, and use it to validate data. Below are the steps:
Step 1: zod-to-json-schema import { z } from "zod"; import { zodToJsonSchema } from "zod-to-json-schema";
const FormFieldSchema = z.object({
name: z.string(),
age: z.number().int().min(0),
});
const json = zodToJsonSchema(FormFieldSchema);
console.log(JSON.stringify(json));
Step 2: Save to DB It needs to convert JSON object to string:
Step 3: JSON to Zod import { jsonSchemaToZod } from "json-schema-to-zod";
const jsonFromDB = ...;
const zodString = jsonSchemaToZod(jsonFromDB); // returns code string
const dynamicSchema = eval(zodString);
dynamicSchema.parse({ name: "Alice", age: 30 });
Example: let model = $state<Record<string, unknown>>( Object.fromEntries( Object.entries(schema).map(([key, def]) => { const field = def as FormFieldDef return [key, field.default ?? ""] } )));
The purpose of the above code is to construct a map (i.e., Record<string, unknown>) from schema, which is a map (Record<string, FormFieldDef>):
- Object.entries(schema) returns a list of the entries in schema: an array of [key, def]
- map iterates on the entries in the list, convert it to [key, field.default | ""], which is an array.
- Object.fromEntries(array) constructs a map from an array.
'writable' is a Svelte feature. It creates a reactive store. import { writable, type Writable } from 'svelte/store'; const my_store = writable('list')
It creates a store 'my_store'. Its value is 'list'. Its value type is 'string' (inferred). A store offers two functions: set(...), update(...), and subscribe(...). Any component that subsribes a store will be re-rendered whenever the store changes. $ is the shorthand of subscribing a store.
Below shows how to use it:
Simple Case (the store has only one value) <script> import { InMemStoreCurrentView } from './stores.js'; </script>
<!-- Read current value -->
<p>Current view: {$InMemStoreCurrentView}</p>
<!-- Update value -->
<button on:click={() => $InMemStoreCurrentView = 'grid'}>
Switch to grid
</button>
Or: InMemStoreCurrentView.set('grid'); // or InMemStoreCurrentView.update(view => view === 'list' ? 'grid' : 'list');
Note that update(func) takes a function as its input. The function: (exist_value) => {...}
The function MUST return a value that serves as the new value of the store. In case of: InMemStoreCurrentView.update(view => view === 'list' ? 'grid' : 'list');
view => view === 'list' ? 'grid' : 'list
is a function. The function's input is 'view', the store's current value. The function returns a string, either 'list' or 'grid', depending on its current value.
Important: if a store's update(func) is conditional, such as: InMemStoreCurrentView.update(view => view !== 'list' ? 'list' : 'list');
When the update(...) is called, it may not actually change anything. Its subscribers, if any, will not re-render if a update(...) call did not change anything (important).
More complicated Case: a map store. export const InMemStoreCrtRecords: Writable = writable({});
To access a member: <script> import { InMemStoreCrtRecords } from './stores'; </script>
{#if $InMemStoreCrtRecords['rec123']}
<p>Name: {$InMemStoreCrtRecords['rec123'].name}</p>
{/if}
Subscribe it: let currentRecords; const unsubscribe = InMemStoreCrtRecords.subscribe(value => { currentRecords = value; });
// Now use currentRecords['rec123']...
// Don’t forget to unsubscribe when done!
Update: InMemStoreCrtRecords.update(records => ({ ...records, rec123: { ...records.rec123, name: 'New Name' } }));
// DON’T do this: const current = $InMemStoreCrtRecords; current.rec123.name = 'New Name'; // ❌ No reactivity!
Create a helper function for a store is very helpful: function updateRecord(id: string, updates: Partial) { InMemStoreCrtRecords.update(records => ({ ...records, [id]: { ...records[id], ...updates } })); }
This function intents to update the record identified by the value of 'id' in the store 'InMemStoreCrtRecords', with the new values 'updates'. Note that updating means:
- Add a new attribute if it does not exist
- Override the attribute if it exists
The notation '[id]' means: the attribute whose name is the value of 'id'. If you write (id without []): function updateRecord(id: string, updates: Partial) { InMemStoreCrtRecords.update(records => ({ ...records, id: { ...records[id], ...updates } })); } it means the attribute whose name is 'id'. Similarly, records[id] does not mean records['id']. It means the value of 'id'.
Example: <script lang="ts"> import {InitInMemStore, StoreMap} from "$lib/stores/store"
let componentStores = StoreMap.get("my_store");
if (!componentStores) {
InitInMemStore("my_store")
componentStores = StoreMap.get("my_store")
}
// Access the stores for this component
const { InMemStoreCrtRecords, InMemStoreCurrentView } = componentStores;
...
</script>
The above code will have a compile error: Property 'InMemStoreCrtRecords' does not exist on type '{ InMemStoreCrtRecords: Writable; InMemStoreCurrentView: Writable; } | undefined'.
The reason is that TypeScript assumes the type of componentStores can be undefined. If it is undefined, there are no 'InMemStoreCrtRecords'. So it generates the error.
We as human develpers 'believes' it should not be 'undefined', but this is only our belief. If we want to tell TypeScript our belief, do the following: <script lang="ts"> import {InitInMemStore, StoreMap} from "$lib/stores/store"
let componentStores = StoreMap.get("my_store");
if (!componentStores) {
InitInMemStore("my_store")
componentStores = StoreMap.get("my_store")! // Very important, '!' tells everything.
}
// Access the stores for this component
const { InMemStoreCrtRecords, InMemStoreCurrentView } = componentStores;
...
</script>
Example: <script lang="ts"> import {get} from 'svelte/store' import {InitInMemStore, StoreMap} from "$lib/stores/store"
let componentStores = StoreMap.get("my_store");
if (!componentStores) {
InitInMemStore("my_store")
componentStores = StoreMap.get("my_store")! // Very important, '!' tells everything.
}
// Access the stores for this component
const { InMemStoreCrtRecords, InMemStoreCurrentView } = componentStores;
console.log("LimitStart:" + InMemStoreCrtRecords.LimitStart) // Wrong, cannot access 'LimitStart'
const current = get(InMemStoreCrtRecords)
console.log("LimitStart:" + current.LimitStart) // OK
console.log("LimitStart:" + $InMemStoreCrtRecords.LimitStart) // OK
</script>
The reason why you cannot use "InMemStoreCrtRecords.LimitStart" is because 'InMemStoreCrtRecords' is an object (kind of 'program') for the store. It is an object like: { subscribe: (fn) => { ... }, set: (value) => { ... }, update: (fn) => { ... } } It is not the actual store.
The statement: const current = get(InMemStoreCrtRecords) retrieves the store by the object. Note that this is not reactive.
If you want to be reactive, use $InMemStoreCrtRecords. This means that '$' converts the object to the store.
export function GetAllStoreNames() {
return Array.from(StoreMap.keys());
}
StoreMap is a map: {[key:string], any}
satisfies is an alternative to an explicit variable type annotation. It tells TypeScript that your assignment should be at least assignable to the provided type. It’s kind of like a type-safe way to cast values.
const person1 = {
name: "Jerred",
} satisfies {name: "Jerred" };
Example: The package is: "github.com/shopspring/decimal"
Run "go get" in server directory: go get github.com/shopspring/decimal
When a parent creates children by data, we need to get the children objects so that the parent can call children's functions. This is done by bind:this = {childComponents[i]}, such as the following:
<script lang="ts"> ... let itemComponents: any[] = [] </script>In this example, the parent creates children by the data 'items'. The child component instances are stored in itemComponents. Note that you do not need to manually add the component instances to itemComponents. Svelte does it automatically when it executes: bind:this={itemComponents[i]}
In TypeScript: if (Object.hasOwn(json_doc, "member_name")) { return "exist" }
Below is an example: data := map[string]interface{}{ "name": "Alice", "age": 30, "active": true, "tags": []string{"user", "admin"}, }
// Check if "name" exists and is a string
if val, exists := data["name"]; exists {
if name, ok := val.(string); ok {
fmt.Println("Name:", name) // ✅ safe
} else {
fmt.Println("'name' is not a string")
}
} else {
fmt.Println("'name' is missing")
}
In Svelte 5, you cannot access members of a store directly. You must subscribe to the store and then access its members. You can subscribe a store by calling subscribe(...) or use the simple notation of "$".
-
Use subscribe(...) const unsubscribe = InMemStore.subscribe((value) => { console.log('Current CrtRecord:', value.CrtRecord); // React to changes here });
// Later, unsubscribe to avoid memory leaks unsubscribe();
Once subscribed to a store, the store (i.e., 'value' in the above example) is reactive.
When you do not need it anymore, make sure you call unsubscribe().
-
Use $: const InMemStore = GetStoreByName("prompt_store") as Writable crt_record = $InMemStore.CrtRecord // CrtRecord is a member of InMemStoreDef
+layout.svelte is a special file in Svelte. If a directory in src/routes or any of its subdirectories have a +layout.svelte, the file is normally used to manage the common part of all pages in the directory. +layout.svelte is not mandatory. But if it exists, it must contain a . Otherwise, contents won't show.
People use +layout.svelte to manage pages. Below is an example. src/routes/ ├── +layout.svelte # Site-wide layout (global header, footer, etc.) ├── +page.svelte # Home page ├── about/ │ └── +page.svelte # About page (uses root layout) ├── dashboard/ # Dashboard section │ ├── +layout.svelte # Dashboard-specific layout (sidebar, etc.) │ ├── +page.svelte # Dashboard home │ ├── settings/ │ │ ├── +layout.svelte # Settings-specific layout (if needed) │ │ └── +page.svelte # Settings page │ └── profile/ │ └── +page.svelte # Profile page (shares dashboard layout) └── admin/ # Admin section ├── +layout.svelte # Admin-specific layout (admin sidebar, etc.) ├── +page.svelte # Admin home └── users/ └── +page.svelte # Users page (shares admin layout)
Example: In a ts file: import {Writable} from 'svelte/store'
Got the following error: Cannot find module 'svelte/store' or its corresponding type declarations.
The reason is that the project does not know anything about 'svelte/store'.
Run the following command in the project root directory: bun add svelte
interface{} is a generic type in Go. 'any' is an alias of interface{}. interface{} and any are 100% compatible. 'any' is just syntactic suger and introduced in Go 1.18.
Note that unknown is not Go's type. It is TypeScript type.
map[string]interface{} implements a map that maps keys to values of any type. It uses more memory. Each value requires + data memory.
map[string]struct{} implements Set. It does not store values, thus uses less memory.
======================================================================================= Aspect map[string]interface{} map[string]struct
Purpose Key-value store with arbitrary values Set (keys only) Value size Variable (carries type info + data) 0 bytes (empty struct) Memory usage Higher overhead Minimal overhead Use case JSON data, dynamic objects Membership testing, deduplication operations Get/set values of any type Check existence, add/remove keys
In PostgreSQL:
- Arrays are defined as VARCHAR(nn)[], TEXT[], ...
- Insert uses ARRAY(['xxx', 'xxx', ...]) or ('{xxx, xxx, ...}')
Below is an example: -- Create table with string array CREATE TABLE example ( id SERIAL PRIMARY KEY, tags TEXT[], categories VARCHAR(50)[] );
-- Insert values
INSERT INTO example (tags, categories)
VALUES (ARRAY['tag1', 'tag2', 'tag3'], ARRAY['cat1', 'cat2']);
-- Or using PostgreSQL syntax
INSERT INTO example (tags, categories)
VALUES ('{tag1,tag2,tag3}', '{cat1,cat2}');
MySQL does not directly support arrays, you can treat arrays as strings, such as: CREATE TABLE example ( id INT PRIMARY KEY, tags VARCHAR(255) );
INSERT INTO example (tags) VALUES ('tag1,tag2,tag3');
Or use JSON: CREATE TABLE example ( id INT PRIMARY KEY, tags JSON );
INSERT INTO example (tags) VALUES ('["tag1", "tag2", "tag3"]');
Go: import "github.com/lib/pq"
// In your struct
type Example struct {
ID int
Tags []string `db:"tags"`
}
// When inserting
stmt, _ := db.Prepare("INSERT INTO example (tags) VALUES ($1)")
tags := []string{"tag1", "tag2", "tag3"}
_, err := stmt.Exec(pq.Array(tags))
For prepared statements:
- Determine whether a field is an array of text
- Check the value type: string, string array, array of generric type ([]interface{}), nil,
Below is an example: log.Printf("processing array-string field:%s (SHD_DUP_095)", f.FieldName) var stringArray []string
switch v := val.(type) {
case []string:
stringArray = v
case []interface{}:
// Convert []interface{} to []string
stringArray = make([]string, len(v))
for i, item := range v {
if item != nil {
stringArray[i] = fmt.Sprintf("%v", item)
} else {
stringArray[i] = ""
}
}
case string:
// If it's a single string, you might want to treat it as single-element array
stringArray = []string{v}
case nil:
stringArray = []string{}
default:
// Try to handle other types that might represent arrays
stringArray = []string{fmt.Sprintf("%v", v)}
}
args = append(args, pq.Array(stringArray))
placeholders = append(placeholders, fmt.Sprintf("$%d", paramCounter))
paramCounter++
Use push: var update_entries: Record<string, string>[] = [] // Initialize to an empty array
update_entries.push({user_name:"abc", email:"aaa@my.domain})
import "math"
// Integer types math.MaxInt8 // 127 math.MinInt8 // -128 math.MaxInt16 // 32767 math.MinInt16 // -32768 math.MaxInt32 // 2147483647 math.MinInt32 // -2147483648 math.MaxInt64 // 9223372036854775807 math.MinInt64 // -9223372036854775808
PostgreSQL can use BIGSERIAL to automatically generate sequence numbers. We can use 'RETURNING <field_name>' to return its value in an insert statement: INSERT INTO your_table (column1, column2, ...) VALUES (value1, value2, ...) RETURNING prompt_id;
Write comments on top of a function (no empty lines between the comments and the function) Example: // InsertBatch inserts multiple records into the given table and returns // the auto-generated prompt_id values for the inserted rows. // // Parameters: // - tx: an active database transaction // - tableName: name of the target table (must be sanitized to avoid SQL injection) // - columns: list of column names to insert into (e.g., []string{"title", "content"}) // - values: a slice of rows, where each row is a slice of interface{} values // // Returns: // - A slice of int64 containing the generated prompt_id values, in the same order // as the input rows. // - An error if the insertion fails. // // Note: This function uses fmt.Sprintf to interpolate table and column names, // so callers must ensure they are not derived from untrusted input. func InsertBatch( user_name string, db *sql.DB, tableName string, ...) (...) { ... }
Go does not support the '&' operator. We can embed a type in another type, such as: type Base struct { ID int64 Name string }
type Extended struct { Base CreatedAt int64 }
When to read data from PostgreSQL string array, there are two methods:
- []string
- pg.StringArray
pg.StringArray has a []string internally, but it implements:
- Scanner interface so that data can be scanned directly to pgStringArray
- Valuer interface so that it can be inserted into SQL queries
func (a *StringArray) Scan(src interface{}) func (a *StringArray) Value()
But modern pg driver can scan both []string and pg.StringArray Examples var tag1 []string var tag2 pgStringArray
rows.Scan(&tag1) // OK rows.Scan(&tag2) // OK
could not import github.com/chendingplano/shared/go/api/sysdatastores (missing metadata for import of "github.com/chendingplano/shared/go/api/sysdatastores")
This is normally a temporary problem. Run the following in the directory the 'go.mod' resides: go mod tidy
Vite is a frontend build tool. Its core functions:
- Serves your Svelte app during development
- File Watching: watches .svelte, .js and .ts files
- Once files are changed, it communites with browsers through WebSocket
- HMR (Hot Module Replacement): it updates only the affected modules, used together with file watching.
- Routing: file-based routing (web/src/routes)
- SSR: renders pages on server first
sudo /nix/var/nix/profiles/default/bin/nix-daemon &
Look at svelte.config.js, If you see something like: import adapter from '@sveltejs/adapter-auto';
export default {
kit: {
adapter: adapter()
}
};
or anything containing a kit: block →
You are using SvelteKit 2. The key presence:
- kit
- a kit adapter
If the file looks like this (no kit block): import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default {
preprocess: vitePreprocess()
};
You are using plain Svelte 5 (no Kit).
SvelteKit adds the abilities:
- File-based routing (src/routes)
[WEB] 3:34:50 PM [vite] (ssr) Error when evaluating SSR module /src/routes/calendar/+page.svelte: [vite] The requested module '@event-calendar/core' does not provide an export named 'Calendar' [WEB] at analyzeImportedModDifference (file:///Users/cding/Workspace/ChenWeb/web/node_modules/vite/dist/node/module-runner.js:457:36) [WEB] at SSRCompatModuleRunner.processImport (file:///Users/cding/Workspace/ChenWeb/web/node_modules/vite/dist/node/module-runner.js:1006:54) [WEB] at SSRCompatModuleRunner.cachedRequest (file:///Users/cding/Workspace/ChenWeb/web/node_modules/vite/dist/node/module-runner.js:1031:59) [WEB] at async eval (/Users/cding/Workspace/ChenWeb/web/src/routes/calendar/+page.svelte:5:31) [WEB] at async ESModulesEvaluator.runInlinedModule (file:///Users/cding/Workspace/ChenWeb/web/node_modules/vite/dist/node/module-runner.js:906:3) [WEB] at async SSRCompatModuleRunner.directRequest (file:///Users/cding/Workspace/ChenWeb/web/node_modules/vite/dist/node/module-runner.js:1113:59) [WEB] at async SSRCompatModuleRunner.directRequest (file:///Users/cding/Workspace/ChenWeb/web/node_modules/vite/dist/node/chunks/dep-Chhhsdoe.js:18607:22) [WEB] at async SSRCompatModuleRunner.cachedRequest (file:///Users/cding/Workspace/ChenWeb/web/node_modules/vite/dist/node/module-runner.js:1031:73) [WEB] at async SSRCompatModuleRunner.import (file:///Users/cding/Workspace/ChenWeb/web/node_modules/vite/dist/node/module-runner.js:992:10) [WEB] at async instantiateModule (file:///Users/cding/Workspace/ChenWeb/web/node_modules/vite/dist/node/chunks/dep-Chhhsdoe.js:18580:10) [WEB] [WEB] [500] GET /calendar [WEB] SyntaxError: [vite] The requested module '@event-calendar/core' does not provide an export named 'Calendar' [WEB] at analyzeImportedModDifference (file:/Users/cding/Workspace/ChenWeb/web/node_modules/vite/dist/node/module-runner.js:457:36) [WEB] at SSRCompatModuleRunner.processImport (file:/Users/cding/Workspace/ChenWeb/web/node_modules/vite/dist/node/module-runner.js:1006:54) [WEB] at SSRCompatModuleRunner.cachedRequest (file:/Users/cding/Workspace/ChenWeb/web/node_modules/vite/dist/node/module-runner.js:1031:59) [WEB] at async eval (src/routes/calendar/+page.svelte:5:31)
This is a runtime error. SvelteKit/Vite tries to resolve code on the server at runtime but failed. What the error says is that the code tries to import a named export 'Calendar' but not found in the package.
To solve this problem, we need to disable its SSR. Below is an example: <script lang="ts"> import { onMount } from 'svelte';
let Calendar = $state<any>(null);
let TimeGrid = $state<any>(null);
let options = $state({
view: 'timeGridWeek',
events: [
// your events
]
});
onMount(async () => {
const mod = await import('@event-calendar/core');
Calendar = mod.Calendar;
TimeGrid = mod.TimeGrid;
});
</script>
{#if Calendar}
<Calendar plugins={[TimeGrid]} {options} />
{/if}
Instead of statically import 'Calendar', it dynamically loaded (through onMount). The onMount function runs in browsers. As for whether 'mod' has a valid mod.Calendar and mod.TimeGrid, it is up to the provider. If it works as documented, it should have it.
The superForm() function returns an object that includes (among other things):
form: a reactive store-like object containing: .data — the current form field values .errors — validation errors .constraints — HTML5 validation attributes .tainted — which fields have been touched .valid — whether the form is valid enhance — an action for progressive enhancement on
When passing props to a child, we can say {}, which is the shorthand of: ={}. Below is an example: <script> let id = "my-input"; let value = "hello"; let disabled = true; </script>
<Input {id} {value} {disabled} />
This is equivalent to: <Input id={id} value={value} disabled={disabled}
ln -s
const currentDate = new Date(2025, 11, 5); // December 5, 2025 (months are 0-indexed in JS)
const year = currentDate.getFullYear(); // 2025
const month = currentDate.getMonth(); // 11 (December)
const lastDay = new Date(year, month + 1, 0).getDate();
In JavaScript, day 0 means the last day of the previous month. Since Date(year, month + 1, 0) is month 13, its previous month is December, and 0 means the last day of that month: lastDay should be 31.
When you declare a state variable, such as: let selectedTime = $state<TimeSlot | null>(null) Svelte will create a state variable, or wraps the value with a reactive proxy. The proxy is not just the value but have other properties for Svelte to handle the reactivity.
If the state variable is later on assigned, be careful. func handleTimeSelected(time: TimeSlot) { selectedTime = time; // This will generate an error. }
The reason why it will generate an error is because 'time' is a plain object (or instance) of type 'TimeSlot', while 'selectedTime' is a proxy object.
Example: export const CustomHttpStatus = { NotLoggedIn: 550, ... } as const;
Http Responses are streamed back, which means that contents can only
be read once.
The code below calls 'const text = await resp.text()'.
if (!resp.ok) {
const text = await resp.text(); // 👈 FIRST read: consumes the response body
if (resp.status !== 550) {
// ... return error
}
//
Once this function is called, the response content was retrieved and is gone. You cannot call it again, such as: const json_value = await resp.json() as JimoResponse
The intent is that if resp.status != 555, report the error. Otherwise, I know the results should be a JSON doc. It calls 'await resp.json()' again. This is an error.
We can either defer calling 'resp.text()' or let JSON parse the content returned by 'resp.text()'.
When working in DEV mode, the URL is localhost:5173, but the backend is localhost:8080. When sending an http request to the backend, the backend looks for routes on localhost:5173. But routes are not registered on localhost:5173. They are registered on localhost:8080.
When this happens, we need to config vite.config.js to proxy the root, such as: ChenWeb/web/vite.config.ts: server: { port: 5173, hmr: { // Connect HMR WebSocket to :5173 directly host: 'localhost', port: 5173, protocol: 'ws' }, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, secure: false, }, '/auth': { target: 'http://localhost:8080', changeOrigin: true, secure: false, }, '/shared_api': { target: 'http://localhost:8080', changeOrigin: true, } } },
The 'server' configuration tells:
- The server is at localhost:5173,
- It proxies '/api', '/auth' and '/shared_api' to 'localhost:8080'
Note: need to configure the Use function, too: Each application should configure its echo.HandlerFunc, such as the one in ChenWeb/server/api/routes.go: e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { path := c.Request().URL.Path
// Public endpoints.
// IMPORTANT: need to load from a configuration file!
publicPaths := map[string]bool{
"/": true,
"/login": true,
"/example-login": true,
// Add other public pages here
}
// Let Echo handle / so we can redirect it
if publicPaths[path] {
log.Printf("Public URL:%s (CWB_RTS_080)", path)
return next(c)
}
// Paths not starting with "/api", "/auth", "/shared_api", or "/_" are treated as frontend routes
if !strings.HasPrefix(path, "/api") && // handle '/api'
!strings.HasPrefix(path, "/auth") && // handle '/auth'
!strings.HasPrefix(path, "/shared_api") && // handle 'shared_api'
path != "/_" &&
!strings.HasPrefix(path, "/_/") {
...
Do not add too many root routes, such as '/api', '/auth', and '/shared_api' because each requires at least two configurations, one in vite.config.ts and one in the routes.go.
We can, theoretically and practically, use just one root: '/api'. The root can then have sub-categories, such as: /api/rag /api/auth /api/shared_api /api/whatever
In HTML, any button inside a that does not have type = "button" (or type="reset") will be treated as a submit button. If you do not want a button to behave like a submit, make sure you type it correctly.
- Create an index.ts, if not yet
- Add export { default as } from './.svelte'
- To import it: import {} from '$lib//index.js'
When a component is created in the frontend, its server side may not be ready yet. If the component tries to fetch onMount, the backend may generate this error.
To prevent this, add a condition. Below is an example (Shared/web/src/lib/stores/auth.svelte.ts): function createAuthStore() { ... const checkAuthStatus = async () => { ... const response = await fetch('/auth/me', ...) ... }
// Don't call 'checkAuthStatus()' immediately:
checkAuthStatus() // Don't do this
if (typeof window !== 'undefined') {
// This means this code is running in a browser, not SSR.
checkAuthStatus() // Now, it should be okey
}
}
Check its type and length: function isValidNonEmptyString(str: unknown): str is string { return typeof str === 'string' && str.length > 0; }
// Usage
const myString = "hello";
if (isValidNonEmptyString(myString)) {
// myString is now typed as string (not unknown)
console.log(myString.toUpperCase());
}
- interface{} (or any since Go 18) covers all data types in Go
- Go data type classifications include:
- Basic Types
- Boolean: bool
- Numeric: int, int64, float64, complex128
- String: string
- Composite/Reference Types
- Arrays: [4]int
- Slices: []int
- Maps: map[string]int
- Structs: struct{ Name string }
- Pointers: *int
- Functions: func() int
- Channels: chan int
- Interfaces: interface{}
- Interface Types
- Empty interface: interface{}
- Basic Types
About interface{}
- interface{} accepts any data type, or any type can be assigned to interface{}
- The interface{} stores both the value and its concrete type
- To use the value of interface{}, you must assert it to a concrete type: var x interface{} = 42 if num, ok := x.(int); ok { result := num + 1 }
PocketBase supports subscription (or listens to) events:
- A collection (or a table) may have 'create', 'update', 'delete' events
- Anyone can subscribe to these events
- When one client performs an action on the collection, such as add, delete or modify records, Pocketbase broadcasts the event to all its subscribers (through Web Socket, I believe).
- A client that subscribes to the events of the collection is called back. It can use the event to update its local pages.
- When a client exits, it should call a function to unsubscribe the event.
Below is an example: let unsubscribeFunc = () => {}; pb.collection('documents') .subscribe('*', (e) => { processDocumentUpdate(e.record, e.action as actionType); }) .then((unsubscribe) => { unsubscribeFunc = unsubscribe; });
return () => {
unsubscribeFunc();
};
This code subscribes to all the events on the collection 'documents'. The 'subscribe' function will return an unsubscribe function.
-
Assume files are stored in rootdir/data
-
Files can be stored in sub-directories
-
Configure vite.config.ts, to add: '/files': { target: 'http://localhost:8080', changeOrigin: true, secure: false } where 'files' serve as the root of file url, such as: localhost:5173/files/username/filename
-
In router.go: func RegisterRoutes() (*echo.Echo, error) { ... e.Use(func(next echo.HandlerFunc) echo.HandlerFunc { ... if !strings.HasPrefix(path, "/api") && !strings.HasPrefix(path, "/files") && ... })
-
Add the route: In router.go: e.GET("/files/:customerId/:filename", func(c echo.Context) error {...})
Note: ":customerId" expects a string as a variable that identifies the user ":filename" is a variable that identifies the file.
TypeScript is pretty powerful in composing types. The operator '&' can be used to compose two or more types into one. type MyType1 = { field1: string }
type MyType2 = {
field2: string
}
type MyComposedType = {
field3: string
} & MyType1 & MyType2
In addition, we may define: type ExpandType = unknown extends T ? { expand?: unknown } : { expand: T } to 'expand' a type by adding a new attribute named 'expand'. Note: the only case 'unknown extends T' is true is T is itself unknown. The above expression simply says: type ExpandType is {expand?: unknown} if T is unknown. Otherwise, it is {expand: T} The "?" in {expand?: unknown} is the author's opinion. It reflects the fact that if T is unknown, the type of 'expand' is unknown, or uncertain. The attribute 'expand' is thus viewed as optional. If T is a concrete type, it is 'certain'. The attribute 'expand' is, therefore, not optional but required. The choice of '?' is opinionated, but it is a common practice.
Below is an example. type ExpandType = unknown extends T ? { expand?: unknown } : { expand: T }
// System fields
export type BaseSystemFields<T = unknown> = {
id: string
tableName: string
} & ExpandType<T>
type DocumentRcd = {
file_url: string
user_name: string
}
type PossibleAdditionalFields = {
phone_number string
}
export type DocumentRecord<Texpand = unknown> = Required<DocumentRcd> & BaseSystemFields<Texpand>
const my_record : DocumentRecord<PossibleAdditionalFields> = {...}
my_record will be: { file_url: string // from DocumentRcd user_name: string // from DocumentRcd id: string // from BaseSystemFields tableName: string // from BaseSystemFields expand: { phone_number: string // from PossibleAdditionalFields } }
If: const my_record : DocumentRecord = {...} my_record will be: { file_url: string // from DocumentRcd user_name: string // from DocumentRcd id: string // from BaseSystemFields tableName: string // from BaseSystemFields expand?: unknown }
- Types Compatible: pick the narrow one: field1: string | number field1: number result: field1: number
- Types Not Compatible: type = never!!! field1: string field1: number result: field1: never
- Types Are Identical: de-duplicate field1: string field1: string result: field1: string
- The above applies recursively
if (Array.isArray(myArray) && myArray.length > 0) { // It's a real array and has at least one element }
Add the following line in a file that you do not want to run it in the backend: export ssr = false
By default, ssr = true
Example: const ( GreaterThan Operator = ">" GreaterEqual Operator = ">=" Equal Operator = "=" LessEqual Operator = "<=" LessThan Operator = "<" NotEqual Operator = "<>" Contain Operator = "contain" Prefix Operator = "prefix" )
type CondDef struct {
FieldName string json:"field_name"
Opr string json:"opr"
Value interface{} json:"value"
}
To cast Opr (string) to Operator: opr := Operator(cond.Opr)
This is different from cond.Opr.(Operator), which is Type Assertion. You cannot assert a type unless it is interface{}, any or other interfaces. Since condOpr type is string, we cannot type assert it.
Below is an example from Pochetbase: export type TemplateQuestionsRecord = { id: string order: number question: RecordIdString required?: boolean template: RecordIdString updated: IsoAutoDateString created: IsoAutoDateString }
export type QuestionsResponse<Tchoices = unknown, Texpand = unknown> = Required<QuestionsRecord> & BaseSystemFields
export type BaseSystemFields<T = unknown> = { id: RecordIdString collectionId: string collectionName: Collections } & ExpandType
export type TemplateQuestionsResponse<Texpand = unknown> = Required & BaseSystemFields
type TemplateQuestionLink = TemplateQuestionsResponse<{ question: QuestionsResponse }>;
What is TemplateQuestionLink?
-
TgemplateQuestionLink = Required & BaseSystemFields<{question: QuestionResponse}> => { id: string order: number question: RecordIdString required?: boolean template: RecordIdString updated: IsoAutoDateString created: IsoAutoDateString } BaseSystemFields<{question: QuestionResponse}> => { // From TemplateQuestionRecord id: string order: number question: RecordIdString required?: boolean template: RecordIdString updated: IsoAutoDateString created: IsoAutoDateString
// From BaseSystemFields collectionId: string collectionName: Collections
// From ExpandType expand: { question: QuestionResponse } }
Assume tax needs to use Shared: in Shared/svelte, run: bun link
in tax/web, run: bun link @chendingplano/shared
Then in tax/, we can use symbols defined in Shared/, such as: import { db_store } from '@chendingplano/shared';
// The shape of CollectionRequest: // If CollectionRequest: // { // <all attributes in CollectionRequestsRecord, required> // reminderRecipientEmails: string[], // // expand?: unknown // } // // If CollectionRequest<{project: ProjectsResponse}>; // { // <all attributes in CollectionRequestsRecord, required> // reminderRecipientEmails: string[], // // expand?: { // // } // } // In other word, it is CollectionRequestsRecord + BaseSystemFields + ProjectsResponse // If CollectionRequest<{project: ProjectsResponse}>;
id: string
project: RecordIdString
taxYear: number
sent?: boolean
dueDate?: IsoDateString
lastActivity?: IsoDateString
scope: CollectionRequestsScopeOptions
template?: RecordIdString
disclaimerAcceptedAt?: IsoDateString
status: CollectionRequestsStatusOptions
reminderRecipientEmails?: null | TreminderRecipientEmails
scheduledSendAt?: IsoDateString
sendToAllContacts?: boolean
created: IsoAutoDateString
updated: IsoAutoDateString
reminderRecipientEmails
id: RecordIdString
collectionId: string
collectionName: Collections
project: { id: string assignedPreparer?: RecordIdString ballInCourt: ProjectsBallInCourtOptions client: RecordIdString created: IsoAutoDateString dueDate?: IsoDateString isOpen?: boolean projectName: string projectStage: ProjectsProjectStageOptions projectStatus: ProjectsProjectStatusOptions projectType: ProjectsProjectTypeOptions startDate?: IsoDateString updated: IsoAutoDateString } }
The browser (Chrome) reported errors: app.js:115 ReferenceError: global is not defined at node_modules/@iarna/toml/lib/create-date.js (@iarna_toml.js?v=16dff8dc:189:20) at __require (chunk-G3PMV62Z.js?v=16dff8dc:8:50) at node_modules/@iarna/toml/lib/toml-parser.js (@iarna_toml.js?v=16dff8dc:259:22) at __require (chunk-G3PMV62Z.js?v=16dff8dc:8:50) at node_modules/@iarna/toml/parse-string.js (@iarna_toml.js?v=16dff8dc:1589:22) at __require (chunk-G3PMV62Z.js?v=16dff8dc:8:50) at node_modules/@iarna/toml/parse.js (@iarna_toml.js?v=16dff8dc:1736:23) at __require (chunk-G3PMV62Z.js?v=16dff8dc:8:50) at node_modules/@iarna/toml/toml.js (@iarna_toml.js?v=16dff8dc:2007:22) at __require (chunk-G3PMV62Z.js?v=16dff8dc:8:50)
Claude helped find the reasons: Now I can see the issue! The error ReferenceError: global is not defined is coming from the @iarna/toml package (line 68 in package.json). This package is trying to access the Node.js global object, which doesn't exist in the browser environment. Let me search for where this package is being imported. That is, it tries to find who is trying to import the file.
This is done by: Grep "@iarna/toml" (in /Users/cding/Workspace/Shared)
Root Cause The @iarna/toml package at line 2 of UtilFuncs.ts is imported at the module level. When Vite bundles this for the browser, the TOML library tries to access Node.js's global object, which doesn't exist in browsers, causing the error and preventing onMount() from being called.
The solution is to add a Vite configuration to polyfill the global object for browser compatibility. This is done by adding 'global: 'globalThis' in tax/vite.config.ts: import devtoolsJson from 'vite-plugin-devtools-json'; import tailwindcss from '@tailwindcss/vite'; import { sveltekit } from '@sveltejs/kit/vite'; import { defineConfig } from 'vite';
export default defineConfig({ plugins: [tailwindcss(), sveltekit(), devtoolsJson()], define: { SVELTE_VERSION: JSON.stringify('5.0.0'), global: 'globalThis', },
Summary Root Cause Analysis
- The error ReferenceError: global is not defined was preventing the dashboard page from loading and thus onMount() was never called. Chain of events:
- The dashboard page imports from @chendingplano/shared package
- The shared package's UtilFuncs.ts:2 imports @iarna/toml at module level
- @iarna/toml is a Node.js library that references the global object
- In browsers, global doesn't exist (only window and globalThis do)
- This causes a runtime error before any component code executes Since the error occurs during module initialization, onMount() never gets a chance to run
The config: global: 'globalthis' in vite.config.ts tells Vite to replace all references to global with globalThis (which exists in both Node.js and browser)
table-questionnaire-questions.ts
echo is the default (backend) struct. Pocketbase uses: *core.RequestEvent Default: func HandleGoogleLogin(c echo.Context) error {...} Pocketbase: func HandleGoogleLoginPocketbase(e *core.RequestEvent) error {...}
Since Pocketbase changed the context type, the ways of retrieving values from the context are different. Below is a comparison between the two:
| Echo (c) | PocketBase (e) |
|---|---|
| c.Request() | e.Request |
| c.Response() | e.Response |
| c.QueryParam("x") | e.Request.URL.Query().Get("x") |
| c.FormValue("x") | e.Request.FormValue("x") (after e.Request.ParseForm()) |
| c.JSON(200, data) | e.Json(200, data) |
| c.String(200, "ok") | e.String(200, "ok") |
| c.Redirect(307, url) | http.Redirect(e.Response, e.Request, url, http. |
StatusTemporaryRedirect)
func RegisterRoutes(r *router.Router[*core.RequestEvent]) { r.GET("/auth/google/login", auth.HandleGoogleLogin) r.GET("/auth/google/callback", auth.HandleGoogleCallback) ... }
Function signature MUST be: func MyRouteHandler(r *core.RequestEvent) { ... }
Both parse inputs to JSON docs. Unmarshal(...) requires reading all contents into memory before parsing it, which NewDecoder(...) requires an io.Reader and reads data in stream. In addition, it can parse multiple JSON documents: func (c echo.Context) { r := c.Request() req JimoRequest reqs []JimoRequest while true { if err := json.NewDecoder(r.Body).Decode(req); err == nil { reqs.push(req) } else { // Finished reading. berak } } }
Use json.Unmarshal for small JSON docs. Otherwise, use json.NewDecoder.
IMPORTANT: echo.Context.Body is not the body. It is an io.Reader ( actually, it is an io.ReadClose)
There are two methods to retrieve body: func myfunc(c echo.Context) { // body is []byte. It is safe. No need to close it. // It reads all data in request. body, err := io.ReadAll(c.Request().Body)
//
jimo_request ApiTypes.JimoRequest
json.NewDecoder(c.Request().Body).Decode(&jimo_request)
}
Request (echo.Context.Request()): Data Type: *http.Request method: c.Request().Method url: c.Request().URL headers: c.Request().Header body, _: io.ReadAll(c.Request().Body)
Response: Data Type: *echo.Response (Echo's wrapper around http.ResponseWriter)
{ "day": "31 12:00:00.000Z", "hour": "14", "minute": "30", "month": "12", "year": "2025" }
import ( "fmt" "os" )
func main() { // Get an environment variable value := os.Getenv("MY_VAR") fmt.Println("MY_VAR =", value)
// Check if it exists (os.Getenv returns empty string if not set)
if value == "" {
fmt.Println("MY_VAR is not set")
}
}
sqlite3 pb_data/data.db
qwen
- cd ChenWeb/web
- rm -rf node_modules/.vite
- rm -rf .svelte-kit
It does six things:
- Run a (development) server on port 5173
- Dynamic compile files, individually on demand
- Hot Module Replacement
- Dependency pre-bundle
- Compile-time Constants
- Proxy
Vite is specially designed to improve development efficiency. Running the Vite server should improve page latency.
Compiling files means:
- Convert .svelte files to JS and HTML
- Convert TypeScript code to JavaScript code
- Convert Tailwind to CSS In other word, the run-time should see only JS and HTML.
Hot Module Replacement is one of the most important features of Vite. Instead of replacing the entire application when you modify one file (such as one .svelte file), it just compiles that file, replace it with the existing one, and update it in browser.
Proxy means when you send something in browser, if it sends to 5173 and it should be handled by the backend, it proxy the request to the backend port (such as 8080).
Vite does not create one, huge file. Instead, when you run: npm run build or bun run build it creates many files, and does many optimization. For instance, one .svelte file is converted to one native file so that when we open the page, it downloads only that file, not the entire app, plus some common files, which are mostly cached.
The cached files include:
- index.html
- some basic common libraries (small enough)
- +layout, which is normally not big.
- CSS
Vite will optimize CSS to extract shared CSS into separate files so that they can be downloaded on demand and cashed and shared with others.
Proxy: It sits between frontend and server and acts as the frontend. Server does not see the real 'frontend' but only the proxy. We can use proxy to: - Hide frontend IP addresses and ports - Filter (reject) certain requests, such as a company can control which Internet sites should be blocked/allowed.
Reverse Proxy It also sits between frontend and backend, but acts as the server. Frontend sees only the reverse proxy, which routes requests to real servers. Load balance happens here. In addition to load balance, it can also do many things, such as caching, controls, etc.
In most cases, clients demand servers, or the requests clients sent normally target specific servers. Forward proxy should not (normally) change it. Reverse proxy, on the other hand, acts as the server. It is, therefore, possible for reverse proxy to load balance requests to different servers.
When doing load balance, the algorithm it uses matters because many calls are stateful.
In this page, when a user selects one or more records (there is a checkbox in the page) and presses the button "Invite to Portal" (Line 636), it will call the function "inviteSelectedClient(...)" (Line 417). Please help me implement this function:
-
The process should be in the backend
-
The system uses Pocketbase and PostgreSQL. Most tables are in Pocketbase unless they are explicitly specified as PostgreSQL in the following context.
-
Below is the way how to retrieve var db *sql.DB: import ( ... "github.com/chendingplano/shared/go/api/ApiTypes" )
db := ApiTypes.ProjectDBHandle (db is *sql.DB, the handle for PostgreSQL )
-
For each record, retrieve its email (there is a field named 'email'). If it is not a valid email, report errors
-
Check whether the user with the email is in the table 'users', which is a Pocketbase collection. The file Shared/go/api/RequestHandlers/RequestContextPocket.go has a function to retrieve users by email (GetUserInfoByEmail(reqID string, email string)).
-
If the user does not exist, create one by calling RequestContextPocket.go::UpsertUser(...), with: user_email = , status="active", auth_type="email", user_id_type="email".
-
Dynamically generate a token for each row, and call RequestContextPocket.go::UpdateTokenByEmail(reqID, , )
-
Set the token to the Clients row (Clients are stored in PostgreSQL), i.e., set the token to the field v_token.
-
Send an email to the email with a link <domain_name>/api/v1/client-user-confirm (similar to signup link), so that when the user clicks the link, it routes the request to tax/server/api/handlers/client-user-binding.go (to be created).
-
The client-user-binding.go uses the token to retrieve the user (through RequestContextPocket.go::GetUserInfoByToken( ...), which returns ApiTypes.UserInfo (defined in ~/Workspace/Shared/go/api/ApiTypes/ApiTypes.go).
-
If no user is found, it is an error. Report the error and ignore the current row. Otherwise, check whether the user has set the password (ApiTypes.UserInfo.Password). If not, open up a page to set the password (similar to the page to reset password). Save the password back to "users" (a Pocketbase collection) by RequestContextPocket::UpsertUser(...).
-
Retrieve user_id from ApiTypes.UserInfo.UserId.
-
Use the token to retrieve the PostgreSQL table "clients" (db name is "tax") by token using the following statement: SELECT id as client_id FROM clients where v_token = ""
If not found, it is an error. Otherwise, it retrieved client_id
- Insert a record to client_members, which is a PostgrelSQL table. This is a relation table that connects clients and users. It has the following fields:
- id (string, need to generate a uuid)
- client_id (string)
- user_id (string)
- created (timestamp, default to NOW())
- updated (timestamp, default to NOW())
ctx := context.Background() ctx := context.TODO() ctx := context.WithTimeout() Don't forget 'defer cancel()'
ctx := context.WithCancel() Don't forget 'defer cancel()'
ctx := context.WithDeadline() deadline := time.Now().Add(5 * time.Second) ctx, cancel := context.WithDeadline(context.Background(), deadline) defer cancel()
Below are the errors: Argument of type '({ client }: { client: ClientRow; }) => ReturnType<import("svelte").Snippet>' is not assignable to parameter of type 'Snippet<[{ client: ClientRow; }]>'. Type '{ '{@render ...} must be called with a Snippet': "import type { Snippet } from 'svelte'"; } & typeof SnippetReturn' is not assignable to type '{ '{@render ...} must be called with a Snippet': "import type { Snippet } from 'svelte'"; } & typeof SnippetReturn'. Two different types with this name exist, but they are unrelated. Type '{ '{@render ...} must be called with a Snippet': "import type { Snippet } from 'svelte'"; } & unique symbol' is not assignable to type 'unique symbol'.ts(2345)
Claude: The renderSnippet function expects Snippet<[TProps]> type, but the snippets defined in the file are using Svelte 5 snippet syntax with destructured parameters like {#snippet ClientNameCell({ client }: { client: ClientRow })}.
The problem is a type incompatibility between how Svelte 5 snippets are declared and the Snippet<[TProps]> type expected by renderSnippet. In Svelte 5, when you declare {#snippet Foo({ a, b }: { a: string, b: number })}, the snippet receives a single object parameter, but TypeScript sees the snippet as having a different internal type representation.
The fix is to cast the snippets when passing them to renderSnippet.
The fix is in tax/web/src/lib/components/tables/client-data-table.svelte.
const asSnippet = <T,>(snippet: unknown) => snippet as Snippet<[T]>;
- asSnippet is a function
- <T,> declares a generic type T (',' is optional)
- The function's parameter is (snippet: unknown)
- It cast snippet to Snippet<[T]>
-
/Users/cding/Workspace/tax/web/src/routes/(admin)/admin/clients/+page.svelte: does not have "onSendBookingLink"
-
Moved /Users/cding/Workspace/tax/web/src/lib/stores/auth.svelte.ts to porting/
- e.Request().PathValue("id") This is from Go’s standard library (net/http), introduced with the new router in Go 1.22.
id := e.Request().PathValue("id")
A path parameter extracted by Go’s built-in router, e.g.: GET /admin/users/42
Route (Go stdlib style): http.HandleFunc("GET /admin/users/{id}", handler)
Key characteristics:
- Uses Go’s native routing (not Echo’s router)
- Only works if the route was defined using the new Go 1.22+ pattern syntax
- Not idiomatic in Echo apps
- Bypasses Echo’s parameter system
Use case: Only relevant if you’re mixing Echo with Go’s new net/http router (rare).
- e.Param("id") ✅ Most common in Echo Echo’s path parameter accessor.
id := e.Param("id")
A path segment defined in your Echo route: GET /admin/users/:id
Example: e.GET("/admin/users/:id", HandleGetAdminUsers)
func HandleGetAdminUsers(e echo.Context) error { id := e.Param("id") // "42" ... }
Key characteristics:
- Framework-native (Echo router)
- Fast and reliable
- Works with Echo middleware, grouping, versioning, etc.
The idiomatic choice in Echo apps Use this for anything in the URL path like /users/42.
- e.QueryParam("id") Echo’s query string accessor. id := e.QueryParam("id")
Where the value comes from: The query part of the URL: GET /admin/users?id=42
Example: func HandleGetAdminUsers(e echo.Context) error { id := e.QueryParam("id") // "42" ... }
Key characteristics:
- Reads from ?key=value in the URL
- Multiple values supported via e.QueryParams()
- Optional parameters (can be absent)
- Common for filters, pagination, sorting
Use case: Use this for things like:
- ?page=2
- ?limit=50
- ?role=admin
When the backend wants to redirect to a page, it should not send a response (JSON). Instead, it should instruct the frontend to redirect. Below is an example:
func HandleEmailVerify(c echo.Context) error { rc := EchoFactory.NewFromEcho(c, "SHD_EML_272") logger := rc.GetLogger() logger.Info("Handle Email Verify")
is_post := false
status_code, msg, resp := HandleEmailVerifyBase(rc, is_post)
if msg == "" {
// Success case: redirect to the dashboard
// Cookie was already set in HandleEmailVerifyBase
redirectURL := resp["redirect_url"]
logger.Info("email verify success, redirecting", "redirect_url", redirectURL)
// IMPORTANT: normally, it sends a response with contents (JSON) (i.e., do
// not do: return c.JSON(status_code, resp))
// But since we want to show pages (redirect), use c.Redirect(...)
return c.Redirect(http.StatusSeeOther, redirectURL)
} else {
// Error case: return error message
logger.Error("failed verify", "error", msg)
return c.String(status_code, msg)
}
}
Sidebar is from ShadCN. It is controlled by tax/web/src/app.css: The entry name is --sidebar-accent.
ctx := context.Background()
Note that it does not 'create' a new context. It just returns a background context, which is a singleton immutable context from 'context'. In Go, context is immutable. If you want to add new values to it, you call ctx.WithValue(...), which returns back a new context with the new value. It is for this reason that 'context.Background()' does not create but return a background context.
Go recommends to use ctx in (every) function call. I prefer rc (ApiTypes.RequestContext). rc is mutable. It is designed to carry information among function calls, such as trace, parameters (or values), etc. 'rc' is created per session. It is not thread-safe. When a request is processed in multi-thread fashion, one should be careful about the 'rc'. In general, every thread should create its own 'rc' unless it is a read-only thread.
As an alternative, we can use ctx: Add a tracer to ctx. The tracer has: - A logger - A tracer - A mutable 'store'
Step 1: cd /root/Workspace
Step 2: Login to PostgreSQL. You need to provide PostgreSQL password (you can find it in the .env file) ./pg_connect.sh
Step 3: Show all tables, optional \d
Step 4: List all users (optional) select * from users;
Step 5: List id, password (Password is saved in hash), optional select id, password from users;
Step 6: List id, user_id_type, first_name, last_name, email, verified, admin, is_owner, email_visibility, auth_type, optional select id, user_id_type, first_name, last_name, email, verified, admin, is_owner, email_visibility, auth_type from users;
Step 7: Update a user to admin: update users set admin = true, is_owner = true where id = 'xxx'
Set bookings::consultant_id to linda's user_id
- time
- msg
- level
- source
launchctl stop ai.openclaw.gateway && launchctl unload ~/Library/LaunchAgents/ai.openclaw.gateway.plist 2>/dev/null || launchctl bootout gui/$(id -u) ai.openclaw.gateway 2>/dev/null; echo "Service stopped"
Launch clawdbot gateway --port 18789 --verbose
Type.String({ minLength: 1, maxLength: 100, pattern: "^\\d+$" })
Type.Number({ minimum: 0, maximum: 100 })
Type.Integer()
Type.Boolean()
Type.Null()
Type.Object({ field: Type.String() }, { additionalProperties: false })
Type.Array(Type.String(), { minItems: 1, maxItems: 10 })
Type.Tuple([Type.String(), Type.Number()])
Type.Union([Type.String(), Type.Number()])
Type.Intersect(Type.Object({...}), Type.Object({...}))
Type.Enum(["option1", "option2"]) // Enum-like
Type.Optional(Type.String()) // Field may be omitted
Type.Nullable(Type.String()) // Field can be null
Type.Undefined() // Explicit undefined
Type.Record(Type.String(), Type.Any()) // Dictionary
Type.Partial(ObjectSchema) // Make all fields optional
Type.Required(ObjectSchema) // Make all fields required
Type.Pick(ObjectSchema, ["field"]) // Select specific fields
Type.Omit(ObjectSchema, ["field"]) // Exclude specific fields
Type.RegEx(/pattern/)
Type.Constraints({ minimum: 1, exclusiveMinimum: true }) // Advanced numeric
- cd ~/Workspace/nix/modules/shared/home.nix
- Add the env var in 'environment.variables', such as: environment.variables = { ... POCKETBASE_ADMIN_EMAIL = " ... }
- Rebuild the environment:
- cd ~/Workspace/nix
- darwin-rebuild switch --flake .#Chens-Mac-mini
- You need to exit ghostty and then restart it to see the new env vars.
Vite handles environment variables automatically. By default, SvelteKit (it uses Vite) looks environment files in your project root directory with the following files (in the listed order):
- .env.production
- .env.development
- .env.local
- .env
Environment variables that start with PUBLIC_ can be accessed in frontend. Otherwise, they can only be accessed on the server side.
To access environment variables in frontend, do the following:
// This will be available on the client
const apiUrl = import.meta.env.PUBLIC_API_BASE_URL;mise use <module-name>
mise use aqua:charmbracelet/crushThis command launches a temporary interactive shell, install the package temporarily. Once exit, the installation is gone:
nix shell nixpkgs#<name>'nix shell' is ephemeral. If you want to install a package permanently, add it to nix's packages.nix file.
If you pick a session from the history and it shows nothing, this is a known bug. Do the following and then restart Claude Code:
mv ~/.claude/file-history ~/.claude/file-history.bakType: Shift + Cmd + T