Seed projects

This commit is contained in:
Ben Visness 2022-05-11 22:24:05 -05:00
parent e4bb741a15
commit a2ec57cf47
3 changed files with 259 additions and 61 deletions

View File

@ -124,6 +124,22 @@ func Query[T any](
}
}
/*
Identical to Query, but panics if there was an error.
*/
func MustQuery[T any](
ctx context.Context,
conn ConnOrTx,
query string,
args ...any,
) []*T {
result, err := Query[T](ctx, conn, query, args...)
if err != nil {
panic(err)
}
return result
}
/*
Identical to Query, but returns only the first result row. If there are no
rows in the result set, returns NotFound.
@ -148,6 +164,22 @@ func QueryOne[T any](
return result, nil
}
/*
Identical to QueryOne, but panics if there was an error.
*/
func MustQueryOne[T any](
ctx context.Context,
conn ConnOrTx,
query string,
args ...any,
) *T {
result, err := QueryOne[T](ctx, conn, query, args...)
if err != nil {
panic(err)
}
return result
}
/*
Identical to Query, but returns concrete values instead of pointers. More convenient
for primitive types.
@ -176,6 +208,22 @@ func QueryScalar[T any](
return result, nil
}
/*
Identical to QueryScalar, but panics if there was an error.
*/
func MustQueryScalar[T any](
ctx context.Context,
conn ConnOrTx,
query string,
args ...any,
) []T {
result, err := QueryScalar[T](ctx, conn, query, args...)
if err != nil {
panic(err)
}
return result
}
/*
Identical to QueryScalar, but returns only the first result value. If there are
no rows in the result set, returns NotFound.
@ -202,6 +250,22 @@ func QueryOneScalar[T any](
return *result, nil
}
/*
Identical to QueryOneScalar, but panics if there was an error.
*/
func MustQueryOneScalar[T any](
ctx context.Context,
conn ConnOrTx,
query string,
args ...any,
) T {
result, err := QueryOneScalar[T](ctx, conn, query, args...)
if err != nil {
panic(err)
}
return result
}
/*
Identical to Query, but returns the ResultIterator instead of automatically converting the results to a slice. The iterator must be closed after use.
*/
@ -249,6 +313,22 @@ func QueryIterator[T any](
return it, nil
}
/*
Identical to QueryIterator, but panics if there was an error.
*/
func MustQueryIterator[T any](
ctx context.Context,
conn ConnOrTx,
query string,
args ...any,
) *Iterator[T] {
result, err := QueryIterator[T](ctx, conn, query, args...)
if err != nil {
panic(err)
}
return result
}
// TODO: QueryFunc?
type compiledQuery struct {

View File

@ -6,6 +6,8 @@ import (
"math/rand"
"os"
"os/exec"
"strings"
"time"
"git.handmade.network/hmn/hmn/src/auth"
"git.handmade.network/hmn/hmn/src/config"
@ -13,6 +15,7 @@ import (
"git.handmade.network/hmn/hmn/src/hmndata"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/parsing"
"git.handmade.network/hmn/hmn/src/utils"
lorem "github.com/HandmadeNetwork/golorem"
"github.com/jackc/pgx/v4"
@ -46,7 +49,7 @@ func SeedFromFile(seedFile string) {
// Creates only what's necessary to get the site running. Not really very useful for
// local dev on its own; sample data makes things a lot better.
func BareMinimumSeed() {
func BareMinimumSeed() *models.Project {
Migrate(LatestVersion())
ctx := context.Background()
@ -55,48 +58,20 @@ func BareMinimumSeed() {
})
defer conn.Close(ctx)
tx, err := conn.Begin(ctx)
if err != nil {
panic(err)
}
tx := utils.Must1(conn.Begin(ctx))
defer tx.Rollback(ctx)
fmt.Println("Creating HMN project...")
_, err = tx.Exec(ctx,
`
INSERT INTO project (id, slug, name, blurb, description, personal, lifecycle, color_1, color_2, forum_enabled, blog_enabled, date_created)
VALUES (1, 'hmn', 'Handmade Network', '', '', FALSE, $1, 'ab4c47', 'a5467d', TRUE, TRUE, '2017-01-01T00:00:00Z')
`,
models.ProjectLifecycleActive,
)
if err != nil {
panic(err)
}
hmn := seedProject(ctx, tx, seedHMN)
fmt.Println("Creating main forum...")
_, err = tx.Exec(ctx, `
INSERT INTO subforum (id, slug, name, parent_id, project_id)
VALUES (2, '', 'Handmade Network', NULL, 1)
`)
if err != nil {
panic(err)
}
_, err = tx.Exec(ctx, `
UPDATE project SET forum_id = 2 WHERE slug = 'hmn'
`)
if err != nil {
panic(err)
}
utils.Must0(tx.Commit(ctx))
err = tx.Commit(ctx)
if err != nil {
panic(err)
}
return hmn
}
// Seeds the database with sample data for local dev.
func SampleSeed() {
BareMinimumSeed()
hmn := BareMinimumSeed()
ctx := context.Background()
conn := db.NewConnWithConfig(config.PostgresConfig{
@ -104,10 +79,7 @@ func SampleSeed() {
})
defer conn.Close(ctx)
tx, err := conn.Begin(ctx)
if err != nil {
panic(err)
}
tx := utils.Must1(conn.Begin(ctx))
defer tx.Rollback(ctx)
fmt.Println("Creating admin user (\"admin\"/\"password\")...")
@ -128,24 +100,48 @@ func SampleSeed() {
users := []*models.User{alice, bob, charlie, spammer}
fmt.Println("Creating some forum threads...")
for i := 0; i < 5; i++ {
thread := seedThread(ctx, tx, models.Thread{})
populateThread(ctx, tx, thread, users, rand.Intn(5)+1)
fmt.Println("Creating starter projects...")
hero := seedProject(ctx, tx, seedHandmadeHero)
fourcoder := seedProject(ctx, tx, seed4coder)
for i := 0; i < 3; i++ {
name := fmt.Sprintf("%s %s", lorem.Word(1, 10), lorem.Word(1, 10))
slug := strings.ReplaceAll(strings.ToLower(name), " ", "-")
seedProject(ctx, tx, models.Project{
Slug: slug,
Name: name,
Blurb: lorem.Sentence(6, 16),
Description: lorem.Paragraph(3, 5),
Personal: true,
})
}
fmt.Println("Creating some forum threads...")
for i := 0; i < 5; i++ {
for _, project := range []*models.Project{hmn, hero, fourcoder} {
thread := seedThread(ctx, tx, project, models.Thread{})
populateThread(ctx, tx, thread, users, rand.Intn(5)+1)
}
}
// spam-only thread
{
thread := seedThread(ctx, tx, models.Thread{})
thread := seedThread(ctx, tx, hmn, models.Thread{})
populateThread(ctx, tx, thread, []*models.User{spammer}, 1)
}
fmt.Println("Creating the news posts...")
fmt.Println("Creating news posts...")
{
// Main site news posts
for i := 0; i < 3; i++ {
thread := seedThread(ctx, tx, models.Thread{Type: models.ThreadTypeProjectBlogPost})
thread := seedThread(ctx, tx, hmn, models.Thread{Type: models.ThreadTypeProjectBlogPost})
populateThread(ctx, tx, thread, []*models.User{admin, alice, bob, charlie}, rand.Intn(5)+1)
}
// 4coder
for i := 0; i < 5; i++ {
thread := seedThread(ctx, tx, fourcoder, models.Thread{Type: models.ThreadTypeProjectBlogPost})
populateThread(ctx, tx, thread, []*models.User{bob}, 1)
}
}
// admin := CreateAdminUser("admin", "12345678")
@ -159,14 +155,11 @@ func SampleSeed() {
// Create codelanguages
// Create library and library resources
err = tx.Commit(ctx)
if err != nil {
panic(err)
}
utils.Must0(tx.Commit(ctx))
}
func seedUser(ctx context.Context, conn db.ConnOrTx, input models.User) *models.User {
user, err := db.QueryOne[models.User](ctx, conn,
user := db.MustQueryOne[models.User](ctx, conn,
`
INSERT INTO hmn_user (
username, password, email,
@ -194,24 +187,17 @@ func seedUser(ctx context.Context, conn db.ConnOrTx, input models.User) *models.
utils.OrDefault(input.Name, randomName()), utils.OrDefault(input.Bio, lorem.Paragraph(0, 2)), utils.OrDefault(input.Blurb, lorem.Sentence(0, 14)), utils.OrDefault(input.Signature, lorem.Sentence(0, 16)),
input.ShowEmail,
)
if err != nil {
panic(err)
}
err = auth.SetPassword(ctx, conn, input.Username, "password")
if err != nil {
panic(err)
}
utils.Must0(auth.SetPassword(ctx, conn, input.Username, "password"))
return user
}
func seedThread(ctx context.Context, tx pgx.Tx, input models.Thread) *models.Thread {
func seedThread(ctx context.Context, tx pgx.Tx, project *models.Project, input models.Thread) *models.Thread {
input.Type = utils.OrDefault(input.Type, models.ThreadTypeForumPost)
var defaultSubforum *int
if input.Type == models.ThreadTypeForumPost {
id := 2
defaultSubforum = &id
defaultSubforum = project.ForumID
}
thread, err := db.QueryOne[models.Thread](ctx, tx,
@ -232,7 +218,7 @@ func seedThread(ctx context.Context, tx pgx.Tx, input models.Thread) *models.Thr
`,
utils.OrDefault(input.Title, lorem.Sentence(3, 8)),
utils.OrDefault(input.Type, models.ThreadTypeForumPost), false,
utils.OrDefault(input.ProjectID, models.HMNProjectID), utils.OrDefault(input.SubforumID, defaultSubforum),
project.ID, utils.OrDefault(input.SubforumID, defaultSubforum),
-1, -1,
)
if err != nil {
@ -258,6 +244,68 @@ func populateThread(ctx context.Context, tx pgx.Tx, thread *models.Thread, users
}
}
var latestProjectId int
func seedProject(ctx context.Context, tx pgx.Tx, input models.Project) *models.Project {
project := db.MustQueryOne[models.Project](ctx, tx,
`
INSERT INTO project (
id,
slug, name, blurb,
description, descparsed,
color_1, color_2,
featured, personal, lifecycle, hidden,
forum_enabled, blog_enabled,
date_created
)
VALUES (
$1,
$2, $3, $4,
$5, $6,
$7, $8,
$9, $10, $11, $12,
$13, $14,
$15
)
RETURNING $columns
`,
utils.OrDefault(input.ID, latestProjectId+1),
input.Slug, input.Name, input.Blurb,
input.Description, parsing.ParseMarkdown(input.Description, parsing.ForumRealMarkdown),
input.Color1, input.Color2,
input.Featured, input.Personal, utils.OrDefault(input.Lifecycle, models.ProjectLifecycleActive), input.Hidden,
input.ForumEnabled, input.BlogEnabled,
utils.OrDefault(input.DateCreated, time.Now()),
)
latestProjectId = utils.IntMax(latestProjectId, project.ID)
// Create forum (even if unused)
forum := db.MustQueryOne[models.Subforum](ctx, tx,
`
INSERT INTO subforum (
slug, name,
project_id
)
VALUES (
$1, $2,
$3
)
RETURNING $columns
`,
"", project.Name,
project.ID,
)
// Associate forum with project
utils.Must1(tx.Exec(ctx,
`UPDATE project SET forum_id = $1 WHERE id = $2`,
forum.ID, project.ID,
))
project.ForumID = &forum.ID
return project
}
func randomName() string {
return "John Doe" // chosen by fair dice roll. guaranteed to be random.
}
@ -265,3 +313,56 @@ func randomName() string {
func randomBool() bool {
return rand.Intn(2) == 1
}
var seedHMN = models.Project{
ID: models.HMNProjectID,
Slug: models.HMNProjectSlug,
Name: "Handmade Network",
Blurb: "Changing the way software is written",
Description: `
[project=hero]Originally inspired by Handmade Hero[/project], we're an offshoot of its community, hoping to change the way software is written. To this end we've circulated our [url=https://handmade.network/manifesto]manifesto[/url] and built this website, in the hopes of fostering this community. We invite others to host projects built with same goals in mind and build up or expand their community's reach in our little tree house and hope it proves vibrant soil for the exchange of ideas as well as code.
`,
Color1: "ab4c47", Color2: "a5467d",
Hidden: true,
ForumEnabled: true, BlogEnabled: true,
DateCreated: time.Date(2017, 1, 1, 0, 0, 0, 0, time.UTC),
}
var seedHandmadeHero = models.Project{
Slug: "hero",
Name: "Handmade Hero",
Blurb: "An ongoing project to create a complete, professional-quality game accompanied by videos that explain every single line of its source code.",
Description: `
Handmade Hero is an ongoing project by [Casey Muratori](http://mollyrocket.com/casey) to create a complete, professional-quality game accompanied by videos that explain every single line of its source code. The series began on November 17th, 2014, and is estimated to run for at least 600 episodes. Programming sessions are limited to one hour per weekday so it remains manageable for people who practice coding along with the series at home.
For more information, see the official website at https://handmadehero.org
`,
Color1: "19328a", Color2: "f1f0a2",
Featured: true,
ForumEnabled: true, BlogEnabled: false,
DateCreated: time.Date(2017, 1, 10, 0, 0, 0, 0, time.UTC),
}
var seed4coder = models.Project{
Slug: "4coder",
Name: "4coder",
Blurb: "A programmable, cross platform, IDE template",
Description: `
4coder preview video: https://www.youtube.com/watch?v=Nop5UW2kV3I
4coder differentiates from other editors by focusing on powerful C/C++ customization and extension, and ease of cross platform use. This means that 4coder greatly reduces the cost of creating cross platform development tools such as debuggers, code intelligence systems. It means that tools specialized to your particular needs can be programmed in C/C++ or any language that interfaces with C/C++, which is almost all of them.
In other words, 4coder is attempting to live in a space between an IDE and a power editor such as Emacs or Vim.
Want to try it out? [url=https://4coder.itch.io/4coder]Get your alpha build now[/url]!
`,
Color1: "002107", Color2: "cccccc",
ForumEnabled: true, BlogEnabled: true,
DateCreated: time.Date(2017, 1, 10, 0, 0, 0, 0, time.UTC),
}

View File

@ -20,6 +20,23 @@ func OrDefault[T comparable](v T, def T) T {
}
}
// Takes an (error) return and panics if there is an error.
// Helps avoid `if err != nil` in scripts. Use sparingly in real code.
func Must0(err error) {
if err != nil {
panic(err)
}
}
// Takes a (something, error) return and panics if there is an error.
// Helps avoid `if err != nil` in scripts. Use sparingly in real code.
func Must1[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}
func IntMin(a, b int) int {
if a < b {
return a