- Added external-call clause to get-req statement. It returns true if the current request handler is called directly from an external entity (web browser, an outside API call, curl call, command line etc.), or false if called from another handler. This allows for greater flexibility in formulating web service's response and its output parameters.
Friday, November 29, 2024
Golf 117 released
Tuesday, November 26, 2024
Golf 114 released
- call-handler statement is now 2.1 times faster. Only the very first call to a local request uses hash table to find it; all subsequent ones use a cached request address. Note that this is true only if the request name is a string constant and not a variable (in which case it's resolved via hash table every time still). However, in most applications request name is a constant string nearly 100% of the time.
Friday, November 22, 2024
Golf 109 released
- New "-k" option in gg utility will create a new Golf application, if it didn't already exist. You can still use mgrg utility to create new applications. This option makes it easier to create one with all the default settings. You can also use "-q" flag to compile and make the executable in a single step.
How to create Golf application
To create Golf application with default settings use an option of gg utility:
gg -k my-app
where "my-app" is your application name. If you already have an application with that name, nothing is done, so this is an idempotent operation.
If you already have source code, you can create and compile your application in one step:
gg -k my-app -q
which is a neat shortcut.
What's default settings? Well, it means your application directory (in "/var/lib/gg/my-app") will be owned by the currently logged on user (and other users can't access it), and any Unix socket can connect to your application server. This is a typical setup you'd probably use in most cases, so it's a useful one.
If you'd like to have more options in creating a Golf application, see service manager).
Thursday, November 21, 2024
Getting help for Golf with man pages
Golf installation will create Linux "man" pages (or manual pages).
They contain the same information as the web documentation, and you can use them for a quick help on syntax even when you're working offline.
For instance to get help on "call-web" statement, you would enter in command line:
man call-web
The result would be something like:
Note that the man section for Golf is "2gg".
Tuesday, November 12, 2024
Multi-tenant SaaS (Notes web application) in 200 lines of code
First create a directory for your application, where the source code will be:
mkdir -p notes cd notesCopied!
Create PostgreSQL user (with the same name as your logged on Linux user, so no password needed), and the database "db_app":
echo "create user $(whoami); create database db_app with owner=$(whoami); grant all on database db_app to $(whoami); \q" | sudo -u postgres psqlCopied!
Create a database configuration file to describe your PostgreSQL database above:
echo "user=$(whoami) dbname=db_app" > db_appCopied!
Create database objects we'll need - users table for application users, and notes table to hold their notes:
echo "create table if not exists notes (dateOf timestamp, noteId bigserial primary key, userId bigint, note varchar(1000)); create table if not exists users (userId bigserial primary key, email varchar(100), hashed_pwd varchar(100), verified smallint, verify_token varchar(30), session varchar(100)); create unique index if not exists users1 on users (email);" | psql -d db_appCopied!
Create application "notes" owned by your Linux user:
sudo mgrg -i -u $(whoami) notesCopied!
This executes before any other handler in an application, making sure all requests are authorized, file "before-handler.golf":
vi before-handler.golfCopied!
Copy and paste:
before-handler set-param displayed_logout = false, is_logged_in = false call-handler "/session/check" end-before-handlerCopied!
- Signup users, login, logout
This is a generic session management web service that handles user creation, verification, login and logout. Create file "session.golf":
vi session.golfCopied!
Copy and paste:
// Display link to login or signup %% /session/login-or-signup private @<a href="<<p-path "/session/user/login">>">Login</a> <a href="<<p-path "/session/user/new/form">>">Sign Up</a><hr/> %% // Login with email and password, and create a new session, then display home pag %% /session/login public get-param pwd, email hash-string pwd to hashed_pwd random-string to sess_id length 30 run-query @db_app = "select userId from users where email='%s' and hashed_pwd='%s'" output sess_user_id : email, hashed_pwd run-query @db_app no-loop = "update users set session='%s' where userId='%s'" input sess_id, sess_user_id affected-rows arows if-true arows not-equal 1 @Could not create a session. Please try again. <<call-handler "/session/login-or-signup">> <hr/> exit-handler end-if set-cookie "sess_user_id" = sess_user_id path "/", "sess_id" = sess_id path "/" call-handler "/session/check" call-handler "/session/show-home" exit-handler end-query @Email or password are not correct. <<call-handler "/session/login-or-signup">><hr/> %% // Starting point of the application. Either display login form or a home page: %% /session/start public get-param action, is_logged_in type bool if-true is_logged_in equal true if-true action not-equal "logout" call-handler "/session/show-home" exit-handler end-if end-if call-handler "/session/user/login" %% // Generic home page, you can call anything from here, in this case a list of note %% /session/show-home private call-handler "/notes/list" %% // Logout user and display home, which will ask to either login or signup %% /session/logout public get-param is_logged_in type bool if-true is_logged_in equal true get-param sess_user_id run-query @db_app = "update users set session='' where userId='%s'" input sess_user_id no-loop affected-rows arows if-true arows equal 1 set-param is_logged_in = false @You have been logged out.<hr/> commit-transaction @db_app end-if end-if call-handler "/session/show-home" %% // Check session based on session cookie. If session cookie corresponds to the email address, the request is a part of an authorized session %% /session/check private get-cookie sess_user_id="sess_user_id", sess_id="sess_id" set-param sess_id, sess_user_id if-true sess_id not-equal "" set-param is_logged_in = false run-query @db_app = "select email from users where userId='%s' and session='%s'" output email input sess_user_id, sess_id row-count rcount set-param is_logged_in = true get-param displayed_logout type bool if-true displayed_logout equal false get-param action if-true action not-equal "logout" @Hi <<p-out email>>! <a href="<<p-path "/session/logout">>">Logout</a><br/> end-if set-param displayed_logout = true end-if end-query if-true rcount not-equal 1 set-param is_logged_in = false end-if end-if %% // Check that email verification token is the one actually sent to the email address %% /session/verify-signup public get-param code, email run-query @db_app = "select verify_token from users where email='%s'" output db_verify : email if-true code equal db_verify @Your email has been verifed. Please <a href="<<p-path "/session/user/login">>">Login</a>. run-query @db_app no-loop = "update users set verified=1 where email='%s'" : email exit-handler end-if end-query @Could not verify the code. Please try <a href="<<p-path "/session/user/new/verify-form">>">again</a>. exit-handler %% // Display login form that asks for email and password %% /session/user/login public call-handler "/session/login-or-signup" @Please Login:<hr/> @<form action="<<p-path "/session/login">>" method="POST"> @<input name="email" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Email"> @<input name="pwd" type="password" value="" size="50" maxlength="50" required placeholder="Password"> @<button type="submit">Go</button> @</form> %% // Display form for a new user, asking for an email and password %% /session/user/new/form public @Create New User<hr/> @<form action="<<p-path "/session/user/new/create">>" method="POST"> @<input name="email" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Email"> @<input name="pwd" type="password" value="" size="50" maxlength="50" required placeholder="Password"> @<input type="submit" value="Sign Up"> @</form> %% // Send verification email %% /session/user/new/send-verify private get-param email, verify write-string msg @From: service@your-service.com @To: <<p-out email>> @Subject: verify your account @ @Your verification code is: <<p-out verify>> end-write-string exec-program "/usr/sbin/sendmail" args "-i", "-t" input msg status st if-true st not-equal 0 or true equal false @Could not send email to <<p-out email>>, code is <<p-out verify>> set-param verify_sent = false else-if set-param verify_sent = true end-if %% // Create new user from email and password %% /session/user/new/create public get-param email, pwd hash-string pwd to hashed_pwd random-string to verify length 5 number begin-transaction @db_app run-query @db_app no-loop = "insert into users (email, hashed_pwd, verified, verify_token, session) values ('%s', '%s', '0', '%s', '')" input email, hashed_pwd, verify affected-rows arows error err on-error-continue if-true err not-equal "0" or arows not-equal 1 call-handler "/session/login-or-signup" @User with this email already exists. rollback-transaction @db_app else-if set-param email, verify call-handler "/session/user/new/send-verify" get-param verify_sent type bool if-true verify_sent equal false rollback-transaction @db_app exit-handler end-if commit-transaction @db_app call-handler "/session/user/new/verify-form" end-if %% // Display form to enter the code emailed to user to verify the email address %% /session/user/new/verify-form public get-param email @Please check your email and enter verification code here: @<form action="<<p-path "/session/verify-signup">>" method="POST"> @<input name="email" type="hidden" value="<<p-out email>>"> @<input name="code" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Verification code"> @<button type="submit">Verify</button> @</form> %%Copied!
- Notes application
This is the actual application that uses above session management services. Create file "notes.golf":
vi notes.golfCopied!
Copy and paste:
// Delete a note %% /notes/delete public call-handler "/notes/header" get-param sess_user_id, note_id run-query @db_app = "delete from notes where noteId='%s' and userId='%s'" : note_id, sess_user_id \ affected-rows arows no-loop error errnote if-true arows equal 1 @Note deleted else-if @Could not delete note (<<p-out errnote>>) end-if %% // Display a form to add a note %% /notes/form-add public call-handler "/notes/header" @Add New Note @<form action="<<p-path "/notes/add">>" method="POST"> @<textarea name="note" rows="5" cols="50" required autofocus placeholder="Enter Note"></textarea> @<button type="submit">Create</button> @</form> %% // Add a note %% /notes/add public call-handler "/notes/header" get-param note, sess_user_id run-query @db_app = "insert into notes (dateOf, userId, note) values (now(), '%s', '%s')" : sess_user_id, note \ affected-rows arows no-loop error errnote if-true arows equal 1 @Note added else-if @Could not add note (<<p-out errnote>>) end-if %% // List all notes %% /notes/list public call-handler "/notes/header" get-param sess_user_id run-query @db_app = "select dateOf, note, noteId from notes where userId='%s' order by dateOf desc" \ input sess_user_id output dateOf, note, noteId match-regex "\n" in note replace-with "<br/>\n" result with_breaks status st cache if-true st equal 0 set-string with_breaks = note end-if @Date: <<p-out dateOf>> (<a href="<<p-path "/notes/ask-delete">>?note_id=<<p-out noteId>>">delete note</a>)<br/> @Note: <<p-out with_breaks>><br/> @<hr/> end-query %% // Display a question whether to delete a note or not %% /notes/ask-delete public call-handler "/notes/header" get-param note_id @Are you sure you want to delete a note? Use Back button to go back,\ or <a href="<<p-path "/notes/delete">>?note_id=<<p-out note_id>>">delete note now</a>. %% // Check if session is authorized, and display an appropriate header %% /notes/header private get-param is_logged_in type bool if-true is_logged_in equal false call-handler "/session/login-or-signup" end-if @<h1>Welcome to Notes!</h1><hr/> if-true is_logged_in equal false exit-handler end-if @<a href="<<p-path "/notes/form-add">>">Add Note</a> <a href="<<p-path "/notes/list">>">List Notes</a><hr/> %%Copied!
gg -q --db=postgres:db_appCopied!
mgrg notesCopied!
In order to use this example, you need to be able to email local users, which means email addresses such as \"myuser@localhost\". To do that, install postfix (or sendmail). On Debian systems (like Ubuntu):
sudo apt install postfix sudo systemctl start postfixCopied!
and on Fedora systems (like RedHat):
sudo dnf install postfix sudo systemctl start postfixCopied!
When the application sends an email to a local user, such as <OS user>@localhost, then you can see the email sent at:
sudo vi /var/mail/<OS user>Copied!
A web server sits in front of Golf application server, so it needs to be setup. This example is for Ubuntu, so edit Nginx config file there:
sudo vi /etc/nginx/sites-enabled/defaultCopied!
Add this in "server {}" section:
location /notes/ { include /etc/nginx/fastcgi_params; fastcgi_pass unix:///var/lib/gg/notes/sock/sock; }Copied!
Restart Nginx:
sudo systemctl restart nginxCopied!
Go to your web browser, and enter:
http://127.0.0.1/notes/session/startCopied!
Sunday, November 10, 2024
Golf 91 released
- Fixed SELinux installation bug for Fedora-like distros.
- Minimum number of CPUs available is 1 in case there's an issue in determining the number of them.
- Fixed issue with highlighting call-handler statement in vim.
Tuesday, November 5, 2024
Golf 87 released
- Added --exclude option to gg in order to exclude sub-directories from compilation.
- Added p-source-line and p-source-file statements to aid in debugging.
- A request handler can now be defined in any source file whose path matches, partially or fully, the request path of the handler. For instance /session/login request handler can be defined either in file "session.golf" or "session/login.golf". A source file can also have multiple request handlers that match path.
- Added --single-file option to gg to force each source code file to have just a single request handler.
- set-param and get-param statements now work with multiple parameters separated by a comma.
- set-cookie and get-cookie statements now work with multiple cookies separated by a comma.
- Fixed bug in end-write-string statement where junk text at the end would be ignored without an error message.
- Fixed error in parallel compilation.
- Fixed bug in get-param where parameter name wouldn't be correct.
- Fixed bug where a function in call-extended statement wouldn't work without any parameters.