Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
8da1f26
feat: modpacks
Vilsol Jun 24, 2025
1fd5ced
chore: schema files don't have coverage
Vilsol Jun 24, 2025
cc14d32
Merge branch 'staging' into feat/modpacks
budak7273 Nov 21, 2025
d2873ba
Fixed issue with depreciated container and function call in modpack r…
rhit-mooretj Feb 14, 2026
8ea0771
Added compatibility field to Modpacks
rhit-zhangl8 Mar 3, 2026
6a0c0c5
Merge branch 'feat/modpacks' of https://github.com/satisfactorymoddin…
rhit-zhangl8 Mar 3, 2026
7509f43
Added calculated modpack compatibility and basic tests for the calcul…
rhit-zhangl8 Mar 21, 2026
ef72bbe
Added calculated modpack compatibility and basic tests for the calcul…
rhit-zhangl8 Mar 21, 2026
4012ab2
ran migrations
rhit-zhangl8 Mar 21, 2026
7be1eea
chore: added generated files, hash migration, and .gitattributes
rhit-zhangl8 Mar 21, 2026
3dcde23
feat: added new tests for multiple mod results
rhit-zhangl8 Mar 22, 2026
fa74f65
fix: new atlas sum, fixed error in test
rhit-zhangl8 Mar 22, 2026
61a2851
chore: updated mise version and re-ran lock files
rhit-zhangl8 Mar 22, 2026
02fa858
style: fix mixed whitespace
budak7273 Apr 4, 2026
8fe5fb0
refactor: simplify logic for worst mod compatibility
budak7273 Apr 4, 2026
a1768e3
test
rhit-mooretj Mar 18, 2026
b357e51
getting closer
rhit-mooretj Mar 18, 2026
1cce2e9
temp fix (not getting user packs)
rhit-mooretj Mar 19, 2026
2b6c475
user modpacks should work
rhit-mooretj Mar 19, 2026
4991b3d
everything works, my only concern is the time to run tests, but I'm g…
rhit-mooretj Mar 19, 2026
847f487
Update ratelimits.go
rhit-mooretj Mar 19, 2026
caf405e
Update user.go
rhit-mooretj Mar 19, 2026
36b67bd
Delete migrations/atlas.sum
rhit-mooretj Mar 19, 2026
47b75c0
Delete migrations/sql/20250623121418_add_modpacks.down.sql
rhit-mooretj Mar 19, 2026
6a6ac7a
Revert "Delete migrations/sql/20250623121418_add_modpacks.down.sql"
rhit-mooretj Mar 19, 2026
5149bce
Running lint to reduce errors on automations
rhit-mooretj Mar 19, 2026
9fb8a44
Fully works on my end
rhit-mooretj Mar 20, 2026
a48acc0
fix: committing from windows via cherry pick portions of cb63ec16dedd…
rhit-mooretj Mar 20, 2026
e850840
chore: regenerate hashes on windows with corrected sql file line endings
rhit-mooretj Mar 21, 2026
59a191c
chore: manually run migrate_diff (task not recognized on windows, see…
rhit-mooretj Mar 21, 2026
963f33c
fix: recreate mise.lock with new format that appeared between mise 20…
rhit-mooretj Mar 21, 2026
2329254
chore: running go mod tidy to update go.sum
rhit-mooretj Mar 21, 2026
a2b1574
chore: temporary commit to see what mise version CI is running
rhit-mooretj Mar 21, 2026
10066f3
chore: previous change for figuring out CI mise version was ineffective
rhit-mooretj Mar 21, 2026
d26ea4f
chore: final attempt at seeing mise version
rhit-mooretj Mar 21, 2026
fc496cf
fix: tried most recent mise version to see if that fixes the lock file
rhit-mooretj Mar 21, 2026
0ba7c9f
fix: running mise lock for specificly linux-x64, which is github OS
rhit-mooretj Mar 21, 2026
ef48f82
fix: running mise.lock for all platforms to meet checksum
rhit-mooretj Mar 21, 2026
29d9545
fix: running mise lock for linux-x64 and x64-musl, which seems to be …
rhit-mooretj Mar 21, 2026
6cac50c
chore: misread the diff, so trying again by adding everything I missed
rhit-mooretj Mar 21, 2026
a4918a2
fix: created mise run lock to reproduce mise.lock fix
rhit-mooretj Mar 21, 2026
bdc0ac6
fix: seeing if reverting mise.toml back to original condition fixes t…
rhit-mooretj Mar 21, 2026
2e690cb
fix: added tests to meet code coverage
rhit-mooretj Apr 2, 2026
99cda20
fix: Code coverage missed in the first pass
rhit-mooretj Apr 2, 2026
82a5a61
chore: running format to fix lint I forgot from last commit
rhit-mooretj Apr 2, 2026
4ae39b4
docs: update purpose description of `format` task
budak7273 Apr 4, 2026
7391acf
refactor: remove debug log
budak7273 Apr 4, 2026
846111f
docs: fix typo in api doc comment
budak7273 Apr 4, 2026
75093be
refactor: remove dead commented code
budak7273 Apr 4, 2026
e9d1012
refactor: fix mixed indents
budak7273 Apr 4, 2026
26ac439
test: validate GetMyModpacks Unauthorized error message
budak7273 Apr 4, 2026
d2b458d
docs: tip to windows devs about firewall prompts
budak7273 Apr 4, 2026
c87e8ae
chore: regenerate mise lock after rebase
budak7273 Apr 4, 2026
b58ccb8
chore: GetModCompatibility returns working instead of null when there…
rhit-zhangl8 Apr 4, 2026
c3ac469
fixed accidental deletion in last commit
rhit-zhangl8 Apr 4, 2026
c19759b
feat: Modified return value of GetModCompatibilities
rhit-zhangl8 Apr 4, 2026
f342714
fix: modpack compatibility tests failing
rhit-zhangl8 Apr 10, 2026
9e938d3
feat: install grid logic and test file
rhit-zhangl8 Apr 7, 2026
700f65c
feat: install grid logic and test file
rhit-zhangl8 Apr 7, 2026
a2eb155
chore: fixed lint issues
rhit-zhangl8 Apr 7, 2026
5916941
fix: fixed error with compatibility test
rhit-zhangl8 Apr 7, 2026
66dd9c2
chore: migrated hash
rhit-zhangl8 Apr 7, 2026
1183f0b
chore: tried running migrate_hash to fix CI
rhit-mooretj Apr 7, 2026
96c4cca
feat: Implemented required components to allow for modpack compatibil…
rhit-mooretj Apr 8, 2026
87b4f9f
fix: GetUser now returns the mod references for a modpack, fixing a c…
rhit-mooretj Apr 9, 2026
cd62bee
feat: moved targets to modpack release, automatically calculate targe…
rhit-zhangl8 Apr 10, 2026
5fe323b
fix: changed return type and re-named query
rhit-zhangl8 Apr 11, 2026
3439d8c
fix: Ensures releases are ordered by creation date to find latest rel…
rhit-mooretj Apr 11, 2026
f18b50b
fix: modpack logos work properly
rhit-mooretj Apr 11, 2026
1a8882e
progress: got modpacktargets to generate
rhit-mooretj Apr 12, 2026
57d3210
feat: revised process for lockfile and target calculations. all tests…
rhit-zhangl8 Apr 12, 2026
fb1e786
fix: required fixes to make modpack creation work on the frontend
rhit-mooretj Apr 13, 2026
7ad0754
feat: mise now has "populate" which gives the user mods and modpacks …
rhit-mooretj Apr 13, 2026
eafbee6
chore: running commands to fix .sum issues
rhit-mooretj Apr 13, 2026
4c6e339
chore: ran go mod tidy
rhit-mooretj Apr 13, 2026
572f5cd
fix: did not mean for the change here to be staged - ratelimits shoul…
rhit-mooretj Apr 14, 2026
345b949
feat: Adding mise task for importing staging dumps, allowing contribu…
rhit-mooretj Apr 22, 2026
0e06568
fix: import staging now pulls from the network and confirms with the …
rhit-mooretj Apr 23, 2026
38c787d
chore: fixed go.sum issue
rhit-mooretj Apr 23, 2026
be503c7
chore: `mise run migrate_diff` for `ci_migration`
rhit-mooretj Apr 28, 2026
bb67f12
chore: ran `go mod tidy`
rhit-mooretj Apr 28, 2026
617294b
chore: ran `mise lock`, but needed to be on mise version 2026.4.24 to…
rhit-mooretj Apr 28, 2026
15fb78b
fix: fixing edit modpack for modpackmods
rhit-silkaijr May 4, 2026
ffab720
fix: running format
rhit-silkaijr May 4, 2026
6153a96
fix: Edit compatibility deletes mods
rhit-mooretj May 6, 2026
a51e796
chore: ran `mise run format`
rhit-mooretj May 6, 2026
cb1fa1c
fix: added name for migration for modpack deletion
rhit-mooretj May 8, 2026
bc7c1ac
fix: Removed unneeded TODO
rhit-mooretj May 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Auto detect text files and perform LF normalization
# This keeps unix/windows machines from wanting to change every file
# from the other because of the line ending (\r vs \r\n)
* text=auto
*.sh text eol=lf
*.env* text eol=lf

# Atlas migration hashes are different if file get windows line endings
*.sql text eol=lf
1 change: 1 addition & 0 deletions .github/workflows/generated.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ jobs:

- name: Ensure no changes
run: git diff --exit-code

25 changes: 25 additions & 0 deletions .mise/tasks/import_staging_dump.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/bin/bash

CONTAINER="smr-api-postgres-1"
URL="https://storage.ficsit.app/file/smr-db-dumps/staging-dump.sql"

echo -e "WARNING: This will wipe your local database and import a fresh staging dump."
read -p "Are you sure you want to proceed? (y/N): " confirm

if [[ $confirm != [yY] && $confirm != [yY][eE][sS] ]]; then
echo "Import cancelled."
exit 0
fi

echo "---"
echo "Wiping existing data for a clean slate..."
docker exec -i $CONTAINER psql -U postgres -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"

echo "Downloading and importing staging dump from Ficsit Storage..."
# Using curl to stream the SQL directly into psql
curl -sSL "$URL" | docker exec -i $CONTAINER psql -U postgres

echo -e "Import complete! Run 'mise run api'."

echo "Press any key to exit..."
read -n 1 -s
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ mise run api
# Testing
mise run localtest
mise run coverage
mise run populate # Runs test to create mods and modpacks
mise run load_staging_dump

# Linting
mise run lint
Expand Down Expand Up @@ -151,12 +153,23 @@ See `config/config.go` for full configuration structure.
mise run test # Run tests
```

### Using Staging Dumps

`mise run load_staging_dump`
This command runs before adding the most
recently saved dump of the staging database on the network. Once the file runs, local migrations will run
when executing `mise run api`. This is useful to check if your changes will break the
data currently in the database.

Windows users, if you get annoyed by the constant firewall block prompts Go creates,
you can silence them by [temporarily adjusting your notification settings](https://serverfault.com/a/1198657/982905).

## Contributing

**Before submitting:**

```bash
mise run lint # Check code quality
mise run format # Automatically fix code quality problems and report those that need manual correction
mise run test # Run test suite
mise run generate # Regenerate if needed
```
Expand All @@ -167,4 +180,4 @@ mise run generate # Regenerate if needed
- Implement GraphQL resolvers for complex queries
- Use Temporal workflows for background processing
- Follow structured logging with `slog`
- Use context for request tracing
- Use context for request tracing
2 changes: 2 additions & 0 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ func Setup(ctx context.Context) *echo.Echo {
nodes.RegisterModRoutes(v1.Group("/mod"))
nodes.RegisterModsRoutes(v1.Group("/mods"))
nodes.RegisterVersionRoutes(v1.Group("/version"))
nodes.RegisterModpackRoutes(v1.Group("/modpack"))
nodes.RegisterModpacksRoutes(v1.Group("/modpacks"))

v2 := e.Group("/v2")

Expand Down
1 change: 1 addition & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ coverage:

ignore:
- "generated"
- "db/schema"
70 changes: 69 additions & 1 deletion conversion/ent_to_graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ type SatisfactoryVersion interface {
// goverter:output:package conv
// goverter:extend TimeToString
type User interface {
// goverter:ignore Roles Groups Mods Guides
// goverter:ignore Roles Groups Mods Guides Modpacks
Convert(source *ent.User) *generated.User
ConvertSlice(source []*ent.User) []*generated.User
}
Expand Down Expand Up @@ -82,6 +82,16 @@ type UserMod interface {
ConvertSlice(source []*ent.UserMod) []*generated.UserMod
}

// goverter:converter
// goverter:output:file ../generated/conv/user_modpack.go
// goverter:output:package conv
// goverter:extend TimeToString
type UserModpack interface {
// goverter:ignore User Modpack
Convert(source *ent.UserModpack) *generated.UserModpack
ConvertSlice(source []*ent.UserModpack) []*generated.UserModpack
}

// goverter:converter
// goverter:output:file ../generated/conv/mod.go
// goverter:output:package conv
Expand Down Expand Up @@ -127,6 +137,64 @@ type VirustotalResult interface {
ConvertSlice(source []*ent.VirustotalResult) []*generated.VirustotalResult
}

// goverter:converter
// goverter:output:file ../generated/conv/modpack.go
// goverter:output:package conv
// goverter:extend TimeToString UIntToInt Int64ToInt EntModpackReleaseToGenerated
type Modpack interface {
// goverter:map Edges.Tags Tags
// goverter:map Edges.ModpackMods Mods
// goverter:map Edges.Parent Parent
// goverter:map Edges.Releases Releases
// goverter:map Edges.Children Children
// goverter:ignore Creator Authors
Convert(source *ent.Modpack) *generated.Modpack
ConvertSlice(source []*ent.Modpack) []*generated.Modpack
}

func EntModpackReleaseToGenerated(source *ent.ModpackRelease) *generated.ModpackRelease {
if source == nil {
return nil
}
targets := make([]*generated.ModpackTarget, len(source.Edges.Targets))
for i, t := range source.Edges.Targets {
targets[i] = &generated.ModpackTarget{
ID: t.ID,
ReleaseID: source.ID,
TargetName: t.TargetName,
}
}
return &generated.ModpackRelease{
ID: source.ID,
Version: source.Version,
CreatedAt: source.CreatedAt.Format(time.RFC3339),
Lockfile: source.Lockfile,
Changelog: source.Changelog,
Targets: targets,
}
}

// goverter:converter
// goverter:output:file ../generated/conv/modpack_release.go
// goverter:output:package conv
// goverter:extend TimeToString EntModpackTargetToGenerated
type ModpackRelease interface {
// goverter:map Edges.Targets Targets
Convert(source *ent.ModpackRelease) *generated.ModpackRelease
ConvertSlice(source []*ent.ModpackRelease) []*generated.ModpackRelease
}

func EntModpackTargetToGenerated(source *ent.ModpackTarget) *generated.ModpackTarget {
if source == nil {
return nil
}
return &generated.ModpackTarget{
ID: source.ID,
ReleaseID: source.VersionID,
TargetName: source.TargetName,
}
}

func TimeToString(i time.Time) string {
return i.Format(time.RFC3339)
}
Expand Down
25 changes: 25 additions & 0 deletions dataloader/loaders.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/satisfactorymodding/smr-api/generated/ent"
"github.com/satisfactorymodding/smr-api/generated/ent/user"
"github.com/satisfactorymodding/smr-api/generated/ent/usermod"
"github.com/satisfactorymodding/smr-api/generated/ent/usermodpack"
"github.com/satisfactorymodding/smr-api/generated/ent/version"
"github.com/satisfactorymodding/smr-api/generated/ent/versiondependency"
)
Expand All @@ -22,6 +23,7 @@ type Loaders struct {
UserByID *dataloader.Loader[string, *ent.User]
VersionDependenciesByVersionID *dataloader.Loader[string, []*ent.VersionDependency]
UserModsByModID *dataloader.Loader[string, []*ent.UserMod]
UserModpacksByModpackID *dataloader.Loader[string, []*ent.UserModpack]
VersionsByModID *dataloader.Loader[string, []*ent.Version]
VersionsByModIDNoMeta *dataloader.Loader[string, []*ent.Version]
}
Expand Down Expand Up @@ -76,6 +78,29 @@ func Middleware() func(handlerFunc echo.HandlerFunc) echo.HandlerFunc {

return results
}, dataloader.WithCache[string, []*ent.UserMod](&dataloader.NoCache[string, []*ent.UserMod]{})),
UserModpacksByModpackID: dataloader.NewBatchedLoader(func(ctx context.Context, ids []string) []*dataloader.Result[[]*ent.UserModpack] {
// TODO Query only selected fields from context
entities, err := db.From(ctx).UserModpack.Query().Where(usermodpack.ModpackIDIn(ids...)).All(ctx)
if err != nil {
return nil
}

byID := map[string][]*ent.UserModpack{}
for _, entity := range entities {
byID[entity.ModpackID] = append(byID[entity.ModpackID], entity)
}

results := make([]*dataloader.Result[[]*ent.UserModpack], len(ids))
for i, id := range ids {
if u, ok := byID[id]; ok {
results[i] = &dataloader.Result[[]*ent.UserModpack]{Data: u}
} else {
results[i] = &dataloader.Result[[]*ent.UserModpack]{Error: errors.New("modpack not found")}
}
}

return results
}, dataloader.WithCache[string, []*ent.UserModpack](&dataloader.NoCache[string, []*ent.UserModpack]{})),
VersionsByModID: dataloader.NewBatchedLoader(func(ctx context.Context, ids []string) []*dataloader.Result[[]*ent.Version] {
// TODO Query only selected fields from context
entities, err := db.From(ctx).Version.Query().WithTargets().Where(
Expand Down
71 changes: 71 additions & 0 deletions db/modpack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package db

import (
"strings"

"entgo.io/ent/dialect/sql"

"github.com/satisfactorymodding/smr-api/generated"
"github.com/satisfactorymodding/smr-api/generated/ent"
"github.com/satisfactorymodding/smr-api/generated/ent/modpack"
"github.com/satisfactorymodding/smr-api/generated/ent/tag"
"github.com/satisfactorymodding/smr-api/models"
)

func ConvertModpackFilter(query *ent.ModpackQuery, filter *models.ModpackFilter, count bool) *ent.ModpackQuery {
if len(filter.IDs) > 0 {
query = query.Where(modpack.IDIn(filter.IDs...))
} else if filter != nil {
if !count {
query = query.
Limit(*filter.Limit).
Offset(*filter.Offset)
}

if filter.OrderBy != nil && *filter.OrderBy != generated.ModpackFieldsSearch {
query = query.Order(sql.OrderByField(
filter.OrderBy.String(),
OrderToOrder(filter.Order.String()),
).ToFunc())
}

if filter.Search != nil && *filter.Search != "" {
cleanSearch := strings.ReplaceAll(strings.TrimSpace(*filter.Search), " ", " & ")

query = query.Where(func(s *sql.Selector) {
join := sql.Select("id")
join = join.AppendSelectExprAs(
sql.P(func(builder *sql.Builder) {
builder.WriteString("similarity(name, ").Arg(cleanSearch).WriteString(") * 2").
WriteString(" + ").
WriteString("similarity(short_description, ").Arg(cleanSearch).WriteString(")").
WriteString(" + ").
WriteString("similarity(full_description, ").Arg(cleanSearch).WriteString(") * 0.5")
}),
"s",
)
join.From(sql.Table(modpack.Table)).As("t1")
s.Join(join).On(s.C(modpack.FieldID), join.C("id"))
})

query = query.Where(func(s *sql.Selector) {
s.Where(sql.ExprP(`"t1"."s" > 0.2`))
})

if !count && *filter.OrderBy == generated.ModpackFieldsSearch {
query = query.Order(func(s *sql.Selector) {
s.OrderExpr(sql.ExprP(`"t1"."s" DESC`))
})
}
}

if filter.Hidden == nil || !(*filter.Hidden) {
query = query.Where(modpack.Hidden(false))
}

if len(filter.TagIDs) > 0 {
query = query.Where(modpack.HasTagsWith(tag.IDIn(filter.TagIDs...)))
}
}
return query
}
3 changes: 3 additions & 0 deletions db/schema/mod.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,8 @@ func (Mod) Edges() []ent.Edge {
edge.From("dependents", Version.Type).
Ref("dependencies").
Through("version_dependencies", VersionDependency.Type),
edge.From("modpacks", Modpack.Type).
Ref("mods").
Through("modpack_mods", ModpackMod.Type),
}
}
64 changes: 64 additions & 0 deletions db/schema/modpack.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package schema

import (
"entgo.io/ent"
"entgo.io/ent/dialect/entsql"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"

"github.com/satisfactorymodding/smr-api/util"
)

type Modpack struct {
ent.Schema
}

func (Modpack) Mixin() []ent.Mixin {
return []ent.Mixin{
IDMixin{},
TimeMixin{},
SoftDeleteMixin{},
}
}

func (Modpack) Fields() []ent.Field {
return []ent.Field{
field.String("name"),
field.String("short_description").MaxLen(128),
field.String("full_description"),
field.String("logo").Optional(),
field.String("logo_thumbhash").Optional(),
field.String("creator_id"),
field.Uint("views").Default(0),
field.Uint("hotness").Default(0),
field.Uint("installs").Default(0),
field.Uint("popularity").Default(0),
field.Bool("hidden").Default(false),
field.JSON("compatibility", &util.CompatibilityInfo{}).Optional(),
field.String("parent_id").Optional().Immutable(),
}
}

func (Modpack) Edges() []ent.Edge {
return []ent.Edge{
edge.To("children", Modpack.Type).
Annotations(entsql.OnDelete(entsql.Restrict)),
edge.From("parent", Modpack.Type).
Ref("children").
Field("parent_id").
Unique().
Immutable().
Annotations(entsql.OnDelete(entsql.Restrict)),
edge.To("releases", ModpackRelease.Type).
Annotations(entsql.OnDelete(entsql.Cascade)),
edge.To("mods", Mod.Type).
Through("modpack_mods", ModpackMod.Type).
Annotations(entsql.OnDelete(entsql.Cascade)),
edge.From("authors", User.Type).
Ref("modpacks").
Through("user_modpacks", UserModpack.Type),
edge.To("tags", Tag.Type).
Through("modpack_tags", ModpackTag.Type).
Annotations(entsql.OnDelete(entsql.Cascade)),
}
}
Loading
Loading