mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
A module is a named import document, and installing one is a grow:
core.GrowCollections creates what is missing and adds the fields, select
values and indexes an existing collection lacks, in one transaction,
changing nothing that exists. A relation names its collection by name.
Installing twice changes nothing.
modules/crm carries the thirteen collections: people, companies,
opportunities, candidates, applications, lists, list_members, segments,
activities (one row per source id), notes, tasks, attachments and the
org's shared views. POST /v1/modules/{name} installs, GET /v1/modules
lists, both for a superuser or the org's admin, who can never reach a
collection only a superuser manages this way.
The _view* system collections are retired. Nothing read them, and team
views are the ordinary views collection now.
100 lines
3.2 KiB
Go
100 lines
3.2 KiB
Go
package apis
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/hanzoai/base/core"
|
|
"github.com/hanzoai/base/modules"
|
|
"github.com/hanzoai/base/tools/router"
|
|
"github.com/spf13/cast"
|
|
)
|
|
|
|
// bindModuleApi registers the module routes: which modules this Base offers,
|
|
// and installing one. Both are schema routes, a superuser's or the org admin's.
|
|
func bindModuleApi(app core.App, rg *router.RouterGroup[*core.RequestEvent]) {
|
|
sub := rg.Group("/modules").Bind(RequireSchemaAuth())
|
|
sub.GET("", modulesList)
|
|
sub.POST("/{name}", moduleInstall)
|
|
}
|
|
|
|
// moduleInfo is one module as the list route answers it.
|
|
type moduleInfo struct {
|
|
Name string `json:"name"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
Installed bool `json:"installed"`
|
|
}
|
|
|
|
func modulesList(e *core.RequestEvent) error {
|
|
list := modules.List()
|
|
out := make([]moduleInfo, len(list))
|
|
for i, m := range list {
|
|
out[i] = moduleInfo{Name: m.Name, Title: m.Title, Description: m.Description, Installed: m.Installed(e.App)}
|
|
}
|
|
return e.JSON(http.StatusOK, out)
|
|
}
|
|
|
|
// moduleInstall grows the named module into the request's Base. Installing a
|
|
// module twice changes nothing the second time; installing a newer version adds
|
|
// what it adds and keeps everything the org changed.
|
|
func moduleInstall(e *core.RequestEvent) error {
|
|
name := e.Request.PathValue("name")
|
|
|
|
m, ok := modules.Get(name)
|
|
if !ok {
|
|
return e.NotFoundError("No such module.", nil)
|
|
}
|
|
|
|
// An org admin installs into its own Base only collections it could have
|
|
// made itself, so a module can never be the way an org admin reaches a
|
|
// collection the schema routes keep for a superuser.
|
|
if !e.HasSuperuserAuth() {
|
|
for _, doc := range m.Collections {
|
|
if err := orgAdminMayGrow(e.App, doc); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
growth, err := modules.Install(e.App, name)
|
|
if err != nil {
|
|
if errors.Is(err, modules.ErrUnknownModule) {
|
|
return e.NotFoundError("No such module.", err)
|
|
}
|
|
return e.BadRequestError("Failed to install the module.", err)
|
|
}
|
|
|
|
if !e.HasSuperuserAuth() {
|
|
org, _ := e.Get(RequestEventKeyOrg).(string)
|
|
sub, _ := e.Get(RequestEventKeySub).(string)
|
|
e.App.Logger().Info("base: schema change by an org admin",
|
|
"org", org, "sub", sub, "action", "module install", "module", name,
|
|
"created", growth.Created, "grown", growth.Grown)
|
|
}
|
|
|
|
return e.JSON(http.StatusOK, struct {
|
|
Module string `json:"module"`
|
|
*core.Growth
|
|
}{name, growth})
|
|
}
|
|
|
|
// orgAdminMayGrow answers whether an org admin may grow a collection document:
|
|
// the collection it names, as it is and as the document describes it, has to be
|
|
// one the org admin manages.
|
|
func orgAdminMayGrow(app core.App, doc map[string]any) error {
|
|
name := cast.ToString(doc["name"])
|
|
|
|
described := core.NewCollection(cast.ToString(doc["type"]), name)
|
|
described.System = cast.ToBool(doc["system"])
|
|
if !SchemaManageable(described) {
|
|
return router.NewForbiddenError("The module describes a collection only a superuser manages: "+name+".", nil)
|
|
}
|
|
|
|
if existing, err := app.FindCachedCollectionByNameOrId(name); err == nil && !SchemaManageable(existing) {
|
|
return router.NewForbiddenError("The module grows a collection only a superuser manages: "+name+".", nil)
|
|
}
|
|
|
|
return nil
|
|
}
|