6.3 KiB
GAS stack
Concepts
-
explict
schema.sql -
solid-state applications
- no Docker
- no environment split ("prod" vs "dev")
- environment distinctions are provided by scripts that call the application, not by the application
- try to avoid config files-- use shell scripts that pass flags instead
- put all the config variable values for a certain environment in that environment's runner context:
- values that are specific to Docker setups should go in docker-compose
- values that are for non-docker dev, maybe in .env or a runner shell script, or something like this
- assume that config values are finalized (i.e., don't need to be checked configVal == "" ? "default" : configVal) as high in the stack as possible (and also ensure this is the case). Ideally, vite.config.ts and onward already shouldn't need to do any val || "default" checking, and can simply accept env.PORT_NUMBER or env.IS_LOGGING_ENABLED as-is (or at most, parsing string->int, etc).
- https://git.tnd.gg/tnd/remichat/pulls/38#issuecomment-1645
- no frontend-backend separation (CORS)
- minimal config
- avoid cascading configs and multiple layers of defaults (e.g.,
{ XYZ: "prefix" + (upperlayer.XYZ || "DefaultValue"), ... })
- avoid cascading configs and multiple layers of defaults (e.g.,
-
subcommands executables
- scriptable applications: anything that the application can do, can be done via command line (i.e., avoid web-only operations)
- config is passed as command-line args
- avoid config env-vars (magical)
- shell scripts as glue code
-
sample data
-
opsdirectory
Borrowed from Ruby On Rails
- Convention Over Configuration
- scaffolding (code generators)
- database migration system
- focus on testing
- ORM-like affordances (but not actually using an ORM)
Vendoring vs Package Management vs In-Sourcing
Dependencies are technical debt. See Loris Cro's talk about "How To Write Better Software with Zig".
Scaffolding
Scaffolding is not boilerplate code, generated code, or library code. It's starter code which is intended to be modified as needed.
Library code is provided as a pre-made, off-the-shelf solution. If your problem is exactly the one the library is intended to solve, and the library does a good job, you should use it. Lots of big stuff is like this; nobody implements their own HTTP server or SQL engine as part of an application, because the domain is big, stable and standardized. It makes perfect sense to use libraries for this.
Boilerplate code and generated code (the latter being a common solution to the former) are usually indicators of bad abstractions. If there's truly something that needs to be done exactly the same way by rote, every time, then there should be a reusable library for it-- or maybe you're even using the wrong programming language.
Scaffolding isn't either of those, because scaffolding is intended to be changed. It's just a starting point as you flesh out your ideas.
Consider this parable comparing Rails and Django on their initial setup and tutorial. The analogy isn't perfect, but his claim is basically that the original Rails official tutorial left you with a tiny app with almost on code and a huge amount of functionality; but since all the functionality was invisible "magic" provided by Rails, as soon as you want something custom, you're nearly starting from scratch'. By comparison, the Django tutorial produces a large amount of code which makes the abstractions explicit. The author refers to all those extra lines of code as "footholds", from which you can start working.
Scaffolding begins as generic boilerplate, but evolves as your application logic becomes more custom and requirements change. One piece of scaffolded code might never change, because the scaffolding was good enough; another piece might be tweaked over time, as you add more to it (or remove parts you don't need); and another piece might change so much that no traces of the original scaffolding existed. One app could contain all three of these.
Scaffolding is intended to make "in-sourcing" your code easier, by getting you to something bare-bones-but-working faster.
Dynamic vs Static linking
TODO: write about this and why it matters for the GAS stack
SQLite and ROWID
Tables must be EITHER:
rowid integer primary keyas first column declaration; ORwithout rowid, i.e., a clustered index
Other primary key and rowid settings are supported by SQLite, but NOT by Gas Stack:
- implicit
rowid - alternate
integer primary keycolumn, i.e., rowid alias - non-rowid
primary keythat isn't a clustered index (note that SQLite doesn't actually support this, it just pretends to; the declaredprimary keyin this case is just a regular unique index)
This is enforced by the Gas Stack schema linter, and cannot be configured. Other Gas Stack schema tools assume one of these arrangements, and will not work right if it's not followed.
What not to do
- "scripts" folder
- "bin" with shell scripts in it
- using HTTP PUT or PATCH
App structure
- pkg/
- db/
- schema.sql
- db_connect.go (migrations, versions and associated funcs, sql_schema, DBCreate, DBConnect)
- db_connect_test.go
- test_utils/
- db_setup.go
- db/
- cmd/
- main.go (parameterize DB_FILENAME)
- doc/
- sample_data/
- mount.sh
- seed.sql
- data/
- .github/
- workflows/
- build.yml
- workflows/
- .gitignore (sample_data/data)
- .golangci.yaml
- README.md
- ARCHITECTURE.md
gas init: - git init - go mod init Params: - omit sqlite - omit github workflow - database filename - go module name - include HTTP? (pkg/web, templ) - data directory or just db file
gas generate_boilerplate [db-table-name]: - struct - Save function - Get[Type]ByID function
gas generate-web - install templ - echo "*_templ.go" >> .gitignore - pkg/web - server.go - middlewares.go
- web/
- static/
- vendor/
- styles.css
- tpl/
- server.go
- middlewares.go
- static.go
gas generate-subcommand
Considerations:
- godoc
Methodologies
-
Timestamp type: store dates and times as int64 (unix millis)
-
code tags: TODO, XXX, WTF, DUPE tags
-
go test -tags integrationfor integration testing (slow tests) -
cobra commands
-
sqlx
-
:memory: databases for testing
-
go-chi router
-
No docker-compose